public inbox for [email protected]  
help / color / mirror / Atom feed
From: Tatsuo Ishii <[email protected]>
To: [email protected]
Cc: [email protected]
Cc: [email protected]
Cc: [email protected]
Cc: [email protected]
Cc: [email protected]
Cc: [email protected]
Cc: [email protected]
Cc: [email protected]
Cc: [email protected]
Subject: Re: Row pattern recognition
Date: Sat, 02 May 2026 14:03:04 +0900 (JST)
Message-ID: <[email protected]> (raw)
In-Reply-To: <CAAAe_zBdwAUDNs_WFdLkFF=ewhkDkv-AqizVEVzhsfremGFb4w@mail.gmail.com>
References: <CAAAe_zA3vXEPkC7=fapx0VCE5F2uSgRjKjur67Yfd+JxtWPCuQ@mail.gmail.com>
	<[email protected]>
	<CAAAe_zBdwAUDNs_WFdLkFF=ewhkDkv-AqizVEVzhsfremGFb4w@mail.gmail.com>

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,
--
Tatsuo Ishii
SRA OSS K.K.
English: http://www.sraoss.co.jp/index_en/
Japanese:http://www.sraoss.co.jp


Attachments:

  [application/octet-stream] v47-0001-Row-pattern-recognition-patch-for-raw-parser.patch (32.0K, 2-v47-0001-Row-pattern-recognition-patch-for-raw-parser.patch)
  download | inline diff:
From 43ac6a3d5ac01357e372a86172b1afce7cfdd72a Mon Sep 17 00:00:00 2001
From: Tatsuo Ishii <[email protected]>
Date: Sat, 2 May 2026 13:40:29 +0900
Subject: [PATCH v47 1/9] Row pattern recognition patch for raw parser.

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 navigations (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)
  Anchores (^, $) are not permitted with RPR in Window clause by the
  standard.
- PERMUTE
- CLASSIFIER

Author: Tatsuo Ishii <[email protected]>
Author: Henson Choi <[email protected]>
Reviewed-by: Vik Fearing <[email protected]>
Reviewed-by: Jacob Champion <[email protected]>
Reviewed-by: Peter Eisentraut <[email protected]>
Reviewed-by: NINGWEI CHEN <[email protected]>
Reviewed-by: "David G. Johnston" <[email protected]>
Reviewed-by: Chao Li <[email protected]>
Reviewed-by: SungJun Jang <[email protected]>
Reviewed-by: Zsolt Parragi <[email protected]>

Discussion: https://postgr.es/m/20230625.210509.1276733411677577841.t-ishii%40sranhm.sra.co.jp

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 implementaion 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 (previouly was in ExecRPR.c)
---
 src/backend/parser/gram.y       | 425 ++++++++++++++++++++++++++++++--
 src/include/nodes/parsenodes.h  |  83 +++++++
 src/include/parser/kwlist.h     |   5 +
 src/include/parser/parse_node.h |   2 +
 4 files changed, 499 insertions(+), 16 deletions(-)

diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index ff4e1388c55..f3cedfbbb18 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 *makeRPRQuantifier(int min, int max, ParseLoc reluctant, int location,
+									   core_yyscan_t yyscanner);
 
 %}
 
@@ -718,6 +720,15 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 				label_term
 %type <str>		opt_colid
 
+%type <target>	row_pattern_definition
+%type <node>	opt_row_pattern_common_syntax
+				row_pattern row_pattern_alt row_pattern_seq
+				row_pattern_term row_pattern_primary
+				row_pattern_quantifier_opt
+%type <list>	row_pattern_definition_list
+%type <ival>	opt_row_pattern_skip_to
+%type <boolean>	opt_row_pattern_initial_or_seek
+
 /*
  * Non-keyword token types.  These are hard-wired into the "flex" lexer.
  * They must be listed first so that their numeric codes do not depend on
@@ -760,7 +771,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	CURRENT_TIME CURRENT_TIMESTAMP CURRENT_USER CURSOR CYCLE
 
 	DATA_P DATABASE DAY_P DEALLOCATE DEC DECIMAL_P DECLARE DEFAULT DEFAULTS
-	DEFERRABLE DEFERRED DEFINER DELETE_P DELIMITER DELIMITERS DEPENDS DEPTH DESC DESTINATION
+	DEFERRABLE DEFERRED DEFINE DEFINER DELETE_P DELIMITER DELIMITERS DEPENDS DEPTH DESC DESTINATION
 	DETACH DICTIONARY DISABLE_P DISCARD DISTINCT DO DOCUMENT_P DOMAIN_P
 	DOUBLE_P DROP
 
@@ -776,7 +787,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	HANDLER HAVING HEADER_P HOLD HOUR_P
 
 	IDENTITY_P IF_P IGNORE_P ILIKE IMMEDIATE IMMUTABLE IMPLICIT_P IMPORT_P IN_P INCLUDE
-	INCLUDING INCREMENT INDENT INDEX INDEXES INHERIT INHERITS INITIALLY INLINE_P
+	INCLUDING INCREMENT INDENT INDEX INDEXES INHERIT INHERITS INITIAL INITIALLY INLINE_P
 	INNER_P INOUT INPUT_P INSENSITIVE INSERT INSTEAD INT_P INTEGER
 	INTERSECT INTERVAL INTO INVOKER IS ISNULL ISOLATION
 
@@ -801,8 +812,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	ORDER ORDINALITY OTHERS OUT_P OUTER_P
 	OVER OVERLAPS OVERLAY OVERRIDING OWNED OWNER
 
-	PARALLEL PARAMETER PARSER PARTIAL PARTITION PARTITIONS PASSING PASSWORD PATH
-	PERIOD PLACING PLAN PLANS POLICY PORTION
+	PARALLEL PARAMETER PARSER PARTIAL PARTITION PARTITIONS PASSING PASSWORD PAST PATH
+	PATTERN_P PERIOD PLACING PLAN PLANS POLICY PORTION
 	POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY
 	PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROCEDURES PROGRAM PROPERTIES PROPERTY PUBLICATION
 
@@ -813,7 +824,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	RESET RESPECT_P RESTART RESTRICT RETURN RETURNING RETURNS REVOKE RIGHT ROLE ROLLBACK ROLLUP
 	ROUTINE ROUTINES ROW ROWS RULE
 
-	SAVEPOINT SCALAR SCHEMA SCHEMAS SCROLL SEARCH SECOND_P SECURITY SELECT
+	SAVEPOINT SCALAR SCHEMA SCHEMAS SCROLL SEARCH SECOND_P SECURITY SEEK SELECT
 	SEQUENCE SEQUENCES
 	SERIALIZABLE SERVER SESSION SESSION_USER SET SETS SETOF SHARE SHOW
 	SIMILAR SIMPLE SKIP SMALLINT SNAPSHOT SOME SPLIT SOURCE SQL_P STABLE STANDALONE_P
@@ -896,8 +907,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
  * reference point for a precedence level that we can assign to other
  * keywords that lack a natural precedence level.
  *
- * We need to do this for PARTITION, RANGE, ROWS, and GROUPS to support
- * opt_existing_window_name (see comment there).
+ * We need to do this for PARTITION, RANGE, ROWS, GROUPS, AFTER, INITIAL,
+ * SEEK, PATTERN_P to support opt_existing_window_name (see comment there).
  *
  * The frame_bound productions UNBOUNDED PRECEDING and UNBOUNDED FOLLOWING
  * are even messier: since UNBOUNDED is an unreserved keyword (per spec!),
@@ -930,6 +941,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %nonassoc	UNBOUNDED NESTED /* ideally would have same precedence as IDENT */
 %nonassoc	IDENT PARTITION RANGE ROWS GROUPS PRECEDING FOLLOWING CUBE ROLLUP
 			SET KEYS OBJECT_P SCALAR TO USING VALUE_P WITH WITHOUT PATH
+			AFTER INITIAL SEEK PATTERN_P
 %left		Op OPERATOR RIGHT_ARROW '|'	/* multi-character ops and user-defined operators */
 %left		'+' '-'
 %left		'*' '/' '%'
@@ -17335,6 +17347,8 @@ over_clause: OVER window_specification
 					n->startOffset = NULL;
 					n->endOffset = NULL;
 					n->location = @2;
+					n->frameLocation = -1;
+					n->excludeLocation = -1;
 					$$ = n;
 				}
 			| /*EMPTY*/
@@ -17342,7 +17356,8 @@ over_clause: OVER window_specification
 		;
 
 window_specification: '(' opt_existing_window_name opt_partition_clause
-						opt_sort_clause opt_frame_clause ')'
+						opt_sort_clause opt_frame_clause
+						opt_row_pattern_common_syntax ')'
 				{
 					WindowDef  *n = makeNode(WindowDef);
 
@@ -17354,20 +17369,23 @@ window_specification: '(' opt_existing_window_name opt_partition_clause
 					n->frameOptions = $5->frameOptions;
 					n->startOffset = $5->startOffset;
 					n->endOffset = $5->endOffset;
+					n->frameLocation = $5->frameLocation;
+					n->excludeLocation = $5->excludeLocation;
+					n->rpCommonSyntax = (RPCommonSyntax *)$6;
 					n->location = @1;
 					$$ = n;
 				}
 		;
 
 /*
- * If we see PARTITION, RANGE, ROWS or GROUPS as the first token after the '('
- * of a window_specification, we want the assumption to be that there is
- * no existing_window_name; but those keywords are unreserved and so could
- * be ColIds.  We fix this by making them have the same precedence as IDENT
- * and giving the empty production here a slightly higher precedence, so
- * that the shift/reduce conflict is resolved in favor of reducing the rule.
- * These keywords are thus precluded from being an existing_window_name but
- * are not reserved for any other purpose.
+ * If we see PARTITION, RANGE, ROWS, GROUPS, AFTER, INITIAL, SEEK or PATTERN_P
+ * as the first token after the '(' of a window_specification, we want the
+ * assumption to be that there is no existing_window_name; but those keywords
+ * are unreserved and so could be ColIds.  We fix this by making them have the
+ * same precedence as IDENT and giving the empty production here a slightly
+ * higher precedence, so that the shift/reduce conflict is resolved in favor
+ * of reducing the rule.  These keywords are thus precluded from being an
+ * existing_window_name but are not reserved for any other purpose.
  */
 opt_existing_window_name: ColId						{ $$ = $1; }
 			| /*EMPTY*/				%prec Op		{ $$ = NULL; }
@@ -17388,6 +17406,9 @@ 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;
 				}
 			| ROWS frame_extent opt_window_exclusion_clause
@@ -17396,6 +17417,9 @@ 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;
 				}
 			| GROUPS frame_extent opt_window_exclusion_clause
@@ -17404,6 +17428,9 @@ 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;
 				}
 			| /*EMPTY*/
@@ -17413,6 +17440,8 @@ opt_frame_clause:
 					n->frameOptions = FRAMEOPTION_DEFAULTS;
 					n->startOffset = NULL;
 					n->endOffset = NULL;
+					n->frameLocation = -1;
+					n->excludeLocation = -1;
 					$$ = n;
 				}
 		;
@@ -17488,6 +17517,8 @@ frame_bound:
 					n->frameOptions = FRAMEOPTION_START_UNBOUNDED_PRECEDING;
 					n->startOffset = NULL;
 					n->endOffset = NULL;
+					n->frameLocation = -1;
+					n->excludeLocation = -1;
 					$$ = n;
 				}
 			| UNBOUNDED FOLLOWING
@@ -17497,6 +17528,8 @@ frame_bound:
 					n->frameOptions = FRAMEOPTION_START_UNBOUNDED_FOLLOWING;
 					n->startOffset = NULL;
 					n->endOffset = NULL;
+					n->frameLocation = -1;
+					n->excludeLocation = -1;
 					$$ = n;
 				}
 			| CURRENT_P ROW
@@ -17506,6 +17539,8 @@ frame_bound:
 					n->frameOptions = FRAMEOPTION_START_CURRENT_ROW;
 					n->startOffset = NULL;
 					n->endOffset = NULL;
+					n->frameLocation = -1;
+					n->excludeLocation = -1;
 					$$ = n;
 				}
 			| a_expr PRECEDING
@@ -17515,6 +17550,8 @@ frame_bound:
 					n->frameOptions = FRAMEOPTION_START_OFFSET_PRECEDING;
 					n->startOffset = $1;
 					n->endOffset = NULL;
+					n->frameLocation = -1;
+					n->excludeLocation = -1;
 					$$ = n;
 				}
 			| a_expr FOLLOWING
@@ -17524,6 +17561,8 @@ frame_bound:
 					n->frameOptions = FRAMEOPTION_START_OFFSET_FOLLOWING;
 					n->startOffset = $1;
 					n->endOffset = NULL;
+					n->frameLocation = -1;
+					n->excludeLocation = -1;
 					$$ = n;
 				}
 		;
@@ -17536,6 +17575,332 @@ opt_window_exclusion_clause:
 			| /*EMPTY*/				{ $$ = 0; }
 		;
 
+opt_row_pattern_common_syntax:
+opt_row_pattern_skip_to opt_row_pattern_initial_or_seek
+				PATTERN_P '(' row_pattern ')'
+				DEFINE row_pattern_definition_list
+			{
+				RPCommonSyntax *n = makeNode(RPCommonSyntax);
+				n->rpSkipTo = $1;
+				n->initial = $2;
+				n->rpPattern = (RPRPatternNode *) $5;
+				n->rpDefs = $8;
+				$$ = (Node *) n;
+			}
+			| /*EMPTY*/		{ $$ = NULL; }
+		;
+
+opt_row_pattern_skip_to:
+			AFTER MATCH SKIP TO NEXT ROW
+				{
+					$$ = ST_NEXT_ROW;
+				}
+			| AFTER MATCH SKIP PAST LAST_P ROW
+				{
+					$$ = ST_PAST_LAST_ROW;
+				}
+			| /*EMPTY*/
+				{
+					$$ = ST_PAST_LAST_ROW;
+				}
+		;
+
+opt_row_pattern_initial_or_seek:
+			INITIAL		{ $$ = true; }
+			| SEEK
+				{
+					ereport(ERROR,
+							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+							 errmsg("SEEK is not supported"),
+							 errhint("Use INITIAL instead."),
+							 parser_errposition(@1)));
+				}
+			| /*EMPTY*/		{ $$ = true; }
+		;
+
+row_pattern:
+			row_pattern_alt						{ $$ = $1; }
+		;
+
+row_pattern_alt:
+			row_pattern_seq						{ $$ = $1; }
+			| row_pattern_alt '|' row_pattern_seq
+				{
+					RPRPatternNode *n;
+					/* 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);
+						$$ = (Node *) n;
+					}
+					else
+					{
+						n = makeNode(RPRPatternNode);
+						n->nodeType = RPR_PATTERN_ALT;
+						n->children = list_make2($1, $3);
+						n->min = 1;
+						n->max = 1;
+						n->reluctant = false;
+						n->reluctant_location = -1;
+						n->location = @1;
+						$$ = (Node *) n;
+					}
+				}
+		;
+
+row_pattern_seq:
+			row_pattern_term					{ $$ = $1; }
+			| row_pattern_seq row_pattern_term
+				{
+					RPRPatternNode *n;
+					/* If left side is already SEQ, append to it */
+					if (IsA($1, RPRPatternNode) &&
+						((RPRPatternNode *) $1)->nodeType == RPR_PATTERN_SEQ)
+					{
+						n = (RPRPatternNode *) $1;
+						n->children = lappend(n->children, $2);
+						$$ = (Node *) n;
+					}
+					else
+					{
+						n = makeNode(RPRPatternNode);
+						n->nodeType = RPR_PATTERN_SEQ;
+						n->children = list_make2($1, $2);
+						n->min = 1;
+						n->max = 1;
+						n->reluctant = false;
+						n->reluctant_location = -1;
+						n->location = @1;
+						$$ = (Node *) n;
+					}
+				}
+		;
+
+row_pattern_term:
+			row_pattern_primary row_pattern_quantifier_opt
+				{
+					RPRPatternNode *n = (RPRPatternNode *) $1;
+					RPRPatternNode *q = (RPRPatternNode *) $2;
+
+					n->min = q->min;
+					n->max = q->max;
+					n->reluctant = q->reluctant;
+					n->reluctant_location = q->reluctant_location;
+					$$ = (Node *) n;
+				}
+		;
+
+row_pattern_primary:
+			ColId
+				{
+					RPRPatternNode *n = makeNode(RPRPatternNode);
+					n->nodeType = RPR_PATTERN_VAR;
+					n->varName = $1;
+					n->min = 1;
+					n->max = 1;
+					n->reluctant = false;
+					n->reluctant_location = -1;
+					n->children = NIL;
+					n->location = @1;
+					$$ = (Node *) n;
+				}
+			| '(' row_pattern ')'
+				{
+					RPRPatternNode *inner = (RPRPatternNode *) $2;
+					RPRPatternNode *n = makeNode(RPRPatternNode);
+					n->nodeType = RPR_PATTERN_GROUP;
+					n->children = list_make1(inner);
+					n->min = 1;
+					n->max = 1;
+					n->reluctant = false;
+					n->reluctant_location = -1;
+					n->location = @1;
+					$$ = (Node *) n;
+				}
+		;
+
+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); }
+			| Op
+				{
+					/* Handle single Op: ? or reluctant quantifiers *?, +?, ?? */
+					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);
+					else if (strcmp($1, "+?") == 0)
+						$$ = (Node *) makeRPRQuantifier(1, INT_MAX, @1 + 1, @1, yyscanner);
+					else if (strcmp($1, "??") == 0)
+						$$ = (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)));
+				}
+			/* 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)));
+					$$ = (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)));
+					$$ = (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)));
+					$$ = (Node *) makeRPRQuantifier(0, 1, @2, @1, yyscanner);
+				}
+			/* {n}, {n,}, {,m}, {n,m} quantifiers */
+			| '{' Iconst '}'
+				{
+					if ($2 <= 0 || $2 >= INT_MAX)
+						ereport(ERROR,
+								errcode(ERRCODE_SYNTAX_ERROR),
+								errmsg("quantifier bound must be between 1 and %d", INT_MAX - 1),
+								parser_errposition(@2));
+					$$ = (Node *) makeRPRQuantifier($2, $2, -1, @1, yyscanner);
+				}
+			| '{' Iconst ',' '}'
+				{
+					if ($2 < 0 || $2 >= INT_MAX)
+						ereport(ERROR,
+								errcode(ERRCODE_SYNTAX_ERROR),
+								errmsg("quantifier bound must be between 0 and %d", INT_MAX - 1),
+								parser_errposition(@2));
+					$$ = (Node *) makeRPRQuantifier($2, INT_MAX, -1, @1, yyscanner);
+				}
+			| '{' ',' Iconst '}'
+				{
+					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, -1, @1, yyscanner);
+				}
+			| '{' Iconst ',' Iconst '}'
+				{
+					if ($2 < 0 || $4 <= 0 || $2 >= INT_MAX || $4 >= INT_MAX)
+						ereport(ERROR,
+								errcode(ERRCODE_SYNTAX_ERROR),
+								errmsg("quantifier bounds must be between 0 and %d with max >= 1", INT_MAX - 1),
+								parser_errposition(@2));
+					if ($2 > $4)
+						ereport(ERROR,
+								errcode(ERRCODE_SYNTAX_ERROR),
+								errmsg("quantifier minimum bound must not exceed maximum"),
+								parser_errposition(@2));
+					$$ = (Node *) makeRPRQuantifier($2, $4, -1, @1, yyscanner);
+				}
+			/* Reluctant versions: {n}?, {n,}?, {,m}?, {n,m}? */
+			| '{' Iconst '}' Op
+				{
+					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)));
+					if ($2 <= 0 || $2 >= INT_MAX)
+						ereport(ERROR,
+								errcode(ERRCODE_SYNTAX_ERROR),
+								errmsg("quantifier bound must be between 1 and %d", INT_MAX - 1),
+								parser_errposition(@2));
+					$$ = (Node *) makeRPRQuantifier($2, $2, @4, @1, yyscanner);
+				}
+			| '{' 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 ($2 < 0 || $2 >= INT_MAX)
+						ereport(ERROR,
+								errcode(ERRCODE_SYNTAX_ERROR),
+								errmsg("quantifier bound must be between 0 and %d", INT_MAX - 1),
+								parser_errposition(@2));
+					$$ = (Node *) makeRPRQuantifier($2, INT_MAX, @5, @1, yyscanner);
+				}
+			| '{' ',' 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);
+				}
+			| '{' Iconst ',' Iconst '}' Op
+				{
+					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)));
+					if ($2 < 0 || $4 <= 0 || $2 >= INT_MAX || $4 >= INT_MAX)
+						ereport(ERROR,
+								errcode(ERRCODE_SYNTAX_ERROR),
+								errmsg("quantifier bounds must be between 0 and %d with max >= 1", INT_MAX - 1),
+								parser_errposition(@2));
+					if ($2 > $4)
+						ereport(ERROR,
+								errcode(ERRCODE_SYNTAX_ERROR),
+								errmsg("quantifier minimum bound must not exceed maximum"),
+								parser_errposition(@2));
+					$$ = (Node *) makeRPRQuantifier($2, $4, @6, @1, yyscanner);
+				}
+		;
+
+row_pattern_definition_list:
+			row_pattern_definition										{ $$ = list_make1($1); }
+			| row_pattern_definition_list ',' row_pattern_definition	{ $$ = lappend($1, $3); }
+		;
+
+row_pattern_definition:
+			ColId AS a_expr
+				{
+					$$ = makeNode(ResTarget);
+					$$->name = $1;
+					$$->indirection = NIL;
+					$$->val = (Node *) $3;
+					$$->location = @1;
+				}
+		;
 
 /*
  * Supporting nonterminals for expressions.
@@ -18885,6 +19250,7 @@ unreserved_keyword:
 			| DECLARE
 			| DEFAULTS
 			| DEFERRED
+			| DEFINE
 			| DEFINER
 			| DELETE_P
 			| DELIMITER
@@ -18953,6 +19319,7 @@ unreserved_keyword:
 			| INDEXES
 			| INHERIT
 			| INHERITS
+			| INITIAL
 			| INLINE_P
 			| INPUT_P
 			| INSENSITIVE
@@ -19029,7 +19396,9 @@ unreserved_keyword:
 			| PARTITIONS
 			| PASSING
 			| PASSWORD
+			| PAST
 			| PATH
+			| PATTERN_P
 			| PERIOD
 			| PLAN
 			| PLANS
@@ -19088,6 +19457,7 @@ unreserved_keyword:
 			| SEARCH
 			| SECOND_P
 			| SECURITY
+			| SEEK
 			| SEQUENCE
 			| SEQUENCES
 			| SERIALIZABLE
@@ -19477,6 +19847,7 @@ bare_label_keyword:
 			| DEFAULTS
 			| DEFERRABLE
 			| DEFERRED
+			| DEFINE
 			| DEFINER
 			| DELETE_P
 			| DELIMITER
@@ -19559,6 +19930,7 @@ bare_label_keyword:
 			| INDEXES
 			| INHERIT
 			| INHERITS
+			| INITIAL
 			| INITIALLY
 			| INLINE_P
 			| INNER_P
@@ -19673,7 +20045,9 @@ bare_label_keyword:
 			| PARTITIONS
 			| PASSING
 			| PASSWORD
+			| PAST
 			| PATH
+			| PATTERN_P
 			| PERIOD
 			| PLACING
 			| PLAN
@@ -19737,6 +20111,7 @@ bare_label_keyword:
 			| SCROLL
 			| SEARCH
 			| SECURITY
+			| SEEK
 			| SELECT
 			| SEQUENCE
 			| SEQUENCES
@@ -20931,6 +21306,24 @@ makeRecursiveViewSelect(char *relname, List *aliases, Node *query)
 	return (Node *) s;
 }
 
+/*
+ * makeRPRQuantifier
+ *		Create an RPRPatternNode with specified quantifier bounds.
+ */
+static RPRPatternNode *
+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_location >= 0);
+	n->reluctant_location = reluctant_location;
+	n->location = location;
+	return n;
+}
+
 /* parser_init()
  * Initialize to parse one query string
  */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 91377a6cde3..455db2cec61 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -581,6 +581,73 @@ typedef struct SortBy
 	ParseLoc	location;		/* operator location, or -1 if none/unknown */
 } SortBy;
 
+/*
+ * AFTER MATCH row pattern skip to types in row pattern common syntax
+ */
+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 */
+} 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
+ */
+typedef enum RPRPatternNodeType
+{
+	RPR_PATTERN_VAR,			/* variable reference */
+	RPR_PATTERN_SEQ,			/* sequence (concatenation) */
+	RPR_PATTERN_ALT,			/* alternation (|) */
+	RPR_PATTERN_GROUP			/* group (parentheses) */
+} RPRPatternNodeType;
+
+/*
+ * RPRPatternNode - Row Pattern Recognition pattern AST node
+ */
+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 *, +) */
+	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 */
+} RPRPatternNode;
+
+/*
+ * RowPatternCommonSyntax - raw representation of row pattern common syntax
+ */
+typedef struct RPCommonSyntax
+{
+	NodeTag		type;
+	RPSkipTo	rpSkipTo;		/* Row Pattern AFTER MATCH SKIP type */
+	bool		initial;		/* true if <row pattern initial or seek> is
+								 * initial */
+	RPRPatternNode *rpPattern;	/* PATTERN clause AST */
+	List	   *rpDefs;			/* row pattern definitions clause (list of
+								 * ResTarget) */
+} RPCommonSyntax;
+
 /*
  * WindowDef - raw representation of WINDOW and OVER clauses
  *
@@ -596,10 +663,13 @@ typedef struct WindowDef
 	char	   *refname;		/* referenced window name, if any */
 	List	   *partitionClause;	/* PARTITION BY expression list */
 	List	   *orderClause;	/* ORDER BY (list of SortBy) */
+	RPCommonSyntax *rpCommonSyntax; /* row pattern common syntax */
 	int			frameOptions;	/* frame_clause options, see below */
 	Node	   *startOffset;	/* expression for starting bound, if any */
 	Node	   *endOffset;		/* expression for ending bound, if any */
 	ParseLoc	location;		/* parse location, or -1 if none/unknown */
+	ParseLoc	frameLocation;	/* ROWS/RANGE/GROUPS location, or -1 */
+	ParseLoc	excludeLocation;	/* EXCLUDE location, or -1 */
 } WindowDef;
 
 /*
@@ -1648,6 +1718,11 @@ typedef struct GroupingSet
  * the orderClause might or might not be copied (see copiedOrder); the framing
  * options are never copied, per spec.
  *
+ * "defineClause" is Row Pattern Recognition DEFINE clause (list of
+ * TargetEntry). TargetEntry.resname represents row pattern definition
+ * variable name. "rpPattern" represents PATTERN clause as an AST tree
+ * (RPRPatternNode).
+ *
  * The information relevant for the query jumbling is the partition clause
  * type and its bounds.
  */
@@ -1677,6 +1752,14 @@ typedef struct WindowClause
 	Index		winref;			/* ID referenced by window functions */
 	/* did we copy orderClause from refname? */
 	bool		copiedOrder pg_node_attr(query_jumble_ignore);
+	/* Row Pattern AFTER MATCH SKIP clause */
+	RPSkipTo	rpSkipTo;		/* Row Pattern Skip To type */
+	bool		initial;		/* true if <row pattern initial or seek> is
+								 * initial */
+	/* Row Pattern DEFINE clause (list of TargetEntry) */
+	List	   *defineClause;
+	/* Row Pattern PATTERN clause AST */
+	RPRPatternNode *rpPattern;
 } WindowClause;
 
 /*
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index 51ead54f015..3894fad9023 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -129,6 +129,7 @@ PG_KEYWORD("default", DEFAULT, RESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("defaults", DEFAULTS, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("deferrable", DEFERRABLE, RESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("deferred", DEFERRED, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("define", DEFINE, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("definer", DEFINER, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("delete", DELETE_P, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("delimiter", DELIMITER, UNRESERVED_KEYWORD, BARE_LABEL)
@@ -221,6 +222,7 @@ PG_KEYWORD("index", INDEX, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("indexes", INDEXES, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("inherit", INHERIT, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("inherits", INHERITS, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("initial", INITIAL, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("initially", INITIALLY, RESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("inline", INLINE_P, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("inner", INNER_P, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL)
@@ -347,7 +349,9 @@ PG_KEYWORD("partition", PARTITION, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("partitions", PARTITIONS, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("passing", PASSING, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("password", PASSWORD, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("past", PAST, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("path", PATH, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("pattern", PATTERN_P, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("period", PERIOD, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("placing", PLACING, RESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("plan", PLAN, UNRESERVED_KEYWORD, BARE_LABEL)
@@ -415,6 +419,7 @@ PG_KEYWORD("scroll", SCROLL, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("search", SEARCH, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("second", SECOND_P, UNRESERVED_KEYWORD, AS_LABEL)
 PG_KEYWORD("security", SECURITY, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("seek", SEEK, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("select", SELECT, RESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("sequence", SEQUENCE, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("sequences", SEQUENCES, UNRESERVED_KEYWORD, BARE_LABEL)
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
index f7f4ba6c2a8..cb9d02c00c4 100644
--- a/src/include/parser/parse_node.h
+++ b/src/include/parser/parse_node.h
@@ -51,6 +51,7 @@ typedef enum ParseExprKind
 	EXPR_KIND_WINDOW_FRAME_RANGE,	/* window frame clause with RANGE */
 	EXPR_KIND_WINDOW_FRAME_ROWS,	/* window frame clause with ROWS */
 	EXPR_KIND_WINDOW_FRAME_GROUPS,	/* window frame clause with GROUPS */
+	EXPR_KIND_RPR_DEFINE,		/* DEFINE */
 	EXPR_KIND_SELECT_TARGET,	/* SELECT target list item */
 	EXPR_KIND_INSERT_TARGET,	/* INSERT target list item */
 	EXPR_KIND_UPDATE_SOURCE,	/* UPDATE assignment source item */
@@ -230,6 +231,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 */
-- 
2.43.0



  [application/octet-stream] v47-0002-Row-pattern-recognition-patch-parse-analysis.patch (42.5K, 3-v47-0002-Row-pattern-recognition-patch-parse-analysis.patch)
  download | inline diff:
From 73ccc0a14771ca64110aeea5303d134d94fadb42 Mon Sep 17 00:00:00 2001
From: Tatsuo Ishii <[email protected]>
Date: Sat, 2 May 2026 13:40:29 +0900
Subject: [PATCH v47 2/9] Row pattern recognition patch (parse/analysis).

---
 src/backend/nodes/copyfuncs.c     |  27 ++
 src/backend/nodes/equalfuncs.c    |  35 ++
 src/backend/nodes/outfuncs.c      |  51 +++
 src/backend/nodes/readfuncs.c     |  85 +++++
 src/backend/parser/Makefile       |   1 +
 src/backend/parser/README         |   1 +
 src/backend/parser/meson.build    |   1 +
 src/backend/parser/parse_agg.c    |   9 +-
 src/backend/parser/parse_clause.c |  12 +-
 src/backend/parser/parse_expr.c   |  42 +++
 src/backend/parser/parse_func.c   |  86 ++++-
 src/backend/parser/parse_rpr.c    | 594 ++++++++++++++++++++++++++++++
 src/include/nodes/primnodes.h     |  54 +++
 src/include/parser/parse_clause.h |   3 +
 src/include/parser/parse_rpr.h    |  22 ++
 15 files changed, 1017 insertions(+), 6 deletions(-)
 create mode 100644 src/backend/parser/parse_rpr.c
 create mode 100644 src/include/parser/parse_rpr.h

diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index ff22a04abe5..e67ad39bdb8 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -16,6 +16,7 @@
 #include "postgres.h"
 
 #include "miscadmin.h"
+#include "nodes/plannodes.h"
 #include "utils/datum.h"
 
 
@@ -166,6 +167,32 @@ _copyBitmapset(const Bitmapset *from)
 	return bms_copy(from);
 }
 
+static RPRPattern *
+_copyRPRPattern(const RPRPattern *from)
+{
+	RPRPattern *newnode = makeNode(RPRPattern);
+
+	COPY_SCALAR_FIELD(numVars);
+	COPY_SCALAR_FIELD(maxDepth);
+	COPY_SCALAR_FIELD(numElements);
+
+	/* Deep copy the varNames array (DEFINE clause is required) */
+	Assert(from->numVars > 0);
+	newnode->varNames = palloc0(from->numVars * sizeof(char *));
+	for (int i = 0; i < from->numVars; i++)
+		newnode->varNames[i] = pstrdup(from->varNames[i]);
+
+	/* Deep copy the elements array (always has at least one element + FIN) */
+	Assert(from->numElements >= 2);
+	newnode->elements = palloc(from->numElements * sizeof(RPRPatternElement));
+	memcpy(newnode->elements, from->elements,
+		   from->numElements * sizeof(RPRPatternElement));
+
+	COPY_SCALAR_FIELD(isAbsorbable);
+
+	return newnode;
+}
+
 
 /*
  * copyObjectImpl -- implementation of copyObject(); see nodes/nodes.h
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 3d1a1adf86e..328199918b8 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -20,6 +20,7 @@
 #include "postgres.h"
 
 #include "miscadmin.h"
+#include "nodes/plannodes.h"
 #include "utils/datum.h"
 
 
@@ -149,6 +150,40 @@ _equalBitmapset(const Bitmapset *a, const Bitmapset *b)
 	return bms_equal(a, b);
 }
 
+static bool
+_equalRPRPattern(const RPRPattern *a, const RPRPattern *b)
+{
+	COMPARE_SCALAR_FIELD(numVars);
+	COMPARE_SCALAR_FIELD(maxDepth);
+	COMPARE_SCALAR_FIELD(numElements);
+
+	/* Compare varNames array */
+	if (a->numVars > 0)
+	{
+		if (a->varNames == NULL || b->varNames == NULL)
+			return false;
+		for (int i = 0; i < a->numVars; i++)
+		{
+			if (strcmp(a->varNames[i], b->varNames[i]) != 0)
+				return false;
+		}
+	}
+
+	/* Compare elements array */
+	if (a->numElements > 0)
+	{
+		if (a->elements == NULL || b->elements == NULL)
+			return false;
+		if (memcmp(a->elements, b->elements,
+				   a->numElements * sizeof(RPRPatternElement)) != 0)
+			return false;
+	}
+
+	COMPARE_SCALAR_FIELD(isAbsorbable);
+
+	return true;
+}
+
 /*
  * Lists are handled specially
  */
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 953c5797c5d..e6ea9ce22d9 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -23,6 +23,7 @@
 #include "nodes/bitmapset.h"
 #include "nodes/nodes.h"
 #include "nodes/pg_list.h"
+#include "nodes/plannodes.h"
 #include "utils/datum.h"
 
 /* State flag that determines how nodeToStringInternal() should treat location fields */
@@ -727,6 +728,56 @@ _outA_Const(StringInfo str, const A_Const *node)
 	WRITE_LOCATION_FIELD(location);
 }
 
+static void
+_outRPRPattern(StringInfo str, const RPRPattern *node)
+{
+	WRITE_NODE_TYPE("RPRPATTERN");
+
+	WRITE_INT_FIELD(numVars);
+	WRITE_INT_FIELD(maxDepth);
+	WRITE_INT_FIELD(numElements);
+
+	/* Write varNames array as list of strings */
+	appendStringInfoString(str, " :varNames");
+	if (node->numVars > 0 && node->varNames != NULL)
+	{
+		appendStringInfoString(str, " (");
+		for (int i = 0; i < node->numVars; i++)
+		{
+			if (i > 0)
+				appendStringInfoChar(str, ' ');
+			outToken(str, node->varNames[i]);
+		}
+		appendStringInfoChar(str, ')');
+	}
+	else
+		appendStringInfoString(str, " <>");
+
+	/* Write elements array */
+	appendStringInfoString(str, " :elements");
+	if (node->numElements > 0 && node->elements != NULL)
+	{
+		appendStringInfoChar(str, ' ');
+		for (int i = 0; i < node->numElements; i++)
+		{
+			const RPRPatternElement *elem = &node->elements[i];
+
+			appendStringInfo(str, "(%d %d %u %d %d %d %d)",
+							 (int) elem->varId,
+							 (int) elem->depth,
+							 (unsigned) elem->flags,
+							 (int) elem->min,
+							 (int) elem->max,
+							 (int) elem->next,
+							 (int) elem->jump);
+		}
+	}
+	else
+		appendStringInfoString(str, " <>");
+
+	WRITE_BOOL_FIELD(isAbsorbable);
+}
+
 
 /*
  * outNode -
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
index b6b2ce6c792..5bbde5bcad2 100644
--- a/src/backend/nodes/readfuncs.c
+++ b/src/backend/nodes/readfuncs.c
@@ -28,6 +28,7 @@
 
 #include "miscadmin.h"
 #include "nodes/bitmapset.h"
+#include "nodes/plannodes.h"
 #include "nodes/readfuncs.h"
 
 
@@ -567,6 +568,90 @@ _readExtensibleNode(void)
 	READ_DONE();
 }
 
+static RPRPattern *
+_readRPRPattern(void)
+{
+	READ_LOCALS(RPRPattern);
+
+	READ_INT_FIELD(numVars);
+	READ_INT_FIELD(maxDepth);
+	READ_INT_FIELD(numElements);
+
+	/* Read varNames array */
+	token = pg_strtok(&length); /* skip :varNames */
+	token = pg_strtok(&length); /* get '(' or '<>' */
+	if (local_node->numVars > 0 && token[0] == '(')
+	{
+		local_node->varNames = palloc(local_node->numVars * sizeof(char *));
+		for (int i = 0; i < local_node->numVars; i++)
+		{
+			token = pg_strtok(&length);
+			local_node->varNames[i] = debackslash(token, length);
+		}
+		token = pg_strtok(&length); /* skip ')' */
+	}
+	else
+	{
+		local_node->varNames = NULL;
+	}
+
+	/* Read elements array */
+	token = pg_strtok(&length); /* skip :elements */
+	token = pg_strtok(&length); /* get '(' or '<>' */
+	if (local_node->numElements > 0 && token[0] == '(')
+	{
+		local_node->elements = palloc0(local_node->numElements * sizeof(RPRPatternElement));
+		for (int i = 0; i < local_node->numElements; i++)
+		{
+			RPRPatternElement *elem = &local_node->elements[i];
+			int			varId,
+						flags,
+						depth,
+						min,
+						max,
+						next,
+						jump;
+
+			/* Parse "(varId depth flags min max next jump)" */
+			token = pg_strtok(&length);
+			varId = atoi(token);
+			token = pg_strtok(&length);
+			depth = atoi(token);
+			token = pg_strtok(&length);
+			flags = atoi(token);
+			token = pg_strtok(&length);
+			min = atoi(token);
+			token = pg_strtok(&length);
+			max = atoi(token);
+			token = pg_strtok(&length);
+			next = atoi(token);
+			token = pg_strtok(&length);
+			jump = atoi(token);
+			token = pg_strtok(&length); /* skip ')' */
+
+			elem->varId = (RPRVarId) varId;
+			elem->flags = (RPRElemFlags) flags;
+			elem->depth = (RPRDepth) depth;
+			elem->min = (RPRQuantity) min;
+			elem->max = (RPRQuantity) max;
+			elem->next = (RPRElemIdx) next;
+			elem->jump = (RPRElemIdx) jump;
+
+			/* Read next element's '(' or end */
+			if (i < local_node->numElements - 1)
+				token = pg_strtok(&length); /* get '(' */
+		}
+	}
+	else
+	{
+		local_node->elements = NULL;
+	}
+
+	READ_BOOL_FIELD(isAbsorbable);
+
+	READ_DONE();
+}
+
 
 /*
  * parseNodeString
diff --git a/src/backend/parser/Makefile b/src/backend/parser/Makefile
index 8b5a4af6bf2..51e6b1adfb8 100644
--- a/src/backend/parser/Makefile
+++ b/src/backend/parser/Makefile
@@ -30,6 +30,7 @@ OBJS = \
 	parse_oper.o \
 	parse_param.o \
 	parse_relation.o \
+	parse_rpr.o \
 	parse_target.o \
 	parse_type.o \
 	parse_utilcmd.o \
diff --git a/src/backend/parser/README b/src/backend/parser/README
index e26eb437a9f..22a5e91c8cf 100644
--- a/src/backend/parser/README
+++ b/src/backend/parser/README
@@ -26,6 +26,7 @@ parse_node.c	create nodes for various structures
 parse_oper.c	handle operators in expressions
 parse_param.c	handle Params (for the cases used in the core backend)
 parse_relation.c support routines for tables and column handling
+parse_rpr.c	handle Row Pattern Recognition
 parse_target.c	handle the result list of the query
 parse_type.c	support routines for data type handling
 parse_utilcmd.c	parse analysis for utility commands (done at execution time)
diff --git a/src/backend/parser/meson.build b/src/backend/parser/meson.build
index 86c09b29ec2..82fe86e10db 100644
--- a/src/backend/parser/meson.build
+++ b/src/backend/parser/meson.build
@@ -17,6 +17,7 @@ backend_sources += files(
   'parse_oper.c',
   'parse_param.c',
   'parse_relation.c',
+  'parse_rpr.c',
   'parse_target.c',
   'parse_type.c',
   'parse_utilcmd.c',
diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c
index acb933392de..b16e54d6e31 100644
--- a/src/backend/parser/parse_agg.c
+++ b/src/backend/parser/parse_agg.c
@@ -597,7 +597,10 @@ check_agglevels_and_constraints(ParseState *pstate, Node *expr)
 				err = _("aggregate functions are not allowed in property definition expressions");
 			else
 				err = _("grouping operations are not allowed in property definition expressions");
+			break;
 
+		case EXPR_KIND_RPR_DEFINE:
+			errkind = true;
 			break;
 
 			/*
@@ -1045,6 +1048,9 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc,
 		case EXPR_KIND_FOR_PORTION:
 			err = _("window functions are not allowed in FOR PORTION OF expressions");
 			break;
+		case EXPR_KIND_RPR_DEFINE:
+			errkind = true;
+			break;
 
 			/*
 			 * There is intentionally no default: case here, so that the
@@ -1125,7 +1131,8 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc,
 				equal(refwin->orderClause, windef->orderClause) &&
 				refwin->frameOptions == windef->frameOptions &&
 				equal(refwin->startOffset, windef->startOffset) &&
-				equal(refwin->endOffset, windef->endOffset))
+				equal(refwin->endOffset, windef->endOffset) &&
+				equal(refwin->rpCommonSyntax, windef->rpCommonSyntax))
 			{
 				/* found a duplicate window specification */
 				wfunc->winref = winref;
diff --git a/src/backend/parser/parse_clause.c b/src/backend/parser/parse_clause.c
index 4270c2382c4..6c443a31e79 100644
--- a/src/backend/parser/parse_clause.c
+++ b/src/backend/parser/parse_clause.c
@@ -39,6 +39,7 @@
 #include "parser/parse_graphtable.h"
 #include "parser/parse_oper.h"
 #include "parser/parse_relation.h"
+#include "parser/parse_rpr.h"
 #include "parser/parse_target.h"
 #include "parser/parse_type.h"
 #include "parser/parser.h"
@@ -88,8 +89,6 @@ static void checkExprIsVarFree(ParseState *pstate, Node *n,
 							   const char *constructName);
 static TargetEntry *findTargetlistEntrySQL92(ParseState *pstate, Node *node,
 											 List **tlist, ParseExprKind exprKind);
-static TargetEntry *findTargetlistEntrySQL99(ParseState *pstate, Node *node,
-											 List **tlist, ParseExprKind exprKind);
 static int	get_matching_location(int sortgroupref,
 								  List *sortgrouprefs, List *exprs);
 static List *resolve_unique_index_expr(ParseState *pstate, InferClause *infer,
@@ -101,7 +100,6 @@ static Node *transformFrameOffset(ParseState *pstate, int frameOptions,
 								  Oid rangeopfamily, Oid rangeopcintype, Oid *inRangeFunc,
 								  Node *clause);
 
-
 /*
  * transformFromClause -
  *	  Process the FROM clause and add items to the query's range table,
@@ -2310,7 +2308,7 @@ findTargetlistEntrySQL92(ParseState *pstate, Node *node, List **tlist,
  * tlist	the target list (passed by reference so we can append to it)
  * exprKind identifies clause type being processed
  */
-static TargetEntry *
+TargetEntry *
 findTargetlistEntrySQL99(ParseState *pstate, Node *node, List **tlist,
 						 ParseExprKind exprKind)
 {
@@ -3033,6 +3031,8 @@ transformWindowDefinitions(ParseState *pstate,
 		 * And prepare the new WindowClause.
 		 */
 		wc = makeNode(WindowClause);
+		wc->rpSkipTo = ST_NONE; /* ST_NONE marks this as a non-RPR window;
+								 * overridden by transformRPR() if RPR is used */
 		wc->name = windef->name;
 		wc->refname = windef->refname;
 
@@ -3161,6 +3161,10 @@ transformWindowDefinitions(ParseState *pstate,
 											 rangeopfamily, rangeopcintype,
 											 &wc->endInRangeFunc,
 											 windef->endOffset);
+
+		/* Process Row Pattern Recognition related clauses */
+		transformRPR(pstate, wc, windef, targetlist);
+
 		wc->winref = winref;
 
 		result = lappend(result, wc);
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index c3c7aa29720..f145342e1fb 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -579,6 +579,7 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 		case EXPR_KIND_GENERATED_COLUMN:
 		case EXPR_KIND_CYCLE_MARK:
 		case EXPR_KIND_PROPGRAPH_PROPERTY:
+		case EXPR_KIND_RPR_DEFINE:
 			/* okay */
 			break;
 
@@ -627,6 +628,42 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 	if (node != NULL)
 		return node;
 
+	/*
+	 * Qualified column references in DEFINE are not supported.  This covers
+	 * both FROM-clause range variables (prohibited by §6.5) and pattern
+	 * variable qualified names (e.g. UP.price), which are valid per §4.16
+	 * but not yet implemented.
+	 */
+	if (pstate->p_expr_kind == EXPR_KIND_RPR_DEFINE &&
+		list_length(cref->fields) != 1)
+	{
+		char	   *qualifier = strVal(linitial(cref->fields));
+		ListCell   *lc;
+		bool		is_pattern_var = false;
+
+		foreach(lc, pstate->p_rpr_pattern_vars)
+		{
+			if (strcmp(strVal(lfirst(lc)), qualifier) == 0)
+			{
+				is_pattern_var = true;
+				break;
+			}
+		}
+
+		if (is_pattern_var)
+			ereport(ERROR,
+					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					 errmsg("pattern variable qualified column reference \"%s\" is not supported in DEFINE clause",
+							NameListToString(cref->fields)),
+					 parser_errposition(pstate, cref->location)));
+		else
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("range variable qualified column reference \"%s\" is not allowed in DEFINE clause",
+							NameListToString(cref->fields)),
+					 parser_errposition(pstate, cref->location)));
+	}
+
 	/*----------
 	 * The allowed syntaxes are:
 	 *
@@ -1892,6 +1929,9 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
 		case EXPR_KIND_FOR_PORTION:
 			err = _("cannot use subquery in FOR PORTION OF expression");
 			break;
+		case EXPR_KIND_RPR_DEFINE:
+			err = _("cannot use subquery in DEFINE expression");
+			break;
 
 			/*
 			 * There is intentionally no default: case here, so that the
@@ -3255,6 +3295,8 @@ ParseExprKindName(ParseExprKind exprKind)
 			return "property definition expression";
 		case EXPR_KIND_FOR_PORTION:
 			return "FOR PORTION OF";
+		case EXPR_KIND_RPR_DEFINE:
+			return "DEFINE";
 
 			/*
 			 * There is intentionally no default: case here, so that the
diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c
index 35ff6427147..1eabcda02a1 100644
--- a/src/backend/parser/parse_func.c
+++ b/src/backend/parser/parse_func.c
@@ -31,6 +31,7 @@
 #include "parser/parse_target.h"
 #include "parser/parse_type.h"
 #include "utils/builtins.h"
+#include "utils/fmgroids.h"
 #include "utils/lsyscache.h"
 #include "utils/syscache.h"
 
@@ -756,8 +757,88 @@ ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs,
 	if (retset)
 		check_srf_call_placement(pstate, last_srf, location);
 
+	/*
+	 * RPR navigation functions (PREV/NEXT/FIRST/LAST) are only meaningful
+	 * inside a WINDOW DEFINE clause.
+	 *
+	 * Outside DEFINE, these polymorphic placeholders can shadow column access
+	 * via functional notation (e.g., last(f) meaning f.last). For the 1-arg
+	 * form, try column projection first; if that succeeds, use it instead.
+	 * Otherwise, report a clear parser error.
+	 */
+	if (fdresult == FUNCDETAIL_NORMAL &&
+		pstate->p_expr_kind != EXPR_KIND_RPR_DEFINE &&
+		(funcid == F_PREV_ANYELEMENT || funcid == F_NEXT_ANYELEMENT ||
+		 funcid == F_PREV_ANYELEMENT_INT8 || funcid == F_NEXT_ANYELEMENT_INT8 ||
+		 funcid == F_FIRST_ANYELEMENT || funcid == F_LAST_ANYELEMENT ||
+		 funcid == F_FIRST_ANYELEMENT_INT8 || funcid == F_LAST_ANYELEMENT_INT8))
+	{
+		/* 1-arg form: try column projection before erroring out */
+		if (nargs == 1 && !agg_star && !agg_distinct && over == NULL &&
+			list_length(funcname) == 1)
+		{
+			Node	   *projection;
+
+			projection = ParseComplexProjection(pstate,
+												strVal(linitial(funcname)),
+												linitial(fargs),
+												location);
+			if (projection)
+				return projection;
+		}
+
+		/* Not a column projection -- report error */
+		ereport(ERROR,
+				(errcode(ERRCODE_SYNTAX_ERROR),
+				 errmsg("cannot use %s outside a DEFINE clause",
+						NameListToString(funcname)),
+				 parser_errposition(pstate, location)));
+	}
+
 	/* build the appropriate output structure */
-	if (fdresult == FUNCDETAIL_NORMAL || fdresult == FUNCDETAIL_PROCEDURE)
+	if (fdresult == FUNCDETAIL_NORMAL &&
+		pstate->p_expr_kind == EXPR_KIND_RPR_DEFINE &&
+		(funcid == F_PREV_ANYELEMENT || funcid == F_NEXT_ANYELEMENT ||
+		 funcid == F_PREV_ANYELEMENT_INT8 || funcid == F_NEXT_ANYELEMENT_INT8 ||
+		 funcid == F_FIRST_ANYELEMENT || funcid == F_LAST_ANYELEMENT ||
+		 funcid == F_FIRST_ANYELEMENT_INT8 || funcid == F_LAST_ANYELEMENT_INT8))
+	{
+		/*
+		 * RPR navigation functions (PREV/NEXT/FIRST/LAST) are compiled into
+		 * EEOP_RPR_NAV_SET / EEOP_RPR_NAV_RESTORE opcodes instead of a normal
+		 * function call.  Represent them as RPRNavExpr nodes so that later
+		 * stages can identify them without relying on funcid comparisons.
+		 */
+		RPRNavKind	kind;
+		bool		has_offset;
+		RPRNavExpr *navexpr;
+
+		if (funcid == F_PREV_ANYELEMENT || funcid == F_PREV_ANYELEMENT_INT8)
+			kind = RPR_NAV_PREV;
+		else if (funcid == F_NEXT_ANYELEMENT || funcid == F_NEXT_ANYELEMENT_INT8)
+			kind = RPR_NAV_NEXT;
+		else if (funcid == F_FIRST_ANYELEMENT || funcid == F_FIRST_ANYELEMENT_INT8)
+			kind = RPR_NAV_FIRST;
+		else
+			kind = RPR_NAV_LAST;
+
+		has_offset = (funcid == F_PREV_ANYELEMENT_INT8 ||
+					  funcid == F_NEXT_ANYELEMENT_INT8 ||
+					  funcid == F_FIRST_ANYELEMENT_INT8 ||
+					  funcid == F_LAST_ANYELEMENT_INT8);
+
+		navexpr = makeNode(RPRNavExpr);
+
+		navexpr->kind = kind;
+		navexpr->arg = (Expr *) linitial(fargs);
+		navexpr->offset_arg = has_offset ? (Expr *) lsecond(fargs) : NULL;
+		navexpr->resulttype = rettype;
+		/* resultcollid will be set by parse_collate.c */
+		navexpr->location = location;
+
+		retval = (Node *) navexpr;
+	}
+	else if (fdresult == FUNCDETAIL_NORMAL || fdresult == FUNCDETAIL_PROCEDURE)
 	{
 		FuncExpr   *funcexpr = makeNode(FuncExpr);
 
@@ -2789,6 +2870,9 @@ check_srf_call_placement(ParseState *pstate, Node *last_srf, int location)
 		case EXPR_KIND_FOR_PORTION:
 			err = _("set-returning functions are not allowed in FOR PORTION OF expressions");
 			break;
+		case EXPR_KIND_RPR_DEFINE:
+			errkind = true;
+			break;
 
 			/*
 			 * There is intentionally no default: case here, so that the
diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
new file mode 100644
index 00000000000..f56b7db5bc8
--- /dev/null
+++ b/src/backend/parser/parse_rpr.c
@@ -0,0 +1,594 @@
+/*-------------------------------------------------------------------------
+ *
+ * parse_rpr.c
+ *	  Handle Row Pattern Recognition clauses in parser.
+ *
+ * This file transforms RPR-related clauses from raw parse tree to planner
+ * structures during query analysis:
+ *   - Validates frame options (must start at CURRENT ROW, no EXCLUDE)
+ *   - Validates PATTERN variable count (max RPR_VARID_MAX)
+ *   - Transforms DEFINE clause into TargetEntry list
+ *   - Stores PATTERN/SKIP TO/INITIAL clauses for planner
+ *
+ * Pattern optimization and compilation to NFA bytecode happens later
+ * in the planner (see optimizer/plan/rpr.c).
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/parser/parse_rpr.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "miscadmin.h"
+#include "nodes/makefuncs.h"
+#include "nodes/nodeFuncs.h"
+#include "optimizer/optimizer.h"
+#include "optimizer/rpr.h"
+#include "parser/parse_coerce.h"
+#include "parser/parse_collate.h"
+#include "parser/parse_expr.h"
+#include "parser/parse_rpr.h"
+#include "parser/parse_target.h"
+
+/* Forward declarations */
+static void validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
+									   List *rpDefs, List **varNames);
+static List *transformDefineClause(ParseState *pstate, WindowClause *wc,
+								   WindowDef *windef, List **targetlist);
+static void check_rpr_nav_expr(RPRNavExpr *nav, ParseState *pstate);
+static bool check_rpr_nav_nesting_walker(Node *node, void *context);
+
+/*
+ * transformRPR
+ *		Process Row Pattern Recognition related clauses.
+ *
+ * Validates and transforms RPR clauses from parse tree to planner structures:
+ *   - Validates frame options (must start at CURRENT ROW, no EXCLUDE)
+ *   - Stores AFTER MATCH SKIP TO clause
+ *   - Stores SEEK/INITIAL clause
+ *   - Transforms DEFINE clause into TargetEntry list
+ *   - Stores PATTERN AST for deparsing (optimization happens in planner)
+ *
+ * Returns early if windef has no rpCommonSyntax (non-RPR window).
+ */
+void
+transformRPR(ParseState *pstate, WindowClause *wc, WindowDef *windef,
+			 List **targetlist)
+{
+	/* Window definition must exist when called */
+	Assert(windef != NULL);
+
+	/*
+	 * Row Pattern Common Syntax clause exists?
+	 */
+	if (windef->rpCommonSyntax == NULL)
+		return;
+
+	/* Check Frame options */
+
+	/* Frame type must be "ROW" */
+	if (wc->frameOptions & FRAMEOPTION_GROUPS)
+		ereport(ERROR,
+				(errcode(ERRCODE_WINDOWING_ERROR),
+				 errmsg("cannot use FRAME option GROUPS with row pattern recognition"),
+				 errhint("Use ROWS instead."),
+				 parser_errposition(pstate,
+									windef->frameLocation >= 0 ?
+									windef->frameLocation : windef->location)));
+	if (wc->frameOptions & FRAMEOPTION_RANGE)
+		ereport(ERROR,
+				(errcode(ERRCODE_WINDOWING_ERROR),
+				 errmsg("cannot use FRAME option RANGE with row pattern recognition"),
+				 errhint("Use ROWS instead."),
+				 parser_errposition(pstate,
+									windef->frameLocation >= 0 ?
+									windef->frameLocation : windef->location)));
+
+	/* Frame must start at current row */
+	if ((wc->frameOptions & FRAMEOPTION_START_CURRENT_ROW) == 0)
+	{
+		const char *frameType = "ROWS";
+		const char *startBound = "unknown";
+
+		/* Determine current start bound */
+		if (wc->frameOptions & FRAMEOPTION_START_UNBOUNDED_PRECEDING)
+			startBound = "UNBOUNDED PRECEDING";
+		else if (wc->frameOptions & FRAMEOPTION_START_OFFSET_PRECEDING)
+			startBound = "offset PRECEDING";
+		else if (wc->frameOptions & FRAMEOPTION_START_OFFSET_FOLLOWING)
+			startBound = "offset FOLLOWING";
+
+		/* At least one valid frame start option should be set */
+		Assert((wc->frameOptions & FRAMEOPTION_START_UNBOUNDED_PRECEDING) ||
+			   (wc->frameOptions & FRAMEOPTION_START_OFFSET_PRECEDING) ||
+			   (wc->frameOptions & FRAMEOPTION_START_OFFSET_FOLLOWING));
+
+		ereport(ERROR,
+				(errcode(ERRCODE_WINDOWING_ERROR),
+				 errmsg("FRAME must start at CURRENT ROW when using row pattern recognition"),
+				 errdetail("Current frame starts with %s.", startBound),
+				 errhint("Use: %s BETWEEN CURRENT ROW AND ...", frameType),
+				 parser_errposition(pstate, windef->frameLocation >= 0 ? windef->frameLocation : windef->location)));
+	}
+
+	/* EXCLUDE options are not permitted */
+	if ((wc->frameOptions & FRAMEOPTION_EXCLUSION) != 0)
+	{
+		const char *excludeType = "EXCLUDE";
+
+		/* Determine which EXCLUDE option was used */
+		if (wc->frameOptions & FRAMEOPTION_EXCLUDE_CURRENT_ROW)
+			excludeType = "EXCLUDE CURRENT ROW";
+		else if (wc->frameOptions & FRAMEOPTION_EXCLUDE_GROUP)
+			excludeType = "EXCLUDE GROUP";
+		else if (wc->frameOptions & FRAMEOPTION_EXCLUDE_TIES)
+			excludeType = "EXCLUDE TIES";
+
+		/* At least one valid exclude option should be set */
+		Assert((wc->frameOptions & FRAMEOPTION_EXCLUDE_CURRENT_ROW) ||
+			   (wc->frameOptions & FRAMEOPTION_EXCLUDE_GROUP) ||
+			   (wc->frameOptions & FRAMEOPTION_EXCLUDE_TIES));
+
+		ereport(ERROR,
+				(errcode(ERRCODE_WINDOWING_ERROR),
+				 errmsg("cannot use EXCLUDE options with row pattern recognition"),
+				 errdetail("Frame definition includes %s.", excludeType),
+				 errhint("Remove the EXCLUDE clause from the window definition."),
+				 parser_errposition(pstate, windef->excludeLocation >= 0 ? windef->excludeLocation : windef->location)));
+	}
+
+	/* Transform AFTER MATCH SKIP TO clause */
+	wc->rpSkipTo = windef->rpCommonSyntax->rpSkipTo;
+
+	/* Transform SEEK or INITIAL clause */
+	wc->initial = windef->rpCommonSyntax->initial;
+
+	/* Transform DEFINE clause into list of TargetEntry's */
+	wc->defineClause = transformDefineClause(pstate, wc, windef, targetlist);
+
+	/* Store PATTERN AST for deparsing */
+	wc->rpPattern = windef->rpCommonSyntax->rpPattern;
+}
+
+/*
+ * validateRPRPatternVarCount
+ *		Validate that PATTERN variables don't exceed RPR_VARID_MAX.
+ *
+ * Recursively traverses the pattern tree, collecting unique variable names.
+ * Throws an error if the number of unique variables exceeds RPR_VARID_MAX.
+ *
+ * If rpDefs is non-NULL, DEFINE variable names are also collected into
+ * varNames so that transformColumnRef can distinguish pattern variable
+ * qualifiers from FROM-clause range variables.
+ *
+ * varNames is both input and output: existing names are preserved, new ones added.
+ */
+static void
+validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
+						   List *rpDefs, List **varNames)
+{
+	ListCell   *lc;
+
+	/* Pattern node must exist - parser always provides non-NULL root */
+	Assert(node != NULL);
+
+	check_stack_depth();
+
+	switch (node->nodeType)
+	{
+		case RPR_PATTERN_VAR:
+			/* Add variable name if not already in list */
+			{
+				bool		found = false;
+
+				foreach(lc, *varNames)
+				{
+					if (strcmp(strVal(lfirst(lc)), node->varName) == 0)
+					{
+						found = true;
+						break;
+					}
+				}
+				if (!found)
+				{
+					/* Check against RPR_VARID_MAX before adding */
+					if (list_length(*varNames) >= RPR_VARID_MAX)
+						ereport(ERROR,
+								(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+								 errmsg("too many pattern variables"),
+								 errdetail("Maximum is %d.", RPR_VARID_MAX),
+								 parser_errposition(pstate,
+													exprLocation((Node *) node))));
+
+					*varNames = lappend(*varNames, makeString(pstrdup(node->varName)));
+				}
+			}
+			break;
+
+		case RPR_PATTERN_SEQ:
+		case RPR_PATTERN_ALT:
+		case RPR_PATTERN_GROUP:
+			/* Recurse into children */
+			foreach(lc, node->children)
+			{
+				validateRPRPatternVarCount(pstate, (RPRPatternNode *) lfirst(lc),
+										   NULL, varNames);
+			}
+			break;
+	}
+
+	/*
+	 * After the top-level call, also collect DEFINE variable names that are
+	 * not already in the list.  This is only done once at the outermost
+	 * recursion level, detected by rpDefs being non-NULL (recursive calls
+	 * pass NULL).
+	 */
+	if (rpDefs)
+	{
+		foreach(lc, rpDefs)
+		{
+			ResTarget  *rt = (ResTarget *) lfirst(lc);
+			ListCell   *lc2;
+			bool		found = false;
+
+			foreach(lc2, *varNames)
+			{
+				if (strcmp(strVal(lfirst(lc2)), rt->name) == 0)
+				{
+					found = true;
+					break;
+				}
+			}
+			if (!found)
+				ereport(ERROR,
+						(errcode(ERRCODE_SYNTAX_ERROR),
+						 errmsg("DEFINE variable \"%s\" is not used in PATTERN",
+								rt->name),
+						 parser_errposition(pstate, rt->location)));
+		}
+	}
+}
+
+/*
+ * transformDefineClause
+ *		Process DEFINE clause and transform ResTarget into list of TargetEntry.
+ *
+ * First:
+ *   1. Validates PATTERN variable count and collects RPR variable names
+ *
+ * Then for each DEFINE variable:
+ *   2. Checks for duplicate variable names in DEFINE clause
+ *   3. Transforms expression via transformExpr() and ensures referenced
+ *      Var nodes are present in the query targetlist (via pull_var_clause)
+ *   4. Creates defineClause entry with proper resname (pattern variable name)
+ *   5. Coerces expressions to boolean type
+ *   6. Marks column origins and assigns collation information
+ *
+ * Note: Variables not in DEFINE are evaluated as TRUE by the executor.
+ * Variables in DEFINE but not in PATTERN are rejected as an error.
+ *
+ * XXX Pattern variable qualified column references in DEFINE (e.g.
+ * "A.price") are not yet supported.  Currently rejected by
+ * transformColumnRef in parse_expr.c via the p_rpr_pattern_vars check.
+ */
+static List *
+transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
+					  List **targetlist)
+{
+	ListCell   *lc,
+			   *l;
+	ResTarget  *restarget,
+			   *r;
+	List	   *restargets;
+	List	   *defineClause = NIL;
+	char	   *name;
+	List	   *patternVarNames = NIL;
+
+	/*
+	 * If Row Definition Common Syntax exists, DEFINE clause must exist. (the
+	 * raw parser should have already checked it.)
+	 */
+	Assert(windef->rpCommonSyntax->rpDefs != NULL);
+
+	/*
+	 * Validate PATTERN variable count and collect all RPR variable names
+	 * (PATTERN + DEFINE) for use in transformColumnRef.
+	 */
+	validateRPRPatternVarCount(pstate, windef->rpCommonSyntax->rpPattern,
+							   windef->rpCommonSyntax->rpDefs,
+							   &patternVarNames);
+	pstate->p_rpr_pattern_vars = patternVarNames;
+
+	/*
+	 * Check for duplicate row pattern definition variables.  The standard
+	 * requires that no two row pattern definition variable names shall be
+	 * equivalent.
+	 */
+	restargets = NIL;
+	foreach(lc, windef->rpCommonSyntax->rpDefs)
+	{
+		TargetEntry *teDefine;
+
+		restarget = (ResTarget *) lfirst(lc);
+		name = restarget->name;
+
+		foreach(l, restargets)
+		{
+			char	   *n;
+
+			r = (ResTarget *) lfirst(l);
+			n = r->name;
+
+			if (!strcmp(n, name))
+				ereport(ERROR,
+						(errcode(ERRCODE_SYNTAX_ERROR),
+						 errmsg("DEFINE variable \"%s\" appears more than once",
+								name),
+						 parser_errposition(pstate, exprLocation((Node *) r))));
+		}
+
+		restargets = lappend(restargets, restarget);
+
+		/*
+		 * Transform the DEFINE expression.  We must NOT add the whole
+		 * expression to the query targetlist, because it may contain
+		 * RPRNavExpr nodes (PREV/NEXT/FIRST/LAST) that can only be evaluated
+		 * inside the owning WindowAgg.
+		 *
+		 * Instead, we transform the expression directly and only ensure that
+		 * the individual Var nodes it references are present in the
+		 * targetlist, so the planner can propagate the referenced columns.
+		 */
+		{
+			Node	   *expr;
+			List	   *vars;
+			ListCell   *lc2;
+
+			expr = transformExpr(pstate, restarget->val,
+								 EXPR_KIND_RPR_DEFINE);
+
+			/*
+			 * Pull out Var nodes from the transformed expression and ensure
+			 * each one is present in the targetlist.  This is needed so the
+			 * planner propagates the referenced columns through the plan
+			 * tree, making them available to the WindowAgg's DEFINE
+			 * evaluation.
+			 */
+			vars = pull_var_clause(expr, 0);
+			foreach(lc2, vars)
+			{
+				Var		   *var = (Var *) lfirst(lc2);
+				bool		found = false;
+				ListCell   *tl;
+
+				foreach(tl, *targetlist)
+				{
+					TargetEntry *tle = (TargetEntry *) lfirst(tl);
+
+					if (IsA(tle->expr, Var) &&
+						((Var *) tle->expr)->varno == var->varno &&
+						((Var *) tle->expr)->varattno == var->varattno)
+					{
+						found = true;
+						break;
+					}
+				}
+				if (!found)
+				{
+					TargetEntry *newtle;
+
+					newtle = makeTargetEntry((Expr *) copyObject(var),
+											 list_length(*targetlist) + 1,
+											 NULL,
+											 true);
+					*targetlist = lappend(*targetlist, newtle);
+				}
+			}
+			list_free(vars);
+
+			/* Build the defineClause entry directly from the transformed expr */
+			teDefine = makeTargetEntry((Expr *) expr,
+									   list_length(defineClause) + 1,
+									   pstrdup(name),
+									   true);
+		}
+
+		/* build transformed DEFINE clause (list of TargetEntry) */
+		defineClause = lappend(defineClause, teDefine);
+	}
+	list_free(restargets);
+	pstate->p_rpr_pattern_vars = NIL;
+
+	/*
+	 * Make sure that the row pattern definition search condition is a boolean
+	 * expression.
+	 */
+	foreach_ptr(TargetEntry, te, defineClause)
+		te->expr = (Expr *) coerce_to_boolean(pstate, (Node *) te->expr, "DEFINE");
+
+	/* check for nested PREV/NEXT and missing column references */
+	foreach_ptr(TargetEntry, te, defineClause)
+		(void) check_rpr_nav_nesting_walker((Node *) te->expr, pstate);
+
+	/* mark column origins */
+	markTargetListOrigins(pstate, defineClause);
+
+	/* mark all nodes in the DEFINE clause tree with collation information */
+	assign_expr_collations(pstate, (Node *) defineClause);
+
+	return defineClause;
+}
+
+/*
+ * check_rpr_nav_expr
+ *		Validate a single RPRNavExpr node by walking its arg and offset_arg
+ *		subtrees in a single pass each.  Check for illegal nesting, missing
+ *		column references, and non-constant offset expressions.
+ *
+ * Nesting rules (SQL standard 5.6.4):
+ *   - PREV/NEXT wrapping FIRST/LAST: allowed (compound navigation)
+ *   - FIRST/LAST wrapping PREV/NEXT: prohibited
+ *   - Same-category nesting (PREV inside PREV, FIRST inside FIRST, etc.):
+ *     prohibited
+ */
+typedef struct
+{
+	int			nav_count;		/* number of RPRNavExpr nodes found */
+	bool		has_column_ref; /* Var found */
+	RPRNavKind	inner_kind;		/* kind of first (outermost) nested RPRNavExpr */
+} NavCheckResult;
+
+static bool
+nav_check_walker(Node *node, void *context)
+{
+	NavCheckResult *result = (NavCheckResult *) context;
+
+	if (node == NULL)
+		return false;
+	if (IsA(node, RPRNavExpr))
+	{
+		if (result->nav_count == 0)
+			result->inner_kind = ((RPRNavExpr *) node)->kind;
+		result->nav_count++;
+	}
+	if (IsA(node, Var))
+		result->has_column_ref = true;
+
+	return expression_tree_walker(node, nav_check_walker, context);
+}
+
+static void
+check_rpr_nav_expr(RPRNavExpr *nav, ParseState *pstate)
+{
+	NavCheckResult result;
+	bool		outer_is_physical = (nav->kind == RPR_NAV_PREV ||
+									 nav->kind == RPR_NAV_NEXT);
+
+	/* Check arg subtree: nesting + column reference in one walk */
+	memset(&result, 0, sizeof(result));
+	(void) nav_check_walker((Node *) nav->arg, &result);
+
+	if (result.nav_count > 0)
+	{
+		bool		inner_is_physical = (result.inner_kind == RPR_NAV_PREV ||
+										 result.inner_kind == RPR_NAV_NEXT);
+
+		if (outer_is_physical && !inner_is_physical)
+		{
+			/*
+			 * PREV/NEXT wrapping FIRST/LAST: compound navigation per SQL
+			 * standard 5.6.4.  Flatten the nested RPRNavExpr into a single
+			 * compound node.  The inner RPRNavExpr must be the direct arg of
+			 * the outer; expressions like PREV(val + FIRST(v)) are not valid
+			 * compound navigation.
+			 */
+			RPRNavExpr *inner;
+
+			/* Reject triple-or-deeper nesting (e.g. PREV(FIRST(PREV(x)))) */
+			if (result.nav_count > 1)
+				ereport(ERROR,
+						(errcode(ERRCODE_SYNTAX_ERROR),
+						 errmsg("cannot nest row pattern navigation more than two levels deep"),
+						 errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
+						 parser_errposition(pstate, nav->location)));
+
+			if (!IsA(nav->arg, RPRNavExpr))
+				ereport(ERROR,
+						(errcode(ERRCODE_SYNTAX_ERROR),
+						 errmsg("row pattern navigation operation must be a direct argument of the outer navigation"),
+						 errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
+						 parser_errposition(pstate, nav->location)));
+			inner = (RPRNavExpr *) nav->arg;
+
+			/* Determine compound kind */
+			if (nav->kind == RPR_NAV_PREV && inner->kind == RPR_NAV_FIRST)
+				nav->kind = RPR_NAV_PREV_FIRST;
+			else if (nav->kind == RPR_NAV_PREV && inner->kind == RPR_NAV_LAST)
+				nav->kind = RPR_NAV_PREV_LAST;
+			else if (nav->kind == RPR_NAV_NEXT && inner->kind == RPR_NAV_FIRST)
+				nav->kind = RPR_NAV_NEXT_FIRST;
+			else if (nav->kind == RPR_NAV_NEXT && inner->kind == RPR_NAV_LAST)
+				nav->kind = RPR_NAV_NEXT_LAST;
+
+			/* Move outer offset to compound_offset_arg */
+			nav->compound_offset_arg = nav->offset_arg;
+
+			/* Move inner offset and arg up */
+			nav->offset_arg = inner->offset_arg;
+			nav->arg = inner->arg;
+
+			/* No further nesting check needed - already validated */
+			return;
+		}
+		else if (!outer_is_physical && inner_is_physical)
+		{
+			/* FIRST/LAST wrapping PREV/NEXT: prohibited by standard */
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("FIRST and LAST cannot contain PREV or NEXT"),
+					 errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
+					 parser_errposition(pstate, nav->location)));
+		}
+		else if (outer_is_physical && inner_is_physical)
+		{
+			/* PREV/NEXT wrapping PREV/NEXT: prohibited */
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("PREV and NEXT cannot contain PREV or NEXT"),
+					 errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
+					 parser_errposition(pstate, nav->location)));
+		}
+		else
+		{
+			/* FIRST/LAST wrapping FIRST/LAST: prohibited */
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("FIRST and LAST cannot contain FIRST or LAST"),
+					 errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
+					 parser_errposition(pstate, nav->location)));
+		}
+	}
+	if (!result.has_column_ref)
+		ereport(ERROR,
+				(errcode(ERRCODE_SYNTAX_ERROR),
+				 errmsg("argument of row pattern navigation operation must include at least one column reference"),
+				 parser_errposition(pstate, nav->location)));
+
+	/* Check offset_arg: column ref + volatile in one walk */
+	if (nav->offset_arg != NULL)
+	{
+		memset(&result, 0, sizeof(result));
+		(void) nav_check_walker((Node *) nav->offset_arg, &result);
+
+		if (result.has_column_ref ||
+			contain_volatile_functions((Node *) nav->offset_arg))
+			ereport(ERROR,
+					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					 errmsg("row pattern navigation offset must be a run-time constant"),
+					 parser_errposition(pstate, nav->location)));
+	}
+}
+
+/*
+ * check_rpr_nav_nesting_walker
+ *		Walk the DEFINE clause expression tree and validate each RPRNavExpr.
+ */
+static bool
+check_rpr_nav_nesting_walker(Node *node, void *context)
+{
+	if (node == NULL)
+		return false;
+	if (IsA(node, RPRNavExpr))
+	{
+		check_rpr_nav_expr((RPRNavExpr *) node, (ParseState *) context);
+		/* don't recurse into arg; nesting already checked above */
+		return false;
+	}
+	return expression_tree_walker(node, check_rpr_nav_nesting_walker, context);
+}
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index 7977ee24783..656c552b0a8 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -648,6 +648,60 @@ typedef struct WindowFuncRunCondition
 	Expr	   *arg;
 } WindowFuncRunCondition;
 
+/*
+ * RPRNavExpr
+ *
+ * Represents a PREV/NEXT/FIRST/LAST navigation call in an RPR DEFINE clause.
+ * At expression compile time this is translated into EEOP_RPR_NAV_SET /
+ * EEOP_RPR_NAV_RESTORE opcodes rather than a normal function call.
+ *
+ * Simple navigation (PREV/NEXT/FIRST/LAST):
+ *   kind:       RPR_NAV_PREV, RPR_NAV_NEXT, RPR_NAV_FIRST, or RPR_NAV_LAST
+ *   arg:        the expression to evaluate against the target row
+ *   offset_arg: optional explicit offset expression (2-arg form); NULL for
+ *               the 1-arg form (implicit offset: 1 for PREV/NEXT, 0 for
+ *               FIRST/LAST)
+ *
+ * Compound navigation (PREV/NEXT wrapping FIRST/LAST):
+ *   kind:              RPR_NAV_PREV_FIRST, PREV_LAST, NEXT_FIRST, NEXT_LAST
+ *   arg:               the expression to evaluate against the final target row
+ *   offset_arg:        inner offset (FIRST/LAST), NULL = implicit default
+ *   compound_offset_arg: outer offset (PREV/NEXT), NULL = implicit default
+ *
+ * Compound target computation:
+ *   PREV_FIRST: (match_start + inner) - outer
+ *   NEXT_FIRST: (match_start + inner) + outer
+ *   PREV_LAST:  (currentpos  - inner) - outer
+ *   NEXT_LAST:  (currentpos  - inner) + outer
+ */
+typedef enum RPRNavKind
+{
+	RPR_NAV_PREV,
+	RPR_NAV_NEXT,
+	RPR_NAV_FIRST,
+	RPR_NAV_LAST,
+	/* compound: outer(inner(arg)) */
+	RPR_NAV_PREV_FIRST,
+	RPR_NAV_PREV_LAST,
+	RPR_NAV_NEXT_FIRST,
+	RPR_NAV_NEXT_LAST
+} RPRNavKind;
+
+typedef struct RPRNavExpr
+{
+	Expr		xpr;
+	RPRNavKind	kind;			/* navigation kind */
+	Expr	   *arg;			/* argument expression */
+	Expr	   *offset_arg;		/* offset expression, or NULL for default */
+	Expr	   *compound_offset_arg;	/* outer offset for compound nav, or
+										 * NULL if simple */
+	Oid			resulttype;		/* result type (same as arg's type) */
+	/* OID of collation of result */
+	Oid			resultcollid pg_node_attr(query_jumble_ignore);
+	/* token location, or -1 if unknown */
+	ParseLoc	location;
+} RPRNavExpr;
+
 /*
  * MergeSupportFunc
  *
diff --git a/src/include/parser/parse_clause.h b/src/include/parser/parse_clause.h
index fe234611007..8aaac881f2b 100644
--- a/src/include/parser/parse_clause.h
+++ b/src/include/parser/parse_clause.h
@@ -52,6 +52,9 @@ extern List *addTargetToSortList(ParseState *pstate, TargetEntry *tle,
 extern Index assignSortGroupRef(TargetEntry *tle, List *tlist);
 extern bool targetIsInSortList(TargetEntry *tle, Oid sortop, List *sortList);
 
+extern TargetEntry *findTargetlistEntrySQL99(ParseState *pstate, Node *node,
+											 List **tlist, ParseExprKind exprKind);
+
 /* functions in parse_jsontable.c */
 extern ParseNamespaceItem *transformJsonTable(ParseState *pstate, JsonTable *jt);
 
diff --git a/src/include/parser/parse_rpr.h b/src/include/parser/parse_rpr.h
new file mode 100644
index 00000000000..7fab6f292aa
--- /dev/null
+++ b/src/include/parser/parse_rpr.h
@@ -0,0 +1,22 @@
+/*-------------------------------------------------------------------------
+ *
+ * parse_rpr.h
+ *	  handle Row Pattern Recognition in parser
+ *
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/parser/parse_rpr.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PARSE_RPR_H
+#define PARSE_RPR_H
+
+#include "parser/parse_node.h"
+
+extern void transformRPR(ParseState *pstate, WindowClause *wc,
+						 WindowDef *windef, List **targetlist);
+
+#endif							/* PARSE_RPR_H */
-- 
2.43.0



  [application/octet-stream] v47-0003-Row-pattern-recognition-patch-rewriter.patch (8.1K, 4-v47-0003-Row-pattern-recognition-patch-rewriter.patch)
  download | inline diff:
From 1af4cced6b445802855e03ce85a4ccd6003eccae Mon Sep 17 00:00:00 2001
From: Tatsuo Ishii <[email protected]>
Date: Sat, 2 May 2026 13:40:29 +0900
Subject: [PATCH v47 3/9] Row pattern recognition patch (rewriter).

---
 src/backend/utils/adt/ruleutils.c | 255 ++++++++++++++++++++++++++++++
 1 file changed, 255 insertions(+)

diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 75b77bb39f1..70bcad9aa36 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -447,6 +447,10 @@ static void get_rule_groupingset(GroupingSet *gset, List *targetlist,
 								 bool omit_parens, deparse_context *context);
 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, 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);
@@ -7107,6 +7111,129 @@ get_rule_orderby(List *orderList, List *targetList,
 	}
 }
 
+/*
+ * Helper function to append quantifier string for pattern node
+ */
+static void
+append_pattern_quantifier(StringInfo buf, RPRPatternNode *node)
+{
+	bool		has_quantifier = true;
+
+	if (node->min == 1 && node->max == 1)
+	{
+		/* {1,1} = no quantifier */
+		has_quantifier = false;
+	}
+	else if (node->min == 0 && node->max == INT_MAX)
+		appendStringInfoChar(buf, '*');
+	else if (node->min == 1 && node->max == INT_MAX)
+		appendStringInfoChar(buf, '+');
+	else if (node->min == 0 && node->max == 1)
+		appendStringInfoChar(buf, '?');
+	else if (node->max == INT_MAX)
+		appendStringInfo(buf, "{%d,}", node->min);
+	else if (node->min == node->max)
+		appendStringInfo(buf, "{%d}", node->min);
+	else
+		appendStringInfo(buf, "{%d,%d}", node->min, node->max);
+
+	if (node->reluctant)
+	{
+		if (!has_quantifier)
+			appendStringInfo(buf, "{1}");	/* make reluctant ? unambiguous */
+		appendStringInfoChar(buf, '?');
+	}
+}
+
+/*
+ * Recursive helper to display RPRPatternNode tree
+ */
+static void
+get_rule_pattern_node(RPRPatternNode *node, deparse_context *context)
+{
+	StringInfo	buf = context->buf;
+	ListCell   *lc;
+	const char *sep;
+
+	Assert(node != NULL);
+
+	switch (node->nodeType)
+	{
+		case RPR_PATTERN_VAR:
+			appendStringInfoString(buf, quote_identifier(node->varName));
+			append_pattern_quantifier(buf, node);
+			break;
+
+		case RPR_PATTERN_SEQ:
+			sep = "";
+			foreach(lc, node->children)
+			{
+				appendStringInfoString(buf, sep);
+				get_rule_pattern_node((RPRPatternNode *) lfirst(lc), context);
+				sep = " ";
+			}
+			break;
+
+		case RPR_PATTERN_ALT:
+			sep = "";
+			foreach(lc, node->children)
+			{
+				appendStringInfoString(buf, sep);
+				get_rule_pattern_node((RPRPatternNode *) lfirst(lc), context);
+				sep = " | ";
+			}
+			break;
+
+		case RPR_PATTERN_GROUP:
+			appendStringInfoChar(buf, '(');
+			sep = "";
+			foreach(lc, node->children)
+			{
+				appendStringInfoString(buf, sep);
+				get_rule_pattern_node((RPRPatternNode *) lfirst(lc), context);
+				sep = " ";
+			}
+			appendStringInfoChar(buf, ')');
+			append_pattern_quantifier(buf, node);
+			break;
+	}
+}
+
+/*
+ * Display a PATTERN clause.
+ */
+static void
+get_rule_pattern(RPRPatternNode *rpPattern, deparse_context *context)
+{
+	StringInfo	buf = context->buf;
+
+	appendStringInfoChar(buf, '(');
+	get_rule_pattern_node(rpPattern, context);
+	appendStringInfoChar(buf, ')');
+}
+
+/*
+ * Display a DEFINE clause.
+ */
+static void
+get_rule_define(List *defineClause, deparse_context *context)
+{
+	StringInfo	buf = context->buf;
+	const char *sep;
+	ListCell   *lc_def;
+
+	sep = "  ";
+
+	foreach(lc_def, 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  ";
+	}
+}
+
 /*
  * Display a WINDOW clause.
  *
@@ -7187,6 +7314,7 @@ get_rule_windowspec(WindowClause *wc, List *targetList,
 		get_rule_orderby(wc->orderClause, targetList, false, context);
 		needspace = true;
 	}
+
 	/* framing clause is never inherited, so print unless it's default */
 	if (wc->frameOptions & FRAMEOPTION_NONDEFAULT)
 	{
@@ -7195,7 +7323,51 @@ get_rule_windowspec(WindowClause *wc, List *targetList,
 		get_window_frame_options(wc->frameOptions,
 								 wc->startOffset, wc->endOffset,
 								 context);
+		needspace = true;
+	}
+
+	/* RPR */
+	if (wc->rpSkipTo == ST_NEXT_ROW)
+	{
+		if (needspace)
+			appendStringInfoChar(buf, ' ');
+		appendStringInfoString(buf,
+							   "\n  AFTER MATCH SKIP TO NEXT ROW ");
+		needspace = true;
 	}
+	else if (wc->rpSkipTo == ST_PAST_LAST_ROW)
+	{
+		if (needspace)
+			appendStringInfoChar(buf, ' ');
+		appendStringInfoString(buf,
+							   "\n  AFTER MATCH SKIP PAST LAST ROW ");
+		needspace = true;
+	}
+	if (wc->initial)
+	{
+		if (needspace)
+			appendStringInfoChar(buf, ' ');
+		appendStringInfoString(buf, "\n  INITIAL");
+		needspace = true;
+	}
+	if (wc->rpPattern)
+	{
+		if (needspace)
+			appendStringInfoChar(buf, ' ');
+		appendStringInfoString(buf, "\n  PATTERN ");
+		get_rule_pattern(wc->rpPattern, context);
+		needspace = true;
+	}
+
+	if (wc->defineClause)
+	{
+		if (needspace)
+			appendStringInfoChar(buf, ' ');
+		appendStringInfoString(buf, "\n  DEFINE\n");
+		get_rule_define(wc->defineClause, context);
+		appendStringInfoChar(buf, ' ');
+	}
+
 	appendStringInfoChar(buf, ')');
 }
 
@@ -9939,6 +10111,89 @@ get_rule_expr(Node *node, deparse_context *context,
 			get_func_expr((FuncExpr *) node, context, showimplicit);
 			break;
 
+		case T_RPRNavExpr:
+			{
+				RPRNavExpr *nav = (RPRNavExpr *) node;
+				const char *outer_func = NULL;
+				const char *inner_func;
+
+				switch (nav->kind)
+				{
+					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, ')');
+				}
+			}
+			break;
+
 		case T_NamedArgExpr:
 			{
 				NamedArgExpr *na = (NamedArgExpr *) node;
-- 
2.43.0



  [application/octet-stream] v47-0004-Row-pattern-recognition-patch-planner.patch (93.3K, 5-v47-0004-Row-pattern-recognition-patch-planner.patch)
  download | inline diff:
From 1a9f209676a9f2095bce03411380296b59729b10 Mon Sep 17 00:00:00 2001
From: Tatsuo Ishii <[email protected]>
Date: Sat, 2 May 2026 13:40:29 +0900
Subject: [PATCH v47 4/9] Row pattern recognition patch (planner).

---
 src/backend/nodes/nodeFuncs.c             |   42 +
 src/backend/optimizer/path/allpaths.c     |   79 +
 src/backend/optimizer/path/costsize.c     |   29 +
 src/backend/optimizer/plan/Makefile       |    1 +
 src/backend/optimizer/plan/createplan.c   |  417 ++++-
 src/backend/optimizer/plan/meson.build    |    1 +
 src/backend/optimizer/plan/planner.c      |   17 +-
 src/backend/optimizer/plan/rpr.c          | 1993 +++++++++++++++++++++
 src/backend/optimizer/plan/setrefs.c      |   29 +-
 src/backend/optimizer/prep/prepjointree.c |    9 +
 src/include/nodes/plannodes.h             |  103 ++
 src/include/optimizer/rpr.h               |   65 +
 12 files changed, 2781 insertions(+), 4 deletions(-)
 create mode 100644 src/backend/optimizer/plan/rpr.c
 create mode 100644 src/include/optimizer/rpr.h

diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index f968ac68314..651d049cfa2 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;
@@ -853,6 +856,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;
@@ -1162,6 +1168,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;
@@ -1437,6 +1446,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);
@@ -2198,6 +2210,18 @@ 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;
+				if (expr->compound_offset_arg && WALK(expr->compound_offset_arg))
+					return true;
+			}
+			break;
 		case T_SubscriptingRef:
 			{
 				SubscriptingRef *sbsref = (SubscriptingRef *) node;
@@ -2431,6 +2455,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:
@@ -2823,6 +2849,8 @@ query_tree_walker_impl(Query *query,
 				return true;
 			if (WALK(wc->endOffset))
 				return true;
+			if (WALK(wc->defineClause))
+				return true;
 		}
 	}
 
@@ -3143,6 +3171,18 @@ 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 *);
+				MUTATE(newnode->compound_offset_arg, nav->compound_offset_arg, Expr *);
+				return (Node *) newnode;
+			}
+			break;
 		case T_SubscriptingRef:
 			{
 				SubscriptingRef *sbsref = (SubscriptingRef *) node;
@@ -3565,6 +3605,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;
@@ -3914,6 +3955,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/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c
index 61093f222a1..470029e42e0 100644
--- a/src/backend/optimizer/path/allpaths.c
+++ b/src/backend/optimizer/path/allpaths.c
@@ -2463,6 +2463,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;
@@ -4739,6 +4750,74 @@ remove_unused_subquery_outputs(Query *subquery, RelOptInfo *rel,
 		if (contain_volatile_functions(texpr))
 			continue;
 
+		/*
+		 * 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/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c
index 1c575e56ff6..b38cad9f121 100644
--- a/src/backend/optimizer/path/costsize.c
+++ b/src/backend/optimizer/path/costsize.c
@@ -104,6 +104,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"
@@ -3228,7 +3229,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);
diff --git a/src/backend/optimizer/plan/Makefile b/src/backend/optimizer/plan/Makefile
index 80ef162e484..7e0167789d8 100644
--- a/src/backend/optimizer/plan/Makefile
+++ b/src/backend/optimizer/plan/Makefile
@@ -19,6 +19,7 @@ OBJS = \
 	planagg.o \
 	planmain.o \
 	planner.o \
+	rpr.o \
 	setrefs.o \
 	subselect.o
 
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index de6a183da79..52205cc7159 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"
@@ -36,6 +37,7 @@
 #include "optimizer/prep.h"
 #include "optimizer/restrictinfo.h"
 #include "optimizer/subselect.h"
+#include "optimizer/rpr.h"
 #include "optimizer/tlist.h"
 #include "parser/parse_clause.h"
 #include "parser/parsetree.h"
@@ -288,7 +290,11 @@ static Memoize *make_memoize(Plan *lefttree, Oid *hashoperators,
 static WindowAgg *make_windowagg(List *tlist, WindowClause *wc,
 								 int partNumCols, AttrNumber *partColIdx, Oid *partOperators, Oid *partCollations,
 								 int ordNumCols, AttrNumber *ordColIdx, Oid *ordOperators, Oid *ordCollations,
-								 List *runCondition, List *qual, bool topWindow,
+								 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,
 						 AttrNumber *grpColIdx, Oid *grpOperators, Oid *grpCollations,
@@ -2457,6 +2463,363 @@ create_minmaxagg_plan(PlannerInfo *root, MinMaxAggPath *best_path)
 	return plan;
 }
 
+/*
+ * NavOffsetContext - context for compute_nav_offsets 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.
+ */
+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 (may be negative for
+								 * PREV_FIRST) */
+	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.
+ *
+ * Non-constant offsets set maxNeedsEval or firstNeedsEval.  Overflow sets
+ * maxOverflow or firstOverflow for RETAIN_ALL fallback.
+ */
+static bool
+nav_offset_walker(Node *node, void *ctx)
+{
+	NavOffsetContext *context = (NavOffsetContext *) ctx;
+
+	if (node == NULL)
+		return false;
+
+	if (IsA(node, RPRNavExpr))
+	{
+		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)
+			{
+				int64		offset;
+
+				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;
+
+			if (!context->firstNeedsEval)
+			{
+				int64		offset;
+
+				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)
+		{
+			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))
+				{
+					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;
+
+					context->maxOffset = Max(context->maxOffset, combined);
+				}
+				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)
+		{
+			context->hasFirst = true;
+
+			if (!context->firstNeedsEval)
+			{
+				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;
+					}
+
+					context->firstOffset = Min(context->firstOffset, combined);
+				}
+				else
+					context->firstNeedsEval = true;
+			}
+		}
+
+		/* 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;
+	}
+}
+
+/*
+ * 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
  *
@@ -2481,6 +2844,11 @@ create_windowagg_plan(PlannerInfo *root, WindowAggPath *best_path)
 	Oid		   *ordOperators;
 	Oid		   *ordCollations;
 	ListCell   *lc;
+	List	   *defineVariableList = NIL;
+	List	   *filteredDefineClause = NIL;
+	RPRPattern *compiledPattern = NULL;
+	Bitmapset  *matchStartDependent = NULL;
+
 
 	/*
 	 * Choice of tlist here is motivated by the fact that WindowAgg will be
@@ -2531,6 +2899,28 @@ create_windowagg_plan(PlannerInfo *root, WindowAggPath *best_path)
 		ordNumCols++;
 	}
 
+	/* Build RPR pattern and defineVariableList */
+	if (wc->rpPattern)
+	{
+		/*
+		 * Build defineVariableList from defineClause.  The parser already
+		 * rejects DEFINE variables not used in PATTERN, so no filtering is
+		 * needed.
+		 */
+		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,
+										  !bms_is_empty(matchStartDependent));
+	}
+
 	/* And finally we can make the WindowAgg node */
 	plan = make_windowagg(tlist,
 						  wc,
@@ -2543,6 +2933,10 @@ create_windowagg_plan(PlannerInfo *root, WindowAggPath *best_path)
 						  ordOperators,
 						  ordCollations,
 						  best_path->runCondition,
+						  wc->rpSkipTo,
+						  compiledPattern,
+						  filteredDefineClause,
+						  matchStartDependent,
 						  best_path->qual,
 						  best_path->topwindow,
 						  subplan);
@@ -6613,7 +7007,11 @@ static WindowAgg *
 make_windowagg(List *tlist, WindowClause *wc,
 			   int partNumCols, AttrNumber *partColIdx, Oid *partOperators, Oid *partCollations,
 			   int ordNumCols, AttrNumber *ordColIdx, Oid *ordOperators, Oid *ordCollations,
-			   List *runCondition, List *qual, bool topWindow, Plan *lefttree)
+			   List *runCondition, RPSkipTo rpSkipTo,
+			   RPRPattern *compiledPattern,
+			   List *defineClause,
+			   Bitmapset *defineMatchStartDependent,
+			   List *qual, bool topWindow, Plan *lefttree)
 {
 	WindowAgg  *node = makeNode(WindowAgg);
 	Plan	   *plan = &node->plan;
@@ -6640,6 +7038,21 @@ make_windowagg(List *tlist, WindowClause *wc,
 	node->inRangeAsc = wc->inRangeAsc;
 	node->inRangeNullsFirst = wc->inRangeNullsFirst;
 	node->topWindow = topWindow;
+	node->rpSkipTo = rpSkipTo;
+
+	/* Store compiled pattern for NFA execution */
+	node->rpPattern = compiledPattern;
+
+	node->defineClause = 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);
 
 	plan->targetlist = tlist;
 	plan->lefttree = lefttree;
diff --git a/src/backend/optimizer/plan/meson.build b/src/backend/optimizer/plan/meson.build
index c565b2adbcc..5b2381cd774 100644
--- a/src/backend/optimizer/plan/meson.build
+++ b/src/backend/optimizer/plan/meson.build
@@ -7,6 +7,7 @@ backend_sources += files(
   'planagg.c',
   'planmain.c',
   'planner.c',
+  'rpr.c',
   'setrefs.c',
   'subselect.c',
 )
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 933dcbf5004..703c6ab7432 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -1053,6 +1053,9 @@ subquery_planner(PlannerGlobal *glob, Query *parse, char *plan_name,
 												EXPRKIND_LIMIT);
 		wc->endOffset = preprocess_expression(root, wc->endOffset,
 											  EXPRKIND_LIMIT);
+		wc->defineClause = (List *) preprocess_expression(root,
+														  (Node *) wc->defineClause,
+														  EXPRKIND_TARGET);
 	}
 
 	parse->limitOffset = preprocess_expression(root, parse->limitOffset,
@@ -6112,6 +6115,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;
@@ -6195,7 +6206,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/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
new file mode 100644
index 00000000000..2543170c374
--- /dev/null
+++ b/src/backend/optimizer/plan/rpr.c
@@ -0,0 +1,1993 @@
+/*-------------------------------------------------------------------------
+ *
+ * rpr.c
+ *	  Row Pattern Recognition pattern compilation for planner
+ *
+ * This file contains functions for optimizing RPR pattern AST and
+ * compiling it to a flat element array for NFA execution by WindowAgg.
+ *
+ * Key components:
+ *   1. Pattern Optimization: Simplifies patterns before compilation
+ *      (e.g., flatten nested SEQ/ALT, merge consecutive vars)
+ *   2. Pattern Compilation: Converts AST to flat element array for NFA
+ *   3. Absorption Analysis: Computes flags for O(n^2)->O(n) optimization
+ *
+ * Context Absorption Optimization:
+ *   When a pattern starts with a greedy unbounded element (e.g., A+ or (A B)+),
+ *   newer contexts cannot produce longer matches than older contexts.
+ *   By absorbing (eliminating) redundant newer contexts, we reduce
+ *   complexity from O(n^2) to O(n) for patterns like A+ B.
+ *
+ *   The absorption analysis uses two element flags:
+ *   - RPR_ELEM_ABSORBABLE: marks WHERE to compare (judgment point)
+ *   - RPR_ELEM_ABSORBABLE_BRANCH: marks the absorbable region
+ *
+ *   See computeAbsorbability() and the detailed comments before
+ *   isUnboundedStart() for the full design explanation.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/optimizer/plan/rpr.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include <limits.h>
+
+#include "miscadmin.h"
+#include "nodes/makefuncs.h"
+#include "optimizer/rpr.h"
+
+/* Forward declarations - pattern comparison */
+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);
+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,
+									RPRDepth depth, RPRDepth *maxDepth);
+static void scanRPRPattern(RPRPatternNode *node, char **varNames, int *numVars,
+						   int *numElements, RPRDepth *maxDepth);
+static RPRPattern *allocateRPRPattern(int numVars, int numElements,
+									  RPRDepth maxDepth, char **varNamesStack);
+static RPRVarId getVarIdFromPattern(RPRPattern *pat, const char *varName);
+static bool fillRPRPatternVar(RPRPatternNode *node, RPRPattern *pat,
+							  int *idx, RPRDepth depth);
+static bool fillRPRPatternGroup(RPRPatternNode *node, RPRPattern *pat,
+								int *idx, RPRDepth depth);
+static bool fillRPRPatternAlt(RPRPatternNode *node, RPRPattern *pat,
+							  int *idx, RPRDepth depth);
+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);
+static void computeAbsorbabilityRecursive(RPRPattern *pattern,
+										  RPRElemIdx startIdx,
+										  bool *hasAbsorbable);
+static void computeAbsorbability(RPRPattern *pattern);
+
+/*
+ * rprPatternEqual
+ *		Compare two RPRPatternNode trees for equality.
+ *
+ * Returns true if the trees are structurally identical.
+ */
+static bool
+rprPatternEqual(RPRPatternNode *a, RPRPatternNode *b)
+{
+	/* Pattern nodes in children lists must never be NULL */
+	Assert(a != NULL && b != NULL);
+
+	/* Must have same node type and quantifiers */
+	if (a->nodeType != b->nodeType)
+		return false;
+	if (a->min != b->min || a->max != b->max)
+		return false;
+	if (a->reluctant != b->reluctant)
+		return false;
+
+	switch (a->nodeType)
+	{
+		case RPR_PATTERN_VAR:
+			return strcmp(a->varName, b->varName) == 0;
+
+		case RPR_PATTERN_SEQ:
+		case RPR_PATTERN_ALT:
+		case RPR_PATTERN_GROUP:
+			return rprPatternChildrenEqual(a->children, b->children);
+	}
+
+	return false;				/* keep compiler quiet */
+}
+
+/*
+ * rprPatternChildrenEqual
+ *		Compare children lists of two pattern nodes for equality.
+ *
+ * Returns true if the children lists are structurally identical.
+ */
+static bool
+rprPatternChildrenEqual(List *a, List *b)
+{
+	ListCell   *lca,
+			   *lcb;
+
+	if (list_length(a) != list_length(b))
+		return false;
+
+	forboth(lca, a, lcb, b)
+	{
+		if (!rprPatternEqual((RPRPatternNode *) lfirst(lca),
+							 (RPRPatternNode *) lfirst(lcb)))
+			return false;
+	}
+
+	return true;
+}
+
+/*
+ * tryUnwrapSingleChild
+ *		Try to unwrap pattern node with single child.
+ *
+ * Examples (internal node representation):
+ *   SEQ[A] -> A    (single-element sequence becomes the element)
+ *   ALT[A] -> A    (single-alternative becomes the alternative)
+ *
+ * If pattern has exactly one child, return the child directly.
+ * Otherwise returns the pattern unchanged.
+ * Used by both SEQ and ALT optimization.
+ */
+static RPRPatternNode *
+tryUnwrapSingleChild(RPRPatternNode *pattern)
+{
+	if (list_length(pattern->children) == 1)
+		return (RPRPatternNode *) linitial(pattern->children);
+
+	return pattern;
+}
+
+/*
+ * flattenSeqChildren
+ *		Recursively optimize children and flatten nested SEQ.
+ *
+ * Example:
+ *   SEQ(A, SEQ(B, C)) -> SEQ(A, B, C)
+ *
+ * Returns a new list with optimized children, with nested SEQ children
+ * flattened into the parent list.
+ */
+static List *
+flattenSeqChildren(List *children)
+{
+	ListCell   *lc;
+	List	   *newChildren = NIL;
+
+	foreach(lc, children)
+	{
+		RPRPatternNode *child = (RPRPatternNode *) lfirst(lc);
+		RPRPatternNode *opt = optimizeRPRPattern(child);
+
+		/* GROUP{1,1} should have been unwrapped by optimizeGroupPattern */
+		Assert(!(opt->nodeType == RPR_PATTERN_GROUP &&
+				 opt->min == 1 && opt->max == 1 && opt->reluctant == false));
+
+		if (opt->nodeType == RPR_PATTERN_SEQ)
+		{
+			newChildren = list_concat(newChildren,
+									  list_copy(opt->children));
+		}
+		else
+		{
+			newChildren = lappend(newChildren, opt);
+		}
+	}
+
+	return newChildren;
+}
+
+/*
+ * mergeConsecutiveVars
+ *		Merge consecutive identical VAR nodes.
+ *
+ * Examples:
+ *   A{m1,M1} A{m2,M2} -> A{m1+m2, M1+M2} where INF + x = INF.
+ *
+ * Only merges non-reluctant VAR nodes with the same variable name.
+ */
+static List *
+mergeConsecutiveVars(List *children)
+{
+	ListCell   *lc;
+	List	   *mergedChildren = NIL;
+	RPRPatternNode *prev = NULL;
+
+	foreach(lc, children)
+	{
+		RPRPatternNode *child = (RPRPatternNode *) lfirst(lc);
+
+		if (child->nodeType == RPR_PATTERN_VAR && child->reluctant == false)
+		{
+			/* ----------------------
+			 * 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)
+			 */
+			if (prev != NULL &&
+				strcmp(prev->varName, child->varName) == 0 &&
+				prev->min <= RPR_QUANTITY_INF - child->min &&
+				(prev->max <= RPR_QUANTITY_INF - child->max ||
+				 prev->max == RPR_QUANTITY_INF ||
+				 child->max == RPR_QUANTITY_INF))
+			{
+				/*
+				 * 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 == false);
+
+				prev->min += child->min;
+
+				if (prev->max == RPR_QUANTITY_INF ||
+					child->max == RPR_QUANTITY_INF)
+					prev->max = RPR_QUANTITY_INF;
+				else
+					prev->max += child->max;
+			}
+			else
+			{
+				/* Flush previous and start new */
+				if (prev != NULL)
+					mergedChildren = lappend(mergedChildren, prev);
+				prev = child;
+			}
+		}
+		else
+		{
+			/* Non-mergeable - flush previous */
+			if (prev != NULL)
+				mergedChildren = lappend(mergedChildren, prev);
+			mergedChildren = lappend(mergedChildren, child);
+			prev = NULL;
+		}
+	}
+
+	/* Flush remaining */
+	if (prev != NULL)
+		mergedChildren = lappend(mergedChildren, prev);
+
+	return mergedChildren;
+}
+
+/*
+ * mergeConsecutiveGroups
+ *		Merge consecutive identical GROUP nodes.
+ *
+ * Example:
+ *   (A B)+ (A B)+ -> (A B){2,}
+ *
+ * Only merges non-reluctant GROUP nodes with identical children.
+ */
+static List *
+mergeConsecutiveGroups(List *children)
+{
+	ListCell   *lc;
+	List	   *mergedChildren = NIL;
+	RPRPatternNode *prev = NULL;
+
+	foreach(lc, children)
+	{
+		RPRPatternNode *child = (RPRPatternNode *) lfirst(lc);
+
+		if (child->nodeType == RPR_PATTERN_GROUP && child->reluctant == false)
+		{
+			/* ----------------------
+			 * 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)
+			 */
+			if (prev != NULL &&
+				rprPatternChildrenEqual(prev->children, child->children) &&
+				prev->min <= RPR_QUANTITY_INF - child->min &&
+				(prev->max <= RPR_QUANTITY_INF - child->max ||
+				 prev->max == RPR_QUANTITY_INF ||
+				 child->max == RPR_QUANTITY_INF))
+			{
+				/*
+				 * 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 == false);
+
+				prev->min += child->min;
+
+				if (prev->max == RPR_QUANTITY_INF ||
+					child->max == RPR_QUANTITY_INF)
+					prev->max = RPR_QUANTITY_INF;
+				else
+					prev->max += child->max;
+			}
+			else
+			{
+				/* Flush previous and start new */
+				if (prev != NULL)
+					mergedChildren = lappend(mergedChildren, prev);
+				prev = child;
+			}
+		}
+		else
+		{
+			/* Non-mergeable - flush previous */
+			if (prev != NULL)
+				mergedChildren = lappend(mergedChildren, prev);
+			mergedChildren = lappend(mergedChildren, child);
+			prev = NULL;
+		}
+	}
+
+	/* Flush remaining */
+	if (prev != NULL)
+		mergedChildren = lappend(mergedChildren, prev);
+
+	return mergedChildren;
+}
+
+/*
+ * mergeConsecutiveAlts
+ *		Merge consecutive identical ALT nodes into a GROUP.
+ *
+ * Example:
+ *   (A | B) (A | B) (A | B) -> (A | B){3}
+ *
+ * After GROUP{1,1} unwrap, bare alternations like (A | B) become ALT nodes
+ * in the SEQ.  This step detects consecutive identical ALT nodes and wraps
+ * them in a GROUP with the appropriate quantifier.
+ */
+static List *
+mergeConsecutiveAlts(List *children)
+{
+	ListCell   *lc;
+	List	   *mergedChildren = NIL;
+	RPRPatternNode *prev = NULL;
+	int			count = 0;
+
+	foreach(lc, children)
+	{
+		RPRPatternNode *child = (RPRPatternNode *) lfirst(lc);
+
+		if (child->nodeType == RPR_PATTERN_ALT && child->reluctant == false)
+		{
+			if (prev != NULL &&
+				rprPatternChildrenEqual(prev->children, child->children))
+			{
+				/* Same ALT as prev - accumulate */
+				count++;
+			}
+			else
+			{
+				/* Different ALT or first ALT - flush previous */
+				if (prev != NULL)
+				{
+					if (count > 1)
+					{
+						/* Wrap in GROUP{count,count}(ALT) */
+						RPRPatternNode *group = makeNode(RPRPatternNode);
+
+						group->nodeType = RPR_PATTERN_GROUP;
+						group->min = count;
+						group->max = count;
+						group->reluctant = false;
+						group->reluctant_location = -1;
+						group->location = -1;
+						group->children = list_make1(prev);
+						mergedChildren = lappend(mergedChildren, group);
+					}
+					else
+						mergedChildren = lappend(mergedChildren, prev);
+				}
+				prev = child;
+				count = 1;
+			}
+		}
+		else
+		{
+			/* Non-ALT - flush previous */
+			if (prev != NULL)
+			{
+				if (count > 1)
+				{
+					RPRPatternNode *group = makeNode(RPRPatternNode);
+
+					group->nodeType = RPR_PATTERN_GROUP;
+					group->min = count;
+					group->max = count;
+					group->reluctant = false;
+					group->reluctant_location = -1;
+					group->location = -1;
+					group->children = list_make1(prev);
+					mergedChildren = lappend(mergedChildren, group);
+				}
+				else
+					mergedChildren = lappend(mergedChildren, prev);
+			}
+			mergedChildren = lappend(mergedChildren, child);
+			prev = NULL;
+			count = 0;
+		}
+	}
+
+	/* Flush remaining */
+	if (prev != NULL)
+	{
+		if (count > 1)
+		{
+			RPRPatternNode *group = makeNode(RPRPatternNode);
+
+			group->nodeType = RPR_PATTERN_GROUP;
+			group->min = count;
+			group->max = count;
+			group->reluctant = false;
+			group->reluctant_location = -1;
+			group->location = -1;
+			group->children = list_make1(prev);
+			mergedChildren = lappend(mergedChildren, group);
+		}
+		else
+			mergedChildren = lappend(mergedChildren, prev);
+	}
+
+	return mergedChildren;
+}
+
+/*
+ * 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,}
+ *   A B (A B)+ A B -> (A B){3,}
+ */
+static List *
+mergeGroupPrefixSuffix(List *children)
+{
+	List	   *result = NIL;
+	int			numChildren = list_length(children);
+	int			i;
+	int			skipUntil = -1; /* skip suffix elements already absorbed */
+
+	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.
+		 */
+		Assert(i >= skipUntil);
+
+		/*
+		 * If this is a GROUP, see if preceding/following elements match its
+		 * children. GROUP's content may be wrapped in a SEQ - unwrap for
+		 * comparison.
+		 */
+		if (child->nodeType == RPR_PATTERN_GROUP && child->reluctant == false)
+		{
+			List	   *groupContent = child->children;
+			int			groupChildCount;
+			int			prefixLen = list_length(result);
+			List	   *trimmed;
+
+			/*
+			 * If GROUP has single SEQ child, compare with SEQ's children.
+			 * e.g., (A B)+ internally contains sequence A B; compare against
+			 * that.
+			 */
+			if (list_length(groupContent) == 1)
+			{
+				RPRPatternNode *inner = (RPRPatternNode *) linitial(groupContent);
+
+				if (inner->nodeType == RPR_PATTERN_SEQ)
+					groupContent = inner->children;
+			}
+
+			groupChildCount = list_length(groupContent);
+
+			/*
+			 * PREFIX MERGE: Check if preceding elements match. Keep absorbing
+			 * as long as we have matching prefixes.
+			 */
+			while (prefixLen >= groupChildCount && groupChildCount > 0)
+			{
+				List	   *prefixElements = NIL;
+				int			j;
+
+				/* Extract last groupChildCount elements from prefix */
+				for (j = prefixLen - groupChildCount; j < prefixLen; j++)
+				{
+					prefixElements = lappend(prefixElements,
+											 list_nth(result, j));
+				}
+
+				/* Compare with GROUP's (possibly unwrapped) children */
+				if (rprPatternChildrenEqual(prefixElements, groupContent) &&
+					child->min < RPR_QUANTITY_INF - 1 &&
+					(child->max == RPR_QUANTITY_INF ||
+					 child->max < RPR_QUANTITY_INF - 1))
+				{
+					/*
+					 * 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;
+					for (j = 0; j < prefixLen - groupChildCount; j++)
+					{
+						trimmed = lappend(trimmed,
+										  list_nth(result, j));
+					}
+					result = trimmed;
+					prefixLen = list_length(result);
+				}
+				else
+				{
+					list_free(prefixElements);
+					break;
+				}
+
+				list_free(prefixElements);
+			}
+
+			/*
+			 * SUFFIX MERGE: Check if following elements match. Keep absorbing
+			 * as long as we have matching suffixes.
+			 */
+			while (i + groupChildCount < numChildren && groupChildCount > 0)
+			{
+				List	   *suffixElements = NIL;
+				int			j;
+				int			suffixStart = i + 1;
+
+				/* suffixStart always >= skipUntil after i adjustment */
+				Assert(skipUntil <= suffixStart);
+
+				/* Extract next groupChildCount elements as suffix */
+				for (j = 0; j < groupChildCount; j++)
+				{
+					int			idx = suffixStart + j;
+
+					/* while condition guarantees idx < numChildren */
+					Assert(idx < numChildren);
+					suffixElements = lappend(suffixElements,
+											 list_nth(children, idx));
+				}
+
+				/* Compare with GROUP's children */
+				if (list_length(suffixElements) == groupChildCount &&
+					rprPatternChildrenEqual(suffixElements, groupContent) &&
+					child->min < RPR_QUANTITY_INF - 1 &&
+					(child->max == RPR_QUANTITY_INF ||
+					 child->max < RPR_QUANTITY_INF - 1))
+				{
+					/*
+					 * Match! Absorb suffix by incrementing quantifier and
+					 * skipping.
+					 */
+					child->min += 1;
+					if (child->max != RPR_QUANTITY_INF)
+						child->max += 1;
+					skipUntil = suffixStart + groupChildCount;
+
+					/*
+					 * Update i to continue suffix check after absorbed
+					 * elements
+					 */
+					i = skipUntil - 1;
+				}
+				else
+				{
+					list_free(suffixElements);
+					break;
+				}
+
+				list_free(suffixElements);
+			}
+		}
+
+		result = lappend(result, child);
+	}
+
+	return result;
+}
+
+/*
+ * optimizeSeqPattern
+ *		Optimize SEQ pattern node.
+ *
+ * Optimizations:
+ *   1. Flatten nested SEQ and GROUP{1,1}
+ *   2. Merge consecutive identical VAR nodes
+ *   3. Merge consecutive identical GROUP nodes
+ *   4. Merge consecutive identical ALT nodes into GROUP
+ *   5. Merge prefix/suffix into GROUP with matching children
+ *   6. Unwrap single-item SEQ
+ */
+static RPRPatternNode *
+optimizeSeqPattern(RPRPatternNode *pattern)
+{
+	/* Recursively optimize children and flatten nested SEQ/GROUP{1,1} */
+	pattern->children = flattenSeqChildren(pattern->children);
+
+	/* Merge consecutive identical VAR nodes */
+	pattern->children = mergeConsecutiveVars(pattern->children);
+
+	/* Merge consecutive identical GROUP nodes */
+	pattern->children = mergeConsecutiveGroups(pattern->children);
+
+	/* Merge consecutive identical ALT nodes into GROUP */
+	pattern->children = mergeConsecutiveAlts(pattern->children);
+
+	/* Merge prefix/suffix into GROUP with matching children */
+	pattern->children = mergeGroupPrefixSuffix(pattern->children);
+
+	/* Unwrap single-item SEQ */
+	return tryUnwrapSingleChild(pattern);
+}
+
+/*
+ * flattenAltChildren
+ *		Recursively optimize children and flatten nested ALT nodes.
+ *
+ * Example:
+ *   (A | (B | C)) -> (A | B | C)
+ *
+ * Returns a new list with optimized children, with nested ALT children
+ * flattened into the parent list.
+ */
+static List *
+flattenAltChildren(List *children)
+{
+	ListCell   *lc;
+	List	   *newChildren = NIL;
+
+	foreach(lc, children)
+	{
+		RPRPatternNode *child = (RPRPatternNode *) lfirst(lc);
+		RPRPatternNode *opt = optimizeRPRPattern(child);
+
+		if (opt->nodeType == RPR_PATTERN_ALT)
+			newChildren = list_concat(newChildren, list_copy(opt->children));
+		else
+			newChildren = lappend(newChildren, opt);
+	}
+
+	return newChildren;
+}
+
+/*
+ * removeDuplicateAlternatives
+ *		Remove duplicate alternatives from a list.
+ *
+ * Examples:
+ *   (A | B | A) -> (A | B)
+ *   (X | Y | X | Z | Y) -> (X | Y | Z)
+ *
+ * Returns a new list with only unique children (first occurrence kept).
+ */
+static List *
+removeDuplicateAlternatives(List *children)
+{
+	ListCell   *lc;
+	ListCell   *lc2;
+	List	   *uniqueChildren = NIL;
+
+	foreach(lc, children)
+	{
+		RPRPatternNode *child = (RPRPatternNode *) lfirst(lc);
+		bool		isDuplicate = false;
+
+		foreach(lc2, uniqueChildren)
+		{
+			if (rprPatternEqual((RPRPatternNode *) lfirst(lc2), child))
+			{
+				isDuplicate = true;
+				break;
+			}
+		}
+
+		if (!isDuplicate)
+			uniqueChildren = lappend(uniqueChildren, child);
+	}
+
+	return uniqueChildren;
+}
+
+/*
+ * optimizeAltPattern
+ *		Optimize ALT pattern node.
+ *
+ * Optimizations:
+ *   1. Flatten nested ALT
+ *   2. Remove duplicate alternatives
+ *   3. Unwrap single-item ALT
+ */
+static RPRPatternNode *
+optimizeAltPattern(RPRPatternNode *pattern)
+{
+	/* Recursively optimize children and flatten nested ALT */
+	pattern->children = flattenAltChildren(pattern->children);
+
+	/* Remove duplicate alternatives */
+	pattern->children = removeDuplicateAlternatives(pattern->children);
+
+	/* Unwrap single-item ALT */
+	return tryUnwrapSingleChild(pattern);
+}
+
+/*
+ * tryMultiplyQuantifiers
+ *		Try to multiply quantifiers.
+ *
+ * Multiplication is SAFE when:
+ *   1. Both unbounded: (A*)* -> A*, (A+)+ -> A+
+ *   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)
+ *
+ * Returns the child node with multiplied quantifiers if successful,
+ * otherwise returns the original pattern unchanged.
+ */
+static RPRPatternNode *
+tryMultiplyQuantifiers(RPRPatternNode *pattern)
+{
+	RPRPatternNode *child;
+	int64		new_min_64;
+	int64		new_max_64;
+
+	/* Parser always creates GROUP with exactly one child */
+	Assert(list_length(pattern->children) == 1);
+
+	if (pattern->reluctant)
+		return pattern;
+
+	child = (RPRPatternNode *) linitial(pattern->children);
+
+	if ((child->nodeType != RPR_PATTERN_VAR &&
+		 child->nodeType != RPR_PATTERN_GROUP) ||
+		child->reluctant)
+		return pattern;
+
+	/* Case 1: Both unbounded - (A*)* -> A*, (A+)+ -> A+ */
+	if (child->max == RPR_QUANTITY_INF && pattern->max == RPR_QUANTITY_INF)
+	{
+		new_min_64 = (int64) child->min * pattern->min;
+		if (new_min_64 >= RPR_QUANTITY_INF)
+			return pattern;		/* overflow, skip optimization */
+
+		child->min = (int) new_min_64;
+		child->max = RPR_QUANTITY_INF;
+		return child;
+	}
+
+	/*----------
+	 * 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 (new_max_64 >= RPR_QUANTITY_INF)
+				return pattern;
+		}
+
+		child->min = (int) new_min_64;
+		child->max = (int) new_max_64;
+		return child;
+	}
+
+	/* Not safe to multiply */
+	return pattern;
+}
+
+/*
+ * tryUnwrapGroup
+ *		Try to unwrap GROUP{1,1} node.
+ *
+ * Examples:
+ *   (A){1,1}   -> A
+ *   (A B){1,1} -> SEQ(A, B)  (unwraps the inner SEQ)
+ *   (A)?       -> A?         (propagate quantifier to single VAR child)
+ *   (A)+?      -> A+?        (propagate quantifier including reluctant)
+ *
+ * If GROUP has min=1, max=1, return the child directly (reluctant on
+ * {1,1} is meaningless).  If GROUP has a single VAR child with default
+ * quantifier {1,1}, propagate the GROUP's quantifier to the child and
+ * unwrap.  Otherwise returns the pattern unchanged.
+ *
+ * Note: Parser always creates GROUP with exactly one child via list_make1().
+ */
+static RPRPatternNode *
+tryUnwrapGroup(RPRPatternNode *pattern)
+{
+	RPRPatternNode *child;
+
+	/* Parser always creates GROUP with single child */
+	Assert(list_length(pattern->children) == 1);
+
+	child = (RPRPatternNode *) linitial(pattern->children);
+
+	/* GROUP{1,1}: unwrap directly (reluctant on {1,1} is meaningless) */
+	if (pattern->min == 1 && pattern->max == 1)
+		return child;
+
+	/*
+	 * Single VAR child with default {1,1}: propagate GROUP's quantifier to
+	 * the child and unwrap.  E.g., (A)?? -> A??, (A)+? -> A+?
+	 */
+	if (child->nodeType == RPR_PATTERN_VAR &&
+		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;
+	}
+
+	return pattern;
+}
+
+/*
+ * optimizeGroupPattern
+ *		Optimize GROUP pattern node.
+ *
+ * Optimizations:
+ *   1. Quantifier multiplication: (A{m}){n} -> A{m*n}
+ *   2. Unwrap GROUP{1,1}
+ */
+static RPRPatternNode *
+optimizeGroupPattern(RPRPatternNode *pattern)
+{
+	ListCell   *lc;
+	List	   *newChildren;
+	RPRPatternNode *result;
+
+	/* Recursively optimize children */
+	newChildren = NIL;
+	foreach(lc, pattern->children)
+	{
+		RPRPatternNode *child = (RPRPatternNode *) lfirst(lc);
+
+		newChildren = lappend(newChildren, optimizeRPRPattern(child));
+	}
+	pattern->children = newChildren;
+
+	/* Try quantifier multiplication */
+	result = tryMultiplyQuantifiers(pattern);
+	if (result != pattern)
+		return result;
+
+	/* Try unwrapping GROUP{1,1} */
+	return tryUnwrapGroup(pattern);
+}
+
+/*
+ * optimizeRPRPattern
+ *		Optimize RPRPatternNode tree (dispatcher).
+ *
+ * Dispatches to type-specific optimization functions.
+ * Returns the optimized pattern (may be a different node).
+ */
+static RPRPatternNode *
+optimizeRPRPattern(RPRPatternNode *pattern)
+{
+	/* Pattern nodes from parser are never NULL */
+	Assert(pattern != NULL);
+
+	check_stack_depth();
+
+	switch (pattern->nodeType)
+	{
+		case RPR_PATTERN_VAR:
+			return pattern;
+		case RPR_PATTERN_SEQ:
+			return optimizeSeqPattern(pattern);
+		case RPR_PATTERN_ALT:
+			return optimizeAltPattern(pattern);
+		case RPR_PATTERN_GROUP:
+			return optimizeGroupPattern(pattern);
+	}
+
+	return pattern;				/* keep compiler quiet */
+}
+
+/*
+ * collectDefineVariables
+ *		Collect variable names from DEFINE clause.
+ *
+ * Populates varNames array with variable names in DEFINE order.
+ * This ensures varId == defineIdx, eliminating runtime mapping.
+ * Returns the number of variables collected.
+ */
+static int
+collectDefineVariables(List *defineVariableList, char **varNames)
+{
+	ListCell   *lc;
+	int			numVars = 0;
+
+	foreach(lc, defineVariableList)
+	{
+		/* Parser already checked this limit in transformDefineClause */
+		Assert(numVars < RPR_VARID_MAX);
+
+		varNames[numVars++] = strVal(lfirst(lc));
+	}
+
+	return numVars;
+}
+
+/*
+ * scanRPRPatternRecursive
+ *		Recursively scan pattern AST (pass 1 internal).
+ *
+ * Collects unique variable names and counts elements while tracking depth.
+ * Variables from DEFINE clause are already in varNames; this adds any
+ * additional variables found in the pattern.
+ */
+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 */
+	Assert(node != NULL);
+
+	check_stack_depth();
+
+	/* 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)));
+
+	/* Track maximum depth */
+	if (depth > *maxDepth)
+		*maxDepth = depth;
+
+	switch (node->nodeType)
+	{
+		case RPR_PATTERN_VAR:
+			/* Count element */
+			(*numElements)++;
+
+			/* Collect variable name if not already present */
+			for (i = 0; i < *numVars; i++)
+			{
+				if (strcmp(varNames[i], node->varName) == 0)
+					return;		/* Already have this variable */
+			}
+
+			/*
+			 * 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.
+			 */
+			Assert(*numVars < RPR_VARID_MAX);
+			varNames[(*numVars)++] = node->varName;
+			break;
+
+		case RPR_PATTERN_SEQ:
+			/* Sequence: just recurse into children */
+			foreach(lc, node->children)
+			{
+				scanRPRPatternRecursive((RPRPatternNode *) lfirst(lc), varNames,
+										numVars, numElements, depth, maxDepth);
+			}
+			break;
+
+		case RPR_PATTERN_GROUP:
+			/* Add BEGIN element if group has non-trivial quantifier */
+			if (node->min != 1 || node->max != 1)
+				(*numElements)++;
+
+			/* Recurse into children at increased depth */
+			foreach(lc, node->children)
+			{
+				scanRPRPatternRecursive((RPRPatternNode *) lfirst(lc), varNames,
+										numVars, numElements, depth + 1, maxDepth);
+			}
+
+			/* Add END element if group has non-trivial quantifier */
+			if (node->min != 1 || node->max != 1)
+				(*numElements)++;
+			break;
+
+		case RPR_PATTERN_ALT:
+			/* Count ALT start element */
+			(*numElements)++;
+
+			/* Recurse into children at increased depth */
+			foreach(lc, node->children)
+			{
+				scanRPRPatternRecursive((RPRPatternNode *) lfirst(lc), varNames,
+										numVars, numElements, depth + 1, maxDepth);
+			}
+			break;
+	}
+}
+
+/*
+ * scanRPRPattern
+ *		Scan pattern AST (pass 1 entry point).
+ *
+ * Collects unique variable names (appending to those from DEFINE clause),
+ * counts total elements (including FIN marker), and tracks maximum depth.
+ * Reports error if element count exceeds RPR_ELEMIDX_MAX.
+ */
+static void
+scanRPRPattern(RPRPatternNode *node, char **varNames, int *numVars,
+			   int *numElements, RPRDepth *maxDepth)
+{
+	*numElements = 0;
+	*maxDepth = 0;
+
+	scanRPRPatternRecursive(node, varNames, numVars, numElements, 0, maxDepth);
+
+	(*numElements)++;			/* +1 for FIN marker */
+
+	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)));
+}
+
+/*
+ * allocateRPRPattern
+ *		Allocate and initialize RPRPattern structure.
+ *
+ * Creates the pattern structure, copies variable names, and allocates
+ * the elements array. The elements array is zero-initialized.
+ */
+static RPRPattern *
+allocateRPRPattern(int numVars, int numElements, RPRDepth maxDepth,
+				   char **varNamesStack)
+{
+	RPRPattern *result;
+	int			i;
+
+	result = makeNode(RPRPattern);
+	result->numVars = numVars;
+	result->maxDepth = maxDepth + 1;	/* +1: depth is 0-based */
+	result->numElements = numElements;
+
+	/* Copy varNames (pattern must have at least one variable) */
+	Assert(numVars > 0);
+	result->varNames = palloc(numVars * sizeof(char *));
+	for (i = 0; i < numVars; i++)
+		result->varNames[i] = pstrdup(varNamesStack[i]);
+
+	/* Allocate elements array (zero-init for reserved fields) */
+	Assert(numElements >= 2);
+	result->elements = palloc0(numElements * sizeof(RPRPatternElement));
+
+	return result;
+}
+
+/*
+ * getVarIdFromPattern
+ *		Get variable ID for a variable name from RPRPattern.
+ *
+ * Returns the index of the variable in the varNames array.
+ */
+static RPRVarId
+getVarIdFromPattern(RPRPattern *pat, const char *varName)
+{
+	for (int i = 0; i < pat->numVars; i++)
+	{
+		if (strcmp(pat->varNames[i], varName) == 0)
+			return (RPRVarId) i;
+	}
+
+	/* Should not happen - variable should already be collected */
+	elog(ERROR, "pattern variable \"%s\" not found", varName);
+	pg_unreachable();
+}
+
+/*
+ * fillRPRPatternVar
+ *		Fill a VAR pattern element.
+ *
+ * Returns true if this VAR is nullable (can match zero rows).
+ */
+static bool
+fillRPRPatternVar(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth depth)
+{
+	RPRPatternElement *elem = &pat->elements[*idx];
+
+	memset(elem, 0, sizeof(RPRPatternElement));
+	elem->varId = getVarIdFromPattern(pat, node->varName);
+	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)
+		elem->flags |= RPR_ELEM_RELUCTANT;
+	(*idx)++;
+
+	return (node->min == 0);
+}
+
+/*
+ * fillRPRPatternGroup
+ *		Fill a GROUP pattern and its children.
+ *
+ * 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
+ * through the body can match zero rows).
+ */
+static bool
+fillRPRPatternGroup(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth depth)
+{
+	ListCell   *lc;
+	int			groupStartIdx = *idx;
+	int			beginIdx = -1;
+	bool		bodyNullable = true;
+
+	/* Add BEGIN marker if group has non-trivial quantifier */
+	if (node->min != 1 || node->max != 1)
+	{
+		RPRPatternElement *elem = &pat->elements[*idx];
+
+		beginIdx = *idx;
+		memset(elem, 0, sizeof(RPRPatternElement));
+		elem->varId = RPR_VARID_BEGIN;
+		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)
+			elem->flags |= RPR_ELEM_RELUCTANT;
+		(*idx)++;
+		groupStartIdx = *idx;	/* children start after BEGIN */
+	}
+
+	foreach(lc, node->children)
+	{
+		if (!fillRPRPattern((RPRPatternNode *) lfirst(lc), pat, idx, depth + 1))
+			bodyNullable = false;
+	}
+
+	/* Add group end marker if group has non-trivial quantifier */
+	if (node->min != 1 || node->max != 1)
+	{
+		RPRPatternElement *beginElem = &pat->elements[beginIdx];
+		RPRPatternElement *endElem = &pat->elements[*idx];
+
+		memset(endElem, 0, sizeof(RPRPatternElement));
+		endElem->varId = RPR_VARID_END;
+		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)
+			endElem->flags |= RPR_ELEM_RELUCTANT;
+
+		/*
+		 * If the group body is nullable (all paths can match empty), mark the
+		 * END element so that nfa_advance_end can fast-forward the iteration
+		 * count to min when reached via empty-match skip paths.
+		 */
+		if (bodyNullable)
+			endElem->flags |= RPR_ELEM_EMPTY_LOOP;
+
+		(*idx)++;
+
+		/* Set BEGIN skip pointer (next is set by finalize) */
+		beginElem->jump = *idx; /* skip: go to after END */
+	}
+
+	return (node->min == 0 || bodyNullable);
+}
+
+/*
+ * fillRPRPatternAlt
+ *		Fill an ALT pattern and its alternatives.
+ *
+ * Creates ALT_START marker, fills each alternative at increased depth,
+ * sets jump pointers for backtracking, and next pointers for successful paths.
+ *
+ * Returns true if any branch is nullable (OR semantics: one nullable
+ * branch suffices for the alternation to produce an empty match).
+ */
+static bool
+fillRPRPatternAlt(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth depth)
+{
+	ListCell   *lc;
+	ListCell   *lc2;
+	RPRPatternElement *elem;
+	List	   *altBranchStarts = NIL;
+	List	   *altEndPositions = NIL;
+	int			afterAltIdx;
+	bool		anyNullable = false;
+
+	/* Add alternation start marker */
+	elem = &pat->elements[*idx];
+	memset(elem, 0, sizeof(RPRPatternElement));
+	elem->varId = RPR_VARID_ALT;
+	elem->depth = depth;
+	elem->min = 1;
+	elem->max = 1;
+	elem->next = RPR_ELEMIDX_INVALID;
+	elem->jump = RPR_ELEMIDX_INVALID;
+	(*idx)++;
+
+	/* Fill each alternative */
+	foreach(lc, node->children)
+	{
+		RPRPatternNode *alt = (RPRPatternNode *) lfirst(lc);
+		int			branchStart = *idx;
+
+		altBranchStarts = lappend_int(altBranchStarts, branchStart);
+		if (fillRPRPattern(alt, pat, idx, depth + 1))
+			anyNullable = true;
+		altEndPositions = lappend_int(altEndPositions, *idx - 1);
+	}
+
+	/* Set jump on first element of each alternative to next alternative */
+	foreach(lc, altBranchStarts)
+	{
+		int			firstElemIdx = lfirst_int(lc);
+
+		if (lnext(altBranchStarts, lc) != NULL)
+			pat->elements[firstElemIdx].jump = lfirst_int(lnext(altBranchStarts, lc));
+	}
+
+	/* Set next on last element of each alternative to after the alternation */
+	afterAltIdx = *idx;
+	lc2 = list_head(altBranchStarts);
+
+	foreach(lc, altEndPositions)
+	{
+		int			endPos = lfirst_int(lc);
+		int			branchStart = lfirst_int(lc2);
+
+		if (pat->elements[endPos].next != RPR_ELEMIDX_INVALID)
+		{
+			/*
+			 * An inner ALT already set next on this element.  Redirect all
+			 * elements in this branch that share the same target to point to
+			 * after this ALT instead.
+			 */
+			int			oldTarget = pat->elements[endPos].next;
+			int			j;
+
+			for (j = branchStart; j <= endPos; j++)
+			{
+				if (pat->elements[j].next == oldTarget)
+					pat->elements[j].next = afterAltIdx;
+			}
+		}
+		else
+		{
+			pat->elements[endPos].next = afterAltIdx;
+		}
+
+		lc2 = lnext(altBranchStarts, lc2);
+	}
+
+	list_free(altBranchStarts);
+	list_free(altEndPositions);
+
+	return anyNullable;
+}
+
+/*
+ * fillRPRPattern
+ *		Fill pattern elements array from AST (pass 2).
+ *
+ * Recursively traverses AST and populates pre-allocated elements array.
+ * Dispatches to type-specific fill functions.
+ *
+ * Returns true if the pattern is nullable (can match zero rows).
+ * For SEQ nodes, all children must be nullable (AND).
+ */
+static bool
+fillRPRPattern(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth depth)
+{
+	ListCell   *lc;
+	bool		allNullable = true;
+
+	/* Pattern nodes from parser are never NULL */
+	Assert(node != NULL);
+
+	check_stack_depth();
+
+	switch (node->nodeType)
+	{
+		case RPR_PATTERN_SEQ:
+			foreach(lc, node->children)
+			{
+				if (!fillRPRPattern((RPRPatternNode *) lfirst(lc), pat, idx, depth))
+					allNullable = false;
+			}
+			return allNullable;
+
+		case RPR_PATTERN_VAR:
+			return fillRPRPatternVar(node, pat, idx, depth);
+
+		case RPR_PATTERN_GROUP:
+			return fillRPRPatternGroup(node, pat, idx, depth);
+
+		case RPR_PATTERN_ALT:
+			return fillRPRPatternAlt(node, pat, idx, depth);
+	}
+
+	return false;				/* unreachable */
+}
+
+/*
+ * finalizeRPRPattern
+ *		Finalize pattern structure after filling elements.
+ *
+ * This performs:
+ *   1. Initialize absorption flag to false
+ *   2. Set up next pointers for sequential flow
+ *   3. Add FIN marker at the end
+ */
+static void
+finalizeRPRPattern(RPRPattern *result)
+{
+	int			finIdx = result->numElements - 1;
+	int			i;
+	RPRPatternElement *finElem;
+
+	/* Initialize absorption flag */
+	result->isAbsorbable = false;
+
+	/* Set up next pointers for elements that don't have one */
+	for (i = 0; i < finIdx; i++)
+	{
+		RPRPatternElement *elem = &result->elements[i];
+
+		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 */
+	finElem = &result->elements[finIdx];
+	memset(finElem, 0, sizeof(RPRPatternElement));
+	finElem->varId = RPR_VARID_FIN;
+	finElem->depth = 0;
+	finElem->min = 1;
+	finElem->max = 1;
+	finElem->next = RPR_ELEMIDX_INVALID;
+	finElem->jump = RPR_ELEMIDX_INVALID;
+}
+
+/*-------------------------------------------------------------------------
+ * CONTEXT ABSORPTION: TWO-FLAG DESIGN
+ *-------------------------------------------------------------------------
+ *
+ * Context absorption eliminates redundant match searches by absorbing newer
+ * contexts that cannot produce longer matches than older contexts. This
+ * achieves O(n^2) -> O(n) performance improvement for patterns like A+ B.
+ *
+ * Core Insight:
+ *   For pattern A+ B, if Ctx1 starts at row 0 and Ctx2 starts at row 1,
+ *   both matching A continuously, Ctx1 will always have more A matches.
+ *   When B finally appears, Ctx1's match (0 to current) is always longer
+ *   than Ctx2's match (1 to current). So Ctx2 can be safely eliminated.
+ *
+ * Two Flags:
+ *   1. RPR_ELEM_ABSORBABLE - "Absorption judgment point"
+ *      WHERE contexts can be compared for absorption.
+ *      - Simple unbounded VAR (A+): the VAR element itself
+ *      - Unbounded GROUP ((A B)+): the END element only
+ *
+ *   2. RPR_ELEM_ABSORBABLE_BRANCH - "Absorbable region marker"
+ *      ALL elements within the absorbable region.
+ *      - Used for tracking state.isAbsorbable at runtime
+ *      - States leaving this region become non-absorbable permanently
+ *
+ * Why Two Flags?
+ *   For pattern "(A B)+", contexts at different positions (one at A,
+ *   another at B) cannot be compared - they must synchronize at END.
+ *
+ *   Example: "(A B)+" with input A B A B A B...
+ *     Row 0 (A): Ctx1 starts, matches A
+ *     Row 1 (B): Ctx1 matches B -> END (count=1)
+ *     Row 2 (A): Ctx1 loops to A, Ctx2 starts at A
+ *     Row 3 (B): Ctx1 at END (count=2), Ctx2 at END (count=1)
+ *                -> Both at END, comparable! Ctx1 absorbs Ctx2.
+ *
+ *   Contexts synchronize at END every group-length rows. Therefore:
+ *   - ABSORBABLE marks END as judgment point (where to compare)
+ *   - ABSORBABLE_BRANCH keeps state.isAbsorbable=true through A->B->END
+ *
+ * Pattern Examples:
+ *
+ *   Pattern: A+ B
+ *   Element 0 (A): ABSORBABLE | ABSORBABLE_BRANCH  <- judgment point
+ *   Element 1 (B): (none)
+ *   -> Compare at A every row. When contexts move to B, absorption stops.
+ *
+ *   Pattern: (A B)+ C
+ *   Element 0 (A): ABSORBABLE_BRANCH
+ *   Element 1 (B): ABSORBABLE_BRANCH
+ *   Element 2 (END): ABSORBABLE | ABSORBABLE_BRANCH  <- judgment point
+ *   Element 3 (C): (none)
+ *   -> Compare at END every 2 rows. When contexts move to C, absorption stops.
+ *
+ *   Pattern: (A+ B+)+ C
+ *   Element 0 (A): ABSORBABLE | ABSORBABLE_BRANCH  <- only first A+ flagged
+ *   Element 1 (B): (none)
+ *   Element 2 (END): (none)
+ *   Element 3 (C): (none)
+ *   -> Only first unbounded portion (A+) gets flags. Absorption happens
+ *      at A during first iteration. After moving to B+, absorption stops.
+ *
+ * First Unbounded Portion Strategy:
+ *   The algorithm only flags the FIRST unbounded portion starting from
+ *   element 0. This is sufficient because:
+ *   - Absorption in first portion already achieves O(n) complexity
+ *   - Later portions have different synchronization characteristics
+ *   - Nested unbounded patterns are too complex for simple absorption
+ *   - Complex patterns (nested groups, etc.) naturally die from mismatch
+ *
+ * Runtime Usage (in nodeWindowAgg.c):
+ *   - state.isAbsorbable = (previous && elem.ABSORBABLE_BRANCH)
+ *   - Monotonic: once false, stays false (cannot re-enter region)
+ *   - context.hasAbsorbableState: can absorb others (>=1 absorbable state)
+ *   - context.allStatesAbsorbable: can be absorbed (ALL states absorbable)
+ *   - Absorption check: if Ctx1.hasAbsorbable && Ctx2.allAbsorbable,
+ *     compare counts at same elemIdx, absorb if Ctx1.count >= Ctx2.count
+ *
+ *-------------------------------------------------------------------------
+ */
+
+/*
+ * 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.
+ *
+ * For context absorption to work, the sequence starting at idx must be:
+ *   - Unbounded (max = infinity)
+ *   - Greedy (not reluctant)
+ *   - At the start of current scope
+ *
+ * Two cases are handled:
+ *   1. Simple VAR: A+ B C - A has max=INF, gets both flags
+ *   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+)+ (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 *e;
+
+	/* Case 1: Simple unbounded VAR at start (greedy only) */
+	if (RPRElemIsVar(elem) && elem->max == RPR_QUANTITY_INF &&
+		!RPRElemIsReluctant(elem))
+	{
+		/* Set both flags on first element */
+		elem->flags |= RPR_ELEM_ABSORBABLE_BRANCH | RPR_ELEM_ABSORBABLE;
+		return true;
+	}
+
+	/*
+	 * 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.
+	 */
+	if (!isFixedLengthChildren(pattern, idx, startDepth))
+		return false;
+
+	/* Find the END element at startDepth - 1 */
+	e = &pattern->elements[idx];
+	while (e->depth >= startDepth)
+		e = &pattern->elements[e->next];
+
+	/* END must be unbounded greedy */
+	if (e->depth == startDepth - 1 &&
+		RPRElemIsEnd(e) && e->max == RPR_QUANTITY_INF &&
+		!RPRElemIsReluctant(e))
+	{
+		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->depth >= startDepth;
+			 e = &pattern->elements[e->next])
+			e->flags |= RPR_ELEM_ABSORBABLE_BRANCH;
+		e->flags |= RPR_ELEM_ABSORBABLE_BRANCH | RPR_ELEM_ABSORBABLE;
+		return true;
+	}
+
+	return false;
+}
+
+/*
+ * computeAbsorbabilityRecursive
+ *		Recursively check absorbability starting from given index.
+ *
+ * If the element at startIdx is ALT, recursively checks each branch independently.
+ * Each branch gets its own absorbability status, and if any branch is absorbable,
+ * the ALT element itself is marked with RPR_ELEM_ABSORBABLE_BRANCH.
+ *
+ * If BEGIN, skips to first child.
+ *
+ * Otherwise (VAR), checks if the element starts an unbounded sequence via
+ * isUnboundedStart.
+ */
+static void
+computeAbsorbabilityRecursive(RPRPattern *pattern, RPRElemIdx startIdx,
+							  bool *hasAbsorbable)
+{
+	RPRPatternElement *elem = &pattern->elements[startIdx];
+
+	check_stack_depth();
+
+	if (RPRElemIsAlt(elem))
+	{
+		/* ALT: recursively check each branch */
+		RPRElemIdx	branchIdx = elem->next;
+		RPRPatternElement *branchFirst;
+		bool		branchAbsorbable;
+
+		while (branchIdx != RPR_ELEMIDX_INVALID)
+		{
+			branchAbsorbable = false;
+
+			Assert(branchIdx < pattern->numElements);
+			branchFirst = &pattern->elements[branchIdx];
+
+			/* Stop if element is outside ALT scope (not a branch) */
+			if (branchFirst->depth <= elem->depth)
+				break;
+
+			/* Recursively check this branch */
+			computeAbsorbabilityRecursive(pattern, branchIdx, &branchAbsorbable);
+			if (branchAbsorbable)
+			{
+				*hasAbsorbable = true;
+			}
+
+			branchIdx = branchFirst->jump;
+		}
+
+		/* Mark ALT element if any branch is absorbable */
+		if (*hasAbsorbable)
+			elem->flags |= RPR_ELEM_ABSORBABLE_BRANCH;
+	}
+	else if (RPRElemIsBegin(elem))
+	{
+		/*
+		 * 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
+	{
+		/* Should never reach END - structural invariant of pattern AST */
+		Assert(!RPRElemIsEnd(elem));
+
+		/* Non-ALT, non-BEGIN: check if unbounded start */
+		if (isUnboundedStart(pattern, startIdx))
+		{
+			*hasAbsorbable = true;
+		}
+	}
+}
+
+/*
+ * computeAbsorbability
+ *		Determine if pattern supports context absorption optimization.
+ *
+ * Context absorption eliminates redundant match searches by absorbing
+ * newer contexts that cannot produce longer matches than older contexts.
+ * This achieves O(n^2) -> O(n) performance improvement.
+ *
+ * Only greedy unbounded quantifiers at pattern start can be absorbable.
+ * Reluctant quantifiers are excluded because they don't maintain monotonic
+ * decrease property required for safe absorption.
+ *
+ * This function sets two flags:
+ *   RPR_ELEM_ABSORBABLE: Absorption judgment point
+ *     - Simple unbounded VAR: the VAR itself (e.g., A in A+)
+ *     - Unbounded GROUP: the END element (e.g., END in (A B)+)
+ *   RPR_ELEM_ABSORBABLE_BRANCH: All elements in absorbable region
+ *     - All elements within the same scope as unbounded start
+ *
+ * Examples:
+ *   A+ B C         - absorbable (A gets both flags)
+ *   (A B)+ C       - absorbable (A,B,END get BRANCH, END gets ABSORBABLE)
+ *   A B+           - NOT absorbable (unbounded not at start)
+ *   A+? B C        - NOT absorbable (reluctant quantifier)
+ *   (A+ B+)+       - only first A+ on first iteration (nested unbounded not supported)
+ *   A+ | B+        - both branches absorbable independently
+ *   A+ | C D       - only A+ branch absorbable (C D branch not absorbable)
+ *   ((A+ B) | C) D - nested ALT: A+ branch is absorbable
+ */
+static void
+computeAbsorbability(RPRPattern *pattern)
+{
+	bool		hasAbsorbable = false;
+
+	/* Parser always produces at least one element + FIN */
+	Assert(pattern->numElements >= 2);
+
+	/* Start recursion from first element */
+	computeAbsorbabilityRecursive(pattern, 0, &hasAbsorbable);
+	pattern->isAbsorbable = hasAbsorbable;
+}
+
+/*
+ * collectPatternVariablesRecursive
+ *		Recursively collect variable names from pattern AST.
+ */
+static void
+collectPatternVariablesRecursive(RPRPatternNode *node, List **varNames)
+{
+	ListCell   *lc;
+
+	Assert(node != NULL);
+
+	check_stack_depth();
+
+	switch (node->nodeType)
+	{
+		case RPR_PATTERN_VAR:
+			/* Add variable if not already in list */
+			foreach(lc, *varNames)
+			{
+				if (strcmp(strVal(lfirst(lc)), node->varName) == 0)
+					return;		/* Already collected */
+			}
+			*varNames = lappend(*varNames, makeString(pstrdup(node->varName)));
+			break;
+
+		case RPR_PATTERN_SEQ:
+		case RPR_PATTERN_ALT:
+		case RPR_PATTERN_GROUP:
+			foreach(lc, node->children)
+			{
+				collectPatternVariablesRecursive((RPRPatternNode *) lfirst(lc),
+												 varNames);
+			}
+			break;
+	}
+}
+
+/*
+ * collectPatternVariables
+ *		Collect unique variable names used in PATTERN clause.
+ *
+ * Returns a list of String nodes containing variable names.
+ * Used to filter defineClause before varId assignment.
+ */
+List *
+collectPatternVariables(RPRPatternNode *pattern)
+{
+	List	   *varNames = NIL;
+
+	/* Caller ensures pattern is not NULL */
+	Assert(pattern != NULL);
+
+	collectPatternVariablesRecursive(pattern, &varNames);
+	return varNames;
+}
+
+/*
+ * buildDefineVariableList
+ *		Build defineVariableList from defineClause.
+ *
+ * The parser already ensures that all DEFINE variables appear in PATTERN,
+ * so no filtering is needed here.
+ *
+ * Sets *defineVariableList to list of variable names (String nodes).
+ */
+void
+buildDefineVariableList(List *defineClause, List **defineVariableList)
+{
+	ListCell   *lc;
+
+	*defineVariableList = NIL;
+
+	foreach(lc, defineClause)
+	{
+		TargetEntry *te = (TargetEntry *) lfirst(lc);
+
+		*defineVariableList = lappend(*defineVariableList,
+									  makeString(pstrdup(te->resname)));
+	}
+}
+
+/*
+ * buildRPRPattern
+ *		Compile pattern AST to flat bytecode array.
+ *
+ * Compilation phases:
+ *   1. Optimize AST (flatten, merge, deduplicate)
+ *   2. Scan: collect variables, count elements (pass 1)
+ *   3. Allocate result structure
+ *   4. Fill elements from AST (pass 2)
+ *   5. Finalize pattern structure
+ *   6. Compute context absorption eligibility
+ *
+ * Called from createplan.c during plan creation.
+ */
+RPRPattern *
+buildRPRPattern(RPRPatternNode *pattern, List *defineVariableList,
+				RPSkipTo rpSkipTo, int frameOptions,
+				bool hasMatchStartDependent)
+{
+	RPRPattern *result;
+	RPRPatternNode *optimized;
+	char	   *varNamesStack[RPR_VARID_MAX];
+	int			numVars;
+	int			numElements;
+	RPRDepth	maxDepth;
+	int			idx;
+	bool		hasLimitedFrame;
+
+	/* Caller must check for NULL pattern before calling */
+	Assert(pattern != NULL);
+
+	/* Optimize the pattern tree */
+	optimized = optimizeRPRPattern(copyObject(pattern));
+
+	/* Collect variable names from DEFINE clause */
+	numVars = collectDefineVariables(defineVariableList, varNamesStack);
+
+	/* Scan pattern: collect variables, count elements, validate limits */
+	scanRPRPattern(optimized, varNamesStack, &numVars, &numElements, &maxDepth);
+
+	/* Allocate result structure */
+	result = allocateRPRPattern(numVars, numElements, maxDepth, varNamesStack);
+
+	/* Fill elements (pass 2) */
+	idx = 0;
+	fillRPRPattern(optimized, result, &idx, 0);
+
+	/* Finalize: set up next pointers, flags, and add FIN marker */
+	finalizeRPRPattern(result);
+
+	/*
+	 * Compute context absorption eligibility. Absorption requires both
+	 * structural absorbability and runtime conditions. Check runtime
+	 * conditions first to avoid unnecessary pattern analysis.
+	 *
+	 * Runtime conditions for absorption:
+	 *
+	 * 1. SKIP TO PAST LAST ROW required (not SKIP TO NEXT ROW): With NEXT
+	 * ROW, after each match the search resumes from the next row, so contexts
+	 * are immediately discarded. No redundant contexts accumulate, making
+	 * absorption unnecessary.
+	 *
+	 * 2. Unbounded frame end required (not ROWS with bounded end): With a
+	 * bounded frame (e.g., ROWS BETWEEN CURRENT ROW AND 10 FOLLOWING),
+	 * matches may be truncated at frame boundaries. This changes the
+	 * 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 &&
+		!hasMatchStartDependent)
+	{
+		/* Runtime conditions met - check structural absorbability */
+		computeAbsorbability(result);
+	}
+
+	return result;
+}
diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c
index ff0e875f2a2..813a326bd78 100644
--- a/src/backend/optimizer/plan/setrefs.c
+++ b/src/backend/optimizer/plan/setrefs.c
@@ -214,7 +214,6 @@ static List *set_windowagg_runcondition_references(PlannerInfo *root,
 static void record_elided_node(PlannerGlobal *glob, int plan_node_id,
 							   NodeTag elided_type, Bitmapset *relids);
 
-
 /*****************************************************************************
  *
  *		SUBPLAN REFERENCES
@@ -2633,6 +2632,34 @@ set_upper_references(PlannerInfo *root, Plan *plan, int rtoffset)
 					   NRM_EQUAL,
 					   NUM_EXEC_QUAL(plan));
 
+	/*
+	 * Replace an expression tree in each DEFINE clause so that all Var
+	 * nodes's varno refers to OUTER_VAR.
+	 */
+	if (IsA(plan, WindowAgg))
+	{
+		WindowAgg  *wplan = (WindowAgg *) plan;
+
+		if (wplan->defineClause != NIL)
+		{
+			foreach(l, wplan->defineClause)
+			{
+				TargetEntry *tle = (TargetEntry *) lfirst(l);
+
+				tle = flatCopyTargetEntry(tle);
+				tle->expr = (Expr *)
+					fix_upper_expr(root,
+								   (Node *) tle->expr,
+								   subplan_itlist,
+								   OUTER_VAR,
+								   rtoffset,
+								   NRM_EQUAL,
+								   NUM_EXEC_QUAL(plan));
+				lfirst(l) = tle;
+			}
+		}
+	}
+
 	pfree(subplan_itlist);
 }
 
diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c
index 4424fdbe906..02898a9106b 100644
--- a/src/backend/optimizer/prep/prepjointree.c
+++ b/src/backend/optimizer/prep/prepjointree.c
@@ -2593,6 +2593,15 @@ perform_pullup_replace_vars(PlannerInfo *root,
 	parse->returningList = (List *)
 		pullup_replace_vars((Node *) parse->returningList, rvcontext);
 
+	foreach(lc, parse->windowClause)
+	{
+		WindowClause *wc = lfirst_node(WindowClause, lc);
+
+		if (wc->defineClause != NIL)
+			wc->defineClause = (List *)
+				pullup_replace_vars((Node *) wc->defineClause, rvcontext);
+	}
+
 	if (parse->onConflict)
 	{
 		parse->onConflict->onConflictSet = (List *)
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 14a1dfed2b9..fd4bdf2cb31 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -18,6 +18,7 @@
 #include "access/stratnum.h"
 #include "nodes/bitmapset.h"
 #include "nodes/lockoptions.h"
+#include "nodes/parsenodes.h"
 #include "nodes/primnodes.h"
 
 
@@ -1244,6 +1245,70 @@ typedef struct Agg
 	List	   *chain;
 } Agg;
 
+/* ----------------
+ *		Row Pattern Recognition compiled pattern types
+ * ----------------
+ */
+
+/* Type definitions for RPR pattern elements */
+typedef uint8 RPRVarId;			/* pattern variable ID */
+typedef uint8 RPRElemFlags;		/* element flags */
+typedef uint8 RPRDepth;			/* group nesting depth */
+typedef int32 RPRQuantity;		/* quantifier min/max */
+typedef int16 RPRElemIdx;		/* element array index */
+
+/*
+ * RPRPatternElement - flat element for NFA pattern matching (16 bytes)
+ *
+ * Layout optimized for alignment (no padding holes):
+ *   varId(1) + depth(1) + flags(1) + reserved(1) + min(4) + max(4) + next(2) + jump(2)
+ */
+typedef struct RPRPatternElement
+{
+	RPRVarId	varId;			/* variable ID, or special value for control */
+	RPRDepth	depth;			/* group nesting depth */
+	RPRElemFlags flags;			/* flags (reluctant, etc.) */
+	uint8		reserved;		/* reserved padding byte */
+	RPRQuantity min;			/* quantifier minimum */
+	RPRQuantity max;			/* quantifier maximum */
+	RPRElemIdx	next;			/* next element index */
+	RPRElemIdx	jump;			/* jump target (for ALT/GROUP) */
+} RPRPatternElement;
+
+/*
+ * RPRPattern - compiled pattern for NFA execution
+ *
+ * Requires custom copy/out/read functions due to elements array.
+ */
+typedef struct RPRPattern
+{
+	pg_node_attr(custom_copy_equal, custom_read_write)
+
+	NodeTag		type;			/* T_RPRPattern */
+	int			numVars;		/* number of pattern variables */
+	char	  **varNames;		/* array of variable names (DEFINE order
+								 * first) */
+	RPRDepth	maxDepth;		/* maximum group nesting depth */
+	int			numElements;	/* number of elements */
+	RPRPatternElement *elements;	/* array of pattern elements */
+
+	/*----------------
+	 * Context absorption optimization.
+	 *
+	 * Absorption is only safe when later matches are guaranteed to be
+	 * suffixes of earlier matches. This requires simple pattern structure:
+	 *
+	 * Case 1: No ALT, single unbounded element (A+, (A B)+)
+	 * Case 2: Top-level ALT with each branch being single unbounded (A+ | B+)
+	 *
+	 * 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).
+	 */
+	bool		isAbsorbable;	/* true if pattern supports context absorption */
+} RPRPattern;
+
 /* ----------------
  *		window aggregate node
  * ----------------
@@ -1314,6 +1379,44 @@ typedef struct WindowAgg
 	/* nulls sort first for in_range tests? */
 	bool		inRangeNullsFirst;
 
+	/* Row Pattern Recognition AFTER MATCH SKIP clause */
+	RPSkipTo	rpSkipTo;		/* Row Pattern Skip To type */
+
+	/* Compiled Row Pattern for NFA execution */
+	struct RPRPattern *rpPattern;
+
+	/* Row Pattern DEFINE clause (list of TargetEntry) */
+	List	   *defineClause;
+
+	/*
+	 * 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/optimizer/rpr.h b/src/include/optimizer/rpr.h
new file mode 100644
index 00000000000..0a14cfad79b
--- /dev/null
+++ b/src/include/optimizer/rpr.h
@@ -0,0 +1,65 @@
+/*-------------------------------------------------------------------------
+ *
+ * rpr.h
+ *	  Row Pattern Recognition pattern compilation for planner
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/optimizer/rpr.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef OPTIMIZER_RPR_H
+#define OPTIMIZER_RPR_H
+
+#include "nodes/parsenodes.h"
+#include "nodes/plannodes.h"
+
+/* 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.
+ */
+#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 */
+#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) */
+
+/* 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 */
+
+/* Element flags */
+#define RPR_ELEM_RELUCTANT			0x01	/* reluctant (non-greedy)
+											 * quantifier */
+#define RPR_ELEM_EMPTY_LOOP			0x02	/* END: group body can produce
+											 * empty match */
+#define RPR_ELEM_ABSORBABLE_BRANCH	0x04	/* element in absorbable region */
+#define RPR_ELEM_ABSORBABLE			0x08	/* absorption judgment point */
+
+/* Accessor macros for RPRPatternElement */
+#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)
+#define RPRElemIsAlt(e)			((e)->varId == RPR_VARID_ALT)
+#define RPRElemIsFin(e)			((e)->varId == RPR_VARID_FIN)
+#define RPRElemCanSkip(e)		((e)->min == 0)
+
+extern List *collectPatternVariables(RPRPatternNode *pattern);
+extern void buildDefineVariableList(List *defineClause,
+									List **defineVariableList);
+extern RPRPattern *buildRPRPattern(RPRPatternNode *pattern, List *defineVariableList,
+								   RPSkipTo rpSkipTo, int frameOptions,
+								   bool hasMatchStartDependent);
+
+#endif							/* OPTIMIZER_RPR_H */
-- 
2.43.0



  [application/octet-stream] v47-0005-Row-pattern-recognition-patch-executor-and-comma.patch (206.6K, 6-v47-0005-Row-pattern-recognition-patch-executor-and-comma.patch)
  download | inline diff:
From 9a401a09b56b18bd08ca576ba44b4017be231fc2 Mon Sep 17 00:00:00 2001
From: Tatsuo Ishii <[email protected]>
Date: Sat, 2 May 2026 13:40:29 +0900
Subject: [PATCH v47 5/9] Row pattern recognition patch (executor and
 commands).

---
 src/backend/commands/explain.c        |  522 ++++++++
 src/backend/executor/Makefile         |    1 +
 src/backend/executor/README.rpr       | 1578 ++++++++++++++++++++++
 src/backend/executor/execExpr.c       |   92 ++
 src/backend/executor/execExprInterp.c |  267 ++++
 src/backend/executor/execRPR.c        | 1772 +++++++++++++++++++++++++
 src/backend/executor/meson.build      |    1 +
 src/backend/executor/nodeWindowAgg.c  | 1066 ++++++++++++++-
 src/backend/jit/llvm/llvmjit_expr.c   |   78 +-
 src/backend/jit/llvm/llvmjit_types.c  |    2 +
 src/backend/utils/adt/windowfuncs.c   |  119 +-
 src/include/catalog/pg_proc.dat       |   24 +
 src/include/executor/execExpr.h       |   20 +
 src/include/executor/execRPR.h        |   40 +
 src/include/executor/nodeWindowAgg.h  |    3 +
 src/include/nodes/execnodes.h         |  130 ++
 16 files changed, 5703 insertions(+), 12 deletions(-)
 create mode 100644 src/backend/executor/README.rpr
 create mode 100644 src/backend/executor/execRPR.c
 create mode 100644 src/include/executor/execRPR.h

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 112c17b0d64..99de36b57f2 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -30,6 +30,7 @@
 #include "nodes/extensible.h"
 #include "nodes/makefuncs.h"
 #include "nodes/nodeFuncs.h"
+#include "optimizer/rpr.h"
 #include "parser/analyze.h"
 #include "parser/parsetree.h"
 #include "rewrite/rewriteHandler.h"
@@ -119,6 +120,20 @@ static void show_window_def(WindowAggState *planstate,
 static void show_window_keys(StringInfo buf, PlanState *planstate,
 							 int nkeys, AttrNumber *keycols,
 							 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 void show_storage_info(char *maxStorageType, int64 maxSpaceUsed,
 							  ExplainState *es);
 static void show_tablesample(TableSampleClause *tsc, PlanState *planstate,
@@ -129,6 +144,7 @@ static void show_incremental_sort_info(IncrementalSortState *incrsortstate,
 static void show_hash_info(HashState *hashstate, ExplainState *es);
 static void show_material_info(MaterialState *mstate, ExplainState *es);
 static void show_windowagg_info(WindowAggState *winstate, ExplainState *es);
+static void show_rpr_nfa_stats(WindowAggState *winstate, ExplainState *es);
 static void show_ctescan_info(CteScanState *ctescanstate, ExplainState *es);
 static void show_table_func_scan_info(TableFuncScanState *tscanstate,
 									  ExplainState *es);
@@ -2898,6 +2914,284 @@ show_sortorder_options(StringInfo buf, Node *sortexpr,
 	}
 }
 
+/*
+ * Append quantifier suffix for a pattern element.
+ */
+static void
+append_rpr_quantifier(StringInfo buf, RPRPatternElement *elem)
+{
+	/* Append quantifier if not {1,1} */
+	if (elem->min == 0 && elem->max == RPR_QUANTITY_INF)
+		appendStringInfoChar(buf, '*');
+	else if (elem->min == 1 && elem->max == RPR_QUANTITY_INF)
+		appendStringInfoChar(buf, '+');
+	else if (elem->min == 0 && elem->max == 1)
+		appendStringInfoChar(buf, '?');
+	else if (elem->max == RPR_QUANTITY_INF)
+		appendStringInfo(buf, "{%d,}", elem->min);
+	else if (elem->min == elem->max && elem->min != 1)
+		appendStringInfo(buf, "{%d}", elem->min);
+	else if (elem->min != 1 || elem->max != 1)
+		appendStringInfo(buf, "{%d,%d}", elem->min, elem->max);
+
+	if (RPRElemIsReluctant(elem))
+	{
+		if (elem->min == 1 && elem->max == 1)
+			appendStringInfo(buf, "{1}");	/* make reluctant ? unambiguous */
+		appendStringInfoChar(buf, '?');
+	}
+
+	/* Append absorption markers: " for judgment point, ' for branch only */
+	if (RPRElemIsAbsorbable(elem))
+	{
+		Assert(elem->max == RPR_QUANTITY_INF);
+		appendStringInfoChar(buf, '"');
+	}
+	else if (RPRElemIsAbsorbableBranch(elem))
+		appendStringInfoChar(buf, '\'');
+}
+
+/*
+ * Deparse a compiled RPRPattern (bytecode) back to 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.
+ */
+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--;
+	}
+
+	return buf.data;
+}
+
+/*
+ * Process pattern elements sequentially until FIN or END at groupDepth.
+ *
+ * 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).
+ */
+static void
+deparse_rpr_elements(RPRPattern *pattern, int *idx, StringInfoData *buf,
+					 RPRDepth groupDepth, RPRDepth *prevDepth,
+					 bool *needSpace)
+{
+	List	   *altSeps = NIL;	/* pending alternation separator indices */
+
+	while (*idx < pattern->numElements)
+	{
+		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);
+}
+
+/*
+ * Process a BEGIN...END group.
+ *
+ * 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))+").
+ */
+static void
+deparse_rpr_group(RPRPattern *pattern, int *idx, StringInfoData *buf,
+				  RPRDepth *prevDepth, bool *needSpace)
+{
+	RPRPatternElement *begin = &pattern->elements[*idx];
+	RPRDepth	childDepth = begin->depth + 1;
+	bool		singleAlt = false;
+	RPRPatternElement *end;
+
+	/*
+	 * 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]))
+	{
+		int			j;
+
+		singleAlt = true;
+		for (j = *idx + 2; j < pattern->numElements; j++)
+		{
+			RPRPatternElement *e = &pattern->elements[j];
+
+			if (RPRElemIsEnd(e) && e->depth == begin->depth)
+				break;
+			if (e->depth <= childDepth)
+			{
+				singleAlt = false;
+				break;
+			}
+		}
+	}
+
+	/* Open group paren (unless single ALT provides it) */
+	if (!singleAlt)
+	{
+		if (*needSpace)
+			appendStringInfoChar(buf, ' ');
+		appendStringInfoChar(buf, '(');
+		*needSpace = false;
+	}
+	*prevDepth = childDepth;
+	(*idx)++;					/* consume BEGIN */
+
+	/* Process group children; stops at matching END */
+	deparse_rpr_elements(pattern, idx, buf, begin->depth,
+						 prevDepth, needSpace);
+
+	/* Consume END and output quantifier */
+	Assert(*idx < pattern->numElements);
+	end = &pattern->elements[*idx];
+	Assert(RPRElemIsEnd(end) && end->depth == begin->depth);
+
+	while (*prevDepth > end->depth + 1)
+	{
+		appendStringInfoChar(buf, ')');
+		(*prevDepth)--;
+	}
+	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.
+ */
+static void
+deparse_rpr_alt(RPRPattern *pattern, int *idx, StringInfoData *buf,
+				RPRDepth *prevDepth, bool *needSpace, List **altSeps)
+{
+	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;
+	}
+
+	/* Register next alternation separator position */
+	if (elem->next != RPR_ELEMIDX_INVALID)
+	{
+		RPRPatternElement *firstElem = &pattern->elements[elem->next];
+
+		if (firstElem->jump != RPR_ELEMIDX_INVALID)
+			*altSeps = lappend_int(*altSeps, firstElem->jump);
+	}
+	if (elem->jump != RPR_ELEMIDX_INVALID)
+		*altSeps = lappend_int(*altSeps, elem->jump);
+	(*idx)++;
+}
+
+/*
+ * Process a VAR element: adjust depth parens and output variable name.
+ */
+static void
+deparse_rpr_var(RPRPattern *pattern, int *idx, StringInfoData *buf,
+				RPRDepth *prevDepth, bool *needSpace, List **altSeps)
+{
+	RPRPatternElement *elem = &pattern->elements[*idx];
+
+	/* Open parens for depth increase */
+	while (*prevDepth < elem->depth)
+	{
+		if (*needSpace)
+			appendStringInfoChar(buf, ' ');
+		appendStringInfoChar(buf, '(');
+		(*prevDepth)++;
+		*needSpace = false;
+	}
+
+	/* Close parens for depth decrease */
+	while (*prevDepth > elem->depth)
+	{
+		appendStringInfoChar(buf, ')');
+		(*prevDepth)--;
+	}
+
+	if (*needSpace)
+		appendStringInfoChar(buf, ' ');
+
+	Assert(elem->varId < pattern->numVars);
+	appendStringInfoString(buf, quote_identifier(pattern->varNames[elem->varId]));
+	append_rpr_quantifier(buf, elem);
+	*needSpace = true;
+
+	if (elem->jump != RPR_ELEMIDX_INVALID)
+		*altSeps = lappend_int(*altSeps, elem->jump);
+	(*idx)++;
+}
+
 /*
  * Show the window definition for a WindowAgg node.
  */
@@ -2956,6 +3250,79 @@ show_window_def(WindowAggState *planstate, List *ancestors, ExplainState *es)
 	appendStringInfoChar(&wbuf, ')');
 	ExplainPropertyText("Window", wbuf.data, es);
 	pfree(wbuf.data);
+
+	/* Show Row Pattern Recognition pattern if present */
+	if (wagg->rpPattern != NULL)
+	{
+		char	   *patternStr = deparse_rpr_pattern(wagg->rpPattern);
+
+		if (patternStr != NULL)
+		{
+			ExplainPropertyText("Pattern", patternStr, es);
+			pfree(patternStr);
+		}
+
+		/*
+		 * Show navigation offsets for tuplestore trim.  For EXPLAIN ANALYZE,
+		 * use the executor-resolved values (which may differ from the plan
+		 * when NEEDS_EVAL was resolved to FIXED or RETAIN_ALL at init).
+		 */
+		{
+			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;
+				}
+			}
+		}
+	}
 }
 
 /*
@@ -3508,6 +3875,7 @@ show_windowagg_info(WindowAggState *winstate, ExplainState *es)
 {
 	char	   *maxStorageType;
 	int64		maxSpaceUsed;
+	WindowAgg  *wagg = (WindowAgg *) winstate->ss.ps.plan;
 
 	Tuplestorestate *tupstore = winstate->buffer;
 
@@ -3520,6 +3888,160 @@ show_windowagg_info(WindowAggState *winstate, ExplainState *es)
 
 	tuplestore_get_stats(tupstore, &maxStorageType, &maxSpaceUsed);
 	show_storage_info(maxStorageType, maxSpaceUsed, es);
+
+	/* Show NFA statistics for Row Pattern Recognition */
+	if (wagg->rpPattern != NULL)
+		show_rpr_nfa_stats(winstate, es);
+}
+
+/*
+ * Show NFA statistics for Row Pattern Recognition on WindowAgg node.
+ */
+static void
+show_rpr_nfa_stats(WindowAggState *winstate, ExplainState *es)
+{
+	if (es->format != EXPLAIN_FORMAT_TEXT)
+	{
+		/* State and context counters */
+		ExplainPropertyInteger("NFA States Peak", NULL, winstate->nfaStatesMax, es);
+		ExplainPropertyInteger("NFA States Total", NULL, winstate->nfaStatesTotalCreated, es);
+		ExplainPropertyInteger("NFA States Merged", NULL, winstate->nfaStatesMerged, es);
+		ExplainPropertyInteger("NFA Contexts Peak", NULL, winstate->nfaContextsMax, es);
+		ExplainPropertyInteger("NFA Contexts Total", NULL, winstate->nfaContextsTotalCreated, es);
+		ExplainPropertyInteger("NFA Contexts Absorbed", NULL, winstate->nfaContextsAbsorbed, es);
+		ExplainPropertyInteger("NFA Contexts Skipped", NULL, winstate->nfaContextsSkipped, es);
+		ExplainPropertyInteger("NFA Contexts Pruned", NULL, winstate->nfaContextsPruned, es);
+
+		/* Match/mismatch counts and length statistics */
+		ExplainPropertyInteger("NFA Matched", NULL, winstate->nfaMatchesSucceeded, es);
+		ExplainPropertyInteger("NFA Mismatched", NULL, winstate->nfaMatchesFailed, es);
+		if (winstate->nfaMatchesSucceeded > 0)
+		{
+			ExplainPropertyInteger("NFA Match Length Min", NULL, winstate->nfaMatchLen.min, es);
+			ExplainPropertyInteger("NFA Match Length Max", NULL, winstate->nfaMatchLen.max, es);
+			ExplainPropertyFloat("NFA Match Length Avg", NULL,
+								 (double) winstate->nfaMatchLen.total / winstate->nfaMatchesSucceeded, 1,
+								 es);
+		}
+		if (winstate->nfaMatchesFailed > 0)
+		{
+			ExplainPropertyInteger("NFA Mismatch Length Min", NULL, winstate->nfaFailLen.min, es);
+			ExplainPropertyInteger("NFA Mismatch Length Max", NULL, winstate->nfaFailLen.max, es);
+			ExplainPropertyFloat("NFA Mismatch Length Avg", NULL,
+								 (double) winstate->nfaFailLen.total / winstate->nfaMatchesFailed, 1,
+								 es);
+		}
+
+		/* Absorbed/skipped context length statistics */
+		if (winstate->nfaContextsAbsorbed > 0)
+		{
+			ExplainPropertyInteger("NFA Absorbed Length Min", NULL, winstate->nfaAbsorbedLen.min, es);
+			ExplainPropertyInteger("NFA Absorbed Length Max", NULL, winstate->nfaAbsorbedLen.max, es);
+			ExplainPropertyFloat("NFA Absorbed Length Avg", NULL,
+								 (double) winstate->nfaAbsorbedLen.total / winstate->nfaContextsAbsorbed, 1,
+								 es);
+		}
+		if (winstate->nfaContextsSkipped > 0)
+		{
+			ExplainPropertyInteger("NFA Skipped Length Min", NULL, winstate->nfaSkippedLen.min, es);
+			ExplainPropertyInteger("NFA Skipped Length Max", NULL, winstate->nfaSkippedLen.max, es);
+			ExplainPropertyFloat("NFA Skipped Length Avg", NULL,
+								 (double) winstate->nfaSkippedLen.total / winstate->nfaContextsSkipped, 1,
+								 es);
+		}
+	}
+	else
+	{
+		/* State and context counters */
+		ExplainIndentText(es);
+		appendStringInfo(es->str,
+						 "NFA States: " INT64_FORMAT " peak, " INT64_FORMAT " total, " INT64_FORMAT " merged\n",
+						 winstate->nfaStatesMax,
+						 winstate->nfaStatesTotalCreated,
+						 winstate->nfaStatesMerged);
+		ExplainIndentText(es);
+		appendStringInfo(es->str,
+						 "NFA Contexts: " INT64_FORMAT " peak, " INT64_FORMAT " total, " INT64_FORMAT " pruned\n",
+						 winstate->nfaContextsMax,
+						 winstate->nfaContextsTotalCreated,
+						 winstate->nfaContextsPruned);
+
+		/* Match/mismatch counts with length min/max/avg */
+		ExplainIndentText(es);
+		appendStringInfo(es->str, "NFA: ");
+		if (winstate->nfaMatchesSucceeded > 0)
+		{
+			double		avgLen = (double) winstate->nfaMatchLen.total / winstate->nfaMatchesSucceeded;
+
+			appendStringInfo(es->str,
+							 INT64_FORMAT " matched (len " INT64_FORMAT "/" INT64_FORMAT "/%.1f)",
+							 winstate->nfaMatchesSucceeded,
+							 winstate->nfaMatchLen.min,
+							 winstate->nfaMatchLen.max,
+							 avgLen);
+		}
+		else
+		{
+			appendStringInfo(es->str, "0 matched");
+		}
+		if (winstate->nfaMatchesFailed > 0)
+		{
+			double		avgFail = (double) winstate->nfaFailLen.total / winstate->nfaMatchesFailed;
+
+			appendStringInfo(es->str,
+							 ", " INT64_FORMAT " mismatched (len " INT64_FORMAT "/" INT64_FORMAT "/%.1f)",
+							 winstate->nfaMatchesFailed,
+							 winstate->nfaFailLen.min,
+							 winstate->nfaFailLen.max,
+							 avgFail);
+		}
+		else
+		{
+			appendStringInfo(es->str, ", 0 mismatched");
+		}
+		appendStringInfoChar(es->str, '\n');
+
+		/* Absorbed/skipped context length statistics */
+		if (winstate->nfaContextsAbsorbed > 0 || winstate->nfaContextsSkipped > 0)
+		{
+			ExplainIndentText(es);
+			appendStringInfo(es->str, "NFA: ");
+
+			if (winstate->nfaContextsAbsorbed > 0)
+			{
+				double		avgAbsorbed = (double) winstate->nfaAbsorbedLen.total / winstate->nfaContextsAbsorbed;
+
+				appendStringInfo(es->str,
+								 INT64_FORMAT " absorbed (len " INT64_FORMAT "/" INT64_FORMAT "/%.1f)",
+								 winstate->nfaContextsAbsorbed,
+								 winstate->nfaAbsorbedLen.min,
+								 winstate->nfaAbsorbedLen.max,
+								 avgAbsorbed);
+			}
+			else
+			{
+				appendStringInfo(es->str, "0 absorbed");
+			}
+
+			if (winstate->nfaContextsSkipped > 0)
+			{
+				double		avgSkipped = (double) winstate->nfaSkippedLen.total / winstate->nfaContextsSkipped;
+
+				appendStringInfo(es->str,
+								 ", " INT64_FORMAT " skipped (len " INT64_FORMAT "/" INT64_FORMAT "/%.1f)",
+								 winstate->nfaContextsSkipped,
+								 winstate->nfaSkippedLen.min,
+								 winstate->nfaSkippedLen.max,
+								 avgSkipped);
+			}
+			else
+			{
+				appendStringInfo(es->str, ", 0 skipped");
+			}
+
+			appendStringInfoChar(es->str, '\n');
+		}
+	}
 }
 
 /*
diff --git a/src/backend/executor/Makefile b/src/backend/executor/Makefile
index 11118d0ce02..2b257427795 100644
--- a/src/backend/executor/Makefile
+++ b/src/backend/executor/Makefile
@@ -25,6 +25,7 @@ OBJS = \
 	execParallel.o \
 	execPartition.o \
 	execProcnode.o \
+	execRPR.o \
 	execReplication.o \
 	execSRF.o \
 	execScan.o \
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/execExpr.c b/src/backend/executor/execExpr.c
index 77229141b38..8d8da67e79f 100644
--- a/src/backend/executor/execExpr.c
+++ b/src/backend/executor/execExpr.c
@@ -1222,6 +1222,98 @@ ExecInitExprRec(Expr *node, ExprState *state,
 				break;
 			}
 
+		case T_RPRNavExpr:
+			{
+				/*
+				 * 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;
+
+				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.kind = nav->kind;
+
+				if (nav->kind >= RPR_NAV_PREV_FIRST)
+				{
+					/*
+					 * 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);
+
+					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;
+				}
+
+				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.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;
+			}
+
 		case T_FuncExpr:
 			{
 				FuncExpr   *func = (FuncExpr *) node;
diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c
index 0634af964a9..324b9a962a8 100644
--- a/src/backend/executor/execExprInterp.c
+++ b/src/backend/executor/execExprInterp.c
@@ -57,11 +57,13 @@
 #include "postgres.h"
 
 #include "access/heaptoast.h"
+#include "common/int.h"
 #include "access/tupconvert.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"
@@ -586,6 +588,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,
@@ -2013,6 +2017,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)
 		{
@@ -5988,3 +6010,248 @@ ExecAggPlainTransByRef(AggState *aggstate, AggStatePerTrans pertrans,
 
 	MemoryContextSwitchTo(oldContext);
 }
+
+/*
+ * 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
+ * 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 inner offset.  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 and compound: 0 for inner, 1 for outer
+	 */
+	if (op->d.rpr_nav.offset_value != NULL)
+	{
+		if (*op->d.rpr_nav.offset_isnull)
+			ereport(ERROR,
+					(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
+					 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("row pattern navigation offset must not be negative")));
+	}
+	else
+	{
+		/* 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,
+	 * 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))
+				target_pos = -1;
+			break;
+		case RPR_NAV_NEXT:
+			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;
+	}
+
+	/*
+	 * 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);
+
+	/*
+	 * 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 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.
+ *
+ * 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,
+					  ExprContext *econtext)
+{
+	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/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
new file mode 100644
index 00000000000..242ae9c6dcf
--- /dev/null
+++ b/src/backend/executor/execRPR.c
@@ -0,0 +1,1772 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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 "common/int.h"
+#include "executor/execRPR.h"
+#include "executor/executor.h"
+#include "miscadmin.h"
+#include "optimizer/rpr.h"
+#include "utils/memutils.h"
+
+/*
+ * 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) */
+#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.
+ *   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:
+ * ---------------------------
+ * 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++;
+	winstate->nfaStatesMax = Max(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;
+
+	/* 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)
+	{
+		CHECK_FOR_INTERRUPTS();
+
+		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)
+	{
+		int64		skippedLen;
+
+		while (ctx->next != NULL &&
+			   ctx->next->matchStartRow <= matchEndRow)
+		{
+			RPRNFAContext *nextCtx = ctx->next;
+
+			Assert(nextCtx->lastProcessedRow >= nextCtx->matchStartRow);
+			skippedLen = nextCtx->lastProcessedRow - nextCtx->matchStartRow + 1;
+			nfa_record_context_skipped(winstate, skippedLen);
+
+			ExecRPRFreeContext(winstate, nextCtx);
+		}
+	}
+}
+
+/*
+ * 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++;
+	winstate->nfaContextsMax = Max(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
+	{
+		stats->min = Min(stats->min, newLen);
+		stats->max = 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)
+	{
+		CHECK_FOR_INTERRUPTS();
+
+		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;
+
+		/*
+		 * 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();
+
+			/* 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)
+	{
+		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
+		 * 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.
+ *
+ * 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,
+				   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 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.
+ */
+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 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)
+	{
+		RPRPatternElement *elem = &elements[state->elemIdx];
+
+		CHECK_FOR_INTERRUPTS();
+
+		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 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.
+				 *
+				 * 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) &&
+					count >= elem->max &&
+					RPRElemIsEnd(&elements[elem->next]))
+				{
+					RPRPatternElement *endElem = &elements[elem->next];
+					int			endDepth = endElem->depth;
+					int32		endCount = state->counts[endDepth];
+
+					/* Increment group count */
+					if (endCount < RPR_COUNT_MAX)
+						endCount++;
+					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 */
+			}
+			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;
+
+		/*----------
+		 * 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);
+
+		/* 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,
+						  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;
+
+			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];
+
+		/* 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]++;
+
+		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 first-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];
+
+			/*
+			 * 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
+			 * 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];
+
+		/*
+		 * 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);
+	}
+}
+
+/*
+ * 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);
+
+	/* 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)))
+	{
+		nfa_state_free(winstate, state);
+		return;
+	}
+
+	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:
+			/* 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;
+	RPRNFAState *savedMatchedState;
+
+	ctx->states = NULL;			/* Will rebuild */
+
+	/* Process each state in lexical order (DFS order from previous advance) */
+	while (states != NULL)
+	{
+		CHECK_FOR_INTERRUPTS();
+		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);
+	}
+}
+
+/*
+ * 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;
+}
+
+/*
+ * 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;
+	bool		hasDependent = !bms_is_empty(winstate->defineMatchStartDependent);
+
+	/* 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.
+	 */
+	for (ctx = winstate->nfaContext; ctx != NULL; ctx = ctx->next)
+	{
+		if (ctx->states == NULL)
+			continue;
+
+		/* Check frame boundary - finalize if exceeded */
+		if (hasLimitedFrame)
+		{
+			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)
+			{
+				/* Frame boundary exceeded: force mismatch */
+				nfa_match(winstate, ctx, NULL);
+				continue;
+			}
+		}
+
+		/*
+		 * 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);
+		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 ||
+			   ctx->matchStartRow > PG_INT64_MAX - frameOffset - 1 ||
+			   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)
+	{
+		CHECK_FOR_INTERRUPTS();
+
+		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)
+	{
+		CHECK_FOR_INTERRUPTS();
+
+		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 f52a7aae843..93cb9bbdd11 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -36,15 +36,19 @@
 #include "access/htup_details.h"
 #include "catalog/objectaccess.h"
 #include "catalog/pg_aggregate.h"
+#include "common/int.h"
 #include "catalog/pg_proc.h"
 #include "common/int.h"
 #include "executor/executor.h"
+#include "executor/execRPR.h"
 #include "executor/instrument.h"
 #include "executor/nodeWindowAgg.h"
 #include "miscadmin.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/plannodes.h"
 #include "optimizer/clauses.h"
 #include "optimizer/optimizer.h"
+#include "optimizer/rpr.h"
 #include "parser/parse_agg.h"
 #include "parser/parse_coerce.h"
 #include "utils/acl.h"
@@ -173,6 +177,7 @@ typedef struct WindowStatePerAggData
 	bool		restart;		/* need to restart this agg in this cycle? */
 } WindowStatePerAggData;
 
+
 static void initialize_windowaggregate(WindowAggState *winstate,
 									   WindowStatePerFunc perfuncstate,
 									   WindowStatePerAgg peraggstate);
@@ -209,6 +214,9 @@ static Datum GetAggInitVal(Datum textInitVal, Oid transtype);
 
 static bool are_peers(WindowAggState *winstate, TupleTableSlot *slot1,
 					  TupleTableSlot *slot2);
+static int	WinGetSlotInFrame(WindowObject winobj, TupleTableSlot *slot,
+							  int relpos, int seektype, bool set_mark,
+							  bool *isnull, bool *isout);
 static bool window_gettupleslot(WindowObject winobj, int64 pos,
 								TupleTableSlot *slot);
 
@@ -227,6 +235,19 @@ 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 bool rpr_is_defined(WindowAggState *winstate);
+static int64 row_is_in_reduced_frame(WindowObject winobj, int64 pos);
+
+static void clear_reduced_frame(WindowAggState *winstate);
+static int	get_reduced_frame_status(WindowAggState *winstate, int64 pos);
+static void 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 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
@@ -820,6 +841,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 (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
@@ -834,7 +858,8 @@ eval_windowaggregates(WindowAggState *winstate)
 			(winstate->aggregatedbase != winstate->frameheadpos &&
 			 !OidIsValid(peraggstate->invtransfn_oid)) ||
 			(winstate->frameOptions & FRAMEOPTION_EXCLUSION) ||
-			winstate->aggregatedupto <= winstate->frameheadpos)
+			winstate->aggregatedupto <= winstate->frameheadpos ||
+			rpr_is_defined(winstate))
 		{
 			peraggstate->restart = true;
 			numaggs_restart++;
@@ -963,6 +988,14 @@ eval_windowaggregates(WindowAggState *winstate)
 	{
 		winstate->aggregatedupto = winstate->frameheadpos;
 		ExecClearTuple(agg_row_slot);
+
+		/*
+		 * If RPR is defined, we do not use aggregatedupto_nonrestarted.  To
+		 * avoid assertion failure below, we reset aggregatedupto_nonrestarted
+		 * to frameheadpos.
+		 */
+		if (rpr_is_defined(winstate))
+			aggregatedupto_nonrestarted = winstate->frameheadpos;
 	}
 
 	/*
@@ -974,7 +1007,7 @@ eval_windowaggregates(WindowAggState *winstate)
 	 */
 	for (;;)
 	{
-		int			ret;
+		int64		ret;
 
 		/* Fetch next row if we didn't already */
 		if (TupIsNull(agg_row_slot))
@@ -992,9 +1025,40 @@ eval_windowaggregates(WindowAggState *winstate)
 							  agg_row_slot, false);
 		if (ret < 0)
 			break;
+
 		if (ret == 0)
 			goto next_tuple;
 
+		if (rpr_is_defined(winstate))
+		{
+			/*
+			 * If currentpos is already decided but aggregatedupto is not yet
+			 * determined, we've passed the last reduced frame.
+			 */
+			if (get_reduced_frame_status(winstate, winstate->currentpos)
+				!= RF_NOT_DETERMINED &&
+				get_reduced_frame_status(winstate, winstate->aggregatedupto)
+				== RF_NOT_DETERMINED)
+				break;
+
+			/*
+			 * Calculate the reduced frame for aggregatedupto.
+			 */
+			ret = row_is_in_reduced_frame(winstate->agg_winobj,
+										  winstate->aggregatedupto);
+			if (ret == -1)		/* unmatched row */
+				break;
+
+			/*
+			 * 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_status(winstate,
+										 winstate->aggregatedupto) == RF_SKIPPED &&
+				winstate->aggregatedupto == winstate->aggregatedbase)
+				break;
+		}
+
 		/* Set tuple context for evaluation of aggregate arguments */
 		winstate->tmpcontext->ecxt_outertuple = agg_row_slot;
 
@@ -1023,6 +1087,7 @@ next_tuple:
 		ExecClearTuple(agg_row_slot);
 	}
 
+
 	/* The frame's end is not supposed to move backwards, ever */
 	Assert(aggregatedupto_nonrestarted <= winstate->aggregatedupto);
 
@@ -1190,6 +1255,28 @@ prepare_tuplestore(WindowAggState *winstate)
 		}
 	}
 
+	/* Create read/mark pointers for RPR navigation if needed */
+	if (winstate->nav_winobj)
+	{
+		/*
+		 * Allocate mark and read pointers for RPR navigation.
+		 *
+		 * 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.
+		 *
+		 * 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);
+		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
@@ -1246,6 +1333,8 @@ begin_partition(WindowAggState *winstate)
 	winstate->framehead_valid = false;
 	winstate->frametail_valid = false;
 	winstate->grouptail_valid = false;
+	if (rpr_is_defined(winstate))
+		clear_reduced_frame(winstate);
 	winstate->spooled_rows = 0;
 	winstate->currentpos = 0;
 	winstate->frameheadpos = 0;
@@ -1299,6 +1388,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++)
 	{
@@ -1467,6 +1563,18 @@ release_partition(WindowAggState *winstate)
 		tuplestore_clear(winstate->buffer);
 	winstate->partition_spooled = false;
 	winstate->next_partition = true;
+
+	/* Reset RPR match results */
+	clear_reduced_frame(winstate);
+
+	/* Reset NFA state for new partition */
+	winstate->nfaContext = NULL;
+	winstate->nfaContextTail = NULL;
+	winstate->nfaContextFree = NULL;
+	winstate->nfaStateFree = NULL;
+	winstate->nfaLastProcessedRow = -1;
+	winstate->nfaStatesActive = 0;
+	winstate->nfaContextsActive = 0;
 }
 
 /*
@@ -2395,6 +2503,16 @@ ExecWindowAgg(PlanState *pstate)
 		/* don't evaluate the window functions when we're in pass-through mode */
 		if (winstate->status == WINDOWAGG_RUN)
 		{
+			/*
+			 * 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(winstate);
+			}
+
 			/*
 			 * Evaluate true window functions
 			 */
@@ -2434,6 +2552,45 @@ ExecWindowAgg(PlanState *pstate)
 		if (winstate->grouptail_ptr >= 0)
 			update_grouptailpos(winstate);
 
+		/*
+		 * Advance RPR navigation mark pointer if possible, so that
+		 * tuplestore_trim() can free rows no longer reachable by navigation.
+		 */
+		if (winstate->nav_winobj &&
+			winstate->rpPattern != NULL &&
+			winstate->navMaxOffsetKind == RPR_NAV_OFFSET_FIXED)
+		{
+			int64		navmarkpos;
+
+			/* Backward reach from PREV/LAST/compound PREV_LAST/NEXT_LAST */
+			if (winstate->currentpos > winstate->navMaxOffset)
+				navmarkpos = winstate->currentpos - winstate->navMaxOffset;
+			else
+				navmarkpos = 0;
+
+			/*
+			 * If FIRST is used, also consider match_start + navFirstOffset.
+			 * The oldest active context (nfaContext) has the smallest
+			 * matchStartRow.
+			 */
+			if (winstate->hasFirstNav &&
+				winstate->navFirstOffsetKind == RPR_NAV_OFFSET_FIXED &&
+				winstate->nfaContext != NULL)
+			{
+				int64		firstreach;
+
+				if (winstate->navFirstOffset > -winstate->nfaContext->matchStartRow)
+					firstreach = winstate->nfaContext->matchStartRow
+						+ winstate->navFirstOffset;
+				else
+					firstreach = 0;
+				navmarkpos = Min(navmarkpos, firstreach);
+			}
+
+			if (navmarkpos > winstate->nav_winobj->markpos)
+				WinSetMarkPosition(winstate->nav_winobj, navmarkpos);
+		}
+
 		/*
 		 * Truncate any no-longer-needed rows from the tuplestore.
 		 */
@@ -2659,6 +2816,20 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
 	winstate->temp_slot_2 = ExecInitExtraTupleSlot(estate, scanDesc,
 												   &TTSOpsMinimalTuple);
 
+	if (node->rpPattern != NULL)
+	{
+		winstate->nav_slot = ExecInitExtraTupleSlot(estate, scanDesc,
+													&TTSOpsMinimalTuple);
+		winstate->nav_slot_pos = -1;
+
+		winstate->nav_null_slot = ExecInitExtraTupleSlot(estate, scanDesc,
+														 &TTSOpsMinimalTuple);
+		winstate->nav_null_slot = ExecStoreAllNullTuple(winstate->nav_null_slot);
+
+		winstate->nav_saved_outertuple = NULL;
+		winstate->nav_match_start = 0;
+	}
+
 	/*
 	 * create frame head and tail slots only if needed (must create slots in
 	 * exactly the same cases that update_frameheadpos and update_frametailpos
@@ -2827,6 +2998,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;
 
@@ -2845,6 +3033,81 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
 	winstate->inRangeAsc = node->inRangeAsc;
 	winstate->inRangeNullsFirst = node->inRangeNullsFirst;
 
+	/* Set up SKIP TO type */
+	winstate->rpSkipTo = node->rpSkipTo;
+	/* Set up row pattern recognition PATTERN clause (compiled NFA) */
+	winstate->rpPattern = node->rpPattern;
+	/* Set up nav offsets for tuplestore trim */
+	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);
+
+	/* 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)
+	{
+		winstate->nfaStateSize = offsetof(RPRNFAState, counts) +
+			sizeof(int32) * node->rpPattern->maxDepth;
+		winstate->nfaVisitedNWords =
+			(node->rpPattern->numElements - 1) / BITS_PER_BITMAPWORD + 1;
+		winstate->nfaVisitedElems = palloc0(sizeof(bitmapword) *
+											winstate->nfaVisitedNWords);
+	}
+
+	/* Set up row pattern recognition DEFINE clause */
+	winstate->defineVariableList = NIL;
+	winstate->defineClauseList = NIL;
+	if (node->defineClause != NIL)
+	{
+		/*
+		 * Compile DEFINE clause expressions.  PREV/NEXT navigation is handled
+		 * by EEOP_RPR_NAV_SET/RESTORE opcodes emitted during ExecInitExpr, so
+		 * no varno rewriting is needed here.
+		 */
+		foreach(l, node->defineClause)
+		{
+			TargetEntry *te = lfirst(l);
+			char	   *name = te->resname;
+			Expr	   *expr = te->expr;
+			ExprState  *exps;
+
+			winstate->defineVariableList =
+				lappend(winstate->defineVariableList,
+						makeString(pstrdup(name)));
+			exps = ExecInitExpr(expr, (PlanState *) winstate);
+			winstate->defineClauseList =
+				lappend(winstate->defineClauseList, exps);
+		}
+	}
+
+	/* Initialize NFA free lists for row pattern matching */
+	winstate->nfaContext = NULL;
+	winstate->nfaContextTail = NULL;
+	winstate->nfaContextFree = NULL;
+	winstate->nfaStateFree = NULL;
+	winstate->nfaLastProcessedRow = -1;
+	winstate->nfaStatesActive = 0;
+	winstate->nfaContextsActive = 0;
+
+	/*
+	 * Allocate varMatched array for NFA evaluation. With the new varNames
+	 * ordering (DEFINE order first), varId == defineIdx for all defined
+	 * variables, so no mapping is needed.
+	 */
+	if (list_length(winstate->defineVariableList) > 0)
+		winstate->nfaVarMatched = palloc0(sizeof(bool) *
+										  list_length(winstate->defineVariableList));
+	else
+		winstate->nfaVarMatched = NULL;
 	winstate->all_first = true;
 	winstate->partition_spooled = false;
 	winstate->more_partitions = false;
@@ -2853,6 +3116,42 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
 	return winstate;
 }
 
+/*
+ * 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.
+ */
+TupleTableSlot *
+ExecRPRNavGetSlot(WindowAggState *winstate, int64 pos)
+{
+	WindowObject winobj = winstate->nav_winobj;
+	TupleTableSlot *slot = winstate->nav_slot;
+
+	if (pos < 0)
+		return winstate->nav_null_slot;
+
+	/*
+	 * 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 (!window_gettupleslot(winobj, pos, slot))
+	{
+		winstate->nav_slot_pos = -1;
+		return winstate->nav_null_slot;
+	}
+
+	winstate->nav_slot_pos = pos;
+	return slot;
+}
+
+
 /* -----------------
  * ExecEndWindowAgg
  * -----------------
@@ -2910,6 +3209,8 @@ ExecReScanWindowAgg(WindowAggState *node)
 	ExecClearTuple(node->agg_row_slot);
 	ExecClearTuple(node->temp_slot_1);
 	ExecClearTuple(node->temp_slot_2);
+	if (node->nav_slot)
+		ExecClearTuple(node->nav_slot);
 	if (node->framehead_slot)
 		ExecClearTuple(node->framehead_slot);
 	if (node->frametail_slot)
@@ -3270,7 +3571,8 @@ window_gettupleslot(WindowObject winobj, int64 pos, TupleTableSlot *slot)
 		return false;
 
 	if (pos < winobj->markpos)
-		elog(ERROR, "cannot fetch row before WindowObject's mark position");
+		elog(ERROR, "cannot fetch row: " INT64_FORMAT " before WindowObject's mark position: " INT64_FORMAT,
+			 pos, winobj->markpos);
 
 	oldcontext = MemoryContextSwitchTo(winstate->ss.ps.ps_ExprContext->ecxt_per_query_memory);
 
@@ -3387,6 +3689,7 @@ ignorenulls_getfuncarginframe(WindowObject winobj, int argno,
 	int			notnull_offset;
 	int			notnull_relpos;
 	int			forward;
+	int64		num_reduced_frame;
 
 	Assert(WindowObjectIsValid(winobj));
 	winstate = winobj->winstate;
@@ -3415,6 +3718,13 @@ ignorenulls_getfuncarginframe(WindowObject winobj, int argno,
 			/* 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;
 			mark_pos = 0;		/* keep compiler quiet */
@@ -3430,6 +3740,35 @@ ignorenulls_getfuncarginframe(WindowObject winobj, int argno,
 	 * Get the next nonnull value in the frame, moving forward or backward
 	 * until we find a value or reach the frame's end.
 	 */
+
+	/*
+	 * Check whether current row is in reduced frame.
+	 */
+	num_reduced_frame = row_is_in_reduced_frame(winobj, winstate->frameheadpos);
+	if (num_reduced_frame < 0)	/* unmatched or skipped row */
+		goto out_of_frame;
+	else if (num_reduced_frame > 0) /* the first row of the reduced frame */
+	{
+		/*
+		 * Early check if row could be out of reduced frame.  When RPR is
+		 * 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.
+		 */
+		if (seektype == WINDOW_SEEK_HEAD && relpos >= num_reduced_frame)
+			goto out_of_frame;
+		if (seektype == WINDOW_SEEK_TAIL)
+		{
+			if (notnull_relpos >= num_reduced_frame)
+				goto out_of_frame;
+
+			/* not out of reduced frame. Set abspos as a starting point */
+			abs_pos = winstate->frameheadpos + num_reduced_frame - 1;
+		}
+	}
+
 	do
 	{
 		int			inframe;
@@ -3491,6 +3830,16 @@ ignorenulls_getfuncarginframe(WindowObject winobj, int argno,
 		}
 advance:
 		abs_pos += forward;
+		if (rpr_is_defined(winstate))
+		{
+			/*
+			 * Check whether we are still in the reduced frame.  (also check
+			 * if we succeeded in getting the target row).
+			 */
+			num_reduced_frame--;
+			if (num_reduced_frame <= 0 && notnull_offset <= notnull_relpos)
+				goto out_of_frame;
+		}
 	} while (notnull_offset <= notnull_relpos);
 
 	if (set_mark)
@@ -3612,6 +3961,631 @@ put_notnull_info(WindowObject winobj, int64 pos, int argno, bool isnull)
 	mbp[bpos] = mb;
 }
 
+/*
+ * 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
+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_LAST)
+			{
+				if (pg_add_s64_overflow(inner, outer, &reach))
+				{
+					context->overflow = true;
+					return false;
+				}
+			}
+			else
+				reach = (inner > outer) ? inner - outer : 0;
+		}
+
+		context->maxOffset = Max(context->maxOffset, reach);
+
+		return false;			/* don't walk into children */
+	}
+
+	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)
+	{
+		TargetEntry *te = (TargetEntry *) lfirst(lc);
+
+		eval_nav_max_offset_walker((Node *) te->expr, &ctx);
+	}
+
+	if (ctx.overflow)
+	{
+		winstate->navMaxOffsetKind = RPR_NAV_OFFSET_RETAIN_ALL;
+		winstate->navMaxOffset = 0;
+	}
+	else
+	{
+		winstate->navMaxOffsetKind = RPR_NAV_OFFSET_FIXED;
+		winstate->navMaxOffset = ctx.maxOffset;
+	}
+}
+
+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)
+		{
+			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;
+			}
+		}
+
+		context->minOffset = Min(context->minOffset, combined);
+
+		return false;
+	}
+
+	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);
+
+		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;
+	}
+}
+
+/*
+ * 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 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 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
+ * -----------------
+ */
+static int64
+row_is_in_reduced_frame(WindowObject winobj, int64 pos)
+{
+	WindowAggState *winstate = winobj->winstate;
+	int			state;
+	int64		rtn;
+
+	if (!rpr_is_defined(winstate))
+	{
+		/*
+		 * RPR is not defined. Assume that we are always in the reduced window
+		 * frame.
+		 */
+		rtn = 0;
+		return rtn;
+	}
+
+	state = get_reduced_frame_status(winstate, pos);
+
+	if (state == RF_NOT_DETERMINED)
+	{
+		update_frameheadpos(winstate);
+		update_reduced_frame(winobj, pos);
+	}
+
+	state = get_reduced_frame_status(winstate, pos);
+
+	switch (state)
+	{
+		case RF_FRAME_HEAD:
+			rtn = winstate->rpr_match_length;
+			break;
+
+		case RF_SKIPPED:
+			rtn = -2;
+			break;
+
+		case RF_UNMATCHED:
+		case RF_EMPTY_MATCH:
+			rtn = -1;
+			break;
+
+		default:
+			elog(ERROR, "unrecognized state: %d at: " INT64_FORMAT,
+				 state, pos);
+			break;
+	}
+
+	return rtn;
+}
+
+/*
+ * clear_reduced_frame
+ * Clear reduced frame status
+ */
+static void
+clear_reduced_frame(WindowAggState *winstate)
+{
+	winstate->rpr_match_valid = false;
+	winstate->rpr_match_matched = false;
+	winstate->rpr_match_start = -1;
+	winstate->rpr_match_length = 0;
+}
+
+/*
+ * 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_status(WindowAggState *winstate, int64 pos)
+{
+	int64		start = winstate->rpr_match_start;
+	int64		length = winstate->rpr_match_length;
+
+	if (!winstate->rpr_match_valid)
+		return RF_NOT_DETERMINED;
+
+	/* Empty match: covers only the start position */
+	if (pos == start && winstate->rpr_match_matched && length == 0)
+		return RF_EMPTY_MATCH;
+
+	/* Outside the result range */
+	if (pos < start || pos >= start + length)
+		return RF_NOT_DETERMINED;
+
+	if (!winstate->rpr_match_matched)
+		return RF_UNMATCHED;
+
+	if (pos == start)
+		return RF_FRAME_HEAD;
+
+	return RF_SKIPPED;
+}
+
+/*
+ * 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)
+{
+	WindowAggState *winstate = winobj->winstate;
+	RPRNFAContext *targetCtx;
+	int64		currentPos;
+	int64		startPos;
+	int			frameOptions = winstate->frameOptions;
+	bool		hasLimitedFrame;
+	int64		frameOffset = 0;
+	int64		matchLen;
+
+	/*
+	 * Check if we have a limited frame (ROWS ... N FOLLOWING). Each context
+	 * needs its own frame end based on matchStartRow + offset.
+	 */
+	hasLimitedFrame = (frameOptions & FRAMEOPTION_ROWS) &&
+		!(frameOptions & FRAMEOPTION_END_UNBOUNDED_FOLLOWING);
+	if (hasLimitedFrame && winstate->endOffsetValue != 0)
+		frameOffset = DatumGetInt64(winstate->endOffsetValue);
+
+	/*
+	 * Case 1: pos is before any existing context's start position. This means
+	 * the position was already processed and determined unmatched. Head is
+	 * the oldest context (lowest matchStartRow) since contexts are added at
+	 * tail with increasing positions.
+	 */
+	if (winstate->nfaContext != NULL &&
+		pos < winstate->nfaContext->matchStartRow)
+	{
+		/* already processed, unmatched */
+		winstate->rpr_match_valid = true;
+		winstate->rpr_match_matched = false;
+		winstate->rpr_match_start = pos;
+		winstate->rpr_match_length = 1;
+		return;
+	}
+
+	/*
+	 * Case 2: Find existing context for this pos, or create new one.
+	 */
+	targetCtx = ExecRPRGetHeadContext(winstate, pos);
+	if (targetCtx == NULL)
+	{
+		/*
+		 * No context exists. If pos is already processed, it means this row
+		 * was already determined to be unmatched or skipped - no need to
+		 * reprocess.
+		 */
+		if (pos <= winstate->nfaLastProcessedRow)
+		{
+			/* 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 */
+		targetCtx = ExecRPRStartContext(winstate, pos);
+	}
+	else if (targetCtx->states == NULL)
+	{
+		/* Context already completed - skip to result registration */
+		goto register_result;
+	}
+
+	/*
+	 * Determine where to start processing. Usually nfaLastProcessedRow+1 >=
+	 * pos since contexts are created at currentPos+1 during processing.
+	 * However, pos can exceed this when rows are skipped (e.g., unmatched
+	 * rows don't update nfaLastProcessedRow).
+	 */
+	startPos = Max(pos, winstate->nfaLastProcessedRow + 1);
+
+	/*
+	 * Process rows until target context completes or we hit boundaries. Each
+	 * row evaluation is shared across all active contexts.
+	 */
+	for (currentPos = startPos; targetCtx->states != NULL; currentPos++)
+	{
+		bool		rowExists;
+
+		/*
+		 * Evaluate variables for this row - done only once, shared by all
+		 * contexts.
+		 *
+		 * Set nav_match_start to the head context's matchStartRow for
+		 * FIRST/LAST navigation.  Match_start-dependent variables (FIRST,
+		 * LAST-with-offset) are re-evaluated per-context in ExecRPRProcessRow
+		 * when matchStartRow differs.
+		 */
+		winstate->nav_match_start = targetCtx->matchStartRow;
+		rowExists = nfa_evaluate_row(winobj, currentPos, winstate->nfaVarMatched);
+
+		/* No more rows in partition? Finalize all contexts */
+		if (!rowExists)
+		{
+			ExecRPRFinalizeAllContexts(winstate, currentPos - 1);
+			/* Clean up dead contexts from finalization */
+			ExecRPRCleanupDeadContexts(winstate, targetCtx);
+			break;
+		}
+
+		/* Update last processed row */
+		winstate->nfaLastProcessedRow = currentPos;
+
+		/*--------------------------
+		 * Process all contexts for this row:
+		 *   1. Match all (convergence)
+		 *   2. Absorb redundant
+		 *   3. Advance all (divergence)
+		 */
+		ExecRPRProcessRow(winstate, currentPos, hasLimitedFrame, frameOffset);
+
+		/*
+		 * Create a new context for the next potential start position. This
+		 * enables overlapping match detection for SKIP TO NEXT ROW.
+		 */
+		ExecRPRStartContext(winstate, currentPos + 1);
+
+		/*
+		 * Clean up dead contexts (failed with no active states and no match).
+		 * This removes contexts that failed during processing and counts them
+		 * appropriately as pruned or mismatched.
+		 */
+		ExecRPRCleanupDeadContexts(winstate, targetCtx);
+	}
+
+register_result:
+	Assert(pos == targetCtx->matchStartRow);
+
+	/*
+	 * 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;
+
+		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 */
+	matchLen = targetCtx->matchEndRow - targetCtx->matchStartRow + 1;
+
+	winstate->rpr_match_matched = true;
+	winstate->rpr_match_length = matchLen;
+	ExecRPRRecordContextSuccess(winstate, matchLen);
+
+	/* Remove the matched context */
+	ExecRPRFreeContext(winstate, targetCtx);
+}
+
+/*
+ * nfa_evaluate_row
+ *
+ * Evaluate all DEFINE variables for current row.
+ * 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/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)
+{
+	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;
+
+	/* 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;
+
+	/*
+	 * Save and set currentpos so that EEOP_RPR_NAV_SET opcodes can calculate
+	 * target positions (currentpos +/- offset).
+	 */
+	saved_pos = winstate->currentpos;
+	winstate->currentpos = pos;
+
+	/* Invalidate nav_slot cache so PREV/NEXT re-fetch for new row */
+	winstate->nav_slot_pos = -1;
+
+	foreach(lc, winstate->defineClauseList)
+	{
+		ExprState  *exprState = (ExprState *) lfirst(lc);
+		Datum		result;
+		bool		isnull;
+
+		/* Evaluate DEFINE expression */
+		result = ExecEvalExpr(exprState, econtext, &isnull);
+
+		varMatched[varIdx] = (!isnull && DatumGetBool(result));
+
+		varIdx++;
+		if (varIdx >= numDefineVars)
+			break;
+	}
+
+	winstate->currentpos = saved_pos;
+
+	return true;				/* Row exists */
+}
+
+
 /***********************************************************************
  * API exposed to window functions
  ***********************************************************************/
@@ -3972,8 +4946,6 @@ WinGetFuncArgInFrame(WindowObject winobj, int argno,
 	WindowAggState *winstate;
 	ExprContext *econtext;
 	TupleTableSlot *slot;
-	int64		abs_pos;
-	int64		mark_pos;
 
 	Assert(WindowObjectIsValid(winobj));
 	winstate = winobj->winstate;
@@ -3984,6 +4956,48 @@ WinGetFuncArgInFrame(WindowObject winobj, int argno,
 		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 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,
+				  int relpos, int seektype, bool set_mark,
+				  bool *isnull, bool *isout)
+{
+	WindowAggState *winstate;
+	int64		abs_pos;
+	int64		mark_pos;
+	int64		num_reduced_frame;
+
+	Assert(WindowObjectIsValid(winobj));
+	winstate = winobj->winstate;
+
 	switch (seektype)
 	{
 		case WINDOW_SEEK_CURRENT:
@@ -4050,11 +5064,25 @@ WinGetFuncArgInFrame(WindowObject winobj, int argno,
 						 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;
 
@@ -4121,6 +5149,18 @@ WinGetFuncArgInFrame(WindowObject winobj, int argno,
 					mark_pos = 0;	/* keep compiler quiet */
 					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;
+				abs_pos = winstate->frameheadpos + relpos +
+					num_reduced_frame - 1;
+			}
 			break;
 		default:
 			elog(ERROR, "unrecognized window seek type: %d", seektype);
@@ -4138,16 +5178,24 @@ WinGetFuncArgInFrame(WindowObject winobj, int argno,
 	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);
-	econtext->ecxt_outertuple = slot;
-	return ExecEvalExpr((ExprState *) list_nth(winobj->argstates, argno),
-						econtext, isnull);
+	}
+	return 0;
 
 out_of_frame:
 	if (isout)
 		*isout = true;
 	*isnull = true;
-	return (Datum) 0;
+	return -1;
 }
 
 /*
diff --git a/src/backend/jit/llvm/llvmjit_expr.c b/src/backend/jit/llvm/llvmjit_expr.c
index 0e160b8502c..e42c5d65bb6 100644
--- a/src/backend/jit/llvm/llvmjit_expr.c
+++ b/src/backend/jit/llvm/llvmjit_expr.c
@@ -129,6 +129,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;
@@ -298,6 +301,36 @@ 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 cached pointers become stale because the new
+	 * slot has its own tts_values/tts_isnull arrays.
+	 *
+	 * 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)
+	{
+		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)
+			{
+				has_rpr_nav = true;
+				break;
+			}
+		}
+	}
+
 	/* 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++)
@@ -460,8 +493,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)
 					{
@@ -2434,6 +2496,18 @@ 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);
+				LLVMBuildBr(b, opblocks[opno + 1]);
+				break;
+
+			case EEOP_RPR_NAV_RESTORE:
+				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/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/utils/adt/windowfuncs.c b/src/backend/utils/adt/windowfuncs.c
index 78b7f05aba2..46e7a03a666 100644
--- a/src/backend/utils/adt/windowfuncs.c
+++ b/src/backend/utils/adt/windowfuncs.c
@@ -41,7 +41,6 @@ static bool rank_up(WindowObject winobj);
 static Datum leadlag_common(FunctionCallInfo fcinfo,
 							bool forward, bool withoffset, bool withdefault);
 
-
 /*
  * utility routine for *_rank functions.
  */
@@ -724,3 +723,121 @@ window_nth_value(PG_FUNCTION_ARGS)
 
 	PG_RETURN_DATUM(result);
 }
+
+/*
+ * prev
+ * Catalog placeholder for RPR's PREV navigation operator.
+ *
+ * The parser transforms prev() calls inside DEFINE into RPRNavExpr nodes,
+ * so this function is never reached during normal RPR execution.  It exists
+ * only so that the parser can resolve the function name from pg_proc.
+ * Calls outside DEFINE are rejected by parse_func.c (EXPR_KIND_RPR_DEFINE
+ * check).  The error below is a defensive measure in case that check is
+ * bypassed (e.g., direct C-level function invocation).
+ */
+Datum
+window_prev(PG_FUNCTION_ARGS)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot use PREV() outside a DEFINE clause")));
+	PG_RETURN_NULL();			/* not reached */
+}
+
+/*
+ * next
+ * Catalog placeholder for RPR's NEXT navigation operator.
+ * See window_prev() for details.
+ */
+Datum
+window_next(PG_FUNCTION_ARGS)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot use NEXT() outside a DEFINE clause")));
+	PG_RETURN_NULL();			/* not reached */
+}
+
+/*
+ * prev(value, offset)
+ * Catalog placeholder for RPR's PREV navigation operator with offset.
+ * See window_prev() for details.
+ */
+Datum
+window_prev_offset(PG_FUNCTION_ARGS)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot use PREV() outside a DEFINE clause")));
+	PG_RETURN_NULL();			/* not reached */
+}
+
+/*
+ * next(value, offset)
+ * Catalog placeholder for RPR's NEXT navigation operator with offset.
+ * See window_prev() for details.
+ */
+Datum
+window_next_offset(PG_FUNCTION_ARGS)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot use NEXT() outside a DEFINE clause")));
+	PG_RETURN_NULL();			/* not reached */
+}
+
+/*
+ * first
+ * Catalog placeholder for RPR's FIRST navigation operator.
+ * See window_prev() for details.
+ */
+Datum
+window_first(PG_FUNCTION_ARGS)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot use FIRST() outside a DEFINE clause")));
+	PG_RETURN_NULL();			/* not reached */
+}
+
+/*
+ * last
+ * Catalog placeholder for RPR's LAST navigation operator.
+ * See window_prev() for details.
+ */
+Datum
+window_last(PG_FUNCTION_ARGS)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot use LAST() outside a DEFINE clause")));
+	PG_RETURN_NULL();			/* not reached */
+}
+
+/*
+ * first(value, offset)
+ * Catalog placeholder for RPR's FIRST navigation operator with offset.
+ * See window_prev() for details.
+ */
+Datum
+window_first_offset(PG_FUNCTION_ARGS)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot use FIRST() outside a DEFINE clause")));
+	PG_RETURN_NULL();			/* not reached */
+}
+
+/*
+ * last(value, offset)
+ * Catalog placeholder for RPR's LAST navigation operator with offset.
+ * See window_prev() for details.
+ */
+Datum
+window_last_offset(PG_FUNCTION_ARGS)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot use LAST() outside a DEFINE clause")));
+	PG_RETURN_NULL();			/* not reached */
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..02b668fc4c5 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -11017,6 +11017,30 @@
 { oid => '3114', descr => 'fetch the Nth row value',
   proname => 'nth_value', prokind => 'w', prorettype => 'anyelement',
   proargtypes => 'anyelement int4', prosrc => 'window_nth_value' },
+{ oid => '8126', descr => 'fetch the preceding row value',
+  proname => 'prev', provolatile => 's', prorettype => 'anyelement',
+  proargtypes => 'anyelement', prosrc => 'window_prev' },
+{ oid => '8128', descr => 'fetch the Nth preceding row value',
+  proname => 'prev', provolatile => 's', proisstrict => 'f', prorettype => 'anyelement',
+  proargtypes => 'anyelement int8', prosrc => 'window_prev_offset' },
+{ oid => '8127', descr => 'fetch the following row value',
+  proname => 'next', provolatile => 's', prorettype => 'anyelement',
+  proargtypes => 'anyelement', prosrc => 'window_next' },
+{ oid => '8129', descr => 'fetch the Nth following row value',
+  proname => 'next', provolatile => 's', proisstrict => 'f', prorettype => 'anyelement',
+  proargtypes => 'anyelement int8', prosrc => 'window_next_offset' },
+{ oid => '8130', descr => 'fetch the first row value within match',
+  proname => 'first', provolatile => 's', prorettype => 'anyelement',
+  proargtypes => 'anyelement', prosrc => 'window_first' },
+{ oid => '8132', descr => 'fetch the Nth row value within match',
+  proname => 'first', provolatile => 's', proisstrict => 'f', prorettype => 'anyelement',
+  proargtypes => 'anyelement int8', prosrc => 'window_first_offset' },
+{ oid => '8131', descr => 'fetch the last row value within match',
+  proname => 'last', provolatile => 's', prorettype => 'anyelement',
+  proargtypes => 'anyelement', prosrc => 'window_last' },
+{ oid => '8133', descr => 'fetch the Nth-from-last row value within match',
+  proname => 'last', provolatile => 's', proisstrict => 'f', prorettype => 'anyelement',
+  proargtypes => 'anyelement int8', prosrc => 'window_last_offset' },
 
 # functions for range types
 { oid => '3832', descr => 'I/O',
diff --git a/src/include/executor/execExpr.h b/src/include/executor/execExpr.h
index c61b3d624d5..db66ebe313c 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,
@@ -695,6 +699,18 @@ typedef struct ExprEvalStep
 			SubPlanState *sstate;
 		}			subplan;
 
+		/* for EEOP_RPR_NAV_SET / EEOP_RPR_NAV_RESTORE */
+		struct
+		{
+			WindowAggState *winstate;
+			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 */
+			int16		resulttyplen;	/* RESTORE: result type length */
+			bool		resulttypbyval; /* RESTORE: result pass-by-value? */
+		}			rpr_nav;
+
 		/* for EEOP_AGG_*DESERIALIZE */
 		struct
 		{
@@ -902,6 +918,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/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 */
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 13359180d25..0cb01baa949 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -38,6 +38,9 @@
 #include "nodes/plannodes.h"
 #include "partitioning/partdefs.h"
 #include "storage/buf.h"
+#include "storage/condition_variable.h"
+#include "utils/hsearch.h"
+#include "utils/queryenvironment.h"
 #include "utils/reltrigger.h"
 #include "utils/typcache.h"
 
@@ -2524,6 +2527,71 @@ typedef enum WindowAggStatus
 									 * tuples during spool */
 } 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 */
+
+/*
+ * RPRNFAState - single NFA state for pattern matching
+ *
+ * counts[] tracks repetition counts at each nesting depth.
+ *
+ * isAbsorbable tracks if state is in absorbable region (ABSORBABLE_BRANCH).
+ * Monotonic property: once false, stays false (can't re-enter region).
+ */
+typedef struct RPRNFAState
+{
+	struct RPRNFAState *next;	/* next state in linked list */
+	int16		elemIdx;		/* current pattern element index */
+	bool		isAbsorbable;	/* true if state is in absorbable region */
+	int32		counts[FLEXIBLE_ARRAY_MEMBER];	/* repetition counts by depth */
+} RPRNFAState;
+
+/*
+ * RPRNFAContext - context for NFA pattern matching execution
+ *
+ * Two-flag absorption design:
+ *   hasAbsorbableState: can this context absorb others? (>=1 absorbable state)
+ *     - Monotonic: true->false only, cannot recover once false
+ *     - Used to skip absorption attempts once all absorbable states are gone
+ *   allStatesAbsorbable: can this context be absorbed? (ALL states absorbable)
+ *     - Dynamic: can change false->true (when non-absorbable states die)
+ *     - Used to determine if this context is eligible for absorption
+ */
+typedef struct RPRNFAContext
+{
+	struct RPRNFAContext *next; /* next context in linked list */
+	struct RPRNFAContext *prev; /* previous context (for reverse traversal) */
+	RPRNFAState *states;		/* active states (linked list) */
+
+	int64		matchStartRow;	/* row where match started */
+	int64		matchEndRow;	/* row where match ended (-1 = no match) */
+	int64		lastProcessedRow;	/* last row processed (for fail depth) */
+	RPRNFAState *matchedState;	/* FIN state for greedy fallback (cloned) */
+
+	/* Two-flag absorption optimization */
+	bool		hasAbsorbableState; /* can absorb others (>=1 absorbable
+									 * state) */
+	bool		allStatesAbsorbable;	/* can be absorbed (ALL states
+										 * absorbable) */
+} RPRNFAContext;
+
+/*
+ * NFALengthStats
+ *
+ * Statistics for length measurements (min/max/total) used for computing
+ * average lengths in EXPLAIN ANALYZE output.
+ */
+typedef struct NFALengthStats
+{
+	int64		min;			/* minimum length */
+	int64		max;			/* maximum length */
+	int64		total;			/* total length (for computing average) */
+} NFALengthStats;
+
 typedef struct WindowAggState
 {
 	ScanState	ss;				/* its first field is NodeTag */
@@ -2583,6 +2651,49 @@ typedef struct WindowAggState
 	int64		groupheadpos;	/* current row's peer group head position */
 	int64		grouptailpos;	/* " " " " tail position (group end+1) */
 
+	/* these fields are used in Row pattern recognition: */
+	RPSkipTo	rpSkipTo;		/* Row Pattern Skip To type */
+	struct RPRPattern *rpPattern;	/* compiled pattern for NFA execution */
+	List	   *defineVariableList; /* list of row pattern definition
+									 * variables (list of String) */
+	List	   *defineClauseList;	/* expression for row pattern definition
+									 * search conditions ExprState list */
+	RPRNFAContext *nfaContext;	/* active matching contexts (head) */
+	RPRNFAContext *nfaContextTail;	/* tail of active contexts (for reverse
+									 * traversal) */
+	RPRNFAContext *nfaContextFree;	/* recycled NFA context nodes */
+	RPRNFAState *nfaStateFree;	/* recycled NFA state nodes */
+	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
+									 * nfaVisitedElems */
+	int64		nfaLastProcessedRow;	/* last row processed by NFA (-1 =
+										 * none) */
+
+	/* NFA statistics for EXPLAIN ANALYZE */
+	int64		nfaStatesActive;	/* current active states (internal) */
+	int64		nfaStatesMax;	/* peak active states */
+	int64		nfaStatesTotalCreated;	/* total states allocated */
+	int64		nfaStatesMerged;	/* states merged (deduplicated) */
+	int64		nfaContextsActive;	/* current active contexts (internal) */
+	int64		nfaContextsMax; /* peak active contexts */
+	int64		nfaContextsTotalCreated;	/* total contexts allocated */
+	int64		nfaContextsAbsorbed;	/* contexts absorbed (optimization) */
+	int64		nfaContextsSkipped; /* contexts skipped (SKIP PAST LAST ROW) */
+	int64		nfaContextsPruned;	/* contexts pruned on first row */
+	int64		nfaMatchesSucceeded;	/* successful pattern matches */
+	int64		nfaMatchesFailed;	/* failed pattern matches */
+	NFALengthStats nfaMatchLen; /* successful match length stats */
+	NFALengthStats nfaFailLen;	/* mismatch length stats */
+	NFALengthStats nfaAbsorbedLen;	/* absorbed context length stats */
+	NFALengthStats nfaSkippedLen;	/* skipped context length stats */
+
 	MemoryContext partcontext;	/* context for partition-lifespan data */
 	MemoryContext aggcontext;	/* shared context for aggregate working data */
 	MemoryContext curaggcontext;	/* current aggregate's working data */
@@ -2610,6 +2721,25 @@ typedef struct WindowAggState
 	TupleTableSlot *agg_row_slot;
 	TupleTableSlot *temp_slot_1;
 	TupleTableSlot *temp_slot_2;
+
+	/* RPR navigation */
+	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/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 */
+	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;
 
 /* ----------------
-- 
2.43.0



  [application/octet-stream] v47-0006-Row-pattern-recognition-patch-docs.patch (19.9K, 7-v47-0006-Row-pattern-recognition-patch-docs.patch)
  download | inline diff:
From 597ccd24e405949e5d320eee0cb1f7262ad3068e Mon Sep 17 00:00:00 2001
From: Tatsuo Ishii <[email protected]>
Date: Sat, 2 May 2026 13:40:29 +0900
Subject: [PATCH v47 6/9] Row pattern recognition patch (docs).

---
 doc/src/sgml/advanced.sgml         | 145 ++++++++++++++++++++++++++++-
 doc/src/sgml/func/func-window.sgml | 121 ++++++++++++++++++++++++
 doc/src/sgml/ref/select.sgml       |  91 +++++++++++++++++-
 3 files changed, 350 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/advanced.sgml b/doc/src/sgml/advanced.sgml
index 3286c2cf0b2..11c2416df51 100644
--- a/doc/src/sgml/advanced.sgml
+++ b/doc/src/sgml/advanced.sgml
@@ -552,13 +552,150 @@ WHERE pos &lt; 3;
     two rows for each department).
    </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
+    clauses: <literal>DEFINE</literal>
+    and <literal>PATTERN</literal>. <literal>DEFINE</literal> defines
+    row pattern variables along with an expression. The expression must be a
+    logical expression, which means it must
+    return <literal>TRUE</literal>, <literal>FALSE</literal>
+    or <literal>NULL</literal>. The expression may comprise column references
+    and functions. Window functions, aggregate functions and subqueries are
+    not allowed. An example of <literal>DEFINE</literal> is as follows.
+
+<programlisting>
+DEFINE
+ LOWPRICE AS price &lt;= 100,
+ UP AS price &gt; PREV(price),
+ DOWN AS price &lt; PREV(price)
+</programlisting>
+
+    Note that <function>PREV</function> returns the <literal>price</literal>
+    column in the previous row if it's called in a context of row pattern
+    recognition. Thus in the second line the row pattern variable "UP"
+    is <literal>TRUE</literal> when the price column in the current row is
+    greater than the price column in the previous row. Likewise, "DOWN"
+    is <literal>TRUE</literal> when the
+    <literal>price</literal> column in the current row is lower than
+    the <literal>price</literal> column in the previous row.
+   </para>
+   <para>
+    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
+    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
+    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)+"
+    matches one or more repetitions of UP followed by DOWN.  If a sequence of
+    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 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.
+
+<programlisting>
+SELECT company, tdate, price,
+ first_value(price) OVER w,
+ max(price) OVER w,
+ count(price) 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 (LOWPRICE UP+ DOWN+)
+ DEFINE
+  LOWPRICE AS price &lt;= 100,
+  UP AS price &gt; PREV(price),
+  DOWN AS price &lt; PREV(price)
+);
+</programlisting>
+<screen>
+ company  |   tdate    | price | first_value | max | count 
+----------+------------+-------+-------------+-----+-------
+ company1 | 2023-07-01 |   100 |         100 | 200 |     4
+ company1 | 2023-07-02 |   200 |             |     |     0
+ company1 | 2023-07-03 |   150 |             |     |     0
+ company1 | 2023-07-04 |   140 |             |     |     0
+ company1 | 2023-07-05 |   150 |             |     |     0
+ company1 | 2023-07-06 |    90 |          90 | 130 |     4
+ company1 | 2023-07-07 |   110 |             |     |     0
+ company1 | 2023-07-08 |   130 |             |     |     0
+ company1 | 2023-07-09 |   120 |             |     |     0
+ company1 | 2023-07-10 |   130 |             |     |     0
+(10 rows)
+</screen>
+   </para>
+
+   <para>
+    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,
+    which could potentially lead to O(n<superscript>2</superscript>)
+    complexity as the number of rows increases.
+   </para>
+
+   <para>
+    Before execution, <productname>PostgreSQL</productname> automatically
+    optimizes patterns to simplify their structure. This includes flattening
+    nested sequences and alternations, merging consecutive identical variables
+    (e.g., <literal>A{2,3} A{1,2}</literal> becomes <literal>A{3,5}</literal>),
+    removing duplicate alternatives
+    (e.g., <literal>(A | B | A)</literal> becomes <literal>(A | B)</literal>),
+    and simplifying nested quantifiers
+    (e.g., <literal>(A*)*</literal> becomes <literal>A*</literal>).
+    These optimizations reduce pattern complexity and also decrease
+    nesting depth, making the 253-level depth limit rarely encountered.
+    They are applied transparently and can be observed
+    in <command>EXPLAIN</command> output.
+   </para>
+
+   <para>
+    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
+    contexts, the matching complexity is reduced from
+    O(n<superscript>2</superscript>) to O(n) for many common patterns.
+   </para>
+
+   <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>
+
    <para>
     When a query involves multiple window functions, it is possible to write
     out each one with a separate <literal>OVER</literal> clause, but this is
-    duplicative and error-prone if the same windowing behavior is wanted
-    for several functions.  Instead, each windowing behavior can be named
-    in a <literal>WINDOW</literal> clause and then referenced in <literal>OVER</literal>.
-    For example:
+    duplicative and error-prone if the same windowing behavior is wanted for
+    several functions.  Instead, each windowing behavior can be named in
+    a <literal>WINDOW</literal> clause and then referenced
+    in <literal>OVER</literal>.  For example:
 
 <programlisting>
 SELECT sum(salary) OVER w, avg(salary) OVER w
diff --git a/doc/src/sgml/func/func-window.sgml b/doc/src/sgml/func/func-window.sgml
index bcf755c9ebc..d1da105ad0f 100644
--- a/doc/src/sgml/func/func-window.sgml
+++ b/doc/src/sgml/func/func-window.sgml
@@ -278,6 +278,127 @@
    <function>nth_value</function>.
   </para>
 
+  <para>
+   Row Pattern Recognition navigation functions are listed in
+   <xref linkend="functions-rpr-navigation-table"/>.  These functions
+   can be used to describe the DEFINE clause of Row Pattern Recognition.
+  </para>
+
+   <table id="functions-rpr-navigation-table">
+    <title>Row Pattern Navigation Functions</title>
+    <tgroup cols="1">
+     <thead>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        Function
+       </para>
+       <para>
+        Description
+       </para></entry>
+      </row>
+     </thead>
+
+     <tbody>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>prev</primary>
+        </indexterm>
+        <function>prev</function> ( <parameter>value</parameter> <type>anyelement</type> [, <parameter>offset</parameter> <type>bigint</type> ] )
+        <returnvalue>anyelement</returnvalue>
+       </para>
+       <para>
+        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;
+        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>
+
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>next</primary>
+        </indexterm>
+        <function>next</function> ( <parameter>value</parameter> <type>anyelement</type> [, <parameter>offset</parameter> <type>bigint</type> ] )
+        <returnvalue>anyelement</returnvalue>
+       </para>
+       <para>
+        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;
+        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>
+
+      <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 <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.
+        <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 <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.
+        <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/doc/src/sgml/ref/select.sgml b/doc/src/sgml/ref/select.sgml
index 09b6ce809bb..5272d6c0bfa 100644
--- a/doc/src/sgml/ref/select.sgml
+++ b/doc/src/sgml/ref/select.sgml
@@ -1022,8 +1022,8 @@ WINDOW <replaceable class="parameter">window_name</replaceable> AS ( <replaceabl
     The <replaceable class="parameter">frame_clause</replaceable> can be one of
 
 <synopsis>
-{ RANGE | ROWS | GROUPS } <replaceable>frame_start</replaceable> [ <replaceable>frame_exclusion</replaceable> ]
-{ RANGE | ROWS | GROUPS } BETWEEN <replaceable>frame_start</replaceable> AND <replaceable>frame_end</replaceable> [ <replaceable>frame_exclusion</replaceable> ]
+{ RANGE | ROWS | GROUPS } <replaceable>frame_start</replaceable> [ <replaceable>frame_exclusion</replaceable> ] [ <replaceable>row_pattern_common_syntax</replaceable> ]
+{ RANGE | ROWS | GROUPS } BETWEEN <replaceable>frame_start</replaceable> AND <replaceable>frame_end</replaceable> [ <replaceable>frame_exclusion</replaceable> ] [ <replaceable>row_pattern_common_syntax</replaceable> ]
 </synopsis>
 
     where <replaceable>frame_start</replaceable>
@@ -1130,9 +1130,94 @@ EXCLUDE NO OTHERS
     a given peer group will be in the frame or excluded from it.
    </para>
 
+   <para>
+    The
+    optional <replaceable class="parameter">row_pattern_common_syntax</replaceable>
+    defines the <firstterm>Row Pattern Recognition condition</firstterm> for
+    this
+    window. <replaceable class="parameter">row_pattern_common_syntax</replaceable>
+    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> ] [ ... ] )
+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 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 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 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),
+    <literal>+</literal> (one or more),
+    <literal>?</literal> (zero or one),
+    <literal>{</literal><replaceable>n</replaceable><literal>}</literal> (exactly <replaceable>n</replaceable> times, n &gt; 0),
+    <literal>{</literal><replaceable>n</replaceable><literal>,}</literal> (at least <replaceable>n</replaceable> times, n &gt;= 0),
+    <literal>{,</literal><replaceable>m</replaceable><literal>}</literal> (at most <replaceable>m</replaceable> times, m &gt; 0), or
+    <literal>{</literal><replaceable>n</replaceable><literal>,</literal><replaceable>m</replaceable><literal>}</literal>
+    (between <replaceable>n</replaceable> and <replaceable>m</replaceable> times, 0 &lt;= n &lt;= m, 0 &lt; m).
+    Reluctant quantifiers (e.g., <literal>*?</literal>, <literal>+?</literal>,
+    <literal>??</literal>, <literal>{</literal><replaceable>n</replaceable><literal>,</literal><replaceable>m</replaceable><literal>}?</literal>)
+    are supported.
+    The exclusion (<literal>{-</literal> and <literal>-}</literal>)
+    is not supported.
+    Patterns can be grouped using parentheses, and alternation (OR) can be
+    expressed using the vertical bar <literal>|</literal>.
+    For example, <literal>(A B)+</literal> matches one or more repetitions
+    of the sequence A followed by B, and <literal>A | B</literal> matches
+    either A or B.
+    If a pattern variable is not defined in
+    the <literal>DEFINE</literal> clause, it is not automatically added
+    to the <literal>DEFINE</literal> clause. Instead, the executor evaluates
+    the variable as <literal>TRUE</literal> at execution time, behaving as if
+    the following definition existed.
+
+<synopsis>
+<literal>variable_name</literal> AS TRUE
+</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.
+   </para>
+
+   <para>
+    Note that the maximum number of unique pattern variables
+    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.
+    However, pattern optimizations such as flattening nested sequences
+    and simplifying nested quantifiers may reduce the effective depth,
+    so this limit is rarely reached in practice.
+   </para>
+
+   <para>
+    The SQL standard defines more subclauses: <literal>MEASURES</literal>
+    and <literal>SUBSET</literal>. They are not currently supported
+    in <productname>PostgreSQL</productname>. Also in the standard there are
+    more variations in <literal>AFTER MATCH</literal> clause.
+   </para>
+
    <para>
     The purpose of a <literal>WINDOW</literal> clause is to specify the
-    behavior of <firstterm>window functions</firstterm> appearing in the query's
+    behavior of <firstterm>window functions</firstterm> appearing in the
+    query's
     <link linkend="sql-select-list"><command>SELECT</command> list</link> or
     <link linkend="sql-orderby"><literal>ORDER BY</literal></link> clause.
     These functions
-- 
2.43.0



  [application/octet-stream] v47-0007-Row-pattern-recognition-patch-tests-SQL-data.patch (489.6K, 8-v47-0007-Row-pattern-recognition-patch-tests-SQL-data.patch)
  download | inline diff:
From d99cd6925dfad30a9c791dfe0a7f31c755821619 Mon Sep 17 00:00:00 2001
From: Tatsuo Ishii <[email protected]>
Date: Sat, 2 May 2026 13:40:29 +0900
Subject: [PATCH v47 7/9] Row pattern recognition patch (tests: SQL, data).

---
 src/test/regress/data/stock.data         | 1632 +++++++++
 src/test/regress/parallel_schedule       |    5 +
 src/test/regress/sql/rpr.sql             | 1960 ++++++++++
 src/test/regress/sql/rpr_base.sql        | 4130 ++++++++++++++++++++++
 src/test/regress/sql/rpr_explain.sql     | 2783 +++++++++++++++
 src/test/regress/sql/rpr_integration.sql |  940 +++++
 src/test/regress/sql/rpr_nfa.sql         | 3446 ++++++++++++++++++
 7 files changed, 14896 insertions(+)
 create mode 100644 src/test/regress/data/stock.data
 create mode 100644 src/test/regress/sql/rpr.sql
 create mode 100644 src/test/regress/sql/rpr_base.sql
 create mode 100644 src/test/regress/sql/rpr_explain.sql
 create mode 100644 src/test/regress/sql/rpr_integration.sql
 create mode 100644 src/test/regress/sql/rpr_nfa.sql

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/parallel_schedule b/src/test/regress/parallel_schedule
index 8fa0a6c47fb..fc48ed2afc5 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -104,6 +104,11 @@ test: publication subscription
 # ----------
 test: select_views portals_p2 foreign_key dependency guc bitmapops combocid tsearch tsdicts foreign_data window xmlmap functional_deps advisory_lock indirect_toast equivclass stats_rewrite graph_table
 
+# ----------
+# Row Pattern Recognition tests
+# ----------
+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.sql b/src/test/regress/sql/rpr.sql
new file mode 100644
index 00000000000..5563e062cde
--- /dev/null
+++ b/src/test/regress/sql/rpr.sql
@@ -0,0 +1,1960 @@
+--
+-- Test for row pattern recognition: WINDOW clause integration and
+-- 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,
+       price INTEGER
+);
+INSERT INTO stock VALUES ('company1', '2023-07-01', 100);
+INSERT INTO stock VALUES ('company1', '2023-07-02', 200);
+INSERT INTO stock VALUES ('company1', '2023-07-03', 150);
+INSERT INTO stock VALUES ('company1', '2023-07-04', 140);
+INSERT INTO stock VALUES ('company1', '2023-07-05', 150);
+INSERT INTO stock VALUES ('company1', '2023-07-06', 90);
+INSERT INTO stock VALUES ('company1', '2023-07-07', 110);
+INSERT INTO stock VALUES ('company1', '2023-07-08', 130);
+INSERT INTO stock VALUES ('company1', '2023-07-09', 120);
+INSERT INTO stock VALUES ('company1', '2023-07-10', 130);
+INSERT INTO stock VALUES ('company2', '2023-07-01', 50);
+INSERT INTO stock VALUES ('company2', '2023-07-02', 2000);
+INSERT INTO stock VALUES ('company2', '2023-07-03', 1500);
+INSERT INTO stock VALUES ('company2', '2023-07-04', 1400);
+INSERT INTO stock VALUES ('company2', '2023-07-05', 1500);
+INSERT INTO stock VALUES ('company2', '2023-07-06', 60);
+INSERT INTO stock VALUES ('company2', '2023-07-07', 1100);
+INSERT INTO stock VALUES ('company2', '2023-07-08', 1300);
+INSERT INTO stock VALUES ('company2', '2023-07-09', 1200);
+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
+ 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)
+);
+
+-- basic test using PREV. UP appears twice
+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+ UP+)
+ DEFINE
+  START AS TRUE,
+  UP AS price > PREV(price),
+  DOWN AS price < PREV(price)
+);
+
+-- basic test using PREV. Use '*'
+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)
+);
+
+-- basic test using PREV. Use '?'
+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)
+);
+
+-- test using alternation (|) with sequence
+SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
+ 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)
+);
+
+-- test using alternation (|) with group quantifier
+SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
+ 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)
+);
+
+-- test using nested alternation
+SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (START ((UP DOWN) | FLAT)+)
+ DEFINE
+  START AS TRUE,
+  UP AS price > PREV(price),
+  DOWN AS price < PREV(price),
+  FLAT AS price = PREV(price)
+);
+
+-- test using group with quantifier
+SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN ((UP DOWN)+)
+ DEFINE
+  UP AS price > PREV(price),
+  DOWN AS price < PREV(price)
+);
+
+-- test using absolute threshold values (not relative PREV)
+-- HIGH: price > 150, LOW: price < 100, MID: neutral range
+SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (LOW MID* HIGH)
+ DEFINE
+  LOW AS price < 100,
+  MID AS price >= 100 AND price <= 150,
+  HIGH AS price > 150
+);
+
+-- test threshold-based pattern with alternation
+SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (LOW (MID | HIGH)+)
+ DEFINE
+  LOW AS price < 100,
+  MID AS price >= 100 AND price <= 150,
+  HIGH AS price > 150
+);
+
+-- basic test with fixed-length pattern (A A A = exactly 3)
+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
+);
+
+-- test using {n} quantifier (A A A should be optimized to A{3})
+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 >= 140 AND price <= 150
+);
+
+-- test using {n,} quantifier (2 or more)
+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
+);
+
+-- test using {n,m} quantifier (2 to 4)
+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})
+ DEFINE
+  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
+ 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)
+);
+
+-- omit "START" in DEFINE but it is ok because "START AS TRUE" is
+-- implicitly defined. per spec.
+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
+  UP AS price > PREV(price),
+  DOWN AS price < PREV(price)
+);
+
+-- the first row start with less than or equal to 100
+SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (LOWPRICE UP+ DOWN+)
+ DEFINE
+  LOWPRICE AS price <= 100,
+  UP AS price > PREV(price),
+  DOWN AS price < PREV(price)
+);
+
+-- second row raises 120%
+SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (LOWPRICE UP+ DOWN+)
+ DEFINE
+  LOWPRICE AS price <= 100,
+  UP AS price > PREV(price) * 1.2,
+  DOWN AS price < PREV(price)
+);
+
+-- using NEXT
+SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (START UPDOWN)
+ DEFINE
+  START AS TRUE,
+  UPDOWN AS price > PREV(price) AND price > NEXT(price)
+);
+
+-- 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 (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ INITIAL
+ PATTERN (START UPDOWN)
+ DEFINE
+  START AS TRUE,
+  UPDOWN AS price > PREV(price) AND price > NEXT(price)
+);
+
+-- 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
+);
+
+--
+-- 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
+);
+
+-- Non-constant offset: subquery 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, (SELECT 1)) > 0
+);
+
+-- First arg: subquery (caught by DEFINE-level subquery restriction)
+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 + (SELECT 1)) > 0
+);
+
+-- First arg: volatile function is allowed (evaluated on target row)
+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 + random() * 0) >= 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 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,
+       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)
+);
+
+-- 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
+--
+
+-- 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
+--
+
+-- match everything
+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 PAST LAST ROW
+ INITIAL
+ PATTERN (A+)
+ DEFINE
+  A AS TRUE
+);
+
+-- nth_value beyond reduced frame (no IGNORE NULLS)
+-- Tests WinGetSlotInFrame/WinGetFuncArgInFrame out-of-frame with RPR
+SELECT company, tdate, price,
+ nth_value(price, 5) OVER w AS nth_5
+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)
+);
+
+-- backtracking with reclassification of rows
+-- using AFTER MATCH SKIP PAST LAST ROW
+SELECT company, tdate, price, first_value(tdate) OVER w, last_value(tdate) 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 (A+ B+)
+ DEFINE
+  A AS price > 100,
+  B AS price > 100
+);
+
+-- backtracking with reclassification of rows
+-- using AFTER MATCH SKIP TO NEXT ROW
+SELECT company, tdate, price, first_value(tdate) OVER w, last_value(tdate) 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 (A+ B+)
+ DEFINE
+  A AS price > 100,
+  B AS price > 100
+);
+
+-- SKIP TO NEXT ROW with limited frame (Ishii-san's test case)
+-- Each row should produce its own match within its frame
+WITH data AS (
+ SELECT * FROM (VALUES
+  ('A', 1), ('A', 2),
+  ('B', 3), ('B', 4)
+ ) AS t(gid, id)
+)
+SELECT gid, id, array_agg(id) OVER w
+FROM data
+WINDOW w AS (
+ PARTITION BY gid
+ ROWS BETWEEN CURRENT ROW AND 2 FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A+)
+ DEFINE A AS id < 10
+);
+
+-- Limited frame with absorption test
+-- Row 0: frame [0,2], can't see B at row 3 -> no match
+-- Row 1: frame [1,3], can see A A B -> should match rows 1-3
+WITH frame_absorb_test AS (
+ SELECT * FROM (VALUES
+  (0, 'A'), (1, 'A'), (2, 'A'), (3, 'B')
+ ) AS t(id, flag)
+)
+SELECT id, flag, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM frame_absorb_test
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND 2 FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE
+  A AS flag = 'A',
+  B AS flag = 'B'
+);
+
+-- ROWS BETWEEN CURRENT ROW AND offset FOLLOWING
+SELECT company, tdate, price, first_value(tdate) OVER w, last_value(tdate) OVER w,
+ count(*) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND 2 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)
+);
+
+--
+-- Aggregates
+--
+
+-- using AFTER MATCH SKIP PAST LAST ROW
+SELECT company, tdate, price,
+ first_value(price) OVER w,
+ last_value(price) OVER w,
+ max(price) OVER w,
+ min(price) OVER w,
+ sum(price) OVER w,
+ avg(price) OVER w,
+ count(price) OVER w
+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)
+);
+
+-- using AFTER MATCH SKIP TO NEXT ROW
+SELECT company, tdate, price,
+ first_value(price) OVER w,
+ last_value(price) OVER w,
+ max(price) OVER w,
+ min(price) OVER w,
+ sum(price) OVER w,
+ avg(price) OVER w,
+ count(price) OVER w
+FROM stock
+WINDOW w AS (
+PARTITION BY company
+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(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);
+INSERT INTO t1 VALUES(1,10);
+INSERT INTO t1 VALUES(1,11);
+INSERT INTO t1 VALUES(1,12);
+INSERT INTO t2 VALUES(2,10);
+INSERT INTO t2 VALUES(2,11);
+INSERT INTO t2 VALUES(2,12);
+
+SELECT * FROM t1, t2 WHERE t1.v1 <= 11 AND t2.v2 <= 11;
+
+SELECT *, count(*) OVER w FROM t1, t2
+WINDOW w AS (
+ PARTITION BY t1.i
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (A)
+ DEFINE
+ A AS v1 <= 11 AND v2 <= 11
+);
+
+-- WITH case
+WITH wstock AS (
+  SELECT * FROM stock WHERE tdate < '2023-07-08'
+)
+SELECT tdate, price,
+first_value(tdate) OVER w,
+count(*) OVER w
+ FROM wstock
+ 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)
+);
+
+-- ReScan test: LATERAL join forces WindowAgg rescan with RPR
+-- Tests ExecReScanWindowAgg clearing nav_slot
+SELECT g.x, sub.*
+FROM generate_series(1, 2) g(x),
+LATERAL (
+  SELECT id, price, count(*) OVER w AS c
+  FROM (VALUES (1, 100), (2, 200), (3, 150)) AS t(id, price)
+  WHERE id <= g.x + 1
+  WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (START UP+)
+    DEFINE
+      START AS TRUE,
+      UP AS price > PREV(price)
+  )
+) sub
+ORDER BY g.x, sub.id;
+
+-- PREV has multiple column reference
+CREATE TEMP TABLE rpr1 (id INTEGER, i SERIAL, j INTEGER);
+INSERT INTO rpr1(id, j) SELECT 1, g*2 FROM generate_series(1, 10) AS g;
+SELECT id, i, j, count(*) OVER w
+ FROM rpr1
+ WINDOW w AS (
+ PARTITION BY id
+ ORDER BY i
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ INITIAL
+ PATTERN (START COND+)
+ DEFINE
+  START AS TRUE,
+  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
+ FROM (SELECT generate_series(1, 5000) v)
+ WINDOW w AS (
+  ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+  AFTER MATCH SKIP PAST LAST ROW
+  INITIAL
+  PATTERN ( r+ )
+  DEFINE r AS TRUE
+ )
+)
+-- Should be exactly one long match across all rows.
+SELECT * FROM s WHERE c > 0;
+
+WITH s AS (
+ SELECT v, count(*) OVER w AS c
+ FROM (SELECT generate_series(1, 5000) v)
+ WINDOW w AS (
+  ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+  AFTER MATCH SKIP PAST LAST ROW
+  INITIAL
+  PATTERN ( r )
+  DEFINE r AS TRUE
+ )
+)
+-- Every row should be its own match.
+SELECT count(*) FROM s WHERE c > 0;
+
+-- Large partition test: 100K rows with A+ B* C{10000,} pattern
+-- Tests that int32 count doesn't overflow with large repetitions
+WITH data AS (
+ SELECT generate_series(0, 100000) AS v
+),
+result AS (
+ SELECT v,
+        count(*) OVER w AS match_len,
+        first_value(v) OVER w AS match_first,
+        last_value(v) OVER w AS match_last
+ FROM data
+ WINDOW w AS (
+  ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+  AFTER MATCH SKIP PAST LAST ROW
+  INITIAL
+  PATTERN (A+ B* C{10000,})
+  DEFINE
+   A AS v < 33333,
+   B AS v >= 33333 AND v < 66666,
+   C AS v >= 66666 AND v < 99999
+ )
+)
+-- 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 = on;
+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;
+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
+--
+
+-- 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,
+ nth_value(tdate, 2) IGNORE NULLS 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)
+);
+
+-- nth_value with IGNORE NULLS option wants to find the second row but
+-- 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)
+  ) AS t(gid, id))
+  SELECT gid, id, nth_value(id, 2) IGNORE NULLS OVER w AS second_val,
+  array_agg(id) OVER w
+  FROM data
+  WINDOW w AS (
+   ORDER BY gid
+   ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+   AFTER MATCH SKIP PAST LAST ROW
+   PATTERN (A+)
+   DEFINE A AS gid < 13
+  );
+
+-- nth_value with IGNORE NULLS option wants to find the third row but
+-- 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)
+  ) AS t(gid, id))
+  SELECT gid, id, nth_value(id, 3) IGNORE NULLS OVER w AS thrid_val,
+  array_agg(id) OVER w
+  FROM data
+  WINDOW w AS (
+   ORDER BY gid
+   ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+   AFTER MATCH SKIP PAST LAST ROW
+   PATTERN (A+)
+   DEFINE A AS gid < 13
+  );
+
+-- nth_value beyond reduced frame with IGNORE NULLS
+-- Tests ignorenulls_getfuncarginframe early out-of-frame check
+SELECT company, tdate, price,
+ nth_value(price, 5) IGNORE NULLS OVER w AS nth_5_in
+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)
+);
+
+-- 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
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+)
+ DEFINE A AS TRUE
+);
+
+-- 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
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+)
+ DEFINE A AS TRUE
+);
+
+--
+-- 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
+);
+
+--
+-- 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)
+);
+
+-- 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
+ 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
+);
+
+-- 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
+ 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
+);
+
+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;
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
new file mode 100644
index 00000000000..86ed06fec68
--- /dev/null
+++ b/src/test/regress/sql/rpr_base.sql
@@ -0,0 +1,4130 @@
+-- ============================================================
+-- RPR Base Tests
+-- Tests for Row Pattern Recognition (ISO/IEC 19075-5:2016)
+-- ============================================================
+--
+-- Parser Layer:
+--   Keyword Usage Tests
+--   DEFINE Clause Tests
+--   FRAME Options Tests
+--   PARTITION BY + FRAME Tests
+--   PATTERN Syntax Tests
+--   Quantifiers Tests
+--   Navigation Functions Tests
+--   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
+--   Absorption Flag Display Tests
+--   Absorption Analysis Tests
+--   Edge Case Tests
+--   Optimization Fallback Tests
+--   Planner Integration Tests
+--   Subquery and CTE Tests
+--   JOIN Tests
+--   Complex Expression Tests
+--   Set Operations Tests
+--   Sorting and Grouping Tests
+--   SQL Function Inlining Tests
+--   Stress Tests
+--   Error Limit Tests
+--
+-- Contributed Tests:
+--   Jacob's Patterns
+--   Pathological Patterns
+-- ============================================================
+
+SET client_min_messages = WARNING;
+
+-- ============================================================
+-- Keyword Usage Tests
+-- ============================================================
+
+-- RPR keywords as column names
+-- Keywords: define, initial, past, pattern, seek
+
+CREATE TABLE rpr_keywords (
+    id INT,
+    define INT,      -- DEFINE keyword
+    initial INT,     -- INITIAL keyword
+    past INT,        -- PAST keyword
+    pattern INT,     -- PATTERN keyword
+    seek INT,        -- SEEK keyword
+    skip INT         -- SKIP keyword (pre-existing)
+);
+
+INSERT INTO rpr_keywords VALUES (1, 10, 20, 30, 40, 50, 60);
+
+SELECT id, define, initial, past, pattern, seek, skip
+FROM rpr_keywords
+ORDER BY id;
+
+DROP TABLE rpr_keywords;
+
+-- ============================================================
+-- DEFINE Clause Tests
+-- ============================================================
+
+
+-- Simple column references
+CREATE TABLE stock_price (
+    dt DATE,
+    symbol TEXT,
+    price NUMERIC,
+    volume INT
+);
+
+INSERT INTO stock_price VALUES
+    ('2024-01-01', 'AAPL', 150, 1000),
+    ('2024-01-02', 'AAPL', 155, 1200),
+    ('2024-01-03', 'AAPL', 152, 900),
+    ('2024-01-04', 'AAPL', 160, 1500),
+    ('2024-01-05', 'AAPL', 158, 1100);
+
+-- Simple column reference
+SELECT dt, price, COUNT(*) OVER w as cnt
+FROM stock_price
+WINDOW w AS (
+    PARTITION BY symbol
+    ORDER BY dt
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (UP+)
+    DEFINE UP AS price > 150
+)
+ORDER BY dt;
+
+-- Multiple column references
+SELECT dt, price, volume, COUNT(*) OVER w as cnt
+FROM stock_price
+WINDOW w AS (
+    PARTITION BY symbol
+    ORDER BY dt
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (GOOD+)
+    DEFINE GOOD AS price > 150 AND volume > 1000
+)
+ORDER BY dt;
+
+-- Expression in DEFINE
+SELECT dt, price, COUNT(*) OVER w as cnt
+FROM stock_price
+WINDOW w AS (
+    PARTITION BY symbol
+    ORDER BY dt
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (HIGH+)
+    DEFINE HIGH AS price * 1.1 > 165
+)
+ORDER BY dt;
+
+-- Arithmetic and functions
+SELECT dt, price, volume, COUNT(*) OVER w as cnt
+FROM stock_price
+WINDOW w AS (
+    PARTITION BY symbol
+    ORDER BY dt
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (CALC+)
+    DEFINE CALC AS (price + volume / 100) > 160
+)
+ORDER BY dt;
+
+DROP TABLE stock_price;
+
+-- Auto-generated DEFINE
+CREATE TABLE rpr_auto (id INT, val INT);
+INSERT INTO rpr_auto VALUES (1, 10), (2, 20), (3, 30), (4, 15);
+
+-- One variable undefined (B auto-generated as "B IS TRUE")
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_auto
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+ B*)
+    DEFINE A AS val > 15
+)
+ORDER BY id;
+
+-- Multiple undefined variables
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_auto
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B C)
+    DEFINE A AS val > 0
+    -- B and C auto-generated as "B IS TRUE", "C IS TRUE"
+)
+ORDER BY id;
+
+-- All variables defined explicitly
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_auto
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (X Y Z)
+    DEFINE
+        X AS val > 10,
+        Y AS val > 20,
+        Z AS val < 20
+)
+ORDER BY id;
+
+DROP TABLE rpr_auto;
+
+-- Duplicate variable names
+CREATE TABLE rpr_dup (id INT);
+INSERT INTO rpr_dup VALUES (1), (2);
+
+-- Duplicate DEFINE entries
+SELECT COUNT(*) OVER w
+FROM rpr_dup
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    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;
+
+-- Boolean coercion
+CREATE TABLE rpr_bool (id INT, flag BOOLEAN);
+INSERT INTO rpr_bool VALUES (1, true), (2, false);
+
+-- Non-boolean expression
+SELECT COUNT(*) OVER w
+FROM rpr_bool
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    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
+FROM rpr_bool
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (T+)
+    DEFINE T AS flag
+)
+ORDER BY id;
+
+-- NULL::boolean
+SELECT id, COUNT(*) OVER w as cnt
+FROM rpr_bool
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (N+)
+    DEFINE N AS NULL::boolean
+)
+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
+CREATE TABLE rpr_complex (id INT, val1 INT, val2 INT);
+INSERT INTO rpr_complex VALUES (1, 10, 20), (2, 15, 25), (3, 20, 30);
+
+-- CASE expression
+SELECT id, val1, val2, COUNT(*) OVER w as cnt
+FROM rpr_complex
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (C+)
+    DEFINE C AS CASE WHEN val1 > 10 THEN val2 > 20 ELSE false END
+)
+ORDER BY id;
+
+DROP TABLE rpr_complex;
+
+-- Pattern variable not in PATTERN (should be ignored)
+CREATE TABLE rpr_unused (id INT);
+INSERT INTO rpr_unused VALUES (1), (2);
+
+-- Extra DEFINE variable
+SELECT id, COUNT(*) OVER w as cnt
+FROM rpr_unused
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS id > 0, B AS id > 5  -- B not in pattern
+)
+ORDER BY id;
+
+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
+    (4, 20), (5, 20),           -- Same val: 20
+    (6, 30);
+
+-- Valid frame options
+
+-- ROWS: counts physical rows (1 FOLLOWING = next 1 physical row)
+-- Expected result: Each row can see 1 physical row ahead
+-- id=1,2,3 (val=10): can see next row -> cnt=2
+-- id=4,5 (val=20): can see next row -> cnt=2
+-- id=6 (val=30): no next row -> cnt=1
+-- Result: [2,2,2,2,2,1]
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_frame
+WINDOW w AS (
+    ORDER BY val
+    ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A B?)
+    DEFINE A AS val >= 0, B AS val >= 0
+)
+ORDER BY id;
+
+-- Invalid frame start positions
+
+-- Not starting at CURRENT ROW
+SELECT COUNT(*) OVER w
+FROM rpr_frame
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS val > 0
+);
+-- Expected: ERROR: FRAME must start at current row when row pattern recognition is used
+
+-- EXCLUDE options
+
+-- EXCLUDE not permitted
+SELECT COUNT(*) OVER w
+FROM rpr_frame
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    EXCLUDE CURRENT ROW
+    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
+FROM rpr_frame
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    EXCLUDE GROUP
+    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
+FROM rpr_frame
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    EXCLUDE TIES
+    PATTERN (A+)
+    DEFINE A AS val > 0
+);
+-- Expected: ERROR: cannot use EXCLUDE options with row pattern recognition
+
+-- RANGE frame not starting at CURRENT ROW
+SELECT COUNT(*) OVER w
+FROM rpr_frame
+WINDOW w AS (
+    ORDER BY id
+    RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
+    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
+SELECT COUNT(*) OVER w
+FROM rpr_frame
+WINDOW w AS (
+    ORDER BY id
+    GROUPS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS val > 0
+);
+-- Expected: ERROR: cannot use FRAME option GROUPS with row pattern recognition
+
+-- Starting with N PRECEDING
+SELECT COUNT(*) OVER w
+FROM rpr_frame
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN 1 PRECEDING AND UNBOUNDED FOLLOWING
+    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
+SELECT COUNT(*) OVER w
+FROM rpr_frame
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN 1 FOLLOWING AND UNBOUNDED FOLLOWING
+    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
+SELECT COUNT(*) OVER w
+FROM rpr_frame
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND 1 PRECEDING
+    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
+SELECT COUNT(*) OVER w
+FROM rpr_frame
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED PRECEDING
+    PATTERN (A+)
+    DEFINE A AS val > 0
+);
+-- Expected: ERROR: frame end cannot be UNBOUNDED PRECEDING
+
+-- Single row frame: CURRENT ROW AND CURRENT ROW
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_frame
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND CURRENT ROW
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A)
+    DEFINE A AS val > 0
+)
+ORDER BY id;
+
+-- Zero offset: CURRENT ROW AND 0 FOLLOWING (equivalent to CURRENT ROW)
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_frame
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND 0 FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A)
+    DEFINE A AS val > 0
+)
+ORDER BY id;
+
+-- Large offset: CURRENT ROW AND 1000 FOLLOWING
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_frame
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND 1000 FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A+)
+    DEFINE A AS val > 0
+)
+ORDER BY id;
+
+-- Maximum offset: CURRENT ROW AND 2147483646 FOLLOWING (INT_MAX - 1)
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_frame
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND 2147483646 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
+WINDOW w AS (
+    ORDER BY val
+    RANGE BETWEEN CURRENT ROW AND 10 FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A B?)
+    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
+FROM rpr_frame
+WINDOW w AS (
+    ORDER BY val
+    GROUPS BETWEEN CURRENT ROW AND 1 FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A B?)
+    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;
+
+-- ============================================================
+-- PARTITION BY + FRAME Tests
+-- ============================================================
+
+-- Test PARTITION BY with RPR to ensure proper partitioning behavior
+CREATE TABLE rpr_partition (id INT, grp INT, val INT);
+INSERT INTO rpr_partition VALUES
+    (1, 1, 10), (2, 1, 20), (3, 1, 30),
+    (4, 2, 15), (5, 2, 25), (6, 2, 35);
+
+-- PARTITION BY with ROWS frame
+SELECT id, grp, val, COUNT(*) OVER w as cnt
+FROM rpr_partition
+WINDOW w AS (
+    PARTITION BY grp
+    ORDER BY val
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A B+)
+    DEFINE A AS val >= 10, B AS val > 15
+)
+ORDER BY id;
+-- Expected: Pattern matching should reset for each partition
+
+-- PARTITION BY with RANGE frame
+SELECT id, grp, val, COUNT(*) OVER w as cnt
+FROM rpr_partition
+WINDOW w AS (
+    PARTITION BY grp
+    ORDER BY val
+    RANGE BETWEEN CURRENT ROW AND 10 FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A B?)
+    DEFINE A AS val >= 10, B AS val >= 20
+)
+ORDER BY id;
+-- Expected: ERROR: cannot use FRAME option RANGE with row pattern recognition
+
+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),
+    (6, 30), (7, 35), (8, 40), (9, 45), (10, 50);
+
+-- Alternation (|)
+
+-- Multiple alternatives
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_pattern
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+ | B+ | C+)
+    DEFINE A AS val > 35, B AS val BETWEEN 15 AND 35, C AS val < 15
+)
+ORDER BY id;
+
+-- Grouping
+
+-- Nested grouping with quantifier
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_pattern
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (((A B) C)+)
+    DEFINE A AS val > 10, B AS val > 20, C AS val > 30
+)
+ORDER BY id;
+
+-- Sequence
+
+-- Multi-element sequence
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_pattern
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B C D E)
+    DEFINE
+        A AS val < 15,
+        B AS val BETWEEN 15 AND 25,
+        C AS val BETWEEN 25 AND 35,
+        D AS val BETWEEN 35 AND 45,
+        E AS val >= 45
+)
+ORDER BY id;
+
+-- Complex combinations
+
+-- Alternation with grouping
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_pattern
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A B) | (C D))
+    DEFINE A AS val < 20, B AS val >= 20, C AS val < 30, D AS val >= 30
+)
+ORDER BY id;
+
+-- Alternation + sequence + grouping
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_pattern
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (START (UP{2,} DOWN? | FLAT+) FINISH)
+    DEFINE
+        START AS val >= 0,
+        UP AS val > 20,
+        DOWN AS val <= 30,
+        FLAT AS val BETWEEN 25 AND 35,
+        FINISH AS val > 40
+)
+ORDER BY id;
+
+-- Nested alternation in groups
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_pattern
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A | B) (C | D))
+    DEFINE A AS val < 15, B AS val BETWEEN 15 AND 25, C AS val BETWEEN 25 AND 35, D AS val > 35
+)
+ORDER BY id;
+
+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),
+    (6, 60), (7, 70), (8, 80), (9, 90), (10, 100);
+
+-- Basic greedy quantifiers
+
+-- * (zero or more)
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_quant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A*)
+    DEFINE A AS val > 0
+)
+ORDER BY id;
+
+-- + (one or more)
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_quant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS val > 50
+)
+ORDER BY id;
+
+-- ? (zero or one)
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_quant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A?)
+    DEFINE A AS val = 50
+)
+ORDER BY id;
+
+-- Edge case quantifiers
+
+-- {0} is not allowed (min must be >= 1)
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_quant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{0} B)
+    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
+FROM rpr_quant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{0,0} B)
+    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
+FROM rpr_quant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{0,1})
+    DEFINE A AS val = 50
+)
+ORDER BY id;
+
+-- Exact quantifiers {n}
+
+-- {3} (representative exact quantifier)
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_quant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{3})
+    DEFINE A AS val > 0
+)
+ORDER BY id;
+
+-- Range quantifiers {n,}
+
+-- {2,} (representative n or more)
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_quant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{2,})
+    DEFINE A AS val > 40
+)
+ORDER BY id;
+
+-- Upper bound quantifiers {,m}
+
+-- {,3} (representative up to m)
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_quant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{,3})
+    DEFINE A AS val > 0
+)
+ORDER BY id;
+
+-- Range quantifiers {n,m}
+
+-- {3,7} (representative range)
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_quant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{3,7})
+    DEFINE A AS val > 0
+)
+ORDER BY id;
+
+DROP TABLE rpr_quant;
+
+-- Reluctant quantifiers
+CREATE TABLE rpr_reluctant (id INT, val INT);
+INSERT INTO rpr_reluctant VALUES (1, 10), (2, 20), (3, 30);
+
+-- *? (zero or more, reluctant)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A*?)
+    DEFINE A AS val > 0
+);
+-- Reluctant quantifier: prefer shortest match
+
+-- +? (one or more, reluctant)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+?)
+    DEFINE A AS val > 0
+);
+-- Reluctant quantifier: prefer shortest match
+
+-- ?? (zero or one, reluctant)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A??)
+    DEFINE A AS val > 0
+);
+-- Reluctant quantifier: prefer shortest match
+
+-- {n,}? (n or more, reluctant)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{2,}?)
+    DEFINE A AS val > 0
+);
+-- Reluctant quantifier: prefer shortest match
+
+-- {n,m}? (n to m, reluctant)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{1,3}?)
+    DEFINE A AS val > 0
+);
+-- Reluctant quantifier: prefer shortest match
+
+-- {n}? (exactly n, reluctant)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{2}?)
+    DEFINE A AS val > 0
+);
+-- Reluctant quantifier: prefer shortest match
+
+-- {,m}? (up to m, reluctant) - COMPLETELY UNTESTED RULE!
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    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
+FROM rpr_reluctant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    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
+FROM rpr_reluctant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    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
+FROM rpr_reluctant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    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
+FROM rpr_reluctant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{1,3}+)
+    DEFINE A AS val > 0
+);
+-- Expected: ERROR: syntax error at or near "+"
+
+-- Boundary errors in reluctant quantifiers
+
+-- {-1}? (negative bound)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{-1}?)
+    DEFINE A AS val > 0
+);
+-- Expected: ERROR: syntax error at or near "-"
+
+-- {2147483647}? (INT_MAX)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{2147483647}?)
+    DEFINE A AS val > 0
+);
+-- Expected: ERROR: quantifier bound must be between 1 and 2147483646
+
+-- {-1,}? (negative lower bound)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{-1,}?)
+    DEFINE A AS val > 0
+);
+-- Expected: ERROR: syntax error at or near "-"
+
+-- {2147483647,}? (INT_MAX lower bound)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{2147483647,}?)
+    DEFINE A AS val > 0
+);
+-- Expected: ERROR: quantifier bound must be between 0 and 2147483646
+
+-- {,0}? (zero upper bound)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{,0}?)
+    DEFINE A AS val > 0
+);
+-- Expected: ERROR: quantifier bound must be between 1 and 2147483646
+
+-- {,2147483647}? (INT_MAX upper bound)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{,2147483647}?)
+    DEFINE A AS val > 0
+);
+-- Expected: ERROR: quantifier bound must be between 1 and 2147483646
+
+-- {-1,3}? (negative lower in range)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{-1,3}?)
+    DEFINE A AS val > 0
+);
+-- Expected: ERROR: syntax error at or near "-"
+
+-- {1,2147483647}? (INT_MAX upper in range)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    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)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    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)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A* ?)
+    DEFINE A AS val > 0
+);
+-- Reluctant quantifier: prefer shortest match
+
+-- + ? (token separated)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+ ?)
+    DEFINE A AS val > 0
+);
+-- Reluctant quantifier: prefer shortest match
+
+-- {2,} ? (token separated)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{2,} ?)
+    DEFINE A AS val > 0
+);
+-- Reluctant quantifier: prefer shortest match
+
+-- Invalid token combinations
+
+-- * + (invalid combination)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A* +)
+    DEFINE A AS val > 0
+);
+-- Expected: ERROR: syntax error at or near "+"
+
+-- + * (invalid combination)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+ *)
+    DEFINE A AS val > 0
+);
+-- Expected: ERROR: syntax error at or near "*"
+
+-- ? ? (parsed as ?? reluctant quantifier)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A? ?)
+    DEFINE A AS val > 0
+);
+-- 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
+SELECT COUNT(*) OVER w
+FROM rpr_bounds
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{5,3})
+    DEFINE A AS id > 0
+);
+-- Expected: ERROR: quantifier minimum bound must not exceed maximum
+
+-- Large bounds
+SELECT COUNT(*) OVER w
+FROM rpr_bounds
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{1000,2000})
+    DEFINE A AS id > 0
+);
+
+-- Very large bound
+SELECT COUNT(*) OVER w
+FROM rpr_bounds
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{100000})
+    DEFINE A AS id > 0
+);
+
+-- INT_MAX - 1 = 2147483646 (at limit)
+SELECT COUNT(*) OVER w
+FROM rpr_bounds
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{2147483646})
+    DEFINE A AS id > 0
+);
+
+-- INT_MAX = 2147483647 (over limit)
+SELECT COUNT(*) OVER w
+FROM rpr_bounds
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    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,}
+SELECT COUNT(*) OVER w
+FROM rpr_bounds
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{-1,})
+    DEFINE A AS id > 0
+);
+-- Expected: ERROR: syntax error at or near "-"
+
+-- INT_MAX in {n,}
+SELECT COUNT(*) OVER w
+FROM rpr_bounds
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{2147483647,})
+    DEFINE A AS id > 0
+);
+-- Expected: ERROR: quantifier bound must be between 0 and 2147483646
+
+-- {,m} boundary errors
+
+-- Zero upper bound in {,m}
+SELECT COUNT(*) OVER w
+FROM rpr_bounds
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{,0})
+    DEFINE A AS id > 0
+);
+-- Expected: ERROR: quantifier bound must be between 1 and 2147483646
+
+-- INT_MAX in {,m}
+SELECT COUNT(*) OVER w
+FROM rpr_bounds
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{,2147483647})
+    DEFINE A AS id > 0
+);
+-- Expected: ERROR: quantifier bound must be between 1 and 2147483646
+
+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);
+
+-- PREV function - reference previous row in pattern
+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 > PREV(val)
+)
+ORDER BY id;
+
+-- NEXT function - reference next row in pattern
+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 < NEXT(val),
+        B AS val > 0
+)
+ORDER BY id;
+
+-- Combined PREV and NEXT
+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 C)
+    DEFINE
+        A AS val > 0,
+        B AS val > PREV(val) AND val < NEXT(val),
+        C AS val > PREV(val)
+)
+ORDER BY id;
+
+-- PREV function cannot be used other than in DEFINE
+SELECT PREV(id), 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 > 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
+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 > 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
+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: 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;
+
+-- ============================================================
+-- 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),
+    (6, 6), (7, 7), (8, 8);
+
+-- SKIP TO NEXT ROW
+
+-- SKIP TO NEXT ROW
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_skip
+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 > 0, B AS val > 2, C AS val > 4
+)
+ORDER BY id;
+
+-- SKIP PAST LAST ROW
+
+-- SKIP PAST LAST ROW
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_skip
+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 val > 0, B AS val > 2, C AS val > 4
+)
+ORDER BY id;
+
+-- Default behavior (should be SKIP PAST LAST ROW)
+
+-- No SKIP TO clause (default)
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_skip
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B)
+    DEFINE A AS val > 0, B AS val > 1
+)
+ORDER BY id;
+
+-- Compare default with explicit PAST LAST ROW
+-- Results should be identical
+WITH default_skip AS (
+    SELECT id, val, COUNT(*) OVER w as cnt
+    FROM rpr_skip
+    WINDOW w AS (
+        ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B C)
+        DEFINE A AS val > 0, B AS val > 2, C AS val > 4
+    )
+),
+explicit_skip AS (
+    SELECT id, val, COUNT(*) OVER w as cnt
+    FROM rpr_skip
+    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 val > 0, B AS val > 2, C AS val > 4
+    )
+)
+SELECT 'default' as type, * FROM default_skip
+UNION ALL
+SELECT 'explicit' as type, * FROM explicit_skip
+ORDER BY type, id;
+
+DROP TABLE rpr_skip;
+
+-- INITIAL clause
+
+CREATE TABLE rpr_init (id INT, val INT);
+INSERT INTO rpr_init VALUES (1, 10), (2, 20), (3, 30), (4, 40);
+
+-- Explicit INITIAL
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_init
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A+)
+    DEFINE A AS val > 0
+)
+ORDER BY id;
+
+-- Implicit INITIAL (default)
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_init
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS val > 0
+)
+ORDER BY id;
+
+DROP TABLE rpr_init;
+
+-- SEEK
+
+CREATE TABLE rpr_seek (id INT, val INT);
+INSERT INTO rpr_seek VALUES (1, 10);
+
+-- SEEK keyword
+SELECT COUNT(*) OVER w
+FROM rpr_seek
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    SEEK
+    PATTERN (A+)
+    DEFINE A AS val > 0
+);
+-- Expected: ERROR: SEEK is not supported
+-- HINT: Use INITIAL instead.
+
+DROP TABLE rpr_seek;
+
+-- ============================================================
+-- Serialization/Deserialization Tests
+-- ============================================================
+
+
+-- View creation and deparsing
+
+CREATE TABLE rpr_serial (id INT, val INT);
+INSERT INTO rpr_serial VALUES
+    (1, 10), (2, 20), (3, 15), (4, 25), (5, 30);
+
+-- Simple pattern
+CREATE VIEW rpr_serial_v1 AS
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_serial
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS val > 0
+);
+
+-- Verify view works (tests deserialization)
+SELECT * FROM rpr_serial_v1 ORDER BY id;
+
+-- Verify deparsing
+SELECT pg_get_viewdef('rpr_serial_v1'::regclass);
+
+-- Complex pattern with alternation
+CREATE VIEW rpr_serial_v2 AS
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_serial
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+ | B*)
+    DEFINE A AS val > 20, B AS val <= 20
+);
+
+SELECT * FROM rpr_serial_v2 ORDER BY id;
+SELECT pg_get_viewdef('rpr_serial_v2'::regclass);
+
+-- Pattern with grouping and quantifiers
+CREATE VIEW rpr_serial_v3 AS
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_serial
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A B){2,5} | C*)
+    DEFINE
+        A AS val > 10,
+        B AS val > 20,
+        C AS val <= 10
+);
+
+SELECT * FROM rpr_serial_v3 ORDER BY id;
+SELECT pg_get_viewdef('rpr_serial_v3'::regclass);
+
+-- All features combined
+CREATE VIEW rpr_serial_v4 AS
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_serial
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    INITIAL
+    PATTERN (START (MID{1,3} | ALT+) FINISH)
+    DEFINE
+        START AS val > 5,
+        MID AS val BETWEEN 10 AND 25,
+        ALT AS val > 25,
+        FINISH AS val > 15
+);
+
+SELECT * FROM rpr_serial_v4 ORDER BY id;
+SELECT pg_get_viewdef('rpr_serial_v4'::regclass);
+
+-- Additional quantifiers for deparsing coverage
+
+-- ? quantifier (zero or one)
+CREATE VIEW rpr_serial_v5 AS
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_serial
+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
+);
+
+SELECT * FROM rpr_serial_v5 ORDER BY id;
+SELECT pg_get_viewdef('rpr_serial_v5'::regclass);
+
+-- {n,} quantifier (n or more)
+CREATE VIEW rpr_serial_v6 AS
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_serial
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{2,})
+    DEFINE A AS val > 15
+);
+
+SELECT * FROM rpr_serial_v6 ORDER BY id;
+SELECT pg_get_viewdef('rpr_serial_v6'::regclass);
+
+-- {n} quantifier (exactly n)
+CREATE VIEW rpr_serial_v7 AS
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_serial
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{3})
+    DEFINE A AS val > 0
+);
+
+SELECT * FROM rpr_serial_v7 ORDER BY id;
+SELECT pg_get_viewdef('rpr_serial_v7'::regclass);
+
+-- Nested ALT pattern (tests deparse of complex nested structure)
+CREATE VIEW rpr_serial_v8 AS
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_serial
+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 <= 15, B AS val <= 25, C AS val <= 30, D AS val > 30
+);
+
+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
+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);
+
+-- 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);
+INSERT INTO rpr_mview VALUES (1, 10), (2, 20), (3, 30);
+
+CREATE MATERIALIZED VIEW rpr_mview_v1 AS
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_mview
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS val > 0
+);
+
+SELECT * FROM rpr_mview_v1 ORDER BY id;
+SELECT pg_get_viewdef('rpr_mview_v1'::regclass);
+
+-- Refresh test
+REFRESH MATERIALIZED VIEW rpr_mview_v1;
+SELECT * FROM rpr_mview_v1 ORDER BY id;
+
+-- 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);
+INSERT INTO rpr_prep VALUES (1, 10), (2, 20), (3, 30);
+
+-- Simple prepared statement
+PREPARE rpr_prep_simple AS
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_prep
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS val > 0
+)
+ORDER BY id;
+
+EXECUTE rpr_prep_simple;
+EXECUTE rpr_prep_simple;
+
+DEALLOCATE rpr_prep_simple;
+
+-- Prepared statement with parameters
+PREPARE rpr_prep_param(int) AS
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_prep
+WHERE id <= $1
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS val > 10
+)
+ORDER BY id;
+
+EXECUTE rpr_prep_param(2);
+EXECUTE rpr_prep_param(3);
+
+DEALLOCATE rpr_prep_param;
+
+-- Complex prepared statement
+PREPARE rpr_prep_complex AS
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_prep
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A B){1,2} | C+)
+    DEFINE
+        A AS val > 5,
+        B AS val > 15,
+        C AS val <= 15
+)
+ORDER BY id;
+
+EXECUTE rpr_prep_complex;
+EXECUTE rpr_prep_complex;
+
+DEALLOCATE rpr_prep_complex;
+
+DROP TABLE rpr_prep;
+
+-- CTE and Subquery (tests copyfuncs.c)
+
+CREATE TABLE rpr_copy (id INT, val INT);
+INSERT INTO rpr_copy VALUES (1, 10), (2, 20), (3, 30), (4, 40);
+
+-- Simple CTE
+WITH rpr_cte AS (
+    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+)
+        DEFINE A AS val > 0
+    )
+)
+SELECT * FROM rpr_cte ORDER BY id;
+
+-- CTE with multiple references (forces node copy)
+WITH rpr_cte AS (
+    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+)
+        DEFINE A AS val > 15
+    )
+)
+SELECT c1.id, c1.cnt as cnt1, c2.cnt as cnt2
+FROM rpr_cte c1
+JOIN rpr_cte c2 ON c1.id = c2.id
+ORDER BY c1.id;
+
+-- Subquery in FROM clause
+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;
+
+-- Nested subqueries
+SELECT *
+FROM (
+    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+)
+            DEFINE A AS val >= 10
+        )
+    ) inner_sub
+    WHERE cnt > 0
+) outer_sub
+ORDER BY id;
+
+DROP TABLE rpr_copy;
+
+-- DISTINCT and set operations (tests equalfuncs.c)
+
+CREATE TABLE rpr_equal (id INT, val INT);
+INSERT INTO rpr_equal VALUES (1, 10), (2, 20), (3, 10), (4, 20);
+
+-- DISTINCT with RPR
+SELECT DISTINCT cnt
+FROM (
+    SELECT id, val, COUNT(*) OVER w as cnt
+    FROM rpr_equal
+    WINDOW w AS (
+        ORDER BY val
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        AFTER MATCH SKIP TO NEXT ROW
+        PATTERN (A+)
+        DEFINE A AS val > 0
+    )
+) sub
+ORDER BY cnt;
+
+-- UNION with RPR in both sides
+SELECT id, val, cnt FROM (
+    SELECT id, val, COUNT(*) OVER w as cnt
+    FROM rpr_equal
+    WHERE val = 10
+    WINDOW w AS (
+        ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A+)
+        DEFINE A AS val > 0
+    )
+) sub1
+UNION
+SELECT id, val, cnt FROM (
+    SELECT id, val, COUNT(*) OVER w as cnt
+    FROM rpr_equal
+    WHERE val = 20
+    WINDOW w AS (
+        ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A+)
+        DEFINE A AS val > 0
+    )
+) sub2
+ORDER BY id;
+
+-- UNION ALL
+SELECT id, cnt FROM (
+    SELECT id, COUNT(*) OVER w as cnt
+    FROM rpr_equal
+    WINDOW w AS (
+        ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A+)
+        DEFINE A AS val > 10
+    )
+) sub
+UNION ALL
+SELECT id, cnt FROM (
+    SELECT id, COUNT(*) OVER w as cnt
+    FROM rpr_equal
+    WINDOW w AS (
+        ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (B+)
+        DEFINE B AS val <= 10
+    )
+) sub
+ORDER BY id, cnt;
+
+-- INTERSECT
+SELECT id, cnt FROM (
+    SELECT id, COUNT(*) OVER w as cnt
+    FROM rpr_equal
+    WHERE id <= 3
+    WINDOW w AS (
+        ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A+)
+        DEFINE A AS val > 0
+    )
+) sub1
+INTERSECT
+SELECT id, cnt FROM (
+    SELECT id, COUNT(*) OVER w as cnt
+    FROM rpr_equal
+    WHERE id >= 2
+    WINDOW w AS (
+        ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A+)
+        DEFINE A AS val > 0
+    )
+) sub2
+ORDER BY id;
+
+DROP TABLE rpr_equal;
+
+-- View with multiple window definitions
+
+CREATE TABLE rpr_multiwin (id INT, val INT);
+INSERT INTO rpr_multiwin VALUES (1, 10), (2, 20), (3, 30);
+
+CREATE VIEW rpr_multiwin_v AS
+SELECT
+    id,
+    val,
+    COUNT(*) OVER w1 as cnt1,
+    COUNT(*) OVER w2 as cnt2
+FROM rpr_multiwin
+WINDOW
+    w1 AS (
+        ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A+)
+        DEFINE A AS val > 15
+    ),
+    w2 AS (
+        ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (B*)
+        DEFINE B AS val <= 15
+    );
+
+SELECT * FROM rpr_multiwin_v ORDER BY id;
+SELECT pg_get_viewdef('rpr_multiwin_v'::regclass);
+
+-- {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);
+
+-- {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);
+
+-- ============================================================
+-- 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
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+!)
+    DEFINE A AS val > 0
+);
+-- Expected: Syntax error
+
+-- Unmatched parentheses
+SET client_min_messages = NOTICE;
+DO $$
+BEGIN
+    EXECUTE '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 val > 0, B AS val > 10)';
+    RAISE NOTICE 'Unmatched parentheses: UNEXPECTED SUCCESS';
+EXCEPTION
+    WHEN syntax_error THEN
+        RAISE NOTICE 'Unmatched parentheses: EXPECTED ERROR - %', SQLERRM;
+    WHEN OTHERS THEN
+        RAISE NOTICE 'Unmatched parentheses: UNEXPECTED ERROR - %', SQLERRM;
+END $$;
+SET client_min_messages = WARNING;
+
+-- Empty DEFINE
+SELECT COUNT(*) OVER w
+FROM rpr_err
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE
+);
+-- Expected: Syntax error
+
+-- Empty PATTERN
+SELECT COUNT(*) OVER w
+FROM rpr_err
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ()
+    DEFINE A AS val > 0
+);
+-- Expected: Syntax error
+
+-- DEFINE without PATTERN (PATTERN and DEFINE must be used together)
+SELECT COUNT(*) OVER w
+FROM rpr_err
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    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)
+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 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
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    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
+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
+);
+-- Expected: ERROR: pattern variable qualified column reference "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
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    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
+FROM rpr_err
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS nonexistent_column > 0
+);
+-- Expected: ERROR: column "nonexistent_column" does not exist
+
+-- Type mismatch
+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 > 'string'
+);
+-- Expected: ERROR: invalid input syntax for type integer: "string"
+
+-- Aggregate function in DEFINE (if not allowed)
+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 COUNT(*) > 0
+);
+-- Expected: ERROR: aggregate functions are not allowed in DEFINE
+
+-- Subquery in DEFINE (NOT SUPPORTED)
+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 > (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
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS val > 0, B AS val > 5, C AS val > 10
+)
+ORDER BY id;
+
+DROP TABLE rpr_err;
+
+-- NULL handling
+
+CREATE TABLE rpr_null (id INT, val INT);
+INSERT INTO rpr_null VALUES (1, 10), (2, NULL), (3, 30);
+
+-- NULL in DEFINE expression
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_null
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS val > 15
+)
+ORDER BY id;
+
+-- IS NULL in DEFINE
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_null
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (N+)
+    DEFINE N AS val IS NULL
+)
+ORDER BY id;
+
+-- IS NOT NULL in DEFINE
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_null
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (NN+)
+    DEFINE NN AS val IS NOT NULL
+)
+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
+-- ============================================================
+
+-- 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
+-- ============================================================
+-- Tests for pattern optimization in optimizer/plan/rpr.c
+-- Use EXPLAIN to verify optimized pattern (shown as "Pattern: ...")
+
+CREATE TABLE rpr_plan (id INT, val INT);
+INSERT INTO rpr_plan VALUES
+    (1, 10), (2, 20), (3, 30), (4, 40), (5, 50),
+    (6, 60), (7, 70), (8, 80), (9, 90), (10, 100);
+
+-- Consecutive VAR merge: A A A -> 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 A A) DEFINE A AS val > 0);
+
+-- Consecutive VAR merge: A{2} A{3} -> a{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{2} A{3}) DEFINE A AS val > 0);
+
+-- Consecutive VAR merge: 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: 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 GROUP merge with finite quantifiers: ((A B){5}) ((A B){10}) -> merged
+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){5}) ((A B){10})) DEFINE A AS val <= 50, B AS val > 50);
+
+-- Consecutive GROUP merge with unbounded: (A B)+ (A B)+ -> (a b){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 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
+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} (A B)+) 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
+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);
+
+-- PREFIX and SUFFIX merge: A B (A B)+ A B -> (a b){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 B (A B)+ A B) DEFINE A AS val <= 40, B AS val > 40);
+
+-- Flatten nested: A ((B) (C)) -> 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))) 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
+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);
+
+-- ALT deduplicate: (A | B | A) -> (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)+) 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
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN ((A{2}){3}) DEFINE A AS val > 0);
+
+-- Quantifier multiply with child range: (A{2,3}){3} -> a{6,9}
+-- outer exact, child range - optimization applies
+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}){3}) DEFINE A AS val > 0);
+
+-- 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
+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}){2,3}) DEFINE A AS val > 0);
+
+-- 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
+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}){2,}) DEFINE A AS val > 0);
+
+-- Quantifier multiply: (A){2,} -> a{2,}
+-- child exact 1 - no gaps, optimization applies
+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,}) DEFINE A AS val > 0);
+
+-- Quantifier multiply: (A)+ -> a+
+-- child exact 1 - no gaps, optimization applies
+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)+) DEFINE A AS val > 0);
+
+-- 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)
+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,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)
+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);
+
+-- Nested unbounded: (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*)*) DEFINE A AS val > 0);
+
+-- Nested unbounded: (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+)*) DEFINE A AS val > 0);
+
+-- Nested unbounded: (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+)+) DEFINE A AS val > 0);
+
+-- Unwrap GROUP{1,1}: (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)) DEFINE A AS val > 0);
+
+-- Unwrap GROUP{1,1}: (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)) DEFINE A AS val <= 50, B AS val > 50);
+
+-- Combined optimization: A A (B B)+ B B C C C -> a{2} (b{2}){2,} c{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 A (B B)+ B B C C C)
+             DEFINE A AS val <= 20, B AS val > 20 AND val <= 70, C AS val > 70);
+
+-- Consecutive GROUP merge with unbounded: (A+) (A+) -> a{2,}
+-- Tests mergeConsecutiveGroups with child->max == INF
+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 GROUP merge finite: (A{10}){20} -> a{200}
+-- Tests mergeConsecutiveGroups with both finite
+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{10}){20}) DEFINE A AS val > 0);
+
+-- Different GROUP prevents merge: (A B){2} (C D){3}
+-- Tests mergeConsecutiveGroups flush previous
+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} (C D){3})
+             DEFINE A AS val <= 25, B AS val > 25 AND val <= 50,
+                    C AS val > 50 AND val <= 75, D AS val > 75);
+
+-- Different children count prevents merge: (A B)+ (A B C)+
+-- Tests rprPatternChildrenEqual length check
+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 C)+)
+             DEFINE A AS val <= 33, B AS val > 33 AND val <= 66, C AS val > 66);
+
+-- PREFIX only merge: A B (A B)+ -> (a b){2,}
+-- Tests mergeGroupPrefixSuffix: absorb preceding elements into GROUP min
+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);
+
+-- SUFFIX only merge: (A B)+ A B -> (a b){2,}
+-- Tests mergeGroupPrefixSuffix: absorb following elements into GROUP min
+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);
+
+-- Multiple SUFFIX absorption with skipUntil: (A B)+ A B A B C
+-- Tests mergeGroupPrefixSuffix: skip absorbed suffix elements
+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 C)
+             DEFINE A AS val <= 50, B AS val > 50 AND val <= 75, C AS val > 75);
+
+-- PREFIX merge with remaining prefix: A B C D (C D)+
+-- Tests mergeGroupPrefixSuffix: trimmed list reconstruction
+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 (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);
+
+-- PREFIX merge with quantifiers: A B* (A B*)+ -> (a b*){2,}
+-- Tests mergeGroupPrefixSuffix: quantifier comparison in rprPatternEqual
+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);
+
+-- PREFIX merge with multiple quantifiers: A+ B* C? (A+ B* C?)+ -> (a+ b* c?){2,}
+-- Tests mergeGroupPrefixSuffix: complex quantifier patterns
+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? (A+ B* C?)+)
+             DEFINE A AS val <= 30, B AS val > 30 AND val <= 60, C AS val > 60);
+
+-- SUFFIX merge with quantifiers: (A B*)+ A B* -> (a b*){2,}
+-- Tests mergeGroupPrefixSuffix: suffix with quantifiers
+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);
+
+-- Unwrap GROUP{1,1}: ((A | B | C)) -> (a | b | c)
+-- Tests tryUnwrapGroup removing redundant outer GROUP
+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)) 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)
+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);
+
+-- Reluctant optimization bypass: GROUP merge
+-- (A B)+? (A B) stays separate (greedy merges to (a b){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 B)+? (A B)) DEFINE A AS val <= 50, B AS val > 50);
+
+-- Reluctant optimization bypass: quantifier multiply (outer reluctant)
+-- (A{2}){3}? stays as (a{2}){3}? (greedy merges to a{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{2}){3}?) DEFINE A AS val > 0);
+
+-- Reluctant optimization bypass: quantifier multiply (inner reluctant)
+-- (A{2}?){3} stays as (a{2}?){3} (greedy merges to a{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{2}?){3}) DEFINE A AS val > 0);
+
+-- Reluctant optimization bypass: PREFIX merge
+-- A B (A B)+? stays separate (greedy merges to (a b){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 B (A B)+?) DEFINE A AS val <= 50, B AS val > 50);
+
+-- Reluctant optimization bypass: SUFFIX merge
+-- (A B)+? A B stays separate (greedy merges to (a b){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 B)+? A B) DEFINE A AS val <= 50, B AS val > 50);
+
+-- GROUP unwrap with quantifier propagation: (A)?? B -> a?? b
+-- Single VAR child {1,1} receives GROUP's quantifier and reluctant
+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) DEFINE A AS val <= 50, B AS val > 50);
+
+-- Reluctant preserved through ALT flatten
+-- (A | (B | C))+? flattens to (a | b | c)+? - inner ALT flattened, reluctant kept
+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))+?) DEFINE A AS val <= 30, B AS val <= 60, C AS val > 60);
+
+-- Reluctant optimization bypass: absorption flags
+-- A+? with SKIP PAST LAST ROW - no absorption markers (greedy A+ gets a+")
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             AFTER MATCH SKIP PAST LAST ROW PATTERN (A+?) DEFINE A AS val > 0);
+
+-- 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
+-- ============================================================
+-- Tests absorption marker display in EXPLAIN output
+-- Markers: ' = branch element, " = judgment point
+-- Files: explain.c (append_rpr_quantifier, deparse_rpr_pattern)
+
+-- Simple VAR: A+ -> a+" (judgment point)
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             AFTER MATCH SKIP PAST LAST ROW PATTERN (A+) DEFINE A AS val > 0);
+
+-- GROUP unbounded: (A B)+ -> (a' b')+" (branch + judgment)
+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);
+
+-- ALT both absorbable: 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
+             AFTER MATCH SKIP PAST LAST ROW PATTERN (A+ | B+) DEFINE A AS val <= 50, B AS val > 50);
+
+-- ALT one absorbable: 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
+             AFTER MATCH SKIP PAST LAST ROW PATTERN (A+ | B) DEFINE A AS val <= 50, B AS val > 50);
+
+-- Sequence with absorbable start: 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
+             AFTER MATCH SKIP PAST LAST ROW PATTERN (A+ B) DEFINE A AS val <= 50, B AS val > 50);
+
+-- Complex nested: ((A+ B) | C) D | A B C - deeply nested ALT
+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) D | A B C)
+             DEFINE A AS val <= 30, B AS val <= 60, C AS val <= 80, D AS val > 80);
+
+-- Nested unbounded: (A+ | B)+ -> (a+" | b)+ (first iteration 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)+)
+             DEFINE A AS val <= 50, B AS val > 50);
+
+-- ALT inside unbounded GROUP: (A+ B | A B)* -> (a+" b | a b)* (first iteration 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 | 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
+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 (no unbounded branch): (A | B){2,} -> (a | b){2,} (no markers)
+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,}) DEFINE A AS val <= 50, B AS val > 50);
+
+-- Non-absorbable (SKIP TO NEXT ROW): A+ -> a+ (no markers)
+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 TO NEXT ROW PATTERN (A+) DEFINE A AS val > 0);
+
+-- Non-absorbable (limited frame): A+ -> a+ (no markers)
+EXPLAIN (COSTS OFF)
+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
+-- ============================================================
+-- Tests context absorption optimization (O(n^2) -> O(n))
+-- Files: rpr.c (computeAbsorbability)
+
+-- Simple Absorbable Pattern: A+ B
+-- Pattern starts with unbounded VAR
+
+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)
+    DEFINE A AS val <= 50, B AS val > 50
+)
+ORDER BY id;
+
+-- Absorbable GROUP Pattern: (A B)+ C
+-- Pattern starts with unbounded GROUP
+
+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)+ C)
+    DEFINE A AS val <= 30, B AS val > 30 AND val <= 60, C AS val > 60
+)
+ORDER BY id;
+
+-- Non-Absorbable: Unbounded Not at Start
+-- Pattern: A B+ (unbounded not at start)
+
+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+)
+    DEFINE A AS val <= 50, B AS val > 50
+)
+ORDER BY id;
+
+-- ALT with Absorbable Branches
+-- Pattern: (A+ | B+) C - both branches absorbable
+
+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+) C)
+    DEFINE A AS val <= 30, B AS val > 30 AND val <= 60, C AS val > 60
+)
+ORDER BY id;
+
+-- ALT with Mixed Branches
+-- Pattern: (A+ | B C) - only first branch absorbable
+
+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 C)+)
+    DEFINE A AS val <= 30, B AS val > 30 AND val <= 60, C AS val > 60
+)
+ORDER BY id;
+
+-- Non-Absorbable: ALT Inside GROUP
+-- Pattern: (A | B){2,} - ALT inside unbounded GROUP
+
+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){2,})
+    DEFINE A AS val <= 50, B AS val > 50
+)
+ORDER BY id;
+
+-- Non-Absorbable: Nested Unbounded
+-- Pattern: ((A B)+ C)+ - nested GROUP structure
+
+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)+ C)+)
+    DEFINE A AS val <= 30, B AS val > 30 AND val <= 60, C AS val > 60
+)
+ORDER BY id;
+
+-- Non-Absorbable: Unbounded Element Inside GROUP
+-- Pattern: (A B+){2,} - unbounded inside GROUP
+
+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+){2,})
+    DEFINE A AS val <= 50, B AS val > 50
+)
+ORDER BY id;
+
+-- Runtime Conditions: SKIP TO NEXT ROW
+-- Absorption disabled with SKIP TO NEXT ROW
+
+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)
+    DEFINE A AS val <= 50, B AS val > 50
+)
+ORDER BY id;
+
+-- Runtime Conditions: Limited Frame
+-- Absorption disabled with limited frame end
+
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_plan
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND 5 FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A+ B)
+    DEFINE A AS val <= 50, B AS val > 50
+)
+ORDER BY id;
+
+-- ============================================================
+-- Edge Case Tests
+-- ============================================================
+-- Tests boundary conditions and complex scenarios
+
+-- Empty Match Prevention
+-- Pattern that could match empty: A*
+
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_plan
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A*)
+    DEFINE A AS val > 1000  -- Never matches
+)
+ORDER BY id;
+
+-- All Rows Match
+-- Pattern where every row matches
+
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_plan
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS val >= 0  -- Always true
+)
+ORDER BY id;
+
+-- Large Quantifiers
+-- Pattern: A{100} (large exact quantifier)
+
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_plan
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{100})
+    DEFINE A AS val > 0
+)
+ORDER BY id;
+
+-- Pattern: A{10,20} (large range quantifier)
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_plan
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{10,20})
+    DEFINE A AS val > 0
+)
+ORDER BY id;
+
+-- Complex Multi-Level Nesting
+-- Pattern: (((A B) | C)+ D)+
+
+SELECT id, val, COUNT(*) OVER w as cnt
+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 <= 20, B AS val > 20 AND val <= 40,
+           C AS val > 40 AND val <= 60, D AS val > 60
+)
+ORDER BY id;
+
+-- Long Alternation Chain
+-- Pattern: A | B | C | D | E (5-way ALT)
+
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_plan
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A | B | C | D | E)
+    DEFINE A AS val = 10, B AS val = 30, C AS val = 50,
+           D AS val = 70, E AS val = 90
+)
+ORDER BY id;
+
+-- Long Sequence
+-- Pattern: A B C D E F G H (8-element SEQ)
+
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_plan
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B C D E F G H)
+    DEFINE A AS val >= 10, B AS val >= 20, C AS val >= 30,
+           D AS val >= 40, E AS val >= 50, F AS val >= 60,
+           G AS val >= 70, H AS val >= 80
+)
+ORDER BY id;
+
+-- Interleaved Quantifiers
+-- Pattern: A{2} B+ C{3,5} D* E{1,}
+
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_plan
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{2} B+ C{3,5} D* E{1,})
+    DEFINE A AS val > 0, B AS val > 0, C AS val > 0,
+           D AS val > 0, E AS val > 0
+)
+ORDER BY id;
+
+-- ============================================================
+-- Optimization Fallback Tests
+-- ============================================================
+-- Tests for optimization edge cases and fallback behavior
+
+CREATE TABLE rpr_fallback (id INT, val INT);
+INSERT INTO rpr_fallback VALUES (1, 10), (2, 20);
+
+-- Test: min quantifier overflow causes optimization fallback (min == max case)
+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{2000000000}){2})
+    DEFINE A AS val > 0
+);
+-- Expected: Fallback - pattern not merged due to min overflow (4000000000 > INT32_MAX)
+
+-- Test: max-only quantifier overflow causes optimization 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{1,2000000000}){2})
+    DEFINE A AS val > 0
+);
+-- 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)
+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{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)
+SELECT COUNT(*) OVER w FROM rpr_fallback
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A{2000000000,}){2000000000,})
+    DEFINE A AS val > 0
+);
+-- Expected: Fallback - min overflow (2000000000 * 2000000000 > INT32_MAX)
+
+-- Test: prefix mismatch causes optimization 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 (C D)+)
+    DEFINE A AS val > 0, B AS val > 5, C AS val > 10, D AS val > 15
+);
+-- Expected: Fallback - prefix elements don't match GROUP content
+
+DROP TABLE rpr_fallback;
+
+-- ============================================================
+-- Planner Integration Tests
+-- ============================================================
+-- Tests full planning pipeline and WindowAgg plan node creation
+-- Files: planner.c, createplan.c
+
+CREATE TABLE rpr_planner (id INT, category VARCHAR(10), val INT);
+INSERT INTO rpr_planner VALUES
+    (1, 'A', 10), (2, 'A', 20), (3, 'A', 30),
+    (4, 'B', 40), (5, 'B', 50), (6, 'B', 60),
+    (7, 'C', 70), (8, 'C', 80), (9, 'C', 90);
+
+-- Multiple Window Functions in Same Query
+SELECT id, category, val,
+       COUNT(*) OVER w1 as cnt1,
+       COUNT(*) OVER w2 as cnt2
+FROM rpr_planner
+WINDOW w1 AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS val > 0
+),
+w2 AS (
+    PARTITION BY category
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (B+)
+    DEFINE B AS val >= 40
+)
+ORDER BY id;
+
+-- Window Function with PARTITION BY
+
+SELECT id, category, val,
+       COUNT(*) OVER w as cnt
+FROM rpr_planner
+WINDOW w AS (
+    PARTITION BY category
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS val > 0
+)
+ORDER BY category, id;
+
+-- Window Function with Complex ORDER BY
+
+SELECT id, category, val,
+       COUNT(*) OVER w as cnt
+FROM rpr_planner
+WINDOW w AS (
+    ORDER BY category DESC, val ASC
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS val > 0
+)
+ORDER BY category DESC, val ASC;
+
+-- Named Window Reference
+
+SELECT id, category, val,
+       COUNT(*) OVER w as cnt
+FROM rpr_planner
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS val > 0
+)
+ORDER BY id;
+
+-- Inline Window Definition
+
+SELECT id, category, val,
+       COUNT(*) OVER (
+           ORDER BY id
+           ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+           PATTERN (A+)
+           DEFINE A AS val > 0
+       ) as cnt
+FROM rpr_planner
+ORDER BY id;
+
+-- Window with Aggregate Functions
+SELECT category,
+       COUNT(*) OVER w as window_cnt,
+       COUNT(*) as agg_cnt
+FROM rpr_planner
+WINDOW w AS (
+    PARTITION BY category
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS val > 0
+)
+GROUP BY category
+ORDER BY category;
+-- 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
+-- ============================================================
+-- Tests RPR with subqueries and CTEs
+
+-- RPR in Subquery (FROM clause)
+
+SELECT * FROM (
+    SELECT id, category, val,
+           COUNT(*) OVER w as cnt
+    FROM rpr_planner
+    WINDOW w AS (
+        ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A+)
+        DEFINE A AS val > 0
+    )
+) sub
+WHERE cnt > 5
+ORDER BY id;
+
+-- RPR with Subquery in WHERE
+
+SELECT id, category, val,
+       COUNT(*) OVER w as cnt
+FROM rpr_planner
+WHERE val > (SELECT AVG(val) FROM rpr_planner)
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS val > 50
+)
+ORDER BY id;
+
+-- CTE with RPR
+
+WITH rpr_cte AS (
+    SELECT id, category, val,
+           COUNT(*) OVER w as cnt
+    FROM rpr_planner
+    WINDOW w AS (
+        ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A+)
+        DEFINE A AS val > 0
+    )
+)
+SELECT * FROM rpr_cte WHERE cnt > 5 ORDER BY id;
+
+-- Multiple CTE References
+
+WITH rpr_cte AS (
+    SELECT id, category, val,
+           COUNT(*) OVER w as cnt
+    FROM rpr_planner
+    WINDOW w AS (
+        ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A+)
+        DEFINE A AS val > 0
+    )
+)
+SELECT c1.id, c1.cnt, c2.cnt as cnt2
+FROM rpr_cte c1
+JOIN rpr_cte c2 ON c1.id = c2.id
+ORDER BY c1.id;
+
+-- Nested CTEs
+
+WITH cte1 AS (
+    SELECT id, category, val FROM rpr_planner WHERE val > 30
+),
+cte2 AS (
+    SELECT id, category, val,
+           COUNT(*) OVER w as cnt
+    FROM cte1
+    WINDOW w AS (
+        ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A+)
+        DEFINE A AS val > 0
+    )
+)
+SELECT * FROM cte2 ORDER BY id;
+
+-- ============================================================
+-- JOIN Tests
+-- Files: prepjointree.c, setrefs.c
+-- ============================================================
+-- Tests RPR with JOINs and multiple table references
+
+CREATE TABLE rpr_join1 (id INT, val1 INT);
+CREATE TABLE rpr_join2 (id INT, val2 INT);
+
+INSERT INTO rpr_join1 VALUES (1, 10), (2, 20), (3, 30), (4, 40), (5, 50);
+INSERT INTO rpr_join2 VALUES (1, 100), (2, 200), (3, 300), (4, 400), (5, 500);
+
+-- RPR After INNER JOIN
+
+SELECT t1.id, t1.val1, t2.val2,
+       COUNT(*) OVER w as cnt
+FROM rpr_join1 t1
+INNER JOIN rpr_join2 t2 ON t1.id = t2.id
+WINDOW w AS (
+    ORDER BY t1.id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS val1 + val2 > 100
+)
+ORDER BY t1.id;
+
+-- RPR After LEFT JOIN
+
+SELECT t1.id, t1.val1, t2.val2,
+       COUNT(*) OVER w as cnt
+FROM rpr_join1 t1
+LEFT JOIN rpr_join2 t2 ON t1.id = t2.id
+WINDOW w AS (
+    ORDER BY t1.id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS val1 > 0
+)
+ORDER BY t1.id;
+
+-- RPR with Multiple Tables in DEFINE
+
+SELECT t1.id, t1.val1, t2.val2,
+       COUNT(*) OVER w as cnt
+FROM rpr_join1 t1
+INNER JOIN rpr_join2 t2 ON t1.id = t2.id
+WINDOW w AS (
+    ORDER BY t1.id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+ B)
+    DEFINE A AS val1 > 20,
+           B AS val2 > 200
+)
+ORDER BY t1.id;
+
+-- RPR After Cross Join
+
+SELECT t1.id as id1, t2.id as id2, t1.val1, t2.val2,
+       COUNT(*) OVER w as cnt
+FROM rpr_join1 t1
+CROSS JOIN rpr_join2 t2
+WHERE t1.id <= 2 AND t2.id <= 2
+WINDOW w AS (
+    ORDER BY t1.id, t2.id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS val1 + val2 > 0
+)
+ORDER BY t1.id, t2.id;
+
+-- Self-Join with RPR
+
+SELECT id, val1, val1_next,
+       COUNT(*) OVER w as cnt
+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 id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (X+)
+    DEFINE X AS val1 < val1_next
+)
+ORDER BY id;
+
+DROP TABLE rpr_join1, rpr_join2;
+
+-- ============================================================
+-- Complex Expression Tests
+-- Files: createplan.c, setrefs.c
+-- ============================================================
+-- Tests complex target list expressions
+
+CREATE TABLE rpr_target (id INT, val INT);
+INSERT INTO rpr_target VALUES (1, 10), (2, 20), (3, 30), (4, 40), (5, 50);
+
+-- Expressions in Target List
+
+SELECT id,
+       val * 2 as doubled,
+       val + 10 as added,
+       COUNT(*) OVER w as cnt
+FROM rpr_target
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS val > 0
+)
+ORDER BY id;
+
+-- CASE Expression in Target List
+
+SELECT id, val,
+       CASE
+           WHEN val < 30 THEN 'low'
+           WHEN val < 50 THEN 'medium'
+           ELSE 'high'
+       END as category,
+       COUNT(*) OVER w as cnt
+FROM rpr_target
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS val > 0
+)
+ORDER BY id;
+
+-- Subquery in Target List
+
+SELECT id, val,
+       (SELECT MAX(val) FROM rpr_target) as max_val,
+       COUNT(*) OVER w as cnt
+FROM rpr_target
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS val > 0
+)
+ORDER BY id;
+
+-- Function Calls in Target List
+
+SELECT id, val,
+       COALESCE(val, 0) as coalesced,
+       ABS(val - 30) as distance,
+       COUNT(*) OVER w as cnt
+FROM rpr_target
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS val > 0
+)
+ORDER BY id;
+
+-- Column Aliases and References
+
+SELECT id as row_id,
+       val as value,
+       COUNT(*) OVER w as cnt
+FROM rpr_target
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS val > 0
+)
+ORDER BY row_id;
+
+DROP TABLE rpr_target;
+
+-- ============================================================
+-- Set Operations Tests
+-- Files: planner.c
+-- ============================================================
+-- Tests RPR with UNION, INTERSECT, EXCEPT
+
+CREATE TABLE rpr_set1 (id INT, val INT);
+CREATE TABLE rpr_set2 (id INT, val INT);
+
+INSERT INTO rpr_set1 VALUES (1, 10), (2, 20), (3, 30);
+INSERT INTO rpr_set2 VALUES (2, 20), (3, 30), (4, 40);
+
+-- UNION with RPR
+
+(SELECT id, val, COUNT(*) OVER w as cnt
+ FROM rpr_set1
+ WINDOW w AS (
+     ORDER BY id
+     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+     PATTERN (A+)
+     DEFINE A AS val > 0
+ ))
+UNION
+(SELECT id, val, COUNT(*) OVER w as cnt
+ FROM rpr_set2
+ WINDOW w AS (
+     ORDER BY id
+     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+     PATTERN (A+)
+     DEFINE A AS val > 0
+ ))
+ORDER BY id;
+
+-- UNION ALL with RPR
+
+(SELECT id, val, COUNT(*) OVER w as cnt
+ FROM rpr_set1
+ WINDOW w AS (
+     ORDER BY id
+     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+     PATTERN (A+)
+     DEFINE A AS val > 0
+ ))
+UNION ALL
+(SELECT id, val, COUNT(*) OVER w as cnt
+ FROM rpr_set2
+ WINDOW w AS (
+     ORDER BY id
+     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+     PATTERN (A+)
+     DEFINE A AS val > 0
+ ))
+ORDER BY id, val;
+
+-- INTERSECT with RPR
+
+(SELECT id, val, COUNT(*) OVER w as cnt
+ FROM rpr_set1
+ WINDOW w AS (
+     ORDER BY id
+     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+     PATTERN (A+)
+     DEFINE A AS val > 0
+ ))
+INTERSECT
+(SELECT id, val, COUNT(*) OVER w as cnt
+ FROM rpr_set2
+ WINDOW w AS (
+     ORDER BY id
+     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+     PATTERN (A+)
+     DEFINE A AS val > 0
+ ))
+ORDER BY id;
+
+-- EXCEPT with RPR
+
+(SELECT id, val, COUNT(*) OVER w as cnt
+ FROM rpr_set1
+ WINDOW w AS (
+     ORDER BY id
+     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+     PATTERN (A+)
+     DEFINE A AS val > 0
+ ))
+EXCEPT
+(SELECT id, val, COUNT(*) OVER w as cnt
+ FROM rpr_set2
+ WINDOW w AS (
+     ORDER BY id
+     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+     PATTERN (A+)
+     DEFINE A AS val > 0
+ ))
+ORDER BY id;
+
+DROP TABLE rpr_set1, rpr_set2;
+
+-- ============================================================
+-- Sorting and Grouping Tests
+-- Files: planner.c, createplan.c
+-- ============================================================
+-- Tests RPR interaction with sorting and grouping
+
+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)
+-- Expected: ERROR: aggregate functions are not allowed in DEFINE
+
+SELECT category,
+       COUNT(*) as group_cnt,
+       MAX(val) as max_val,
+       COUNT(*) OVER w as window_cnt
+FROM rpr_sort
+GROUP BY category
+WINDOW w AS (
+    ORDER BY category
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS COUNT(*) > 0
+)
+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,
+       COUNT(*) OVER w as window_cnt
+FROM rpr_sort
+GROUP BY category
+HAVING COUNT(*) > 2
+WINDOW w AS (
+    ORDER BY category
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS COUNT(*) > 0
+)
+ORDER BY category;
+
+-- RPR with DISTINCT
+
+SELECT DISTINCT category,
+       COUNT(*) OVER w as cnt
+FROM rpr_sort
+WINDOW w AS (
+    PARTITION BY category
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS val > 0
+)
+ORDER BY category;
+
+-- RPR with ORDER BY (different from window ORDER BY)
+
+SELECT id, category, val,
+       COUNT(*) OVER w as cnt
+FROM rpr_sort
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS val > 0
+)
+ORDER BY val DESC;
+
+-- RPR with LIMIT and OFFSET
+
+SELECT id, category, val,
+       COUNT(*) OVER w as cnt
+FROM rpr_sort
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS val > 0
+)
+ORDER BY id
+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;
+
+-- ============================================================
+-- Stress Tests
+-- ============================================================
+-- Edge cases and stress scenarios
+
+CREATE TABLE rpr_stress (id INT, val INT);
+INSERT INTO rpr_stress SELECT i, i * 10 FROM generate_series(1, 20) i;
+
+-- Very Long Query with Many Windows
+SELECT id, val,
+       COUNT(*) OVER w1 as cnt1,
+       COUNT(*) OVER w2 as cnt2,
+       COUNT(*) OVER w3 as cnt3
+FROM rpr_stress
+WINDOW w1 AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS val > 0
+),
+w2 AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (B+)
+    DEFINE B AS val > 50
+),
+w3 AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (C+)
+    DEFINE C AS val > 100
+)
+ORDER BY id;
+
+-- Deeply Nested Subqueries with RPR
+
+SELECT * FROM (
+    SELECT * FROM (
+        SELECT * FROM (
+            SELECT id, val,
+                   COUNT(*) OVER w as cnt
+            FROM rpr_stress
+            WINDOW w AS (
+                ORDER BY id
+                ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+                PATTERN (A+)
+                DEFINE A AS val > 0
+            )
+        ) sub1
+    ) sub2
+) sub3
+WHERE cnt > 10
+ORDER BY id;
+
+-- Complex Expression in DEFINE Clause
+
+SELECT id, val,
+       COUNT(*) OVER w as cnt
+FROM rpr_stress
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+ B)
+    DEFINE A AS (val % 3 = 0 OR val % 5 = 0),
+           B AS (val * 2 > 100 AND val / 2 < 100)
+)
+ORDER BY id;
+
+-- Window with No Matching Rows
+
+SELECT id, val,
+       COUNT(*) OVER w as cnt
+FROM rpr_stress
+WHERE val > 1000  -- No rows match
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS val > 0
+)
+ORDER BY id;
+
+-- Window on Single Row
+
+SELECT id, val,
+       COUNT(*) OVER w as cnt
+FROM rpr_stress
+WHERE id = 10
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS val > 0
+)
+ORDER BY id;
+
+DROP TABLE rpr_stress;
+
+-- ============================================================
+-- Error Limit Tests
+-- ============================================================
+-- 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: DEFINE variable not in PATTERN (error)
+SELECT id, val, COUNT(*) OVER w FROM rpr_errors
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A)
+    DEFINE
+      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
+    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)
+    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
+);
+-- 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 (
+    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)
+    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
+);
+-- Expected: ERROR - too many pattern variables (Maximum is 251)
+
+-- 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
+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})
+    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
+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})
+    DEFINE A AS val > 0
+);
+-- Expected: ERROR - pattern nesting too deep
+
+DROP TABLE rpr_errors;
+
+-- ============================================================
+-- Jacob's Patterns
+-- ============================================================
+-- Basic pattern matching tests from jacob branch
+
+-- Test: A? (optional, greedy)
+SELECT id, val, count(*) OVER w AS c
+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 > 50
+);
+
+-- Test: A{2} (exact count)
+SELECT id, val, count(*) OVER w AS c
+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})
+    DEFINE A AS val <= 50
+);
+
+-- Test: A{1,3} (bounded range, greedy)
+SELECT id, val, count(*) OVER w AS c
+FROM rpr_plan
+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 val <= 50
+);
+
+-- Test: A | B (simple alternation)
+SELECT id, val, count(*) OVER w AS c
+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 <= 30, B AS val > 70
+);
+
+-- Test: A | B | C (three-way alternation)
+SELECT id, val, count(*) OVER w AS c
+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)
+    DEFINE A AS val <= 20, B AS val BETWEEN 40 AND 60, C AS val > 80
+);
+
+-- Test: A B C (concatenation)
+SELECT id, val, count(*) OVER w AS c
+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)
+    DEFINE A AS val <= 30, B AS val BETWEEN 31 AND 60, C AS val > 60
+);
+
+-- Test: A B? C (optional middle)
+SELECT id, val, count(*) OVER w AS c
+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)
+    DEFINE A AS val <= 30, B AS val BETWEEN 31 AND 60, C AS val > 60
+);
+
+-- Test: (A B)+ (grouped quantifier)
+SELECT id, val, count(*) OVER w AS c
+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
+);
+
+-- Test: (A | B)+ C (alternation with quantifier)
+SELECT id, val, count(*) OVER w AS c
+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)
+    DEFINE A AS val <= 30, B AS val BETWEEN 31 AND 60, C AS val > 80
+);
+
+-- Test: (A+ | (A | B)+)* - nested alternation inside quantified group
+-- Previously caused infinite recursion in nfa_advance_alt when the inner
+-- BEGIN(+)'s skip jump was followed as an ALT branch pointer.
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM (VALUES
+    (1, ARRAY['A', 'B']),
+    (2, ARRAY['B']),
+    (3, ARRAY['C'])
+) AS t(id, flags)
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A+ | (A | B)+)*)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags)
+);
+
+-- ============================================================
+-- Pathological Patterns
+-- ============================================================
+-- These patterns previously caused issues. Now optimized or handled safely.
+
+-- Test: (A*)* - nested unbounded (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 (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)*)*)*  - triple nested (optimized to A*)
+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
+);
+
+-- Optional group with alternation: A ((B | C) (D | E))* F?
+-- When only A matches, the * group matches 0 times and F? matches 0 times
+SELECT id, val, match_len
+FROM (SELECT id, val,
+             COUNT(*) OVER w AS match_len
+      FROM (VALUES (1, 1), (2, 99)) AS t(id, val)
+      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))* F?)
+          DEFINE A AS val = 1,
+                 B AS val = 2, C AS val = 3,
+                 D AS val = 4, E AS val = 5,
+                 F AS val = 6
+      )
+) s;
+
+DROP TABLE rpr_plan;
diff --git a/src/test/regress/sql/rpr_explain.sql b/src/test/regress/sql/rpr_explain.sql
new file mode 100644
index 00000000000..a3789e92631
--- /dev/null
+++ b/src/test/regress/sql/rpr_explain.sql
@@ -0,0 +1,2783 @@
+-- ============================================================
+-- RPR EXPLAIN Tests
+-- 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
+--   - 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)
+--
+-- Test Coverage:
+--   Basic NFA Statistics Tests
+--   State Statistics Tests
+--   Context Statistics Tests
+--   Match Length Statistics Tests
+--   Mismatch Length Statistics Tests
+--   JSON Format Tests
+--   XML Format Tests
+--   Multiple Partitions Tests
+--   Edge Cases
+--   Complex Pattern Tests
+--   Real-world Pattern Examples
+--   Performance-oriented Tests
+--   INITIAL vs no INITIAL comparison
+--   Quantifier Variations
+--   Regression Tests for Statistics Accuracy
+--   Alternation Pattern Tests
+--   Group Pattern Tests
+--   Window Function Combinations
+--   DEFINE Expression Variations
+--   Large Scale Statistics Verification
+--   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
+-- indicate issues such as uninitialized memory access.
+-- Works for text, JSON, and XML formats.
+create function rpr_explain_filter(text) returns setof text
+language plpgsql as
+$$
+declare
+    ln text;
+begin
+    for ln in execute $1
+    loop
+        -- Normalize platform-dependent memory values
+        -- Keep NFA statistics numbers unchanged (they are test assertions)
+
+        -- Text format: "Storage: Memory  Maximum Storage: 18kB"
+        if ln ~ 'Storage:.*Maximum Storage:' then
+            ln := regexp_replace(ln, '\m\d+kB', 'NkB', 'g');
+        end if;
+
+        -- JSON format: "Maximum Storage": 17 (number in kB units)
+        if ln ~ '"Maximum Storage":' then
+            ln := regexp_replace(ln, '"Maximum Storage": \d+', '"Maximum Storage": 0', 'g');
+        end if;
+
+        -- XML format: <Maximum-Storage>17</Maximum-Storage> (number in kB units)
+        if ln ~ '<Maximum-Storage>' then
+            ln := regexp_replace(ln, '<Maximum-Storage>\d+</Maximum-Storage>', '<Maximum-Storage>0</Maximum-Storage>', 'g');
+        end if;
+
+        -- Sort Method memory is platform-dependent (32-bit vs 64-bit)
+        if ln ~ 'Sort Method:.*Memory:' then
+            ln := regexp_replace(ln, 'Memory: \d+kB', 'Memory: NkB');
+        end if;
+
+        return next ln;
+    end loop;
+end;
+$$;
+
+-- Setup: Create test tables
+CREATE TABLE rpr_nfa_test (
+    id serial,
+    v int,
+    cat char(1)
+);
+
+-- Insert test data: 100 rows with predictable pattern
+INSERT INTO rpr_nfa_test (v, cat)
+SELECT i,
+       CASE
+           WHEN i % 5 = 1 THEN 'A'
+           WHEN i % 5 = 2 THEN 'B'
+           WHEN i % 5 = 3 THEN 'C'
+           WHEN i % 5 = 4 THEN 'D'
+           ELSE 'E'
+       END
+FROM generate_series(1, 100) i;
+
+-- Additional test table with more complex patterns
+CREATE TABLE rpr_nfa_complex (
+    id serial,
+    price int,
+    trend char(1)  -- U=up, D=down, S=stable
+);
+
+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'),
+    (120, 'U'), (125, 'U'), (130, 'U'), (128, 'D'), (126, 'D'),
+    (124, 'D'), (122, 'D'), (120, 'D'), (118, 'D'), (119, 'U'),
+    (121, 'U'), (123, 'U'), (125, 'U'), (127, 'U'), (129, 'U'),
+    (131, 'U'), (133, 'U'), (130, 'D'), (127, 'D'), (124, 'D');
+
+-- ============================================================
+-- Basic NFA Statistics Tests
+-- ============================================================
+
+-- Simple pattern - should show basic statistics
+CREATE VIEW rpr_ev_basic_simple AS
+SELECT count(*) OVER w
+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_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
+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''
+)');
+
+-- Pattern with no matches - 0 matched
+CREATE VIEW rpr_ev_basic_nomatch AS
+SELECT count(*) OVER w
+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_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
+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''
+);');
+
+-- Pattern matching every row - high match count
+CREATE VIEW rpr_ev_basic_allrows AS
+SELECT count(*) OVER w
+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_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
+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
+);');
+
+-- Regression test: Space before parenthesis in pattern deparse
+-- Verifies that "A (B | C)" correctly outputs as "a (b | c)" with space
+CREATE VIEW rpr_ev_basic_deparse_space 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 % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
+);
+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
+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 % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
+);');
+
+-- 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_ev_basic_deparse_seqalt AS
+SELECT count(*) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    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_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
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    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
+);');
+
+-- 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)
+-- ============================================================
+
+-- Simple quantifier pattern - A+ with short matches (no merging)
+CREATE VIEW rpr_ev_state_simple_quant 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+)
+    DEFINE A AS v % 2 = 1
+);
+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
+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+)
+    DEFINE A AS v % 2 = 1
+);');
+
+-- Alternation pattern - multiple state branches
+CREATE VIEW rpr_ev_state_alt AS
+SELECT count(*) OVER w
+FROM rpr_nfa_test
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A | B | C) (D | E))
+    DEFINE
+        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_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
+FROM rpr_nfa_test
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A | B | C) (D | E))
+    DEFINE
+        A AS cat = ''A'', B AS cat = ''B'', C AS cat = ''C'',
+        D AS cat = ''D'', E AS cat = ''E''
+);');
+
+-- Complex pattern with high state count
+CREATE VIEW rpr_ev_state_complex AS
+SELECT count(*) OVER w
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    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_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
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A+ B* C+)
+    DEFINE
+        A AS v % 3 = 1,
+        B AS v % 3 = 2,
+        C AS v % 3 = 0
+);');
+
+-- Grouped pattern with quantifier - state count with grouping
+CREATE VIEW rpr_ev_state_group_quant AS
+SELECT count(*) OVER w
+FROM generate_series(1, 60) 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_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
+FROM generate_series(1, 60) 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
+);');
+
+-- State explosion pattern - many alternations
+-- Pattern (A|B)(A|B)(A|B)(A|B) can create many parallel states
+CREATE VIEW rpr_ev_state_explosion AS
+SELECT count(*) OVER w
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    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_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
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    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
+);');
+
+-- Consecutive ALT merge followed by different ALT
+-- Tests mergeConsecutiveAlts flush on ALT change: (A|B){2} (C|D)
+CREATE VIEW rpr_ev_state_alt_merge_alt AS
+SELECT count(*) OVER w
+FROM generate_series(1, 40) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    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_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
+FROM generate_series(1, 40) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    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
+);');
+
+-- Consecutive ALT merge followed by non-ALT element
+-- Tests mergeConsecutiveAlts flush on non-ALT: (A|B){2} c
+CREATE VIEW rpr_ev_state_alt_merge_nonalt AS
+SELECT count(*) OVER w
+FROM generate_series(1, 40) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    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_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
+FROM generate_series(1, 40) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A | B) (A | B) C)
+    DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2
+);');
+
+-- ALT prefix/suffix absorbed into GROUP: (A|B) (A|B)+ (A|B) -> (A|B){3,}
+CREATE VIEW rpr_ev_state_alt_absorb_group AS
+SELECT count(*) OVER w
+FROM generate_series(1, 40) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    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_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
+FROM generate_series(1, 40) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A | B) (A | B)+ (A | B))
+    DEFINE A AS v % 2 = 0, B AS v % 2 = 1
+);');
+
+-- High state count - alternation with plus quantifier
+CREATE VIEW rpr_ev_state_alt_plus AS
+SELECT count(*) OVER w
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    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_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
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    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
+);');
+
+-- Early termination: first ALT branch (A) reaches FIN immediately,
+-- pruning second branch (A B+) before it can accumulate B repetitions.
+CREATE VIEW rpr_ev_state_alt_prune AS
+SELECT count(*) OVER w
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A | A B)+)
+    DEFINE A AS v = 1, B AS v > 1
+);
+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
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A | A B)+)
+    DEFINE A AS v = 1, B AS v > 1
+);');
+
+-- Nested quantifiers causing state growth
+CREATE VIEW rpr_ev_state_nested_quant AS
+SELECT count(*) OVER w
+FROM generate_series(1, 1000) 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 % 3 = 1, B AS v % 3 = 2
+);
+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
+FROM generate_series(1, 1000) 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 % 3 = 1, B AS v % 3 = 2
+);');
+
+-- ============================================================
+-- Context Statistics Tests (peak, total, pruned + absorbed/skipped)
+-- ============================================================
+
+-- Context absorption with unbounded quantifier at start
+CREATE VIEW rpr_ev_ctx_absorb_unbounded 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
+);
+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
+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
+);');
+
+-- No absorption - bounded quantifier
+CREATE VIEW rpr_ev_ctx_no_absorb 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{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_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
+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{2,4} B)
+    DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
+);');
+
+-- Contexts skipped by SKIP PAST LAST ROW
+CREATE VIEW rpr_ev_ctx_skip AS
+SELECT count(*) OVER w
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    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_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
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B C)
+    DEFINE A AS v % 10 = 1, B AS v % 10 = 2, C AS v % 10 = 3
+);');
+
+-- High context absorption - unbounded group
+CREATE VIEW rpr_ev_ctx_absorb_group AS
+SELECT count(*) OVER w
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    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_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
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A B)+ C)
+    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_ev_ctx_absorb_fixedvar 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_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
+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_ev_ctx_absorb_nested 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_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
+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_ev_ctx_absorb_deep 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_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
+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_ev_ctx_absorb_endchain 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_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
+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)
+);');
+
+-- 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
+);');
+
+-- No absorption with compound PREV(FIRST()) (match_start-dependent)
+CREATE VIEW rpr_ev_ctx_no_absorb_compound 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 PREV(FIRST(v), 1) IS NOT NULL
+);
+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 PREV(FIRST(v), 1) IS NOT NULL
+);');
+
+-- ============================================================
+-- Match Length Statistics Tests
+-- ============================================================
+
+-- Fixed length matches - all same length
+CREATE VIEW rpr_ev_mlen_fixed AS
+SELECT count(*) OVER w
+FROM rpr_nfa_test
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B C D E)
+    DEFINE
+        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_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
+FROM rpr_nfa_test
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B C D E)
+    DEFINE
+        A AS cat = ''A'', B AS cat = ''B'', C AS cat = ''C'',
+        D AS cat = ''D'', E AS cat = ''E''
+);');
+
+-- Variable length matches - min/max/avg differ
+CREATE VIEW rpr_ev_mlen_variable AS
+SELECT count(*) OVER w
+FROM generate_series(1, 100) 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 % 10 <> 0, B AS v % 10 = 0
+);
+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
+FROM generate_series(1, 100) 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 % 10 <> 0, B AS v % 10 = 0
+);');
+
+-- Very long matches
+CREATE VIEW rpr_ev_mlen_long AS
+SELECT count(*) OVER w
+FROM generate_series(1, 200) 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 <= 195, B AS v > 195
+);
+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
+FROM generate_series(1, 200) 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 <= 195, B AS v > 195
+);');
+
+-- Uniform match length with mismatches from gap rows (v%20 = 11..15)
+CREATE VIEW rpr_ev_mlen_with_mismatch AS
+SELECT count(*) OVER w
+FROM generate_series(1, 100) 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 % 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_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
+FROM generate_series(1, 100) 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 % 20 <> 0) AND (v % 20 <= 10 OR v % 20 > 15),
+        B AS v % 20 = 0
+);');
+
+-- ============================================================
+-- 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 VIEW rpr_ev_mlen_no_mismatch AS
+SELECT count(*) OVER w
+FROM (
+    SELECT v,
+           CASE WHEN v % 10 IN (1,2,3) THEN 'A'
+                WHEN v % 10 IN (4,5) THEN 'B'
+                WHEN v % 10 = 6 THEN 'C'
+                ELSE 'X' END AS cat
+    FROM generate_series(1, 100) AS s(v)
+) t
+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_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
+FROM (
+    SELECT v,
+           CASE WHEN v % 10 IN (1,2,3) THEN ''A''
+                WHEN v % 10 IN (4,5) THEN ''B''
+                WHEN v % 10 = 6 THEN ''C''
+                ELSE ''X'' END AS cat
+    FROM generate_series(1, 100) AS s(v)
+) t
+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''
+);');
+
+-- Long partial matches that fail
+CREATE VIEW rpr_ev_mlen_long_partial AS
+SELECT count(*) OVER w
+FROM (
+    SELECT i AS v,
+           CASE
+               WHEN i <= 20 THEN 'A'
+               WHEN i <= 25 THEN 'B'
+               WHEN i = 26 THEN 'X'  -- breaks the pattern
+               WHEN i <= 50 THEN 'A'
+               WHEN i <= 55 THEN 'B'
+               WHEN i = 56 THEN 'C'  -- completes pattern
+               ELSE 'Y'
+           END AS cat
+    FROM generate_series(1, 60) i
+) t
+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_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
+FROM (
+    SELECT i AS v,
+           CASE
+               WHEN i <= 20 THEN ''A''
+               WHEN i <= 25 THEN ''B''
+               WHEN i = 26 THEN ''X''  -- breaks the pattern
+               WHEN i <= 50 THEN ''A''
+               WHEN i <= 55 THEN ''B''
+               WHEN i = 56 THEN ''C''  -- completes pattern
+               ELSE ''Y''
+           END AS cat
+    FROM generate_series(1, 60) i
+) t
+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''
+);');
+
+-- ============================================================
+-- JSON Format Tests
+-- ============================================================
+
+-- JSON format output with all statistics
+CREATE VIEW rpr_ev_json_basic 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 % 3 = 1, B AS v % 3 = 2
+);
+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
+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 % 3 = 1, B AS v % 3 = 2
+)');
+
+-- JSON format with match length statistics
+CREATE VIEW rpr_ev_json_matchlen AS
+SELECT count(*) OVER w
+FROM generate_series(1, 100) 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 % 10 <> 0, B AS v % 10 = 0
+);
+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
+FROM generate_series(1, 100) 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 % 10 <> 0, B AS v % 10 = 0
+)');
+
+-- JSON format with mismatch statistics
+-- Pattern A B C expects 1,2,3 but gets 1,2,4 twice causing mismatches
+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 (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    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_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
+FROM (VALUES (1),(2),(4), (1),(2),(4), (1),(2),(3)) AS t(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B C)
+    DEFINE A AS v = 1, B AS v = 2, C AS v = 3
+)');
+
+-- JSON format with skipped context statistics
+-- Alternation pattern with SKIP PAST LAST ROW causes many contexts to be skipped
+CREATE VIEW rpr_ev_json_skip AS
+SELECT count(*) OVER w
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    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_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
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    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
+)');
+
+-- ============================================================
+-- XML Format Tests
+-- ============================================================
+
+-- XML format output
+CREATE VIEW rpr_ev_xml_basic 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_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
+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
+)');
+
+-- ============================================================
+-- Multiple Partitions Tests
+-- ============================================================
+
+-- Statistics across multiple partitions
+CREATE VIEW rpr_ev_part_multi AS
+SELECT count(*) OVER w
+FROM (
+    SELECT p, v
+    FROM generate_series(1, 3) p,
+         generate_series(1, 30) v
+) t
+WINDOW w AS (
+    PARTITION BY p
+    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
+);
+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
+FROM (
+    SELECT p, v
+    FROM generate_series(1, 3) p,
+         generate_series(1, 30) v
+) t
+WINDOW w AS (
+    PARTITION BY p
+    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
+);');
+
+-- Different pattern behavior per partition
+CREATE VIEW rpr_ev_part_diff AS
+SELECT count(*) OVER w
+FROM (
+    SELECT
+        CASE WHEN v <= 25 THEN 1 ELSE 2 END AS p,
+        v % 10 AS val
+    FROM generate_series(1, 50) v
+) t
+WINDOW w AS (
+    PARTITION BY p
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A+ B)
+    DEFINE A AS val < 5, B AS val >= 5
+);
+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
+FROM (
+    SELECT
+        CASE WHEN v <= 25 THEN 1 ELSE 2 END AS p,
+        v % 10 AS val
+    FROM generate_series(1, 50) v
+) t
+WINDOW w AS (
+    PARTITION BY p
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A+ B)
+    DEFINE A AS val < 5, B AS val >= 5
+);');
+
+-- ============================================================
+-- Edge Cases
+-- ============================================================
+
+-- Empty result set
+CREATE VIEW rpr_ev_edge_empty AS
+SELECT count(*) OVER w
+FROM generate_series(1, 0) 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 = 1, B AS v = 2
+);
+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
+FROM generate_series(1, 0) 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 = 1, B AS v = 2
+);');
+
+-- Single row
+CREATE VIEW rpr_ev_edge_single_row AS
+SELECT count(*) OVER w
+FROM generate_series(1, 1) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A)
+    DEFINE A AS TRUE
+);
+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
+FROM generate_series(1, 1) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A)
+    DEFINE A AS TRUE
+);');
+
+-- Pattern longer than data
+CREATE VIEW rpr_ev_edge_pattern_longer AS
+SELECT count(*) OVER w
+FROM generate_series(1, 5) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B C D E F G H I J)
+    DEFINE
+        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_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
+FROM generate_series(1, 5) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B C D E F G H I J)
+    DEFINE
+        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
+);');
+
+-- All rows match as single match
+CREATE VIEW rpr_ev_edge_single_match 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+)
+    DEFINE A AS TRUE
+);
+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
+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+)
+    DEFINE A AS TRUE
+);');
+
+-- ============================================================
+-- Complex Pattern Tests
+-- ============================================================
+
+-- Nested groups
+CREATE VIEW rpr_ev_cpx_nested AS
+SELECT count(*) OVER w
+FROM generate_series(1, 60) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    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_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
+FROM generate_series(1, 60) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (((A B) C)+)
+    DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
+);');
+
+-- Multiple alternations
+CREATE VIEW rpr_ev_cpx_multi_alt AS
+SELECT count(*) OVER w
+FROM rpr_nfa_test
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A | B) (C | D | E))
+    DEFINE
+        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_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
+FROM rpr_nfa_test
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A | B) (C | D | E))
+    DEFINE
+        A AS cat = ''A'', B AS cat = ''B'', C AS cat = ''C'',
+        D AS cat = ''D'', E AS cat = ''E''
+);');
+
+-- Optional elements
+CREATE VIEW rpr_ev_cpx_optional 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? 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_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
+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? C)
+    DEFINE A AS v % 4 = 1, B AS v % 4 = 2, C AS v % 4 = 3
+);');
+
+-- Bounded quantifiers
+CREATE VIEW rpr_ev_cpx_bounded AS
+SELECT count(*) OVER w
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    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_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
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A{2,5} B)
+    DEFINE A AS v % 10 <> 0, B AS v % 10 = 0
+);');
+
+-- Star quantifier
+CREATE VIEW rpr_ev_cpx_star 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* 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_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
+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* C)
+    DEFINE A AS v % 10 = 1, B AS v % 10 IN (2,3,4,5,6,7,8), C AS v % 10 = 9
+);');
+
+-- ============================================================
+-- Real-world Pattern Examples
+-- ============================================================
+
+-- Stock price pattern - V-shape (down then up)
+CREATE VIEW rpr_ev_real_vshape AS
+SELECT count(*) OVER w
+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_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
+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''
+);');
+
+-- Stock price pattern - peak (up, stable, down)
+CREATE VIEW rpr_ev_real_peak AS
+SELECT count(*) OVER w
+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_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
+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''
+);');
+
+-- Consecutive increasing values (using PREV)
+CREATE VIEW rpr_ev_real_increasing 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{3,})
+    DEFINE A AS v > PREV(v) OR PREV(v) IS NULL
+);
+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
+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{3,})
+    DEFINE A AS v > PREV(v) OR PREV(v) IS NULL
+);');
+
+-- ============================================================
+-- Performance-oriented Tests
+-- ============================================================
+
+-- Large dataset with simple pattern
+CREATE VIEW rpr_ev_perf_large_simple AS
+SELECT count(*) OVER w
+FROM generate_series(1, 1000) 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_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
+FROM generate_series(1, 1000) 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
+);');
+
+-- Large dataset with absorption
+CREATE VIEW rpr_ev_perf_large_absorb AS
+SELECT count(*) OVER w
+FROM generate_series(1, 1000) 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 % 100 <> 0, B AS v % 100 = 0
+);
+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
+FROM generate_series(1, 1000) 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 % 100 <> 0, B AS v % 100 = 0
+);');
+
+-- High state merge ratio
+CREATE VIEW rpr_ev_perf_high_merge AS
+SELECT count(*) OVER w
+FROM generate_series(1, 500) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    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_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
+FROM generate_series(1, 500) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A | B)+ C)
+    DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
+);');
+
+-- ============================================================
+-- INITIAL vs no INITIAL comparison
+-- ============================================================
+
+-- With INITIAL keyword
+CREATE VIEW rpr_ev_initial_with 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
+    INITIAL
+    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_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
+FROM generate_series(1, 50) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    INITIAL
+    PATTERN (A+ B)
+    DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
+);');
+
+-- Without INITIAL keyword (same behavior currently)
+CREATE VIEW rpr_ev_initial_without 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
+);
+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
+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
+);');
+
+-- ============================================================
+-- Quantifier Variations
+-- ============================================================
+
+-- Plus quantifier
+CREATE VIEW rpr_ev_quant_plus AS
+SELECT count(*) OVER w
+FROM generate_series(1, 40) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A+)
+    DEFINE A AS v % 4 <> 0
+);
+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
+FROM generate_series(1, 40) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A+)
+    DEFINE A AS v % 4 <> 0
+);');
+
+-- Star quantifier (zero or more)
+CREATE VIEW rpr_ev_quant_star AS
+SELECT count(*) OVER w
+FROM generate_series(1, 40) 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 % 4 IN (1, 2), B AS v % 4 = 3
+);
+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
+FROM generate_series(1, 40) 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 % 4 IN (1, 2), B AS v % 4 = 3
+);');
+
+-- Question mark (zero or one)
+CREATE VIEW rpr_ev_quant_question AS
+SELECT count(*) OVER w
+FROM generate_series(1, 40) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    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_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
+FROM generate_series(1, 40) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A? B C)
+    DEFINE A AS v % 4 = 1, B AS v % 4 = 2, C AS v % 4 = 3
+);');
+
+-- Exact count {n}
+CREATE VIEW rpr_ev_quant_exact 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{3} B)
+    DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
+);
+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
+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{3} B)
+    DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
+);');
+
+-- Range {n,m}
+CREATE VIEW rpr_ev_quant_range 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{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_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
+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{2,4} B)
+    DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
+);');
+
+-- At least {n,}
+CREATE VIEW rpr_ev_quant_atleast 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{3,} B)
+    DEFINE A AS v % 10 <> 0, B AS v % 10 = 0
+);
+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
+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{3,} B)
+    DEFINE A AS v % 10 <> 0, B AS v % 10 = 0
+);');
+
+-- ============================================================
+-- Regression Tests for Statistics Accuracy
+-- ============================================================
+
+-- Verify state count accuracy
+-- Pattern A+ B with 20 rows should show predictable state behavior
+CREATE VIEW rpr_ev_reg_state_count AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) 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
+);
+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
+FROM generate_series(1, 20) 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
+);');
+
+-- Verify context count with known absorption
+CREATE VIEW rpr_ev_reg_ctx_absorb 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 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_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
+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 C)
+    DEFINE A AS v % 10 IN (1,2,3,4,5,6,7), B AS v % 10 = 8, C AS v % 10 = 9
+);');
+
+-- Verify match length with fixed-length pattern
+CREATE VIEW rpr_ev_reg_matchlen 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 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_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
+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 C)
+    DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
+);');
+
+-- ============================================================
+-- Alternation Pattern Tests
+-- ============================================================
+
+-- Simple alternation
+CREATE VIEW rpr_ev_alt_simple AS
+SELECT count(*) OVER w
+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_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
+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''
+);');
+
+-- Multiple items in alternation
+CREATE VIEW rpr_ev_alt_multi_item AS
+SELECT count(*) OVER w
+FROM rpr_nfa_test
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A | B | C | D) E)
+    DEFINE
+        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_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
+FROM rpr_nfa_test
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A | B | C | D) E)
+    DEFINE
+        A AS cat = ''A'', B AS cat = ''B'', C AS cat = ''C'',
+        D AS cat = ''D'', E AS cat = ''E''
+);');
+
+-- Alternation with quantifiers
+CREATE VIEW rpr_ev_alt_with_quant 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)+ 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_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
+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)+ C)
+    DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
+);');
+
+-- Multiple alternatives (4+)
+CREATE VIEW rpr_ev_alt_four_plus AS
+SELECT count(*) OVER w
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    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_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
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    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
+);');
+
+-- Alternation at start
+CREATE VIEW rpr_ev_alt_at_start AS
+SELECT count(*) OVER w
+FROM generate_series(1, 60) 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_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
+FROM generate_series(1, 60) 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
+);');
+
+-- Multiple sequential alternations
+CREATE VIEW rpr_ev_alt_sequential AS
+SELECT count(*) OVER w
+FROM generate_series(1, 100) 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_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
+FROM generate_series(1, 100) 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
+);');
+
+-- Quantified alternatives
+CREATE VIEW rpr_ev_alt_quantified AS
+SELECT count(*) OVER w
+FROM generate_series(1, 60) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    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_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
+FROM generate_series(1, 60) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A+ | B+) C)
+    DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2
+);');
+
+-- Alternation at end
+CREATE VIEW rpr_ev_alt_at_end AS
+SELECT count(*) OVER w
+FROM generate_series(1, 60) 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_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
+FROM generate_series(1, 60) 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
+);');
+
+-- 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_ev_alt_nested_start 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))
+    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_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
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    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
+);');
+
+-- 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_ev_alt_nested_end 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_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
+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
+);');
+
+-- ============================================================
+-- Group Pattern Tests
+-- ============================================================
+
+-- Simple group
+CREATE VIEW rpr_ev_grp_simple AS
+SELECT count(*) OVER w
+FROM generate_series(1, 40) 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_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
+FROM generate_series(1, 40) 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
+);');
+
+-- Group with bounded quantifier
+CREATE VIEW rpr_ev_grp_bounded AS
+SELECT count(*) OVER w
+FROM generate_series(1, 40) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    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_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
+FROM generate_series(1, 40) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A B){2,4})
+    DEFINE A AS v % 2 = 1, B AS v % 2 = 0
+);');
+
+-- Nested groups
+CREATE VIEW rpr_ev_grp_nested AS
+SELECT count(*) OVER w
+FROM generate_series(1, 60) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    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_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
+FROM generate_series(1, 60) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (((A B){2})+)
+    DEFINE A AS v % 2 = 1, B AS v % 2 = 0
+);');
+
+-- Deep nesting (3+ levels)
+CREATE VIEW rpr_ev_grp_deep AS
+SELECT count(*) OVER w
+FROM generate_series(1, 40) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    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_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
+FROM generate_series(1, 40) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((((A | B)+)+)+)
+    DEFINE A AS v % 2 = 0, B AS v % 2 = 1
+);');
+
+-- Bounded quantifier on alternation
+CREATE VIEW rpr_ev_grp_bounded_alt AS
+SELECT count(*) OVER w
+FROM generate_series(1, 60) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    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_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
+FROM generate_series(1, 60) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A | B){2,3} C)
+    DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2
+);');
+
+-- Nested groups with quantifiers
+CREATE VIEW rpr_ev_grp_nested_quant AS
+SELECT count(*) OVER w
+FROM generate_series(1, 60) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    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_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
+FROM generate_series(1, 60) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (((A B)+ C)*)
+    DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2
+);');
+
+-- Partial nested quantification
+CREATE VIEW rpr_ev_grp_partial_quant AS
+SELECT count(*) OVER w
+FROM generate_series(1, 60) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    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_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
+FROM generate_series(1, 60) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A (B C)+)*)
+    DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2
+);');
+
+-- ============================================================
+-- Window Function Combinations
+-- ============================================================
+
+-- count(*) with pattern
+CREATE VIEW rpr_ev_wfn_count 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 % 5 <> 0, B AS v % 5 = 0
+);
+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
+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 % 5 <> 0, B AS v % 5 = 0
+);');
+
+-- first_value with pattern
+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 (
+    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
+);
+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
+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 % 5 <> 0, B AS v % 5 = 0
+);');
+
+-- last_value with pattern
+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 (
+    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
+);
+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
+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 % 5 <> 0, B AS v % 5 = 0
+);');
+
+-- Multiple window functions
+CREATE VIEW rpr_ev_wfn_multi AS
+SELECT
+    count(*) OVER w,
+    first_value(v) OVER w,
+    last_value(v) 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 % 5 <> 0, B AS v % 5 = 0
+);
+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
+    count(*) OVER w,
+    first_value(v) OVER w,
+    last_value(v) 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 % 5 <> 0, B AS v % 5 = 0
+);');
+
+-- ============================================================
+-- DEFINE Expression Variations
+-- ============================================================
+
+-- Complex boolean expressions
+CREATE VIEW rpr_ev_def_complex_bool 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) AND (v % 3 <> 0),
+        B AS (v % 5 = 0) OR (v % 3 = 0)
+);
+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
+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) AND (v % 3 <> 0),
+        B AS (v % 5 = 0) OR (v % 3 = 0)
+);');
+
+-- Using PREV function
+CREATE VIEW rpr_ev_def_prev 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 (S U+ D+)
+    DEFINE
+        S AS TRUE,
+        U AS v > PREV(v),
+        D AS v < PREV(v)
+);
+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
+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 (S U+ D+)
+    DEFINE
+        S AS TRUE,
+        U AS v > PREV(v),
+        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
+FROM (
+    SELECT CASE WHEN v % 5 = 0 THEN NULL ELSE v END AS v
+    FROM generate_series(1, 30) v
+) t
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    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_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
+FROM (
+    SELECT CASE WHEN v % 5 = 0 THEN NULL ELSE v END AS v
+    FROM generate_series(1, 30) v
+) t
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A+ B)
+    DEFINE A AS v IS NOT NULL, B AS v IS NULL
+);');
+
+-- ============================================================
+-- Large Scale Statistics Verification
+-- ============================================================
+
+-- 500 rows - verify statistics scale correctly
+CREATE VIEW rpr_ev_scale_500rows AS
+SELECT count(*) OVER w
+FROM generate_series(1, 500) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    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_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
+FROM generate_series(1, 500) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A+ B C)
+    DEFINE A AS v % 10 < 7, B AS v % 10 = 7, C AS v % 10 = 8
+);');
+
+-- High match count scenario
+CREATE VIEW rpr_ev_scale_high_match AS
+SELECT count(*) OVER w
+FROM generate_series(1, 500) 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_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
+FROM generate_series(1, 500) 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
+);');
+
+-- High skip count scenario
+CREATE VIEW rpr_ev_scale_high_skip AS
+SELECT count(*) OVER w
+FROM generate_series(1, 500) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B C D E)
+    DEFINE
+        A AS v % 100 = 1,
+        B AS v % 100 = 2,
+        C AS v % 100 = 3,
+        D AS v % 100 = 4,
+        E AS v % 100 = 5
+);
+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
+FROM generate_series(1, 500) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B C D E)
+    DEFINE
+        A AS v % 100 = 1,
+        B AS v % 100 = 2,
+        C AS v % 100 = 3,
+        D AS v % 100 = 4,
+        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_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_ev_opt_no_rpr;
+
+-- With RPR: frame must remain ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+CREATE VIEW rpr_ev_opt_with_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
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B+)
+    DEFINE
+        B AS v > PREV(v)
+);
+
+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_ev_opt_mixed 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_ev_opt_mixed;
+
+--
+-- 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;
+
+-- ============================================================
+-- 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
+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;
+
+-- 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/test/regress/sql/rpr_integration.sql b/src/test/regress/sql/rpr_integration.sql
new file mode 100644
index 00000000000..6d47728e911
--- /dev/null
+++ b/src/test/regress/sql/rpr_integration.sql
@@ -0,0 +1,940 @@
+-- ============================================================
+-- 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
+-- ============================================================
+-- 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 row_number() OVER w FROM rpr_integ
+WINDOW w AS (ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING);
+
+-- RPR case: the frame is preserved as specified.
+EXPLAIN (COSTS OFF)
+SELECT row_number() 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;
diff --git a/src/test/regress/sql/rpr_nfa.sql b/src/test/regress/sql/rpr_nfa.sql
new file mode 100644
index 00000000000..1d27e0dc09e
--- /dev/null
+++ b/src/test/regress/sql/rpr_nfa.sql
@@ -0,0 +1,3446 @@
+-- ============================================================
+-- RPR NFA Tests
+-- Tests for Row Pattern Recognition NFA Runtime Execution
+-- ============================================================
+--
+-- This test suite validates the NFA (Non-deterministic Finite
+-- Automaton) runtime execution engine in nodeWindowAgg.c,
+-- focusing on update_reduced_frame and related functions.
+--
+-- Test Strategy:
+--   Diagonal pattern style using ARRAY flags to explicitly
+--   control which pattern variables match at each row.
+--
+-- Test Coverage:
+--   Basic NFA Flow (match->absorb->advance)
+--   Absorption Optimization
+--   Context Lifecycle Management
+--   Advance Phase (Epsilon Transitions)
+--   Match Phase (Variable Matching)
+--   Frame Boundary Handling
+--   State Management (Deduplication)
+--   Statistics and Diagnostics
+--   Quantifier Runtime Behavior
+--   Pathological Pattern Protection
+--   Alternation Runtime Behavior
+--   Deep Nested Groups
+--   SKIP Options (Runtime)
+--   INITIAL Mode (Runtime)
+--   Frame Boundary Variations
+--   Special Partition Cases
+--   DEFINE Special Cases
+--   Absorption Dynamic Flags
+--   Zero-Consumption Cycle Detection
+--
+-- Responsibility:
+--   - NFA runtime execution paths
+--   - Context/State lifecycle management
+--   - Runtime boundary conditions and protections
+--
+-- NOT tested here (covered in other files):
+--   - Pattern parsing/optimization (rpr_base.sql)
+--   - EXPLAIN output (rpr_explain.sql)
+--   - PREV/NEXT semantics (rpr.sql)
+-- ============================================================
+
+-- ============================================================
+-- Basic NFA Flow
+-- ============================================================
+
+-- Simple sequential pattern
+WITH test_sequential AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['B']),
+        (3, ARRAY['C']),
+        (4, ARRAY['D']),
+        (5, ARRAY['_'])  -- No match
+    ) 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_sequential
+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)
+);
+
+-- Quantified pattern (A+ B+ C+)
+WITH test_quantified AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),
+        (4, ARRAY['B']),
+        (5, ARRAY['B']),
+        (6, ARRAY['C']),
+        (7, ARRAY['C']),
+        (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
+FROM test_quantified
+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 'A' = ANY(flags),
+        B AS 'B' = ANY(flags),
+        C AS 'C' = ANY(flags)
+);
+
+-- Optional pattern (A B? C)
+WITH test_optional AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['C']),  -- B skipped
+        (3, ARRAY['A']),
+        (4, ARRAY['B']),
+        (5, ARRAY['C']),  -- B matched
+        (6, ARRAY['_'])
+    ) 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_optional
+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 'A' = ANY(flags),
+        B AS 'B' = ANY(flags),
+        C AS 'C' = ANY(flags)
+);
+
+-- Alternation pattern (A (B|C) D)
+WITH test_alternation AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['B']),  -- First branch
+        (3, ARRAY['D']),
+        (4, ARRAY['A']),
+        (5, ARRAY['C']),  -- Second branch
+        (6, ARRAY['D']),
+        (7, ARRAY['_'])
+    ) 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_alternation
+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)
+);
+
+-- ============================================================
+-- Absorption Optimization
+-- ============================================================
+
+-- Absorbable pattern (A+)
+WITH test_absorbable AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),
+        (4, ARRAY['A']),
+        (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
+FROM test_absorbable
+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)
+);
+
+-- Mixed absorbable/non-absorbable ((A+) | B)
+WITH test_mixed_absorption AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),
+        (4, ARRAY['B']),
+        (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
+FROM test_mixed_absorption
+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)
+);
+
+-- State coverage (same elemIdx, different count)
+WITH test_state_coverage AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),
+        (4, ARRAY['B']),
+        (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
+FROM test_state_coverage
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A{2,} B)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags)
+);
+
+-- Reluctant pattern (A+?) - not absorbable
+-- Compare with greedy A+ above: reluctant excluded from absorption.
+-- Each context produces minimum match independently.
+WITH test_reluctant_absorption AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),
+        (4, ARRAY['A']),
+        (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
+FROM test_reluctant_absorption
+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)
+);
+
+-- 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)
+);
+
+-- 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
+        (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
+-- ============================================================
+
+-- Multiple overlapping contexts (SKIP TO NEXT ROW)
+WITH test_overlapping_contexts AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),
+        (4, ARRAY['B']),
+        (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
+FROM test_overlapping_contexts
+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)
+);
+
+-- Failed context cleanup (early failure)
+WITH test_context_cleanup AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['_']),  -- Pruned at first row
+        (2, ARRAY['A']),
+        (3, ARRAY['_']),  -- Mismatched after row 2
+        (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_context_cleanup
+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)
+);
+
+-- Partition end (incomplete contexts)
+WITH test_partition_end AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A'])
+        -- Pattern requires B, but partition 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_partition_end
+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)
+);
+
+-- Completed context encountered during processing
+-- Pattern (A | B C D): Ctx1 takes long B->C->D path, while Ctx2 takes
+-- short A path and completes first. Next row sees Ctx2
+-- with states=NULL and skips it.
+WITH test_completed_ctx AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['B', '_']),
+        (2, ARRAY['C', 'A']),
+        (3, ARRAY['D', '_']),
+        (4, ARRAY['_', '_'])
+    ) 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_completed_ctx
+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)
+);
+
+-- Reluctant context lifecycle (A+? B with SKIP TO NEXT ROW)
+-- A+? exits early but if B not available, falls back to loop.
+-- Contexts not absorbed (reluctant), so multiple survive.
+WITH test_reluctant_context AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['B']),
+        (4, ARRAY['_'])
+    ) 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_context
+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)
+);
+
+-- ============================================================
+-- Advance Phase (Epsilon Transitions)
+-- ============================================================
+
+-- Nested groups ((A B)+)
+WITH test_nested_groups AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['B']),
+        (3, ARRAY['A']),
+        (4, ARRAY['B']),
+        (5, ARRAY['A']),
+        (6, ARRAY['B']),
+        (7, ARRAY['_'])
+    ) 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_groups
+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)
+);
+
+-- Multiple alternation branches (A (B|C|D) E)
+WITH test_multi_alt AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['B']),
+        (3, ARRAY['E']),
+        (4, ARRAY['A']),
+        (5, ARRAY['C']),
+        (6, ARRAY['E']),
+        (7, ARRAY['A']),
+        (8, ARRAY['D']),
+        (9, 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_alt
+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)
+    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)
+);
+
+-- Optional VAR at start (A? B C)
+WITH test_optional_var AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['B']),  -- A skipped
+        (2, ARRAY['C']),
+        (3, ARRAY['A']),  -- A matched
+        (4, ARRAY['B']),
+        (5, 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_optional_var
+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 'A' = ANY(flags),
+        B AS 'B' = ANY(flags),
+        C AS 'C' = ANY(flags)
+);
+
+-- Nested alternation ((A|B) (C|D))
+WITH test_nested_alt AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['C']),  -- A C
+        (3, ARRAY['A']),
+        (4, ARRAY['D']),  -- A D
+        (5, ARRAY['B']),
+        (6, ARRAY['C']),  -- B C
+        (7, ARRAY['B']),
+        (8, ARRAY['D'])   -- B 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_nested_alt
+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)
+);
+
+-- Mixed greedy/reluctant sequence: A+? B+ (reluctant A, greedy B)
+-- A exits as early as possible, B consumes the rest greedily
+WITH test_mixed_reluctant AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A','B']),
+        (4, ARRAY['B']),
+        (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_mixed_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)
+);
+
+-- 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).
+WITH test_optional_reluctant AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['B']),
+        (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_optional_reluctant
+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)
+);
+
+-- 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 (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A','B']),
+        (3, ARRAY['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_greedy_then_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)
+);
+
+-- 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
+-- ============================================================
+
+-- Simple VAR with END next (A B C all min=max=1)
+WITH test_simple_var AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['B']),
+        (3, ARRAY['C']),
+        (4, ARRAY['_'])
+    ) 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_simple_var
+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 'A' = ANY(flags),
+        B AS 'B' = ANY(flags),
+        C AS 'C' = ANY(flags)
+);
+
+-- VAR max exceeded (A{2,3})
+WITH test_max_exceeded AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),  -- Max = 3
+        (4, ARRAY['A']),  -- Exceeds max, state removed
+        (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_max_exceeded
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A{2,3} B)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags)
+);
+
+-- Non-matching VAR (DEFINE false)
+WITH test_non_matching AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['_']),  -- B not matched (DEFINE false)
+        (3, ARRAY['A']),
+        (4, ARRAY['B']),  -- B matched
+        (5, 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_non_matching
+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 'A' = ANY(flags),
+        B AS 'B' = ANY(flags),
+        C AS 'C' = ANY(flags)
+);
+
+-- ============================================================
+-- Frame Boundary Handling
+-- ============================================================
+
+-- Limited frame (ROWS BETWEEN CURRENT ROW AND 3 FOLLOWING)
+WITH test_limited_frame AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),
+        (4, ARRAY['B']),  -- Within 3 FOLLOWING
+        (5, ARRAY['B']),  -- Beyond 3 FOLLOWING from row 1
+        (6, ARRAY['_'])
+    ) 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_limited_frame
+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)
+);
+
+-- Unbounded frame (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+WITH test_unbounded_frame AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),
+        (4, ARRAY['A']),
+        (5, ARRAY['A']),
+        (6, ARRAY['B'])  -- Far from start, but unbounded
+    ) 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_unbounded_frame
+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)
+);
+
+-- Match exceeds frame boundary
+WITH test_frame_exceeded AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A'])
+        -- Frame ends at row 3 (2 FOLLOWING), B never appears
+    ) 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_exceeded
+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)
+);
+
+-- Frame boundary forced mismatch
+-- Limited frame with enough rows so that a context's frame boundary
+-- is exceeded while still processing.
+WITH test_frame_boundary 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_frame_boundary
+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)
+);
+
+-- Reluctant with limited frame (A+? B with 2 FOLLOWING)
+-- Reluctant exits early, B must be within frame boundary
+WITH test_reluctant_frame AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['B']),
+        (4, ARRAY['_'])
+    ) 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_frame
+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)
+);
+
+-- ============================================================
+-- State Management
+-- ============================================================
+
+-- Duplicate state creation
+WITH test_duplicate_states AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A', 'B']),  -- Both A and B match (creates duplicate states via different paths)
+        (2, ARRAY['C', '_']),
+        (3, 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_duplicate_states
+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)
+);
+
+-- Reluctant duplicate state handling
+-- (A+? | B+?) creates exit and loop states; exit paths may converge
+WITH test_reluctant_dedup AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A','B']),
+        (2, ARRAY['A','B']),
+        (3, ARRAY['_'])
+    ) 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_dedup
+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)
+);
+
+-- Large pattern (stress free list)
+WITH test_large_pattern AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['B']),
+        (3, ARRAY['C']),
+        (4, ARRAY['D']),
+        (5, ARRAY['E']),
+        (6, ARRAY['F']),
+        (7, ARRAY['G']),
+        (8, ARRAY['H']),
+        (9, ARRAY['I']),
+        (10, ARRAY['J'])
+    ) 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_large_pattern
+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 F G H I J)
+    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),
+        G AS 'G' = ANY(flags),
+        H AS 'H' = ANY(flags),
+        I AS 'I' = ANY(flags),
+        J AS 'J' = ANY(flags)
+);
+
+-- Reduced frame map reallocation (> 1024 rows)
+WITH test_map_realloc AS (
+    SELECT id, CASE WHEN id % 2 = 1 THEN ARRAY['A'] ELSE ARRAY['B'] END AS flags
+    FROM generate_series(1, 1100) AS id
+)
+SELECT count(*), min(match_start), max(match_end)
+FROM (
+    SELECT id, flags,
+           first_value(id) OVER w AS match_start,
+           last_value(id) OVER w AS match_end
+    FROM test_map_realloc
+    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)
+    )
+) sub;
+
+-- ============================================================
+-- Statistics and Diagnostics
+-- ============================================================
+
+-- Matched contexts
+WITH test_matched AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['B']),
+        (3, ARRAY['A']),
+        (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_matched
+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)
+);
+
+-- Pruned contexts (failed at first row)
+WITH test_pruned AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['_']),  -- Pruned
+        (2, ARRAY['_']),  -- Pruned
+        (3, ARRAY['A']),
+        (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_pruned
+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)
+);
+
+-- Mismatched contexts (failed after multiple rows)
+WITH test_mismatched AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['_']),  -- Mismatched after 2 rows
+        (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_mismatched
+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)
+);
+
+-- Reluctant not absorbed (A+? with SKIP TO NEXT ROW)
+-- Compare with greedy A+ below: reluctant is not absorbable,
+-- so all contexts survive independently.
+WITH test_reluctant_stats AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),
+        (4, ARRAY['A']),
+        (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
+FROM test_reluctant_stats
+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)
+);
+
+-- Absorbed contexts
+WITH test_absorbed AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),
+        (4, ARRAY['A']),
+        (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
+FROM test_absorbed
+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)
+);
+
+-- Skipped contexts (SKIP TO NEXT ROW)
+WITH test_skipped AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),
+        (4, ARRAY['B'])  -- Completes match starting at row 1
+    ) 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_skipped
+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)
+);
+
+-- ============================================================
+-- Quantifier Runtime Behavior
+-- ============================================================
+
+-- Large count handling (A{100})
+WITH test_large_count AS (
+    SELECT i AS id, ARRAY['A'] AS flags
+    FROM generate_series(1, 105) i
+)
+SELECT id, flags,
+       first_value(id) OVER w AS match_start,
+       last_value(id) OVER w AS match_end
+FROM test_large_count
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A{100})
+    DEFINE
+        A AS 'A' = ANY(flags)
+);
+
+-- Unlimited quantifier (A{10,})
+WITH test_unlimited AS (
+    SELECT i AS id, ARRAY['A'] AS flags
+    FROM generate_series(1, 15) i
+    UNION ALL
+    SELECT 16, ARRAY['B']
+)
+SELECT id, flags,
+       first_value(id) OVER w AS match_start,
+       last_value(id) OVER w AS match_end
+FROM test_unlimited
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A{10,} B)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags)
+);
+
+-- Min boundary (A{3,5})
+WITH test_min_boundary AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),  -- Min=3 reached, exit path available
+        (4, ARRAY['B']),  -- Match ends at min
+        (5, ARRAY['A']),
+        (6, ARRAY['A']),
+        (7, ARRAY['A']),
+        (8, ARRAY['A']),
+        (9, ARRAY['A']),  -- Count=5, max reached
+        (10, ARRAY['B'])  -- Match ends at max
+    ) 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_min_boundary
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A{3,5} B)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags)
+);
+
+-- Max boundary exceeded (A{3,5})
+WITH test_max_boundary AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),
+        (4, ARRAY['A']),
+        (5, ARRAY['A']),
+        (6, ARRAY['A']),  -- Count=6 > max=5, row 1 context removed
+        (7, ARRAY['B'])   -- Row 1 context: no match (exceeded max)
+    ) 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_max_boundary
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A{3,5} B)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags)
+);
+
+-- Greedy vs reluctant: A+ matches all rows, A+? matches minimum
+WITH test_greedy_vs_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 test_greedy_vs_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)
+);
+
+-- Same data, reluctant A+? exits at row 3 where B is first available
+WITH test_greedy_vs_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 test_greedy_vs_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)
+);
+
+-- Reluctant group: (A B)+? matches minimum 1 iteration
+WITH test_reluctant_group AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['B']),
+        (3, ARRAY['A']),
+        (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_group
+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+? 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
+-- ============================================================
+
+-- Complex nested nullable ((A* B*)*) - Runtime protection
+WITH test_complex_nested AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['B']),
+        (4, ARRAY['B']),
+        (5, 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_complex_nested
+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)
+);
+
+-- Nested nullable with quantifier ((A{0,3})*)
+WITH test_nested_quantifier AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),
+        (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_nested_quantifier
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A{0,3})*)
+    DEFINE
+        A AS 'A' = ANY(flags)
+);
+
+-- Reluctant nullable: A*? (prefers 0 matches)
+-- A*? always takes skip path (0 iterations preferred)
+WITH test_reluctant_nullable AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),
+        (4, ARRAY['_'])
+    ) 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_nullable
+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)
+);
+
+-- ============================================================
+-- Alternation Runtime Behavior
+-- ============================================================
+
+-- Multi-branch alternation (A (B|C|D|E) F)
+WITH test_multi_branch AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['B']),
+        (3, ARRAY['F']),
+        (4, ARRAY['A']),
+        (5, ARRAY['C']),
+        (6, ARRAY['F']),
+        (7, ARRAY['A']),
+        (8, ARRAY['D']),
+        (9, ARRAY['F']),
+        (10, ARRAY['A']),
+        (11, ARRAY['E']),
+        (12, 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_multi_branch
+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) 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)
+);
+
+-- Alternation with quantifiers (A+ | B+ | C+)
+WITH test_alt_quantifiers AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),
+        (4, ARRAY['B']),
+        (5, ARRAY['B']),
+        (6, ARRAY['C']),
+        (7, ARRAY['C']),
+        (8, ARRAY['C']),
+        (9, 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_alt_quantifiers
+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 'A' = ANY(flags),
+        B AS 'B' = ANY(flags),
+        C AS 'C' = ANY(flags)
+);
+
+-- altPriority replacement (A B C | D)
+-- D branch (higher altPriority) matches first at row 1,
+-- then A B C branch (lower altPriority) replaces it at row 3.
+WITH test_alt_replace AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A', 'D']),
+        (2, ARRAY['B', '_']),
+        (3, ARRAY['C', '_']),
+        (4, ARRAY['_', '_'])
+    ) 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_replace
+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)
+);
+
+-- ALT lexical order takes priority over greedy (longer match).
+-- Row 1 matches both A and B; A wins by lexical order (match 1-1).
+WITH test_alt_lexical_order AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A','B']),  -- A and B both match
+        (2, ARRAY['_','C'])   -- only C matches (would continue B 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_alt_lexical_order
+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)
+);
+
+-- ALT with reluctant: (A+? | B+) - A branch is reluctant, B is greedy.
+-- Row 1 matches both A and B. A+? exits immediately (match 1-1).
+WITH test_alt_reluctant AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A','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_alt_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)
+);
+
+-- Optional first branch in ALT with quantifier: (A? | B){1,2}
+-- First branch A? exit path may loop back to ALT and trigger cycle
+-- detection during DFS.  All branches must receive correct counts.
+WITH test_alt_opt_first 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_alt_opt_first
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (((A? | B){1,2}))
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags)
+);
+
+-- Mixed A/B rows across iterations of (A? | B){1,2}
+WITH test_alt_opt_mixed AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['B']),
+        (3, ARRAY['A','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_alt_opt_mixed
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (((A? | B){1,2}))
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags)
+);
+
+-- Reluctant variant: (A?? | B){1,2}
+WITH test_alt_opt_reluctant 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_alt_opt_reluctant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (((A?? | B){1,2}))
+    DEFINE
+        A AS 'A' = ANY(flags),
+        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
+-- ============================================================
+
+-- Three-level nesting ((((A B)+)+)+)
+WITH test_deep_nesting AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['B']),
+        (3, ARRAY['A']),
+        (4, ARRAY['B']),
+        (5, ARRAY['A']),
+        (6, ARRAY['B']),
+        (7, ARRAY['_'])
+    ) 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_deep_nesting
+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)
+);
+
+-- Multiple groups in nesting (((A B) (C D))+)
+WITH test_nested_sequential 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']),
+        (9, ARRAY['_'])
+    ) 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_sequential
+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)
+);
+
+-- 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']),
+        (2, ARRAY['B']),
+        (3, ARRAY['A']),
+        (4, ARRAY['B']),
+        (5, ARRAY['A']),
+        (6, ARRAY['B']),
+        (7, ARRAY['A']),
+        (8, ARRAY['B']),
+        (9, ARRAY['_'])
+    ) 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_end_nested_max
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (((A B){2})+)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags)
+);
+
+-- 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']),
+        (2, ARRAY['B']),
+        (3, ARRAY['A']),
+        (4, ARRAY['B']),
+        (5, ARRAY['A']),
+        (6, ARRAY['B']),
+        (7, ARRAY['A']),
+        (8, ARRAY['B']),
+        (9, ARRAY['_'])
+    ) 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_end_nested_mid
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (((A B){1,3})+)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags)
+);
+
+-- Nested reluctant group ((A B)+?) with following element C
+-- Inner group exits after minimum 1 iteration
+WITH test_nested_reluctant AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['B']),
+        (3, ARRAY['A']),
+        (4, ARRAY['B']),
+        (5, 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_nested_reluctant
+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 'A' = ANY(flags),
+        B AS 'B' = ANY(flags),
+        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)
+-- ============================================================
+
+-- SKIP PAST LAST ROW (non-overlapping matches)
+WITH test_skip_past AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),
+        (4, ARRAY['A']),
+        (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
+FROM test_skip_past
+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)
+);
+
+-- SKIP TO NEXT ROW (overlapping matches)
+WITH test_skip_next AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),
+        (4, ARRAY['A']),
+        (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
+FROM test_skip_next
+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)
+);
+
+-- SKIP difference verification
+WITH test_skip_diff AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['B']),
+        (3, ARRAY['A']),
+        (4, ARRAY['B'])
+    ) AS t(id, flags)
+)
+SELECT 'SKIP PAST' AS mode, id, flags,
+       first_value(id) OVER w AS match_start,
+       last_value(id) OVER w AS match_end
+FROM test_skip_diff
+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)
+)
+UNION ALL
+SELECT 'SKIP NEXT' AS mode, id, flags,
+       first_value(id) OVER w AS match_start,
+       last_value(id) OVER w AS match_end
+FROM test_skip_diff
+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)
+)
+ORDER BY mode, id;
+
+-- Reluctant SKIP comparison: A+? with SKIP PAST vs SKIP NEXT
+WITH test_reluctant_skip AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),
+        (4, ARRAY['_'])
+    ) AS t(id, flags)
+)
+SELECT 'SKIP PAST' AS mode, id, flags,
+       first_value(id) OVER w AS match_start,
+       last_value(id) OVER w AS match_end
+FROM test_reluctant_skip
+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)
+)
+UNION ALL
+SELECT 'SKIP NEXT' AS mode, id, flags,
+       first_value(id) OVER w AS match_start,
+       last_value(id) OVER w AS match_end
+FROM test_reluctant_skip
+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)
+)
+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 (
+    SELECT * FROM (VALUES
+        (1, ARRAY['_']),  -- Unmatched
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),
+        (4, ARRAY['_']),  -- Unmatched
+        (5, ARRAY['A'])
+    ) 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_initial_mode
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A+)
+    DEFINE
+        A AS 'A' = ANY(flags)
+);
+
+-- Default mode (include all rows)
+WITH test_default_mode AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['_']),  -- Unmatched, but included
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),
+        (4, ARRAY['_']),  -- Unmatched, but included
+        (5, ARRAY['A'])
+    ) 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_default_mode
+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)
+);
+
+-- Mode difference verification (INITIAL not yet supported - produces syntax error)
+WITH test_mode_diff AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['_']),
+        (2, ARRAY['A']),
+        (3, ARRAY['_'])
+    ) AS t(id, flags)
+)
+SELECT 'INITIAL' AS mode, COUNT(*) AS row_count
+FROM (
+    SELECT id FROM test_mode_diff
+    WINDOW w AS (
+        ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        INITIAL
+        AFTER MATCH SKIP TO NEXT ROW
+        PATTERN (A)
+        DEFINE A AS 'A' = ANY(flags)
+    )
+) sub
+UNION ALL
+SELECT 'DEFAULT' AS mode, COUNT(*) AS row_count
+FROM (
+    SELECT id FROM test_mode_diff
+    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)
+    )
+) sub
+ORDER BY mode;
+
+-- ============================================================
+-- Frame Boundary Variations
+-- ============================================================
+
+-- Very limited frame (1 FOLLOWING)
+WITH test_one_following AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['B']),  -- Within 1 FOLLOWING
+        (3, ARRAY['A']),  -- Beyond 1 FOLLOWING from row 1
+        (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_one_following
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A B)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags)
+);
+
+-- Medium frame (10 FOLLOWING)
+WITH test_ten_following AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),
+        (4, ARRAY['A']),
+        (5, ARRAY['A']),
+        (6, ARRAY['A']),
+        (7, ARRAY['A']),
+        (8, ARRAY['A']),
+        (9, ARRAY['A']),
+        (10, ARRAY['A']),
+        (11, ARRAY['B']),  -- Within 10 FOLLOWING from row 1
+        (12, ARRAY['A']),
+        (13, ARRAY['B'])   -- Beyond 10 FOLLOWING from row 1
+    ) 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_ten_following
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND 10 FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A+ B)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags)
+);
+
+-- Exact boundary match
+WITH test_exact_boundary AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),
+        (4, ARRAY['A']),
+        (5, ARRAY['B'])   -- Exactly at 4 FOLLOWING (frame end)
+    ) 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_exact_boundary
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND 4 FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A+ B)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        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
+-- ============================================================
+
+-- Empty partition (0 rows)
+WITH test_empty_partition AS (
+    SELECT * FROM (VALUES
+        (1, 1, ARRAY['A']),
+        (2, 2, ARRAY['_'])  -- Different partition
+    ) AS t(id, part, flags)
+)
+SELECT id, part, flags,
+       first_value(id) OVER w AS match_start,
+       last_value(id) OVER w AS match_end
+FROM test_empty_partition
+WHERE part = 99  -- No rows match
+WINDOW w AS (
+    PARTITION BY part
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A)
+    DEFINE
+        A AS 'A' = ANY(flags)
+);
+
+-- Single row partition
+WITH test_single_row AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A'])
+    ) 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_single_row
+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)
+);
+
+-- All rows fail matching (all DEFINE false)
+WITH test_all_fail AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A'])
+    ) 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_all_fail
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A+)
+    DEFINE
+        A AS false  -- All rows fail
+);
+
+-- Partition end with absorbable pattern
+-- SKIP PAST LAST ROW + unbounded frame + all rows match A
+-- Triggers absorb in !rowExists path at partition boundary.
+WITH test_absorb_partition_end AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),
+        (4, ARRAY['A']),
+        (5, ARRAY['A'])
+    ) 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_partition_end
+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)
+);
+
+-- ============================================================
+-- DEFINE Special Cases
+-- ============================================================
+
+-- Undefined variable in DEFINE
+WITH test_undefined_var AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['X']),  -- B not defined, defaults to TRUE
+        (3, ARRAY['C']),
+        (4, ARRAY['A']),
+        (5, ARRAY['_']),  -- B defaults to TRUE, but no flags
+        (6, 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_undefined_var
+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 'A' = ANY(flags),
+        -- B is undefined, defaults to TRUE
+        C AS 'C' = ANY(flags)
+);
+
+-- ============================================================
+-- Absorption Dynamic Flags
+-- ============================================================
+
+-- Partial absorbable pattern ((A+) B)
+WITH test_partial_absorbable AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),
+        (4, ARRAY['B']),
+        (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
+FROM test_partial_absorbable
+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)
+);
+
+-- Dynamic flag update ((A+) | B)
+WITH test_dynamic_flags 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_dynamic_flags
+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)
+);
+
+-- 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.
+WITH test_non_absorbable AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['B']),
+        (3, ARRAY['A']),
+        (4, ARRAY['B']),
+        (5, ARRAY['C']),
+        (6, ARRAY['_'])
+    ) 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_non_absorbable
+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)
+);
+
+-- Absorption flags early return (!hasAbsorbableState)
+-- Pattern (A B)+ C D with SKIP PAST LAST ROW
+-- After reaching C (non-absorbable), hasAbsorbableState becomes false.
+-- On next row (D), the early return fires.
+WITH test_absorption_early_return AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['B']),
+        (3, ARRAY['A']),
+        (4, ARRAY['B']),
+        (5, ARRAY['C']),
+        (6, ARRAY['D']),
+        (7, ARRAY['_'])
+    ) 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_absorption_early_return
+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)
+);
+
+-- 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.
+WITH test_coverage_fail AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A', '_']),
+        (2, ARRAY['A', 'B']),
+        (3, ARRAY['A', '_']),
+        (4, ARRAY['A', '_']),
+        (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
+FROM test_coverage_fail
+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)
+);
+
+-- 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.
+WITH test_older_completed AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['B']),
+        (3, ARRAY['B']),
+        (4, ARRAY['_'])
+    ) 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_older_completed
+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)
+);
+
+-- 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.
+WITH test_older_non_absorbable AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['B', '_']),
+        (2, ARRAY['C', 'A']),
+        (3, ARRAY['_', 'A']),
+        (4, ARRAY['_', '_'])
+    ) 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_older_non_absorbable
+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)
+);
+
+-- Reluctant branch in ALT not absorbable: (A+?) | B
+-- A+? is reluctant so not absorbable. Compare with greedy (A+) | B above.
+WITH test_reluctant_alt_absorption AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),
+        (4, ARRAY['B']),
+        (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
+FROM test_reluctant_alt_absorption
+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)
+);
+
+-- ============================================================
+-- Zero-Consumption Cycle Detection
+-- ============================================================
+
+-- Cycle prevention at count > 0: (A*)* inner skip cycles at count=3
+WITH test_cycle_nonzero AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),
+        (4, ARRAY['B'])  -- Inner A* matches 0, cycles at count=3
+    ) 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_cycle_nonzero
+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)
+);
+
+-- Cycle with mixed nullables: (A* B*)* multiple nullable paths
+WITH test_cycle_mixed 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 test_cycle_mixed
+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)
+);
+
+-- ============================================================
+-- Standard Clause 7: Formal Pattern Matching Rules
+-- ISO/IEC 19075-5:2021, Clause 7
+-- ============================================================
+
+-- ------------------------------------------------------------
+-- 7.2.2 Alternation: first alternative is preferred
+-- ------------------------------------------------------------
+
+-- (A | B): A preferred over B when both could match
+-- Row 1 has both A and B flags: A should be chosen (first alternative)
+WITH test_alt_prefer AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A','B']),
+        (2, ARRAY['B']),
+        (3, ARRAY['A'])
+    ) 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_prefer
+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)
+);
+
+-- (A{1,2} | B{2,3}): all A-matches before all B-matches
+-- Standard example: preferment order is AA, A, BBB, BB
+-- Rows 1-2 have both A and B: greedy A{1,2} should match 1-2
+WITH test_alt_quantified AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A','B']),
+        (2, ARRAY['A','B']),
+        (3, ARRAY['B']),
+        (4, ARRAY['B']),
+        (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_alt_quantified
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A{1,2} | B{2,3}))
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags)
+);
+
+-- ------------------------------------------------------------
+-- 7.2.3 Concatenation: lexicographic ordering
+-- ------------------------------------------------------------
+
+-- ((A | B) (C | D)): preferment order is AC, AD, BC, BD
+-- Row 1 matches A and B, Row 2 matches C and D
+-- Preferred match: A then C (first alternatives in both positions)
+WITH test_concat_lex AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A','B']),
+        (2, ARRAY['C','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_concat_lex
+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)
+);
+
+-- ((A | B) C): first alt (A) fails, second alt (B) succeeds
+-- Tests backtracking: row 1 has only B, row 2 has C
+WITH test_concat_backtrack AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['B']),
+        (2, ARRAY['C']),
+        (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 test_concat_backtrack
+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 'A' = ANY(flags),
+        B AS 'B' = ANY(flags),
+        C AS 'C' = ANY(flags)
+);
+
+-- ------------------------------------------------------------
+-- 7.2.4 Quantification: greedy/reluctant, lexicographic > length
+-- ------------------------------------------------------------
+
+-- V{2,4} greedy: longer match preferred
+WITH test_quant_greedy AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),
+        (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_quant_greedy
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A{2,4})
+    DEFINE
+        A AS 'A' = ANY(flags)
+);
+
+-- V{2,4}? reluctant: shorter match preferred
+WITH test_quant_reluctant AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),
+        (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_quant_reluctant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A{2,4}?)
+    DEFINE
+        A AS 'A' = ANY(flags)
+);
+
+-- ((A|B){1,2}) greedy: lexicographic > length
+-- Standard example: preferment AA, AB, A, BA, BB, B
+-- Single A preferred over B-starting longer match
+WITH test_quant_lex_greedy AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A','B']),
+        (2, 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_quant_lex_greedy
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (((A | B){1,2}))
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags)
+);
+
+-- ((A|B){1,2}?) reluctant: lexicographic > length
+-- Standard example: preferment A, AA, AB, B, BA, BB
+-- Single A preferred over any B-starting match
+WITH test_quant_lex_reluctant AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A','B']),
+        (2, 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_quant_lex_reluctant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (((A | B){1,2}?))
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags)
+);
+
+-- ------------------------------------------------------------
+-- 7.2.6 Anchors (not yet implemented - syntax error expected)
+-- ------------------------------------------------------------
+
+-- ^ anchor: not yet supported
+SELECT count(*) OVER w FROM (SELECT 1 AS v) t
+WINDOW w AS (ORDER BY v ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (^ A) DEFINE A AS TRUE);
+
+-- $ anchor: not yet supported
+SELECT count(*) OVER w FROM (SELECT 1 AS v) t
+WINDOW w AS (ORDER BY v ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A $) DEFINE A AS TRUE);
+
+-- ------------------------------------------------------------
+-- 7.2.8 Infinite repetitions of empty matches
+-- (Perl lower-bound stopping rule)
+-- ------------------------------------------------------------
+-- Standard examples from 7.2.8:
+--   (A?){0,3}: allowed strings include STR00=(), STR01=(A), STR02=(empty),
+--              STR03=(AA), STR04=(A,empty), STR07=(AAA), STR08=(AA,empty)
+--   (A?){1,3}: same as {0,3} but STR00 excluded (min=1 not met)
+--   (A?){2,3}: STR03-06 (len 2) and STR07,08,11,12 (len 3) are valid
+--              STR06=(STRE,STRE) IS valid because non-final STRE at
+--              position 1 fills the lower bound
+
+-- (A??)*B: Standard 7.2.8 introductory example
+-- "matched against a sequence of rows for which the only feasible
+--  matching is: B"
+-- A?? is reluctant, prefers empty. * is greedy but Perl rule stops
+-- after empty match with min(=0) satisfied.
+-- Expected: each B row matches alone (A?? empty, * stops, B matches)
+WITH test_empty_reluctant_star AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['B']),
+        (2, ARRAY['B']),
+        (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_empty_reluctant_star
+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)
+);
+
+-- (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})
+WITH test_728_min0 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_728_min0
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A?){0,3})
+    DEFINE
+        A AS 'A' = ANY(flags)
+);
+
+-- (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})
+WITH test_728_min1 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_728_min1
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A?){1,3})
+    DEFINE
+        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
+WITH test_728_min2 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_728_min2
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A?){2,3})
+    DEFINE
+        A AS 'A' = ANY(flags)
+);
+
+-- (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})
+-- 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']),
+        (2, ARRAY['A']),
+        (3, ARRAY['B']),
+        (4, ARRAY['A'])
+    ) 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_728_min2_mixed
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A?){2,3})
+    DEFINE
+        A AS 'A' = ANY(flags)
+);
+
+-- (A? B?){2,3}: multi-element nullable body with real matches
+-- Body A? B? is nullable (both optional), but A and B DO match rows.
+-- Real (non-empty) iterations loop back normally; fast-forward only
+-- 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
+WITH test_728_multi_body AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['B']),
+        (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_728_multi_body
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A? B?){2,3})
+    DEFINE
+        A AS 'A' = ANY(flags),
+        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)
+WITH test_728_multi_empty AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['C']),
+        (2, ARRAY['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_728_multi_empty
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A? B?){2,3})
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags)
+);
+
+-- (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
+WITH test_728_multi_mixed AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['B']),
+        (3, ARRAY['C']),
+        (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_728_multi_mixed
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A? B?){2,3})
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags)
+);
+
+-- ------------------------------------------------------------
+-- 7.3 Pattern matching in theory and practice
+-- ------------------------------------------------------------
+
+-- 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
+-- Backtrack: empty A?, then B+ from row 1
+-- Expected: rows 1-3 match as B (A? takes empty match)
+WITH test_73_example AS (
+    SELECT * FROM (VALUES
+        (1, 60),
+        (2, 70),
+        (3, 40)
+    ) AS t(id, price)
+)
+SELECT id, price,
+       first_value(id) OVER w AS match_start,
+       last_value(id) OVER w AS match_end
+FROM test_73_example
+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 price > 100,
+        B AS TRUE
+);
-- 
2.43.0



  [application/octet-stream] v47-0008-Row-pattern-recognition-patch-tests-expected.patch (737.0K, 9-v47-0008-Row-pattern-recognition-patch-tests-expected.patch)
  download | inline diff:
From d7f1706150ac751e30f8b64ec951e6ece2dcf404 Mon Sep 17 00:00:00 2001
From: Tatsuo Ishii <[email protected]>
Date: Sat, 2 May 2026 13:40:29 +0900
Subject: [PATCH v47 8/9] Row pattern recognition patch (tests: expected).

---
 src/test/regress/expected/rpr.out             | 3740 ++++++++++
 src/test/regress/expected/rpr_base.out        | 6589 +++++++++++++++++
 src/test/regress/expected/rpr_explain.out     | 4885 ++++++++++++
 src/test/regress/expected/rpr_integration.out | 1518 ++++
 src/test/regress/expected/rpr_nfa.out         | 4661 ++++++++++++
 5 files changed, 21393 insertions(+)
 create mode 100644 src/test/regress/expected/rpr.out
 create mode 100644 src/test/regress/expected/rpr_base.out
 create mode 100644 src/test/regress/expected/rpr_explain.out
 create mode 100644 src/test/regress/expected/rpr_integration.out
 create mode 100644 src/test/regress/expected/rpr_nfa.out

diff --git a/src/test/regress/expected/rpr.out b/src/test/regress/expected/rpr.out
new file mode 100644
index 00000000000..85384f6b096
--- /dev/null
+++ b/src/test/regress/expected/rpr.out
@@ -0,0 +1,3740 @@
+--
+-- Test for row pattern recognition: WINDOW clause integration and
+-- 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,
+       price INTEGER
+);
+INSERT INTO stock VALUES ('company1', '2023-07-01', 100);
+INSERT INTO stock VALUES ('company1', '2023-07-02', 200);
+INSERT INTO stock VALUES ('company1', '2023-07-03', 150);
+INSERT INTO stock VALUES ('company1', '2023-07-04', 140);
+INSERT INTO stock VALUES ('company1', '2023-07-05', 150);
+INSERT INTO stock VALUES ('company1', '2023-07-06', 90);
+INSERT INTO stock VALUES ('company1', '2023-07-07', 110);
+INSERT INTO stock VALUES ('company1', '2023-07-08', 130);
+INSERT INTO stock VALUES ('company1', '2023-07-09', 120);
+INSERT INTO stock VALUES ('company1', '2023-07-10', 130);
+INSERT INTO stock VALUES ('company2', '2023-07-01', 50);
+INSERT INTO stock VALUES ('company2', '2023-07-02', 2000);
+INSERT INTO stock VALUES ('company2', '2023-07-03', 1500);
+INSERT INTO stock VALUES ('company2', '2023-07-04', 1400);
+INSERT INTO stock VALUES ('company2', '2023-07-05', 1500);
+INSERT INTO stock VALUES ('company2', '2023-07-06', 60);
+INSERT INTO stock VALUES ('company2', '2023-07-07', 1100);
+INSERT INTO stock VALUES ('company2', '2023-07-08', 1300);
+INSERT INTO stock VALUES ('company2', '2023-07-09', 1200);
+INSERT INTO stock VALUES ('company2', '2023-07-10', 1300);
+SELECT * FROM stock;
+ company  |   tdate    | price 
+----------+------------+-------
+ company1 | 07-01-2023 |   100
+ 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
+ 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
+ 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
+ company2 | 07-07-2023 |  1100
+ company2 | 07-08-2023 |  1300
+ company2 | 07-09-2023 |  1200
+ 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
+ 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)
+);
+ 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)
+
+-- basic test using PREV. UP appears twice
+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+ UP+)
+ DEFINE
+  START AS TRUE,
+  UP AS price > PREV(price),
+  DOWN AS price < PREV(price)
+);
+ company  |   tdate    | price | first_value | last_value | nth_second 
+----------+------------+-------+-------------+------------+------------
+ company1 | 07-01-2023 |   100 |         100 |        150 | 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 |        130 | 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 |       1500 | 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 |       1300 | 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)
+
+-- basic test using PREV. Use '*'
+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)
+);
+ 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 |         150 |         90 | 07-06-2023
+ company1 | 07-06-2023 |    90 |             |            | 
+ company1 | 07-07-2023 |   110 |         110 |        120 | 07-08-2023
+ 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 |        1500 |         60 | 07-06-2023
+ company2 | 07-06-2023 |    60 |             |            | 
+ company2 | 07-07-2023 |  1100 |        1100 |       1200 | 07-08-2023
+ company2 | 07-08-2023 |  1300 |             |            | 
+ company2 | 07-09-2023 |  1200 |             |            | 
+ company2 | 07-10-2023 |  1300 |             |            | 
+(20 rows)
+
+-- basic test using PREV. Use '?'
+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)
+);
+ 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 |         150 |         90 | 07-06-2023
+ company1 | 07-06-2023 |    90 |             |            | 
+ company1 | 07-07-2023 |   110 |         110 |        120 | 07-08-2023
+ 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 |        1500 |         60 | 07-06-2023
+ company2 | 07-06-2023 |    60 |             |            | 
+ company2 | 07-07-2023 |  1100 |        1100 |       1200 | 07-08-2023
+ company2 | 07-08-2023 |  1300 |             |            | 
+ company2 | 07-09-2023 |  1200 |             |            | 
+ company2 | 07-10-2023 |  1300 |             |            | 
+(20 rows)
+
+-- test using alternation (|) with sequence
+SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
+ 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)
+);
+ company  |   tdate    | price | first_value | last_value 
+----------+------------+-------+-------------+------------
+ company1 | 07-01-2023 |   100 |         100 |        200
+ company1 | 07-02-2023 |   200 |             |           
+ company1 | 07-03-2023 |   150 |         150 |        140
+ company1 | 07-04-2023 |   140 |             |           
+ company1 | 07-05-2023 |   150 |         150 |         90
+ company1 | 07-06-2023 |    90 |             |           
+ company1 | 07-07-2023 |   110 |         110 |        130
+ company1 | 07-08-2023 |   130 |             |           
+ company1 | 07-09-2023 |   120 |         120 |        130
+ company1 | 07-10-2023 |   130 |             |           
+ company2 | 07-01-2023 |    50 |          50 |       2000
+ company2 | 07-02-2023 |  2000 |             |           
+ company2 | 07-03-2023 |  1500 |        1500 |       1400
+ company2 | 07-04-2023 |  1400 |             |           
+ company2 | 07-05-2023 |  1500 |        1500 |         60
+ company2 | 07-06-2023 |    60 |             |           
+ company2 | 07-07-2023 |  1100 |        1100 |       1300
+ company2 | 07-08-2023 |  1300 |             |           
+ company2 | 07-09-2023 |  1200 |        1200 |       1300
+ company2 | 07-10-2023 |  1300 |             |           
+(20 rows)
+
+-- test using alternation (|) with group quantifier
+SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
+ 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)
+);
+ company  |   tdate    | price | first_value | last_value 
+----------+------------+-------+-------------+------------
+ company1 | 07-01-2023 |   100 |         100 |        130
+ 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 |             |           
+ 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 |       1300
+ 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 |             |           
+ company2 | 07-07-2023 |  1100 |             |           
+ company2 | 07-08-2023 |  1300 |             |           
+ company2 | 07-09-2023 |  1200 |             |           
+ company2 | 07-10-2023 |  1300 |             |           
+(20 rows)
+
+-- test using nested alternation
+SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (START ((UP DOWN) | FLAT)+)
+ DEFINE
+  START AS TRUE,
+  UP AS price > PREV(price),
+  DOWN AS price < PREV(price),
+  FLAT AS price = PREV(price)
+);
+ company  |   tdate    | price | first_value | last_value 
+----------+------------+-------+-------------+------------
+ company1 | 07-01-2023 |   100 |         100 |        150
+ company1 | 07-02-2023 |   200 |             |           
+ company1 | 07-03-2023 |   150 |             |           
+ company1 | 07-04-2023 |   140 |         140 |         90
+ company1 | 07-05-2023 |   150 |             |           
+ company1 | 07-06-2023 |    90 |             |           
+ company1 | 07-07-2023 |   110 |         110 |        120
+ company1 | 07-08-2023 |   130 |             |           
+ company1 | 07-09-2023 |   120 |             |           
+ company1 | 07-10-2023 |   130 |             |           
+ company2 | 07-01-2023 |    50 |          50 |       1500
+ company2 | 07-02-2023 |  2000 |             |           
+ company2 | 07-03-2023 |  1500 |             |           
+ company2 | 07-04-2023 |  1400 |        1400 |         60
+ company2 | 07-05-2023 |  1500 |             |           
+ company2 | 07-06-2023 |    60 |             |           
+ company2 | 07-07-2023 |  1100 |        1100 |       1200
+ company2 | 07-08-2023 |  1300 |             |           
+ company2 | 07-09-2023 |  1200 |             |           
+ company2 | 07-10-2023 |  1300 |             |           
+(20 rows)
+
+-- test using group with quantifier
+SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN ((UP DOWN)+)
+ DEFINE
+  UP AS price > PREV(price),
+  DOWN AS price < PREV(price)
+);
+ company  |   tdate    | price | first_value | last_value 
+----------+------------+-------+-------------+------------
+ company1 | 07-01-2023 |   100 |             |           
+ company1 | 07-02-2023 |   200 |         200 |        150
+ company1 | 07-03-2023 |   150 |             |           
+ company1 | 07-04-2023 |   140 |             |           
+ company1 | 07-05-2023 |   150 |         150 |         90
+ company1 | 07-06-2023 |    90 |             |           
+ company1 | 07-07-2023 |   110 |             |           
+ company1 | 07-08-2023 |   130 |         130 |        120
+ company1 | 07-09-2023 |   120 |             |           
+ company1 | 07-10-2023 |   130 |             |           
+ company2 | 07-01-2023 |    50 |             |           
+ company2 | 07-02-2023 |  2000 |        2000 |       1500
+ company2 | 07-03-2023 |  1500 |             |           
+ company2 | 07-04-2023 |  1400 |             |           
+ company2 | 07-05-2023 |  1500 |        1500 |         60
+ company2 | 07-06-2023 |    60 |             |           
+ company2 | 07-07-2023 |  1100 |             |           
+ company2 | 07-08-2023 |  1300 |        1300 |       1200
+ company2 | 07-09-2023 |  1200 |             |           
+ company2 | 07-10-2023 |  1300 |             |           
+(20 rows)
+
+-- test using absolute threshold values (not relative PREV)
+-- HIGH: price > 150, LOW: price < 100, MID: neutral range
+SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (LOW MID* HIGH)
+ DEFINE
+  LOW AS price < 100,
+  MID AS price >= 100 AND price <= 150,
+  HIGH AS price > 150
+);
+ company  |   tdate    | price | first_value | last_value 
+----------+------------+-------+-------------+------------
+ company1 | 07-01-2023 |   100 |             |           
+ 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 |             |           
+ 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 |       2000
+ 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 |       1100
+ company2 | 07-07-2023 |  1100 |             |           
+ company2 | 07-08-2023 |  1300 |             |           
+ company2 | 07-09-2023 |  1200 |             |           
+ company2 | 07-10-2023 |  1300 |             |           
+(20 rows)
+
+-- test threshold-based pattern with alternation
+SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (LOW (MID | HIGH)+)
+ DEFINE
+  LOW AS price < 100,
+  MID AS price >= 100 AND price <= 150,
+  HIGH AS price > 150
+);
+ company  |   tdate    | price | first_value | last_value 
+----------+------------+-------+-------------+------------
+ company1 | 07-01-2023 |   100 |             |           
+ 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 |        130
+ 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 |       1500
+ 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 |       1300
+ company2 | 07-07-2023 |  1100 |             |           
+ company2 | 07-08-2023 |  1300 |             |           
+ company2 | 07-09-2023 |  1200 |             |           
+ company2 | 07-10-2023 |  1300 |             |           
+(20 rows)
+
+-- basic test with fixed-length pattern (A A A = exactly 3)
+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
+);
+ company  |   tdate    | price | count 
+----------+------------+-------+-------
+ company1 | 07-01-2023 |   100 |     0
+ company1 | 07-02-2023 |   200 |     0
+ company1 | 07-03-2023 |   150 |     3
+ 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 |     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)
+
+-- test using {n} quantifier (A A A should be optimized to A{3})
+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 >= 140 AND price <= 150
+);
+ company  |   tdate    | price | count 
+----------+------------+-------+-------
+ company1 | 07-01-2023 |   100 |     0
+ company1 | 07-02-2023 |   200 |     0
+ company1 | 07-03-2023 |   150 |     3
+ 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 |     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)
+
+-- test using {n,} quantifier (2 or more)
+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
+);
+ company  |   tdate    | price | count 
+----------+------------+-------+-------
+ company1 | 07-01-2023 |   100 |     0
+ company1 | 07-02-2023 |   200 |     4
+ 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 |     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 |     4
+ 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 |     4
+ company2 | 07-08-2023 |  1300 |     0
+ company2 | 07-09-2023 |  1200 |     0
+ company2 | 07-10-2023 |  1300 |     0
+(20 rows)
+
+-- test using {n,m} quantifier (2 to 4)
+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})
+ DEFINE
+  A AS price > 100
+);
+ company  |   tdate    | price | count 
+----------+------------+-------+-------
+ company1 | 07-01-2023 |   100 |     0
+ company1 | 07-02-2023 |   200 |     4
+ 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 |     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 |     4
+ 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 |     4
+ company2 | 07-08-2023 |  1300 |     0
+ company2 | 07-09-2023 |  1200 |     0
+ 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
+ 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)
+);
+ company  |   tdate    | price | last_value 
+----------+------------+-------+------------
+ company1 | 07-01-2023 |   100 |        140
+ 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 |        120
+ 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 |       1400
+ 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 |       1200
+ company2 | 07-07-2023 |  1100 |           
+ company2 | 07-08-2023 |  1300 |           
+ company2 | 07-09-2023 |  1200 |           
+ company2 | 07-10-2023 |  1300 |           
+(20 rows)
+
+-- omit "START" in DEFINE but it is ok because "START AS TRUE" is
+-- implicitly defined. per spec.
+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
+  UP AS price > PREV(price),
+  DOWN AS price < PREV(price)
+);
+ 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)
+
+-- the first row start with less than or equal to 100
+SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (LOWPRICE UP+ DOWN+)
+ DEFINE
+  LOWPRICE AS price <= 100,
+  UP AS price > PREV(price),
+  DOWN AS price < PREV(price)
+);
+ company  |   tdate    | price | first_value | last_value 
+----------+------------+-------+-------------+------------
+ company1 | 07-01-2023 |   100 |         100 |        140
+ 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
+ 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
+ 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
+ company2 | 07-07-2023 |  1100 |             |           
+ company2 | 07-08-2023 |  1300 |             |           
+ company2 | 07-09-2023 |  1200 |             |           
+ company2 | 07-10-2023 |  1300 |             |           
+(20 rows)
+
+-- second row raises 120%
+SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (LOWPRICE UP+ DOWN+)
+ DEFINE
+  LOWPRICE AS price <= 100,
+  UP AS price > PREV(price) * 1.2,
+  DOWN AS price < PREV(price)
+);
+ company  |   tdate    | price | first_value | last_value 
+----------+------------+-------+-------------+------------
+ company1 | 07-01-2023 |   100 |         100 |        140
+ 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 |             |           
+ 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
+ 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 |             |           
+ company2 | 07-07-2023 |  1100 |             |           
+ company2 | 07-08-2023 |  1300 |             |           
+ company2 | 07-09-2023 |  1200 |             |           
+ company2 | 07-10-2023 |  1300 |             |           
+(20 rows)
+
+-- using NEXT
+SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (START UPDOWN)
+ DEFINE
+  START AS TRUE,
+  UPDOWN AS price > PREV(price) AND price > NEXT(price)
+);
+ company  |   tdate    | price | first_value | last_value 
+----------+------------+-------+-------------+------------
+ company1 | 07-01-2023 |   100 |         100 |        200
+ company1 | 07-02-2023 |   200 |             |           
+ company1 | 07-03-2023 |   150 |             |           
+ company1 | 07-04-2023 |   140 |         140 |        150
+ company1 | 07-05-2023 |   150 |             |           
+ company1 | 07-06-2023 |    90 |             |           
+ company1 | 07-07-2023 |   110 |         110 |        130
+ company1 | 07-08-2023 |   130 |             |           
+ company1 | 07-09-2023 |   120 |             |           
+ company1 | 07-10-2023 |   130 |             |           
+ company2 | 07-01-2023 |    50 |          50 |       2000
+ company2 | 07-02-2023 |  2000 |             |           
+ company2 | 07-03-2023 |  1500 |             |           
+ company2 | 07-04-2023 |  1400 |        1400 |       1500
+ company2 | 07-05-2023 |  1500 |             |           
+ company2 | 07-06-2023 |    60 |             |           
+ company2 | 07-07-2023 |  1100 |        1100 |       1300
+ company2 | 07-08-2023 |  1300 |             |           
+ company2 | 07-09-2023 |  1200 |             |           
+ company2 | 07-10-2023 |  1300 |             |           
+(20 rows)
+
+-- 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 (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ INITIAL
+ PATTERN (START UPDOWN)
+ DEFINE
+  START AS TRUE,
+  UPDOWN AS price > PREV(price) AND price > NEXT(price)
+);
+ company  |   tdate    | price | first_value | last_value 
+----------+------------+-------+-------------+------------
+ company1 | 07-01-2023 |   100 |         100 |        200
+ company1 | 07-02-2023 |   200 |             |           
+ company1 | 07-03-2023 |   150 |             |           
+ company1 | 07-04-2023 |   140 |         140 |        150
+ company1 | 07-05-2023 |   150 |             |           
+ company1 | 07-06-2023 |    90 |             |           
+ company1 | 07-07-2023 |   110 |         110 |        130
+ company1 | 07-08-2023 |   130 |             |           
+ company1 | 07-09-2023 |   120 |             |           
+ company1 | 07-10-2023 |   130 |             |           
+ company2 | 07-01-2023 |    50 |          50 |       2000
+ company2 | 07-02-2023 |  2000 |             |           
+ company2 | 07-03-2023 |  1500 |             |           
+ company2 | 07-04-2023 |  1400 |        1400 |       1500
+ company2 | 07-05-2023 |  1500 |             |           
+ company2 | 07-06-2023 |    60 |             |           
+ company2 | 07-07-2023 |  1100 |        1100 |       1300
+ company2 | 07-08-2023 |  1300 |             |           
+ company2 | 07-09-2023 |  1200 |             |           
+ 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)
+
+--
+-- Error cases: PREV/NEXT usage restrictions
+--
+-- PREV outside DEFINE clause
+SELECT prev(price) FROM stock;
+ERROR:  cannot use prev outside a DEFINE clause
+LINE 1: SELECT prev(price) FROM stock;
+               ^
+-- NEXT outside DEFINE clause
+SELECT next(price) FROM stock;
+ERROR:  cannot use next outside a DEFINE clause
+LINE 1: SELECT next(price) FROM stock;
+               ^
+-- Nested PREV
+SELECT price FROM stock
+WINDOW w AS (
+    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 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 (
+    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 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 (
+    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 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 (
+    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 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 (
+    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 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
+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:  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
+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:  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
+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, (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)
+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 + (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)
+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 + 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)
+
+--
+-- 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:  cannot use prev outside a DEFINE clause
+LINE 1: SELECT prev(price, 2) FROM stock;
+               ^
+SELECT next(price, 2) FROM stock;
+ERROR:  cannot use next outside a DEFINE clause
+LINE 1: SELECT next(price, 2) FROM stock;
+               ^
+-- 2-arg PREV/NEXT: negative offset
+SELECT company, tdate, price, first_value(price) OVER w
+FROM stock
+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:  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
+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:  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
+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:  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
+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:  row pattern navigation offset must not be negative
+EXECUTE test_prev_offset(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
+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:  row pattern navigation offset must not be negative
+EXECUTE test_prev_offset(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
+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
+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)
+
+-- 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
+--
+-- 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:  cannot use first outside a DEFINE clause
+LINE 1: SELECT first(val) FROM rpr_nav;
+               ^
+SELECT last(val) FROM rpr_nav;
+ERROR:  cannot use last outside a DEFINE clause
+LINE 1: SELECT last(val) FROM rpr_nav;
+               ^
+SELECT first(val, 1) FROM rpr_nav;
+ERROR:  cannot use first outside a DEFINE clause
+LINE 1: SELECT first(val, 1) FROM rpr_nav;
+               ^
+-- Functional notation: should access column, not RPR navigation
+CREATE TEMP TABLE rpr_names (prev int, next int, first text, last text);
+INSERT INTO rpr_names VALUES (1, 2, 'Joe', 'Blow');
+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
+--
+-- match everything
+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 PAST LAST ROW
+ INITIAL
+ PATTERN (A+)
+ DEFINE
+  A AS TRUE
+);
+ company  |   tdate    | price | first_value | last_value 
+----------+------------+-------+-------------+------------
+ company1 | 07-01-2023 |   100 |         100 |        130
+ 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 |             |           
+ 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 |       1300
+ 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 |             |           
+ company2 | 07-07-2023 |  1100 |             |           
+ company2 | 07-08-2023 |  1300 |             |           
+ company2 | 07-09-2023 |  1200 |             |           
+ company2 | 07-10-2023 |  1300 |             |           
+(20 rows)
+
+-- nth_value beyond reduced frame (no IGNORE NULLS)
+-- Tests WinGetSlotInFrame/WinGetFuncArgInFrame out-of-frame with RPR
+SELECT company, tdate, price,
+ nth_value(price, 5) OVER w AS nth_5
+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 | nth_5 
+----------+------------+-------+-------
+ company1 | 07-01-2023 |   100 |      
+ 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 |      
+ 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 |      
+ 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 |      
+ company2 | 07-07-2023 |  1100 |      
+ company2 | 07-08-2023 |  1300 |      
+ company2 | 07-09-2023 |  1200 |      
+ company2 | 07-10-2023 |  1300 |      
+(20 rows)
+
+-- backtracking with reclassification of rows
+-- using AFTER MATCH SKIP PAST LAST ROW
+SELECT company, tdate, price, first_value(tdate) OVER w, last_value(tdate) 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 (A+ B+)
+ DEFINE
+  A AS price > 100,
+  B AS price > 100
+);
+ company  |   tdate    | price | first_value | last_value 
+----------+------------+-------+-------------+------------
+ company1 | 07-01-2023 |   100 |             | 
+ company1 | 07-02-2023 |   200 | 07-02-2023  | 07-05-2023
+ company1 | 07-03-2023 |   150 |             | 
+ company1 | 07-04-2023 |   140 |             | 
+ company1 | 07-05-2023 |   150 |             | 
+ company1 | 07-06-2023 |    90 |             | 
+ company1 | 07-07-2023 |   110 | 07-07-2023  | 07-10-2023
+ company1 | 07-08-2023 |   130 |             | 
+ company1 | 07-09-2023 |   120 |             | 
+ company1 | 07-10-2023 |   130 |             | 
+ company2 | 07-01-2023 |    50 |             | 
+ company2 | 07-02-2023 |  2000 | 07-02-2023  | 07-05-2023
+ company2 | 07-03-2023 |  1500 |             | 
+ company2 | 07-04-2023 |  1400 |             | 
+ company2 | 07-05-2023 |  1500 |             | 
+ company2 | 07-06-2023 |    60 |             | 
+ company2 | 07-07-2023 |  1100 | 07-07-2023  | 07-10-2023
+ company2 | 07-08-2023 |  1300 |             | 
+ company2 | 07-09-2023 |  1200 |             | 
+ company2 | 07-10-2023 |  1300 |             | 
+(20 rows)
+
+-- backtracking with reclassification of rows
+-- using AFTER MATCH SKIP TO NEXT ROW
+SELECT company, tdate, price, first_value(tdate) OVER w, last_value(tdate) 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 (A+ B+)
+ DEFINE
+  A AS price > 100,
+  B AS price > 100
+);
+ company  |   tdate    | price | first_value | last_value 
+----------+------------+-------+-------------+------------
+ company1 | 07-01-2023 |   100 |             | 
+ company1 | 07-02-2023 |   200 | 07-02-2023  | 07-05-2023
+ company1 | 07-03-2023 |   150 | 07-03-2023  | 07-05-2023
+ company1 | 07-04-2023 |   140 | 07-04-2023  | 07-05-2023
+ company1 | 07-05-2023 |   150 |             | 
+ company1 | 07-06-2023 |    90 |             | 
+ company1 | 07-07-2023 |   110 | 07-07-2023  | 07-10-2023
+ company1 | 07-08-2023 |   130 | 07-08-2023  | 07-10-2023
+ company1 | 07-09-2023 |   120 | 07-09-2023  | 07-10-2023
+ company1 | 07-10-2023 |   130 |             | 
+ company2 | 07-01-2023 |    50 |             | 
+ company2 | 07-02-2023 |  2000 | 07-02-2023  | 07-05-2023
+ company2 | 07-03-2023 |  1500 | 07-03-2023  | 07-05-2023
+ company2 | 07-04-2023 |  1400 | 07-04-2023  | 07-05-2023
+ company2 | 07-05-2023 |  1500 |             | 
+ company2 | 07-06-2023 |    60 |             | 
+ company2 | 07-07-2023 |  1100 | 07-07-2023  | 07-10-2023
+ company2 | 07-08-2023 |  1300 | 07-08-2023  | 07-10-2023
+ company2 | 07-09-2023 |  1200 | 07-09-2023  | 07-10-2023
+ company2 | 07-10-2023 |  1300 |             | 
+(20 rows)
+
+-- SKIP TO NEXT ROW with limited frame (Ishii-san's test case)
+-- Each row should produce its own match within its frame
+WITH data AS (
+ SELECT * FROM (VALUES
+  ('A', 1), ('A', 2),
+  ('B', 3), ('B', 4)
+ ) AS t(gid, id)
+)
+SELECT gid, id, array_agg(id) OVER w
+FROM data
+WINDOW w AS (
+ PARTITION BY gid
+ ROWS BETWEEN CURRENT ROW AND 2 FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A+)
+ DEFINE A AS id < 10
+);
+ gid | id | array_agg 
+-----+----+-----------
+ A   |  1 | {1,2}
+ A   |  2 | {2}
+ B   |  3 | {3,4}
+ B   |  4 | {4}
+(4 rows)
+
+-- Limited frame with absorption test
+-- Row 0: frame [0,2], can't see B at row 3 -> no match
+-- Row 1: frame [1,3], can see A A B -> should match rows 1-3
+WITH frame_absorb_test AS (
+ SELECT * FROM (VALUES
+  (0, 'A'), (1, 'A'), (2, 'A'), (3, 'B')
+ ) AS t(id, flag)
+)
+SELECT id, flag, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM frame_absorb_test
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND 2 FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE
+  A AS flag = 'A',
+  B AS flag = 'B'
+);
+ id | flag | match_start | match_end 
+----+------+-------------+-----------
+  0 | A    |             |          
+  1 | A    |           1 |         3
+  2 | A    |             |          
+  3 | B    |             |          
+(4 rows)
+
+-- ROWS BETWEEN CURRENT ROW AND offset FOLLOWING
+SELECT company, tdate, price, first_value(tdate) OVER w, last_value(tdate) OVER w,
+ count(*) OVER w
+ FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND 2 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 | first_value | last_value | count 
+----------+------------+-------+-------------+------------+-------
+ company1 | 07-01-2023 |   100 | 07-01-2023  | 07-03-2023 |     3
+ company1 | 07-02-2023 |   200 |             |            |     0
+ company1 | 07-03-2023 |   150 |             |            |     0
+ company1 | 07-04-2023 |   140 | 07-04-2023  | 07-06-2023 |     3
+ company1 | 07-05-2023 |   150 |             |            |     0
+ company1 | 07-06-2023 |    90 |             |            |     0
+ company1 | 07-07-2023 |   110 | 07-07-2023  | 07-09-2023 |     3
+ company1 | 07-08-2023 |   130 |             |            |     0
+ company1 | 07-09-2023 |   120 |             |            |     0
+ company1 | 07-10-2023 |   130 |             |            |     0
+ company2 | 07-01-2023 |    50 | 07-01-2023  | 07-03-2023 |     3
+ company2 | 07-02-2023 |  2000 |             |            |     0
+ company2 | 07-03-2023 |  1500 |             |            |     0
+ company2 | 07-04-2023 |  1400 | 07-04-2023  | 07-06-2023 |     3
+ company2 | 07-05-2023 |  1500 |             |            |     0
+ company2 | 07-06-2023 |    60 |             |            |     0
+ company2 | 07-07-2023 |  1100 | 07-07-2023  | 07-09-2023 |     3
+ company2 | 07-08-2023 |  1300 |             |            |     0
+ company2 | 07-09-2023 |  1200 |             |            |     0
+ company2 | 07-10-2023 |  1300 |             |            |     0
+(20 rows)
+
+--
+-- Aggregates
+--
+-- using AFTER MATCH SKIP PAST LAST ROW
+SELECT company, tdate, price,
+ first_value(price) OVER w,
+ last_value(price) OVER w,
+ max(price) OVER w,
+ min(price) OVER w,
+ sum(price) OVER w,
+ avg(price) OVER w,
+ count(price) OVER w
+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)
+);
+ company  |   tdate    | price | first_value | last_value | max  | min | sum  |          avg          | count 
+----------+------------+-------+-------------+------------+------+-----+------+-----------------------+-------
+ company1 | 07-01-2023 |   100 |         100 |        140 |  200 | 100 |  590 |  147.5000000000000000 |     4
+ 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 |          90 |        120 |  130 |  90 |  450 |  112.5000000000000000 |     4
+ 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 | 2000 |  50 | 4950 | 1237.5000000000000000 |     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 | 1300 |  60 | 3660 |  915.0000000000000000 |     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)
+
+-- using AFTER MATCH SKIP TO NEXT ROW
+SELECT company, tdate, price,
+ first_value(price) OVER w,
+ last_value(price) OVER w,
+ max(price) OVER w,
+ min(price) OVER w,
+ sum(price) OVER w,
+ avg(price) OVER w,
+ count(price) OVER w
+FROM stock
+WINDOW w AS (
+PARTITION BY company
+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(price),
+DOWN AS price < PREV(price)
+);
+ company  |   tdate    | price | first_value | last_value | max  | min  | sum  |          avg          | count 
+----------+------------+-------+-------------+------------+------+------+------+-----------------------+-------
+ company1 | 07-01-2023 |   100 |         100 |        140 |  200 |  100 |  590 |  147.5000000000000000 |     4
+ company1 | 07-02-2023 |   200 |             |            |      |      |      |                       |     0
+ company1 | 07-03-2023 |   150 |             |            |      |      |      |                       |     0
+ company1 | 07-04-2023 |   140 |         140 |         90 |  150 |   90 |  380 |  126.6666666666666667 |     3
+ company1 | 07-05-2023 |   150 |             |            |      |      |      |                       |     0
+ company1 | 07-06-2023 |    90 |          90 |        120 |  130 |   90 |  450 |  112.5000000000000000 |     4
+ company1 | 07-07-2023 |   110 |         110 |        120 |  130 |  110 |  360 |  120.0000000000000000 |     3
+ 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 | 2000 |   50 | 4950 | 1237.5000000000000000 |     4
+ company2 | 07-02-2023 |  2000 |             |            |      |      |      |                       |     0
+ company2 | 07-03-2023 |  1500 |             |            |      |      |      |                       |     0
+ company2 | 07-04-2023 |  1400 |        1400 |         60 | 1500 |   60 | 2960 |  986.6666666666666667 |     3
+ company2 | 07-05-2023 |  1500 |             |            |      |      |      |                       |     0
+ company2 | 07-06-2023 |    60 |          60 |       1200 | 1300 |   60 | 3660 |  915.0000000000000000 |     4
+ company2 | 07-07-2023 |  1100 |        1100 |       1200 | 1300 | 1100 | 3600 | 1200.0000000000000000 |     3
+ company2 | 07-08-2023 |  1300 |             |            |      |      |      |                       |     0
+ company2 | 07-09-2023 |  1200 |             |            |      |      |      |                       |     0
+ 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);
+INSERT INTO t1 VALUES(1,10);
+INSERT INTO t1 VALUES(1,11);
+INSERT INTO t1 VALUES(1,12);
+INSERT INTO t2 VALUES(2,10);
+INSERT INTO t2 VALUES(2,11);
+INSERT INTO t2 VALUES(2,12);
+SELECT * FROM t1, t2 WHERE t1.v1 <= 11 AND t2.v2 <= 11;
+ i | v1 | j | v2 
+---+----+---+----
+ 1 | 10 | 2 | 10
+ 1 | 10 | 2 | 11
+ 1 | 11 | 2 | 10
+ 1 | 11 | 2 | 11
+(4 rows)
+
+SELECT *, count(*) OVER w FROM t1, t2
+WINDOW w AS (
+ PARTITION BY t1.i
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (A)
+ DEFINE
+ A AS v1 <= 11 AND v2 <= 11
+);
+ i | v1 | j | v2 | count 
+---+----+---+----+-------
+ 1 | 10 | 2 | 10 |     1
+ 1 | 10 | 2 | 11 |     1
+ 1 | 10 | 2 | 12 |     0
+ 1 | 11 | 2 | 10 |     1
+ 1 | 11 | 2 | 11 |     1
+ 1 | 11 | 2 | 12 |     0
+ 1 | 12 | 2 | 10 |     0
+ 1 | 12 | 2 | 11 |     0
+ 1 | 12 | 2 | 12 |     0
+(9 rows)
+
+-- WITH case
+WITH wstock AS (
+  SELECT * FROM stock WHERE tdate < '2023-07-08'
+)
+SELECT tdate, price,
+first_value(tdate) OVER w,
+count(*) OVER w
+ FROM wstock
+ 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)
+);
+   tdate    | price | first_value | count 
+------------+-------+-------------+-------
+ 07-01-2023 |   100 | 07-01-2023  |     4
+ 07-02-2023 |   200 |             |     0
+ 07-03-2023 |   150 |             |     0
+ 07-04-2023 |   140 |             |     0
+ 07-05-2023 |   150 |             |     0
+ 07-06-2023 |    90 |             |     0
+ 07-07-2023 |   110 |             |     0
+ 07-01-2023 |    50 | 07-01-2023  |     4
+ 07-02-2023 |  2000 |             |     0
+ 07-03-2023 |  1500 |             |     0
+ 07-04-2023 |  1400 |             |     0
+ 07-05-2023 |  1500 |             |     0
+ 07-06-2023 |    60 |             |     0
+ 07-07-2023 |  1100 |             |     0
+(14 rows)
+
+-- ReScan test: LATERAL join forces WindowAgg rescan with RPR
+-- Tests ExecReScanWindowAgg clearing nav_slot
+SELECT g.x, sub.*
+FROM generate_series(1, 2) g(x),
+LATERAL (
+  SELECT id, price, count(*) OVER w AS c
+  FROM (VALUES (1, 100), (2, 200), (3, 150)) AS t(id, price)
+  WHERE id <= g.x + 1
+  WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (START UP+)
+    DEFINE
+      START AS TRUE,
+      UP AS price > PREV(price)
+  )
+) sub
+ORDER BY g.x, sub.id;
+ x | id | price | c 
+---+----+-------+---
+ 1 |  1 |   100 | 2
+ 1 |  2 |   200 | 0
+ 2 |  1 |   100 | 2
+ 2 |  2 |   200 | 0
+ 2 |  3 |   150 | 0
+(5 rows)
+
+-- PREV has multiple column reference
+CREATE TEMP TABLE rpr1 (id INTEGER, i SERIAL, j INTEGER);
+INSERT INTO rpr1(id, j) SELECT 1, g*2 FROM generate_series(1, 10) AS g;
+SELECT id, i, j, count(*) OVER w
+ FROM rpr1
+ WINDOW w AS (
+ PARTITION BY id
+ ORDER BY i
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ INITIAL
+ PATTERN (START COND+)
+ DEFINE
+  START AS TRUE,
+  COND AS PREV(i + j + 1) < 10
+);
+ id | i  | j  | count 
+----+----+----+-------
+  1 |  1 |  2 |     3
+  1 |  2 |  4 |     0
+  1 |  3 |  6 |     0
+  1 |  4 |  8 |     0
+  1 |  5 | 10 |     0
+  1 |  6 | 12 |     0
+  1 |  7 | 14 |     0
+  1 |  8 | 16 |     0
+  1 |  9 | 18 |     0
+  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
+ FROM (SELECT generate_series(1, 5000) v)
+ WINDOW w AS (
+  ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+  AFTER MATCH SKIP PAST LAST ROW
+  INITIAL
+  PATTERN ( r+ )
+  DEFINE r AS TRUE
+ )
+)
+-- Should be exactly one long match across all rows.
+SELECT * FROM s WHERE c > 0;
+ v |  c   
+---+------
+ 1 | 5000
+(1 row)
+
+WITH s AS (
+ SELECT v, count(*) OVER w AS c
+ FROM (SELECT generate_series(1, 5000) v)
+ WINDOW w AS (
+  ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+  AFTER MATCH SKIP PAST LAST ROW
+  INITIAL
+  PATTERN ( r )
+  DEFINE r AS TRUE
+ )
+)
+-- Every row should be its own match.
+SELECT count(*) FROM s WHERE c > 0;
+ count 
+-------
+  5000
+(1 row)
+
+-- Large partition test: 100K rows with A+ B* C{10000,} pattern
+-- Tests that int32 count doesn't overflow with large repetitions
+WITH data AS (
+ SELECT generate_series(0, 100000) AS v
+),
+result AS (
+ SELECT v,
+        count(*) OVER w AS match_len,
+        first_value(v) OVER w AS match_first,
+        last_value(v) OVER w AS match_last
+ FROM data
+ WINDOW w AS (
+  ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+  AFTER MATCH SKIP PAST LAST ROW
+  INITIAL
+  PATTERN (A+ B* C{10000,})
+  DEFINE
+   A AS v < 33333,
+   B AS v >= 33333 AND v < 66666,
+   C AS v >= 66666 AND v < 99999
+ )
+)
+-- 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;
+ match_first | match_last | match_len 
+-------------+------------+-----------
+           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 = on;
+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;
+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;
+ matched_rows 
+--------------
+            1
+(1 row)
+
+RESET jit_above_cost;
+RESET jit;
+--
+-- 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,
+ nth_value(tdate, 2) IGNORE NULLS 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)
+);
+ 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)
+
+-- nth_value with IGNORE NULLS option wants to find the second row but
+-- 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)
+  ) AS t(gid, id))
+  SELECT gid, id, nth_value(id, 2) IGNORE NULLS OVER w AS second_val,
+  array_agg(id) OVER w
+  FROM data
+  WINDOW w AS (
+   ORDER BY gid
+   ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+   AFTER MATCH SKIP PAST LAST ROW
+   PATTERN (A+)
+   DEFINE A AS gid < 13
+  );
+ gid | id | second_val | array_agg  
+-----+----+------------+------------
+  10 |  1 |          3 | {1,NULL,3}
+  11 |    |            | 
+  12 |  3 |            | 
+  13 |  4 |            | 
+(4 rows)
+
+-- nth_value with IGNORE NULLS option wants to find the third row but
+-- 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)
+  ) AS t(gid, id))
+  SELECT gid, id, nth_value(id, 3) IGNORE NULLS OVER w AS thrid_val,
+  array_agg(id) OVER w
+  FROM data
+  WINDOW w AS (
+   ORDER BY gid
+   ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+   AFTER MATCH SKIP PAST LAST ROW
+   PATTERN (A+)
+   DEFINE A AS gid < 13
+  );
+ gid | id | thrid_val | array_agg  
+-----+----+-----------+------------
+  10 |  1 |           | {1,NULL,3}
+  11 |    |           | 
+  12 |  3 |           | 
+  13 |  4 |           | 
+(4 rows)
+
+-- nth_value beyond reduced frame with IGNORE NULLS
+-- Tests ignorenulls_getfuncarginframe early out-of-frame check
+SELECT company, tdate, price,
+ nth_value(price, 5) IGNORE NULLS OVER w AS nth_5_in
+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 | nth_5_in 
+----------+------------+-------+----------
+ company1 | 07-01-2023 |   100 |         
+ 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 |         
+ 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 |         
+ 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 |         
+ company2 | 07-07-2023 |  1100 |         
+ company2 | 07-08-2023 |  1300 |         
+ company2 | 07-09-2023 |  1200 |         
+ company2 | 07-10-2023 |  1300 |         
+(20 rows)
+
+-- 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
+ 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)
+
+-- 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
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+)
+ DEFINE A AS TRUE
+);
+ id | val | fv_ignull | lv_ignull | count 
+----+-----+-----------+-----------+-------
+  1 |     |           |           |     3
+  2 |     |           |           |     0
+  3 |     |           |           |     0
+(3 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)
+
+--
+-- 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)
+);
+ 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)
+
+-- 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
+ 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)
+
+-- 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
+ 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)
+
+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/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
new file mode 100644
index 00000000000..a63211ff364
--- /dev/null
+++ b/src/test/regress/expected/rpr_base.out
@@ -0,0 +1,6589 @@
+-- ============================================================
+-- RPR Base Tests
+-- Tests for Row Pattern Recognition (ISO/IEC 19075-5:2016)
+-- ============================================================
+--
+-- Parser Layer:
+--   Keyword Usage Tests
+--   DEFINE Clause Tests
+--   FRAME Options Tests
+--   PARTITION BY + FRAME Tests
+--   PATTERN Syntax Tests
+--   Quantifiers Tests
+--   Navigation Functions Tests
+--   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
+--   Absorption Flag Display Tests
+--   Absorption Analysis Tests
+--   Edge Case Tests
+--   Optimization Fallback Tests
+--   Planner Integration Tests
+--   Subquery and CTE Tests
+--   JOIN Tests
+--   Complex Expression Tests
+--   Set Operations Tests
+--   Sorting and Grouping Tests
+--   SQL Function Inlining Tests
+--   Stress Tests
+--   Error Limit Tests
+--
+-- Contributed Tests:
+--   Jacob's Patterns
+--   Pathological Patterns
+-- ============================================================
+SET client_min_messages = WARNING;
+-- ============================================================
+-- Keyword Usage Tests
+-- ============================================================
+-- RPR keywords as column names
+-- Keywords: define, initial, past, pattern, seek
+CREATE TABLE rpr_keywords (
+    id INT,
+    define INT,      -- DEFINE keyword
+    initial INT,     -- INITIAL keyword
+    past INT,        -- PAST keyword
+    pattern INT,     -- PATTERN keyword
+    seek INT,        -- SEEK keyword
+    skip INT         -- SKIP keyword (pre-existing)
+);
+INSERT INTO rpr_keywords VALUES (1, 10, 20, 30, 40, 50, 60);
+SELECT id, define, initial, past, pattern, seek, skip
+FROM rpr_keywords
+ORDER BY id;
+ id | define | initial | past | pattern | seek | skip 
+----+--------+---------+------+---------+------+------
+  1 |     10 |      20 |   30 |      40 |   50 |   60
+(1 row)
+
+DROP TABLE rpr_keywords;
+-- ============================================================
+-- DEFINE Clause Tests
+-- ============================================================
+-- Simple column references
+CREATE TABLE stock_price (
+    dt DATE,
+    symbol TEXT,
+    price NUMERIC,
+    volume INT
+);
+INSERT INTO stock_price VALUES
+    ('2024-01-01', 'AAPL', 150, 1000),
+    ('2024-01-02', 'AAPL', 155, 1200),
+    ('2024-01-03', 'AAPL', 152, 900),
+    ('2024-01-04', 'AAPL', 160, 1500),
+    ('2024-01-05', 'AAPL', 158, 1100);
+-- Simple column reference
+SELECT dt, price, COUNT(*) OVER w as cnt
+FROM stock_price
+WINDOW w AS (
+    PARTITION BY symbol
+    ORDER BY dt
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (UP+)
+    DEFINE UP AS price > 150
+)
+ORDER BY dt;
+     dt     | price | cnt 
+------------+-------+-----
+ 01-01-2024 |   150 |   0
+ 01-02-2024 |   155 |   4
+ 01-03-2024 |   152 |   0
+ 01-04-2024 |   160 |   0
+ 01-05-2024 |   158 |   0
+(5 rows)
+
+-- Multiple column references
+SELECT dt, price, volume, COUNT(*) OVER w as cnt
+FROM stock_price
+WINDOW w AS (
+    PARTITION BY symbol
+    ORDER BY dt
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (GOOD+)
+    DEFINE GOOD AS price > 150 AND volume > 1000
+)
+ORDER BY dt;
+     dt     | price | volume | cnt 
+------------+-------+--------+-----
+ 01-01-2024 |   150 |   1000 |   0
+ 01-02-2024 |   155 |   1200 |   1
+ 01-03-2024 |   152 |    900 |   0
+ 01-04-2024 |   160 |   1500 |   2
+ 01-05-2024 |   158 |   1100 |   0
+(5 rows)
+
+-- Expression in DEFINE
+SELECT dt, price, COUNT(*) OVER w as cnt
+FROM stock_price
+WINDOW w AS (
+    PARTITION BY symbol
+    ORDER BY dt
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (HIGH+)
+    DEFINE HIGH AS price * 1.1 > 165
+)
+ORDER BY dt;
+     dt     | price | cnt 
+------------+-------+-----
+ 01-01-2024 |   150 |   0
+ 01-02-2024 |   155 |   4
+ 01-03-2024 |   152 |   0
+ 01-04-2024 |   160 |   0
+ 01-05-2024 |   158 |   0
+(5 rows)
+
+-- Arithmetic and functions
+SELECT dt, price, volume, COUNT(*) OVER w as cnt
+FROM stock_price
+WINDOW w AS (
+    PARTITION BY symbol
+    ORDER BY dt
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (CALC+)
+    DEFINE CALC AS (price + volume / 100) > 160
+)
+ORDER BY dt;
+     dt     | price | volume | cnt 
+------------+-------+--------+-----
+ 01-01-2024 |   150 |   1000 |   0
+ 01-02-2024 |   155 |   1200 |   4
+ 01-03-2024 |   152 |    900 |   0
+ 01-04-2024 |   160 |   1500 |   0
+ 01-05-2024 |   158 |   1100 |   0
+(5 rows)
+
+DROP TABLE stock_price;
+-- Auto-generated DEFINE
+CREATE TABLE rpr_auto (id INT, val INT);
+INSERT INTO rpr_auto VALUES (1, 10), (2, 20), (3, 30), (4, 15);
+-- One variable undefined (B auto-generated as "B IS TRUE")
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_auto
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+ B*)
+    DEFINE A AS val > 15
+)
+ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   0
+  2 |  20 |   3
+  3 |  30 |   0
+  4 |  15 |   0
+(4 rows)
+
+-- Multiple undefined variables
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_auto
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B C)
+    DEFINE A AS val > 0
+    -- B and C auto-generated as "B IS TRUE", "C IS TRUE"
+)
+ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   3
+  2 |  20 |   0
+  3 |  30 |   0
+  4 |  15 |   0
+(4 rows)
+
+-- All variables defined explicitly
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_auto
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (X Y Z)
+    DEFINE
+        X AS val > 10,
+        Y AS val > 20,
+        Z AS val < 20
+)
+ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   0
+  2 |  20 |   3
+  3 |  30 |   0
+  4 |  15 |   0
+(4 rows)
+
+DROP TABLE rpr_auto;
+-- Duplicate variable names
+CREATE TABLE rpr_dup (id INT);
+INSERT INTO rpr_dup VALUES (1), (2);
+-- Duplicate DEFINE entries
+SELECT COUNT(*) OVER w
+FROM rpr_dup
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS id > 0, A AS id < 10
+);
+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
+SELECT COUNT(*) OVER w
+FROM rpr_bool
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS id
+);
+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
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (T+)
+    DEFINE T AS flag
+)
+ORDER BY id;
+ id | flag | cnt 
+----+------+-----
+  1 | t    |   1
+  2 | f    |   0
+(2 rows)
+
+-- NULL::boolean
+SELECT id, COUNT(*) OVER w as cnt
+FROM rpr_bool
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (N+)
+    DEFINE N AS NULL::boolean
+)
+ORDER BY id;
+ id | cnt 
+----+-----
+  1 |   0
+  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);
+INSERT INTO rpr_complex VALUES (1, 10, 20), (2, 15, 25), (3, 20, 30);
+-- CASE expression
+SELECT id, val1, val2, COUNT(*) OVER w as cnt
+FROM rpr_complex
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (C+)
+    DEFINE C AS CASE WHEN val1 > 10 THEN val2 > 20 ELSE false END
+)
+ORDER BY id;
+ id | val1 | val2 | cnt 
+----+------+------+-----
+  1 |   10 |   20 |   0
+  2 |   15 |   25 |   2
+  3 |   20 |   30 |   0
+(3 rows)
+
+DROP TABLE rpr_complex;
+-- Pattern variable not in PATTERN (should be ignored)
+CREATE TABLE rpr_unused (id INT);
+INSERT INTO rpr_unused VALUES (1), (2);
+-- Extra DEFINE variable
+SELECT id, COUNT(*) OVER w as cnt
+FROM rpr_unused
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS id > 0, B AS id > 5  -- B not in pattern
+)
+ORDER BY id;
+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
+-- ============================================================
+CREATE TABLE rpr_frame (id INT, val INT);
+INSERT INTO rpr_frame VALUES
+    (1, 10), (2, 10), (3, 10),  -- Same val: 10
+    (4, 20), (5, 20),           -- Same val: 20
+    (6, 30);
+-- Valid frame options
+-- ROWS: counts physical rows (1 FOLLOWING = next 1 physical row)
+-- Expected result: Each row can see 1 physical row ahead
+-- id=1,2,3 (val=10): can see next row -> cnt=2
+-- id=4,5 (val=20): can see next row -> cnt=2
+-- id=6 (val=30): no next row -> cnt=1
+-- Result: [2,2,2,2,2,1]
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_frame
+WINDOW w AS (
+    ORDER BY val
+    ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A B?)
+    DEFINE A AS val >= 0, B AS val >= 0
+)
+ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   2
+  2 |  10 |   2
+  3 |  10 |   2
+  4 |  20 |   2
+  5 |  20 |   2
+  6 |  30 |   1
+(6 rows)
+
+-- Invalid frame start positions
+-- Not starting at CURRENT ROW
+SELECT COUNT(*) OVER w
+FROM rpr_frame
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS val > 0
+);
+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.
+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
+FROM rpr_frame
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    EXCLUDE CURRENT ROW
+    PATTERN (A+)
+    DEFINE A AS val > 0
+);
+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: cannot use EXCLUDE options with row pattern recognition
+-- EXCLUDE GROUP not permitted
+SELECT COUNT(*) OVER w
+FROM rpr_frame
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    EXCLUDE GROUP
+    PATTERN (A+)
+    DEFINE A AS val > 0
+);
+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: cannot use EXCLUDE options with row pattern recognition
+-- EXCLUDE TIES not permitted
+SELECT COUNT(*) OVER w
+FROM rpr_frame
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    EXCLUDE TIES
+    PATTERN (A+)
+    DEFINE A AS val > 0
+);
+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: cannot use EXCLUDE options with row pattern recognition
+-- RANGE frame not starting at CURRENT ROW
+SELECT COUNT(*) OVER w
+FROM rpr_frame
+WINDOW w AS (
+    ORDER BY id
+    RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS val > 0
+);
+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
+SELECT COUNT(*) OVER w
+FROM rpr_frame
+WINDOW w AS (
+    ORDER BY id
+    GROUPS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS val > 0
+);
+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
+SELECT COUNT(*) OVER w
+FROM rpr_frame
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN 1 PRECEDING AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS val > 0
+);
+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.
+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
+SELECT COUNT(*) OVER w
+FROM rpr_frame
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN 1 FOLLOWING AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS val > 0
+);
+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.
+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
+SELECT COUNT(*) OVER w
+FROM rpr_frame
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND 1 PRECEDING
+    PATTERN (A+)
+    DEFINE A AS val > 0
+);
+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
+SELECT COUNT(*) OVER w
+FROM rpr_frame
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED PRECEDING
+    PATTERN (A+)
+    DEFINE A AS val > 0
+);
+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
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_frame
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND CURRENT ROW
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A)
+    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)
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_frame
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND 0 FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A)
+    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)
+
+-- Large offset: CURRENT ROW AND 1000 FOLLOWING
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_frame
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND 1000 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)
+
+-- Maximum offset: CURRENT ROW AND 2147483646 FOLLOWING (INT_MAX - 1)
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_frame
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND 2147483646 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
+WINDOW w AS (
+    ORDER BY val
+    RANGE BETWEEN CURRENT ROW AND 10 FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A B?)
+    DEFINE A AS val >= 0, B AS val >= 0
+)
+ORDER BY id;
+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
+WINDOW w AS (
+    ORDER BY val
+    GROUPS BETWEEN CURRENT ROW AND 1 FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A B?)
+    DEFINE A AS val >= 0, B AS val >= 0
+)
+ORDER BY id;
+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
+-- ============================================================
+-- Test PARTITION BY with RPR to ensure proper partitioning behavior
+CREATE TABLE rpr_partition (id INT, grp INT, val INT);
+INSERT INTO rpr_partition VALUES
+    (1, 1, 10), (2, 1, 20), (3, 1, 30),
+    (4, 2, 15), (5, 2, 25), (6, 2, 35);
+-- PARTITION BY with ROWS frame
+SELECT id, grp, val, COUNT(*) OVER w as cnt
+FROM rpr_partition
+WINDOW w AS (
+    PARTITION BY grp
+    ORDER BY val
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A B+)
+    DEFINE A AS val >= 10, B AS val > 15
+)
+ORDER BY id;
+ id | grp | val | cnt 
+----+-----+-----+-----
+  1 |   1 |  10 |   3
+  2 |   1 |  20 |   2
+  3 |   1 |  30 |   0
+  4 |   2 |  15 |   3
+  5 |   2 |  25 |   2
+  6 |   2 |  35 |   0
+(6 rows)
+
+-- Expected: Pattern matching should reset for each partition
+-- PARTITION BY with RANGE frame
+SELECT id, grp, val, COUNT(*) OVER w as cnt
+FROM rpr_partition
+WINDOW w AS (
+    PARTITION BY grp
+    ORDER BY val
+    RANGE BETWEEN CURRENT ROW AND 10 FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A B?)
+    DEFINE A AS val >= 10, B AS val >= 20
+)
+ORDER BY id;
+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
+-- ============================================================
+CREATE TABLE rpr_pattern (id INT, val INT);
+INSERT INTO rpr_pattern VALUES
+    (1, 5), (2, 10), (3, 15), (4, 20), (5, 25),
+    (6, 30), (7, 35), (8, 40), (9, 45), (10, 50);
+-- Alternation (|)
+-- Multiple alternatives
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_pattern
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+ | B+ | C+)
+    DEFINE A AS val > 35, B AS val BETWEEN 15 AND 35, C AS val < 15
+)
+ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |   5 |   2
+  2 |  10 |   0
+  3 |  15 |   5
+  4 |  20 |   0
+  5 |  25 |   0
+  6 |  30 |   0
+  7 |  35 |   0
+  8 |  40 |   3
+  9 |  45 |   0
+ 10 |  50 |   0
+(10 rows)
+
+-- Grouping
+-- Nested grouping with quantifier
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_pattern
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (((A B) C)+)
+    DEFINE A AS val > 10, B AS val > 20, C AS val > 30
+)
+ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |   5 |   0
+  2 |  10 |   0
+  3 |  15 |   0
+  4 |  20 |   0
+  5 |  25 |   6
+  6 |  30 |   0
+  7 |  35 |   0
+  8 |  40 |   0
+  9 |  45 |   0
+ 10 |  50 |   0
+(10 rows)
+
+-- Sequence
+-- Multi-element sequence
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_pattern
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B C D E)
+    DEFINE
+        A AS val < 15,
+        B AS val BETWEEN 15 AND 25,
+        C AS val BETWEEN 25 AND 35,
+        D AS val BETWEEN 35 AND 45,
+        E AS val >= 45
+)
+ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |   5 |   0
+  2 |  10 |   0
+  3 |  15 |   0
+  4 |  20 |   0
+  5 |  25 |   0
+  6 |  30 |   0
+  7 |  35 |   0
+  8 |  40 |   0
+  9 |  45 |   0
+ 10 |  50 |   0
+(10 rows)
+
+-- Complex combinations
+-- Alternation with grouping
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_pattern
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A B) | (C D))
+    DEFINE A AS val < 20, B AS val >= 20, C AS val < 30, D AS val >= 30
+)
+ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |   5 |   0
+  2 |  10 |   0
+  3 |  15 |   2
+  4 |  20 |   0
+  5 |  25 |   2
+  6 |  30 |   0
+  7 |  35 |   0
+  8 |  40 |   0
+  9 |  45 |   0
+ 10 |  50 |   0
+(10 rows)
+
+-- Alternation + sequence + grouping
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_pattern
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (START (UP{2,} DOWN? | FLAT+) FINISH)
+    DEFINE
+        START AS val >= 0,
+        UP AS val > 20,
+        DOWN AS val <= 30,
+        FLAT AS val BETWEEN 25 AND 35,
+        FINISH AS val > 40
+)
+ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |   5 |   0
+  2 |  10 |   0
+  3 |  15 |   0
+  4 |  20 |   7
+  5 |  25 |   0
+  6 |  30 |   0
+  7 |  35 |   0
+  8 |  40 |   0
+  9 |  45 |   0
+ 10 |  50 |   0
+(10 rows)
+
+-- Nested alternation in groups
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_pattern
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A | B) (C | D))
+    DEFINE A AS val < 15, B AS val BETWEEN 15 AND 25, C AS val BETWEEN 25 AND 35, D AS val > 35
+)
+ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |   5 |   0
+  2 |  10 |   0
+  3 |  15 |   0
+  4 |  20 |   2
+  5 |  25 |   0
+  6 |  30 |   0
+  7 |  35 |   0
+  8 |  40 |   0
+  9 |  45 |   0
+ 10 |  50 |   0
+(10 rows)
+
+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),
+    (6, 60), (7, 70), (8, 80), (9, 90), (10, 100);
+-- Basic greedy quantifiers
+-- * (zero or more)
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_quant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A*)
+    DEFINE A AS val > 0
+)
+ORDER BY id;
+ 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)
+
+-- + (one or more)
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_quant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS val > 50
+)
+ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   0
+  2 |  20 |   0
+  3 |  30 |   0
+  4 |  40 |   0
+  5 |  50 |   0
+  6 |  60 |   5
+  7 |  70 |   0
+  8 |  80 |   0
+  9 |  90 |   0
+ 10 | 100 |   0
+(10 rows)
+
+-- ? (zero or one)
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_quant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A?)
+    DEFINE A AS val = 50
+)
+ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   0
+  2 |  20 |   0
+  3 |  30 |   0
+  4 |  40 |   0
+  5 |  50 |   1
+  6 |  60 |   0
+  7 |  70 |   0
+  8 |  80 |   0
+  9 |  90 |   0
+ 10 | 100 |   0
+(10 rows)
+
+-- Edge case quantifiers
+-- {0} is not allowed (min must be >= 1)
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_quant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{0} B)
+    DEFINE A AS val > 1000, B AS val > 0
+)
+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
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{0,0} B)
+    DEFINE A AS val > 1000, B AS val > 0
+)
+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
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{0,1})
+    DEFINE A AS val = 50
+)
+ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   0
+  2 |  20 |   0
+  3 |  30 |   0
+  4 |  40 |   0
+  5 |  50 |   1
+  6 |  60 |   0
+  7 |  70 |   0
+  8 |  80 |   0
+  9 |  90 |   0
+ 10 | 100 |   0
+(10 rows)
+
+-- Exact quantifiers {n}
+-- {3} (representative exact quantifier)
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_quant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{3})
+    DEFINE A AS val > 0
+)
+ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   3
+  2 |  20 |   0
+  3 |  30 |   0
+  4 |  40 |   3
+  5 |  50 |   0
+  6 |  60 |   0
+  7 |  70 |   3
+  8 |  80 |   0
+  9 |  90 |   0
+ 10 | 100 |   0
+(10 rows)
+
+-- Range quantifiers {n,}
+-- {2,} (representative n or more)
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_quant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{2,})
+    DEFINE A AS val > 40
+)
+ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   0
+  2 |  20 |   0
+  3 |  30 |   0
+  4 |  40 |   0
+  5 |  50 |   6
+  6 |  60 |   0
+  7 |  70 |   0
+  8 |  80 |   0
+  9 |  90 |   0
+ 10 | 100 |   0
+(10 rows)
+
+-- Upper bound quantifiers {,m}
+-- {,3} (representative up to m)
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_quant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{,3})
+    DEFINE A AS val > 0
+)
+ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   3
+  2 |  20 |   0
+  3 |  30 |   0
+  4 |  40 |   3
+  5 |  50 |   0
+  6 |  60 |   0
+  7 |  70 |   3
+  8 |  80 |   0
+  9 |  90 |   0
+ 10 | 100 |   1
+(10 rows)
+
+-- Range quantifiers {n,m}
+-- {3,7} (representative range)
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_quant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{3,7})
+    DEFINE A AS val > 0
+)
+ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   7
+  2 |  20 |   0
+  3 |  30 |   0
+  4 |  40 |   0
+  5 |  50 |   0
+  6 |  60 |   0
+  7 |  70 |   0
+  8 |  80 |   3
+  9 |  90 |   0
+ 10 | 100 |   0
+(10 rows)
+
+DROP TABLE rpr_quant;
+-- Reluctant quantifiers
+CREATE TABLE rpr_reluctant (id INT, val INT);
+INSERT INTO rpr_reluctant VALUES (1, 10), (2, 20), (3, 30);
+-- *? (zero or more, reluctant)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A*?)
+    DEFINE A AS val > 0
+);
+ count 
+-------
+     0
+     0
+     0
+(3 rows)
+
+-- Reluctant quantifier: prefer shortest match
+-- +? (one or more, reluctant)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+?)
+    DEFINE A AS val > 0
+);
+ count 
+-------
+     1
+     1
+     1
+(3 rows)
+
+-- Reluctant quantifier: prefer shortest match
+-- ?? (zero or one, reluctant)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A??)
+    DEFINE A AS val > 0
+);
+ count 
+-------
+     0
+     0
+     0
+(3 rows)
+
+-- Reluctant quantifier: prefer shortest match
+-- {n,}? (n or more, reluctant)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{2,}?)
+    DEFINE A AS val > 0
+);
+ count 
+-------
+     2
+     0
+     0
+(3 rows)
+
+-- Reluctant quantifier: prefer shortest match
+-- {n,m}? (n to m, reluctant)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{1,3}?)
+    DEFINE A AS val > 0
+);
+ count 
+-------
+     1
+     1
+     1
+(3 rows)
+
+-- Reluctant quantifier: prefer shortest match
+-- {n}? (exactly n, reluctant)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{2}?)
+    DEFINE A AS val > 0
+);
+ count 
+-------
+     2
+     0
+     0
+(3 rows)
+
+-- Reluctant quantifier: prefer shortest match
+-- {,m}? (up to m, reluctant) - COMPLETELY UNTESTED RULE!
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{,3}?)
+    DEFINE A AS val > 0
+);
+ count 
+-------
+     0
+     0
+     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
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{2}+)
+    DEFINE A AS val > 0
+);
+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
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{2,}*)
+    DEFINE A AS val > 0
+);
+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
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{,3}*)
+    DEFINE A AS val > 0
+);
+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
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{1,3}+)
+    DEFINE A AS val > 0
+);
+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)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{-1}?)
+    DEFINE A AS val > 0
+);
+ERROR:  syntax error at or near "-"
+LINE 6:     PATTERN (A{-1}?)
+                       ^
+-- Expected: ERROR: syntax error at or near "-"
+-- {2147483647}? (INT_MAX)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{2147483647}?)
+    DEFINE A AS val > 0
+);
+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)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{-1,}?)
+    DEFINE A AS val > 0
+);
+ERROR:  syntax error at or near "-"
+LINE 6:     PATTERN (A{-1,}?)
+                       ^
+-- Expected: ERROR: syntax error at or near "-"
+-- {2147483647,}? (INT_MAX lower bound)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{2147483647,}?)
+    DEFINE A AS val > 0
+);
+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)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{,0}?)
+    DEFINE A AS val > 0
+);
+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)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{,2147483647}?)
+    DEFINE A AS val > 0
+);
+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)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{-1,3}?)
+    DEFINE A AS val > 0
+);
+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)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{1,2147483647}?)
+    DEFINE A AS val > 0
+);
+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)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{5,3}?)
+    DEFINE A AS val > 0
+);
+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)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A* ?)
+    DEFINE A AS val > 0
+);
+ count 
+-------
+     0
+     0
+     0
+(3 rows)
+
+-- Reluctant quantifier: prefer shortest match
+-- + ? (token separated)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+ ?)
+    DEFINE A AS val > 0
+);
+ count 
+-------
+     1
+     1
+     1
+(3 rows)
+
+-- Reluctant quantifier: prefer shortest match
+-- {2,} ? (token separated)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{2,} ?)
+    DEFINE A AS val > 0
+);
+ count 
+-------
+     2
+     0
+     0
+(3 rows)
+
+-- Reluctant quantifier: prefer shortest match
+-- Invalid token combinations
+-- * + (invalid combination)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+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 6:     PATTERN (A* +)
+                        ^
+-- Expected: ERROR: syntax error at or near "+"
+-- + * (invalid combination)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+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 6:     PATTERN (A+ *)
+                        ^
+-- Expected: ERROR: syntax error at or near "*"
+-- ? ? (parsed as ?? reluctant quantifier)
+SELECT COUNT(*) OVER w
+FROM rpr_reluctant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A? ?)
+    DEFINE A AS val > 0
+);
+ count 
+-------
+     0
+     0
+     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
+SELECT COUNT(*) OVER w
+FROM rpr_bounds
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{5,3})
+    DEFINE A AS id > 0
+);
+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
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{1000,2000})
+    DEFINE A AS id > 0
+);
+ count 
+-------
+     0
+     0
+(2 rows)
+
+-- Very large bound
+SELECT COUNT(*) OVER w
+FROM rpr_bounds
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{100000})
+    DEFINE A AS id > 0
+);
+ count 
+-------
+     0
+     0
+(2 rows)
+
+-- INT_MAX - 1 = 2147483646 (at limit)
+SELECT COUNT(*) OVER w
+FROM rpr_bounds
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{2147483646})
+    DEFINE A AS id > 0
+);
+ count 
+-------
+     0
+     0
+(2 rows)
+
+-- INT_MAX = 2147483647 (over limit)
+SELECT COUNT(*) OVER w
+FROM rpr_bounds
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{2147483647})
+    DEFINE A AS id > 0
+);
+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,}
+SELECT COUNT(*) OVER w
+FROM rpr_bounds
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{-1,})
+    DEFINE A AS id > 0
+);
+ERROR:  syntax error at or near "-"
+LINE 6:     PATTERN (A{-1,})
+                       ^
+-- Expected: ERROR: syntax error at or near "-"
+-- INT_MAX in {n,}
+SELECT COUNT(*) OVER w
+FROM rpr_bounds
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{2147483647,})
+    DEFINE A AS id > 0
+);
+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
+FROM rpr_bounds
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{,0})
+    DEFINE A AS id > 0
+);
+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}
+SELECT COUNT(*) OVER w
+FROM rpr_bounds
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{,2147483647})
+    DEFINE A AS id > 0
+);
+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)
+-- ============================================================
+CREATE TABLE rpr_nav (id INT, val INT);
+INSERT INTO rpr_nav VALUES
+    (1, 10), (2, 20), (3, 15), (4, 25), (5, 30);
+-- PREV function - reference previous row in pattern
+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 > 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)
+
+-- NEXT function - reference next row in pattern
+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 < NEXT(val),
+        B AS val > 0
+)
+ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   2
+  2 |  20 |   0
+  3 |  15 |   3
+  4 |  25 |   0
+  5 |  30 |   0
+(5 rows)
+
+-- Combined PREV and NEXT
+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 C)
+    DEFINE
+        A AS val > 0,
+        B AS val > PREV(val) AND val < NEXT(val),
+        C AS val > PREV(val)
+)
+ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   0
+  2 |  20 |   0
+  3 |  15 |   3
+  4 |  25 |   0
+  5 |  30 |   0
+(5 rows)
+
+-- PREV function cannot be used other than in DEFINE
+SELECT PREV(id), 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 > PREV(val)
+)
+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
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE
+        A AS val > 0,
+        B AS val > PREV(val)
+)
+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
+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:  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
+-- ============================================================
+CREATE TABLE rpr_skip (id INT, val INT);
+INSERT INTO rpr_skip VALUES
+    (1, 1), (2, 2), (3, 3), (4, 4), (5, 5),
+    (6, 6), (7, 7), (8, 8);
+-- SKIP TO NEXT ROW
+-- SKIP TO NEXT ROW
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_skip
+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 > 0, B AS val > 2, C AS val > 4
+)
+ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |   1 |   0
+  2 |   2 |   0
+  3 |   3 |   3
+  4 |   4 |   3
+  5 |   5 |   3
+  6 |   6 |   3
+  7 |   7 |   0
+  8 |   8 |   0
+(8 rows)
+
+-- SKIP PAST LAST ROW
+-- SKIP PAST LAST ROW
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_skip
+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 val > 0, B AS val > 2, C AS val > 4
+)
+ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |   1 |   0
+  2 |   2 |   0
+  3 |   3 |   3
+  4 |   4 |   0
+  5 |   5 |   0
+  6 |   6 |   3
+  7 |   7 |   0
+  8 |   8 |   0
+(8 rows)
+
+-- Default behavior (should be SKIP PAST LAST ROW)
+-- No SKIP TO clause (default)
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_skip
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B)
+    DEFINE A AS val > 0, B AS val > 1
+)
+ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |   1 |   2
+  2 |   2 |   0
+  3 |   3 |   2
+  4 |   4 |   0
+  5 |   5 |   2
+  6 |   6 |   0
+  7 |   7 |   2
+  8 |   8 |   0
+(8 rows)
+
+-- Compare default with explicit PAST LAST ROW
+-- Results should be identical
+WITH default_skip AS (
+    SELECT id, val, COUNT(*) OVER w as cnt
+    FROM rpr_skip
+    WINDOW w AS (
+        ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B C)
+        DEFINE A AS val > 0, B AS val > 2, C AS val > 4
+    )
+),
+explicit_skip AS (
+    SELECT id, val, COUNT(*) OVER w as cnt
+    FROM rpr_skip
+    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 val > 0, B AS val > 2, C AS val > 4
+    )
+)
+SELECT 'default' as type, * FROM default_skip
+UNION ALL
+SELECT 'explicit' as type, * FROM explicit_skip
+ORDER BY type, id;
+   type   | id | val | cnt 
+----------+----+-----+-----
+ default  |  1 |   1 |   0
+ default  |  2 |   2 |   0
+ default  |  3 |   3 |   3
+ default  |  4 |   4 |   0
+ default  |  5 |   5 |   0
+ default  |  6 |   6 |   3
+ default  |  7 |   7 |   0
+ default  |  8 |   8 |   0
+ explicit |  1 |   1 |   0
+ explicit |  2 |   2 |   0
+ explicit |  3 |   3 |   3
+ explicit |  4 |   4 |   0
+ explicit |  5 |   5 |   0
+ explicit |  6 |   6 |   3
+ explicit |  7 |   7 |   0
+ explicit |  8 |   8 |   0
+(16 rows)
+
+DROP TABLE rpr_skip;
+-- INITIAL clause
+CREATE TABLE rpr_init (id INT, val INT);
+INSERT INTO rpr_init VALUES (1, 10), (2, 20), (3, 30), (4, 40);
+-- Explicit INITIAL
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_init
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A+)
+    DEFINE A AS val > 0
+)
+ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   4
+  2 |  20 |   0
+  3 |  30 |   0
+  4 |  40 |   0
+(4 rows)
+
+-- Implicit INITIAL (default)
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_init
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS val > 0
+)
+ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   4
+  2 |  20 |   0
+  3 |  30 |   0
+  4 |  40 |   0
+(4 rows)
+
+DROP TABLE rpr_init;
+-- SEEK
+CREATE TABLE rpr_seek (id INT, val INT);
+INSERT INTO rpr_seek VALUES (1, 10);
+-- SEEK keyword
+SELECT COUNT(*) OVER w
+FROM rpr_seek
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    SEEK
+    PATTERN (A+)
+    DEFINE A AS val > 0
+);
+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
+-- ============================================================
+-- View creation and deparsing
+CREATE TABLE rpr_serial (id INT, val INT);
+INSERT INTO rpr_serial VALUES
+    (1, 10), (2, 20), (3, 15), (4, 25), (5, 30);
+-- Simple pattern
+CREATE VIEW rpr_serial_v1 AS
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_serial
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS val > 0
+);
+-- Verify view works (tests deserialization)
+SELECT * FROM rpr_serial_v1 ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   5
+  2 |  20 |   0
+  3 |  15 |   0
+  4 |  25 |   0
+  5 |  30 |   0
+(5 rows)
+
+-- Verify deparsing
+SELECT pg_get_viewdef('rpr_serial_v1'::regclass);
+                                pg_get_viewdef                                
+------------------------------------------------------------------------------
+  SELECT id,                                                                 +
+     val,                                                                    +
+     count(*) OVER w AS cnt                                                  +
+    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+)                                                              +
+   DEFINE                                                                    +
+   a AS (val > 0) );
+(1 row)
+
+-- Complex pattern with alternation
+CREATE VIEW rpr_serial_v2 AS
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_serial
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+ | B*)
+    DEFINE A AS val > 20, B AS val <= 20
+);
+SELECT * FROM rpr_serial_v2 ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   3
+  2 |  20 |   0
+  3 |  15 |   0
+  4 |  25 |   2
+  5 |  30 |   0
+(5 rows)
+
+SELECT pg_get_viewdef('rpr_serial_v2'::regclass);
+                                pg_get_viewdef                                
+------------------------------------------------------------------------------
+  SELECT id,                                                                 +
+     val,                                                                    +
+     count(*) OVER w AS cnt                                                  +
+    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 (val > 20),                                                          +
+   b AS (val <= 20) );
+(1 row)
+
+-- Pattern with grouping and quantifiers
+CREATE VIEW rpr_serial_v3 AS
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_serial
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A B){2,5} | C*)
+    DEFINE
+        A AS val > 10,
+        B AS val > 20,
+        C AS val <= 10
+);
+SELECT * FROM rpr_serial_v3 ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   1
+  2 |  20 |   0
+  3 |  15 |   0
+  4 |  25 |   0
+  5 |  30 |   0
+(5 rows)
+
+SELECT pg_get_viewdef('rpr_serial_v3'::regclass);
+                                pg_get_viewdef                                
+------------------------------------------------------------------------------
+  SELECT id,                                                                 +
+     val,                                                                    +
+     count(*) OVER w AS cnt                                                  +
+    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){2,5} | c*)                                                 +
+   DEFINE                                                                    +
+   a AS (val > 10),                                                          +
+   b AS (val > 20),                                                          +
+   c AS (val <= 10) );
+(1 row)
+
+-- All features combined
+CREATE VIEW rpr_serial_v4 AS
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_serial
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    INITIAL
+    PATTERN (START (MID{1,3} | ALT+) FINISH)
+    DEFINE
+        START AS val > 5,
+        MID AS val BETWEEN 10 AND 25,
+        ALT AS val > 25,
+        FINISH AS val > 15
+);
+SELECT * FROM rpr_serial_v4 ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   5
+  2 |  20 |   4
+  3 |  15 |   3
+  4 |  25 |   0
+  5 |  30 |   0
+(5 rows)
+
+SELECT pg_get_viewdef('rpr_serial_v4'::regclass);
+                                pg_get_viewdef                                
+------------------------------------------------------------------------------
+  SELECT id,                                                                 +
+     val,                                                                    +
+     count(*) OVER w AS cnt                                                  +
+    FROM rpr_serial                                                          +
+   WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
+   AFTER MATCH SKIP TO NEXT ROW                                              +
+   INITIAL                                                                   +
+   PATTERN (start (mid{1,3} | alt+) finish)                                  +
+   DEFINE                                                                    +
+   start AS (val > 5),                                                       +
+   mid AS ((val >= 10) AND (val <= 25)),                                     +
+   alt AS (val > 25),                                                        +
+   finish AS (val > 15) );
+(1 row)
+
+-- Additional quantifiers for deparsing coverage
+-- ? quantifier (zero or one)
+CREATE VIEW rpr_serial_v5 AS
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_serial
+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
+);
+SELECT * FROM rpr_serial_v5 ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   0
+  2 |  20 |   1
+  3 |  15 |   2
+  4 |  25 |   0
+  5 |  30 |   1
+(5 rows)
+
+SELECT pg_get_viewdef('rpr_serial_v5'::regclass);
+                                pg_get_viewdef                                
+------------------------------------------------------------------------------
+  SELECT id,                                                                 +
+     val,                                                                    +
+     count(*) OVER w AS cnt                                                  +
+    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 (val > 10),                                                          +
+   b AS (val > 20) );
+(1 row)
+
+-- {n,} quantifier (n or more)
+CREATE VIEW rpr_serial_v6 AS
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_serial
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{2,})
+    DEFINE A AS val > 15
+);
+SELECT * FROM rpr_serial_v6 ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   0
+  2 |  20 |   0
+  3 |  15 |   0
+  4 |  25 |   2
+  5 |  30 |   0
+(5 rows)
+
+SELECT pg_get_viewdef('rpr_serial_v6'::regclass);
+                                pg_get_viewdef                                
+------------------------------------------------------------------------------
+  SELECT id,                                                                 +
+     val,                                                                    +
+     count(*) OVER w AS cnt                                                  +
+    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 > 15) );
+(1 row)
+
+-- {n} quantifier (exactly n)
+CREATE VIEW rpr_serial_v7 AS
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_serial
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{3})
+    DEFINE A AS val > 0
+);
+SELECT * FROM rpr_serial_v7 ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   3
+  2 |  20 |   0
+  3 |  15 |   0
+  4 |  25 |   0
+  5 |  30 |   0
+(5 rows)
+
+SELECT pg_get_viewdef('rpr_serial_v7'::regclass);
+                                pg_get_viewdef                                
+------------------------------------------------------------------------------
+  SELECT id,                                                                 +
+     val,                                                                    +
+     count(*) OVER w AS cnt                                                  +
+    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)
+
+-- Nested ALT pattern (tests deparse of complex nested structure)
+CREATE VIEW rpr_serial_v8 AS
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_serial
+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 <= 15, B AS val <= 25, C AS val <= 30, D AS val > 30
+);
+SELECT * FROM rpr_serial_v8 ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   3
+  2 |  20 |   0
+  3 |  15 |   0
+  4 |  25 |   0
+  5 |  30 |   0
+(5 rows)
+
+SELECT pg_get_viewdef('rpr_serial_v8'::regclass);
+                                pg_get_viewdef                                
+------------------------------------------------------------------------------
+  SELECT id,                                                                 +
+     val,                                                                    +
+     count(*) OVER w AS cnt                                                  +
+    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) | c) d | a b c)                                          +
+   DEFINE                                                                    +
+   a AS (val <= 15),                                                         +
+   b AS (val <= 25),                                                         +
+   c AS (val <= 30),                                                         +
+   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
+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)
+
+-- 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);
+CREATE MATERIALIZED VIEW rpr_mview_v1 AS
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_mview
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS val > 0
+);
+SELECT * FROM rpr_mview_v1 ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   3
+  2 |  20 |   0
+  3 |  30 |   0
+(3 rows)
+
+SELECT pg_get_viewdef('rpr_mview_v1'::regclass);
+                                pg_get_viewdef                                
+------------------------------------------------------------------------------
+  SELECT id,                                                                 +
+     val,                                                                    +
+     count(*) OVER w AS cnt                                                  +
+    FROM rpr_mview                                                           +
+   WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
+   AFTER MATCH SKIP PAST LAST ROW                                            +
+   INITIAL                                                                   +
+   PATTERN (a+)                                                              +
+   DEFINE                                                                    +
+   a AS (val > 0) );
+(1 row)
+
+-- Refresh test
+REFRESH MATERIALIZED VIEW rpr_mview_v1;
+SELECT * FROM rpr_mview_v1 ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   3
+  2 |  20 |   0
+  3 |  30 |   0
+(3 rows)
+
+-- 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);
+-- Simple prepared statement
+PREPARE rpr_prep_simple AS
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_prep
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS val > 0
+)
+ORDER BY id;
+EXECUTE rpr_prep_simple;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   3
+  2 |  20 |   0
+  3 |  30 |   0
+(3 rows)
+
+EXECUTE rpr_prep_simple;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   3
+  2 |  20 |   0
+  3 |  30 |   0
+(3 rows)
+
+DEALLOCATE rpr_prep_simple;
+-- Prepared statement with parameters
+PREPARE rpr_prep_param(int) AS
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_prep
+WHERE id <= $1
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS val > 10
+)
+ORDER BY id;
+EXECUTE rpr_prep_param(2);
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   0
+  2 |  20 |   1
+(2 rows)
+
+EXECUTE rpr_prep_param(3);
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   0
+  2 |  20 |   2
+  3 |  30 |   0
+(3 rows)
+
+DEALLOCATE rpr_prep_param;
+-- Complex prepared statement
+PREPARE rpr_prep_complex AS
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_prep
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A B){1,2} | C+)
+    DEFINE
+        A AS val > 5,
+        B AS val > 15,
+        C AS val <= 15
+)
+ORDER BY id;
+EXECUTE rpr_prep_complex;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   2
+  2 |  20 |   2
+  3 |  30 |   0
+(3 rows)
+
+EXECUTE rpr_prep_complex;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   2
+  2 |  20 |   2
+  3 |  30 |   0
+(3 rows)
+
+DEALLOCATE rpr_prep_complex;
+DROP TABLE rpr_prep;
+-- CTE and Subquery (tests copyfuncs.c)
+CREATE TABLE rpr_copy (id INT, val INT);
+INSERT INTO rpr_copy VALUES (1, 10), (2, 20), (3, 30), (4, 40);
+-- Simple CTE
+WITH rpr_cte AS (
+    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+)
+        DEFINE A AS val > 0
+    )
+)
+SELECT * FROM rpr_cte ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   4
+  2 |  20 |   0
+  3 |  30 |   0
+  4 |  40 |   0
+(4 rows)
+
+-- CTE with multiple references (forces node copy)
+WITH rpr_cte AS (
+    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+)
+        DEFINE A AS val > 15
+    )
+)
+SELECT c1.id, c1.cnt as cnt1, c2.cnt as cnt2
+FROM rpr_cte c1
+JOIN rpr_cte c2 ON c1.id = c2.id
+ORDER BY c1.id;
+ id | cnt1 | cnt2 
+----+------+------
+  1 |    0 |    0
+  2 |    3 |    3
+  3 |    0 |    0
+  4 |    0 |    0
+(4 rows)
+
+-- Subquery in FROM clause
+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;
+ id | val | cnt 
+----+-----+-----
+  2 |  20 |   2
+  4 |  40 |   1
+(2 rows)
+
+-- Nested subqueries
+SELECT *
+FROM (
+    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+)
+            DEFINE A AS val >= 10
+        )
+    ) inner_sub
+    WHERE cnt > 0
+) outer_sub
+ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   4
+(1 row)
+
+DROP TABLE rpr_copy;
+-- DISTINCT and set operations (tests equalfuncs.c)
+CREATE TABLE rpr_equal (id INT, val INT);
+INSERT INTO rpr_equal VALUES (1, 10), (2, 20), (3, 10), (4, 20);
+-- DISTINCT with RPR
+SELECT DISTINCT cnt
+FROM (
+    SELECT id, val, COUNT(*) OVER w as cnt
+    FROM rpr_equal
+    WINDOW w AS (
+        ORDER BY val
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        AFTER MATCH SKIP TO NEXT ROW
+        PATTERN (A+)
+        DEFINE A AS val > 0
+    )
+) sub
+ORDER BY cnt;
+ cnt 
+-----
+   1
+   2
+   3
+   4
+(4 rows)
+
+-- UNION with RPR in both sides
+SELECT id, val, cnt FROM (
+    SELECT id, val, COUNT(*) OVER w as cnt
+    FROM rpr_equal
+    WHERE val = 10
+    WINDOW w AS (
+        ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A+)
+        DEFINE A AS val > 0
+    )
+) sub1
+UNION
+SELECT id, val, cnt FROM (
+    SELECT id, val, COUNT(*) OVER w as cnt
+    FROM rpr_equal
+    WHERE val = 20
+    WINDOW w AS (
+        ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A+)
+        DEFINE A AS val > 0
+    )
+) sub2
+ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   2
+  2 |  20 |   2
+  3 |  10 |   0
+  4 |  20 |   0
+(4 rows)
+
+-- UNION ALL
+SELECT id, cnt FROM (
+    SELECT id, COUNT(*) OVER w as cnt
+    FROM rpr_equal
+    WINDOW w AS (
+        ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A+)
+        DEFINE A AS val > 10
+    )
+) sub
+UNION ALL
+SELECT id, cnt FROM (
+    SELECT id, COUNT(*) OVER w as cnt
+    FROM rpr_equal
+    WINDOW w AS (
+        ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (B+)
+        DEFINE B AS val <= 10
+    )
+) sub
+ORDER BY id, cnt;
+ id | cnt 
+----+-----
+  1 |   0
+  1 |   1
+  2 |   0
+  2 |   1
+  3 |   0
+  3 |   1
+  4 |   0
+  4 |   1
+(8 rows)
+
+-- INTERSECT
+SELECT id, cnt FROM (
+    SELECT id, COUNT(*) OVER w as cnt
+    FROM rpr_equal
+    WHERE id <= 3
+    WINDOW w AS (
+        ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A+)
+        DEFINE A AS val > 0
+    )
+) sub1
+INTERSECT
+SELECT id, cnt FROM (
+    SELECT id, COUNT(*) OVER w as cnt
+    FROM rpr_equal
+    WHERE id >= 2
+    WINDOW w AS (
+        ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A+)
+        DEFINE A AS val > 0
+    )
+) sub2
+ORDER BY id;
+ id | cnt 
+----+-----
+  3 |   0
+(1 row)
+
+DROP TABLE rpr_equal;
+-- View with multiple window definitions
+CREATE TABLE rpr_multiwin (id INT, val INT);
+INSERT INTO rpr_multiwin VALUES (1, 10), (2, 20), (3, 30);
+CREATE VIEW rpr_multiwin_v AS
+SELECT
+    id,
+    val,
+    COUNT(*) OVER w1 as cnt1,
+    COUNT(*) OVER w2 as cnt2
+FROM rpr_multiwin
+WINDOW
+    w1 AS (
+        ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A+)
+        DEFINE A AS val > 15
+    ),
+    w2 AS (
+        ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (B*)
+        DEFINE B AS val <= 15
+    );
+SELECT * FROM rpr_multiwin_v ORDER BY id;
+ id | val | cnt1 | cnt2 
+----+-----+------+------
+  1 |  10 |    0 |    1
+  2 |  20 |    2 |    0
+  3 |  30 |    0 |    0
+(3 rows)
+
+SELECT pg_get_viewdef('rpr_multiwin_v'::regclass);
+                                      pg_get_viewdef                                       
+-------------------------------------------------------------------------------------------
+  SELECT id,                                                                              +
+     val,                                                                                 +
+     count(*) OVER w1 AS cnt1,                                                            +
+     count(*) OVER w2 AS cnt2                                                             +
+    FROM rpr_multiwin                                                                     +
+   WINDOW w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING             +
+   AFTER MATCH SKIP PAST LAST ROW                                                         +
+   INITIAL                                                                                +
+   PATTERN (a+)                                                                           +
+   DEFINE                                                                                 +
+   a AS (val > 15) ), w2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
+   AFTER MATCH SKIP PAST LAST ROW                                                         +
+   INITIAL                                                                                +
+   PATTERN (b*)                                                                           +
+   DEFINE                                                                                 +
+   b AS (val <= 15) );
+(1 row)
+
+-- {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)
+
+-- {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)
+
+-- ============================================================
+-- 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
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+!)
+    DEFINE A AS val > 0
+);
+ERROR:  unsupported quantifier "+!"
+LINE 6:     PATTERN (A+!)
+                      ^
+HINT:  Valid quantifiers are: *, +, ?, *?, +?, ??, {n}, {n,}, {,m}, {n,m} and their reluctant versions.
+-- Expected: Syntax error
+-- Unmatched parentheses
+SET client_min_messages = NOTICE;
+DO $$
+BEGIN
+    EXECUTE '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 val > 0, B AS val > 10)';
+    RAISE NOTICE 'Unmatched parentheses: UNEXPECTED SUCCESS';
+EXCEPTION
+    WHEN syntax_error THEN
+        RAISE NOTICE 'Unmatched parentheses: EXPECTED ERROR - %', SQLERRM;
+    WHEN OTHERS THEN
+        RAISE NOTICE 'Unmatched parentheses: UNEXPECTED ERROR - %', SQLERRM;
+END $$;
+NOTICE:  Unmatched parentheses: EXPECTED ERROR - syntax error at or near "AS"
+SET client_min_messages = WARNING;
+-- Empty DEFINE
+SELECT COUNT(*) OVER w
+FROM rpr_err
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE
+);
+ERROR:  syntax error at or near ")"
+LINE 8: );
+        ^
+-- Expected: Syntax error
+-- Empty PATTERN
+SELECT COUNT(*) OVER w
+FROM rpr_err
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ()
+    DEFINE A AS val > 0
+);
+ERROR:  syntax error at or near ")"
+LINE 6:     PATTERN ()
+                     ^
+-- Expected: Syntax error
+-- DEFINE without PATTERN (PATTERN and DEFINE must be used together)
+SELECT COUNT(*) OVER w
+FROM rpr_err
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    DEFINE A AS val > 0
+);
+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 SQL standard 4.16, not yet implemented)
+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 A.val > 0
+);
+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
+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
+                        ^
+-- 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
+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:  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 SQL standard 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
+                        ^
+-- Expected: ERROR: range variable qualified column reference "rpr_err.val" is not allowed
+-- Semantic errors
+-- Undefined column in DEFINE
+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 nonexistent_column > 0
+);
+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
+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 > 'string'
+);
+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)
+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 COUNT(*) > 0
+);
+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)
+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 > (SELECT max(val) FROM rpr_err)
+);
+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
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS val > 0, B AS val > 5, C AS val > 10
+)
+ORDER BY id;
+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);
+INSERT INTO rpr_null VALUES (1, 10), (2, NULL), (3, 30);
+-- NULL in DEFINE expression
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_null
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS val > 15
+)
+ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   0
+  2 |     |   0
+  3 |  30 |   1
+(3 rows)
+
+-- IS NULL in DEFINE
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_null
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (N+)
+    DEFINE N AS val IS NULL
+)
+ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   0
+  2 |     |   1
+  3 |  30 |   0
+(3 rows)
+
+-- IS NOT NULL in DEFINE
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_null
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (NN+)
+    DEFINE NN AS val IS NOT NULL
+)
+ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   1
+  2 |     |   0
+  3 |  30 |   1
+(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:  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.
+-- ============================================================
+-- 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
+-- ============================================================
+-- Tests for pattern optimization in optimizer/plan/rpr.c
+-- Use EXPLAIN to verify optimized pattern (shown as "Pattern: ...")
+CREATE TABLE rpr_plan (id INT, val INT);
+INSERT INTO rpr_plan VALUES
+    (1, 10), (2, 20), (3, 30), (4, 40), (5, 50),
+    (6, 60), (7, 70), (8, 80), (9, 90), (10, 100);
+-- Consecutive VAR merge: A A A -> 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 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{3}
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 rows)
+
+-- Consecutive VAR merge: A{2} A{3} -> a{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{2} 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{5}
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 rows)
+
+-- Consecutive VAR merge: 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+"
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(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
+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{2,}"
+   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
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN (((A B){5}) ((A B){10})) 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){15}
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 rows)
+
+-- Consecutive GROUP merge with unbounded: (A B)+ (A B)+ -> (a b){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 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'){2,}"
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(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
+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} (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'){3,}"
+   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
+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'){2,}"
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 rows)
+
+-- PREFIX and SUFFIX merge: A B (A B)+ A B -> (a b){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 B (A B)+ A B) DEFINE A AS val <= 40, B AS val > 40);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ 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
+(7 rows)
+
+-- Flatten nested: A ((B) (C)) -> 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))) DEFINE A AS val <= 30, B AS val <= 60, C AS val > 60);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ 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
+(7 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
+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);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ 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
+(7 rows)
+
+-- ALT deduplicate: (A | B | A) -> (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)+) 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)+
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 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
+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)
+
+-- Quantifier multiply with child range: (A{2,3}){3} -> a{6,9}
+-- outer exact, child range - optimization applies
+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}){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,9}
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(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
+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}){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}){2,3}
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(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
+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}){2,}) DEFINE A AS val > 0);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ 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
+(7 rows)
+
+-- Quantifier multiply: (A){2,} -> a{2,}
+-- child exact 1 - no gaps, optimization applies
+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,}) 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)
+
+-- Quantifier multiply: (A)+ -> a+
+-- child exact 1 - no gaps, optimization applies
+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)+) DEFINE A AS val > 0);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ 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
+(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)
+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,5}) 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,5}
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  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)
+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);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ 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
+(7 rows)
+
+-- Nested unbounded: (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*)*) DEFINE A AS val > 0);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ 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
+(7 rows)
+
+-- Nested unbounded: (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+)*) DEFINE A AS val > 0);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ 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
+(7 rows)
+
+-- Nested unbounded: (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+)+) DEFINE A AS val > 0);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ 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
+(7 rows)
+
+-- Unwrap GROUP{1,1}: (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)) DEFINE A AS val > 0);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ 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
+(7 rows)
+
+-- Unwrap GROUP{1,1}: (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)) 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
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 rows)
+
+-- Combined optimization: A A (B B)+ B B C C C -> a{2} (b{2}){2,} c{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 A (B B)+ B B C C C)
+             DEFINE A AS val <= 20, B AS val > 20 AND val <= 70, C AS val > 70);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ 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
+(7 rows)
+
+-- Consecutive GROUP merge with unbounded: (A+) (A+) -> a{2,}
+-- Tests mergeConsecutiveGroups with child->max == INF
+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{2,}"
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 rows)
+
+-- Consecutive GROUP merge finite: (A{10}){20} -> a{200}
+-- Tests mergeConsecutiveGroups with both finite
+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{10}){20}) DEFINE A AS val > 0);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ 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
+(7 rows)
+
+-- Different GROUP prevents merge: (A B){2} (C D){3}
+-- Tests mergeConsecutiveGroups flush previous
+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} (C D){3})
+             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){2} (c d){3}
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 rows)
+
+-- Different children count prevents merge: (A B)+ (A B C)+
+-- Tests rprPatternChildrenEqual length check
+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 C)+)
+             DEFINE A AS val <= 33, B AS val > 33 AND val <= 66, C AS val > 66);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ 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
+(7 rows)
+
+-- PREFIX only merge: A B (A B)+ -> (a b){2,}
+-- Tests mergeGroupPrefixSuffix: absorb preceding elements into GROUP min
+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'){2,}"
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 rows)
+
+-- SUFFIX only merge: (A B)+ A B -> (a b){2,}
+-- Tests mergeGroupPrefixSuffix: absorb following elements into GROUP min
+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'){2,}"
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 rows)
+
+-- Multiple SUFFIX absorption with skipUntil: (A B)+ A B A B C
+-- Tests mergeGroupPrefixSuffix: skip absorbed suffix elements
+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 C)
+             DEFINE A AS val <= 50, B AS val > 50 AND val <= 75, C AS val > 75);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ 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
+(7 rows)
+
+-- PREFIX merge with remaining prefix: A B C D (C D)+
+-- Tests mergeGroupPrefixSuffix: trimmed list reconstruction
+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 (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){2,}
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 rows)
+
+-- PREFIX merge with quantifiers: A B* (A B*)+ -> (a b*){2,}
+-- Tests mergeGroupPrefixSuffix: quantifier comparison in rprPatternEqual
+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*){2,}
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 rows)
+
+-- PREFIX merge with multiple quantifiers: A+ B* C? (A+ B* C?)+ -> (a+ b* c?){2,}
+-- Tests mergeGroupPrefixSuffix: complex quantifier patterns
+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? (A+ B* C?)+)
+             DEFINE A AS val <= 30, B AS val > 30 AND val <= 60, C AS val > 60);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ 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
+(7 rows)
+
+-- SUFFIX merge with quantifiers: (A B*)+ A B* -> (a b*){2,}
+-- Tests mergeGroupPrefixSuffix: suffix with quantifiers
+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*){2,}
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 rows)
+
+-- Unwrap GROUP{1,1}: ((A | B | C)) -> (a | b | c)
+-- Tests tryUnwrapGroup removing redundant outer GROUP
+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)) DEFINE A AS val <= 30, B AS val <= 60, C AS val > 60);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ 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
+(7 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)
+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+? a
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 rows)
+
+-- Reluctant optimization bypass: GROUP merge
+-- (A B)+? (A B) stays separate (greedy merges to (a b){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 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)+? a b
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 rows)
+
+-- Reluctant optimization bypass: quantifier multiply (outer reluctant)
+-- (A{2}){3}? stays as (a{2}){3}? (greedy merges to a{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{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)
+
+-- Reluctant optimization bypass: quantifier multiply (inner reluctant)
+-- (A{2}?){3} stays as (a{2}?){3} (greedy merges to a{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{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)
+
+-- Reluctant optimization bypass: PREFIX merge
+-- A B (A B)+? stays separate (greedy merges to (a b){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 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 (a b)+?
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 rows)
+
+-- Reluctant optimization bypass: SUFFIX merge
+-- (A B)+? A B stays separate (greedy merges to (a b){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 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)+? a b
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 rows)
+
+-- GROUP unwrap with quantifier propagation: (A)?? B -> a?? b
+-- Single VAR child {1,1} receives GROUP's quantifier and reluctant
+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) 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
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 rows)
+
+-- Reluctant preserved through ALT flatten
+-- (A | (B | C))+? flattens to (a | b | c)+? - inner ALT flattened, reluctant kept
+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))+?) DEFINE A AS val <= 30, B AS val <= 60, C AS val > 60);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ 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
+(7 rows)
+
+-- Reluctant optimization bypass: absorption flags
+-- A+? with SKIP PAST LAST ROW - no absorption markers (greedy A+ gets a+")
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             AFTER MATCH SKIP PAST LAST ROW PATTERN (A+?) DEFINE A AS val > 0);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ 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
+(7 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)+
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 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+"
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 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,}"
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 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,}"
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 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+"
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 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
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 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)
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 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)
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 rows)
+
+-- ============================================================
+-- Absorption Flag Display Tests
+-- ============================================================
+-- Tests absorption marker display in EXPLAIN output
+-- Markers: ' = branch element, " = judgment point
+-- Files: explain.c (append_rpr_quantifier, deparse_rpr_pattern)
+-- Simple VAR: A+ -> a+" (judgment point)
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             AFTER MATCH SKIP PAST LAST ROW PATTERN (A+) DEFINE A AS val > 0);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ 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
+(7 rows)
+
+-- GROUP unbounded: (A B)+ -> (a' b')+" (branch + judgment)
+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')+"
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 rows)
+
+-- ALT both absorbable: 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
+             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+")
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 rows)
+
+-- ALT one absorbable: 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
+             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)
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 rows)
+
+-- Sequence with absorbable start: 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
+             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
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 rows)
+
+-- Complex nested: ((A+ B) | C) D | A B C - deeply nested ALT
+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) D | A B C)
+             DEFINE A AS val <= 30, B AS val <= 60, C AS val <= 80, D AS val > 80);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ 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
+(7 rows)
+
+-- Nested unbounded: (A+ | B)+ -> (a+" | b)+ (first iteration 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)+)
+             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)+
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 rows)
+
+-- ALT inside unbounded GROUP: (A+ B | A B)* -> (a+" b | a b)* (first iteration 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 | 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 | a b)*
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(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}
+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}')+"
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 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')+"
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 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}')+"
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 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})+
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 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?)+
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 rows)
+
+-- Non-absorbable (unbounded not at start): A B+ -> a b+ (no markers)
+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+
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 rows)
+
+-- Non-absorbable (no unbounded branch): (A | B){2,} -> (a | b){2,} (no markers)
+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,}) 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,}
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 rows)
+
+-- Non-absorbable (SKIP TO NEXT ROW): A+ -> a+ (no markers)
+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 TO NEXT ROW PATTERN (A+) DEFINE A AS val > 0);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ 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
+(7 rows)
+
+-- Non-absorbable (limited frame): A+ -> a+ (no markers)
+EXPLAIN (COSTS OFF)
+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);
+                                    QUERY PLAN                                    
+----------------------------------------------------------------------------------
+ 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
+(7 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
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: val
+         ->  Seq Scan on rpr_plan
+(7 rows)
+
+-- ============================================================
+-- Absorption Analysis Tests
+-- ============================================================
+-- Tests context absorption optimization (O(n^2) -> O(n))
+-- Files: rpr.c (computeAbsorbability)
+-- Simple Absorbable Pattern: A+ B
+-- Pattern starts with unbounded VAR
+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)
+    DEFINE A AS val <= 50, B AS val > 50
+)
+ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   6
+  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)
+
+-- Absorbable GROUP Pattern: (A B)+ C
+-- Pattern starts with unbounded GROUP
+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)+ C)
+    DEFINE A AS val <= 30, B AS val > 30 AND val <= 60, C AS val > 60
+)
+ORDER BY id;
+ 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)
+
+-- Non-Absorbable: Unbounded Not at Start
+-- Pattern: A B+ (unbounded not at start)
+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+)
+    DEFINE A AS val <= 50, B AS val > 50
+)
+ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   0
+  2 |  20 |   0
+  3 |  30 |   0
+  4 |  40 |   0
+  5 |  50 |   6
+  6 |  60 |   0
+  7 |  70 |   0
+  8 |  80 |   0
+  9 |  90 |   0
+ 10 | 100 |   0
+(10 rows)
+
+-- ALT with Absorbable Branches
+-- Pattern: (A+ | B+) C - both branches absorbable
+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+) C)
+    DEFINE A AS val <= 30, B AS val > 30 AND val <= 60, C AS val > 60
+)
+ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   0
+  2 |  20 |   0
+  3 |  30 |   0
+  4 |  40 |   4
+  5 |  50 |   0
+  6 |  60 |   0
+  7 |  70 |   0
+  8 |  80 |   0
+  9 |  90 |   0
+ 10 | 100 |   0
+(10 rows)
+
+-- ALT with Mixed Branches
+-- Pattern: (A+ | B C) - only first branch absorbable
+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 C)+)
+    DEFINE A AS val <= 30, B AS val > 30 AND val <= 60, C AS val > 60
+)
+ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   3
+  2 |  20 |   0
+  3 |  30 |   0
+  4 |  40 |   0
+  5 |  50 |   0
+  6 |  60 |   2
+  7 |  70 |   0
+  8 |  80 |   0
+  9 |  90 |   0
+ 10 | 100 |   0
+(10 rows)
+
+-- Non-Absorbable: ALT Inside GROUP
+-- Pattern: (A | B){2,} - ALT inside unbounded GROUP
+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){2,})
+    DEFINE A AS val <= 50, B AS val > 50
+)
+ORDER BY id;
+ 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)
+
+-- Non-Absorbable: Nested Unbounded
+-- Pattern: ((A B)+ C)+ - nested GROUP structure
+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)+ C)+)
+    DEFINE A AS val <= 30, B AS val > 30 AND val <= 60, C AS val > 60
+)
+ORDER BY id;
+ 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)
+
+-- Non-Absorbable: Unbounded Element Inside GROUP
+-- Pattern: (A B+){2,} - unbounded inside GROUP
+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+){2,})
+    DEFINE A AS val <= 50, B AS val > 50
+)
+ORDER BY id;
+ 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)
+
+-- Runtime Conditions: SKIP TO NEXT ROW
+-- Absorption disabled with SKIP TO NEXT ROW
+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)
+    DEFINE A AS val <= 50, B AS val > 50
+)
+ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   6
+  2 |  20 |   5
+  3 |  30 |   4
+  4 |  40 |   3
+  5 |  50 |   2
+  6 |  60 |   0
+  7 |  70 |   0
+  8 |  80 |   0
+  9 |  90 |   0
+ 10 | 100 |   0
+(10 rows)
+
+-- Runtime Conditions: Limited Frame
+-- Absorption disabled with limited frame end
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_plan
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND 5 FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A+ B)
+    DEFINE A AS val <= 50, B AS val > 50
+)
+ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   6
+  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)
+
+-- ============================================================
+-- Edge Case Tests
+-- ============================================================
+-- Tests boundary conditions and complex scenarios
+-- Empty Match Prevention
+-- Pattern that could match empty: A*
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_plan
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A*)
+    DEFINE A AS val > 1000  -- Never matches
+)
+ORDER BY id;
+ 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)
+
+-- All Rows Match
+-- Pattern where every row matches
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_plan
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS val >= 0  -- Always true
+)
+ORDER BY id;
+ 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)
+
+-- Large Quantifiers
+-- Pattern: A{100} (large exact quantifier)
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_plan
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{100})
+    DEFINE A AS val > 0
+)
+ORDER BY id;
+ 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)
+
+-- Pattern: A{10,20} (large range quantifier)
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_plan
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{10,20})
+    DEFINE A AS val > 0
+)
+ORDER BY id;
+ 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)
+
+-- Complex Multi-Level Nesting
+-- Pattern: (((A B) | C)+ D)+
+SELECT id, val, COUNT(*) OVER w as cnt
+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 <= 20, B AS val > 20 AND val <= 40,
+           C AS val > 40 AND val <= 60, D AS val > 60
+)
+ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   0
+  2 |  20 |   0
+  3 |  30 |   0
+  4 |  40 |   0
+  5 |  50 |   3
+  6 |  60 |   0
+  7 |  70 |   0
+  8 |  80 |   0
+  9 |  90 |   0
+ 10 | 100 |   0
+(10 rows)
+
+-- Long Alternation Chain
+-- Pattern: A | B | C | D | E (5-way ALT)
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_plan
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A | B | C | D | E)
+    DEFINE A AS val = 10, B AS val = 30, C AS val = 50,
+           D AS val = 70, E AS val = 90
+)
+ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   1
+  2 |  20 |   0
+  3 |  30 |   1
+  4 |  40 |   0
+  5 |  50 |   1
+  6 |  60 |   0
+  7 |  70 |   1
+  8 |  80 |   0
+  9 |  90 |   1
+ 10 | 100 |   0
+(10 rows)
+
+-- Long Sequence
+-- Pattern: A B C D E F G H (8-element SEQ)
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_plan
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B C D E F G H)
+    DEFINE A AS val >= 10, B AS val >= 20, C AS val >= 30,
+           D AS val >= 40, E AS val >= 50, F AS val >= 60,
+           G AS val >= 70, H AS val >= 80
+)
+ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   8
+  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)
+
+-- Interleaved Quantifiers
+-- Pattern: A{2} B+ C{3,5} D* E{1,}
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_plan
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{2} B+ C{3,5} D* E{1,})
+    DEFINE A AS val > 0, B AS val > 0, C AS val > 0,
+           D AS val > 0, E AS val > 0
+)
+ORDER BY id;
+ 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)
+
+-- ============================================================
+-- Optimization Fallback Tests
+-- ============================================================
+-- Tests for optimization edge cases and fallback behavior
+CREATE TABLE rpr_fallback (id INT, val INT);
+INSERT INTO rpr_fallback VALUES (1, 10), (2, 20);
+-- Test: min quantifier overflow causes optimization fallback (min == max case)
+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{2000000000}){2})
+    DEFINE A AS val > 0
+);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ 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
+(7 rows)
+
+-- Expected: Fallback - pattern not merged due to min overflow (4000000000 > INT32_MAX)
+-- Test: max-only quantifier overflow causes optimization 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{1,2000000000}){2})
+    DEFINE A AS val > 0
+);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ 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
+(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)
+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{2000000000,2147483647}){2})
+    DEFINE A AS val > 0
+);
+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
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A{2000000000,}){2000000000,})
+    DEFINE A AS val > 0
+);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ 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
+(7 rows)
+
+-- Expected: Fallback - min overflow (2000000000 * 2000000000 > INT32_MAX)
+-- Test: prefix mismatch causes optimization 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 (C D)+)
+    DEFINE A AS val > 0, B AS val > 5, C AS val > 10, D AS val > 15
+);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ 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
+(7 rows)
+
+-- Expected: Fallback - prefix elements don't match GROUP content
+DROP TABLE rpr_fallback;
+-- ============================================================
+-- Planner Integration Tests
+-- ============================================================
+-- Tests full planning pipeline and WindowAgg plan node creation
+-- Files: planner.c, createplan.c
+CREATE TABLE rpr_planner (id INT, category VARCHAR(10), val INT);
+INSERT INTO rpr_planner VALUES
+    (1, 'A', 10), (2, 'A', 20), (3, 'A', 30),
+    (4, 'B', 40), (5, 'B', 50), (6, 'B', 60),
+    (7, 'C', 70), (8, 'C', 80), (9, 'C', 90);
+-- Multiple Window Functions in Same Query
+SELECT id, category, val,
+       COUNT(*) OVER w1 as cnt1,
+       COUNT(*) OVER w2 as cnt2
+FROM rpr_planner
+WINDOW w1 AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS val > 0
+),
+w2 AS (
+    PARTITION BY category
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (B+)
+    DEFINE B AS val >= 40
+)
+ORDER BY id;
+ id | category | val | cnt1 | cnt2 
+----+----------+-----+------+------
+  1 | A        |  10 |    9 |    0
+  2 | A        |  20 |    0 |    0
+  3 | A        |  30 |    0 |    0
+  4 | B        |  40 |    0 |    3
+  5 | B        |  50 |    0 |    0
+  6 | B        |  60 |    0 |    0
+  7 | C        |  70 |    0 |    3
+  8 | C        |  80 |    0 |    0
+  9 | C        |  90 |    0 |    0
+(9 rows)
+
+-- Window Function with PARTITION BY
+SELECT id, category, val,
+       COUNT(*) OVER w as cnt
+FROM rpr_planner
+WINDOW w AS (
+    PARTITION BY category
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS val > 0
+)
+ORDER BY category, id;
+ id | category | val | cnt 
+----+----------+-----+-----
+  1 | A        |  10 |   3
+  2 | A        |  20 |   0
+  3 | A        |  30 |   0
+  4 | B        |  40 |   3
+  5 | B        |  50 |   0
+  6 | B        |  60 |   0
+  7 | C        |  70 |   3
+  8 | C        |  80 |   0
+  9 | C        |  90 |   0
+(9 rows)
+
+-- Window Function with Complex ORDER BY
+SELECT id, category, val,
+       COUNT(*) OVER w as cnt
+FROM rpr_planner
+WINDOW w AS (
+    ORDER BY category DESC, val ASC
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS val > 0
+)
+ORDER BY category DESC, val ASC;
+ id | category | val | cnt 
+----+----------+-----+-----
+  7 | C        |  70 |   9
+  8 | C        |  80 |   0
+  9 | C        |  90 |   0
+  4 | B        |  40 |   0
+  5 | B        |  50 |   0
+  6 | B        |  60 |   0
+  1 | A        |  10 |   0
+  2 | A        |  20 |   0
+  3 | A        |  30 |   0
+(9 rows)
+
+-- Named Window Reference
+SELECT id, category, val,
+       COUNT(*) OVER w as cnt
+FROM rpr_planner
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS val > 0
+)
+ORDER BY id;
+ id | category | val | cnt 
+----+----------+-----+-----
+  1 | A        |  10 |   9
+  2 | A        |  20 |   0
+  3 | A        |  30 |   0
+  4 | B        |  40 |   0
+  5 | B        |  50 |   0
+  6 | B        |  60 |   0
+  7 | C        |  70 |   0
+  8 | C        |  80 |   0
+  9 | C        |  90 |   0
+(9 rows)
+
+-- Inline Window Definition
+SELECT id, category, val,
+       COUNT(*) OVER (
+           ORDER BY id
+           ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+           PATTERN (A+)
+           DEFINE A AS val > 0
+       ) as cnt
+FROM rpr_planner
+ORDER BY id;
+ id | category | val | cnt 
+----+----------+-----+-----
+  1 | A        |  10 |   9
+  2 | A        |  20 |   0
+  3 | A        |  30 |   0
+  4 | B        |  40 |   0
+  5 | B        |  50 |   0
+  6 | B        |  60 |   0
+  7 | C        |  70 |   0
+  8 | C        |  80 |   0
+  9 | C        |  90 |   0
+(9 rows)
+
+-- Window with Aggregate Functions
+SELECT category,
+       COUNT(*) OVER w as window_cnt,
+       COUNT(*) as agg_cnt
+FROM rpr_planner
+WINDOW w AS (
+    PARTITION BY category
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS val > 0
+)
+GROUP BY category
+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
+-- Files: planner.c, prepjointree.c
+-- ============================================================
+-- Tests RPR with subqueries and CTEs
+-- RPR in Subquery (FROM clause)
+SELECT * FROM (
+    SELECT id, category, val,
+           COUNT(*) OVER w as cnt
+    FROM rpr_planner
+    WINDOW w AS (
+        ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A+)
+        DEFINE A AS val > 0
+    )
+) sub
+WHERE cnt > 5
+ORDER BY id;
+ id | category | val | cnt 
+----+----------+-----+-----
+  1 | A        |  10 |   9
+(1 row)
+
+-- RPR with Subquery in WHERE
+SELECT id, category, val,
+       COUNT(*) OVER w as cnt
+FROM rpr_planner
+WHERE val > (SELECT AVG(val) FROM rpr_planner)
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS val > 50
+)
+ORDER BY id;
+ id | category | val | cnt 
+----+----------+-----+-----
+  6 | B        |  60 |   4
+  7 | C        |  70 |   0
+  8 | C        |  80 |   0
+  9 | C        |  90 |   0
+(4 rows)
+
+-- CTE with RPR
+WITH rpr_cte AS (
+    SELECT id, category, val,
+           COUNT(*) OVER w as cnt
+    FROM rpr_planner
+    WINDOW w AS (
+        ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A+)
+        DEFINE A AS val > 0
+    )
+)
+SELECT * FROM rpr_cte WHERE cnt > 5 ORDER BY id;
+ id | category | val | cnt 
+----+----------+-----+-----
+  1 | A        |  10 |   9
+(1 row)
+
+-- Multiple CTE References
+WITH rpr_cte AS (
+    SELECT id, category, val,
+           COUNT(*) OVER w as cnt
+    FROM rpr_planner
+    WINDOW w AS (
+        ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A+)
+        DEFINE A AS val > 0
+    )
+)
+SELECT c1.id, c1.cnt, c2.cnt as cnt2
+FROM rpr_cte c1
+JOIN rpr_cte c2 ON c1.id = c2.id
+ORDER BY c1.id;
+ id | cnt | cnt2 
+----+-----+------
+  1 |   9 |    9
+  2 |   0 |    0
+  3 |   0 |    0
+  4 |   0 |    0
+  5 |   0 |    0
+  6 |   0 |    0
+  7 |   0 |    0
+  8 |   0 |    0
+  9 |   0 |    0
+(9 rows)
+
+-- Nested CTEs
+WITH cte1 AS (
+    SELECT id, category, val FROM rpr_planner WHERE val > 30
+),
+cte2 AS (
+    SELECT id, category, val,
+           COUNT(*) OVER w as cnt
+    FROM cte1
+    WINDOW w AS (
+        ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A+)
+        DEFINE A AS val > 0
+    )
+)
+SELECT * FROM cte2 ORDER BY id;
+ id | category | val | cnt 
+----+----------+-----+-----
+  4 | B        |  40 |   6
+  5 | B        |  50 |   0
+  6 | B        |  60 |   0
+  7 | C        |  70 |   0
+  8 | C        |  80 |   0
+  9 | C        |  90 |   0
+(6 rows)
+
+-- ============================================================
+-- JOIN Tests
+-- Files: prepjointree.c, setrefs.c
+-- ============================================================
+-- Tests RPR with JOINs and multiple table references
+CREATE TABLE rpr_join1 (id INT, val1 INT);
+CREATE TABLE rpr_join2 (id INT, val2 INT);
+INSERT INTO rpr_join1 VALUES (1, 10), (2, 20), (3, 30), (4, 40), (5, 50);
+INSERT INTO rpr_join2 VALUES (1, 100), (2, 200), (3, 300), (4, 400), (5, 500);
+-- RPR After INNER JOIN
+SELECT t1.id, t1.val1, t2.val2,
+       COUNT(*) OVER w as cnt
+FROM rpr_join1 t1
+INNER JOIN rpr_join2 t2 ON t1.id = t2.id
+WINDOW w AS (
+    ORDER BY t1.id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS val1 + val2 > 100
+)
+ORDER BY t1.id;
+ id | val1 | val2 | cnt 
+----+------+------+-----
+  1 |   10 |  100 |   5
+  2 |   20 |  200 |   0
+  3 |   30 |  300 |   0
+  4 |   40 |  400 |   0
+  5 |   50 |  500 |   0
+(5 rows)
+
+-- RPR After LEFT JOIN
+SELECT t1.id, t1.val1, t2.val2,
+       COUNT(*) OVER w as cnt
+FROM rpr_join1 t1
+LEFT JOIN rpr_join2 t2 ON t1.id = t2.id
+WINDOW w AS (
+    ORDER BY t1.id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS val1 > 0
+)
+ORDER BY t1.id;
+ id | val1 | val2 | cnt 
+----+------+------+-----
+  1 |   10 |  100 |   5
+  2 |   20 |  200 |   0
+  3 |   30 |  300 |   0
+  4 |   40 |  400 |   0
+  5 |   50 |  500 |   0
+(5 rows)
+
+-- RPR with Multiple Tables in DEFINE
+SELECT t1.id, t1.val1, t2.val2,
+       COUNT(*) OVER w as cnt
+FROM rpr_join1 t1
+INNER JOIN rpr_join2 t2 ON t1.id = t2.id
+WINDOW w AS (
+    ORDER BY t1.id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+ B)
+    DEFINE A AS val1 > 20,
+           B AS val2 > 200
+)
+ORDER BY t1.id;
+ id | val1 | val2 | cnt 
+----+------+------+-----
+  1 |   10 |  100 |   0
+  2 |   20 |  200 |   0
+  3 |   30 |  300 |   3
+  4 |   40 |  400 |   0
+  5 |   50 |  500 |   0
+(5 rows)
+
+-- RPR After Cross Join
+SELECT t1.id as id1, t2.id as id2, t1.val1, t2.val2,
+       COUNT(*) OVER w as cnt
+FROM rpr_join1 t1
+CROSS JOIN rpr_join2 t2
+WHERE t1.id <= 2 AND t2.id <= 2
+WINDOW w AS (
+    ORDER BY t1.id, t2.id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS val1 + val2 > 0
+)
+ORDER BY t1.id, t2.id;
+ id1 | id2 | val1 | val2 | cnt 
+-----+-----+------+------+-----
+   1 |   1 |   10 |  100 |   4
+   1 |   2 |   10 |  200 |   0
+   2 |   1 |   20 |  100 |   0
+   2 |   2 |   20 |  200 |   0
+(4 rows)
+
+-- Self-Join with RPR
+SELECT id, val1, val1_next,
+       COUNT(*) OVER w as cnt
+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 id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (X+)
+    DEFINE X AS val1 < val1_next
+)
+ORDER BY id;
+ id | val1 | val1_next | cnt 
+----+------+-----------+-----
+  1 |   10 |        20 |   4
+  2 |   20 |        30 |   0
+  3 |   30 |        40 |   0
+  4 |   40 |        50 |   0
+(4 rows)
+
+DROP TABLE rpr_join1, rpr_join2;
+-- ============================================================
+-- Complex Expression Tests
+-- Files: createplan.c, setrefs.c
+-- ============================================================
+-- Tests complex target list expressions
+CREATE TABLE rpr_target (id INT, val INT);
+INSERT INTO rpr_target VALUES (1, 10), (2, 20), (3, 30), (4, 40), (5, 50);
+-- Expressions in Target List
+SELECT id,
+       val * 2 as doubled,
+       val + 10 as added,
+       COUNT(*) OVER w as cnt
+FROM rpr_target
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS val > 0
+)
+ORDER BY id;
+ id | doubled | added | cnt 
+----+---------+-------+-----
+  1 |      20 |    20 |   5
+  2 |      40 |    30 |   0
+  3 |      60 |    40 |   0
+  4 |      80 |    50 |   0
+  5 |     100 |    60 |   0
+(5 rows)
+
+-- CASE Expression in Target List
+SELECT id, val,
+       CASE
+           WHEN val < 30 THEN 'low'
+           WHEN val < 50 THEN 'medium'
+           ELSE 'high'
+       END as category,
+       COUNT(*) OVER w as cnt
+FROM rpr_target
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS val > 0
+)
+ORDER BY id;
+ id | val | category | cnt 
+----+-----+----------+-----
+  1 |  10 | low      |   5
+  2 |  20 | low      |   0
+  3 |  30 | medium   |   0
+  4 |  40 | medium   |   0
+  5 |  50 | high     |   0
+(5 rows)
+
+-- Subquery in Target List
+SELECT id, val,
+       (SELECT MAX(val) FROM rpr_target) as max_val,
+       COUNT(*) OVER w as cnt
+FROM rpr_target
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS val > 0
+)
+ORDER BY id;
+ id | val | max_val | cnt 
+----+-----+---------+-----
+  1 |  10 |      50 |   5
+  2 |  20 |      50 |   0
+  3 |  30 |      50 |   0
+  4 |  40 |      50 |   0
+  5 |  50 |      50 |   0
+(5 rows)
+
+-- Function Calls in Target List
+SELECT id, val,
+       COALESCE(val, 0) as coalesced,
+       ABS(val - 30) as distance,
+       COUNT(*) OVER w as cnt
+FROM rpr_target
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS val > 0
+)
+ORDER BY id;
+ id | val | coalesced | distance | cnt 
+----+-----+-----------+----------+-----
+  1 |  10 |        10 |       20 |   5
+  2 |  20 |        20 |       10 |   0
+  3 |  30 |        30 |        0 |   0
+  4 |  40 |        40 |       10 |   0
+  5 |  50 |        50 |       20 |   0
+(5 rows)
+
+-- Column Aliases and References
+SELECT id as row_id,
+       val as value,
+       COUNT(*) OVER w as cnt
+FROM rpr_target
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS val > 0
+)
+ORDER BY row_id;
+ row_id | value | cnt 
+--------+-------+-----
+      1 |    10 |   5
+      2 |    20 |   0
+      3 |    30 |   0
+      4 |    40 |   0
+      5 |    50 |   0
+(5 rows)
+
+DROP TABLE rpr_target;
+-- ============================================================
+-- Set Operations Tests
+-- Files: planner.c
+-- ============================================================
+-- Tests RPR with UNION, INTERSECT, EXCEPT
+CREATE TABLE rpr_set1 (id INT, val INT);
+CREATE TABLE rpr_set2 (id INT, val INT);
+INSERT INTO rpr_set1 VALUES (1, 10), (2, 20), (3, 30);
+INSERT INTO rpr_set2 VALUES (2, 20), (3, 30), (4, 40);
+-- UNION with RPR
+(SELECT id, val, COUNT(*) OVER w as cnt
+ FROM rpr_set1
+ WINDOW w AS (
+     ORDER BY id
+     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+     PATTERN (A+)
+     DEFINE A AS val > 0
+ ))
+UNION
+(SELECT id, val, COUNT(*) OVER w as cnt
+ FROM rpr_set2
+ WINDOW w AS (
+     ORDER BY id
+     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+     PATTERN (A+)
+     DEFINE A AS val > 0
+ ))
+ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   3
+  2 |  20 |   0
+  2 |  20 |   3
+  3 |  30 |   0
+  4 |  40 |   0
+(5 rows)
+
+-- UNION ALL with RPR
+(SELECT id, val, COUNT(*) OVER w as cnt
+ FROM rpr_set1
+ WINDOW w AS (
+     ORDER BY id
+     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+     PATTERN (A+)
+     DEFINE A AS val > 0
+ ))
+UNION ALL
+(SELECT id, val, COUNT(*) OVER w as cnt
+ FROM rpr_set2
+ WINDOW w AS (
+     ORDER BY id
+     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+     PATTERN (A+)
+     DEFINE A AS val > 0
+ ))
+ORDER BY id, val;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   3
+  2 |  20 |   0
+  2 |  20 |   3
+  3 |  30 |   0
+  3 |  30 |   0
+  4 |  40 |   0
+(6 rows)
+
+-- INTERSECT with RPR
+(SELECT id, val, COUNT(*) OVER w as cnt
+ FROM rpr_set1
+ WINDOW w AS (
+     ORDER BY id
+     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+     PATTERN (A+)
+     DEFINE A AS val > 0
+ ))
+INTERSECT
+(SELECT id, val, COUNT(*) OVER w as cnt
+ FROM rpr_set2
+ WINDOW w AS (
+     ORDER BY id
+     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+     PATTERN (A+)
+     DEFINE A AS val > 0
+ ))
+ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  3 |  30 |   0
+(1 row)
+
+-- EXCEPT with RPR
+(SELECT id, val, COUNT(*) OVER w as cnt
+ FROM rpr_set1
+ WINDOW w AS (
+     ORDER BY id
+     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+     PATTERN (A+)
+     DEFINE A AS val > 0
+ ))
+EXCEPT
+(SELECT id, val, COUNT(*) OVER w as cnt
+ FROM rpr_set2
+ WINDOW w AS (
+     ORDER BY id
+     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+     PATTERN (A+)
+     DEFINE A AS val > 0
+ ))
+ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   3
+  2 |  20 |   0
+(2 rows)
+
+DROP TABLE rpr_set1, rpr_set2;
+-- ============================================================
+-- Sorting and Grouping Tests
+-- Files: planner.c, createplan.c
+-- ============================================================
+-- Tests RPR interaction with sorting and grouping
+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)
+-- Expected: ERROR: aggregate functions are not allowed in DEFINE
+SELECT category,
+       COUNT(*) as group_cnt,
+       MAX(val) as max_val,
+       COUNT(*) OVER w as window_cnt
+FROM rpr_sort
+GROUP BY category
+WINDOW w AS (
+    ORDER BY category
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS COUNT(*) > 0
+)
+ORDER BY category;
+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
+FROM rpr_sort
+GROUP BY category
+HAVING COUNT(*) > 2
+WINDOW w AS (
+    ORDER BY category
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS COUNT(*) > 0
+)
+ORDER BY category;
+ERROR:  aggregate functions are not allowed in DEFINE
+LINE 11:     DEFINE A AS COUNT(*) > 0
+                         ^
+-- RPR with DISTINCT
+SELECT DISTINCT category,
+       COUNT(*) OVER w as cnt
+FROM rpr_sort
+WINDOW w AS (
+    PARTITION BY category
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS val > 0
+)
+ORDER BY category;
+ category | cnt 
+----------+-----
+ A        |   3
+ A        |   0
+ B        |   0
+ B        |   3
+(4 rows)
+
+-- RPR with ORDER BY (different from window ORDER BY)
+SELECT id, category, val,
+       COUNT(*) OVER w as cnt
+FROM rpr_sort
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS val > 0
+)
+ORDER BY val DESC;
+ id | category | val | cnt 
+----+----------+-----+-----
+  6 | B        |  60 |   0
+  5 | A        |  50 |   0
+  4 | B        |  40 |   0
+  1 | A        |  30 |   6
+  2 | B        |  20 |   0
+  3 | A        |  10 |   0
+(6 rows)
+
+-- RPR with LIMIT and OFFSET
+SELECT id, category, val,
+       COUNT(*) OVER w as cnt
+FROM rpr_sort
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS val > 0
+)
+ORDER BY id
+LIMIT 3 OFFSET 1;
+ id | category | val | cnt 
+----+----------+-----+-----
+  2 | B        |  20 |   0
+  3 | A        |  10 |   0
+  4 | B        |  40 |   0
+(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
+-- ============================================================
+-- Edge cases and stress scenarios
+CREATE TABLE rpr_stress (id INT, val INT);
+INSERT INTO rpr_stress SELECT i, i * 10 FROM generate_series(1, 20) i;
+-- Very Long Query with Many Windows
+SELECT id, val,
+       COUNT(*) OVER w1 as cnt1,
+       COUNT(*) OVER w2 as cnt2,
+       COUNT(*) OVER w3 as cnt3
+FROM rpr_stress
+WINDOW w1 AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS val > 0
+),
+w2 AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (B+)
+    DEFINE B AS val > 50
+),
+w3 AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (C+)
+    DEFINE C AS val > 100
+)
+ORDER BY id;
+ id | val | cnt1 | cnt2 | cnt3 
+----+-----+------+------+------
+  1 |  10 |   20 |    0 |    0
+  2 |  20 |    0 |    0 |    0
+  3 |  30 |    0 |    0 |    0
+  4 |  40 |    0 |    0 |    0
+  5 |  50 |    0 |    0 |    0
+  6 |  60 |    0 |   15 |    0
+  7 |  70 |    0 |    0 |    0
+  8 |  80 |    0 |    0 |    0
+  9 |  90 |    0 |    0 |    0
+ 10 | 100 |    0 |    0 |    0
+ 11 | 110 |    0 |    0 |   10
+ 12 | 120 |    0 |    0 |    0
+ 13 | 130 |    0 |    0 |    0
+ 14 | 140 |    0 |    0 |    0
+ 15 | 150 |    0 |    0 |    0
+ 16 | 160 |    0 |    0 |    0
+ 17 | 170 |    0 |    0 |    0
+ 18 | 180 |    0 |    0 |    0
+ 19 | 190 |    0 |    0 |    0
+ 20 | 200 |    0 |    0 |    0
+(20 rows)
+
+-- Deeply Nested Subqueries with RPR
+SELECT * FROM (
+    SELECT * FROM (
+        SELECT * FROM (
+            SELECT id, val,
+                   COUNT(*) OVER w as cnt
+            FROM rpr_stress
+            WINDOW w AS (
+                ORDER BY id
+                ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+                PATTERN (A+)
+                DEFINE A AS val > 0
+            )
+        ) sub1
+    ) sub2
+) sub3
+WHERE cnt > 10
+ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |  20
+(1 row)
+
+-- Complex Expression in DEFINE Clause
+SELECT id, val,
+       COUNT(*) OVER w as cnt
+FROM rpr_stress
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+ B)
+    DEFINE A AS (val % 3 = 0 OR val % 5 = 0),
+           B AS (val * 2 > 100 AND val / 2 < 100)
+)
+ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |  19
+  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
+ 11 | 110 |   0
+ 12 | 120 |   0
+ 13 | 130 |   0
+ 14 | 140 |   0
+ 15 | 150 |   0
+ 16 | 160 |   0
+ 17 | 170 |   0
+ 18 | 180 |   0
+ 19 | 190 |   0
+ 20 | 200 |   0
+(20 rows)
+
+-- Window with No Matching Rows
+SELECT id, val,
+       COUNT(*) OVER w as cnt
+FROM rpr_stress
+WHERE val > 1000  -- No rows match
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS val > 0
+)
+ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+(0 rows)
+
+-- Window on Single Row
+SELECT id, val,
+       COUNT(*) OVER w as cnt
+FROM rpr_stress
+WHERE id = 10
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS val > 0
+)
+ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+ 10 | 100 |   1
+(1 row)
+
+DROP TABLE rpr_stress;
+-- ============================================================
+-- Error Limit Tests
+-- ============================================================
+-- 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: DEFINE variable not in PATTERN (error)
+SELECT id, val, COUNT(*) OVER w FROM rpr_errors
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A)
+    DEFINE
+      B AS TRUE
+);
+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
+    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)
+    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
+);
+ count 
+-------
+     0
+     0
+(2 rows)
+
+-- 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 (
+    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)
+    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
+);
+ERROR:  too many pattern variables
+DETAIL:  Maximum is 251.
+-- Expected: ERROR - too many pattern variables (Maximum is 251)
+-- 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
+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})
+    DEFINE A AS val > 0
+);
+ id | val | count 
+----+-----+-------
+  1 |  10 |     0
+  2 |  20 |     0
+(2 rows)
+
+-- Expected: Should succeed
+-- Test: Pattern nesting depth exceeds maximum (depth 254)
+-- Note: 254 nested GROUP{3,7} quantifiers produce depth 254 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}){3,7})
+    DEFINE A AS val > 0
+);
+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 tests from jacob branch
+-- Test: A? (optional, greedy)
+SELECT id, val, count(*) OVER w AS c
+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 > 50
+);
+ id | val | c 
+----+-----+---
+  1 |  10 | 0
+  2 |  20 | 0
+  3 |  30 | 0
+  4 |  40 | 0
+  5 |  50 | 0
+  6 |  60 | 1
+  7 |  70 | 1
+  8 |  80 | 1
+  9 |  90 | 1
+ 10 | 100 | 1
+(10 rows)
+
+-- Test: A{2} (exact count)
+SELECT id, val, count(*) OVER w AS c
+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})
+    DEFINE A AS val <= 50
+);
+ id | val | c 
+----+-----+---
+  1 |  10 | 2
+  2 |  20 | 0
+  3 |  30 | 2
+  4 |  40 | 0
+  5 |  50 | 0
+  6 |  60 | 0
+  7 |  70 | 0
+  8 |  80 | 0
+  9 |  90 | 0
+ 10 | 100 | 0
+(10 rows)
+
+-- Test: A{1,3} (bounded range, greedy)
+SELECT id, val, count(*) OVER w AS c
+FROM rpr_plan
+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 val <= 50
+);
+ id | val | c 
+----+-----+---
+  1 |  10 | 3
+  2 |  20 | 0
+  3 |  30 | 0
+  4 |  40 | 2
+  5 |  50 | 0
+  6 |  60 | 0
+  7 |  70 | 0
+  8 |  80 | 0
+  9 |  90 | 0
+ 10 | 100 | 0
+(10 rows)
+
+-- Test: A | B (simple alternation)
+SELECT id, val, count(*) OVER w AS c
+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 <= 30, B AS val > 70
+);
+ id | val | c 
+----+-----+---
+  1 |  10 | 1
+  2 |  20 | 1
+  3 |  30 | 1
+  4 |  40 | 0
+  5 |  50 | 0
+  6 |  60 | 0
+  7 |  70 | 0
+  8 |  80 | 1
+  9 |  90 | 1
+ 10 | 100 | 1
+(10 rows)
+
+-- Test: A | B | C (three-way alternation)
+SELECT id, val, count(*) OVER w AS c
+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)
+    DEFINE A AS val <= 20, B AS val BETWEEN 40 AND 60, C AS val > 80
+);
+ id | val | c 
+----+-----+---
+  1 |  10 | 1
+  2 |  20 | 1
+  3 |  30 | 0
+  4 |  40 | 1
+  5 |  50 | 1
+  6 |  60 | 1
+  7 |  70 | 0
+  8 |  80 | 0
+  9 |  90 | 1
+ 10 | 100 | 1
+(10 rows)
+
+-- Test: A B C (concatenation)
+SELECT id, val, count(*) OVER w AS c
+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)
+    DEFINE A AS val <= 30, B AS val BETWEEN 31 AND 60, C AS val > 60
+);
+ id | val | c 
+----+-----+---
+  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)
+
+-- Test: A B? C (optional middle)
+SELECT id, val, count(*) OVER w AS c
+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)
+    DEFINE A AS val <= 30, B AS val BETWEEN 31 AND 60, C AS val > 60
+);
+ id | val | c 
+----+-----+---
+  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)
+
+-- Test: (A B)+ (grouped quantifier)
+SELECT id, val, count(*) OVER w AS c
+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
+);
+ id | val | c 
+----+-----+---
+  1 |  10 | 0
+  2 |  20 | 0
+  3 |  30 | 0
+  4 |  40 | 0
+  5 |  50 | 2
+  6 |  60 | 0
+  7 |  70 | 0
+  8 |  80 | 0
+  9 |  90 | 0
+ 10 | 100 | 0
+(10 rows)
+
+-- Test: (A | B)+ C (alternation with quantifier)
+SELECT id, val, count(*) OVER w AS c
+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)
+    DEFINE A AS val <= 30, B AS val BETWEEN 31 AND 60, C AS val > 80
+);
+ id | val | c 
+----+-----+---
+  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)
+
+-- Test: (A+ | (A | B)+)* - nested alternation inside quantified group
+-- Previously caused infinite recursion in nfa_advance_alt when the inner
+-- BEGIN(+)'s skip jump was followed as an ALT branch pointer.
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM (VALUES
+    (1, ARRAY['A', 'B']),
+    (2, ARRAY['B']),
+    (3, ARRAY['C'])
+) AS t(id, flags)
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A+ | (A | B)+)*)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {A,B} |           1 |         2
+  2 | {B}   |             |          
+  3 | {C}   |             |          
+(3 rows)
+
+-- ============================================================
+-- Pathological Patterns
+-- ============================================================
+-- These patterns previously caused issues. Now optimized or handled safely.
+-- Test: (A*)* - nested unbounded (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 (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)*)*)*  - triple nested (optimized to A*)
+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)
+
+-- Optional group with alternation: A ((B | C) (D | E))* F?
+-- When only A matches, the * group matches 0 times and F? matches 0 times
+SELECT id, val, match_len
+FROM (SELECT id, val,
+             COUNT(*) OVER w AS match_len
+      FROM (VALUES (1, 1), (2, 99)) AS t(id, val)
+      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))* F?)
+          DEFINE A AS val = 1,
+                 B AS val = 2, C AS val = 3,
+                 D AS val = 4, E AS val = 5,
+                 F AS val = 6
+      )
+) s;
+ id | val | match_len 
+----+-----+-----------
+  1 |   1 |         1
+  2 |  99 |         0
+(2 rows)
+
+DROP TABLE rpr_plan;
diff --git a/src/test/regress/expected/rpr_explain.out b/src/test/regress/expected/rpr_explain.out
new file mode 100644
index 00000000000..dc3522f930f
--- /dev/null
+++ b/src/test/regress/expected/rpr_explain.out
@@ -0,0 +1,4885 @@
+-- ============================================================
+-- RPR EXPLAIN Tests
+-- 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
+--   - 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)
+--
+-- Test Coverage:
+--   Basic NFA Statistics Tests
+--   State Statistics Tests
+--   Context Statistics Tests
+--   Match Length Statistics Tests
+--   Mismatch Length Statistics Tests
+--   JSON Format Tests
+--   XML Format Tests
+--   Multiple Partitions Tests
+--   Edge Cases
+--   Complex Pattern Tests
+--   Real-world Pattern Examples
+--   Performance-oriented Tests
+--   INITIAL vs no INITIAL comparison
+--   Quantifier Variations
+--   Regression Tests for Statistics Accuracy
+--   Alternation Pattern Tests
+--   Group Pattern Tests
+--   Window Function Combinations
+--   DEFINE Expression Variations
+--   Large Scale Statistics Verification
+--   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
+-- indicate issues such as uninitialized memory access.
+-- Works for text, JSON, and XML formats.
+create function rpr_explain_filter(text) returns setof text
+language plpgsql as
+$$
+declare
+    ln text;
+begin
+    for ln in execute $1
+    loop
+        -- Normalize platform-dependent memory values
+        -- Keep NFA statistics numbers unchanged (they are test assertions)
+
+        -- Text format: "Storage: Memory  Maximum Storage: 18kB"
+        if ln ~ 'Storage:.*Maximum Storage:' then
+            ln := regexp_replace(ln, '\m\d+kB', 'NkB', 'g');
+        end if;
+
+        -- JSON format: "Maximum Storage": 17 (number in kB units)
+        if ln ~ '"Maximum Storage":' then
+            ln := regexp_replace(ln, '"Maximum Storage": \d+', '"Maximum Storage": 0', 'g');
+        end if;
+
+        -- XML format: <Maximum-Storage>17</Maximum-Storage> (number in kB units)
+        if ln ~ '<Maximum-Storage>' then
+            ln := regexp_replace(ln, '<Maximum-Storage>\d+</Maximum-Storage>', '<Maximum-Storage>0</Maximum-Storage>', 'g');
+        end if;
+
+        -- Sort Method memory is platform-dependent (32-bit vs 64-bit)
+        if ln ~ 'Sort Method:.*Memory:' then
+            ln := regexp_replace(ln, 'Memory: \d+kB', 'Memory: NkB');
+        end if;
+
+        return next ln;
+    end loop;
+end;
+$$;
+-- Setup: Create test tables
+CREATE TABLE rpr_nfa_test (
+    id serial,
+    v int,
+    cat char(1)
+);
+-- Insert test data: 100 rows with predictable pattern
+INSERT INTO rpr_nfa_test (v, cat)
+SELECT i,
+       CASE
+           WHEN i % 5 = 1 THEN 'A'
+           WHEN i % 5 = 2 THEN 'B'
+           WHEN i % 5 = 3 THEN 'C'
+           WHEN i % 5 = 4 THEN 'D'
+           ELSE 'E'
+       END
+FROM generate_series(1, 100) i;
+-- Additional test table with more complex patterns
+CREATE TABLE rpr_nfa_complex (
+    id serial,
+    price int,
+    trend char(1)  -- U=up, D=down, S=stable
+);
+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'),
+    (120, 'U'), (125, 'U'), (130, 'U'), (128, 'D'), (126, 'D'),
+    (124, 'D'), (122, 'D'), (120, 'D'), (118, 'D'), (119, 'U'),
+    (121, 'U'), (123, 'U'), (125, 'U'), (127, 'U'), (129, 'U'),
+    (131, 'U'), (133, 'U'), (130, 'D'), (127, 'D'), (124, 'D');
+-- ============================================================
+-- Basic NFA Statistics Tests
+-- ============================================================
+-- Simple pattern - should show basic statistics
+CREATE VIEW rpr_ev_basic_simple AS
+SELECT count(*) OVER w
+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_ev_basic_simple'), 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 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''
+)');
+                        rpr_explain_filter                         
+-------------------------------------------------------------------
+ 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)
+(10 rows)
+
+-- Pattern with no matches - 0 matched
+CREATE VIEW rpr_ev_basic_nomatch AS
+SELECT count(*) OVER w
+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_ev_basic_nomatch'), E'\n')) AS line WHERE line ~ 'PATTERN';
+        line        
+--------------------
+   PATTERN (x y z) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+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''
+);');
+                        rpr_explain_filter                         
+-------------------------------------------------------------------
+ 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)
+(9 rows)
+
+-- Pattern matching every row - high match count
+CREATE VIEW rpr_ev_basic_allrows AS
+SELECT count(*) OVER w
+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_ev_basic_allrows'), E'\n')) AS line WHERE line ~ 'PATTERN';
+      line      
+----------------
+   PATTERN (r) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+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
+);');
+                        rpr_explain_filter                         
+-------------------------------------------------------------------
+ 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)
+(9 rows)
+
+-- Regression test: Space before parenthesis in pattern deparse
+-- Verifies that "A (B | C)" correctly outputs as "a (b | c)" with space
+CREATE VIEW rpr_ev_basic_deparse_space 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 % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
+);
+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)) 
+(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 % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
+);');
+                          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: 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)
+(10 rows)
+
+-- 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_ev_basic_deparse_seqalt AS
+SELECT count(*) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    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_ev_basic_deparse_seqalt'), E'\n')) AS line WHERE line ~ 'PATTERN';
+               line                
+-----------------------------------
+   PATTERN (a ((b | c) (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, 30) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    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
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ 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)
+(9 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"+
+   Nav Mark Lookback: 1
+   ->  Function Scan on generate_series s
+(5 rows)
+
+-- ============================================================
+-- State Statistics Tests (peak, total, merged)
+-- ============================================================
+-- Simple quantifier pattern - A+ with short matches (no merging)
+CREATE VIEW rpr_ev_state_simple_quant 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+)
+    DEFINE A AS v % 2 = 1
+);
+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+) 
+(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+)
+    DEFINE A AS v % 2 = 1
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ 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)
+(9 rows)
+
+-- Alternation pattern - multiple state branches
+CREATE VIEW rpr_ev_state_alt AS
+SELECT count(*) OVER w
+FROM rpr_nfa_test
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A | B | C) (D | E))
+    DEFINE
+        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_ev_state_alt'), E'\n')) AS line WHERE line ~ 'PATTERN';
+               line               
+----------------------------------
+   PATTERN ((a | b | c) (d | e)) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM rpr_nfa_test
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A | B | C) (D | E))
+    DEFINE
+        A AS cat = ''A'', B AS cat = ''B'', C AS cat = ''C'',
+        D AS cat = ''D'', E AS cat = ''E''
+);');
+                        rpr_explain_filter                         
+-------------------------------------------------------------------
+ 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)
+(10 rows)
+
+-- Complex pattern with high state count
+CREATE VIEW rpr_ev_state_complex AS
+SELECT count(*) OVER w
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    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_ev_state_complex'), 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, 100) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A+ B* C+)
+    DEFINE
+        A AS v % 3 = 1,
+        B AS v % 3 = 2,
+        C AS v % 3 = 0
+);');
+                          rpr_explain_filter                           
+-----------------------------------------------------------------------
+ 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)
+(10 rows)
+
+-- Grouped pattern with quantifier - state count with grouping
+CREATE VIEW rpr_ev_state_group_quant AS
+SELECT count(*) OVER w
+FROM generate_series(1, 60) 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_ev_state_group_quant'), 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, 60) 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                          
+----------------------------------------------------------------------
+ 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)
+(10 rows)
+
+-- State explosion pattern - many alternations
+-- Pattern (A|B)(A|B)(A|B)(A|B) can create many parallel states
+CREATE VIEW rpr_ev_state_explosion AS
+SELECT count(*) OVER w
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    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_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)) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    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
+);');
+                          rpr_explain_filter                           
+-----------------------------------------------------------------------
+ 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)
+(10 rows)
+
+-- Consecutive ALT merge followed by different ALT
+-- Tests mergeConsecutiveAlts flush on ALT change: (A|B){2} (C|D)
+CREATE VIEW rpr_ev_state_alt_merge_alt AS
+SELECT count(*) OVER w
+FROM generate_series(1, 40) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    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_ev_state_alt_merge_alt'), E'\n')) AS line WHERE line ~ 'PATTERN';
+                 line                 
+--------------------------------------
+   PATTERN ((a | b) (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, 40) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    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
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ 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)
+(10 rows)
+
+-- Consecutive ALT merge followed by non-ALT element
+-- Tests mergeConsecutiveAlts flush on non-ALT: (A|B){2} c
+CREATE VIEW rpr_ev_state_alt_merge_nonalt AS
+SELECT count(*) OVER w
+FROM generate_series(1, 40) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    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_ev_state_alt_merge_nonalt'), E'\n')) AS line WHERE line ~ 'PATTERN';
+              line              
+--------------------------------
+   PATTERN ((a | b) (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, 40) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A | B) (A | B) C)
+    DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ 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)
+(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
+SELECT count(*) OVER w
+FROM generate_series(1, 40) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    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_ev_state_alt_absorb_group'), E'\n')) AS line WHERE line ~ 'PATTERN';
+                 line                  
+---------------------------------------
+   PATTERN ((a | b) (a | b)+ (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, 40) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A | B) (A | B)+ (A | B))
+    DEFINE A AS v % 2 = 0, B AS v % 2 = 1
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ 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)
+(10 rows)
+
+-- High state count - alternation with plus quantifier
+CREATE VIEW rpr_ev_state_alt_plus AS
+SELECT count(*) OVER w
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    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_ev_state_alt_plus'), 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, 100) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    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
+);');
+                          rpr_explain_filter                           
+-----------------------------------------------------------------------
+ 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)
+(10 rows)
+
+-- Early termination: first ALT branch (A) reaches FIN immediately,
+-- pruning second branch (A B+) before it can accumulate B repetitions.
+CREATE VIEW rpr_ev_state_alt_prune AS
+SELECT count(*) OVER w
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A | A B)+)
+    DEFINE A AS v = 1, B AS v > 1
+);
+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)+) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A | A B)+)
+    DEFINE A AS v = 1, B AS v > 1
+);');
+                          rpr_explain_filter                           
+-----------------------------------------------------------------------
+ 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)
+(9 rows)
+
+-- Nested quantifiers causing state growth
+CREATE VIEW rpr_ev_state_nested_quant AS
+SELECT count(*) OVER w
+FROM generate_series(1, 1000) 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 % 3 = 1, B AS v % 3 = 2
+);
+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)+)+) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 1000) 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 % 3 = 1, B AS v % 3 = 2
+);');
+                           rpr_explain_filter                           
+------------------------------------------------------------------------
+ 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)
+(10 rows)
+
+-- ============================================================
+-- Context Statistics Tests (peak, total, pruned + absorbed/skipped)
+-- ============================================================
+-- Context absorption with unbounded quantifier at start
+CREATE VIEW rpr_ev_ctx_absorb_unbounded 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
+);
+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) 
+(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
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ 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)
+(10 rows)
+
+-- No absorption - bounded quantifier
+CREATE VIEW rpr_ev_ctx_no_absorb 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{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_ev_ctx_no_absorb'), E'\n')) AS line WHERE line ~ 'PATTERN';
+         line          
+-----------------------
+   PATTERN (a{2,4} 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{2,4} B)
+    DEFINE A AS v % 5 <> 0, B AS 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{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)
+(10 rows)
+
+-- Contexts skipped by SKIP PAST LAST ROW
+CREATE VIEW rpr_ev_ctx_skip AS
+SELECT count(*) OVER w
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    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_ev_ctx_skip'), 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, 100) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B C)
+    DEFINE A AS v % 10 = 1, B AS v % 10 = 2, C AS v % 10 = 3
+);');
+                          rpr_explain_filter                           
+-----------------------------------------------------------------------
+ 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)
+(10 rows)
+
+-- High context absorption - unbounded group
+CREATE VIEW rpr_ev_ctx_absorb_group AS
+SELECT count(*) OVER w
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    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_ev_ctx_absorb_group'), 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, 100) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A B)+ C)
+    DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
+);');
+                          rpr_explain_filter                           
+-----------------------------------------------------------------------
+ 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)
+(10 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_ev_ctx_absorb_fixedvar 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_ev_ctx_absorb_fixedvar'), 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
+   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)
+(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
+CREATE VIEW rpr_ev_ctx_absorb_nested 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_ev_ctx_absorb_nested'), 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
+   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)
+(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
+CREATE VIEW rpr_ev_ctx_absorb_deep 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_ev_ctx_absorb_deep'), 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
+   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)
+(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
+-- END chain: END(BC{2}) -> END(A..{2}) -> END(+, ABSORBABLE)
+CREATE VIEW rpr_ev_ctx_absorb_endchain 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_ev_ctx_absorb_endchain'), 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}')+"
+   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)
+(10 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
+   Nav Mark Lookback: 0
+   Nav Mark Lookahead: 0
+   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)
+(11 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
+   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)
+(10 rows)
+
+-- No absorption with compound PREV(FIRST()) (match_start-dependent)
+CREATE VIEW rpr_ev_ctx_no_absorb_compound 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 PREV(FIRST(v), 1) IS NOT NULL
+);
+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 PREV(FIRST(v), 1) IS NOT NULL
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ WindowAgg (actual rows=50.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a+ b
+   Nav Mark Lookback: 0
+   Nav Mark Lookahead: -1
+   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 4/5/4.9), 1 mismatched (len 5/5/5.0)
+   NFA: 0 absorbed, 39 skipped (len 1/4/2.5)
+   ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
+(11 rows)
+
+-- ============================================================
+-- Match Length Statistics Tests
+-- ============================================================
+-- Fixed length matches - all same length
+CREATE VIEW rpr_ev_mlen_fixed AS
+SELECT count(*) OVER w
+FROM rpr_nfa_test
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B C D E)
+    DEFINE
+        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_ev_mlen_fixed'), E'\n')) AS line WHERE line ~ 'PATTERN';
+          line          
+------------------------
+   PATTERN (a b c d e) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM rpr_nfa_test
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B C D E)
+    DEFINE
+        A AS cat = ''A'', B AS cat = ''B'', C AS cat = ''C'',
+        D AS cat = ''D'', E AS cat = ''E''
+);');
+                        rpr_explain_filter                         
+-------------------------------------------------------------------
+ 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)
+(10 rows)
+
+-- Variable length matches - min/max/avg differ
+CREATE VIEW rpr_ev_mlen_variable AS
+SELECT count(*) OVER w
+FROM generate_series(1, 100) 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 % 10 <> 0, B AS v % 10 = 0
+);
+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) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 100) 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 % 10 <> 0, B AS v % 10 = 0
+);');
+                          rpr_explain_filter                           
+-----------------------------------------------------------------------
+ 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)
+(10 rows)
+
+-- Very long matches
+CREATE VIEW rpr_ev_mlen_long AS
+SELECT count(*) OVER w
+FROM generate_series(1, 200) 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 <= 195, B AS v > 195
+);
+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) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 200) 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 <= 195, B AS v > 195
+);');
+                          rpr_explain_filter                           
+-----------------------------------------------------------------------
+ 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)
+(10 rows)
+
+-- Uniform match length with mismatches from gap rows (v%20 = 11..15)
+CREATE VIEW rpr_ev_mlen_with_mismatch AS
+SELECT count(*) OVER w
+FROM generate_series(1, 100) 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 % 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_ev_mlen_with_mismatch'), 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, 100) 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 % 20 <> 0) AND (v % 20 <= 10 OR v % 20 > 15),
+        B AS v % 20 = 0
+);');
+                          rpr_explain_filter                           
+-----------------------------------------------------------------------
+ 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)
+(10 rows)
+
+-- ============================================================
+-- 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 VIEW rpr_ev_mlen_no_mismatch AS
+SELECT count(*) OVER w
+FROM (
+    SELECT v,
+           CASE WHEN v % 10 IN (1,2,3) THEN 'A'
+                WHEN v % 10 IN (4,5) THEN 'B'
+                WHEN v % 10 = 6 THEN 'C'
+                ELSE 'X' END AS cat
+    FROM generate_series(1, 100) AS s(v)
+) t
+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_ev_mlen_no_mismatch'), 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 (
+    SELECT v,
+           CASE WHEN v % 10 IN (1,2,3) THEN ''A''
+                WHEN v % 10 IN (4,5) THEN ''B''
+                WHEN v % 10 = 6 THEN ''C''
+                ELSE ''X'' END AS cat
+    FROM generate_series(1, 100) AS s(v)
+) t
+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''
+);');
+                          rpr_explain_filter                           
+-----------------------------------------------------------------------
+ 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)
+(10 rows)
+
+-- Long partial matches that fail
+CREATE VIEW rpr_ev_mlen_long_partial AS
+SELECT count(*) OVER w
+FROM (
+    SELECT i AS v,
+           CASE
+               WHEN i <= 20 THEN 'A'
+               WHEN i <= 25 THEN 'B'
+               WHEN i = 26 THEN 'X'  -- breaks the pattern
+               WHEN i <= 50 THEN 'A'
+               WHEN i <= 55 THEN 'B'
+               WHEN i = 56 THEN 'C'  -- completes pattern
+               ELSE 'Y'
+           END AS cat
+    FROM generate_series(1, 60) i
+) t
+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_ev_mlen_long_partial'), 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 (
+    SELECT i AS v,
+           CASE
+               WHEN i <= 20 THEN ''A''
+               WHEN i <= 25 THEN ''B''
+               WHEN i = 26 THEN ''X''  -- breaks the pattern
+               WHEN i <= 50 THEN ''A''
+               WHEN i <= 55 THEN ''B''
+               WHEN i = 56 THEN ''C''  -- completes pattern
+               ELSE ''Y''
+           END AS cat
+    FROM generate_series(1, 60) i
+) t
+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''
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ 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)
+(10 rows)
+
+-- ============================================================
+-- JSON Format Tests
+-- ============================================================
+-- JSON format output with all statistics
+CREATE VIEW rpr_ev_json_basic 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 % 3 = 1, B AS v % 3 = 2
+);
+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+) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF, FORMAT JSON)
+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 % 3 = 1, B AS v % 3 = 2
+)');
+                             rpr_explain_filter                             
+----------------------------------------------------------------------------
+ [                                                                         +
+   {                                                                       +
+     "Plan": {                                                             +
+       "Node Type": "WindowAgg",                                           +
+       "Parallel Aware": false,                                            +
+       "Async Capable": false,                                             +
+       "Actual Rows": 50.00,                                               +
+       "Actual Loops": 1,                                                  +
+       "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,                                               +
+       "NFA States Total": 85,                                             +
+       "NFA States Merged": 0,                                             +
+       "NFA Contexts Peak": 3,                                             +
+       "NFA Contexts Total": 51,                                           +
+       "NFA Contexts Absorbed": 0,                                         +
+       "NFA Contexts Skipped": 17,                                         +
+       "NFA Contexts Pruned": 16,                                          +
+       "NFA Matched": 17,                                                  +
+       "NFA Mismatched": 0,                                                +
+       "NFA Match Length Min": 2,                                          +
+       "NFA Match Length Max": 2,                                          +
+       "NFA Match Length Avg": 2.0,                                        +
+       "NFA Skipped Length Min": 1,                                        +
+       "NFA Skipped Length Max": 1,                                        +
+       "NFA Skipped Length Avg": 1.0,                                      +
+       "Plans": [                                                          +
+         {                                                                 +
+           "Node Type": "Function Scan",                                   +
+           "Parent Relationship": "Outer",                                 +
+           "Parallel Aware": false,                                        +
+           "Async Capable": false,                                         +
+           "Function Name": "generate_series",                             +
+           "Alias": "s",                                                   +
+           "Actual Rows": 50.00,                                           +
+           "Actual Loops": 1,                                              +
+           "Disabled": false                                               +
+         }                                                                 +
+       ]                                                                   +
+     },                                                                    +
+     "Triggers": [                                                         +
+     ]                                                                     +
+   }                                                                       +
+ ]
+(1 row)
+
+-- JSON format with match length statistics
+CREATE VIEW rpr_ev_json_matchlen AS
+SELECT count(*) OVER w
+FROM generate_series(1, 100) 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 % 10 <> 0, B AS v % 10 = 0
+);
+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) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF, FORMAT JSON)
+SELECT count(*) OVER w
+FROM generate_series(1, 100) 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 % 10 <> 0, B AS v % 10 = 0
+)');
+                             rpr_explain_filter                             
+----------------------------------------------------------------------------
+ [                                                                         +
+   {                                                                       +
+     "Plan": {                                                             +
+       "Node Type": "WindowAgg",                                           +
+       "Parallel Aware": false,                                            +
+       "Async Capable": false,                                             +
+       "Actual Rows": 100.00,                                              +
+       "Actual Loops": 1,                                                  +
+       "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,                                               +
+       "NFA States Total": 191,                                            +
+       "NFA States Merged": 0,                                             +
+       "NFA Contexts Peak": 2,                                             +
+       "NFA Contexts Total": 101,                                          +
+       "NFA Contexts Absorbed": 80,                                        +
+       "NFA Contexts Skipped": 10,                                         +
+       "NFA Contexts Pruned": 0,                                           +
+       "NFA Matched": 10,                                                  +
+       "NFA Mismatched": 0,                                                +
+       "NFA Match Length Min": 10,                                         +
+       "NFA Match Length Max": 10,                                         +
+       "NFA Match Length Avg": 10.0,                                       +
+       "NFA Absorbed Length Min": 1,                                       +
+       "NFA Absorbed Length Max": 1,                                       +
+       "NFA Absorbed Length Avg": 1.0,                                     +
+       "NFA Skipped Length Min": 1,                                        +
+       "NFA Skipped Length Max": 1,                                        +
+       "NFA Skipped Length Avg": 1.0,                                      +
+       "Plans": [                                                          +
+         {                                                                 +
+           "Node Type": "Function Scan",                                   +
+           "Parent Relationship": "Outer",                                 +
+           "Parallel Aware": false,                                        +
+           "Async Capable": false,                                         +
+           "Function Name": "generate_series",                             +
+           "Alias": "s",                                                   +
+           "Actual Rows": 100.00,                                          +
+           "Actual Loops": 1,                                              +
+           "Disabled": false                                               +
+         }                                                                 +
+       ]                                                                   +
+     },                                                                    +
+     "Triggers": [                                                         +
+     ]                                                                     +
+   }                                                                       +
+ ]
+(1 row)
+
+-- JSON format with mismatch statistics
+-- Pattern A B C expects 1,2,3 but gets 1,2,4 twice causing mismatches
+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 (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    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_ev_json_mismatch'), 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, FORMAT JSON)
+SELECT count(*) OVER w
+FROM (VALUES (1),(2),(4), (1),(2),(4), (1),(2),(3)) AS t(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B C)
+    DEFINE A AS v = 1, B AS v = 2, C AS v = 3
+)');
+                             rpr_explain_filter                             
+----------------------------------------------------------------------------
+ [                                                                         +
+   {                                                                       +
+     "Plan": {                                                             +
+       "Node Type": "WindowAgg",                                           +
+       "Parallel Aware": false,                                            +
+       "Async Capable": false,                                             +
+       "Actual Rows": 9.00,                                                +
+       "Actual Loops": 1,                                                  +
+       "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,                                               +
+       "NFA States Total": 10,                                             +
+       "NFA States Merged": 0,                                             +
+       "NFA Contexts Peak": 3,                                             +
+       "NFA Contexts Total": 10,                                           +
+       "NFA Contexts Absorbed": 0,                                         +
+       "NFA Contexts Skipped": 1,                                          +
+       "NFA Contexts Pruned": 5,                                           +
+       "NFA Matched": 1,                                                   +
+       "NFA Mismatched": 2,                                                +
+       "NFA Match Length Min": 3,                                          +
+       "NFA Match Length Max": 3,                                          +
+       "NFA Match Length Avg": 3.0,                                        +
+       "NFA Mismatch Length Min": 3,                                       +
+       "NFA Mismatch Length Max": 3,                                       +
+       "NFA Mismatch Length Avg": 3.0,                                     +
+       "NFA Skipped Length Min": 1,                                        +
+       "NFA Skipped Length Max": 1,                                        +
+       "NFA Skipped Length Avg": 1.0,                                      +
+       "Plans": [                                                          +
+         {                                                                 +
+           "Node Type": "Values Scan",                                     +
+           "Parent Relationship": "Outer",                                 +
+           "Parallel Aware": false,                                        +
+           "Async Capable": false,                                         +
+           "Alias": "*VALUES*",                                            +
+           "Actual Rows": 9.00,                                            +
+           "Actual Loops": 1,                                              +
+           "Disabled": false                                               +
+         }                                                                 +
+       ]                                                                   +
+     },                                                                    +
+     "Triggers": [                                                         +
+     ]                                                                     +
+   }                                                                       +
+ ]
+(1 row)
+
+-- JSON format with skipped context statistics
+-- Alternation pattern with SKIP PAST LAST ROW causes many contexts to be skipped
+CREATE VIEW rpr_ev_json_skip AS
+SELECT count(*) OVER w
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    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_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)) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF, FORMAT JSON)
+SELECT count(*) OVER w
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    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
+)');
+                             rpr_explain_filter                             
+----------------------------------------------------------------------------
+ [                                                                         +
+   {                                                                       +
+     "Plan": {                                                             +
+       "Node Type": "WindowAgg",                                           +
+       "Parallel Aware": false,                                            +
+       "Async Capable": false,                                             +
+       "Actual Rows": 100.00,                                              +
+       "Actual Loops": 1,                                                  +
+       "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,                                              +
+       "NFA States Total": 995,                                            +
+       "NFA States Merged": 0,                                             +
+       "NFA Contexts Peak": 8,                                             +
+       "NFA Contexts Total": 101,                                          +
+       "NFA Contexts Absorbed": 0,                                         +
+       "NFA Contexts Skipped": 84,                                         +
+       "NFA Contexts Pruned": 1,                                           +
+       "NFA Matched": 12,                                                  +
+       "NFA Mismatched": 3,                                                +
+       "NFA Match Length Min": 8,                                          +
+       "NFA Match Length Max": 8,                                          +
+       "NFA Match Length Avg": 8.0,                                        +
+       "NFA Mismatch Length Min": 2,                                       +
+       "NFA Mismatch Length Max": 4,                                       +
+       "NFA Mismatch Length Avg": 3.0,                                     +
+       "NFA Skipped Length Min": 1,                                        +
+       "NFA Skipped Length Max": 7,                                        +
+       "NFA Skipped Length Avg": 4.0,                                      +
+       "Plans": [                                                          +
+         {                                                                 +
+           "Node Type": "Function Scan",                                   +
+           "Parent Relationship": "Outer",                                 +
+           "Parallel Aware": false,                                        +
+           "Async Capable": false,                                         +
+           "Function Name": "generate_series",                             +
+           "Alias": "s",                                                   +
+           "Actual Rows": 100.00,                                          +
+           "Actual Loops": 1,                                              +
+           "Disabled": false                                               +
+         }                                                                 +
+       ]                                                                   +
+     },                                                                    +
+     "Triggers": [                                                         +
+     ]                                                                     +
+   }                                                                       +
+ ]
+(1 row)
+
+-- ============================================================
+-- XML Format Tests
+-- ============================================================
+-- XML format output
+CREATE VIEW rpr_ev_xml_basic 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_ev_xml_basic'), 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>                                                  +
+       <Nav-Mark-Lookback>0</Nav-Mark-Lookback>                                +
+       <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)
+
+-- ============================================================
+-- Multiple Partitions Tests
+-- ============================================================
+-- Statistics across multiple partitions
+CREATE VIEW rpr_ev_part_multi AS
+SELECT count(*) OVER w
+FROM (
+    SELECT p, v
+    FROM generate_series(1, 3) p,
+         generate_series(1, 30) v
+) t
+WINDOW w AS (
+    PARTITION BY p
+    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
+);
+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) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM (
+    SELECT p, v
+    FROM generate_series(1, 3) p,
+         generate_series(1, 30) v
+) t
+WINDOW w AS (
+    PARTITION BY p
+    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
+);');
+                                 rpr_explain_filter                                 
+------------------------------------------------------------------------------------
+ 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
+   NFA: 18 matched (len 5/5/5.0), 0 mismatched
+   NFA: 54 absorbed (len 1/1/1.0), 18 skipped (len 1/1/1.0)
+   ->  Sort (actual rows=90.00 loops=1)
+         Sort Key: p.p
+         Sort Method: quicksort  Memory: NkB
+         ->  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)
+(15 rows)
+
+-- Different pattern behavior per partition
+CREATE VIEW rpr_ev_part_diff AS
+SELECT count(*) OVER w
+FROM (
+    SELECT
+        CASE WHEN v <= 25 THEN 1 ELSE 2 END AS p,
+        v % 10 AS val
+    FROM generate_series(1, 50) v
+) t
+WINDOW w AS (
+    PARTITION BY p
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A+ B)
+    DEFINE A AS val < 5, B AS val >= 5
+);
+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) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM (
+    SELECT
+        CASE WHEN v <= 25 THEN 1 ELSE 2 END AS p,
+        v % 10 AS val
+    FROM generate_series(1, 50) v
+) t
+WINDOW w AS (
+    PARTITION BY p
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A+ B)
+    DEFINE A AS val < 5, B AS val >= 5
+);');
+                                                    rpr_explain_filter                                                    
+--------------------------------------------------------------------------------------------------------------------------
+ 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
+   NFA: 5 matched (len 5/6/5.8), 0 mismatched
+   NFA: 19 absorbed (len 1/1/1.0), 5 skipped (len 1/1/1.0)
+   ->  Sort (actual rows=50.00 loops=1)
+         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)
+(13 rows)
+
+-- ============================================================
+-- Edge Cases
+-- ============================================================
+-- Empty result set
+CREATE VIEW rpr_ev_edge_empty AS
+SELECT count(*) OVER w
+FROM generate_series(1, 0) 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 = 1, B AS v = 2
+);
+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) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 0) 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 = 1, B AS v = 2
+);');
+                         rpr_explain_filter                          
+---------------------------------------------------------------------
+ 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)
+(5 rows)
+
+-- Single row
+CREATE VIEW rpr_ev_edge_single_row AS
+SELECT count(*) OVER w
+FROM generate_series(1, 1) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A)
+    DEFINE A AS TRUE
+);
+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) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 1) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A)
+    DEFINE A AS TRUE
+);');
+                         rpr_explain_filter                          
+---------------------------------------------------------------------
+ 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)
+(9 rows)
+
+-- Pattern longer than data
+CREATE VIEW rpr_ev_edge_pattern_longer AS
+SELECT count(*) OVER w
+FROM generate_series(1, 5) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B C D E F G H I J)
+    DEFINE
+        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_ev_edge_pattern_longer'), E'\n')) AS line WHERE line ~ 'PATTERN';
+               line               
+----------------------------------
+   PATTERN (a b c d e f g h i j) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 5) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B C D E F G H I J)
+    DEFINE
+        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
+);');
+                         rpr_explain_filter                          
+---------------------------------------------------------------------
+ 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)
+(9 rows)
+
+-- All rows match as single match
+CREATE VIEW rpr_ev_edge_single_match 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+)
+    DEFINE A AS TRUE
+);
+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+) 
+(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+)
+    DEFINE A AS TRUE
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ 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)
+(10 rows)
+
+-- ============================================================
+-- Complex Pattern Tests
+-- ============================================================
+-- Nested groups
+CREATE VIEW rpr_ev_cpx_nested AS
+SELECT count(*) OVER w
+FROM generate_series(1, 60) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    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_ev_cpx_nested'), 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, 60) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (((A B) C)+)
+    DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ 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)
+(10 rows)
+
+-- Multiple alternations
+CREATE VIEW rpr_ev_cpx_multi_alt AS
+SELECT count(*) OVER w
+FROM rpr_nfa_test
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A | B) (C | D | E))
+    DEFINE
+        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_ev_cpx_multi_alt'), E'\n')) AS line WHERE line ~ 'PATTERN';
+               line               
+----------------------------------
+   PATTERN ((a | b) (c | d | e)) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM rpr_nfa_test
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A | B) (C | D | E))
+    DEFINE
+        A AS cat = ''A'', B AS cat = ''B'', C AS cat = ''C'',
+        D AS cat = ''D'', E AS cat = ''E''
+);');
+                        rpr_explain_filter                         
+-------------------------------------------------------------------
+ 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)
+(10 rows)
+
+-- Optional elements
+CREATE VIEW rpr_ev_cpx_optional 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? 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_ev_cpx_optional'), 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, 50) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B? C)
+    DEFINE A AS v % 4 = 1, B AS v % 4 = 2, C AS v % 4 = 3
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ 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)
+(10 rows)
+
+-- Bounded quantifiers
+CREATE VIEW rpr_ev_cpx_bounded AS
+SELECT count(*) OVER w
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    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_ev_cpx_bounded'), E'\n')) AS line WHERE line ~ 'PATTERN';
+         line          
+-----------------------
+   PATTERN (a{2,5} 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, 100) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A{2,5} B)
+    DEFINE A AS v % 10 <> 0, B AS v % 10 = 0
+);');
+                          rpr_explain_filter                           
+-----------------------------------------------------------------------
+ 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)
+(10 rows)
+
+-- Star quantifier
+CREATE VIEW rpr_ev_cpx_star 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* 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_ev_cpx_star'), 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, 50) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    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
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ 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)
+(10 rows)
+
+-- ============================================================
+-- Real-world Pattern Examples
+-- ============================================================
+-- Stock price pattern - V-shape (down then up)
+CREATE VIEW rpr_ev_real_vshape AS
+SELECT count(*) OVER w
+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_ev_real_vshape'), E'\n')) AS line WHERE line ~ 'PATTERN';
+        line        
+--------------------
+   PATTERN (d+ u+) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+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''
+);');
+                        rpr_explain_filter                         
+-------------------------------------------------------------------
+ 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)
+(10 rows)
+
+-- Stock price pattern - peak (up, stable, down)
+CREATE VIEW rpr_ev_real_peak AS
+SELECT count(*) OVER w
+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_ev_real_peak'), E'\n')) AS line WHERE line ~ 'PATTERN';
+         line          
+-----------------------
+   PATTERN (u+ s* d+) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+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''
+);');
+                        rpr_explain_filter                         
+-------------------------------------------------------------------
+ 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)
+(10 rows)
+
+-- Consecutive increasing values (using PREV)
+CREATE VIEW rpr_ev_real_increasing 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{3,})
+    DEFINE A AS v > PREV(v) OR PREV(v) IS NULL
+);
+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,}) 
+(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{3,})
+    DEFINE A AS v > PREV(v) OR PREV(v) IS NULL
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ 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)
+(10 rows)
+
+-- ============================================================
+-- Performance-oriented Tests
+-- ============================================================
+-- Large dataset with simple pattern
+CREATE VIEW rpr_ev_perf_large_simple AS
+SELECT count(*) OVER w
+FROM generate_series(1, 1000) 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_ev_perf_large_simple'), 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, 1000) 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                           
+------------------------------------------------------------------------
+ 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)
+(10 rows)
+
+-- Large dataset with absorption
+CREATE VIEW rpr_ev_perf_large_absorb AS
+SELECT count(*) OVER w
+FROM generate_series(1, 1000) 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 % 100 <> 0, B AS v % 100 = 0
+);
+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) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 1000) 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 % 100 <> 0, B AS v % 100 = 0
+);');
+                           rpr_explain_filter                           
+------------------------------------------------------------------------
+ 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)
+(10 rows)
+
+-- High state merge ratio
+CREATE VIEW rpr_ev_perf_high_merge AS
+SELECT count(*) OVER w
+FROM generate_series(1, 500) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    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_ev_perf_high_merge'), 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, 500) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A | B)+ C)
+    DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
+);');
+                          rpr_explain_filter                           
+-----------------------------------------------------------------------
+ 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)
+(10 rows)
+
+-- ============================================================
+-- INITIAL vs no INITIAL comparison
+-- ============================================================
+-- With INITIAL keyword
+CREATE VIEW rpr_ev_initial_with 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
+    INITIAL
+    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_ev_initial_with'), 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
+    INITIAL
+    PATTERN (A+ B)
+    DEFINE A AS v % 5 <> 0, B AS 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
+   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)
+(10 rows)
+
+-- Without INITIAL keyword (same behavior currently)
+CREATE VIEW rpr_ev_initial_without 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
+);
+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) 
+(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
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ 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)
+(10 rows)
+
+-- ============================================================
+-- Quantifier Variations
+-- ============================================================
+-- Plus quantifier
+CREATE VIEW rpr_ev_quant_plus AS
+SELECT count(*) OVER w
+FROM generate_series(1, 40) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A+)
+    DEFINE A AS v % 4 <> 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_quant_plus'), E'\n')) AS line WHERE line ~ 'PATTERN';
+      line       
+-----------------
+   PATTERN (a+) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 40) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A+)
+    DEFINE A AS v % 4 <> 0
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ 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)
+(10 rows)
+
+-- Star quantifier (zero or more)
+CREATE VIEW rpr_ev_quant_star AS
+SELECT count(*) OVER w
+FROM generate_series(1, 40) 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 % 4 IN (1, 2), B AS v % 4 = 3
+);
+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) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 40) 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 % 4 IN (1, 2), B AS v % 4 = 3
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ 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)
+(10 rows)
+
+-- Question mark (zero or one)
+CREATE VIEW rpr_ev_quant_question AS
+SELECT count(*) OVER w
+FROM generate_series(1, 40) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    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_ev_quant_question'), 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, 40) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A? B C)
+    DEFINE A AS v % 4 = 1, B AS v % 4 = 2, C AS v % 4 = 3
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ 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)
+(10 rows)
+
+-- Exact count {n}
+CREATE VIEW rpr_ev_quant_exact 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{3} B)
+    DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
+);
+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) 
+(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{3} B)
+    DEFINE A AS v % 5 <> 0, B AS 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{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)
+(10 rows)
+
+-- Range {n,m}
+CREATE VIEW rpr_ev_quant_range 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{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_ev_quant_range'), E'\n')) AS line WHERE line ~ 'PATTERN';
+         line          
+-----------------------
+   PATTERN (a{2,4} 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{2,4} B)
+    DEFINE A AS v % 5 <> 0, B AS 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{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)
+(10 rows)
+
+-- At least {n,}
+CREATE VIEW rpr_ev_quant_atleast 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{3,} B)
+    DEFINE A AS v % 10 <> 0, B AS v % 10 = 0
+);
+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) 
+(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{3,} B)
+    DEFINE A AS v % 10 <> 0, B AS v % 10 = 0
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ 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)
+(10 rows)
+
+-- ============================================================
+-- Regression Tests for Statistics Accuracy
+-- ============================================================
+-- Verify state count accuracy
+-- Pattern A+ B with 20 rows should show predictable state behavior
+CREATE VIEW rpr_ev_reg_state_count AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) 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
+);
+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) 
+(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
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A+ B)
+    DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
+);');
+                          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: 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)
+(10 rows)
+
+-- Verify context count with known absorption
+CREATE VIEW rpr_ev_reg_ctx_absorb 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 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_ev_reg_ctx_absorb'), 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, 30) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    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
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ 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)
+(10 rows)
+
+-- Verify match length with fixed-length pattern
+CREATE VIEW rpr_ev_reg_matchlen 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 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_ev_reg_matchlen'), 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, 30) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B C)
+    DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ 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)
+(10 rows)
+
+-- ============================================================
+-- Alternation Pattern Tests
+-- ============================================================
+-- Simple alternation
+CREATE VIEW rpr_ev_alt_simple AS
+SELECT count(*) OVER w
+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_ev_alt_simple'), 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 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''
+);');
+                        rpr_explain_filter                         
+-------------------------------------------------------------------
+ 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)
+(10 rows)
+
+-- Multiple items in alternation
+CREATE VIEW rpr_ev_alt_multi_item AS
+SELECT count(*) OVER w
+FROM rpr_nfa_test
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A | B | C | D) E)
+    DEFINE
+        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_ev_alt_multi_item'), E'\n')) AS line WHERE line ~ 'PATTERN';
+              line              
+--------------------------------
+   PATTERN ((a | b | c | d) e) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM rpr_nfa_test
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A | B | C | D) E)
+    DEFINE
+        A AS cat = ''A'', B AS cat = ''B'', C AS cat = ''C'',
+        D AS cat = ''D'', E AS cat = ''E''
+);');
+                        rpr_explain_filter                         
+-------------------------------------------------------------------
+ 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)
+(10 rows)
+
+-- Alternation with quantifiers
+CREATE VIEW rpr_ev_alt_with_quant 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)+ 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_ev_alt_with_quant'), 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, 50) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A | B)+ C)
+    DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ 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)
+(10 rows)
+
+-- Multiple alternatives (4+)
+CREATE VIEW rpr_ev_alt_four_plus AS
+SELECT count(*) OVER w
+FROM generate_series(1, 100) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    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_ev_alt_four_plus'), E'\n')) AS line WHERE line ~ 'PATTERN';
+              line              
+--------------------------------
+   PATTERN (a | b | c | 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, 100) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    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
+);');
+                          rpr_explain_filter                           
+-----------------------------------------------------------------------
+ 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)
+(9 rows)
+
+-- Alternation at start
+CREATE VIEW rpr_ev_alt_at_start AS
+SELECT count(*) OVER w
+FROM generate_series(1, 60) 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_at_start'), 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, 60) 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=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)
+(10 rows)
+
+-- Multiple sequential alternations
+CREATE VIEW rpr_ev_alt_sequential AS
+SELECT count(*) OVER w
+FROM generate_series(1, 100) 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_sequential'), 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, 100) 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=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)
+(9 rows)
+
+-- Quantified alternatives
+CREATE VIEW rpr_ev_alt_quantified AS
+SELECT count(*) OVER w
+FROM generate_series(1, 60) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    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_ev_alt_quantified'), 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, 60) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A+ | B+) C)
+    DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ 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)
+(10 rows)
+
+-- Alternation at end
+CREATE VIEW rpr_ev_alt_at_end AS
+SELECT count(*) OVER w
+FROM generate_series(1, 60) 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_at_end'), 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, 60) 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=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)
+(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
+CREATE VIEW rpr_ev_alt_nested_start 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))
+    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_ev_alt_nested_start'), E'\n')) AS line WHERE line ~ 'PATTERN';
+              line              
+--------------------------------
+   PATTERN (a ((b | c) 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, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    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
+);');
+                          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)
+   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)
+(9 rows)
+
+-- 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_ev_alt_nested_end 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_nested_end'), 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, 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)
+(9 rows)
+
+-- ============================================================
+-- Group Pattern Tests
+-- ============================================================
+-- Simple group
+CREATE VIEW rpr_ev_grp_simple AS
+SELECT count(*) OVER w
+FROM generate_series(1, 40) 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_ev_grp_simple'), 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, 40) 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                          
+----------------------------------------------------------------------
+ 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)
+(10 rows)
+
+-- Group with bounded quantifier
+CREATE VIEW rpr_ev_grp_bounded AS
+SELECT count(*) OVER w
+FROM generate_series(1, 40) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    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_ev_grp_bounded'), E'\n')) AS line WHERE line ~ 'PATTERN';
+          line           
+-------------------------
+   PATTERN ((a b){2,4}) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 40) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A B){2,4})
+    DEFINE A AS v % 2 = 1, B AS v % 2 = 0
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ 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)
+(10 rows)
+
+-- Nested groups
+CREATE VIEW rpr_ev_grp_nested AS
+SELECT count(*) OVER w
+FROM generate_series(1, 60) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    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_ev_grp_nested'), E'\n')) AS line WHERE line ~ 'PATTERN';
+           line           
+--------------------------
+   PATTERN (((a b){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, 60) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (((A B){2})+)
+    DEFINE A AS v % 2 = 1, B AS v % 2 = 0
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ 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)
+(10 rows)
+
+-- Deep nesting (3+ levels)
+CREATE VIEW rpr_ev_grp_deep AS
+SELECT count(*) OVER w
+FROM generate_series(1, 40) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    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_ev_grp_deep'), 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, 40) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((((A | B)+)+)+)
+    DEFINE A AS v % 2 = 0, B AS v % 2 = 1
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ 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)
+(10 rows)
+
+-- Bounded quantifier on alternation
+CREATE VIEW rpr_ev_grp_bounded_alt AS
+SELECT count(*) OVER w
+FROM generate_series(1, 60) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    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_ev_grp_bounded_alt'), E'\n')) AS line WHERE line ~ 'PATTERN';
+            line             
+-----------------------------
+   PATTERN ((a | b){2,3} 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, 60) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A | B){2,3} C)
+    DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ 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)
+(10 rows)
+
+-- Nested groups with quantifiers
+CREATE VIEW rpr_ev_grp_nested_quant AS
+SELECT count(*) OVER w
+FROM generate_series(1, 60) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    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_ev_grp_nested_quant'), 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, 60) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (((A B)+ C)*)
+    DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ 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)
+(10 rows)
+
+-- Partial nested quantification
+CREATE VIEW rpr_ev_grp_partial_quant AS
+SELECT count(*) OVER w
+FROM generate_series(1, 60) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    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_ev_grp_partial_quant'), 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, 60) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A (B C)+)*)
+    DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ 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)
+(10 rows)
+
+-- ============================================================
+-- Window Function Combinations
+-- ============================================================
+-- count(*) with pattern
+CREATE VIEW rpr_ev_wfn_count 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 % 5 <> 0, B AS v % 5 = 0
+);
+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) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+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 % 5 <> 0, B AS v % 5 = 0
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ 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)
+(10 rows)
+
+-- first_value with pattern
+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 (
+    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
+);
+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) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT first_value(v) 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 % 5 <> 0, B AS v % 5 = 0
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ 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)
+(10 rows)
+
+-- last_value with pattern
+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 (
+    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
+);
+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) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT last_value(v) 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 % 5 <> 0, B AS v % 5 = 0
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ 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)
+(10 rows)
+
+-- Multiple window functions
+CREATE VIEW rpr_ev_wfn_multi AS
+SELECT
+    count(*) OVER w,
+    first_value(v) OVER w,
+    last_value(v) 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 % 5 <> 0, B AS v % 5 = 0
+);
+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) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT
+    count(*) OVER w,
+    first_value(v) OVER w,
+    last_value(v) 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 % 5 <> 0, B AS v % 5 = 0
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ 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)
+(10 rows)
+
+-- ============================================================
+-- DEFINE Expression Variations
+-- ============================================================
+-- Complex boolean expressions
+CREATE VIEW rpr_ev_def_complex_bool 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) AND (v % 3 <> 0),
+        B AS (v % 5 = 0) OR (v % 3 = 0)
+);
+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) 
+(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) AND (v % 3 <> 0),
+        B AS (v % 5 = 0) OR (v % 3 = 0)
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ 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)
+(10 rows)
+
+-- Using PREV function
+CREATE VIEW rpr_ev_def_prev 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 (S U+ D+)
+    DEFINE
+        S AS TRUE,
+        U AS v > PREV(v),
+        D AS v < PREV(v)
+);
+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+) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+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 (S U+ D+)
+    DEFINE
+        S AS TRUE,
+        U AS v > PREV(v),
+        D AS v < PREV(v)
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ 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)
+(9 rows)
+
+-- 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
+FROM (
+    SELECT CASE WHEN v % 5 = 0 THEN NULL ELSE v END AS v
+    FROM generate_series(1, 30) v
+) t
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    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_ev_def_null'), 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 (
+    SELECT CASE WHEN v % 5 = 0 THEN NULL ELSE v END AS v
+    FROM generate_series(1, 30) v
+) t
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A+ B)
+    DEFINE A AS v IS NOT NULL, B AS v IS NULL
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ 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)
+(10 rows)
+
+-- ============================================================
+-- Large Scale Statistics Verification
+-- ============================================================
+-- 500 rows - verify statistics scale correctly
+CREATE VIEW rpr_ev_scale_500rows AS
+SELECT count(*) OVER w
+FROM generate_series(1, 500) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    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_ev_scale_500rows'), 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, 500) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A+ B C)
+    DEFINE A AS v % 10 < 7, B AS v % 10 = 7, C AS v % 10 = 8
+);');
+                          rpr_explain_filter                           
+-----------------------------------------------------------------------
+ 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)
+(10 rows)
+
+-- High match count scenario
+CREATE VIEW rpr_ev_scale_high_match AS
+SELECT count(*) OVER w
+FROM generate_series(1, 500) 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_ev_scale_high_match'), 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, 500) 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                           
+-----------------------------------------------------------------------
+ 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)
+(10 rows)
+
+-- High skip count scenario
+CREATE VIEW rpr_ev_scale_high_skip AS
+SELECT count(*) OVER w
+FROM generate_series(1, 500) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B C D E)
+    DEFINE
+        A AS v % 100 = 1,
+        B AS v % 100 = 2,
+        C AS v % 100 = 3,
+        D AS v % 100 = 4,
+        E AS v % 100 = 5
+);
+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) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 500) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B C D E)
+    DEFINE
+        A AS v % 100 = 1,
+        B AS v % 100 = 2,
+        C AS v % 100 = 3,
+        D AS v % 100 = 4,
+        E AS v % 100 = 5
+);');
+                          rpr_explain_filter                           
+-----------------------------------------------------------------------
+ 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)
+(10 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_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_ev_opt_no_rpr;
+                          QUERY PLAN                          
+--------------------------------------------------------------
+ Subquery Scan on rpr_ev_opt_no_rpr
+   ->  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_ev_opt_with_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
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B+)
+    DEFINE
+        B AS v > PREV(v)
+);
+EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev_opt_with_rpr;
+                                      QUERY PLAN                                      
+--------------------------------------------------------------------------------------
+ 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+
+         Nav Mark Lookback: 1
+         ->  Sort
+               Sort Key: s.v
+               ->  Function Scan on generate_series s
+(8 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_ev_opt_mixed 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_ev_opt_mixed;
+                                        QUERY PLAN                                        
+------------------------------------------------------------------------------------------
+ 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+"
+         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
+(10 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+
+         Nav Mark Lookback: 1
+         ->  Sort
+               Sort Key: s.v
+               ->  Function Scan on generate_series s
+(9 rows)
+
+-- ============================================================
+-- 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
+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;
+-- 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/expected/rpr_integration.out b/src/test/regress/expected/rpr_integration.out
new file mode 100644
index 00000000000..0cc79b75601
--- /dev/null
+++ b/src/test/regress/expected/rpr_integration.out
@@ -0,0 +1,1518 @@
+-- ============================================================
+-- 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
+-- ============================================================
+-- 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 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 ROWS UNBOUNDED PRECEDING)
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_integ
+(5 rows)
+
+-- RPR case: the frame is preserved as specified.
+EXPLAIN (COSTS OFF)
+SELECT row_number() 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+
+   Nav Mark Lookback: 1
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_integ
+(7 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+
+         Nav Mark Lookback: 1
+         ->  Sort
+               Sort Key: rpr_integ.id
+               ->  Seq Scan on rpr_integ
+(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
+-- 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+
+         Nav Mark Lookback: 1
+         ->  Sort
+               Sort Key: id
+               ->  Seq Scan on rpr_integ
+(9 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+
+   Nav Mark Lookback: 1
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_integ
+(7 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+
+   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
+(11 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+"
+         Nav Mark Lookback: 1
+         ->  Seq Scan on rpr_integ
+(6 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+"
+         Nav Mark Lookback: 1
+         ->  Seq Scan on rpr_integ
+(6 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+"
+         Nav Mark Lookback: 0
+         ->  Seq Scan on rpr_integ
+(6 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+
+         Nav Mark Lookback: 1
+         ->  Sort
+               Sort Key: rpr_integ.id
+               ->  Seq Scan on rpr_integ
+(8 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+
+         Nav Mark Lookback: 1
+         ->  Sort
+               Sort Key: rpr_integ.id
+               ->  Seq Scan on rpr_integ
+(9 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+
+         Nav Mark Lookback: 1
+         ->  Sort
+               Output: id, val
+               Sort Key: rpr_integ.id
+               ->  Seq Scan on public.rpr_integ
+                     Output: id, val
+(13 rows)
+
+-- Executing the same query shows the client result is limited to
+-- the two projected columns; "id" and "val" that appeared in the
+-- upper WindowAgg Output line are resjunk-only and do not reach
+-- the client.
+SELECT
+    count(*) OVER w_rpr AS rpr_cnt,
+    count(*) OVER w_normal AS normal_cnt
+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+
+         Nav Mark Lookback: 1
+         ->  Sort
+               Sort Key: id
+               ->  Seq Scan on rpr_integ
+(8 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+
+   Nav Mark Lookback: 1
+   ->  Sort
+         Sort Key: rpr_integ.id
+         ->  Seq Scan on rpr_integ
+(7 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+
+           Nav Mark Lookback: 1
+           ->  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)
+(18 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+
+               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
+(14 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+
+                     Nav Mark Lookback: 1
+                     ->  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
+(17 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;
+-- Custom plan: Nav Mark Lookback resolved to the literal 1.
+SET plan_cache_mode = force_custom_plan;
+EXPLAIN (COSTS OFF) EXECUTE rpr_prev(1);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ 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)
+
+EXECUTE rpr_prev(1);
+ 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);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ 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
+(7 rows)
+
+EXECUTE rpr_prev(1);
+ 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;
+-- ============================================================
+-- 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+
+   Nav Mark Lookback: 1
+   ->  Sort
+         Sort Key: rpr_part.id
+         ->  Append
+               ->  Seq Scan on rpr_part_1
+               ->  Seq Scan on rpr_part_2
+(9 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+
+                     Nav Mark Lookback: 1
+                     ->  Sort
+                           Sort Key: rpr_integ.id
+                           ->  Seq Scan on rpr_integ
+                                 Filter: (id <= o.id)
+(15 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+
+                 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.
+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+
+   Nav Mark Lookback: 1
+   ->  Incremental Sort
+         Sort Key: id, val
+         Presorted Key: id
+         ->  Index Scan using rpr_integ_id_idx on rpr_integ
+(8 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+
+                       Nav Mark Lookback: 1
+                       ->  Sort
+                             Sort Key: i.id
+                             ->  Seq Scan on rpr_integ i
+                                   Filter: (id <= o.id)
+(13 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/expected/rpr_nfa.out b/src/test/regress/expected/rpr_nfa.out
new file mode 100644
index 00000000000..a19b26c3b94
--- /dev/null
+++ b/src/test/regress/expected/rpr_nfa.out
@@ -0,0 +1,4661 @@
+-- ============================================================
+-- RPR NFA Tests
+-- Tests for Row Pattern Recognition NFA Runtime Execution
+-- ============================================================
+--
+-- This test suite validates the NFA (Non-deterministic Finite
+-- Automaton) runtime execution engine in nodeWindowAgg.c,
+-- focusing on update_reduced_frame and related functions.
+--
+-- Test Strategy:
+--   Diagonal pattern style using ARRAY flags to explicitly
+--   control which pattern variables match at each row.
+--
+-- Test Coverage:
+--   Basic NFA Flow (match->absorb->advance)
+--   Absorption Optimization
+--   Context Lifecycle Management
+--   Advance Phase (Epsilon Transitions)
+--   Match Phase (Variable Matching)
+--   Frame Boundary Handling
+--   State Management (Deduplication)
+--   Statistics and Diagnostics
+--   Quantifier Runtime Behavior
+--   Pathological Pattern Protection
+--   Alternation Runtime Behavior
+--   Deep Nested Groups
+--   SKIP Options (Runtime)
+--   INITIAL Mode (Runtime)
+--   Frame Boundary Variations
+--   Special Partition Cases
+--   DEFINE Special Cases
+--   Absorption Dynamic Flags
+--   Zero-Consumption Cycle Detection
+--
+-- Responsibility:
+--   - NFA runtime execution paths
+--   - Context/State lifecycle management
+--   - Runtime boundary conditions and protections
+--
+-- NOT tested here (covered in other files):
+--   - Pattern parsing/optimization (rpr_base.sql)
+--   - EXPLAIN output (rpr_explain.sql)
+--   - PREV/NEXT semantics (rpr.sql)
+-- ============================================================
+-- ============================================================
+-- Basic NFA Flow
+-- ============================================================
+-- Simple sequential pattern
+WITH test_sequential AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['B']),
+        (3, ARRAY['C']),
+        (4, ARRAY['D']),
+        (5, ARRAY['_'])  -- No match
+    ) 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_sequential
+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}   |             |          
+  3 | {C}   |             |          
+  4 | {D}   |             |          
+  5 | {_}   |             |          
+(5 rows)
+
+-- Quantified pattern (A+ B+ C+)
+WITH test_quantified AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),
+        (4, ARRAY['B']),
+        (5, ARRAY['B']),
+        (6, ARRAY['C']),
+        (7, ARRAY['C']),
+        (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
+FROM test_quantified
+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 'A' = ANY(flags),
+        B AS 'B' = ANY(flags),
+        C AS 'C' = ANY(flags)
+);
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {A}   |           1 |         7
+  2 | {A}   |           2 |         7
+  3 | {A}   |           3 |         7
+  4 | {B}   |             |          
+  5 | {B}   |             |          
+  6 | {C}   |             |          
+  7 | {C}   |             |          
+  8 | {_}   |             |          
+(8 rows)
+
+-- Optional pattern (A B? C)
+WITH test_optional AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['C']),  -- B skipped
+        (3, ARRAY['A']),
+        (4, ARRAY['B']),
+        (5, ARRAY['C']),  -- B matched
+        (6, ARRAY['_'])
+    ) 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_optional
+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 '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 | {A}   |           3 |         5
+  4 | {B}   |             |          
+  5 | {C}   |             |          
+  6 | {_}   |             |          
+(6 rows)
+
+-- Alternation pattern (A (B|C) D)
+WITH test_alternation AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['B']),  -- First branch
+        (3, ARRAY['D']),
+        (4, ARRAY['A']),
+        (5, ARRAY['C']),  -- Second branch
+        (6, ARRAY['D']),
+        (7, ARRAY['_'])
+    ) 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_alternation
+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 |         3
+  2 | {B}   |             |          
+  3 | {D}   |             |          
+  4 | {A}   |           4 |         6
+  5 | {C}   |             |          
+  6 | {D}   |             |          
+  7 | {_}   |             |          
+(7 rows)
+
+-- ============================================================
+-- Absorption Optimization
+-- ============================================================
+-- Absorbable pattern (A+)
+WITH test_absorbable AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),
+        (4, ARRAY['A']),
+        (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
+FROM test_absorbable
+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 | {_}   |             |          
+(5 rows)
+
+-- Mixed absorbable/non-absorbable ((A+) | B)
+WITH test_mixed_absorption AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),
+        (4, ARRAY['B']),
+        (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
+FROM test_mixed_absorption
+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 |         3
+  2 | {A}   |           2 |         3
+  3 | {A}   |           3 |         3
+  4 | {B}   |           4 |         4
+  5 | {_}   |             |          
+(5 rows)
+
+-- State coverage (same elemIdx, different count)
+WITH test_state_coverage AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),
+        (4, ARRAY['B']),
+        (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
+FROM test_state_coverage
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A{2,} 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}   |             |          
+  4 | {B}   |             |          
+  5 | {_}   |             |          
+(5 rows)
+
+-- Reluctant pattern (A+?) - not absorbable
+-- Compare with greedy A+ above: reluctant excluded from absorption.
+-- Each context produces minimum match independently.
+WITH test_reluctant_absorption AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),
+        (4, ARRAY['A']),
+        (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
+FROM test_reluctant_absorption
+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 |         1
+  2 | {A}   |           2 |         2
+  3 | {A}   |           3 |         3
+  4 | {A}   |           4 |         4
+  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)
+
+-- 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
+        (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
+-- ============================================================
+-- Multiple overlapping contexts (SKIP TO NEXT ROW)
+WITH test_overlapping_contexts AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),
+        (4, ARRAY['B']),
+        (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
+FROM test_overlapping_contexts
+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 | {_}   |             |          
+(5 rows)
+
+-- Failed context cleanup (early failure)
+WITH test_context_cleanup AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['_']),  -- Pruned at first row
+        (2, ARRAY['A']),
+        (3, ARRAY['_']),  -- Mismatched after row 2
+        (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_context_cleanup
+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 | {_}   |             |          
+  2 | {A}   |             |          
+  3 | {_}   |             |          
+  4 | {A}   |           4 |         5
+  5 | {B}   |             |          
+(5 rows)
+
+-- Partition end (incomplete contexts)
+WITH test_partition_end AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A'])
+        -- Pattern requires B, but partition 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_partition_end
+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}   |             |          
+  2 | {A}   |             |          
+  3 | {A}   |             |          
+(3 rows)
+
+-- Completed context encountered during processing
+-- Pattern (A | B C D): Ctx1 takes long B->C->D path, while Ctx2 takes
+-- short A path and completes first. Next row sees Ctx2
+-- with states=NULL and skips it.
+WITH test_completed_ctx AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['B', '_']),
+        (2, ARRAY['C', 'A']),
+        (3, ARRAY['D', '_']),
+        (4, ARRAY['_', '_'])
+    ) 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_completed_ctx
+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 | {B,_} |           1 |         3
+  2 | {C,A} |           2 |         2
+  3 | {D,_} |             |          
+  4 | {_,_} |             |          
+(4 rows)
+
+-- Reluctant context lifecycle (A+? B with SKIP TO NEXT ROW)
+-- A+? exits early but if B not available, falls back to loop.
+-- Contexts not absorbed (reluctant), so multiple survive.
+WITH test_reluctant_context AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['B']),
+        (4, ARRAY['_'])
+    ) 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_context
+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 |         3
+  2 | {A}   |           2 |         3
+  3 | {B}   |             |          
+  4 | {_}   |             |          
+(4 rows)
+
+-- ============================================================
+-- Advance Phase (Epsilon Transitions)
+-- ============================================================
+-- Nested groups ((A B)+)
+WITH test_nested_groups AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['B']),
+        (3, ARRAY['A']),
+        (4, ARRAY['B']),
+        (5, ARRAY['A']),
+        (6, ARRAY['B']),
+        (7, ARRAY['_'])
+    ) 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_groups
+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 |         6
+  2 | {B}   |             |          
+  3 | {A}   |           3 |         6
+  4 | {B}   |             |          
+  5 | {A}   |           5 |         6
+  6 | {B}   |             |          
+  7 | {_}   |             |          
+(7 rows)
+
+-- Multiple alternation branches (A (B|C|D) E)
+WITH test_multi_alt AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['B']),
+        (3, ARRAY['E']),
+        (4, ARRAY['A']),
+        (5, ARRAY['C']),
+        (6, ARRAY['E']),
+        (7, ARRAY['A']),
+        (8, ARRAY['D']),
+        (9, 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_alt
+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)
+    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 | {E}   |             |          
+  4 | {A}   |           4 |         6
+  5 | {C}   |             |          
+  6 | {E}   |             |          
+  7 | {A}   |           7 |         9
+  8 | {D}   |             |          
+  9 | {E}   |             |          
+(9 rows)
+
+-- Optional VAR at start (A? B C)
+WITH test_optional_var AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['B']),  -- A skipped
+        (2, ARRAY['C']),
+        (3, ARRAY['A']),  -- A matched
+        (4, ARRAY['B']),
+        (5, 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_optional_var
+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 'A' = ANY(flags),
+        B AS 'B' = ANY(flags),
+        C AS 'C' = ANY(flags)
+);
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {B}   |           1 |         2
+  2 | {C}   |             |          
+  3 | {A}   |           3 |         5
+  4 | {B}   |           4 |         5
+  5 | {C}   |             |          
+(5 rows)
+
+-- Nested alternation ((A|B) (C|D))
+WITH test_nested_alt AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['C']),  -- A C
+        (3, ARRAY['A']),
+        (4, ARRAY['D']),  -- A D
+        (5, ARRAY['B']),
+        (6, ARRAY['C']),  -- B C
+        (7, ARRAY['B']),
+        (8, ARRAY['D'])   -- B 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_nested_alt
+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 |         2
+  2 | {C}   |             |          
+  3 | {A}   |           3 |         4
+  4 | {D}   |             |          
+  5 | {B}   |           5 |         6
+  6 | {C}   |             |          
+  7 | {B}   |           7 |         8
+  8 | {D}   |             |          
+(8 rows)
+
+-- Mixed greedy/reluctant sequence: A+? B+ (reluctant A, greedy B)
+-- A exits as early as possible, B consumes the rest greedily
+WITH test_mixed_reluctant AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A','B']),
+        (4, ARRAY['B']),
+        (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_mixed_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 |         5
+  2 | {A}   |             |          
+  3 | {A,B} |             |          
+  4 | {B}   |             |          
+  5 | {B}   |             |          
+(5 rows)
+
+-- 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).
+WITH test_optional_reluctant AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['B']),
+        (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_optional_reluctant
+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}   |             |          
+(3 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 (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A','B']),
+        (3, ARRAY['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_greedy_then_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,B} |             |          
+  3 | {B}   |             |          
+  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
+-- ============================================================
+-- Simple VAR with END next (A B C all min=max=1)
+WITH test_simple_var AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['B']),
+        (3, ARRAY['C']),
+        (4, ARRAY['_'])
+    ) 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_simple_var
+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 '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 | {_}   |             |          
+(4 rows)
+
+-- VAR max exceeded (A{2,3})
+WITH test_max_exceeded AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),  -- Max = 3
+        (4, ARRAY['A']),  -- Exceeds max, state removed
+        (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_max_exceeded
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A{2,3} B)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {A}   |             |          
+  2 | {A}   |           2 |         5
+  3 | {A}   |           3 |         5
+  4 | {A}   |             |          
+  5 | {B}   |             |          
+(5 rows)
+
+-- Non-matching VAR (DEFINE false)
+WITH test_non_matching AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['_']),  -- B not matched (DEFINE false)
+        (3, ARRAY['A']),
+        (4, ARRAY['B']),  -- B matched
+        (5, 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_non_matching
+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 'A' = ANY(flags),
+        B AS 'B' = ANY(flags),
+        C AS 'C' = ANY(flags)
+);
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {A}   |             |          
+  2 | {_}   |             |          
+  3 | {A}   |           3 |         5
+  4 | {B}   |             |          
+  5 | {C}   |             |          
+(5 rows)
+
+-- ============================================================
+-- Frame Boundary Handling
+-- ============================================================
+-- Limited frame (ROWS BETWEEN CURRENT ROW AND 3 FOLLOWING)
+WITH test_limited_frame AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),
+        (4, ARRAY['B']),  -- Within 3 FOLLOWING
+        (5, ARRAY['B']),  -- Beyond 3 FOLLOWING from row 1
+        (6, ARRAY['_'])
+    ) 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_limited_frame
+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 | {B}   |             |          
+  6 | {_}   |             |          
+(6 rows)
+
+-- Unbounded frame (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+WITH test_unbounded_frame AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),
+        (4, ARRAY['A']),
+        (5, ARRAY['A']),
+        (6, ARRAY['B'])  -- Far from start, but unbounded
+    ) 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_unbounded_frame
+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 |         6
+  2 | {A}   |           2 |         6
+  3 | {A}   |           3 |         6
+  4 | {A}   |           4 |         6
+  5 | {A}   |           5 |         6
+  6 | {B}   |             |          
+(6 rows)
+
+-- Match exceeds frame boundary
+WITH test_frame_exceeded AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A'])
+        -- Frame ends at row 3 (2 FOLLOWING), B never appears
+    ) 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_exceeded
+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}   |             |          
+  3 | {A}   |             |          
+(3 rows)
+
+-- Frame boundary forced mismatch
+-- Limited frame with enough rows so that a context's frame boundary
+-- is exceeded while still processing.
+WITH test_frame_boundary 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_frame_boundary
+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}   |             |          
+  3 | {A}   |             |          
+  4 | {A}   |           4 |         6
+  5 | {A}   |           5 |         6
+  6 | {B}   |             |          
+(6 rows)
+
+-- Reluctant with limited frame (A+? B with 2 FOLLOWING)
+-- Reluctant exits early, B must be within frame boundary
+WITH test_reluctant_frame AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['B']),
+        (4, ARRAY['_'])
+    ) 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_frame
+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}   |           1 |         3
+  2 | {A}   |           2 |         3
+  3 | {B}   |             |          
+  4 | {_}   |             |          
+(4 rows)
+
+-- ============================================================
+-- State Management
+-- ============================================================
+-- Duplicate state creation
+WITH test_duplicate_states AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A', 'B']),  -- Both A and B match (creates duplicate states via different paths)
+        (2, ARRAY['C', '_']),
+        (3, 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_duplicate_states
+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,B} |           1 |         3
+  2 | {C,_} |             |          
+  3 | {D,_} |             |          
+(3 rows)
+
+-- Reluctant duplicate state handling
+-- (A+? | B+?) creates exit and loop states; exit paths may converge
+WITH test_reluctant_dedup AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A','B']),
+        (2, ARRAY['A','B']),
+        (3, ARRAY['_'])
+    ) 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_dedup
+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,B} |           1 |         1
+  2 | {A,B} |           2 |         2
+  3 | {_}   |             |          
+(3 rows)
+
+-- Large pattern (stress free list)
+WITH test_large_pattern AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['B']),
+        (3, ARRAY['C']),
+        (4, ARRAY['D']),
+        (5, ARRAY['E']),
+        (6, ARRAY['F']),
+        (7, ARRAY['G']),
+        (8, ARRAY['H']),
+        (9, ARRAY['I']),
+        (10, ARRAY['J'])
+    ) 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_large_pattern
+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 F G H I J)
+    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),
+        G AS 'G' = ANY(flags),
+        H AS 'H' = ANY(flags),
+        I AS 'I' = ANY(flags),
+        J AS 'J' = ANY(flags)
+);
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {A}   |           1 |        10
+  2 | {B}   |             |          
+  3 | {C}   |             |          
+  4 | {D}   |             |          
+  5 | {E}   |             |          
+  6 | {F}   |             |          
+  7 | {G}   |             |          
+  8 | {H}   |             |          
+  9 | {I}   |             |          
+ 10 | {J}   |             |          
+(10 rows)
+
+-- Reduced frame map reallocation (> 1024 rows)
+WITH test_map_realloc AS (
+    SELECT id, CASE WHEN id % 2 = 1 THEN ARRAY['A'] ELSE ARRAY['B'] END AS flags
+    FROM generate_series(1, 1100) AS id
+)
+SELECT count(*), min(match_start), max(match_end)
+FROM (
+    SELECT id, flags,
+           first_value(id) OVER w AS match_start,
+           last_value(id) OVER w AS match_end
+    FROM test_map_realloc
+    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)
+    )
+) sub;
+ count | min | max  
+-------+-----+------
+  1100 |   1 | 1100
+(1 row)
+
+-- ============================================================
+-- Statistics and Diagnostics
+-- ============================================================
+-- Matched contexts
+WITH test_matched AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['B']),
+        (3, ARRAY['A']),
+        (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_matched
+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 |         2
+  2 | {B}   |             |          
+  3 | {A}   |           3 |         4
+  4 | {B}   |             |          
+(4 rows)
+
+-- Pruned contexts (failed at first row)
+WITH test_pruned AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['_']),  -- Pruned
+        (2, ARRAY['_']),  -- Pruned
+        (3, ARRAY['A']),
+        (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_pruned
+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 | {_}   |             |          
+  2 | {_}   |             |          
+  3 | {A}   |           3 |         4
+  4 | {B}   |             |          
+(4 rows)
+
+-- Mismatched contexts (failed after multiple rows)
+WITH test_mismatched AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['_']),  -- Mismatched after 2 rows
+        (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_mismatched
+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}   |             |          
+  2 | {A}   |             |          
+  3 | {_}   |             |          
+  4 | {A}   |           4 |         5
+  5 | {B}   |             |          
+(5 rows)
+
+-- Reluctant not absorbed (A+? with SKIP TO NEXT ROW)
+-- Compare with greedy A+ below: reluctant is not absorbable,
+-- so all contexts survive independently.
+WITH test_reluctant_stats AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),
+        (4, ARRAY['A']),
+        (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
+FROM test_reluctant_stats
+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 |         1
+  2 | {A}   |           2 |         2
+  3 | {A}   |           3 |         3
+  4 | {A}   |           4 |         4
+  5 | {_}   |             |          
+(5 rows)
+
+-- Absorbed contexts
+WITH test_absorbed AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),
+        (4, ARRAY['A']),
+        (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
+FROM test_absorbed
+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 | {_}   |             |          
+(5 rows)
+
+-- Skipped contexts (SKIP TO NEXT ROW)
+WITH test_skipped AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),
+        (4, ARRAY['B'])  -- Completes match starting at row 1
+    ) 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_skipped
+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}   |             |          
+(4 rows)
+
+-- ============================================================
+-- Quantifier Runtime Behavior
+-- ============================================================
+-- Large count handling (A{100})
+WITH test_large_count AS (
+    SELECT i AS id, ARRAY['A'] AS flags
+    FROM generate_series(1, 105) i
+)
+SELECT id, flags,
+       first_value(id) OVER w AS match_start,
+       last_value(id) OVER w AS match_end
+FROM test_large_count
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A{100})
+    DEFINE
+        A AS 'A' = ANY(flags)
+);
+ id  | flags | match_start | match_end 
+-----+-------+-------------+-----------
+   1 | {A}   |           1 |       100
+   2 | {A}   |           2 |       101
+   3 | {A}   |           3 |       102
+   4 | {A}   |           4 |       103
+   5 | {A}   |           5 |       104
+   6 | {A}   |           6 |       105
+   7 | {A}   |             |          
+   8 | {A}   |             |          
+   9 | {A}   |             |          
+  10 | {A}   |             |          
+  11 | {A}   |             |          
+  12 | {A}   |             |          
+  13 | {A}   |             |          
+  14 | {A}   |             |          
+  15 | {A}   |             |          
+  16 | {A}   |             |          
+  17 | {A}   |             |          
+  18 | {A}   |             |          
+  19 | {A}   |             |          
+  20 | {A}   |             |          
+  21 | {A}   |             |          
+  22 | {A}   |             |          
+  23 | {A}   |             |          
+  24 | {A}   |             |          
+  25 | {A}   |             |          
+  26 | {A}   |             |          
+  27 | {A}   |             |          
+  28 | {A}   |             |          
+  29 | {A}   |             |          
+  30 | {A}   |             |          
+  31 | {A}   |             |          
+  32 | {A}   |             |          
+  33 | {A}   |             |          
+  34 | {A}   |             |          
+  35 | {A}   |             |          
+  36 | {A}   |             |          
+  37 | {A}   |             |          
+  38 | {A}   |             |          
+  39 | {A}   |             |          
+  40 | {A}   |             |          
+  41 | {A}   |             |          
+  42 | {A}   |             |          
+  43 | {A}   |             |          
+  44 | {A}   |             |          
+  45 | {A}   |             |          
+  46 | {A}   |             |          
+  47 | {A}   |             |          
+  48 | {A}   |             |          
+  49 | {A}   |             |          
+  50 | {A}   |             |          
+  51 | {A}   |             |          
+  52 | {A}   |             |          
+  53 | {A}   |             |          
+  54 | {A}   |             |          
+  55 | {A}   |             |          
+  56 | {A}   |             |          
+  57 | {A}   |             |          
+  58 | {A}   |             |          
+  59 | {A}   |             |          
+  60 | {A}   |             |          
+  61 | {A}   |             |          
+  62 | {A}   |             |          
+  63 | {A}   |             |          
+  64 | {A}   |             |          
+  65 | {A}   |             |          
+  66 | {A}   |             |          
+  67 | {A}   |             |          
+  68 | {A}   |             |          
+  69 | {A}   |             |          
+  70 | {A}   |             |          
+  71 | {A}   |             |          
+  72 | {A}   |             |          
+  73 | {A}   |             |          
+  74 | {A}   |             |          
+  75 | {A}   |             |          
+  76 | {A}   |             |          
+  77 | {A}   |             |          
+  78 | {A}   |             |          
+  79 | {A}   |             |          
+  80 | {A}   |             |          
+  81 | {A}   |             |          
+  82 | {A}   |             |          
+  83 | {A}   |             |          
+  84 | {A}   |             |          
+  85 | {A}   |             |          
+  86 | {A}   |             |          
+  87 | {A}   |             |          
+  88 | {A}   |             |          
+  89 | {A}   |             |          
+  90 | {A}   |             |          
+  91 | {A}   |             |          
+  92 | {A}   |             |          
+  93 | {A}   |             |          
+  94 | {A}   |             |          
+  95 | {A}   |             |          
+  96 | {A}   |             |          
+  97 | {A}   |             |          
+  98 | {A}   |             |          
+  99 | {A}   |             |          
+ 100 | {A}   |             |          
+ 101 | {A}   |             |          
+ 102 | {A}   |             |          
+ 103 | {A}   |             |          
+ 104 | {A}   |             |          
+ 105 | {A}   |             |          
+(105 rows)
+
+-- Unlimited quantifier (A{10,})
+WITH test_unlimited AS (
+    SELECT i AS id, ARRAY['A'] AS flags
+    FROM generate_series(1, 15) i
+    UNION ALL
+    SELECT 16, ARRAY['B']
+)
+SELECT id, flags,
+       first_value(id) OVER w AS match_start,
+       last_value(id) OVER w AS match_end
+FROM test_unlimited
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A{10,} B)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {A}   |           1 |        16
+  2 | {A}   |           2 |        16
+  3 | {A}   |           3 |        16
+  4 | {A}   |           4 |        16
+  5 | {A}   |           5 |        16
+  6 | {A}   |           6 |        16
+  7 | {A}   |             |          
+  8 | {A}   |             |          
+  9 | {A}   |             |          
+ 10 | {A}   |             |          
+ 11 | {A}   |             |          
+ 12 | {A}   |             |          
+ 13 | {A}   |             |          
+ 14 | {A}   |             |          
+ 15 | {A}   |             |          
+ 16 | {B}   |             |          
+(16 rows)
+
+-- Min boundary (A{3,5})
+WITH test_min_boundary AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),  -- Min=3 reached, exit path available
+        (4, ARRAY['B']),  -- Match ends at min
+        (5, ARRAY['A']),
+        (6, ARRAY['A']),
+        (7, ARRAY['A']),
+        (8, ARRAY['A']),
+        (9, ARRAY['A']),  -- Count=5, max reached
+        (10, ARRAY['B'])  -- Match ends at max
+    ) 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_min_boundary
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT 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 |         4
+  2 | {A}   |             |          
+  3 | {A}   |             |          
+  4 | {B}   |             |          
+  5 | {A}   |           5 |        10
+  6 | {A}   |           6 |        10
+  7 | {A}   |           7 |        10
+  8 | {A}   |             |          
+  9 | {A}   |             |          
+ 10 | {B}   |             |          
+(10 rows)
+
+-- Max boundary exceeded (A{3,5})
+WITH test_max_boundary AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),
+        (4, ARRAY['A']),
+        (5, ARRAY['A']),
+        (6, ARRAY['A']),  -- Count=6 > max=5, row 1 context removed
+        (7, ARRAY['B'])   -- Row 1 context: no match (exceeded max)
+    ) 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_max_boundary
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT 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}   |             |          
+  2 | {A}   |           2 |         7
+  3 | {A}   |           3 |         7
+  4 | {A}   |           4 |         7
+  5 | {A}   |             |          
+  6 | {A}   |             |          
+  7 | {B}   |             |          
+(7 rows)
+
+-- Greedy vs reluctant: A+ matches all rows, A+? matches minimum
+WITH test_greedy_vs_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 test_greedy_vs_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 |         4
+  2 | {A,_} |             |          
+  3 | {A,B} |             |          
+  4 | {B,_} |             |          
+(4 rows)
+
+-- Same data, reluctant A+? exits at row 3 where B is first available
+WITH test_greedy_vs_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 test_greedy_vs_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)
+
+-- Reluctant group: (A B)+? matches minimum 1 iteration
+WITH test_reluctant_group AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['B']),
+        (3, ARRAY['A']),
+        (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_group
+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 |         2
+  2 | {B}   |             |          
+  3 | {A}   |           3 |         4
+  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
+-- ============================================================
+-- Complex nested nullable ((A* B*)*) - Runtime protection
+WITH test_complex_nested AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['B']),
+        (4, ARRAY['B']),
+        (5, 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_complex_nested
+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}   |           3 |         4
+  4 | {B}   |           4 |         4
+  5 | {C}   |             |          
+(5 rows)
+
+-- Nested nullable with quantifier ((A{0,3})*)
+WITH test_nested_quantifier AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),
+        (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_nested_quantifier
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A{0,3})*)
+    DEFINE
+        A AS 'A' = ANY(flags)
+);
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {A}   |           1 |         3
+  2 | {A}   |           2 |         3
+  3 | {A}   |           3 |         3
+  4 | {B}   |             |          
+(4 rows)
+
+-- Reluctant nullable: A*? (prefers 0 matches)
+-- A*? always takes skip path (0 iterations preferred)
+WITH test_reluctant_nullable AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),
+        (4, ARRAY['_'])
+    ) 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_nullable
+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}   |             |          
+  2 | {A}   |             |          
+  3 | {A}   |             |          
+  4 | {_}   |             |          
+(4 rows)
+
+-- ============================================================
+-- Alternation Runtime Behavior
+-- ============================================================
+-- Multi-branch alternation (A (B|C|D|E) F)
+WITH test_multi_branch AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['B']),
+        (3, ARRAY['F']),
+        (4, ARRAY['A']),
+        (5, ARRAY['C']),
+        (6, ARRAY['F']),
+        (7, ARRAY['A']),
+        (8, ARRAY['D']),
+        (9, ARRAY['F']),
+        (10, ARRAY['A']),
+        (11, ARRAY['E']),
+        (12, 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_multi_branch
+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) 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 |         3
+  2 | {B}   |             |          
+  3 | {F}   |             |          
+  4 | {A}   |           4 |         6
+  5 | {C}   |             |          
+  6 | {F}   |             |          
+  7 | {A}   |           7 |         9
+  8 | {D}   |             |          
+  9 | {F}   |             |          
+ 10 | {A}   |          10 |        12
+ 11 | {E}   |             |          
+ 12 | {F}   |             |          
+(12 rows)
+
+-- Alternation with quantifiers (A+ | B+ | C+)
+WITH test_alt_quantifiers AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),
+        (4, ARRAY['B']),
+        (5, ARRAY['B']),
+        (6, ARRAY['C']),
+        (7, ARRAY['C']),
+        (8, ARRAY['C']),
+        (9, 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_alt_quantifiers
+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 'A' = ANY(flags),
+        B AS 'B' = ANY(flags),
+        C AS 'C' = ANY(flags)
+);
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {A}   |           1 |         3
+  2 | {A}   |           2 |         3
+  3 | {A}   |           3 |         3
+  4 | {B}   |           4 |         5
+  5 | {B}   |           5 |         5
+  6 | {C}   |           6 |         9
+  7 | {C}   |           7 |         9
+  8 | {C}   |           8 |         9
+  9 | {C}   |           9 |         9
+(9 rows)
+
+-- altPriority replacement (A B C | D)
+-- D branch (higher altPriority) matches first at row 1,
+-- then A B C branch (lower altPriority) replaces it at row 3.
+WITH test_alt_replace AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A', 'D']),
+        (2, ARRAY['B', '_']),
+        (3, ARRAY['C', '_']),
+        (4, ARRAY['_', '_'])
+    ) 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_replace
+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,D} |           1 |         3
+  2 | {B,_} |             |          
+  3 | {C,_} |             |          
+  4 | {_,_} |             |          
+(4 rows)
+
+-- ALT lexical order takes priority over greedy (longer match).
+-- Row 1 matches both A and B; A wins by lexical order (match 1-1).
+WITH test_alt_lexical_order AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A','B']),  -- A and B both match
+        (2, ARRAY['_','C'])   -- only C matches (would continue B 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_alt_lexical_order
+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,B} |           1 |         1
+  2 | {_,C} |             |          
+(2 rows)
+
+-- ALT with reluctant: (A+? | B+) - A branch is reluctant, B is greedy.
+-- Row 1 matches both A and B. A+? exits immediately (match 1-1).
+WITH test_alt_reluctant AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A','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_alt_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,B} |           1 |         1
+  2 | {B,_} |           2 |         3
+  3 | {B,_} |             |          
+(3 rows)
+
+-- Optional first branch in ALT with quantifier: (A? | B){1,2}
+-- First branch A? exit path may loop back to ALT and trigger cycle
+-- detection during DFS.  All branches must receive correct counts.
+WITH test_alt_opt_first 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_alt_opt_first
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (((A? | B){1,2}))
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {B}   |           1 |         2
+  2 | {B}   |           2 |         3
+  3 | {B}   |           3 |         3
+(3 rows)
+
+-- Mixed A/B rows across iterations of (A? | B){1,2}
+WITH test_alt_opt_mixed AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['B']),
+        (3, ARRAY['A','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_alt_opt_mixed
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (((A? | B){1,2}))
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {A}   |           1 |         2
+  2 | {B}   |           2 |         3
+  3 | {A,B} |           3 |         3
+(3 rows)
+
+-- Reluctant variant: (A?? | B){1,2}
+WITH test_alt_opt_reluctant 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_alt_opt_reluctant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (((A?? | B){1,2}))
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {B}   |           1 |         2
+  2 | {B}   |           2 |         3
+  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
+-- ============================================================
+-- Three-level nesting ((((A B)+)+)+)
+WITH test_deep_nesting AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['B']),
+        (3, ARRAY['A']),
+        (4, ARRAY['B']),
+        (5, ARRAY['A']),
+        (6, ARRAY['B']),
+        (7, ARRAY['_'])
+    ) 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_deep_nesting
+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 |         6
+  2 | {B}   |             |          
+  3 | {A}   |           3 |         6
+  4 | {B}   |             |          
+  5 | {A}   |           5 |         6
+  6 | {B}   |             |          
+  7 | {_}   |             |          
+(7 rows)
+
+-- Multiple groups in nesting (((A B) (C D))+)
+WITH test_nested_sequential 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']),
+        (9, ARRAY['_'])
+    ) 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_sequential
+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 |         8
+  2 | {B}   |             |          
+  3 | {C}   |             |          
+  4 | {D}   |             |          
+  5 | {A}   |           5 |         8
+  6 | {B}   |             |          
+  7 | {C}   |             |          
+  8 | {D}   |             |          
+  9 | {_}   |             |          
+(9 rows)
+
+-- 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']),
+        (2, ARRAY['B']),
+        (3, ARRAY['A']),
+        (4, ARRAY['B']),
+        (5, ARRAY['A']),
+        (6, ARRAY['B']),
+        (7, ARRAY['A']),
+        (8, ARRAY['B']),
+        (9, ARRAY['_'])
+    ) 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_end_nested_max
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT 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 |         8
+  2 | {B}   |             |          
+  3 | {A}   |           3 |         6
+  4 | {B}   |             |          
+  5 | {A}   |           5 |         8
+  6 | {B}   |             |          
+  7 | {A}   |             |          
+  8 | {B}   |             |          
+  9 | {_}   |             |          
+(9 rows)
+
+-- 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']),
+        (2, ARRAY['B']),
+        (3, ARRAY['A']),
+        (4, ARRAY['B']),
+        (5, ARRAY['A']),
+        (6, ARRAY['B']),
+        (7, ARRAY['A']),
+        (8, ARRAY['B']),
+        (9, ARRAY['_'])
+    ) 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_end_nested_mid
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (((A B){1,3})+)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {A}   |           1 |         8
+  2 | {B}   |             |          
+  3 | {A}   |           3 |         8
+  4 | {B}   |             |          
+  5 | {A}   |           5 |         8
+  6 | {B}   |             |          
+  7 | {A}   |           7 |         8
+  8 | {B}   |             |          
+  9 | {_}   |             |          
+(9 rows)
+
+-- Nested reluctant group ((A B)+?) with following element C
+-- Inner group exits after minimum 1 iteration
+WITH test_nested_reluctant AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['B']),
+        (3, ARRAY['A']),
+        (4, ARRAY['B']),
+        (5, 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_nested_reluctant
+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 'A' = ANY(flags),
+        B AS 'B' = ANY(flags),
+        C AS 'C' = ANY(flags)
+);
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {A}   |           1 |         5
+  2 | {B}   |             |          
+  3 | {A}   |           3 |         5
+  4 | {B}   |             |          
+  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)
+-- ============================================================
+-- SKIP PAST LAST ROW (non-overlapping matches)
+WITH test_skip_past AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),
+        (4, ARRAY['A']),
+        (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
+FROM test_skip_past
+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 |         4
+  2 | {A}   |             |          
+  3 | {A}   |             |          
+  4 | {A}   |             |          
+  5 | {_}   |             |          
+(5 rows)
+
+-- SKIP TO NEXT ROW (overlapping matches)
+WITH test_skip_next AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),
+        (4, ARRAY['A']),
+        (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
+FROM test_skip_next
+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 | {_}   |             |          
+(5 rows)
+
+-- SKIP difference verification
+WITH test_skip_diff AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['B']),
+        (3, ARRAY['A']),
+        (4, ARRAY['B'])
+    ) AS t(id, flags)
+)
+SELECT 'SKIP PAST' AS mode, id, flags,
+       first_value(id) OVER w AS match_start,
+       last_value(id) OVER w AS match_end
+FROM test_skip_diff
+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)
+)
+UNION ALL
+SELECT 'SKIP NEXT' AS mode, id, flags,
+       first_value(id) OVER w AS match_start,
+       last_value(id) OVER w AS match_end
+FROM test_skip_diff
+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)
+)
+ORDER BY mode, id;
+   mode    | id | flags | match_start | match_end 
+-----------+----+-------+-------------+-----------
+ SKIP NEXT |  1 | {A}   |           1 |         2
+ SKIP NEXT |  2 | {B}   |             |          
+ SKIP NEXT |  3 | {A}   |           3 |         4
+ SKIP NEXT |  4 | {B}   |             |          
+ SKIP PAST |  1 | {A}   |           1 |         2
+ SKIP PAST |  2 | {B}   |             |          
+ SKIP PAST |  3 | {A}   |           3 |         4
+ SKIP PAST |  4 | {B}   |             |          
+(8 rows)
+
+-- Reluctant SKIP comparison: A+? with SKIP PAST vs SKIP NEXT
+WITH test_reluctant_skip AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),
+        (4, ARRAY['_'])
+    ) AS t(id, flags)
+)
+SELECT 'SKIP PAST' AS mode, id, flags,
+       first_value(id) OVER w AS match_start,
+       last_value(id) OVER w AS match_end
+FROM test_reluctant_skip
+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)
+)
+UNION ALL
+SELECT 'SKIP NEXT' AS mode, id, flags,
+       first_value(id) OVER w AS match_start,
+       last_value(id) OVER w AS match_end
+FROM test_reluctant_skip
+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)
+)
+ORDER BY mode, id;
+   mode    | id | flags | match_start | match_end 
+-----------+----+-------+-------------+-----------
+ SKIP NEXT |  1 | {A}   |           1 |         1
+ SKIP NEXT |  2 | {A}   |           2 |         2
+ SKIP NEXT |  3 | {A}   |           3 |         3
+ SKIP NEXT |  4 | {_}   |             |          
+ SKIP PAST |  1 | {A}   |           1 |         1
+ SKIP PAST |  2 | {A}   |           2 |         2
+ SKIP PAST |  3 | {A}   |           3 |         3
+ SKIP PAST |  4 | {_}   |             |          
+(8 rows)
+
+-- ============================================================
+-- 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 (
+    SELECT * FROM (VALUES
+        (1, ARRAY['_']),  -- Unmatched
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),
+        (4, ARRAY['_']),  -- Unmatched
+        (5, ARRAY['A'])
+    ) 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_initial_mode
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A+)
+    DEFINE
+        A AS 'A' = ANY(flags)
+);
+ERROR:  syntax error at or near "AFTER"
+LINE 18:     AFTER MATCH SKIP TO NEXT ROW
+             ^
+-- Default mode (include all rows)
+WITH test_default_mode AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['_']),  -- Unmatched, but included
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),
+        (4, ARRAY['_']),  -- Unmatched, but included
+        (5, ARRAY['A'])
+    ) 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_default_mode
+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 | {_}   |             |          
+  2 | {A}   |           2 |         3
+  3 | {A}   |           3 |         3
+  4 | {_}   |             |          
+  5 | {A}   |           5 |         5
+(5 rows)
+
+-- Mode difference verification (INITIAL not yet supported - produces syntax error)
+WITH test_mode_diff AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['_']),
+        (2, ARRAY['A']),
+        (3, ARRAY['_'])
+    ) AS t(id, flags)
+)
+SELECT 'INITIAL' AS mode, COUNT(*) AS row_count
+FROM (
+    SELECT id FROM test_mode_diff
+    WINDOW w AS (
+        ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        INITIAL
+        AFTER MATCH SKIP TO NEXT ROW
+        PATTERN (A)
+        DEFINE A AS 'A' = ANY(flags)
+    )
+) sub
+UNION ALL
+SELECT 'DEFAULT' AS mode, COUNT(*) AS row_count
+FROM (
+    SELECT id FROM test_mode_diff
+    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)
+    )
+) sub
+ORDER BY mode;
+ERROR:  syntax error at or near "AFTER"
+LINE 15:         AFTER MATCH SKIP TO NEXT ROW
+                 ^
+-- ============================================================
+-- Frame Boundary Variations
+-- ============================================================
+-- Very limited frame (1 FOLLOWING)
+WITH test_one_following AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['B']),  -- Within 1 FOLLOWING
+        (3, ARRAY['A']),  -- Beyond 1 FOLLOWING from row 1
+        (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_one_following
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND 1 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 |         2
+  2 | {B}   |             |          
+  3 | {A}   |           3 |         4
+  4 | {B}   |             |          
+(4 rows)
+
+-- Medium frame (10 FOLLOWING)
+WITH test_ten_following AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),
+        (4, ARRAY['A']),
+        (5, ARRAY['A']),
+        (6, ARRAY['A']),
+        (7, ARRAY['A']),
+        (8, ARRAY['A']),
+        (9, ARRAY['A']),
+        (10, ARRAY['A']),
+        (11, ARRAY['B']),  -- Within 10 FOLLOWING from row 1
+        (12, ARRAY['A']),
+        (13, ARRAY['B'])   -- Beyond 10 FOLLOWING from row 1
+    ) 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_ten_following
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND 10 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 |        11
+  2 | {A}   |           2 |        11
+  3 | {A}   |           3 |        11
+  4 | {A}   |           4 |        11
+  5 | {A}   |           5 |        11
+  6 | {A}   |           6 |        11
+  7 | {A}   |           7 |        11
+  8 | {A}   |           8 |        11
+  9 | {A}   |           9 |        11
+ 10 | {A}   |          10 |        11
+ 11 | {B}   |             |          
+ 12 | {A}   |          12 |        13
+ 13 | {B}   |             |          
+(13 rows)
+
+-- Exact boundary match
+WITH test_exact_boundary AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),
+        (4, ARRAY['A']),
+        (5, ARRAY['B'])   -- Exactly at 4 FOLLOWING (frame end)
+    ) 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_exact_boundary
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND 4 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 |         5
+  2 | {A}   |           2 |         5
+  3 | {A}   |           3 |         5
+  4 | {A}   |           4 |         5
+  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
+-- ============================================================
+-- Empty partition (0 rows)
+WITH test_empty_partition AS (
+    SELECT * FROM (VALUES
+        (1, 1, ARRAY['A']),
+        (2, 2, ARRAY['_'])  -- Different partition
+    ) AS t(id, part, flags)
+)
+SELECT id, part, flags,
+       first_value(id) OVER w AS match_start,
+       last_value(id) OVER w AS match_end
+FROM test_empty_partition
+WHERE part = 99  -- No rows match
+WINDOW w AS (
+    PARTITION BY part
+    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 | part | flags | match_start | match_end 
+----+------+-------+-------------+-----------
+(0 rows)
+
+-- Single row partition
+WITH test_single_row AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A'])
+    ) 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_single_row
+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 |         1
+(1 row)
+
+-- All rows fail matching (all DEFINE false)
+WITH test_all_fail AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A'])
+    ) 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_all_fail
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A+)
+    DEFINE
+        A AS false  -- All rows fail
+);
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {A}   |             |          
+  2 | {A}   |             |          
+  3 | {A}   |             |          
+(3 rows)
+
+-- Partition end with absorbable pattern
+-- SKIP PAST LAST ROW + unbounded frame + all rows match A
+-- Triggers absorb in !rowExists path at partition boundary.
+WITH test_absorb_partition_end AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),
+        (4, ARRAY['A']),
+        (5, ARRAY['A'])
+    ) 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_partition_end
+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 |         5
+  2 | {A}   |             |          
+  3 | {A}   |             |          
+  4 | {A}   |             |          
+  5 | {A}   |             |          
+(5 rows)
+
+-- ============================================================
+-- DEFINE Special Cases
+-- ============================================================
+-- Undefined variable in DEFINE
+WITH test_undefined_var AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['X']),  -- B not defined, defaults to TRUE
+        (3, ARRAY['C']),
+        (4, ARRAY['A']),
+        (5, ARRAY['_']),  -- B defaults to TRUE, but no flags
+        (6, 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_undefined_var
+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 'A' = ANY(flags),
+        -- B is undefined, defaults to TRUE
+        C AS 'C' = ANY(flags)
+);
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {A}   |           1 |         3
+  2 | {X}   |             |          
+  3 | {C}   |             |          
+  4 | {A}   |           4 |         6
+  5 | {_}   |             |          
+  6 | {C}   |             |          
+(6 rows)
+
+-- ============================================================
+-- Absorption Dynamic Flags
+-- ============================================================
+-- Partial absorbable pattern ((A+) B)
+WITH test_partial_absorbable AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),
+        (4, ARRAY['B']),
+        (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
+FROM test_partial_absorbable
+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 | {_}   |             |          
+(5 rows)
+
+-- Dynamic flag update ((A+) | B)
+WITH test_dynamic_flags 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_dynamic_flags
+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 |         3
+  2 | {A}   |           2 |         3
+  3 | {A}   |           3 |         3
+  4 | {B}   |           4 |         4
+  5 | {A}   |           5 |         5
+  6 | {B}   |           6 |         6
+(6 rows)
+
+-- 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.
+WITH test_non_absorbable AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['B']),
+        (3, ARRAY['A']),
+        (4, ARRAY['B']),
+        (5, ARRAY['C']),
+        (6, ARRAY['_'])
+    ) 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_non_absorbable
+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 |         5
+  2 | {B}   |             |          
+  3 | {A}   |             |          
+  4 | {B}   |             |          
+  5 | {C}   |             |          
+  6 | {_}   |             |          
+(6 rows)
+
+-- Absorption flags early return (!hasAbsorbableState)
+-- Pattern (A B)+ C D with SKIP PAST LAST ROW
+-- After reaching C (non-absorbable), hasAbsorbableState becomes false.
+-- On next row (D), the early return fires.
+WITH test_absorption_early_return AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['B']),
+        (3, ARRAY['A']),
+        (4, ARRAY['B']),
+        (5, ARRAY['C']),
+        (6, ARRAY['D']),
+        (7, ARRAY['_'])
+    ) 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_absorption_early_return
+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 |         6
+  2 | {B}   |             |          
+  3 | {A}   |             |          
+  4 | {B}   |             |          
+  5 | {C}   |             |          
+  6 | {D}   |             |          
+  7 | {_}   |             |          
+(7 rows)
+
+-- 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.
+WITH test_coverage_fail AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A', '_']),
+        (2, ARRAY['A', 'B']),
+        (3, ARRAY['A', '_']),
+        (4, ARRAY['A', '_']),
+        (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
+FROM test_coverage_fail
+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 |         4
+  2 | {A,B} |             |          
+  3 | {A,_} |             |          
+  4 | {A,_} |             |          
+  5 | {_,_} |             |          
+(5 rows)
+
+-- 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.
+WITH test_older_completed AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['B']),
+        (3, ARRAY['B']),
+        (4, ARRAY['_'])
+    ) 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_older_completed
+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 |         3
+  3 | {B}   |             |          
+  4 | {_}   |             |          
+(4 rows)
+
+-- 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.
+WITH test_older_non_absorbable AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['B', '_']),
+        (2, ARRAY['C', 'A']),
+        (3, ARRAY['_', 'A']),
+        (4, ARRAY['_', '_'])
+    ) 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_older_non_absorbable
+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 |         2
+  2 | {C,A} |             |          
+  3 | {_,A} |           3 |         3
+  4 | {_,_} |             |          
+(4 rows)
+
+-- Reluctant branch in ALT not absorbable: (A+?) | B
+-- A+? is reluctant so not absorbable. Compare with greedy (A+) | B above.
+WITH test_reluctant_alt_absorption AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),
+        (4, ARRAY['B']),
+        (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
+FROM test_reluctant_alt_absorption
+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 |         1
+  2 | {A}   |           2 |         2
+  3 | {A}   |           3 |         3
+  4 | {B}   |           4 |         4
+  5 | {_}   |             |          
+(5 rows)
+
+-- ============================================================
+-- Zero-Consumption Cycle Detection
+-- ============================================================
+-- Cycle prevention at count > 0: (A*)* inner skip cycles at count=3
+WITH test_cycle_nonzero AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),
+        (4, ARRAY['B'])  -- Inner A* matches 0, cycles at count=3
+    ) 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_cycle_nonzero
+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 |         3
+  2 | {A}   |           2 |         3
+  3 | {A}   |           3 |         3
+  4 | {B}   |             |          
+(4 rows)
+
+-- Cycle with mixed nullables: (A* B*)* multiple nullable paths
+WITH test_cycle_mixed 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 test_cycle_mixed
+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 |         3
+  2 | {B}   |           2 |         3
+  3 | {A}   |           3 |         3
+  4 | {C}   |             |          
+(4 rows)
+
+-- ============================================================
+-- Standard Clause 7: Formal Pattern Matching Rules
+-- ISO/IEC 19075-5:2021, Clause 7
+-- ============================================================
+-- ------------------------------------------------------------
+-- 7.2.2 Alternation: first alternative is preferred
+-- ------------------------------------------------------------
+-- (A | B): A preferred over B when both could match
+-- Row 1 has both A and B flags: A should be chosen (first alternative)
+WITH test_alt_prefer AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A','B']),
+        (2, ARRAY['B']),
+        (3, ARRAY['A'])
+    ) 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_prefer
+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,B} |           1 |         1
+  2 | {B}   |           2 |         2
+  3 | {A}   |           3 |         3
+(3 rows)
+
+-- (A{1,2} | B{2,3}): all A-matches before all B-matches
+-- Standard example: preferment order is AA, A, BBB, BB
+-- Rows 1-2 have both A and B: greedy A{1,2} should match 1-2
+WITH test_alt_quantified AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A','B']),
+        (2, ARRAY['A','B']),
+        (3, ARRAY['B']),
+        (4, ARRAY['B']),
+        (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_alt_quantified
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A{1,2} | B{2,3}))
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {A,B} |           1 |         2
+  2 | {A,B} |           2 |         2
+  3 | {B}   |           3 |         5
+  4 | {B}   |           4 |         5
+  5 | {B}   |             |          
+(5 rows)
+
+-- ------------------------------------------------------------
+-- 7.2.3 Concatenation: lexicographic ordering
+-- ------------------------------------------------------------
+-- ((A | B) (C | D)): preferment order is AC, AD, BC, BD
+-- Row 1 matches A and B, Row 2 matches C and D
+-- Preferred match: A then C (first alternatives in both positions)
+WITH test_concat_lex AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A','B']),
+        (2, ARRAY['C','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_concat_lex
+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,B} |           1 |         2
+  2 | {C,D} |             |          
+(2 rows)
+
+-- ((A | B) C): first alt (A) fails, second alt (B) succeeds
+-- Tests backtracking: row 1 has only B, row 2 has C
+WITH test_concat_backtrack AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['B']),
+        (2, ARRAY['C']),
+        (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 test_concat_backtrack
+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 'A' = ANY(flags),
+        B AS 'B' = ANY(flags),
+        C AS 'C' = ANY(flags)
+);
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {B}   |           1 |         2
+  2 | {C}   |             |          
+  3 | {A}   |           3 |         4
+  4 | {C}   |             |          
+(4 rows)
+
+-- ------------------------------------------------------------
+-- 7.2.4 Quantification: greedy/reluctant, lexicographic > length
+-- ------------------------------------------------------------
+-- V{2,4} greedy: longer match preferred
+WITH test_quant_greedy AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),
+        (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_quant_greedy
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A{2,4})
+    DEFINE
+        A AS 'A' = ANY(flags)
+);
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {A}   |           1 |         3
+  2 | {A}   |             |          
+  3 | {A}   |             |          
+  4 | {B}   |             |          
+(4 rows)
+
+-- V{2,4}? reluctant: shorter match preferred
+WITH test_quant_reluctant AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),
+        (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_quant_reluctant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A{2,4}?)
+    DEFINE
+        A AS 'A' = ANY(flags)
+);
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {A}   |           1 |         2
+  2 | {A}   |             |          
+  3 | {A}   |             |          
+  4 | {B}   |             |          
+(4 rows)
+
+-- ((A|B){1,2}) greedy: lexicographic > length
+-- Standard example: preferment AA, AB, A, BA, BB, B
+-- Single A preferred over B-starting longer match
+WITH test_quant_lex_greedy AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A','B']),
+        (2, 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_quant_lex_greedy
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (((A | B){1,2}))
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {A,B} |           1 |         2
+  2 | {B}   |             |          
+(2 rows)
+
+-- ((A|B){1,2}?) reluctant: lexicographic > length
+-- Standard example: preferment A, AA, AB, B, BA, BB
+-- Single A preferred over any B-starting match
+WITH test_quant_lex_reluctant AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A','B']),
+        (2, 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_quant_lex_reluctant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (((A | B){1,2}?))
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {A,B} |           1 |         1
+  2 | {B}   |           2 |         2
+(2 rows)
+
+-- ------------------------------------------------------------
+-- 7.2.6 Anchors (not yet implemented - syntax error expected)
+-- ------------------------------------------------------------
+-- ^ anchor: not yet supported
+SELECT count(*) OVER w FROM (SELECT 1 AS v) t
+WINDOW w AS (ORDER BY v ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (^ A) DEFINE A AS TRUE);
+ERROR:  syntax error at or near "^"
+LINE 3:     PATTERN (^ A) DEFINE A AS TRUE);
+                     ^
+-- $ anchor: not yet supported
+SELECT count(*) OVER w FROM (SELECT 1 AS v) t
+WINDOW w AS (ORDER BY v ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A $) DEFINE A AS TRUE);
+ERROR:  syntax error at or near "$"
+LINE 3:     PATTERN (A $) DEFINE A AS TRUE);
+                       ^
+-- ------------------------------------------------------------
+-- 7.2.8 Infinite repetitions of empty matches
+-- (Perl lower-bound stopping rule)
+-- ------------------------------------------------------------
+-- Standard examples from 7.2.8:
+--   (A?){0,3}: allowed strings include STR00=(), STR01=(A), STR02=(empty),
+--              STR03=(AA), STR04=(A,empty), STR07=(AAA), STR08=(AA,empty)
+--   (A?){1,3}: same as {0,3} but STR00 excluded (min=1 not met)
+--   (A?){2,3}: STR03-06 (len 2) and STR07,08,11,12 (len 3) are valid
+--              STR06=(STRE,STRE) IS valid because non-final STRE at
+--              position 1 fills the lower bound
+-- (A??)*B: Standard 7.2.8 introductory example
+-- "matched against a sequence of rows for which the only feasible
+--  matching is: B"
+-- A?? is reluctant, prefers empty. * is greedy but Perl rule stops
+-- after empty match with min(=0) satisfied.
+-- Expected: each B row matches alone (A?? empty, * stops, B matches)
+WITH test_empty_reluctant_star AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['B']),
+        (2, ARRAY['B']),
+        (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_empty_reluctant_star
+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 | {B}   |           1 |         1
+  2 | {B}   |           2 |         2
+  3 | {C}   |             |          
+(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})
+WITH test_728_min0 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_728_min0
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A?){0,3})
+    DEFINE
+        A AS 'A' = ANY(flags)
+);
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {B}   |             |          
+  2 | {B}   |             |          
+  3 | {B}   |             |          
+(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})
+WITH test_728_min1 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_728_min1
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A?){1,3})
+    DEFINE
+        A AS 'A' = ANY(flags)
+);
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {B}   |             |          
+  2 | {B}   |             |          
+  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
+WITH test_728_min2 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_728_min2
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A?){2,3})
+    DEFINE
+        A AS 'A' = ANY(flags)
+);
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {B}   |             |          
+  2 | {B}   |             |          
+  3 | {B}   |             |          
+(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})
+-- 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']),
+        (2, ARRAY['A']),
+        (3, ARRAY['B']),
+        (4, ARRAY['A'])
+    ) 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_728_min2_mixed
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A?){2,3})
+    DEFINE
+        A AS 'A' = ANY(flags)
+);
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {A}   |           1 |         2
+  2 | {A}   |           2 |         2
+  3 | {B}   |             |          
+  4 | {A}   |           4 |         4
+(4 rows)
+
+-- (A? B?){2,3}: multi-element nullable body with real matches
+-- Body A? B? is nullable (both optional), but A and B DO match rows.
+-- Real (non-empty) iterations loop back normally; fast-forward only
+-- 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
+WITH test_728_multi_body AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['B']),
+        (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_728_multi_body
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A? B?){2,3})
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {A}   |           1 |         6
+  2 | {B}   |           2 |         6
+  3 | {A}   |           3 |         6
+  4 | {B}   |           4 |         6
+  5 | {A}   |           5 |         6
+  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)
+WITH test_728_multi_empty AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['C']),
+        (2, ARRAY['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_728_multi_empty
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A? B?){2,3})
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {C}   |             |          
+  2 | {C}   |             |          
+  3 | {C}   |             |          
+(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
+WITH test_728_multi_mixed AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['B']),
+        (3, ARRAY['C']),
+        (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_728_multi_mixed
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A? B?){2,3})
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {A}   |           1 |         2
+  2 | {B}   |           2 |         2
+  3 | {C}   |             |          
+  4 | {A}   |           4 |         5
+  5 | {B}   |           5 |         5
+(5 rows)
+
+-- ------------------------------------------------------------
+-- 7.3 Pattern matching in theory and practice
+-- ------------------------------------------------------------
+-- 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
+-- Backtrack: empty A?, then B+ from row 1
+-- Expected: rows 1-3 match as B (A? takes empty match)
+WITH test_73_example AS (
+    SELECT * FROM (VALUES
+        (1, 60),
+        (2, 70),
+        (3, 40)
+    ) AS t(id, price)
+)
+SELECT id, price,
+       first_value(id) OVER w AS match_start,
+       last_value(id) OVER w AS match_end
+FROM test_73_example
+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 price > 100,
+        B AS TRUE
+);
+ id | price | match_start | match_end 
+----+-------+-------------+-----------
+  1 |    60 |           1 |         3
+  2 |    70 |             |          
+  3 |    40 |             |          
+(3 rows)
+
-- 
2.43.0



  [application/octet-stream] v47-0009-Row-pattern-recognition-patch-typedefs.list.patch (1.5K, 10-v47-0009-Row-pattern-recognition-patch-typedefs.list.patch)
  download | inline diff:
From 215abb8535441073bc107950020fcf2e87c31762 Mon Sep 17 00:00:00 2001
From: Tatsuo Ishii <[email protected]>
Date: Sat, 2 May 2026 13:40:29 +0900
Subject: [PATCH v47 9/9] Row pattern recognition patch (typedefs.list).

---
 src/tools/pgindent/typedefs.list | 21 +++++++++++++++++++++
 1 file changed, 21 insertions(+)

diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0abdb2d37e2..4b8225cbf3a 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -761,6 +761,8 @@ ErrorData
 ErrorSaveContext
 EstimateDSMForeignScan_function
 EstimationInfo
+EvalNavFirstContext
+EvalNavMaxContext
 EventTriggerCacheEntry
 EventTriggerCacheItem
 EventTriggerCacheStateType
@@ -1820,6 +1822,8 @@ NamedLWLockTrancheRequest
 NamedTuplestoreScan
 NamedTuplestoreScanState
 NamespaceInfo
+NavCheckResult
+NavOffsetContext
 NestLoop
 NestLoopParam
 NestLoopState
@@ -1829,6 +1833,7 @@ NewConstraint
 NextSampleBlock_function
 NextSampleTuple_function
 NextValueExpr
+NFALengthStats
 Node
 NodeInstrumentation
 NodeTag
@@ -2499,6 +2504,9 @@ QuerySource
 QueueBackendStatus
 QueuePosition
 QuitSignalReason
+RPRNavExpr
+RPRNavKind
+RPRNavOffsetKind
 RBTNode
 RBTOrderControl
 RBTree
@@ -2511,6 +2519,19 @@ RI_ConstraintInfo
 RI_FastPathEntry
 RI_QueryHashEntry
 RI_QueryKey
+RPCommonSyntax
+RPRDepth
+RPRElemFlags
+RPRElemIdx
+RPRNFAContext
+RPRNFAState
+RPRPattern
+RPRPatternElement
+RPRPatternNode
+RPRPatternNodeType
+RPRQuantity
+RPRVarId
+RPSkipTo
 RTEKind
 RTEPermissionInfo
 RWConflict
-- 
2.43.0



reply

Reply instructions:

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

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

  To: [email protected]
  Cc: [email protected], [email protected], [email protected], [email protected], [email protected], [email protected], [email protected], [email protected], [email protected], [email protected]
  Subject: Re: Row pattern recognition
  In-Reply-To: <[email protected]>

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

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