public inbox for [email protected]  
help / color / mirror / Atom feed
From: Henson Choi <[email protected]>
To: Tatsuo Ishii <[email protected]>
Cc: [email protected]
Cc: [email protected]
Cc: [email protected]
Cc: [email protected]
Cc: [email protected]
Cc: [email protected]
Subject: Re: Row pattern recognition
Date: Sat, 14 Feb 2026 23:58:10 +0900
Message-ID: <CAAAe_zB+W+xBJpYNzRLzh6BH9HrFycTjoPUnVvnU1BT_7RR8Bw@mail.gmail.com> (raw)
In-Reply-To: <[email protected]>
References: <[email protected]>
	<CAAAe_zDPaPiRg2gn=kRtrh+VTis5EDUeCftBK46vKsZjUHAEtA@mail.gmail.com>
	<CAAAe_zAzOgB2KR9ACDD2o3QNP_gaCKnUd2bRGgbhcD=og50XXA@mail.gmail.com>
	<[email protected]>

Hi Tatsuo,

This round focused on reviewing nodeWindowAgg.c -- the NFA executor
-- which was the last piece remaining on my review list.  I've also
added a comprehensive NFA runtime test suite.

The attached patch continues the incremental series on top of v42.
Here are the changes:


FIXME issues documented:

These require non-trivial structural changes, so I've documented
them as FIXME comments for now rather than attempting a fix.

- altPriority tracks only the last ALT choice, so repeated or nested
  ALTs like (A|B)+ cannot correctly implement SQL standard lexical
  ordering.  A full-path classifier structure is needed.

- Cycle prevention condition (count == 0 && min == 0) is insufficient
  for patterns like (A*)* where cycles occur at count > 0.  Currently
  relies on implicit duplicate detection in nfa_add_state_unique.


Bugs fixed:

- Fix nfa_advance_begin() routing order: enter group first (lexically
  first), then skip path (lexically second).

- Add ALT scope boundary check in nfa_advance_alt() and
  computeAbsorbabilityRecursive() to stop branch traversal when
  element depth exits ALT scope.

- Disallow RANGE and GROUPS frame types with row pattern
  recognition, as the SQL standard only requires ROWS.
  Also remove the now-dead frame-type determination code.


NFA executor refactoring (nodeWindowAgg.c):

- Simplify nfa_process_row() phase 1: remove nfa_advance() call
  after forced mismatch at frame boundary, since all states are
  already at VAR positions and mismatch removes them.  Replace
  the redundant phase 3 frame boundary check with Assert.

- Simplify update_reduced_frame() result registration: mismatch
  path now returns early, and the post-processing SKIP mode cleanup
  block (nfa_remove_contexts_up_to vs nfa_context_free) is removed
  since eager pruning handles SKIP PAST LAST ROW and both paths
  now simply call nfa_context_free().

- Replace nfa_remove_contexts_up_to() with eager pruning inside
  nfa_add_matched_state().  When matchEndRow is extended during
  greedy matching, pending contexts within the match range are
  pruned incrementally instead of after match completion.  For
  SKIP PAST LAST ROW patterns like START UP+, this reduces the
  number of live contexts at each row from O(n) to O(1), avoiding
  O(n^2) per-row processing of contexts that would be skipped
  anyway.

- Optimize nfa_states_equal() to compare counts only up to the
  current element's depth instead of the full maxDepth.

- Rename nfa_state_clone -> nfa_state_create,
  nfa_find_context_for_pos -> nfa_get_head_context for clarity.

- Add nfa_record_context_success/skipped/absorbed statistics helpers.

- Remove unused RPRPattern parameter from
  nfa_update_absorption_flags().


Absorbability refactoring (rpr.c):

- Remove parentDepth parameter from isUnboundedStart() and
  computeAbsorbabilityRecursive().  Scope boundaries are now
  determined by element depth comparison instead of an explicit
  parent depth parameter.

- Simplify isUnboundedStart(): check simple VAR case first, then
  GROUP case with a depth-bounded loop instead of a FIN-terminated
  traversal with multiple break conditions.


Test changes:

- Add rpr_nfa.sql: comprehensive NFA runtime test suite covering
  quantifier boundaries (min/max/exact), alternation priority, nested
  patterns, frame boundary variations, INITIAL mode (syntax error
  expected), pathological patterns, context absorption, and FIXME
  reproduction cases.

- Add nth_value out-of-frame tests, ReScan/LATERAL test, and
  nth_value IGNORE NULLS test to rpr.sql.

- Change selected EXPLAIN test queries in rpr_explain.sql from
  EXPLAIN (COSTS OFF) to EXPLAIN (ANALYZE, ...) to verify actual
  NFA execution statistics.

- Fix stale comments across rpr.sql, rpr_base.sql, and rpr_nfa.sql:
  remove resolved BUG annotations, update error messages to match
  actual output, correct optimization result descriptions, and
  standardize Expected comment placement to after SQL statements.


Other changes:

- Run pgindent on RPR source files.  Add /*----------*/ comment
  guards to protect structured comments from reformatting.


Coverage:

I ran gcov on the modified lines (diff-only coverage).  The attached
coverage.zip contains an HTML report.  Summary:

- 18 files, 124 functions, 2238 modified lines analyzed
- Overall: 92.1% line coverage (2061/2238)
- Core files (nodeWindowAgg.c, rpr.c, explain.c, parse_rpr.c):
  98.0-98.5% each
- Node serialization (outfuncs.c, readfuncs.c, equalfuncs.c):
  0% -- these implement RPRPattern serialization for plan caching,
  but regression tests don't exercise prepared statements with RPR
- ruleutils.c: 96.3% -- untested lines are the reluctant quantifier
  display path ({1}?), which is currently rejected at parse time

The node serialization functions (141 lines, 0% coverage) are the
largest untested area.  I'm not sure how to trigger these paths
in the regression test framework.  Any suggestions?

I'll send a separate email within a few days listing the FIXME
issues and other unresolved items from the mailing list discussion
for your review.


Best regards,
Henson

From 1cc2ebd59597c672b752d7b6912dedc2bde88d66 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Tue, 3 Feb 2026 17:30:06 +0900
Subject: [PATCH 1/1] Fix non-ASCII characters in RPR code and comments

---
 src/backend/executor/nodeWindowAgg.c   | 10 ++++-----
 src/backend/optimizer/plan/rpr.c       |  2 +-
 src/include/nodes/execnodes.h          |  8 ++++----
 src/test/regress/expected/rpr.out      | 14 ++++++-------
 src/test/regress/expected/rpr_base.out | 28 +++++++++++++-------------
 src/test/regress/sql/rpr.sql           | 14 ++++++-------
 src/test/regress/sql/rpr_base.sql      | 28 +++++++++++++-------------
 7 files changed, 52 insertions(+), 52 deletions(-)

diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index bcdf8b6db81..1176df04b2c 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -4977,7 +4977,7 @@ register_result:
  * These functions implement direct NFA execution using the compiled
  * RPRPattern structure, avoiding regex compilation overhead.
  *
- * Execution Flow: match → absorb → advance
+ * Execution Flow: match -> absorb -> advance
  * -----------------------------------------
  * The NFA execution follows a three-phase cycle for each row:
  *
@@ -4997,7 +4997,7 @@ register_result:
  *
  * Key Design Decisions:
  * ---------------------
- * - VAR→END transition in match phase: When a simple VAR (max=1) matches
+ * - 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
@@ -5008,7 +5008,7 @@ register_result:
  *   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->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.
@@ -6092,7 +6092,7 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
 		state->elemIdx = elem->next;
 		nextElem = &elements[state->elemIdx];
 
-		/* END→END: increment outer END's count */
+		/* END->END: increment outer END's count */
 		if (RPRElemIsEnd(nextElem) && state->counts[nextElem->depth] < RPR_COUNT_MAX)
 			state->counts[nextElem->depth]++;
 
@@ -6123,7 +6123,7 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
 		exitState->counts[depth] = 0;
 		nextElem = &elements[exitState->elemIdx];
 
-		/* END→END: increment outer END's count */
+		/* END->END: increment outer END's count */
 		if (RPRElemIsEnd(nextElem) && exitState->counts[nextElem->depth] < RPR_COUNT_MAX)
 			exitState->counts[nextElem->depth]++;
 
diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index 230c545a631..50043c416c6 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -1379,7 +1379,7 @@ isUnboundedStart(RPRPattern *pattern, RPRElemIdx idx, RPRDepth parentDepth)
  *
  * Context absorption eliminates redundant match searches by absorbing
  * newer contexts that cannot produce longer matches than older contexts.
- * This achieves O(n²) → O(n) performance improvement.
+ * 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
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index b6d6d942de9..e0704742d16 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -2545,11 +2545,11 @@ typedef struct 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
+ *   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)
+ *     - Dynamic: can change false->true (when non-absorbable states die)
  *     - Used to determine if this context is eligible for absorption
  */
 typedef struct RPRNFAContext
@@ -2564,7 +2564,7 @@ typedef struct RPRNFAContext
 	RPRNFAState *matchedState;	/* FIN state for greedy fallback (cloned) */
 
 	/* Two-flag absorption optimization */
-	bool		hasAbsorbableState; /* can absorb others (≥1 absorbable
+	bool		hasAbsorbableState; /* can absorb others (>=1 absorbable
 									 * state) */
 	bool		allStatesAbsorbable;	/* can be absorbed (ALL states
 										 * absorbable) */
diff --git a/src/test/regress/expected/rpr.out b/src/test/regress/expected/rpr.out
index c4794c5cd88..d4298860865 100644
--- a/src/test/regress/expected/rpr.out
+++ b/src/test/regress/expected/rpr.out
@@ -3099,11 +3099,11 @@ WINDOW w AS (
   5 | {E,_} |             |          
 (5 rows)
 
--- Row 1: A=T, B=T → matches A
--- Row 2: B=T, C=T → matches B
--- Row 3: C=T, D=T → matches C
--- Row 4: D=T, E=T → matches D
--- Row 5: E=T      → matches E
+-- Row 1: A=T, B=T -> matches A
+-- Row 2: B=T, C=T -> matches B
+-- Row 3: C=T, D=T -> matches C
+-- Row 4: D=T, E=T -> matches D
+-- Row 5: E=T      -> matches E
 -- Result: match 1-5 (A B C D E)
 -- Test 6: Diagonal pattern with multi-TRUE (shifted overlap)
 WITH test_diagonal AS (
@@ -3138,8 +3138,8 @@ WINDOW w AS (
 (5 rows)
 
 -- Possible matches:
---   Start Row 1: A(1) B(2) C(3) D(4) → 1-4
---   Start Row 2: A(2) B(3) C(4) D(5) → 2-5 (because Row 2 has A too!)
+--   Start Row 1: A(1) B(2) C(3) D(4) -> 1-4
+--   Start Row 2: A(2) B(3) C(4) D(5) -> 2-5 (because Row 2 has A too!)
 -- ===================================================================
 -- Context Absorption Tests
 -- ===================================================================
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index b34b9e59d25..23851c5a11c 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -336,9 +336,9 @@ INSERT INTO rpr_frame VALUES
 -- 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
+-- 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
@@ -598,11 +598,11 @@ ORDER BY id;
 
 -- RANGE: includes all rows with same ORDER BY value
 -- Expected result: Includes peer rows (same val) in range calculation
--- id=1 (val=10): range [10,20] includes all val=10 and val=20 peers → cnt=2 (only first in peer group gets match)
--- id=2,3 (val=10): already matched by id=1 → cnt=0
--- id=4 (val=20): range [20,30] includes all val=20 and val=30 peers → cnt=2
--- id=5 (val=20): already matched by id=4 → cnt=0
--- id=6 (val=30): range [30,40] includes only val=30 → cnt=1
+-- id=1 (val=10): range [10,20] includes all val=10 and val=20 peers -> cnt=2 (only first in peer group gets match)
+-- id=2,3 (val=10): already matched by id=1 -> cnt=0
+-- id=4 (val=20): range [20,30] includes all val=20 and val=30 peers -> cnt=2
+-- id=5 (val=20): already matched by id=4 -> cnt=0
+-- id=6 (val=30): range [30,40] includes only val=30 -> cnt=1
 -- Result: [2,0,0,2,0,1]
 SELECT id, val, COUNT(*) OVER w as cnt
 FROM rpr_frame
@@ -626,11 +626,11 @@ ORDER BY id;
 
 -- GROUPS: treats rows with same value as one group (1 FOLLOWING = next group)
 -- Expected result: 1 FOLLOWING means current group + 1 next group
--- id=1 (val=10): groups [val=10, val=20] → cnt=2 (only first in group gets match)
--- id=2,3 (val=10): already matched by id=1 → cnt=0
--- id=4 (val=20): groups [val=20, val=30] → cnt=2
--- id=5 (val=20): already matched by id=4 → cnt=0
--- id=6 (val=30): groups [val=30] (no next group) → cnt=1
+-- id=1 (val=10): groups [val=10, val=20] -> cnt=2 (only first in group gets match)
+-- id=2,3 (val=10): already matched by id=1 -> cnt=0
+-- id=4 (val=20): groups [val=20, val=30] -> cnt=2
+-- id=5 (val=20): already matched by id=4 -> cnt=0
+-- id=6 (val=30): groups [val=30] (no next group) -> cnt=1
 -- Result: [2,0,0,2,0,1]
 SELECT id, val, COUNT(*) OVER w as cnt
 FROM rpr_frame
@@ -3549,7 +3549,7 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND 10 FOLLOWING
 -- ============================================================
 -- Absorption Analysis Tests
 -- ============================================================
--- Tests context absorption optimization (O(n²) → O(n))
+-- Tests context absorption optimization (O(n^2) -> O(n))
 -- Files: rpr.c (computeAbsorbability)
 -- Simple Absorbable Pattern: A+ B
 -- Pattern starts with unbounded VAR
diff --git a/src/test/regress/sql/rpr.sql b/src/test/regress/sql/rpr.sql
index 8ab1daf87d6..788a77e5279 100644
--- a/src/test/regress/sql/rpr.sql
+++ b/src/test/regress/sql/rpr.sql
@@ -1527,11 +1527,11 @@ WINDOW w AS (
         D AS 'D' = ANY(flags),
         E AS 'E' = ANY(flags)
 );
--- Row 1: A=T, B=T → matches A
--- Row 2: B=T, C=T → matches B
--- Row 3: C=T, D=T → matches C
--- Row 4: D=T, E=T → matches D
--- Row 5: E=T      → matches E
+-- Row 1: A=T, B=T -> matches A
+-- Row 2: B=T, C=T -> matches B
+-- Row 3: C=T, D=T -> matches C
+-- Row 4: D=T, E=T -> matches D
+-- Row 5: E=T      -> matches E
 -- Result: match 1-5 (A B C D E)
 
 -- Test 6: Diagonal pattern with multi-TRUE (shifted overlap)
@@ -1558,8 +1558,8 @@ WINDOW w AS (
         D AS 'D' = ANY(flags)
 );
 -- Possible matches:
---   Start Row 1: A(1) B(2) C(3) D(4) → 1-4
---   Start Row 2: A(2) B(3) C(4) D(5) → 2-5 (because Row 2 has A too!)
+--   Start Row 1: A(1) B(2) C(3) D(4) -> 1-4
+--   Start Row 2: A(2) B(3) C(4) D(5) -> 2-5 (because Row 2 has A too!)
 
 -- ===================================================================
 -- Context Absorption Tests
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index 88f0a2c2083..a5a66d2ca81 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -280,9 +280,9 @@ INSERT INTO rpr_frame VALUES
 
 -- 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
+-- 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
@@ -464,11 +464,11 @@ ORDER BY id;
 
 -- RANGE: includes all rows with same ORDER BY value
 -- Expected result: Includes peer rows (same val) in range calculation
--- id=1 (val=10): range [10,20] includes all val=10 and val=20 peers → cnt=2 (only first in peer group gets match)
--- id=2,3 (val=10): already matched by id=1 → cnt=0
--- id=4 (val=20): range [20,30] includes all val=20 and val=30 peers → cnt=2
--- id=5 (val=20): already matched by id=4 → cnt=0
--- id=6 (val=30): range [30,40] includes only val=30 → cnt=1
+-- id=1 (val=10): range [10,20] includes all val=10 and val=20 peers -> cnt=2 (only first in peer group gets match)
+-- id=2,3 (val=10): already matched by id=1 -> cnt=0
+-- id=4 (val=20): range [20,30] includes all val=20 and val=30 peers -> cnt=2
+-- id=5 (val=20): already matched by id=4 -> cnt=0
+-- id=6 (val=30): range [30,40] includes only val=30 -> cnt=1
 -- Result: [2,0,0,2,0,1]
 SELECT id, val, COUNT(*) OVER w as cnt
 FROM rpr_frame
@@ -483,11 +483,11 @@ ORDER BY id;
 
 -- GROUPS: treats rows with same value as one group (1 FOLLOWING = next group)
 -- Expected result: 1 FOLLOWING means current group + 1 next group
--- id=1 (val=10): groups [val=10, val=20] → cnt=2 (only first in group gets match)
--- id=2,3 (val=10): already matched by id=1 → cnt=0
--- id=4 (val=20): groups [val=20, val=30] → cnt=2
--- id=5 (val=20): already matched by id=4 → cnt=0
--- id=6 (val=30): groups [val=30] (no next group) → cnt=1
+-- id=1 (val=10): groups [val=10, val=20] -> cnt=2 (only first in group gets match)
+-- id=2,3 (val=10): already matched by id=1 -> cnt=0
+-- id=4 (val=20): groups [val=20, val=30] -> cnt=2
+-- id=5 (val=20): already matched by id=4 -> cnt=0
+-- id=6 (val=30): groups [val=30] (no next group) -> cnt=1
 -- Result: [2,0,0,2,0,1]
 SELECT id, val, COUNT(*) OVER w as cnt
 FROM rpr_frame
@@ -2331,7 +2331,7 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND 10 FOLLOWING
 -- ============================================================
 -- Absorption Analysis Tests
 -- ============================================================
--- Tests context absorption optimization (O(n²) → O(n))
+-- Tests context absorption optimization (O(n^2) -> O(n))
 -- Files: rpr.c (computeAbsorbability)
 
 -- Simple Absorbable Pattern: A+ B
-- 
2.50.1 (Apple Git-155)


From d06525db781c3432d488706759c653d00a5e1980 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Thu, 5 Feb 2026 09:41:03 +0900
Subject: [PATCH 1/1] Initialize NFA per-partition counters in
 ExecInitWindowAgg

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

diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 1176df04b2c..1e088615d19 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -3054,6 +3054,8 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
 	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
-- 
2.50.1 (Apple Git-155)


From be455295ab82ccc2ae80d6cb633b114a7c345ae4 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Fri, 13 Feb 2026 21:57:59 +0900
Subject: [PATCH 1/1] Disallow RANGE and GROUPS frame types with row pattern
 recognition

---
 src/backend/parser/parse_rpr.c         | 19 ++++++++++
 src/test/regress/expected/rpr_base.out | 52 ++++++++------------------
 2 files changed, 35 insertions(+), 36 deletions(-)

diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index 048e84bd7bd..80ebf3c33f8 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -68,6 +68,25 @@ transformRPR(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 		return;
 
 	/* Check Frame options */
+
+	/* Frame type must be "ROW" */
+	if (wc->frameOptions & FRAMEOPTION_GROUPS)
+		ereport(ERROR,
+				(errcode(ERRCODE_SYNTAX_ERROR),
+				 errmsg("FRAME option GROUP is not permitted when row pattern recognition is used"),
+				 errhint("Use: ROWS instead"),
+				 parser_errposition(pstate,
+									windef->frameLocation >= 0 ?
+									windef->frameLocation : windef->location)));
+	if (wc->frameOptions & FRAMEOPTION_RANGE)
+		ereport(ERROR,
+				(errcode(ERRCODE_SYNTAX_ERROR),
+				 errmsg("FRAME option RANGE is not permitted when row pattern recognition is used"),
+				 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)
 	{
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index c269ab99651..a1f11bd61ce 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -434,11 +434,10 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > 0
 );
-ERROR:  FRAME must start at CURRENT ROW when row pattern recognition is used
+ERROR:  FRAME option RANGE is not permitted when row pattern recognition is used
 LINE 5:     RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWIN...
             ^
-DETAIL:  Current frame starts with UNBOUNDED PRECEDING.
-HINT:  Use: RANGE BETWEEN CURRENT ROW AND ...
+HINT:  Use: ROWS instead
 -- Expected: ERROR: FRAME must start at current row when row pattern recognition is used
 -- GROUPS frame not starting at CURRENT ROW
 SELECT COUNT(*) OVER w
@@ -449,11 +448,10 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > 0
 );
-ERROR:  FRAME must start at CURRENT ROW when row pattern recognition is used
+ERROR:  FRAME option GROUP is not permitted when row pattern recognition is used
 LINE 5:     GROUPS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWI...
             ^
-DETAIL:  Current frame starts with UNBOUNDED PRECEDING.
-HINT:  Use: GROUPS BETWEEN CURRENT ROW AND ...
+HINT:  Use: ROWS instead
 -- Expected: ERROR: FRAME must start at current row when row pattern recognition is used
 -- Starting with N PRECEDING
 SELECT COUNT(*) OVER w
@@ -614,16 +612,10 @@ WINDOW w AS (
     DEFINE A AS val >= 0, B AS val >= 0
 )
 ORDER BY id;
- id | val | cnt 
-----+-----+-----
-  1 |  10 |   2
-  2 |  10 |   0
-  3 |  10 |   0
-  4 |  20 |   2
-  5 |  20 |   0
-  6 |  30 |   1
-(6 rows)
-
+ERROR:  FRAME option RANGE is not permitted when row pattern recognition is used
+LINE 5:     RANGE BETWEEN CURRENT ROW AND 10 FOLLOWING
+            ^
+HINT:  Use: ROWS instead
 -- GROUPS: treats rows with same value as one group (1 FOLLOWING = next group)
 -- Expected result: 1 FOLLOWING means current group + 1 next group
 -- id=1 (val=10): groups [val=10, val=20] -> cnt=2 (only first in group gets match)
@@ -642,16 +634,10 @@ WINDOW w AS (
     DEFINE A AS val >= 0, B AS val >= 0
 )
 ORDER BY id;
- id | val | cnt 
-----+-----+-----
-  1 |  10 |   2
-  2 |  10 |   0
-  3 |  10 |   0
-  4 |  20 |   2
-  5 |  20 |   0
-  6 |  30 |   1
-(6 rows)
-
+ERROR:  FRAME option GROUP is not permitted when row pattern recognition is used
+LINE 5:     GROUPS BETWEEN CURRENT ROW AND 1 FOLLOWING
+            ^
+HINT:  Use: ROWS instead
 DROP TABLE rpr_frame;
 -- ============================================================
 -- PARTITION BY + FRAME Tests
@@ -697,16 +683,10 @@ WINDOW w AS (
     DEFINE A AS val >= 10, B AS val >= 20
 )
 ORDER BY id;
- id | grp | val | cnt 
-----+-----+-----+-----
-  1 |   1 |  10 |   2
-  2 |   1 |  20 |   2
-  3 |   1 |  30 |   1
-  4 |   2 |  15 |   2
-  5 |   2 |  25 |   2
-  6 |   2 |  35 |   1
-(6 rows)
-
+ERROR:  FRAME option RANGE is not permitted when row pattern recognition is used
+LINE 6:     RANGE BETWEEN CURRENT ROW AND 10 FOLLOWING
+            ^
+HINT:  Use: ROWS instead
 DROP TABLE rpr_partition;
 -- ============================================================
 -- PATTERN Syntax Tests
-- 
2.50.1 (Apple Git-155)


From f3097f9c82b25cb5ed60caa3241e58935e797535 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Thu, 5 Feb 2026 22:31:14 +0900
Subject: [PATCH 1/1] Remove FIXME and normalize Storage values

---
 src/test/regress/expected/rpr_explain.out   |  559 +++---
 src/test/regress/expected/rpr_explain_1.out | 1803 -------------------
 src/test/regress/parallel_schedule          |    2 +-
 src/test/regress/sql/rpr_explain.sql        |  294 ++-
 4 files changed, 521 insertions(+), 2137 deletions(-)
 delete mode 100644 src/test/regress/expected/rpr_explain_1.out

diff --git a/src/test/regress/expected/rpr_explain.out b/src/test/regress/expected/rpr_explain.out
index d8804911c93..b5ceaae53b5 100644
--- a/src/test/regress/expected/rpr_explain.out
+++ b/src/test/regress/expected/rpr_explain.out
@@ -6,6 +6,38 @@
 -- - NFA Contexts: peak, total, absorbed, skipped
 -- - NFA: matched (len min/max/avg), mismatched (len min/max/avg)
 --
+-- Filter function to normalize Storage memory values only (not NFA statistics)
+-- 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 memory size in Storage line only (platform-dependent)
+        -- 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;
+
+        return next ln;
+    end loop;
+end;
+$$;
 -- Setup: Create test tables
 CREATE TEMP TABLE nfa_test (
     id serial,
@@ -41,6 +73,7 @@ VALUES
 -- Section 1: Basic NFA Statistics Tests
 --
 -- Test 1.1: Simple pattern - should show basic statistics
+SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
 FROM nfa_test
@@ -48,14 +81,14 @@ 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'
-);
-                            QUERY PLAN                             
+    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
-   Storage: Memory  Maximum Storage: 17kB
+   Storage: Memory  Maximum Storage: NkB
    NFA States: 2 peak, 101 total, 0 merged
    NFA Contexts: 3 peak, 101 total, 80 pruned
    NFA: 20 matched (len 2/2/2.0), 0 mismatched
@@ -63,6 +96,7 @@ WINDOW w AS (
 (8 rows)
 
 -- Test 1.2: Pattern with no matches - 0 matched
+SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
 FROM nfa_test
@@ -70,14 +104,14 @@ 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'
-);
-                            QUERY PLAN                             
+    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
-   Storage: Memory  Maximum Storage: 17kB
+   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
@@ -85,6 +119,7 @@ WINDOW w AS (
 (8 rows)
 
 -- Test 1.3: Pattern matching every row - high match count
+SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
 FROM nfa_test
@@ -93,13 +128,13 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (R)
     DEFINE R AS TRUE
-);
-                            QUERY PLAN                             
+);');
+                        rpr_explain_filter                         
 -------------------------------------------------------------------
  WindowAgg (actual rows=100.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: r
-   Storage: Memory  Maximum Storage: 17kB
+   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
@@ -110,6 +145,7 @@ WINDOW w AS (
 -- Section 2: State Statistics Tests (peak, total, merged)
 --
 -- Test 2.1: Simple quantifier pattern - A+ with short matches (no merging)
+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)
@@ -118,13 +154,13 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (A+)
     DEFINE A AS v % 2 = 1
-);
-                              QUERY PLAN                              
+);');
+                          rpr_explain_filter                          
 ----------------------------------------------------------------------
  WindowAgg (actual rows=50.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a+"
-   Storage: Memory  Maximum Storage: 17kB
+   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
@@ -132,6 +168,7 @@ WINDOW w AS (
 (8 rows)
 
 -- Test 2.2: Alternation pattern - multiple state branches
+SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
 FROM nfa_test
@@ -140,15 +177,15 @@ WINDOW w AS (
     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'
-);
-                            QUERY PLAN                             
+        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)
-   Storage: Memory  Maximum Storage: 17kB
+   Storage: Memory  Maximum Storage: NkB
    NFA States: 5 peak, 363 total, 0 merged
    NFA Contexts: 3 peak, 101 total, 40 pruned
    NFA: 20 matched (len 2/2/2.0), 40 mismatched (len 2/2/2.0)
@@ -156,6 +193,7 @@ WINDOW w AS (
 (8 rows)
 
 -- Test 2.3: Complex pattern with high state count
+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)
@@ -167,13 +205,13 @@ WINDOW w AS (
         A AS v % 3 = 1,
         B AS v % 3 = 2,
         C AS v % 3 = 0
-);
-                              QUERY PLAN                               
+);');
+                          rpr_explain_filter                           
 -----------------------------------------------------------------------
  WindowAgg (actual rows=100.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a+" b* c+
-   Storage: Memory  Maximum Storage: 17kB
+   Storage: Memory  Maximum Storage: NkB
    NFA States: 5 peak, 235 total, 0 merged
    NFA Contexts: 3 peak, 101 total, 67 pruned
    NFA: 33 matched (len 3/3/3.0), 0 mismatched
@@ -181,6 +219,7 @@ WINDOW w AS (
 (8 rows)
 
 -- Test 2.4: Grouped pattern with quantifier - state merging
+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)
@@ -189,13 +228,13 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN ((A B)+)
     DEFINE A AS v % 2 = 1, B AS v % 2 = 0
-);
-                              QUERY PLAN                              
+);');
+                          rpr_explain_filter                          
 ----------------------------------------------------------------------
  WindowAgg (actual rows=60.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a' b')+"
-   Storage: Memory  Maximum Storage: 18kB
+   Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 91 total, 0 merged
    NFA Contexts: 3 peak, 61 total, 30 pruned
    NFA: 1 matched (len 60/60/60.0), 0 mismatched
@@ -205,6 +244,7 @@ WINDOW w AS (
 
 -- Test 2.5: State explosion pattern - many alternations
 -- Pattern (A|B)(A|B)(A|B)(A|B) can create many parallel states
+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)
@@ -213,13 +253,13 @@ WINDOW w AS (
     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
-);
-                              QUERY PLAN                               
+);');
+                          rpr_explain_filter                           
 -----------------------------------------------------------------------
  WindowAgg (actual rows=100.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a | b a | b a | b a | b a | b a | b a | b a | b)
-   Storage: Memory  Maximum Storage: 17kB
+   Storage: Memory  Maximum Storage: NkB
    NFA States: 17 peak, 632 total, 0 merged
    NFA Contexts: 9 peak, 101 total, 1 pruned
    NFA: 12 matched (len 8/8/8.0), 3 mismatched (len 2/4/3.0)
@@ -228,6 +268,7 @@ WINDOW w AS (
 (9 rows)
 
 -- Test 2.6: High state merging - alternation with plus quantifier
+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)
@@ -236,13 +277,13 @@ WINDOW w AS (
     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
-);
-                              QUERY PLAN                               
+);');
+                          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
-   Storage: Memory  Maximum Storage: 17kB
+   Storage: Memory  Maximum Storage: NkB
    NFA States: 15 peak, 753 total, 0 merged
    NFA Contexts: 5 peak, 101 total, 25 pruned
    NFA: 25 matched (len 4/4/4.0), 0 mismatched
@@ -251,6 +292,7 @@ WINDOW w AS (
 (9 rows)
 
 -- Test 2.7: Nested quantifiers causing state growth
+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)
@@ -259,13 +301,13 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (((A | B)+)+)
     DEFINE A AS v % 3 = 1, B AS v % 3 = 2
-);
-                               QUERY PLAN                               
+);');
+                           rpr_explain_filter                           
 ------------------------------------------------------------------------
  WindowAgg (actual rows=1000.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: ((a | b)+)+
-   Storage: Memory  Maximum Storage: 17kB
+   Storage: Memory  Maximum Storage: NkB
    NFA States: 16 peak, 7334 total, 0 merged
    NFA Contexts: 4 peak, 1001 total, 333 pruned
    NFA: 334 matched (len 1/2/2.0), 0 mismatched
@@ -277,6 +319,7 @@ WINDOW w AS (
 -- Section 3: Context Statistics Tests (peak, total, absorbed, skipped)
 --
 -- Test 3.1: Context absorption with unbounded quantifier at start
+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)
@@ -285,13 +328,13 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
-);
-                              QUERY PLAN                              
+);');
+                          rpr_explain_filter                          
 ----------------------------------------------------------------------
  WindowAgg (actual rows=50.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a+" b
-   Storage: Memory  Maximum Storage: 17kB
+   Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 91 total, 0 merged
    NFA Contexts: 3 peak, 51 total, 10 pruned
    NFA: 10 matched (len 5/5/5.0), 0 mismatched
@@ -300,6 +343,7 @@ WINDOW w AS (
 (9 rows)
 
 -- Test 3.2: No absorption - bounded quantifier
+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)
@@ -308,13 +352,13 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (A{2,4} B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
-);
-                              QUERY PLAN                              
+);');
+                          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
-   Storage: Memory  Maximum Storage: 17kB
+   Storage: Memory  Maximum Storage: NkB
    NFA States: 7 peak, 101 total, 0 merged
    NFA Contexts: 6 peak, 51 total, 10 pruned
    NFA: 10 matched (len 5/5/5.0), 10 mismatched (len 2/2/2.0)
@@ -323,6 +367,7 @@ WINDOW w AS (
 (9 rows)
 
 -- Test 3.3: Contexts skipped by SKIP PAST LAST 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)
@@ -331,13 +376,13 @@ WINDOW w AS (
     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
-);
-                              QUERY PLAN                               
+);');
+                          rpr_explain_filter                           
 -----------------------------------------------------------------------
  WindowAgg (actual rows=100.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a b c
-   Storage: Memory  Maximum Storage: 17kB
+   Storage: Memory  Maximum Storage: NkB
    NFA States: 2 peak, 101 total, 0 merged
    NFA Contexts: 3 peak, 101 total, 90 pruned
    NFA: 10 matched (len 3/3/3.0), 0 mismatched
@@ -345,6 +390,7 @@ WINDOW w AS (
 (8 rows)
 
 -- Test 3.4: High context absorption - unbounded group
+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)
@@ -353,13 +399,13 @@ WINDOW w AS (
     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
-);
-                              QUERY PLAN                               
+);');
+                          rpr_explain_filter                           
 -----------------------------------------------------------------------
  WindowAgg (actual rows=100.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a' b')+" c
-   Storage: Memory  Maximum Storage: 17kB
+   Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 134 total, 0 merged
    NFA Contexts: 3 peak, 101 total, 67 pruned
    NFA: 33 matched (len 3/3/3.0), 0 mismatched
@@ -370,6 +416,7 @@ WINDOW w AS (
 -- Section 4: Match Length Statistics Tests
 --
 -- Test 4.1: Fixed length matches - all same length
+SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
 FROM nfa_test
@@ -378,15 +425,15 @@ WINDOW w AS (
     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'
-);
-                            QUERY PLAN                             
+        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
-   Storage: Memory  Maximum Storage: 17kB
+   Storage: Memory  Maximum Storage: NkB
    NFA States: 2 peak, 101 total, 0 merged
    NFA Contexts: 3 peak, 101 total, 80 pruned
    NFA: 20 matched (len 5/5/5.0), 0 mismatched
@@ -394,6 +441,7 @@ WINDOW w AS (
 (8 rows)
 
 -- Test 4.2: Variable length matches - min/max/avg differ
+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)
@@ -402,13 +450,13 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (A+ B)
     DEFINE A AS v % 10 <> 0, B AS v % 10 = 0
-);
-                              QUERY PLAN                               
+);');
+                          rpr_explain_filter                           
 -----------------------------------------------------------------------
  WindowAgg (actual rows=100.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a+" b
-   Storage: Memory  Maximum Storage: 17kB
+   Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 191 total, 0 merged
    NFA Contexts: 3 peak, 101 total, 10 pruned
    NFA: 10 matched (len 10/10/10.0), 0 mismatched
@@ -417,6 +465,7 @@ WINDOW w AS (
 (9 rows)
 
 -- Test 4.3: Very long matches
+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)
@@ -425,13 +474,13 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (A+ B)
     DEFINE A AS v <= 195, B AS v > 195
-);
-                              QUERY PLAN                               
+);');
+                          rpr_explain_filter                           
 -----------------------------------------------------------------------
  WindowAgg (actual rows=200.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a+" b
-   Storage: Memory  Maximum Storage: 23kB
+   Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 396 total, 0 merged
    NFA Contexts: 3 peak, 201 total, 5 pruned
    NFA: 1 matched (len 196/196/196.0), 0 mismatched
@@ -440,6 +489,7 @@ WINDOW w AS (
 (9 rows)
 
 -- Test 4.4: Mix of short and long matches
+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)
@@ -450,13 +500,13 @@ WINDOW w AS (
     DEFINE
         A AS (v % 20 <> 0) AND (v % 20 <= 10 OR v % 20 > 15),
         B AS v % 20 = 0
-);
-                              QUERY PLAN                               
+);');
+                          rpr_explain_filter                           
 -----------------------------------------------------------------------
  WindowAgg (actual rows=100.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a+" b
-   Storage: Memory  Maximum Storage: 17kB
+   Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 171 total, 0 merged
    NFA Contexts: 3 peak, 101 total, 30 pruned
    NFA: 5 matched (len 5/5/5.0), 5 mismatched (len 11/11/11.0)
@@ -469,28 +519,29 @@ WINDOW w AS (
 --
 -- Test 5.1: Pattern that causes mismatches with length > 1
 -- Mismatch happens when partial match fails after processing multiple rows
+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
+           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'
-);
-                              QUERY PLAN                               
+    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
-   Storage: Memory  Maximum Storage: 17kB
+   Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 151 total, 0 merged
    NFA Contexts: 3 peak, 101 total, 70 pruned
    NFA: 10 matched (len 6/6/6.0), 0 mismatched
@@ -499,18 +550,19 @@ WINDOW w AS (
 (9 rows)
 
 -- Test 5.2: Long partial matches that fail
+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'
+               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
@@ -518,14 +570,14 @@ 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'
-);
-                              QUERY PLAN                              
+    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
-   Storage: Memory  Maximum Storage: 18kB
+   Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 115 total, 0 merged
    NFA Contexts: 3 peak, 61 total, 16 pruned
    NFA: 1 matched (len 30/30/30.0), 1 mismatched (len 26/26/26.0)
@@ -537,6 +589,7 @@ WINDOW w AS (
 -- Section 6: JSON Format Tests
 --
 -- Test 6.1: JSON format output with all statistics
+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)
@@ -545,8 +598,8 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (A+ B+)
     DEFINE A AS v % 3 = 1, B AS v % 3 = 2
-);
-                                 QUERY PLAN                                 
+)');
+                             rpr_explain_filter                             
 ----------------------------------------------------------------------------
  [                                                                         +
    {                                                                       +
@@ -560,7 +613,7 @@ WINDOW w AS (
        "Window": "w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)",+
        "Pattern": "a+\" b+",                                               +
        "Storage": "Memory",                                                +
-       "Maximum Storage": 17,                                              +
+       "Maximum Storage": 0,                                               +
        "NFA States Peak": 3,                                               +
        "NFA States Total": 85,                                             +
        "NFA States Merged": 0,                                             +
@@ -595,6 +648,7 @@ WINDOW w AS (
 (1 row)
 
 -- Test 6.2: JSON format with match length statistics
+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)
@@ -603,8 +657,8 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (A+ B)
     DEFINE A AS v % 10 <> 0, B AS v % 10 = 0
-);
-                                 QUERY PLAN                                 
+)');
+                             rpr_explain_filter                             
 ----------------------------------------------------------------------------
  [                                                                         +
    {                                                                       +
@@ -618,7 +672,7 @@ WINDOW w AS (
        "Window": "w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)",+
        "Pattern": "a+\" b",                                                +
        "Storage": "Memory",                                                +
-       "Maximum Storage": 17,                                              +
+       "Maximum Storage": 0,                                               +
        "NFA States Peak": 3,                                               +
        "NFA States Total": 191,                                            +
        "NFA States Merged": 0,                                             +
@@ -659,6 +713,7 @@ WINDOW w AS (
 -- Section 7: XML Format Tests
 --
 -- Test 7.1: XML format output
+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)
@@ -667,8 +722,8 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (A B)
     DEFINE A AS v % 2 = 1, B AS v % 2 = 0
-);
-                                   QUERY PLAN                                   
+)');
+                               rpr_explain_filter                               
 --------------------------------------------------------------------------------
  <explain xmlns="http://www.postgresql.org/2009/explain";                      +
    <Query>                                                                     +
@@ -682,7 +737,7 @@ WINDOW w AS (
        <Window>w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)</Window>+
        <Pattern>a b</Pattern>                                                  +
        <Storage>Memory</Storage>                                               +
-       <Maximum-Storage>17</Maximum-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>                                +
@@ -720,6 +775,7 @@ WINDOW w AS (
 -- Section 8: Multiple Partitions Tests
 --
 -- Test 8.1: Statistics across multiple partitions
+SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
 FROM (
@@ -733,13 +789,13 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
-);
-                                     QUERY PLAN                                     
+);');
+                                 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
-   Storage: Memory  Maximum Storage: 17kB
+   Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 165 total, 0 merged
    NFA Contexts: 3 peak, 93 total, 18 pruned
    NFA: 18 matched (len 5/5/5.0), 0 mismatched
@@ -753,6 +809,7 @@ WINDOW w AS (
 (14 rows)
 
 -- Test 8.2: Different pattern behavior per partition
+SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
 FROM (
@@ -767,13 +824,13 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (A+ B)
     DEFINE A AS val < 5, B AS val >= 5
-);
-                                                        QUERY PLAN                                                        
+);');
+                                                    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
-   Storage: Memory  Maximum Storage: 17kB
+   Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 77 total, 0 merged
    NFA Contexts: 3 peak, 52 total, 26 pruned
    NFA: 5 matched (len 5/6/5.8), 0 mismatched
@@ -788,6 +845,7 @@ WINDOW w AS (
 -- Section 9: Edge Cases
 --
 -- Test 9.1: Empty result set
+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)
@@ -796,8 +854,8 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (A B)
     DEFINE A AS v = 1, B AS v = 2
-);
-                             QUERY PLAN                              
+);');
+                         rpr_explain_filter                          
 ---------------------------------------------------------------------
  WindowAgg (actual rows=0.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
@@ -806,6 +864,7 @@ WINDOW w AS (
 (4 rows)
 
 -- Test 9.2: Single 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)
@@ -814,13 +873,13 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (A)
     DEFINE A AS TRUE
-);
-                             QUERY PLAN                              
+);');
+                         rpr_explain_filter                          
 ---------------------------------------------------------------------
  WindowAgg (actual rows=1.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a
-   Storage: Memory  Maximum Storage: 17kB
+   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
@@ -828,6 +887,7 @@ WINDOW w AS (
 (8 rows)
 
 -- Test 9.3: Pattern longer than data
+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)
@@ -838,13 +898,13 @@ WINDOW w AS (
     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
-);
-                             QUERY PLAN                              
+);');
+                         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
-   Storage: Memory  Maximum Storage: 17kB
+   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)
@@ -852,6 +912,7 @@ WINDOW w AS (
 (8 rows)
 
 -- Test 9.4: All rows match as single match
+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)
@@ -860,13 +921,13 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (A+)
     DEFINE A AS TRUE
-);
-                              QUERY PLAN                              
+);');
+                          rpr_explain_filter                          
 ----------------------------------------------------------------------
  WindowAgg (actual rows=50.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a+"
-   Storage: Memory  Maximum Storage: 18kB
+   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
@@ -878,6 +939,7 @@ WINDOW w AS (
 -- Section 10: Complex Pattern Tests
 --
 -- Test 10.1: Nested groups
+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)
@@ -886,13 +948,13 @@ WINDOW w AS (
     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
-);
-                              QUERY PLAN                              
+);');
+                          rpr_explain_filter                          
 ----------------------------------------------------------------------
  WindowAgg (actual rows=60.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a' b' c')+"
-   Storage: Memory  Maximum Storage: 18kB
+   Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 81 total, 0 merged
    NFA Contexts: 3 peak, 61 total, 40 pruned
    NFA: 1 matched (len 60/60/60.0), 0 mismatched
@@ -901,6 +963,7 @@ WINDOW w AS (
 (9 rows)
 
 -- Test 10.2: Multiple alternations
+SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
 FROM nfa_test
@@ -909,15 +972,15 @@ WINDOW w AS (
     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'
-);
-                            QUERY PLAN                             
+        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)
-   Storage: Memory  Maximum Storage: 17kB
+   Storage: Memory  Maximum Storage: NkB
    NFA States: 5 peak, 282 total, 0 merged
    NFA Contexts: 3 peak, 101 total, 60 pruned
    NFA: 20 matched (len 2/2/2.0), 20 mismatched (len 2/2/2.0)
@@ -925,6 +988,7 @@ WINDOW w AS (
 (8 rows)
 
 -- Test 10.3: Optional elements
+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)
@@ -933,13 +997,13 @@ WINDOW w AS (
     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
-);
-                              QUERY PLAN                              
+);');
+                          rpr_explain_filter                          
 ----------------------------------------------------------------------
  WindowAgg (actual rows=50.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a b? c
-   Storage: Memory  Maximum Storage: 17kB
+   Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 64 total, 0 merged
    NFA Contexts: 3 peak, 51 total, 37 pruned
    NFA: 12 matched (len 3/3/3.0), 1 mismatched (len 2/2/2.0)
@@ -947,6 +1011,7 @@ WINDOW w AS (
 (8 rows)
 
 -- Test 10.4: Bounded quantifiers
+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)
@@ -955,13 +1020,13 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (A{2,5} B)
     DEFINE A AS v % 10 <> 0, B AS v % 10 = 0
-);
-                              QUERY PLAN                               
+);');
+                          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
-   Storage: Memory  Maximum Storage: 17kB
+   Storage: Memory  Maximum Storage: NkB
    NFA States: 9 peak, 311 total, 0 merged
    NFA Contexts: 7 peak, 101 total, 10 pruned
    NFA: 10 matched (len 6/6/6.0), 50 mismatched (len 2/6/5.2)
@@ -970,6 +1035,7 @@ WINDOW w AS (
 (9 rows)
 
 -- Test 10.5: Star quantifier
+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)
@@ -978,13 +1044,13 @@ WINDOW w AS (
     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
-);
-                              QUERY PLAN                              
+);');
+                          rpr_explain_filter                          
 ----------------------------------------------------------------------
  WindowAgg (actual rows=50.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a b* c
-   Storage: Memory  Maximum Storage: 17kB
+   Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 91 total, 0 merged
    NFA Contexts: 3 peak, 51 total, 45 pruned
    NFA: 5 matched (len 9/9/9.0), 0 mismatched
@@ -995,6 +1061,7 @@ WINDOW w AS (
 -- Section 11: Real-world Pattern Examples
 --
 -- Test 11.1: Stock price pattern - V-shape (down then up)
+SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
 FROM nfa_complex
@@ -1002,14 +1069,14 @@ 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'
-);
-                            QUERY PLAN                             
+    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+
-   Storage: Memory  Maximum Storage: 17kB
+   Storage: Memory  Maximum Storage: NkB
    NFA States: 4 peak, 58 total, 0 merged
    NFA Contexts: 3 peak, 31 total, 17 pruned
    NFA: 3 matched (len 3/14/8.0), 1 mismatched (len 3/3/3.0)
@@ -1018,6 +1085,7 @@ WINDOW w AS (
 (9 rows)
 
 -- Test 11.2: Stock price pattern - peak (up, stable, down)
+SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
 FROM nfa_complex
@@ -1025,14 +1093,14 @@ 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'
-);
-                            QUERY PLAN                             
+    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+
-   Storage: Memory  Maximum Storage: 17kB
+   Storage: Memory  Maximum Storage: NkB
    NFA States: 5 peak, 76 total, 0 merged
    NFA Contexts: 3 peak, 31 total, 14 pruned
    NFA: 4 matched (len 3/11/7.2), 0 mismatched
@@ -1041,12 +1109,7 @@ WINDOW w AS (
 (9 rows)
 
 -- Test 11.3: Consecutive increasing values (using PREV)
--- FIXME: The original pattern was:
---   DEFINE A AS v > PREV(v) OR PREV(v) IS NULL
--- This causes "ERROR: unrecognized node type: 15" (T_FuncExpr) because
--- NullTest(FuncExpr(PREV)) is not properly handled somewhere in the planner.
--- The expression v > PREV(v) works fine, but PREV(v) IS NULL fails.
--- Using COALESCE(PREV(v), 0) as a workaround until the bug is fixed.
+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)
@@ -1054,14 +1117,14 @@ WINDOW w AS (
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (A{3,})
-    DEFINE A AS v > COALESCE(PREV(v), 0)
-);
-                              QUERY PLAN                              
+    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,}"
-   Storage: Memory  Maximum Storage: 18kB
+   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
@@ -1073,6 +1136,7 @@ WINDOW w AS (
 -- Section 12: Performance-oriented Tests
 --
 -- Test 12.1: Large dataset with simple 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)
@@ -1081,13 +1145,13 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (A B)
     DEFINE A AS v % 2 = 1, B AS v % 2 = 0
-);
-                               QUERY PLAN                               
+);');
+                           rpr_explain_filter                           
 ------------------------------------------------------------------------
  WindowAgg (actual rows=1000.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a b
-   Storage: Memory  Maximum Storage: 17kB
+   Storage: Memory  Maximum Storage: NkB
    NFA States: 2 peak, 1001 total, 0 merged
    NFA Contexts: 3 peak, 1001 total, 500 pruned
    NFA: 500 matched (len 2/2/2.0), 0 mismatched
@@ -1095,6 +1159,7 @@ WINDOW w AS (
 (8 rows)
 
 -- Test 12.2: Large dataset with absorption
+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)
@@ -1103,13 +1168,13 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (A+ B)
     DEFINE A AS v % 100 <> 0, B AS v % 100 = 0
-);
-                               QUERY PLAN                               
+);');
+                           rpr_explain_filter                           
 ------------------------------------------------------------------------
  WindowAgg (actual rows=1000.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a+" b
-   Storage: Memory  Maximum Storage: 20kB
+   Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 1991 total, 0 merged
    NFA Contexts: 3 peak, 1001 total, 10 pruned
    NFA: 10 matched (len 100/100/100.0), 0 mismatched
@@ -1118,6 +1183,7 @@ WINDOW w AS (
 (9 rows)
 
 -- Test 12.3: High state merge ratio
+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)
@@ -1126,13 +1192,13 @@ WINDOW w AS (
     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
-);
-                              QUERY PLAN                               
+);');
+                          rpr_explain_filter                           
 -----------------------------------------------------------------------
  WindowAgg (actual rows=500.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a | b)+ c
-   Storage: Memory  Maximum Storage: 17kB
+   Storage: Memory  Maximum Storage: NkB
    NFA States: 8 peak, 2004 total, 0 merged
    NFA Contexts: 4 peak, 501 total, 167 pruned
    NFA: 166 matched (len 3/3/3.0), 1 mismatched (len 2/2/2.0)
@@ -1144,6 +1210,7 @@ WINDOW w AS (
 -- Section 13: INITIAL vs no INITIAL comparison
 --
 -- Test 13.1: With INITIAL keyword
+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)
@@ -1153,13 +1220,13 @@ WINDOW w AS (
     INITIAL
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
-);
-                              QUERY PLAN                              
+);');
+                          rpr_explain_filter                          
 ----------------------------------------------------------------------
  WindowAgg (actual rows=50.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a+" b
-   Storage: Memory  Maximum Storage: 17kB
+   Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 91 total, 0 merged
    NFA Contexts: 3 peak, 51 total, 10 pruned
    NFA: 10 matched (len 5/5/5.0), 0 mismatched
@@ -1168,6 +1235,7 @@ WINDOW w AS (
 (9 rows)
 
 -- Test 13.2: Without INITIAL keyword (same behavior currently)
+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)
@@ -1176,13 +1244,13 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
-);
-                              QUERY PLAN                              
+);');
+                          rpr_explain_filter                          
 ----------------------------------------------------------------------
  WindowAgg (actual rows=50.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a+" b
-   Storage: Memory  Maximum Storage: 17kB
+   Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 91 total, 0 merged
    NFA Contexts: 3 peak, 51 total, 10 pruned
    NFA: 10 matched (len 5/5/5.0), 0 mismatched
@@ -1194,6 +1262,7 @@ WINDOW w AS (
 -- Section 14: Quantifier Variations
 --
 -- Test 14.1: Plus quantifier
+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)
@@ -1202,13 +1271,13 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (A+)
     DEFINE A AS v % 4 <> 0
-);
-                              QUERY PLAN                              
+);');
+                          rpr_explain_filter                          
 ----------------------------------------------------------------------
  WindowAgg (actual rows=40.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a+"
-   Storage: Memory  Maximum Storage: 17kB
+   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
@@ -1217,6 +1286,7 @@ WINDOW w AS (
 (9 rows)
 
 -- Test 14.2: Star quantifier (zero or more)
+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)
@@ -1225,13 +1295,13 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (A* B)
     DEFINE A AS v % 4 IN (1, 2), B AS v % 4 = 3
-);
-                              QUERY PLAN                              
+);');
+                          rpr_explain_filter                          
 ----------------------------------------------------------------------
  WindowAgg (actual rows=40.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a*" b
-   Storage: Memory  Maximum Storage: 17kB
+   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
@@ -1240,6 +1310,7 @@ WINDOW w AS (
 (9 rows)
 
 -- Test 14.3: Question mark (zero or one)
+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)
@@ -1248,13 +1319,13 @@ WINDOW w AS (
     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
-);
-                              QUERY PLAN                              
+);');
+                          rpr_explain_filter                          
 ----------------------------------------------------------------------
  WindowAgg (actual rows=40.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a? b c
-   Storage: Memory  Maximum Storage: 17kB
+   Storage: Memory  Maximum Storage: NkB
    NFA States: 4 peak, 82 total, 0 merged
    NFA Contexts: 4 peak, 41 total, 20 pruned
    NFA: 10 matched (len 3/3/3.0), 0 mismatched
@@ -1263,6 +1334,7 @@ WINDOW w AS (
 (9 rows)
 
 -- Test 14.4: Exact count {n}
+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)
@@ -1271,13 +1343,13 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (A{3} B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
-);
-                              QUERY PLAN                              
+);');
+                          rpr_explain_filter                          
 ----------------------------------------------------------------------
  WindowAgg (actual rows=50.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a{3} b
-   Storage: Memory  Maximum Storage: 17kB
+   Storage: Memory  Maximum Storage: NkB
    NFA States: 4 peak, 51 total, 0 merged
    NFA Contexts: 5 peak, 51 total, 10 pruned
    NFA: 10 matched (len 4/4/4.0), 30 mismatched (len 2/4/3.0)
@@ -1285,6 +1357,7 @@ WINDOW w AS (
 (8 rows)
 
 -- Test 14.5: Range {n,m}
+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)
@@ -1293,13 +1366,13 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (A{2,4} B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
-);
-                              QUERY PLAN                              
+);');
+                          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
-   Storage: Memory  Maximum Storage: 17kB
+   Storage: Memory  Maximum Storage: NkB
    NFA States: 7 peak, 101 total, 0 merged
    NFA Contexts: 6 peak, 51 total, 10 pruned
    NFA: 10 matched (len 5/5/5.0), 10 mismatched (len 2/2/2.0)
@@ -1308,6 +1381,7 @@ WINDOW w AS (
 (9 rows)
 
 -- Test 14.6: At least {n,}
+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)
@@ -1316,13 +1390,13 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (A{3,} B)
     DEFINE A AS v % 10 <> 0, B AS v % 10 = 0
-);
-                              QUERY PLAN                              
+);');
+                          rpr_explain_filter                          
 ----------------------------------------------------------------------
  WindowAgg (actual rows=50.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a{3,}" b
-   Storage: Memory  Maximum Storage: 17kB
+   Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 86 total, 0 merged
    NFA Contexts: 3 peak, 51 total, 5 pruned
    NFA: 5 matched (len 10/10/10.0), 0 mismatched
@@ -1335,6 +1409,7 @@ WINDOW w AS (
 --
 -- Test 15.1: Verify state count accuracy
 -- Pattern A+ B with 20 rows should show predictable state behavior
+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)
@@ -1343,13 +1418,13 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
-);
-                              QUERY PLAN                              
+);');
+                          rpr_explain_filter                          
 ----------------------------------------------------------------------
  WindowAgg (actual rows=20.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a+" b
-   Storage: Memory  Maximum Storage: 17kB
+   Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 37 total, 0 merged
    NFA Contexts: 3 peak, 21 total, 4 pruned
    NFA: 4 matched (len 5/5/5.0), 0 mismatched
@@ -1358,6 +1433,7 @@ WINDOW w AS (
 (9 rows)
 
 -- Test 15.2: Verify context count with known absorption
+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)
@@ -1366,13 +1442,13 @@ WINDOW w AS (
     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
-);
-                              QUERY PLAN                              
+);');
+                          rpr_explain_filter                          
 ----------------------------------------------------------------------
  WindowAgg (actual rows=30.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a+" b c
-   Storage: Memory  Maximum Storage: 17kB
+   Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 52 total, 0 merged
    NFA Contexts: 3 peak, 31 total, 9 pruned
    NFA: 3 matched (len 9/9/9.0), 0 mismatched
@@ -1381,6 +1457,7 @@ WINDOW w AS (
 (9 rows)
 
 -- Test 15.3: Verify match length with fixed-length 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)
@@ -1389,13 +1466,13 @@ WINDOW w AS (
     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
-);
-                              QUERY PLAN                              
+);');
+                          rpr_explain_filter                          
 ----------------------------------------------------------------------
  WindowAgg (actual rows=30.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a b c
-   Storage: Memory  Maximum Storage: 17kB
+   Storage: Memory  Maximum Storage: NkB
    NFA States: 2 peak, 31 total, 0 merged
    NFA Contexts: 3 peak, 31 total, 20 pruned
    NFA: 10 matched (len 3/3/3.0), 0 mismatched
@@ -1406,6 +1483,7 @@ WINDOW w AS (
 -- Section 16: Alternation Pattern Tests
 --
 -- Test 16.1: Simple alternation
+SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
 FROM nfa_test
@@ -1413,14 +1491,14 @@ 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'
-);
-                            QUERY PLAN                             
+    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
-   Storage: Memory  Maximum Storage: 17kB
+   Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 202 total, 0 merged
    NFA Contexts: 3 peak, 101 total, 60 pruned
    NFA: 20 matched (len 2/2/2.0), 20 mismatched (len 2/2/2.0)
@@ -1428,6 +1506,7 @@ WINDOW w AS (
 (8 rows)
 
 -- Test 16.2: Multiple items in alternation
+SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
 FROM nfa_test
@@ -1436,15 +1515,15 @@ WINDOW w AS (
     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'
-);
-                            QUERY PLAN                             
+        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
-   Storage: Memory  Maximum Storage: 17kB
+   Storage: Memory  Maximum Storage: NkB
    NFA States: 5 peak, 404 total, 0 merged
    NFA Contexts: 3 peak, 101 total, 20 pruned
    NFA: 20 matched (len 2/2/2.0), 60 mismatched (len 2/2/2.0)
@@ -1452,6 +1531,7 @@ WINDOW w AS (
 (8 rows)
 
 -- Test 16.3: Alternation with quantifiers
+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)
@@ -1460,13 +1540,13 @@ WINDOW w AS (
     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
-);
-                              QUERY PLAN                              
+);');
+                          rpr_explain_filter                          
 ----------------------------------------------------------------------
  WindowAgg (actual rows=50.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a | b)+ c
-   Storage: Memory  Maximum Storage: 17kB
+   Storage: Memory  Maximum Storage: NkB
    NFA States: 8 peak, 204 total, 0 merged
    NFA Contexts: 4 peak, 51 total, 17 pruned
    NFA: 16 matched (len 3/3/3.0), 1 mismatched (len 2/2/2.0)
@@ -1478,6 +1558,7 @@ WINDOW w AS (
 -- Section 17: Group Pattern Tests
 --
 -- Test 17.1: Simple group
+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)
@@ -1486,13 +1567,13 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN ((A B)+)
     DEFINE A AS v % 2 = 1, B AS v % 2 = 0
-);
-                              QUERY PLAN                              
+);');
+                          rpr_explain_filter                          
 ----------------------------------------------------------------------
  WindowAgg (actual rows=40.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a' b')+"
-   Storage: Memory  Maximum Storage: 18kB
+   Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 61 total, 0 merged
    NFA Contexts: 3 peak, 41 total, 20 pruned
    NFA: 1 matched (len 40/40/40.0), 0 mismatched
@@ -1501,6 +1582,7 @@ WINDOW w AS (
 (9 rows)
 
 -- Test 17.2: Group with bounded quantifier
+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)
@@ -1509,13 +1591,13 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN ((A B){2,4})
     DEFINE A AS v % 2 = 1, B AS v % 2 = 0
-);
-                              QUERY PLAN                              
+);');
+                          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}
-   Storage: Memory  Maximum Storage: 17kB
+   Storage: Memory  Maximum Storage: NkB
    NFA States: 7 peak, 66 total, 0 merged
    NFA Contexts: 6 peak, 41 total, 20 pruned
    NFA: 5 matched (len 8/8/8.0), 0 mismatched
@@ -1524,6 +1606,7 @@ WINDOW w AS (
 (9 rows)
 
 -- Test 17.3: Nested groups
+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)
@@ -1532,13 +1615,13 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (((A B){2})+)
     DEFINE A AS v % 2 = 1, B AS v % 2 = 0
-);
-                              QUERY PLAN                              
+);');
+                          rpr_explain_filter                          
 ----------------------------------------------------------------------
  WindowAgg (actual rows=60.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: ((a b){2})+
-   Storage: Memory  Maximum Storage: 18kB
+   Storage: Memory  Maximum Storage: NkB
    NFA States: 60 peak, 286 total, 0 merged
    NFA Contexts: 32 peak, 61 total, 30 pruned
    NFA: 1 matched (len 60/60/60.0), 1 mismatched (len 2/2/2.0)
@@ -1550,6 +1633,7 @@ WINDOW w AS (
 -- Section 18: Window Function Combinations
 --
 -- Test 18.1: count(*) with 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)
@@ -1558,13 +1642,13 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
-);
-                              QUERY PLAN                              
+);');
+                          rpr_explain_filter                          
 ----------------------------------------------------------------------
  WindowAgg (actual rows=30.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a+" b
-   Storage: Memory  Maximum Storage: 17kB
+   Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 55 total, 0 merged
    NFA Contexts: 3 peak, 31 total, 6 pruned
    NFA: 6 matched (len 5/5/5.0), 0 mismatched
@@ -1573,6 +1657,7 @@ WINDOW w AS (
 (9 rows)
 
 -- Test 18.2: first_value with 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)
@@ -1581,13 +1666,13 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
-);
-                              QUERY PLAN                              
+);');
+                          rpr_explain_filter                          
 ----------------------------------------------------------------------
  WindowAgg (actual rows=30.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a+" b
-   Storage: Memory  Maximum Storage: 17kB
+   Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 55 total, 0 merged
    NFA Contexts: 3 peak, 31 total, 6 pruned
    NFA: 6 matched (len 5/5/5.0), 0 mismatched
@@ -1596,6 +1681,7 @@ WINDOW w AS (
 (9 rows)
 
 -- Test 18.3: last_value with 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)
@@ -1604,13 +1690,13 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
-);
-                              QUERY PLAN                              
+);');
+                          rpr_explain_filter                          
 ----------------------------------------------------------------------
  WindowAgg (actual rows=30.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a+" b
-   Storage: Memory  Maximum Storage: 17kB
+   Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 55 total, 0 merged
    NFA Contexts: 3 peak, 31 total, 6 pruned
    NFA: 6 matched (len 5/5/5.0), 0 mismatched
@@ -1619,6 +1705,7 @@ WINDOW w AS (
 (9 rows)
 
 -- Test 18.4: Multiple window functions
+SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT
     count(*) OVER w,
@@ -1630,13 +1717,13 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
-);
-                              QUERY PLAN                              
+);');
+                          rpr_explain_filter                          
 ----------------------------------------------------------------------
  WindowAgg (actual rows=30.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a+" b
-   Storage: Memory  Maximum Storage: 17kB
+   Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 55 total, 0 merged
    NFA Contexts: 3 peak, 31 total, 6 pruned
    NFA: 6 matched (len 5/5/5.0), 0 mismatched
@@ -1648,6 +1735,7 @@ WINDOW w AS (
 -- Section 19: DEFINE Expression Variations
 --
 -- Test 19.1: Complex boolean expressions
+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)
@@ -1658,13 +1746,13 @@ WINDOW w AS (
     DEFINE
         A AS (v % 5 <> 0) AND (v % 3 <> 0),
         B AS (v % 5 = 0) OR (v % 3 = 0)
-);
-                              QUERY PLAN                              
+);');
+                          rpr_explain_filter                          
 ----------------------------------------------------------------------
  WindowAgg (actual rows=50.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a+" b
-   Storage: Memory  Maximum Storage: 17kB
+   Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 78 total, 0 merged
    NFA Contexts: 3 peak, 51 total, 23 pruned
    NFA: 17 matched (len 2/3/2.6), 0 mismatched
@@ -1673,6 +1761,7 @@ WINDOW w AS (
 (9 rows)
 
 -- Test 19.2: Using PREV function
+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)
@@ -1684,13 +1773,13 @@ WINDOW w AS (
         S AS TRUE,
         U AS v > PREV(v),
         D AS v < PREV(v)
-);
-                              QUERY PLAN                              
+);');
+                          rpr_explain_filter                          
 ----------------------------------------------------------------------
  WindowAgg (actual rows=30.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: s u+ d+
-   Storage: Memory  Maximum Storage: 17kB
+   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)
@@ -1698,6 +1787,7 @@ WINDOW w AS (
 (8 rows)
 
 -- Test 19.3: Using NULL comparisons
+SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
 FROM (
@@ -1709,13 +1799,13 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (A+ B)
     DEFINE A AS v IS NOT NULL, B AS v IS NULL
-);
-                              QUERY PLAN                              
+);');
+                          rpr_explain_filter                          
 ----------------------------------------------------------------------
  WindowAgg (actual rows=30.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a+" b
-   Storage: Memory  Maximum Storage: 17kB
+   Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 55 total, 0 merged
    NFA Contexts: 3 peak, 31 total, 6 pruned
    NFA: 6 matched (len 5/5/5.0), 0 mismatched
@@ -1727,6 +1817,7 @@ WINDOW w AS (
 -- Section 20: Large Scale Statistics Verification
 --
 -- Test 20.1: 500 rows - verify statistics scale correctly
+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)
@@ -1735,13 +1826,13 @@ WINDOW w AS (
     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
-);
-                              QUERY PLAN                               
+);');
+                          rpr_explain_filter                           
 -----------------------------------------------------------------------
  WindowAgg (actual rows=500.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a+" b c
-   Storage: Memory  Maximum Storage: 17kB
+   Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 851 total, 0 merged
    NFA Contexts: 3 peak, 501 total, 151 pruned
    NFA: 50 matched (len 8/9/9.0), 0 mismatched
@@ -1750,6 +1841,7 @@ WINDOW w AS (
 (9 rows)
 
 -- Test 20.2: High match count scenario
+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)
@@ -1758,13 +1850,13 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (A B)
     DEFINE A AS v % 2 = 1, B AS v % 2 = 0
-);
-                              QUERY PLAN                               
+);');
+                          rpr_explain_filter                           
 -----------------------------------------------------------------------
  WindowAgg (actual rows=500.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a b
-   Storage: Memory  Maximum Storage: 17kB
+   Storage: Memory  Maximum Storage: NkB
    NFA States: 2 peak, 501 total, 0 merged
    NFA Contexts: 3 peak, 501 total, 250 pruned
    NFA: 250 matched (len 2/2/2.0), 0 mismatched
@@ -1772,6 +1864,7 @@ WINDOW w AS (
 (8 rows)
 
 -- Test 20.3: High skip count scenario
+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)
@@ -1785,13 +1878,13 @@ WINDOW w AS (
         C AS v % 100 = 3,
         D AS v % 100 = 4,
         E AS v % 100 = 5
-);
-                              QUERY PLAN                               
+);');
+                          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
-   Storage: Memory  Maximum Storage: 17kB
+   Storage: Memory  Maximum Storage: NkB
    NFA States: 2 peak, 501 total, 0 merged
    NFA Contexts: 3 peak, 501 total, 495 pruned
    NFA: 5 matched (len 5/5/5.0), 0 mismatched
diff --git a/src/test/regress/expected/rpr_explain_1.out b/src/test/regress/expected/rpr_explain_1.out
deleted file mode 100644
index 1c1bbda42b2..00000000000
--- a/src/test/regress/expected/rpr_explain_1.out
+++ /dev/null
@@ -1,1803 +0,0 @@
---
--- Test: EXPLAIN ANALYZE output for Row Pattern Recognition NFA statistics
---
--- This file tests the NFA statistics shown in EXPLAIN ANALYZE output:
--- - NFA States: peak, total, merged
--- - NFA Contexts: peak, total, absorbed, skipped
--- - NFA: matched (len min/max/avg), mismatched (len min/max/avg)
---
--- Setup: Create test tables
-CREATE TEMP TABLE nfa_test (
-    id serial,
-    v int,
-    cat char(1)
-);
--- Insert test data: 100 rows with predictable pattern
-INSERT INTO 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 TEMP TABLE nfa_complex (
-    id serial,
-    price int,
-    trend char(1)  -- U=up, D=down, S=stable
-);
-INSERT INTO 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');
---
--- Section 1: Basic NFA Statistics Tests
---
--- Test 1.1: Simple pattern - should show basic statistics
-EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
-SELECT count(*) OVER w
-FROM 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'
-);
-                            QUERY PLAN                             
--------------------------------------------------------------------
- WindowAgg (actual rows=100.00 loops=1)
-   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: a b
-   Storage: Memory  Maximum Storage: 17kB
-   NFA States: 2 peak, 101 total, 0 merged
-   NFA Contexts: 3 peak, 101 total, 80 pruned
-   NFA: 20 matched (len 2/2/2.0), 0 mismatched
-   ->  Seq Scan on nfa_test (actual rows=100.00 loops=1)
-(8 rows)
-
--- Test 1.2: Pattern with no matches - 0 matched
-EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
-SELECT count(*) OVER w
-FROM 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'
-);
-                            QUERY PLAN                             
--------------------------------------------------------------------
- WindowAgg (actual rows=100.00 loops=1)
-   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: x y z
-   Storage: Memory  Maximum Storage: 17kB
-   NFA States: 1 peak, 101 total, 0 merged
-   NFA Contexts: 2 peak, 101 total, 100 pruned
-   NFA: 0 matched, 0 mismatched
-   ->  Seq Scan on nfa_test (actual rows=100.00 loops=1)
-(8 rows)
-
--- Test 1.3: Pattern matching every row - high match count
-EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
-SELECT count(*) OVER w
-FROM nfa_test
-WINDOW w AS (
-    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    AFTER MATCH SKIP PAST LAST ROW
-    PATTERN (R)
-    DEFINE R AS TRUE
-);
-                            QUERY PLAN                             
--------------------------------------------------------------------
- WindowAgg (actual rows=100.00 loops=1)
-   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: r
-   Storage: Memory  Maximum Storage: 17kB
-   NFA States: 2 peak, 101 total, 0 merged
-   NFA Contexts: 2 peak, 101 total, 0 pruned
-   NFA: 100 matched (len 1/1/1.0), 0 mismatched
-   ->  Seq Scan on nfa_test (actual rows=100.00 loops=1)
-(8 rows)
-
---
--- Section 2: State Statistics Tests (peak, total, merged)
---
--- Test 2.1: Simple quantifier pattern - A+ with short matches (no merging)
-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
-);
-                              QUERY PLAN                              
-----------------------------------------------------------------------
- WindowAgg (actual rows=50.00 loops=1)
-   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: a+"
-   Storage: Memory  Maximum Storage: 17kB
-   NFA States: 3 peak, 76 total, 0 merged
-   NFA Contexts: 3 peak, 51 total, 25 pruned
-   NFA: 25 matched (len 1/1/1.0), 0 mismatched
-   ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
-(8 rows)
-
--- Test 2.2: Alternation pattern - multiple state branches
-EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
-SELECT count(*) OVER w
-FROM 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'
-);
-                            QUERY PLAN                             
--------------------------------------------------------------------
- WindowAgg (actual rows=100.00 loops=1)
-   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: (a | b | c d | e)
-   Storage: Memory  Maximum Storage: 17kB
-   NFA States: 5 peak, 363 total, 0 merged
-   NFA Contexts: 3 peak, 101 total, 40 pruned
-   NFA: 20 matched (len 2/2/2.0), 40 mismatched (len 2/2/2.0)
-   ->  Seq Scan on nfa_test (actual rows=100.00 loops=1)
-(8 rows)
-
--- Test 2.3: Complex pattern with high state count
-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
-);
-                              QUERY PLAN                               
------------------------------------------------------------------------
- WindowAgg (actual rows=100.00 loops=1)
-   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: a+" b* c+
-   Storage: Memory  Maximum Storage: 17kB
-   NFA States: 5 peak, 235 total, 0 merged
-   NFA Contexts: 3 peak, 101 total, 67 pruned
-   NFA: 33 matched (len 3/3/3.0), 0 mismatched
-   ->  Function Scan on generate_series s (actual rows=100.00 loops=1)
-(8 rows)
-
--- Test 2.4: Grouped pattern with quantifier - state merging
-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
-);
-                              QUERY PLAN                              
-----------------------------------------------------------------------
- WindowAgg (actual rows=60.00 loops=1)
-   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: (a' b')+"
-   Storage: Memory  Maximum Storage: 19kB
-   NFA States: 3 peak, 91 total, 0 merged
-   NFA Contexts: 3 peak, 61 total, 30 pruned
-   NFA: 1 matched (len 60/60/60.0), 0 mismatched
-   NFA: 29 absorbed (len 1/1/1.0), 0 skipped
-   ->  Function Scan on generate_series s (actual rows=60.00 loops=1)
-(9 rows)
-
--- Test 2.5: State explosion pattern - many alternations
--- Pattern (A|B)(A|B)(A|B)(A|B) can create many parallel states
-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
-);
-                              QUERY PLAN                               
------------------------------------------------------------------------
- WindowAgg (actual rows=100.00 loops=1)
-   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: (a | b a | b a | b a | b a | b a | b a | b a | b)
-   Storage: Memory  Maximum Storage: 17kB
-   NFA States: 17 peak, 632 total, 0 merged
-   NFA Contexts: 9 peak, 101 total, 1 pruned
-   NFA: 12 matched (len 8/8/8.0), 3 mismatched (len 2/4/3.0)
-   NFA: 0 absorbed, 84 skipped (len 1/7/4.0)
-   ->  Function Scan on generate_series s (actual rows=100.00 loops=1)
-(9 rows)
-
--- Test 2.6: High state merging - alternation with plus quantifier
-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
-);
-                              QUERY PLAN                               
------------------------------------------------------------------------
- WindowAgg (actual rows=100.00 loops=1)
-   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: (a | b | c)+ d
-   Storage: Memory  Maximum Storage: 17kB
-   NFA States: 15 peak, 753 total, 0 merged
-   NFA Contexts: 5 peak, 101 total, 25 pruned
-   NFA: 25 matched (len 4/4/4.0), 0 mismatched
-   NFA: 0 absorbed, 50 skipped (len 2/3/2.5)
-   ->  Function Scan on generate_series s (actual rows=100.00 loops=1)
-(9 rows)
-
--- Test 2.7: Nested quantifiers causing state growth
-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
-);
-                               QUERY PLAN                               
-------------------------------------------------------------------------
- WindowAgg (actual rows=1000.00 loops=1)
-   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: ((a | b)+)+
-   Storage: Memory  Maximum Storage: 17kB
-   NFA States: 16 peak, 7334 total, 0 merged
-   NFA Contexts: 4 peak, 1001 total, 333 pruned
-   NFA: 334 matched (len 1/2/2.0), 0 mismatched
-   NFA: 0 absorbed, 333 skipped (len 2/2/2.0)
-   ->  Function Scan on generate_series s (actual rows=1000.00 loops=1)
-(9 rows)
-
---
--- Section 3: Context Statistics Tests (peak, total, absorbed, skipped)
---
--- Test 3.1: Context absorption with unbounded quantifier at start
-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
-);
-                              QUERY PLAN                              
-----------------------------------------------------------------------
- WindowAgg (actual rows=50.00 loops=1)
-   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: a+" b
-   Storage: Memory  Maximum Storage: 17kB
-   NFA States: 3 peak, 91 total, 0 merged
-   NFA Contexts: 3 peak, 51 total, 10 pruned
-   NFA: 10 matched (len 5/5/5.0), 0 mismatched
-   NFA: 30 absorbed (len 1/1/1.0), 0 skipped
-   ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
-(9 rows)
-
--- Test 3.2: No absorption - bounded quantifier
-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
-);
-                              QUERY PLAN                              
-----------------------------------------------------------------------
- WindowAgg (actual rows=50.00 loops=1)
-   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: a{2,4} b
-   Storage: Memory  Maximum Storage: 17kB
-   NFA States: 7 peak, 101 total, 0 merged
-   NFA Contexts: 6 peak, 51 total, 10 pruned
-   NFA: 10 matched (len 5/5/5.0), 10 mismatched (len 2/2/2.0)
-   NFA: 0 absorbed, 20 skipped (len 3/4/3.5)
-   ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
-(9 rows)
-
--- Test 3.3: Contexts skipped by SKIP PAST LAST ROW
-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
-);
-                              QUERY PLAN                               
------------------------------------------------------------------------
- WindowAgg (actual rows=100.00 loops=1)
-   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: a b c
-   Storage: Memory  Maximum Storage: 17kB
-   NFA States: 2 peak, 101 total, 0 merged
-   NFA Contexts: 3 peak, 101 total, 90 pruned
-   NFA: 10 matched (len 3/3/3.0), 0 mismatched
-   ->  Function Scan on generate_series s (actual rows=100.00 loops=1)
-(8 rows)
-
--- Test 3.4: High context absorption - unbounded group
-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
-);
-                              QUERY PLAN                               
------------------------------------------------------------------------
- WindowAgg (actual rows=100.00 loops=1)
-   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: (a' b')+" c
-   Storage: Memory  Maximum Storage: 17kB
-   NFA States: 3 peak, 134 total, 0 merged
-   NFA Contexts: 3 peak, 101 total, 67 pruned
-   NFA: 33 matched (len 3/3/3.0), 0 mismatched
-   ->  Function Scan on generate_series s (actual rows=100.00 loops=1)
-(8 rows)
-
---
--- Section 4: Match Length Statistics Tests
---
--- Test 4.1: Fixed length matches - all same length
-EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
-SELECT count(*) OVER w
-FROM 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'
-);
-                            QUERY PLAN                             
--------------------------------------------------------------------
- WindowAgg (actual rows=100.00 loops=1)
-   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: a b c d e
-   Storage: Memory  Maximum Storage: 17kB
-   NFA States: 2 peak, 101 total, 0 merged
-   NFA Contexts: 3 peak, 101 total, 80 pruned
-   NFA: 20 matched (len 5/5/5.0), 0 mismatched
-   ->  Seq Scan on nfa_test (actual rows=100.00 loops=1)
-(8 rows)
-
--- Test 4.2: Variable length matches - min/max/avg differ
-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
-);
-                              QUERY PLAN                               
------------------------------------------------------------------------
- WindowAgg (actual rows=100.00 loops=1)
-   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: a+" b
-   Storage: Memory  Maximum Storage: 17kB
-   NFA States: 3 peak, 191 total, 0 merged
-   NFA Contexts: 3 peak, 101 total, 10 pruned
-   NFA: 10 matched (len 10/10/10.0), 0 mismatched
-   NFA: 80 absorbed (len 1/1/1.0), 0 skipped
-   ->  Function Scan on generate_series s (actual rows=100.00 loops=1)
-(9 rows)
-
--- Test 4.3: Very long matches
-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
-);
-                              QUERY PLAN                               
------------------------------------------------------------------------
- WindowAgg (actual rows=200.00 loops=1)
-   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: a+" b
-   Storage: Memory  Maximum Storage: 24kB
-   NFA States: 3 peak, 396 total, 0 merged
-   NFA Contexts: 3 peak, 201 total, 5 pruned
-   NFA: 1 matched (len 196/196/196.0), 0 mismatched
-   NFA: 194 absorbed (len 1/1/1.0), 0 skipped
-   ->  Function Scan on generate_series s (actual rows=200.00 loops=1)
-(9 rows)
-
--- Test 4.4: Mix of short and long matches
-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
-);
-                              QUERY PLAN                               
------------------------------------------------------------------------
- WindowAgg (actual rows=100.00 loops=1)
-   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: a+" b
-   Storage: Memory  Maximum Storage: 17kB
-   NFA States: 3 peak, 171 total, 0 merged
-   NFA Contexts: 3 peak, 101 total, 30 pruned
-   NFA: 5 matched (len 5/5/5.0), 5 mismatched (len 11/11/11.0)
-   NFA: 60 absorbed (len 1/1/1.0), 0 skipped
-   ->  Function Scan on generate_series s (actual rows=100.00 loops=1)
-(9 rows)
-
---
--- Section 5: Mismatch Length Statistics Tests
---
--- Test 5.1: Pattern that causes mismatches with length > 1
--- Mismatch happens when partial match fails after processing multiple rows
-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'
-);
-                              QUERY PLAN                               
------------------------------------------------------------------------
- WindowAgg (actual rows=100.00 loops=1)
-   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: a+" b+ c
-   Storage: Memory  Maximum Storage: 17kB
-   NFA States: 3 peak, 151 total, 0 merged
-   NFA Contexts: 3 peak, 101 total, 70 pruned
-   NFA: 10 matched (len 6/6/6.0), 0 mismatched
-   NFA: 20 absorbed (len 1/1/1.0), 0 skipped
-   ->  Function Scan on generate_series s (actual rows=100.00 loops=1)
-(9 rows)
-
--- Test 5.2: Long partial matches that fail
-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'
-);
-                              QUERY PLAN                              
-----------------------------------------------------------------------
- WindowAgg (actual rows=60.00 loops=1)
-   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: a+" b+ c
-   Storage: Memory  Maximum Storage: 18kB
-   NFA States: 3 peak, 115 total, 0 merged
-   NFA Contexts: 3 peak, 61 total, 16 pruned
-   NFA: 1 matched (len 30/30/30.0), 1 mismatched (len 26/26/26.0)
-   NFA: 42 absorbed (len 1/1/1.0), 0 skipped
-   ->  Function Scan on generate_series i (actual rows=60.00 loops=1)
-(9 rows)
-
---
--- Section 6: JSON Format Tests
---
--- Test 6.1: JSON format output with all statistics
-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
-);
-                                 QUERY PLAN                                 
-----------------------------------------------------------------------------
- [                                                                         +
-   {                                                                       +
-     "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+",                                               +
-       "Storage": "Memory",                                                +
-       "Maximum Storage": 17,                                              +
-       "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": 0,                                          +
-       "NFA Contexts Pruned": 33,                                          +
-       "NFA Matched": 17,                                                  +
-       "NFA Mismatched": 0,                                                +
-       "NFA Match Length Min": 2,                                          +
-       "NFA Match Length Max": 2,                                          +
-       "NFA Match Length Avg": 2.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)
-
--- Test 6.2: JSON format with match length statistics
-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
-);
-                                 QUERY PLAN                                 
-----------------------------------------------------------------------------
- [                                                                         +
-   {                                                                       +
-     "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",                                                +
-       "Storage": "Memory",                                                +
-       "Maximum Storage": 17,                                              +
-       "NFA States Peak": 3,                                               +
-       "NFA States Total": 191,                                            +
-       "NFA States Merged": 0,                                             +
-       "NFA Contexts Peak": 3,                                             +
-       "NFA Contexts Total": 101,                                          +
-       "NFA Contexts Absorbed": 80,                                        +
-       "NFA Contexts Skipped": 0,                                          +
-       "NFA Contexts Pruned": 10,                                          +
-       "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,                                     +
-       "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)
-
---
--- Section 7: XML Format Tests
---
--- Test 7.1: XML format output
-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
-);
-                                   QUERY PLAN                                   
---------------------------------------------------------------------------------
- <explain xmlns="http://www.postgresql.org/2009/explain";                      +
-   <Query>                                                                     +
-     <Plan>                                                                    +
-       <Node-Type>WindowAgg</Node-Type>                                        +
-       <Parallel-Aware>false</Parallel-Aware>                                  +
-       <Async-Capable>false</Async-Capable>                                    +
-       <Actual-Rows>30.00</Actual-Rows>                                        +
-       <Actual-Loops>1</Actual-Loops>                                          +
-       <Disabled>false</Disabled>                                              +
-       <Window>w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)</Window>+
-       <Pattern>a b</Pattern>                                                  +
-       <Storage>Memory</Storage>                                               +
-       <Maximum-Storage>17</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>3</NFA-Contexts-Peak>                                +
-       <NFA-Contexts-Total>31</NFA-Contexts-Total>                             +
-       <NFA-Contexts-Absorbed>0</NFA-Contexts-Absorbed>                        +
-       <NFA-Contexts-Skipped>0</NFA-Contexts-Skipped>                          +
-       <NFA-Contexts-Pruned>15</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>                        +
-       <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)
-
---
--- Section 8: Multiple Partitions Tests
---
--- Test 8.1: Statistics across multiple partitions
-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
-);
-                                     QUERY PLAN                                     
-------------------------------------------------------------------------------------
- WindowAgg (actual rows=90.00 loops=1)
-   Window: w AS (PARTITION BY p.p ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: a+" b
-   Storage: Memory  Maximum Storage: 17kB
-   NFA States: 3 peak, 165 total, 0 merged
-   NFA Contexts: 3 peak, 93 total, 18 pruned
-   NFA: 18 matched (len 5/5/5.0), 0 mismatched
-   NFA: 54 absorbed (len 1/1/1.0), 0 skipped
-   ->  Sort (actual rows=90.00 loops=1)
-         Sort Key: p.p
-         Sort Method: quicksort  Memory: 27kB
-         ->  Nested Loop (actual rows=90.00 loops=1)
-               ->  Function Scan on generate_series p (actual rows=3.00 loops=1)
-               ->  Function Scan on generate_series v (actual rows=30.00 loops=3)
-(14 rows)
-
--- Test 8.2: Different pattern behavior per partition
-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
-);
-                                                        QUERY PLAN                                                        
---------------------------------------------------------------------------------------------------------------------------
- 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
-   Storage: Memory  Maximum Storage: 17kB
-   NFA States: 3 peak, 77 total, 0 merged
-   NFA Contexts: 3 peak, 52 total, 26 pruned
-   NFA: 5 matched (len 5/6/5.8), 0 mismatched
-   NFA: 19 absorbed (len 1/1/1.0), 0 skipped
-   ->  Sort (actual rows=50.00 loops=1)
-         Sort Key: (CASE WHEN (v.v <= 25) THEN 1 ELSE 2 END)
-         Sort Method: quicksort  Memory: 26kB
-         ->  Function Scan on generate_series v (actual rows=50.00 loops=1)
-(12 rows)
-
---
--- Section 9: Edge Cases
---
--- Test 9.1: Empty result set
-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
-);
-                             QUERY PLAN                              
----------------------------------------------------------------------
- WindowAgg (actual rows=0.00 loops=1)
-   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: a b
-   ->  Function Scan on generate_series s (actual rows=0.00 loops=1)
-(4 rows)
-
--- Test 9.2: Single row
-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
-);
-                             QUERY PLAN                              
----------------------------------------------------------------------
- WindowAgg (actual rows=1.00 loops=1)
-   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: a
-   Storage: Memory  Maximum Storage: 17kB
-   NFA States: 2 peak, 2 total, 0 merged
-   NFA Contexts: 2 peak, 2 total, 0 pruned
-   NFA: 1 matched (len 1/1/1.0), 0 mismatched
-   ->  Function Scan on generate_series s (actual rows=1.00 loops=1)
-(8 rows)
-
--- Test 9.3: Pattern longer than data
-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
-);
-                             QUERY PLAN                              
----------------------------------------------------------------------
- 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
-   Storage: Memory  Maximum Storage: 17kB
-   NFA States: 2 peak, 6 total, 0 merged
-   NFA Contexts: 3 peak, 6 total, 4 pruned
-   NFA: 0 matched, 1 mismatched (len 5/5/5.0)
-   ->  Function Scan on generate_series s (actual rows=5.00 loops=1)
-(8 rows)
-
--- Test 9.4: All rows match as single match
-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
-);
-                              QUERY PLAN                              
-----------------------------------------------------------------------
- WindowAgg (actual rows=50.00 loops=1)
-   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: a+"
-   Storage: Memory  Maximum Storage: 18kB
-   NFA States: 3 peak, 101 total, 0 merged
-   NFA Contexts: 2 peak, 51 total, 0 pruned
-   NFA: 1 matched (len 50/50/50.0), 0 mismatched
-   NFA: 49 absorbed (len 1/1/1.0), 0 skipped
-   ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
-(9 rows)
-
---
--- Section 10: Complex Pattern Tests
---
--- Test 10.1: Nested groups
-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
-);
-                              QUERY PLAN                              
-----------------------------------------------------------------------
- WindowAgg (actual rows=60.00 loops=1)
-   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: (a' b' c')+"
-   Storage: Memory  Maximum Storage: 19kB
-   NFA States: 3 peak, 81 total, 0 merged
-   NFA Contexts: 3 peak, 61 total, 40 pruned
-   NFA: 1 matched (len 60/60/60.0), 0 mismatched
-   NFA: 19 absorbed (len 1/1/1.0), 0 skipped
-   ->  Function Scan on generate_series s (actual rows=60.00 loops=1)
-(9 rows)
-
--- Test 10.2: Multiple alternations
-EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
-SELECT count(*) OVER w
-FROM 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'
-);
-                            QUERY PLAN                             
--------------------------------------------------------------------
- WindowAgg (actual rows=100.00 loops=1)
-   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: (a | b c | d | e)
-   Storage: Memory  Maximum Storage: 17kB
-   NFA States: 5 peak, 282 total, 0 merged
-   NFA Contexts: 3 peak, 101 total, 60 pruned
-   NFA: 20 matched (len 2/2/2.0), 20 mismatched (len 2/2/2.0)
-   ->  Seq Scan on nfa_test (actual rows=100.00 loops=1)
-(8 rows)
-
--- Test 10.3: Optional elements
-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
-);
-                              QUERY PLAN                              
-----------------------------------------------------------------------
- WindowAgg (actual rows=50.00 loops=1)
-   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: a b? c
-   Storage: Memory  Maximum Storage: 17kB
-   NFA States: 3 peak, 64 total, 0 merged
-   NFA Contexts: 3 peak, 51 total, 37 pruned
-   NFA: 12 matched (len 3/3/3.0), 1 mismatched (len 2/2/2.0)
-   ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
-(8 rows)
-
--- Test 10.4: Bounded quantifiers
-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
-);
-                              QUERY PLAN                               
------------------------------------------------------------------------
- WindowAgg (actual rows=100.00 loops=1)
-   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: a{2,5} b
-   Storage: Memory  Maximum Storage: 17kB
-   NFA States: 9 peak, 311 total, 0 merged
-   NFA Contexts: 7 peak, 101 total, 10 pruned
-   NFA: 10 matched (len 6/6/6.0), 50 mismatched (len 2/6/5.2)
-   NFA: 0 absorbed, 30 skipped (len 3/5/4.0)
-   ->  Function Scan on generate_series s (actual rows=100.00 loops=1)
-(9 rows)
-
--- Test 10.5: Star quantifier
-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
-);
-                              QUERY PLAN                              
-----------------------------------------------------------------------
- WindowAgg (actual rows=50.00 loops=1)
-   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: a b* c
-   Storage: Memory  Maximum Storage: 17kB
-   NFA States: 3 peak, 91 total, 0 merged
-   NFA Contexts: 3 peak, 51 total, 45 pruned
-   NFA: 5 matched (len 9/9/9.0), 0 mismatched
-   ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
-(8 rows)
-
---
--- Section 11: Real-world Pattern Examples
---
--- Test 11.1: Stock price pattern - V-shape (down then up)
-EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
-SELECT count(*) OVER w
-FROM 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'
-);
-                            QUERY PLAN                             
--------------------------------------------------------------------
- WindowAgg (actual rows=30.00 loops=1)
-   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: d+" u+
-   Storage: Memory  Maximum Storage: 17kB
-   NFA States: 4 peak, 58 total, 0 merged
-   NFA Contexts: 3 peak, 31 total, 17 pruned
-   NFA: 3 matched (len 3/14/8.0), 1 mismatched (len 3/3/3.0)
-   NFA: 9 absorbed (len 1/1/1.0), 0 skipped
-   ->  Seq Scan on nfa_complex (actual rows=30.00 loops=1)
-(9 rows)
-
--- Test 11.2: Stock price pattern - peak (up, stable, down)
-EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
-SELECT count(*) OVER w
-FROM 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'
-);
-                            QUERY PLAN                             
--------------------------------------------------------------------
- WindowAgg (actual rows=30.00 loops=1)
-   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: u+" s* d+
-   Storage: Memory  Maximum Storage: 17kB
-   NFA States: 5 peak, 76 total, 0 merged
-   NFA Contexts: 3 peak, 31 total, 14 pruned
-   NFA: 4 matched (len 3/11/7.2), 0 mismatched
-   NFA: 12 absorbed (len 1/1/1.0), 0 skipped
-   ->  Seq Scan on nfa_complex (actual rows=30.00 loops=1)
-(9 rows)
-
--- Test 11.3: Consecutive increasing values (using PREV)
--- FIXME: The original pattern was:
---   DEFINE A AS v > PREV(v) OR PREV(v) IS NULL
--- This causes "ERROR: unrecognized node type: 15" (T_FuncExpr) because
--- NullTest(FuncExpr(PREV)) is not properly handled somewhere in the planner.
--- The expression v > PREV(v) works fine, but PREV(v) IS NULL fails.
--- Using COALESCE(PREV(v), 0) as a workaround until the bug is fixed.
-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 > COALESCE(PREV(v), 0)
-);
-                              QUERY PLAN                              
-----------------------------------------------------------------------
- WindowAgg (actual rows=50.00 loops=1)
-   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: a{3,}"
-   Storage: Memory  Maximum Storage: 18kB
-   NFA States: 3 peak, 99 total, 0 merged
-   NFA Contexts: 2 peak, 51 total, 0 pruned
-   NFA: 1 matched (len 50/50/50.0), 0 mismatched
-   NFA: 49 absorbed (len 1/1/1.0), 0 skipped
-   ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
-(9 rows)
-
---
--- Section 12: Performance-oriented Tests
---
--- Test 12.1: Large dataset with simple pattern
-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
-);
-                               QUERY PLAN                               
-------------------------------------------------------------------------
- WindowAgg (actual rows=1000.00 loops=1)
-   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: a b
-   Storage: Memory  Maximum Storage: 17kB
-   NFA States: 2 peak, 1001 total, 0 merged
-   NFA Contexts: 3 peak, 1001 total, 500 pruned
-   NFA: 500 matched (len 2/2/2.0), 0 mismatched
-   ->  Function Scan on generate_series s (actual rows=1000.00 loops=1)
-(8 rows)
-
--- Test 12.2: Large dataset with absorption
-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
-);
-                               QUERY PLAN                               
-------------------------------------------------------------------------
- WindowAgg (actual rows=1000.00 loops=1)
-   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: a+" b
-   Storage: Memory  Maximum Storage: 21kB
-   NFA States: 3 peak, 1991 total, 0 merged
-   NFA Contexts: 3 peak, 1001 total, 10 pruned
-   NFA: 10 matched (len 100/100/100.0), 0 mismatched
-   NFA: 980 absorbed (len 1/1/1.0), 0 skipped
-   ->  Function Scan on generate_series s (actual rows=1000.00 loops=1)
-(9 rows)
-
--- Test 12.3: High state merge ratio
-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
-);
-                              QUERY PLAN                               
------------------------------------------------------------------------
- WindowAgg (actual rows=500.00 loops=1)
-   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: (a | b)+ c
-   Storage: Memory  Maximum Storage: 17kB
-   NFA States: 8 peak, 2004 total, 0 merged
-   NFA Contexts: 4 peak, 501 total, 167 pruned
-   NFA: 166 matched (len 3/3/3.0), 1 mismatched (len 2/2/2.0)
-   NFA: 0 absorbed, 166 skipped (len 2/2/2.0)
-   ->  Function Scan on generate_series s (actual rows=500.00 loops=1)
-(9 rows)
-
---
--- Section 13: INITIAL vs no INITIAL comparison
---
--- Test 13.1: With INITIAL keyword
-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
-);
-                              QUERY PLAN                              
-----------------------------------------------------------------------
- WindowAgg (actual rows=50.00 loops=1)
-   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: a+" b
-   Storage: Memory  Maximum Storage: 17kB
-   NFA States: 3 peak, 91 total, 0 merged
-   NFA Contexts: 3 peak, 51 total, 10 pruned
-   NFA: 10 matched (len 5/5/5.0), 0 mismatched
-   NFA: 30 absorbed (len 1/1/1.0), 0 skipped
-   ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
-(9 rows)
-
--- Test 13.2: Without INITIAL keyword (same behavior currently)
-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
-);
-                              QUERY PLAN                              
-----------------------------------------------------------------------
- WindowAgg (actual rows=50.00 loops=1)
-   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: a+" b
-   Storage: Memory  Maximum Storage: 17kB
-   NFA States: 3 peak, 91 total, 0 merged
-   NFA Contexts: 3 peak, 51 total, 10 pruned
-   NFA: 10 matched (len 5/5/5.0), 0 mismatched
-   NFA: 30 absorbed (len 1/1/1.0), 0 skipped
-   ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
-(9 rows)
-
---
--- Section 14: Quantifier Variations
---
--- Test 14.1: Plus quantifier
-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
-);
-                              QUERY PLAN                              
-----------------------------------------------------------------------
- WindowAgg (actual rows=40.00 loops=1)
-   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: a+"
-   Storage: Memory  Maximum Storage: 17kB
-   NFA States: 3 peak, 71 total, 0 merged
-   NFA Contexts: 3 peak, 41 total, 10 pruned
-   NFA: 10 matched (len 3/3/3.0), 0 mismatched
-   NFA: 20 absorbed (len 1/1/1.0), 0 skipped
-   ->  Function Scan on generate_series s (actual rows=40.00 loops=1)
-(9 rows)
-
--- Test 14.2: Star quantifier (zero or more)
-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
-);
-                              QUERY PLAN                              
-----------------------------------------------------------------------
- WindowAgg (actual rows=40.00 loops=1)
-   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: a*" b
-   Storage: Memory  Maximum Storage: 17kB
-   NFA States: 4 peak, 102 total, 0 merged
-   NFA Contexts: 2 peak, 41 total, 10 pruned
-   NFA: 10 matched (len 3/3/3.0), 0 mismatched
-   NFA: 20 absorbed (len 1/1/1.0), 0 skipped
-   ->  Function Scan on generate_series s (actual rows=40.00 loops=1)
-(9 rows)
-
--- Test 14.3: Question mark (zero or one)
-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
-);
-                              QUERY PLAN                              
-----------------------------------------------------------------------
- WindowAgg (actual rows=40.00 loops=1)
-   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: a? b c
-   Storage: Memory  Maximum Storage: 17kB
-   NFA States: 4 peak, 82 total, 0 merged
-   NFA Contexts: 4 peak, 41 total, 20 pruned
-   NFA: 10 matched (len 3/3/3.0), 0 mismatched
-   NFA: 0 absorbed, 10 skipped (len 2/2/2.0)
-   ->  Function Scan on generate_series s (actual rows=40.00 loops=1)
-(9 rows)
-
--- Test 14.4: Exact count {n}
-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
-);
-                              QUERY PLAN                              
-----------------------------------------------------------------------
- WindowAgg (actual rows=50.00 loops=1)
-   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: a{3} b
-   Storage: Memory  Maximum Storage: 17kB
-   NFA States: 4 peak, 51 total, 0 merged
-   NFA Contexts: 5 peak, 51 total, 10 pruned
-   NFA: 10 matched (len 4/4/4.0), 30 mismatched (len 2/4/3.0)
-   ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
-(8 rows)
-
--- Test 14.5: Range {n,m}
-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
-);
-                              QUERY PLAN                              
-----------------------------------------------------------------------
- WindowAgg (actual rows=50.00 loops=1)
-   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: a{2,4} b
-   Storage: Memory  Maximum Storage: 17kB
-   NFA States: 7 peak, 101 total, 0 merged
-   NFA Contexts: 6 peak, 51 total, 10 pruned
-   NFA: 10 matched (len 5/5/5.0), 10 mismatched (len 2/2/2.0)
-   NFA: 0 absorbed, 20 skipped (len 3/4/3.5)
-   ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
-(9 rows)
-
--- Test 14.6: At least {n,}
-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
-);
-                              QUERY PLAN                              
-----------------------------------------------------------------------
- WindowAgg (actual rows=50.00 loops=1)
-   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: a{3,}" b
-   Storage: Memory  Maximum Storage: 17kB
-   NFA States: 3 peak, 86 total, 0 merged
-   NFA Contexts: 3 peak, 51 total, 5 pruned
-   NFA: 5 matched (len 10/10/10.0), 0 mismatched
-   NFA: 40 absorbed (len 1/1/1.0), 0 skipped
-   ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
-(9 rows)
-
---
--- Section 15: Regression Tests for Statistics Accuracy
---
--- Test 15.1: Verify state count accuracy
--- Pattern A+ B with 20 rows should show predictable state behavior
-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
-);
-                              QUERY PLAN                              
-----------------------------------------------------------------------
- WindowAgg (actual rows=20.00 loops=1)
-   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: a+" b
-   Storage: Memory  Maximum Storage: 17kB
-   NFA States: 3 peak, 37 total, 0 merged
-   NFA Contexts: 3 peak, 21 total, 4 pruned
-   NFA: 4 matched (len 5/5/5.0), 0 mismatched
-   NFA: 12 absorbed (len 1/1/1.0), 0 skipped
-   ->  Function Scan on generate_series s (actual rows=20.00 loops=1)
-(9 rows)
-
--- Test 15.2: Verify context count with known absorption
-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
-);
-                              QUERY PLAN                              
-----------------------------------------------------------------------
- WindowAgg (actual rows=30.00 loops=1)
-   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: a+" b c
-   Storage: Memory  Maximum Storage: 17kB
-   NFA States: 3 peak, 52 total, 0 merged
-   NFA Contexts: 3 peak, 31 total, 9 pruned
-   NFA: 3 matched (len 9/9/9.0), 0 mismatched
-   NFA: 18 absorbed (len 1/1/1.0), 0 skipped
-   ->  Function Scan on generate_series s (actual rows=30.00 loops=1)
-(9 rows)
-
--- Test 15.3: Verify match length with fixed-length pattern
-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
-);
-                              QUERY PLAN                              
-----------------------------------------------------------------------
- WindowAgg (actual rows=30.00 loops=1)
-   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: a b c
-   Storage: Memory  Maximum Storage: 17kB
-   NFA States: 2 peak, 31 total, 0 merged
-   NFA Contexts: 3 peak, 31 total, 20 pruned
-   NFA: 10 matched (len 3/3/3.0), 0 mismatched
-   ->  Function Scan on generate_series s (actual rows=30.00 loops=1)
-(8 rows)
-
---
--- Section 16: Alternation Pattern Tests
---
--- Test 16.1: Simple alternation
-EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
-SELECT count(*) OVER w
-FROM 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'
-);
-                            QUERY PLAN                             
--------------------------------------------------------------------
- WindowAgg (actual rows=100.00 loops=1)
-   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: (a | b) c
-   Storage: Memory  Maximum Storage: 17kB
-   NFA States: 3 peak, 202 total, 0 merged
-   NFA Contexts: 3 peak, 101 total, 60 pruned
-   NFA: 20 matched (len 2/2/2.0), 20 mismatched (len 2/2/2.0)
-   ->  Seq Scan on nfa_test (actual rows=100.00 loops=1)
-(8 rows)
-
--- Test 16.2: Multiple items in alternation
-EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
-SELECT count(*) OVER w
-FROM 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'
-);
-                            QUERY PLAN                             
--------------------------------------------------------------------
- WindowAgg (actual rows=100.00 loops=1)
-   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: (a | b | c | d) e
-   Storage: Memory  Maximum Storage: 17kB
-   NFA States: 5 peak, 404 total, 0 merged
-   NFA Contexts: 3 peak, 101 total, 20 pruned
-   NFA: 20 matched (len 2/2/2.0), 60 mismatched (len 2/2/2.0)
-   ->  Seq Scan on nfa_test (actual rows=100.00 loops=1)
-(8 rows)
-
--- Test 16.3: Alternation with quantifiers
-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
-);
-                              QUERY PLAN                              
-----------------------------------------------------------------------
- WindowAgg (actual rows=50.00 loops=1)
-   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: (a | b)+ c
-   Storage: Memory  Maximum Storage: 17kB
-   NFA States: 8 peak, 204 total, 0 merged
-   NFA Contexts: 4 peak, 51 total, 17 pruned
-   NFA: 16 matched (len 3/3/3.0), 1 mismatched (len 2/2/2.0)
-   NFA: 0 absorbed, 16 skipped (len 2/2/2.0)
-   ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
-(9 rows)
-
---
--- Section 17: Group Pattern Tests
---
--- Test 17.1: Simple group
-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
-);
-                              QUERY PLAN                              
-----------------------------------------------------------------------
- WindowAgg (actual rows=40.00 loops=1)
-   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: (a' b')+"
-   Storage: Memory  Maximum Storage: 18kB
-   NFA States: 3 peak, 61 total, 0 merged
-   NFA Contexts: 3 peak, 41 total, 20 pruned
-   NFA: 1 matched (len 40/40/40.0), 0 mismatched
-   NFA: 19 absorbed (len 1/1/1.0), 0 skipped
-   ->  Function Scan on generate_series s (actual rows=40.00 loops=1)
-(9 rows)
-
--- Test 17.2: Group with bounded quantifier
-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
-);
-                              QUERY PLAN                              
-----------------------------------------------------------------------
- WindowAgg (actual rows=40.00 loops=1)
-   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: (a b){2,4}
-   Storage: Memory  Maximum Storage: 17kB
-   NFA States: 7 peak, 66 total, 0 merged
-   NFA Contexts: 6 peak, 41 total, 20 pruned
-   NFA: 5 matched (len 8/8/8.0), 0 mismatched
-   NFA: 0 absorbed, 15 skipped (len 2/6/4.0)
-   ->  Function Scan on generate_series s (actual rows=40.00 loops=1)
-(9 rows)
-
--- Test 17.3: Nested groups
-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
-);
-                              QUERY PLAN                              
-----------------------------------------------------------------------
- WindowAgg (actual rows=60.00 loops=1)
-   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: ((a b){2})+
-   Storage: Memory  Maximum Storage: 19kB
-   NFA States: 60 peak, 286 total, 0 merged
-   NFA Contexts: 32 peak, 61 total, 30 pruned
-   NFA: 1 matched (len 60/60/60.0), 1 mismatched (len 2/2/2.0)
-   NFA: 0 absorbed, 28 skipped (len 4/58/31.0)
-   ->  Function Scan on generate_series s (actual rows=60.00 loops=1)
-(9 rows)
-
---
--- Section 18: Window Function Combinations
---
--- Test 18.1: count(*) with pattern
-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
-);
-                              QUERY PLAN                              
-----------------------------------------------------------------------
- WindowAgg (actual rows=30.00 loops=1)
-   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: a+" b
-   Storage: Memory  Maximum Storage: 17kB
-   NFA States: 3 peak, 55 total, 0 merged
-   NFA Contexts: 3 peak, 31 total, 6 pruned
-   NFA: 6 matched (len 5/5/5.0), 0 mismatched
-   NFA: 18 absorbed (len 1/1/1.0), 0 skipped
-   ->  Function Scan on generate_series s (actual rows=30.00 loops=1)
-(9 rows)
-
--- Test 18.2: first_value with pattern
-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
-);
-                              QUERY PLAN                              
-----------------------------------------------------------------------
- WindowAgg (actual rows=30.00 loops=1)
-   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: a+" b
-   Storage: Memory  Maximum Storage: 17kB
-   NFA States: 3 peak, 55 total, 0 merged
-   NFA Contexts: 3 peak, 31 total, 6 pruned
-   NFA: 6 matched (len 5/5/5.0), 0 mismatched
-   NFA: 18 absorbed (len 1/1/1.0), 0 skipped
-   ->  Function Scan on generate_series s (actual rows=30.00 loops=1)
-(9 rows)
-
--- Test 18.3: last_value with pattern
-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
-);
-                              QUERY PLAN                              
-----------------------------------------------------------------------
- WindowAgg (actual rows=30.00 loops=1)
-   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: a+" b
-   Storage: Memory  Maximum Storage: 18kB
-   NFA States: 3 peak, 55 total, 0 merged
-   NFA Contexts: 3 peak, 31 total, 6 pruned
-   NFA: 6 matched (len 5/5/5.0), 0 mismatched
-   NFA: 18 absorbed (len 1/1/1.0), 0 skipped
-   ->  Function Scan on generate_series s (actual rows=30.00 loops=1)
-(9 rows)
-
--- Test 18.4: Multiple window functions
-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
-);
-                              QUERY PLAN                              
-----------------------------------------------------------------------
- WindowAgg (actual rows=30.00 loops=1)
-   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: a+" b
-   Storage: Memory  Maximum Storage: 18kB
-   NFA States: 3 peak, 55 total, 0 merged
-   NFA Contexts: 3 peak, 31 total, 6 pruned
-   NFA: 6 matched (len 5/5/5.0), 0 mismatched
-   NFA: 18 absorbed (len 1/1/1.0), 0 skipped
-   ->  Function Scan on generate_series s (actual rows=30.00 loops=1)
-(9 rows)
-
---
--- Section 19: DEFINE Expression Variations
---
--- Test 19.1: Complex boolean expressions
-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)
-);
-                              QUERY PLAN                              
-----------------------------------------------------------------------
- WindowAgg (actual rows=50.00 loops=1)
-   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: a+" b
-   Storage: Memory  Maximum Storage: 17kB
-   NFA States: 3 peak, 78 total, 0 merged
-   NFA Contexts: 3 peak, 51 total, 23 pruned
-   NFA: 17 matched (len 2/3/2.6), 0 mismatched
-   NFA: 10 absorbed (len 1/1/1.0), 0 skipped
-   ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
-(9 rows)
-
--- Test 19.2: Using PREV function
-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)
-);
-                              QUERY PLAN                              
-----------------------------------------------------------------------
- WindowAgg (actual rows=30.00 loops=1)
-   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: s u+ d+
-   Storage: Memory  Maximum Storage: 18kB
-   NFA States: 60 peak, 466 total, 0 merged
-   NFA Contexts: 31 peak, 31 total, 1 pruned
-   NFA: 0 matched, 29 mismatched (len 2/30/16.0)
-   ->  Function Scan on generate_series s (actual rows=30.00 loops=1)
-(8 rows)
-
--- Test 19.3: Using NULL comparisons
-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
-);
-                              QUERY PLAN                              
-----------------------------------------------------------------------
- WindowAgg (actual rows=30.00 loops=1)
-   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: a+" b
-   Storage: Memory  Maximum Storage: 17kB
-   NFA States: 3 peak, 55 total, 0 merged
-   NFA Contexts: 3 peak, 31 total, 6 pruned
-   NFA: 6 matched (len 5/5/5.0), 0 mismatched
-   NFA: 18 absorbed (len 1/1/1.0), 0 skipped
-   ->  Function Scan on generate_series v (actual rows=30.00 loops=1)
-(9 rows)
-
---
--- Section 20: Large Scale Statistics Verification
---
--- Test 20.1: 500 rows - verify statistics scale correctly
-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
-);
-                              QUERY PLAN                               
------------------------------------------------------------------------
- WindowAgg (actual rows=500.00 loops=1)
-   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: a+" b c
-   Storage: Memory  Maximum Storage: 17kB
-   NFA States: 3 peak, 851 total, 0 merged
-   NFA Contexts: 3 peak, 501 total, 151 pruned
-   NFA: 50 matched (len 8/9/9.0), 0 mismatched
-   NFA: 299 absorbed (len 1/1/1.0), 0 skipped
-   ->  Function Scan on generate_series s (actual rows=500.00 loops=1)
-(9 rows)
-
--- Test 20.2: High match count scenario
-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
-);
-                              QUERY PLAN                               
------------------------------------------------------------------------
- WindowAgg (actual rows=500.00 loops=1)
-   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: a b
-   Storage: Memory  Maximum Storage: 17kB
-   NFA States: 2 peak, 501 total, 0 merged
-   NFA Contexts: 3 peak, 501 total, 250 pruned
-   NFA: 250 matched (len 2/2/2.0), 0 mismatched
-   ->  Function Scan on generate_series s (actual rows=500.00 loops=1)
-(8 rows)
-
--- Test 20.3: High skip count scenario
-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
-);
-                              QUERY PLAN                               
------------------------------------------------------------------------
- WindowAgg (actual rows=500.00 loops=1)
-   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: a b c d e
-   Storage: Memory  Maximum Storage: 17kB
-   NFA States: 2 peak, 501 total, 0 merged
-   NFA Contexts: 3 peak, 501 total, 495 pruned
-   NFA: 5 matched (len 5/5/5.0), 0 mismatched
-   ->  Function Scan on generate_series s (actual rows=500.00 loops=1)
-(8 rows)
-
--- Cleanup
-DROP TABLE nfa_test;
-DROP TABLE nfa_complex;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 4a839d827a2..fc61d90ebaf 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -107,7 +107,7 @@ test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combo
 # ----------
 # Row Pattern Recognition tests
 # ----------
-test: rpr rpr_base
+test: rpr rpr_base rpr_explain
 
 # ----------
 # Another group of parallel tests (JSON related)
diff --git a/src/test/regress/sql/rpr_explain.sql b/src/test/regress/sql/rpr_explain.sql
index f070f05a497..8156121b3cd 100644
--- a/src/test/regress/sql/rpr_explain.sql
+++ b/src/test/regress/sql/rpr_explain.sql
@@ -7,6 +7,39 @@
 -- - NFA: matched (len min/max/avg), mismatched (len min/max/avg)
 --
 
+-- Filter function to normalize Storage memory values only (not NFA statistics)
+-- 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 memory size in Storage line only (platform-dependent)
+        -- 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;
+
+        return next ln;
+    end loop;
+end;
+$$;
+
 -- Setup: Create test tables
 CREATE TEMP TABLE nfa_test (
     id serial,
@@ -47,6 +80,7 @@ VALUES
 --
 
 -- Test 1.1: Simple pattern - should show basic statistics
+SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
 FROM nfa_test
@@ -54,10 +88,11 @@ 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'
-);
+    DEFINE A AS cat = ''A'', B AS cat = ''B''
+)');
 
 -- Test 1.2: Pattern with no matches - 0 matched
+SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
 FROM nfa_test
@@ -65,10 +100,11 @@ 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'
-);
+    DEFINE X AS cat = ''X'', Y AS cat = ''Y'', Z AS cat = ''Z''
+);');
 
 -- Test 1.3: Pattern matching every row - high match count
+SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
 FROM nfa_test
@@ -77,13 +113,14 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (R)
     DEFINE R AS TRUE
-);
+);');
 
 --
 -- Section 2: State Statistics Tests (peak, total, merged)
 --
 
 -- Test 2.1: Simple quantifier pattern - A+ with short matches (no merging)
+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)
@@ -92,9 +129,10 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (A+)
     DEFINE A AS v % 2 = 1
-);
+);');
 
 -- Test 2.2: Alternation pattern - multiple state branches
+SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
 FROM nfa_test
@@ -103,11 +141,12 @@ WINDOW w AS (
     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'
-);
+        A AS cat = ''A'', B AS cat = ''B'', C AS cat = ''C'',
+        D AS cat = ''D'', E AS cat = ''E''
+);');
 
 -- Test 2.3: Complex pattern with high state count
+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)
@@ -119,9 +158,10 @@ WINDOW w AS (
         A AS v % 3 = 1,
         B AS v % 3 = 2,
         C AS v % 3 = 0
-);
+);');
 
 -- Test 2.4: Grouped pattern with quantifier - state merging
+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)
@@ -130,10 +170,11 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN ((A B)+)
     DEFINE A AS v % 2 = 1, B AS v % 2 = 0
-);
+);');
 
 -- Test 2.5: State explosion pattern - many alternations
 -- Pattern (A|B)(A|B)(A|B)(A|B) can create many parallel states
+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)
@@ -142,9 +183,10 @@ WINDOW w AS (
     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
-);
+);');
 
 -- Test 2.6: High state merging - alternation with plus quantifier
+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)
@@ -153,9 +195,10 @@ WINDOW w AS (
     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
-);
+);');
 
 -- Test 2.7: Nested quantifiers causing state growth
+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)
@@ -164,13 +207,14 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (((A | B)+)+)
     DEFINE A AS v % 3 = 1, B AS v % 3 = 2
-);
+);');
 
 --
 -- Section 3: Context Statistics Tests (peak, total, absorbed, skipped)
 --
 
 -- Test 3.1: Context absorption with unbounded quantifier at start
+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)
@@ -179,9 +223,10 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
-);
+);');
 
 -- Test 3.2: No absorption - bounded quantifier
+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)
@@ -190,9 +235,10 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (A{2,4} B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
-);
+);');
 
 -- Test 3.3: Contexts skipped by SKIP PAST LAST 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)
@@ -201,9 +247,10 @@ WINDOW w AS (
     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
-);
+);');
 
 -- Test 3.4: High context absorption - unbounded group
+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)
@@ -212,13 +259,14 @@ WINDOW w AS (
     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
-);
+);');
 
 --
 -- Section 4: Match Length Statistics Tests
 --
 
 -- Test 4.1: Fixed length matches - all same length
+SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
 FROM nfa_test
@@ -227,11 +275,12 @@ WINDOW w AS (
     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'
-);
+        A AS cat = ''A'', B AS cat = ''B'', C AS cat = ''C'',
+        D AS cat = ''D'', E AS cat = ''E''
+);');
 
 -- Test 4.2: Variable length matches - min/max/avg differ
+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)
@@ -240,9 +289,10 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (A+ B)
     DEFINE A AS v % 10 <> 0, B AS v % 10 = 0
-);
+);');
 
 -- Test 4.3: Very long matches
+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)
@@ -251,9 +301,10 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (A+ B)
     DEFINE A AS v <= 195, B AS v > 195
-);
+);');
 
 -- Test 4.4: Mix of short and long matches
+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)
@@ -264,7 +315,7 @@ WINDOW w AS (
     DEFINE
         A AS (v % 20 <> 0) AND (v % 20 <= 10 OR v % 20 > 15),
         B AS v % 20 = 0
-);
+);');
 
 --
 -- Section 5: Mismatch Length Statistics Tests
@@ -272,36 +323,38 @@ WINDOW w AS (
 
 -- Test 5.1: Pattern that causes mismatches with length > 1
 -- Mismatch happens when partial match fails after processing multiple rows
+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
+           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'
-);
+    DEFINE A AS cat = ''A'', B AS cat = ''B'', C AS cat = ''C''
+);');
 
 -- Test 5.2: Long partial matches that fail
+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'
+               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
@@ -309,14 +362,15 @@ 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'
-);
+    DEFINE A AS cat = ''A'', B AS cat = ''B'', C AS cat = ''C''
+);');
 
 --
 -- Section 6: JSON Format Tests
 --
 
 -- Test 6.1: JSON format output with all statistics
+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)
@@ -325,9 +379,10 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (A+ B+)
     DEFINE A AS v % 3 = 1, B AS v % 3 = 2
-);
+)');
 
 -- Test 6.2: JSON format with match length statistics
+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)
@@ -336,13 +391,14 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (A+ B)
     DEFINE A AS v % 10 <> 0, B AS v % 10 = 0
-);
+)');
 
 --
 -- Section 7: XML Format Tests
 --
 
 -- Test 7.1: XML format output
+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)
@@ -351,13 +407,14 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (A B)
     DEFINE A AS v % 2 = 1, B AS v % 2 = 0
-);
+)');
 
 --
 -- Section 8: Multiple Partitions Tests
 --
 
 -- Test 8.1: Statistics across multiple partitions
+SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
 FROM (
@@ -371,9 +428,10 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
-);
+);');
 
 -- Test 8.2: Different pattern behavior per partition
+SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
 FROM (
@@ -388,13 +446,14 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (A+ B)
     DEFINE A AS val < 5, B AS val >= 5
-);
+);');
 
 --
 -- Section 9: Edge Cases
 --
 
 -- Test 9.1: Empty result set
+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)
@@ -403,9 +462,10 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (A B)
     DEFINE A AS v = 1, B AS v = 2
-);
+);');
 
 -- Test 9.2: Single 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)
@@ -414,9 +474,10 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (A)
     DEFINE A AS TRUE
-);
+);');
 
 -- Test 9.3: Pattern longer than data
+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)
@@ -427,9 +488,10 @@ WINDOW w AS (
     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
-);
+);');
 
 -- Test 9.4: All rows match as single match
+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)
@@ -438,13 +500,14 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (A+)
     DEFINE A AS TRUE
-);
+);');
 
 --
 -- Section 10: Complex Pattern Tests
 --
 
 -- Test 10.1: Nested groups
+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)
@@ -453,9 +516,10 @@ WINDOW w AS (
     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
-);
+);');
 
 -- Test 10.2: Multiple alternations
+SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
 FROM nfa_test
@@ -464,11 +528,12 @@ WINDOW w AS (
     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'
-);
+        A AS cat = ''A'', B AS cat = ''B'', C AS cat = ''C'',
+        D AS cat = ''D'', E AS cat = ''E''
+);');
 
 -- Test 10.3: Optional elements
+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)
@@ -477,9 +542,10 @@ WINDOW w AS (
     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
-);
+);');
 
 -- Test 10.4: Bounded quantifiers
+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)
@@ -488,9 +554,10 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (A{2,5} B)
     DEFINE A AS v % 10 <> 0, B AS v % 10 = 0
-);
+);');
 
 -- Test 10.5: Star quantifier
+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)
@@ -499,13 +566,14 @@ WINDOW w AS (
     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
-);
+);');
 
 --
 -- Section 11: Real-world Pattern Examples
 --
 
 -- Test 11.1: Stock price pattern - V-shape (down then up)
+SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
 FROM nfa_complex
@@ -513,10 +581,11 @@ 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'
-);
+    DEFINE D AS trend = ''D'', U AS trend = ''U''
+);');
 
 -- Test 11.2: Stock price pattern - peak (up, stable, down)
+SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
 FROM nfa_complex
@@ -524,16 +593,11 @@ 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'
-);
+    DEFINE U AS trend = ''U'', S AS trend = ''S'', D AS trend = ''D''
+);');
 
 -- Test 11.3: Consecutive increasing values (using PREV)
--- FIXME: The original pattern was:
---   DEFINE A AS v > PREV(v) OR PREV(v) IS NULL
--- This causes "ERROR: unrecognized node type: 15" (T_FuncExpr) because
--- NullTest(FuncExpr(PREV)) is not properly handled somewhere in the planner.
--- The expression v > PREV(v) works fine, but PREV(v) IS NULL fails.
--- Using COALESCE(PREV(v), 0) as a workaround until the bug is fixed.
+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)
@@ -541,14 +605,15 @@ WINDOW w AS (
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (A{3,})
-    DEFINE A AS v > COALESCE(PREV(v), 0)
-);
+    DEFINE A AS v > PREV(v) OR PREV(v) IS NULL
+);');
 
 --
 -- Section 12: Performance-oriented Tests
 --
 
 -- Test 12.1: Large dataset with simple 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)
@@ -557,9 +622,10 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (A B)
     DEFINE A AS v % 2 = 1, B AS v % 2 = 0
-);
+);');
 
 -- Test 12.2: Large dataset with absorption
+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)
@@ -568,9 +634,10 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (A+ B)
     DEFINE A AS v % 100 <> 0, B AS v % 100 = 0
-);
+);');
 
 -- Test 12.3: High state merge ratio
+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)
@@ -579,13 +646,14 @@ WINDOW w AS (
     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
-);
+);');
 
 --
 -- Section 13: INITIAL vs no INITIAL comparison
 --
 
 -- Test 13.1: With INITIAL keyword
+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)
@@ -595,9 +663,10 @@ WINDOW w AS (
     INITIAL
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
-);
+);');
 
 -- Test 13.2: Without INITIAL keyword (same behavior currently)
+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)
@@ -606,13 +675,14 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
-);
+);');
 
 --
 -- Section 14: Quantifier Variations
 --
 
 -- Test 14.1: Plus quantifier
+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)
@@ -621,9 +691,10 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (A+)
     DEFINE A AS v % 4 <> 0
-);
+);');
 
 -- Test 14.2: Star quantifier (zero or more)
+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)
@@ -632,9 +703,10 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (A* B)
     DEFINE A AS v % 4 IN (1, 2), B AS v % 4 = 3
-);
+);');
 
 -- Test 14.3: Question mark (zero or one)
+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)
@@ -643,9 +715,10 @@ WINDOW w AS (
     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
-);
+);');
 
 -- Test 14.4: Exact count {n}
+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)
@@ -654,9 +727,10 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (A{3} B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
-);
+);');
 
 -- Test 14.5: Range {n,m}
+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)
@@ -665,9 +739,10 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (A{2,4} B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
-);
+);');
 
 -- Test 14.6: At least {n,}
+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)
@@ -676,7 +751,7 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (A{3,} B)
     DEFINE A AS v % 10 <> 0, B AS v % 10 = 0
-);
+);');
 
 --
 -- Section 15: Regression Tests for Statistics Accuracy
@@ -684,6 +759,7 @@ WINDOW w AS (
 
 -- Test 15.1: Verify state count accuracy
 -- Pattern A+ B with 20 rows should show predictable state behavior
+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)
@@ -692,9 +768,10 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
-);
+);');
 
 -- Test 15.2: Verify context count with known absorption
+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)
@@ -703,9 +780,10 @@ WINDOW w AS (
     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
-);
+);');
 
 -- Test 15.3: Verify match length with fixed-length 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)
@@ -714,13 +792,14 @@ WINDOW w AS (
     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
-);
+);');
 
 --
 -- Section 16: Alternation Pattern Tests
 --
 
 -- Test 16.1: Simple alternation
+SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
 FROM nfa_test
@@ -728,10 +807,11 @@ 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'
-);
+    DEFINE A AS cat = ''A'', B AS cat = ''B'', C AS cat = ''C''
+);');
 
 -- Test 16.2: Multiple items in alternation
+SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
 FROM nfa_test
@@ -740,11 +820,12 @@ WINDOW w AS (
     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'
-);
+        A AS cat = ''A'', B AS cat = ''B'', C AS cat = ''C'',
+        D AS cat = ''D'', E AS cat = ''E''
+);');
 
 -- Test 16.3: Alternation with quantifiers
+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)
@@ -753,13 +834,14 @@ WINDOW w AS (
     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
-);
+);');
 
 --
 -- Section 17: Group Pattern Tests
 --
 
 -- Test 17.1: Simple group
+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)
@@ -768,9 +850,10 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN ((A B)+)
     DEFINE A AS v % 2 = 1, B AS v % 2 = 0
-);
+);');
 
 -- Test 17.2: Group with bounded quantifier
+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)
@@ -779,9 +862,10 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN ((A B){2,4})
     DEFINE A AS v % 2 = 1, B AS v % 2 = 0
-);
+);');
 
 -- Test 17.3: Nested groups
+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)
@@ -790,13 +874,14 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (((A B){2})+)
     DEFINE A AS v % 2 = 1, B AS v % 2 = 0
-);
+);');
 
 --
 -- Section 18: Window Function Combinations
 --
 
 -- Test 18.1: count(*) with 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)
@@ -805,9 +890,10 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
-);
+);');
 
 -- Test 18.2: first_value with 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)
@@ -816,9 +902,10 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
-);
+);');
 
 -- Test 18.3: last_value with 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)
@@ -827,9 +914,10 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
-);
+);');
 
 -- Test 18.4: Multiple window functions
+SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT
     count(*) OVER w,
@@ -841,13 +929,14 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
-);
+);');
 
 --
 -- Section 19: DEFINE Expression Variations
 --
 
 -- Test 19.1: Complex boolean expressions
+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)
@@ -858,9 +947,10 @@ WINDOW w AS (
     DEFINE
         A AS (v % 5 <> 0) AND (v % 3 <> 0),
         B AS (v % 5 = 0) OR (v % 3 = 0)
-);
+);');
 
 -- Test 19.2: Using PREV function
+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)
@@ -872,9 +962,10 @@ WINDOW w AS (
         S AS TRUE,
         U AS v > PREV(v),
         D AS v < PREV(v)
-);
+);');
 
 -- Test 19.3: Using NULL comparisons
+SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
 FROM (
@@ -886,13 +977,14 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (A+ B)
     DEFINE A AS v IS NOT NULL, B AS v IS NULL
-);
+);');
 
 --
 -- Section 20: Large Scale Statistics Verification
 --
 
 -- Test 20.1: 500 rows - verify statistics scale correctly
+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)
@@ -901,9 +993,10 @@ WINDOW w AS (
     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
-);
+);');
 
 -- Test 20.2: High match count scenario
+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)
@@ -912,9 +1005,10 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (A B)
     DEFINE A AS v % 2 = 1, B AS v % 2 = 0
-);
+);');
 
 -- Test 20.3: High skip count scenario
+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)
@@ -928,7 +1022,7 @@ WINDOW w AS (
         C AS v % 100 = 3,
         D AS v % 100 = 4,
         E AS v % 100 = 5
-);
+);');
 
 -- Cleanup
 DROP TABLE nfa_test;
-- 
2.50.1 (Apple Git-155)


From 6be166670fbdaa233d343125cbf81b3af82d83bc Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Fri, 6 Feb 2026 14:15:45 +0900
Subject: [PATCH 1/1] Fix RPR pattern compilation crash and refactor EXPLAIN
 deparse

Fix several bugs in RPR (Row Pattern Recognition) NFA bytecode
compilation, pattern optimization, and EXPLAIN output.

Bugs fixed:

- Fix server crash in fillRPRPatternAlt() when an inner ALT is the
  last element of an outer ALT's branch.  Both alternations tried to
  set the 'next' pointer on the same endpoint element, triggering
  Assert(pat->elements[endPos].next == RPR_ELEMIDX_INVALID).  Fix by
  redirecting all elements in the branch that share the old target to
  point to the outer ALT's afterAlt instead.
  (Test: PATTERN (C (A | B) | D) -- inner ALT at end of outer branch)

- Add BEGIN element to fillRPRPatternGroup().  Previously only END was
  emitted for quantified groups, so groups with min=0 had no skip path
  to bypass the group content.  Change to BEGIN/END pairs where
  BEGIN.jump points past END (skip) and END.jump points to the first
  child (loop-back).  Add nfa_advance_begin() to the NFA executor for
  group entry and skip handling.
  (Test: PATTERN (A ((B | C) (D | E))* F?) -- * group matching 0 times)

- Fix deparse output producing wrong parenthesization for nested ALT
  patterns.  The flat-loop approach could not correctly suppress double
  parentheses for single-ALT groups or handle depth transitions around
  alternation separators.  Refactor into mutual recursion:
  deparse_rpr_elements, deparse_rpr_group, deparse_rpr_alt, and
  deparse_rpr_var.

- Fix deparse separator ordering for nested ALT: the ' | ' separator
  was emitted before closing inner parentheses, producing output like
  '(c (a | b | )d)' instead of '(c (a | b) | d)'.  Close parens to
  match separator depth before emitting ' | '.

- Fix missing ALT separator registration in deparse_rpr_alt() when an
  ALT is the first element of an outer ALT's branch.  The code only
  checked elem->next (always -1 for ALT markers) but not elem->jump,
  which carries the outer branch's separator position.

- Fix altEndPositions/altBranchStarts length mismatch caused by the
  'if (*idx > branchStart)' guard that skipped empty branches.  Remove
  the guard so both lists always have matching entries.

- Fix RPRPatternNode.reluctant initialization in gram.y.  ALT, SEQ,
  VAR, and GROUP primary productions used false (0) or left it
  uninitialized (defaulting to 0 from palloc0), but 0 is a valid
  ParseLoc meaning "location at offset 0".  Change all four creation
  sites to use -1 (no location), matching the convention used by
  ParseLoc throughout the parser.

- Fix reluctant quantifier display in both explain.c and ruleutils.c.
  A bare variable with reluctant ? (e.g. A?) was displayed as just 'a'
  since there was no quantifier to attach ? to.  Now emit '{1}?' to
  make the reluctant marker unambiguous.

New optimization:

- Add mergeConsecutiveAlts() to the SEQ optimization pipeline.  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, e.g.
  (A | B) (A | B) (A | B) -> (A | B){3}.  Combined with the existing
  mergeGroupPrefixSuffix, patterns like (A | B) (A | B)+ (A | B)
  further reduce to (A | B){3,}.

- Extend tryMultiplyQuantifiers() to fold nested GROUP quantifiers
  (e.g. ((A B)+)* -> (A B)*) in addition to VAR quantifiers.

Other changes:

- Add RPR_VARID_BEGIN (252) to rpr.h for the new control element type.
  Reduce RPR_VARID_MAX from 252 to 251 accordingly and update the
  maximum-variables boundary tests.

- Add RPR_DEPTH_NONE (255) as sentinel for top-level elements that
  have no enclosing group.  Reduce RPR_DEPTH_MAX to 254 accordingly.

- Add BEGIN handling to computeAbsorbabilityRecursive() so that
  absorbability flags propagate correctly through group boundaries.

- Extract show_rpr_nfa_stats() from show_windowagg_info() for NFA
  statistics display.

- Replace defensive NULL check in collectPatternVariables() with
  Assert, since the caller guarantees a non-NULL pattern.

- Pair each of the 82 EXPLAIN test queries in rpr_explain.sql with a
  pg_get_viewdef() check via CREATE TEMP VIEW, verifying that both
  the ruleutils (parse tree) and explain (bytecode) deparse paths
  produce consistent PATTERN output.

Also add EXPLAIN test cases for nested ALT patterns, consecutive ALT
merging, and ALT prefix/suffix merging.
---
 doc/src/sgml/advanced.sgml                |   58 +-
 doc/src/sgml/ref/select.sgml              |   52 +-
 src/backend/commands/explain.c            |  529 +++--
 src/backend/executor/nodeWindowAgg.c      |   39 +
 src/backend/optimizer/plan/rpr.c          |  224 ++-
 src/backend/parser/gram.y                 |    6 +-
 src/backend/utils/adt/ruleutils.c         |    6 +-
 src/include/optimizer/rpr.h               |   15 +-
 src/test/regress/expected/rpr.out         |   15 +-
 src/test/regress/expected/rpr_base.out    |   87 +-
 src/test/regress/expected/rpr_explain.out | 2166 +++++++++++++++++++--
 src/test/regress/sql/rpr.sql              |    6 +-
 src/test/regress/sql/rpr_base.sql         |   60 +-
 src/test/regress/sql/rpr_explain.sql      | 1517 +++++++++++++--
 14 files changed, 4211 insertions(+), 569 deletions(-)

diff --git a/doc/src/sgml/advanced.sgml b/doc/src/sgml/advanced.sgml
index eec2a0a9346..241c7e03a5a 100644
--- a/doc/src/sgml/advanced.sgml
+++ b/doc/src/sgml/advanced.sgml
@@ -574,9 +574,15 @@ DEFINE
     conditions defined in the <literal>DEFINE</literal> clause.  For example
     following <literal>PATTERN</literal> defines a sequence of rows starting
     with the a row satisfying "LOWPRICE", then one or more rows satisfying
-    "UP" and finally one or more rows satisfying "DOWN". Note that "+" means
-    one or more matches. Also you can use "*", which means zero or more
-    matches. If a sequence of rows which satisfies the PATTERN is found, in
+    "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 (optional),
+    "{n}" means exactly n matches, "{n,}" means at least n matches,
+    "{,m}" means at most m matches, and "{n,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
@@ -622,6 +628,52 @@ FROM stock
 </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 255-level depth limit rarely encountered.
+    They are applied transparently and can be observed
+    in <command>EXPLAIN</command> output.
+   </para>
+
+   <para>
+    To mitigate this, <productname>PostgreSQL</productname> 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 A can be absorbed, while <literal>(A' B')+"</literal>
+    shows that both A and B 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
diff --git a/doc/src/sgml/ref/select.sgml b/doc/src/sgml/ref/select.sgml
index 49b3c00d9f2..7ec7760f472 100644
--- a/doc/src/sgml/ref/select.sgml
+++ b/doc/src/sgml/ref/select.sgml
@@ -979,8 +979,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> ] [row_pattern_common_syntax]
-{ RANGE | ROWS | GROUPS } BETWEEN <replaceable>frame_start</replaceable> AND <replaceable>frame_end</replaceable> [ <replaceable>frame_exclusion</replaceable> ] [row_pattern_common_syntax]
+{ 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>
@@ -1098,15 +1098,15 @@ EXCLUDE NO OTHERS
 <synopsis>
 [ { AFTER MATCH SKIP PAST LAST ROW | AFTER MATCH SKIP TO NEXT ROW } ]
 [ INITIAL | SEEK ]
-PATTERN ( <replaceable class="parameter">pattern_variable_name</replaceable>[*|+|?] [, ...] )
-DEFINE <replaceable class="parameter">definition_varible_name</replaceable> AS <replaceable class="parameter">expression</replaceable> [, ...]
+PATTERN ( <replaceable class="parameter">pattern_variable_name</replaceable> [ <replaceable>quantifier</replaceable> ] [, ...] )
+DEFINE <replaceable class="parameter">definition_variable_name</replaceable> AS <replaceable class="parameter">expression</replaceable> [, ...]
 </synopsis>
     <literal>AFTER MATCH SKIP PAST LAST ROW</literal> or <literal>AFTER MATCH
     SKIP TO NEXT ROW</literal> controls how to proceed to next row position
     after a match found. With <literal>AFTER MATCH SKIP PAST LAST
     ROW</literal> (the default) next row position is next to the last row of
     previous match. On the other hand, with <literal>AFTER MATCH SKIP TO NEXT
-    ROW</literal> next row position is always next to the last row of previous
+    ROW</literal> next row position is next to the first row of previous
     match. <literal>INITIAL</literal> or <literal>SEEK</literal> defines how a
     successful pattern matching starts from which row in a
     frame. If <literal>INITIAL</literal> is specified, the match must start
@@ -1117,16 +1117,48 @@ DEFINE <replaceable class="parameter">definition_varible_name</replaceable> AS <
     defines definition variables along with a boolean
     expression. <literal>PATTERN</literal> defines a sequence of rows that
     satisfies certain conditions using variables defined
-    in <literal>DEFINE</literal> clause. If the variable is not defined in
-    the <literal>DEFINE</literal> clause, it is implicitly assumed following
-    is defined in the <literal>DEFINE</literal> clause.
+    in <literal>DEFINE</literal> clause. 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),
+    <literal>{</literal><replaceable>n</replaceable><literal>,}</literal> (at least <replaceable>n</replaceable> times),
+    <literal>{,</literal><replaceable>m</replaceable><literal>}</literal> (at most <replaceable>m</replaceable> times), or
+    <literal>{</literal><replaceable>n</replaceable><literal>,</literal><replaceable>m</replaceable><literal>}</literal>
+    (between <replaceable>n</replaceable> and <replaceable>m</replaceable> times).
+    Reluctant quantifiers (e.g., <literal>*?</literal>, <literal>+?</literal>,
+    <literal>??</literal>, <literal>{</literal><replaceable>n</replaceable><literal>,</literal><replaceable>m</replaceable><literal>}?</literal>)
+    are 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>
 
-    Note that the maximum number of variables defined
-    in <literal>DEFINE</literal> clause is 26.
+    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 <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 255 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>
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index cc762d7b21b..96542e9538d 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -120,6 +120,18 @@ static void show_window_keys(StringInfo buf, PlanState *planstate,
 							 List *ancestors, ExplainState *es);
 static void append_rpr_quantifier(StringInfo buf, RPRPatternElement *elem);
 static char *deparse_rpr_pattern(RPRPattern *pattern);
+static void deparse_rpr_elements(RPRPattern *pattern, int *idx,
+								 StringInfoData *buf, RPRDepth groupDepth,
+								 RPRDepth *prevDepth, bool *needSpace);
+static void deparse_rpr_group(RPRPattern *pattern, int *idx,
+							  StringInfoData *buf, RPRDepth *prevDepth,
+							  bool *needSpace);
+static void deparse_rpr_alt(RPRPattern *pattern, int *idx,
+							StringInfoData *buf, RPRDepth *prevDepth,
+							bool *needSpace, List **altSeps);
+static void deparse_rpr_var(RPRPattern *pattern, int *idx,
+							StringInfoData *buf, RPRDepth *prevDepth,
+							bool *needSpace, List **altSeps);
 static void show_storage_info(char *maxStorageType, int64 maxSpaceUsed,
 							  ExplainState *es);
 static void show_tablesample(TableSampleClause *tsc, PlanState *planstate,
@@ -130,6 +142,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);
@@ -2913,126 +2926,261 @@ append_rpr_quantifier(StringInfo buf, RPRPatternElement *elem)
 		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.
- * Simple approach: output parens for each depth level as-is.
+ *
+ * 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			i;
+	int			idx = 0;
 	RPRDepth	prevDepth = 0;
 	bool		needSpace = false;
-	List	   *altSepList = NIL;	/* list of pending ALT separator positions */
 
-	if (pattern == NULL || pattern->numElements == 0)
-		return NULL;
+	Assert(pattern != NULL && pattern->numElements >= 2);
 
 	initStringInfo(&buf);
 
-	for (i = 0; i < pattern->numElements; i++)
+	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[i];
+		RPRPatternElement *elem = &pattern->elements[*idx];
 
 		if (RPRElemIsFin(elem))
 			break;
 
-		/* Alternation separator - check if current position is in the list */
-		if (list_member_int(altSepList, i))
-		{
-			appendStringInfoString(&buf, " | ");
-			needSpace = false;
-			altSepList = list_delete_int(altSepList, i);
-		}
+		/* Stop at END matching our group depth; caller handles it */
+		if (RPRElemIsEnd(elem) && elem->depth == groupDepth)
+			break;
 
-		if (RPRElemIsAlt(elem))
+		/* Alternation separator */
+		if (list_member_int(altSeps, *idx))
 		{
-			/* Open parens up to ALT's depth (content is at depth+1) */
-			while (prevDepth < elem->depth)
+			/* Close parens to match separator depth first */
+			while (*prevDepth > elem->depth)
 			{
-				if (needSpace)
-					appendStringInfoChar(&buf, ' ');
-				appendStringInfoChar(&buf, '(');
-				prevDepth++;
-				needSpace = false;
+				appendStringInfoChar(buf, ')');
+				(*prevDepth)--;
 			}
-			/* ALT's first element may have jump to next alternative */
-			if (elem->next != RPR_ELEMIDX_INVALID)
-			{
-				RPRPatternElement *firstElem = &pattern->elements[elem->next];
-
-				if (firstElem->jump != RPR_ELEMIDX_INVALID)
-					altSepList = list_append_unique_int(altSepList, firstElem->jump);
-			}
-			continue;
+			appendStringInfoString(buf, " | ");
+			*needSpace = false;
+			altSeps = list_delete_int(altSeps, *idx);
 		}
 
-		if (RPRElemIsEnd(elem))
+		/* 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++)
 		{
-			/* Close down to END's depth, output quantifier */
-			while (prevDepth > elem->depth + 1)
+			RPRPatternElement *e = &pattern->elements[j];
+
+			if (RPRElemIsEnd(e) && e->depth == begin->depth)
+				break;
+			if (e->depth <= childDepth)
 			{
-				appendStringInfoChar(&buf, ')');
-				prevDepth--;
+				singleAlt = false;
+				break;
 			}
-			appendStringInfoChar(&buf, ')');
-			append_rpr_quantifier(&buf, elem);
-			prevDepth = elem->depth;
-			needSpace = true;
-			continue;
 		}
+	}
 
-		if (RPRElemIsVar(elem))
-		{
-			/* Open parens for depth increase */
-			while (prevDepth < elem->depth)
-			{
-				if (needSpace)
-					appendStringInfoChar(&buf, ' ');
-				appendStringInfoChar(&buf, '(');
-				prevDepth++;
-				needSpace = false;
-			}
+	/* Open group paren (unless single ALT provides it) */
+	if (!singleAlt)
+	{
+		if (*needSpace)
+			appendStringInfoChar(buf, ' ');
+		appendStringInfoChar(buf, '(');
+		*needSpace = false;
+	}
+	*prevDepth = childDepth;
+	(*idx)++;						/* consume BEGIN */
 
-			/* Close parens for depth decrease */
-			while (prevDepth > elem->depth)
-			{
-				appendStringInfoChar(&buf, ')');
-				prevDepth--;
-			}
+	/* Process group children; stops at matching END */
+	deparse_rpr_elements(pattern, idx, buf, begin->depth,
+						 prevDepth, needSpace);
 
-			if (needSpace)
-				appendStringInfoChar(&buf, ' ');
+	/* Consume END and output quantifier */
+	Assert(*idx < pattern->numElements);
+	end = &pattern->elements[*idx];
+	Assert(RPRElemIsEnd(end) && end->depth == begin->depth);
 
-			appendStringInfoString(&buf, pattern->varNames[elem->varId]);
-			append_rpr_quantifier(&buf, elem);
-			needSpace = true;
+	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 */
+}
 
-			if (elem->jump != RPR_ELEMIDX_INVALID)
-				altSepList = list_append_unique_int(altSepList, elem->jump);
-		}
+/*
+ * 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;
 	}
 
-	list_free(altSepList);
+	/* Open parens up to ALT's depth */
+	while (*prevDepth < elem->depth)
+	{
+		if (*needSpace)
+			appendStringInfoChar(buf, ' ');
+		appendStringInfoChar(buf, '(');
+		(*prevDepth)++;
+		*needSpace = false;
+	}
 
-	/* Close remaining */
-	while (prevDepth > 0)
+	/* Register next alternation separator position */
+	if (elem->next != RPR_ELEMIDX_INVALID)
 	{
-		appendStringInfoChar(&buf, ')');
-		prevDepth--;
+		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;
 	}
 
-	return buf.data;
+	/* Close parens for depth decrease */
+	while (*prevDepth > elem->depth)
+	{
+		appendStringInfoChar(buf, ')');
+		(*prevDepth)--;
+	}
+
+	if (*needSpace)
+		appendStringInfoChar(buf, ' ');
+
+	Assert(elem->varId < pattern->numVars);
+	appendStringInfoString(buf, pattern->varNames[elem->varId]);
+	append_rpr_quantifier(buf, elem);
+	*needSpace = true;
+
+	if (elem->jump != RPR_ELEMIDX_INVALID)
+		*altSeps = lappend_int(*altSeps, elem->jump);
+	(*idx)++;
 }
 
 /*
@@ -3673,140 +3821,155 @@ show_windowagg_info(WindowAggState *winstate, ExplainState *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)
 	{
-		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 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);
-			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);
-			}
-			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);
-			}
+			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);
 		}
-		else
+		if (winstate->nfaMatchesFailed > 0)
 		{
-			ExplainIndentText(es);
+			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,
-							 "NFA States: " INT64_FORMAT " peak, " INT64_FORMAT " total, " INT64_FORMAT " merged\n",
-							 winstate->nfaStatesMax,
-							 winstate->nfaStatesTotalCreated,
-							 winstate->nfaStatesMerged);
-			ExplainIndentText(es);
+							 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,
-							 "NFA Contexts: " INT64_FORMAT " peak, " INT64_FORMAT " total, " INT64_FORMAT " pruned\n",
-							 winstate->nfaContextsMax,
-							 winstate->nfaContextsTotalCreated,
-							 winstate->nfaContextsPruned);
+							 ", " 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->nfaMatchesSucceeded > 0)
+
+			if (winstate->nfaContextsAbsorbed > 0)
 			{
-				double		avgLen = (double) winstate->nfaMatchLen.total / winstate->nfaMatchesSucceeded;
+				double		avgAbsorbed = (double) winstate->nfaAbsorbedLen.total / winstate->nfaContextsAbsorbed;
 
 				appendStringInfo(es->str,
-								 INT64_FORMAT " matched (len " INT64_FORMAT "/" INT64_FORMAT "/%.1f)",
-								 winstate->nfaMatchesSucceeded,
-								 winstate->nfaMatchLen.min,
-								 winstate->nfaMatchLen.max,
-								 avgLen);
+								 INT64_FORMAT " absorbed (len " INT64_FORMAT "/" INT64_FORMAT "/%.1f)",
+								 winstate->nfaContextsAbsorbed,
+								 winstate->nfaAbsorbedLen.min,
+								 winstate->nfaAbsorbedLen.max,
+								 avgAbsorbed);
 			}
 			else
 			{
-				appendStringInfo(es->str, "0 matched");
+				appendStringInfo(es->str, "0 absorbed");
 			}
-			if (winstate->nfaMatchesFailed > 0)
+
+			if (winstate->nfaContextsSkipped > 0)
 			{
-				double		avgFail = (double) winstate->nfaFailLen.total / winstate->nfaMatchesFailed;
+				double		avgSkipped = (double) winstate->nfaSkippedLen.total / winstate->nfaContextsSkipped;
 
 				appendStringInfo(es->str,
-								 ", " INT64_FORMAT " mismatched (len " INT64_FORMAT "/" INT64_FORMAT "/%.1f)",
-								 winstate->nfaMatchesFailed,
-								 winstate->nfaFailLen.min,
-								 winstate->nfaFailLen.max,
-								 avgFail);
+								 ", " INT64_FORMAT " skipped (len " INT64_FORMAT "/" INT64_FORMAT "/%.1f)",
+								 winstate->nfaContextsSkipped,
+								 winstate->nfaSkippedLen.min,
+								 winstate->nfaSkippedLen.max,
+								 avgSkipped);
 			}
 			else
 			{
-				appendStringInfo(es->str, ", 0 mismatched");
+				appendStringInfo(es->str, ", 0 skipped");
 			}
-			appendStringInfoChar(es->str, '\n');
-
-			/* Show absorbed and 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');
-			}
+			appendStringInfoChar(es->str, '\n');
 		}
 	}
 }
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 1e088615d19..b2874bf6e9d 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -6048,6 +6048,41 @@ nfa_advance_alt(WindowAggState *winstate, RPRNFAContext *ctx,
 		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, bool initialAdvance)
+{
+	RPRPattern *pattern = winstate->rpPattern;
+	RPRPatternElement *elements = pattern->elements;
+
+	state->counts[elem->depth] = 0;
+
+	/* Optional group: create skip path */
+	if (elem->min == 0)
+	{
+		RPRNFAState *skipState;
+
+		skipState = nfa_state_clone(winstate, elem->jump, state->altPriority,
+									 state->counts, state->isAbsorbable);
+		nfa_route_to_elem(winstate, ctx, skipState,
+						  &elements[elem->jump], currentPos, initialAdvance);
+	}
+
+	/* Enter group: route to first child */
+	state->elemIdx = elem->next;
+	nfa_route_to_elem(winstate, ctx, state,
+					  &elements[state->elemIdx], currentPos, initialAdvance);
+}
+
 /*
  * nfa_advance_end
  *
@@ -6233,6 +6268,10 @@ nfa_advance_state(WindowAggState *winstate, RPRNFAContext *ctx,
 			nfa_advance_alt(winstate, ctx, state, elem, currentPos, initialAdvance);
 			break;
 
+		case RPR_VARID_BEGIN:
+			nfa_advance_begin(winstate, ctx, state, elem, currentPos, initialAdvance);
+			break;
+
 		case RPR_VARID_END:
 			nfa_advance_end(winstate, ctx, state, elem, currentPos, initialAdvance);
 			break;
diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index 50043c416c6..67710a94a0d 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -51,6 +51,7 @@ static RPRPatternNode *tryUnwrapSingleChild(RPRPatternNode *pattern);
 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);
 
@@ -363,6 +364,110 @@ mergeConsecutiveGroups(List *children)
 	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 < 0)
+		{
+			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 = -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 = -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 = -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.
@@ -529,8 +634,9 @@ mergeGroupPrefixSuffix(List *children)
  *   1. Flatten nested SEQ and GROUP{1,1}
  *   2. Merge consecutive identical VAR nodes
  *   3. Merge consecutive identical GROUP nodes
- *   4. Merge prefix/suffix into GROUP with matching children
- *   5. Unwrap single-item SEQ
+ *   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)
@@ -544,6 +650,9 @@ optimizeSeqPattern(RPRPatternNode *pattern)
 	/* 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);
 
@@ -665,12 +774,17 @@ tryMultiplyQuantifiers(RPRPatternNode *pattern)
 	int64		new_min_64;
 	int64		new_max_64;
 
-	if (list_length(pattern->children) != 1 || pattern->reluctant >= 0)
+	/* Parser always creates GROUP with exactly one child */
+	Assert(list_length(pattern->children) == 1);
+
+	if (pattern->reluctant >= 0)
 		return pattern;
 
 	child = (RPRPatternNode *) linitial(pattern->children);
 
-	if (child->nodeType != RPR_PATTERN_VAR || child->reluctant >= 0)
+	if ((child->nodeType != RPR_PATTERN_VAR &&
+		 child->nodeType != RPR_PATTERN_GROUP) ||
+		child->reluctant >= 0)
 		return pattern;
 
 	/* Case 1: Both unbounded - (A*)* -> A*, (A+)+ -> A+ */
@@ -890,6 +1004,10 @@ scanRPRPatternRecursive(RPRPatternNode *node, char **varNames, int *numVars,
 			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)
 			{
@@ -1028,38 +1146,51 @@ fillRPRPatternGroup(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth de
 {
 	ListCell   *lc;
 	int			groupStartIdx = *idx;
-	bool		altOnlyChild;
-	RPRDepth	childDepth;
-
-	/*
-	 * Fill group content at increased depth. Exception: if the only child is
-	 * ALT, don't increase depth since GROUP's parens already provide visual
-	 * grouping. This avoids output like "((a | b))+" instead of "(a | b)+".
-	 */
-	altOnlyChild = (list_length(node->children) == 1 &&
-					((RPRPatternNode *) linitial(node->children))->nodeType == RPR_PATTERN_ALT);
-	childDepth = altOnlyChild ? depth : depth + 1;
+	int			beginIdx = -1;
 
-	foreach(lc, node->children)
-	{
-		fillRPRPattern((RPRPatternNode *) lfirst(lc), pat, idx, childDepth);
-	}
-
-	/* Add group end marker if group has non-trivial quantifier */
+	/* 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_END;
+		elem->varId = RPR_VARID_BEGIN;
 		elem->depth = depth;
 		elem->min = node->min;
 		elem->max = (node->max == INT_MAX) ? RPR_QUANTITY_INF : node->max;
-		elem->next = RPR_ELEMIDX_INVALID;
-		elem->jump = groupStartIdx;
+		elem->next = RPR_ELEMIDX_INVALID;	/* set by finalize */
+		elem->jump = RPR_ELEMIDX_INVALID;	/* set after END */
 		if (node->reluctant >= 0)
 			elem->flags |= RPR_ELEM_RELUCTANT;
 		(*idx)++;
+		groupStartIdx = *idx;	/* children start after BEGIN */
+	}
+
+	foreach(lc, node->children)
+	{
+		fillRPRPattern((RPRPatternNode *) lfirst(lc), pat, idx, depth + 1);
+	}
+
+	/* 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;
+		endElem->next = RPR_ELEMIDX_INVALID;
+		endElem->jump = groupStartIdx;		/* loop to first child */
+		if (node->reluctant >= 0)
+			endElem->flags |= RPR_ELEM_RELUCTANT;
+		(*idx)++;
+
+		/* Set BEGIN skip pointer (next is set by finalize) */
+		beginElem->jump = *idx;				/* skip: go to after END */
 	}
 }
 
@@ -1097,9 +1228,7 @@ fillRPRPatternAlt(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth dept
 
 		altBranchStarts = lappend_int(altBranchStarts, branchStart);
 		fillRPRPattern(alt, pat, idx, depth + 1);
-
-		if (*idx > branchStart)
-			altEndPositions = lappend_int(altEndPositions, *idx - 1);
+		altEndPositions = lappend_int(altEndPositions, *idx - 1);
 	}
 
 	/* Set jump on first element of each alternative to next alternative */
@@ -1114,13 +1243,35 @@ fillRPRPatternAlt(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth dept
 	/* Set next on last element of each alternative to after the alternation */
 	{
 		int			afterAltIdx = *idx;
+		ListCell   *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;
+			}
 
-			Assert(pat->elements[endPos].next == RPR_ELEMIDX_INVALID);
-			pat->elements[endPos].next = afterAltIdx;
+			lc2 = lnext(altBranchStarts, lc2);
 		}
 	}
 
@@ -1451,9 +1602,18 @@ computeAbsorbabilityRecursive(RPRPattern *pattern, RPRElemIdx startIdx,
 		if (*hasAbsorbable)
 			elem->flags |= RPR_ELEM_ABSORBABLE_BRANCH;
 	}
+	else if (RPRElemIsBegin(elem))
+	{
+		/* BEGIN: skip to first child and check that */
+		computeAbsorbabilityRecursive(pattern, elem->next, hasAbsorbable, parentDepth);
+
+		/* Mark BEGIN element if contents are absorbable */
+		if (*hasAbsorbable)
+			elem->flags |= RPR_ELEM_ABSORBABLE_BRANCH;
+	}
 	else
 	{
-		/* Non-ALT: check if unbounded start */
+		/* Non-ALT, non-BEGIN: check if unbounded start */
 		if (isUnboundedStart(pattern, startIdx, parentDepth))
 		{
 			*hasAbsorbable = true;
@@ -1521,8 +1681,8 @@ collectPatternVariables(RPRPatternNode *pattern)
 {
 	List	   *varNames = NIL;
 
-	if (pattern == NULL)
-		return NIL;
+	/* Caller ensures pattern is not NULL */
+	Assert(pattern != NULL);
 
 	collectPatternVariablesRecursive(pattern, &varNames);
 	return varNames;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index dbd667fc51a..ca7d0d25db3 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -16922,6 +16922,7 @@ row_pattern_alt:
 						n->children = list_make2($1, $3);
 						n->min = 1;
 						n->max = 1;
+						n->reluctant = -1;
 						n->location = @1;
 						$$ = (Node *) n;
 					}
@@ -16948,6 +16949,7 @@ row_pattern_seq:
 						n->children = list_make2($1, $2);
 						n->min = 1;
 						n->max = 1;
+						n->reluctant = -1;
 						n->location = @1;
 						$$ = (Node *) n;
 					}
@@ -16975,7 +16977,7 @@ row_pattern_primary:
 					n->varName = $1;
 					n->min = 1;
 					n->max = 1;
-					n->reluctant = false;
+					n->reluctant = -1;
 					n->children = NIL;
 					n->location = @1;
 					$$ = (Node *) n;
@@ -16988,7 +16990,7 @@ row_pattern_primary:
 					n->children = list_make1(inner);
 					n->min = 1;
 					n->max = 1;
-					n->reluctant = false;
+					n->reluctant = -1;
 					n->location = @1;
 					$$ = (Node *) n;
 				}
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index a7adba616e6..0d9e2e004e8 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -6776,8 +6776,12 @@ append_pattern_quantifier(StringInfo buf, RPRPatternNode *node)
 	else
 		appendStringInfo(buf, "{%d,%d}", node->min, node->max);
 
-	if (node->reluctant >= 0 && has_quantifier)
+	if (node->reluctant >= 0)
+	{
+		if (!has_quantifier)
+			appendStringInfo(buf, "{1}");	/* make reluctant ? unambiguous */
 		appendStringInfoChar(buf, '?');
+	}
 }
 
 /*
diff --git a/src/include/optimizer/rpr.h b/src/include/optimizer/rpr.h
index 691c6c27e97..8e1bc47643c 100644
--- a/src/include/optimizer/rpr.h
+++ b/src/include/optimizer/rpr.h
@@ -17,16 +17,18 @@
 #include "nodes/plannodes.h"
 
 /* Limits and special values */
-#define RPR_VARID_MAX		252 /* max pattern variables: 252 */
+#define RPR_VARID_MAX		251 /* max pattern variables: 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	/* max pattern nesting depth */
+#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 (253-255) */
-#define RPR_VARID_ALT		((RPRVarId) 253)	/* alternation start */
-#define RPR_VARID_END		((RPRVarId) 254)	/* group end */
+/* 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 */
@@ -40,8 +42,9 @@
 #define RPRElemIsAbsorbableBranch(e)	((e)->flags & RPR_ELEM_ABSORBABLE_BRANCH)
 #define RPRElemIsAbsorbable(e)			((e)->flags & RPR_ELEM_ABSORBABLE)
 #define RPRElemIsVar(e)			((e)->varId <= RPR_VARID_MAX)
-#define RPRElemIsAlt(e)			((e)->varId == RPR_VARID_ALT)
+#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)
 
diff --git a/src/test/regress/expected/rpr.out b/src/test/regress/expected/rpr.out
index d4298860865..8c8ff90e1cd 100644
--- a/src/test/regress/expected/rpr.out
+++ b/src/test/regress/expected/rpr.out
@@ -2724,8 +2724,8 @@ SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER
 ERROR:  reluctant quantifiers are not yet supported
 LINE 9:  PATTERN (START UP+? DOWN+)
                            ^
--- Maximum pattern variables is 252 (RPR_VARID_MAX)
--- Ok: 252 variables (maximum allowed)
+-- Maximum pattern variables is 251 (RPR_VARID_MAX)
+-- Error: 252 variables exceeds limit of 251
 DO $$
 DECLARE
     pattern_vars text;
@@ -2745,7 +2745,14 @@ BEGIN
     EXECUTE query;
 END;
 $$;
--- Error: 253 variables exceeds limit of 252
+ERROR:  too many pattern variables
+DETAIL:  Maximum is 251.
+CONTEXT:  SQL statement "SELECT * FROM (SELECT 1 AS x) t WINDOW w AS (
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (v001 v002 v003 v004 v005 v006 v007 v008 v009 v010 v011 v012 v013 v014 v015 v016 v017 v018 v019 v020 v021 v022 v023 v024 v025 v026 v027 v028 v029 v030 v031 v032 v033 v034 v035 v036 v037 v038 v039 v040 v041 v042 v043 v044 v045 v046 v047 v048 v049 v050 v051 v052 v053 v054 v055 v056 v057 v058 v059 v060 v061 v062 v063 v064 v065 v066 v067 v068 v069 v070 v071 v072 v073 v074 v075 v076 v077 v078 v079 v080 v081 v082 v083 v084 v085 v086 v087 v088 v089 v090 v091 v092 v093 v094 v095 v096 v097 v098 v099 v100 v101 v102 v103 v104 v105 v106 v107 v108 v109 v110 v111 v112 v113 v114 v115 v116 v117 v118 v119 v120 v121 v122 v123 v124 v125 v126 v127 v128 v129 v130 v131 v132 v133 v134 v135 v136 v137 v138 v139 v140 v141 v142 v143 v144 v145 v146 v147 v148 v149 v150 v151 v152 v153 v154 v155 v156 v157 v158 v159 v160 v161 v162 v163 v164 v165 v166 v167 v168 v169 v170 v171 v172 v173 v174 v175 v176 v177 v178 v179 v180 v181 v182 v183 v184 v185 v186 v187 v188 v189 v190 v191 v192 v193 v194 v195 v196 v197 v198 v199 v200 v201 v202 v203 v204 v205 v206 v207 v208 v209 v210 v211 v212 v213 v214 v215 v216 v217 v218 v219 v220 v221 v222 v223 v224 v225 v226 v227 v228 v229 v230 v231 v232 v233 v234 v235 v236 v237 v238 v239 v240 v241 v242 v243 v244 v245 v246 v247 v248 v249 v250 v251 v252)
+        DEFINE v001 AS TRUE, v002 AS TRUE, v003 AS TRUE, v004 AS TRUE, v005 AS TRUE, v006 AS TRUE, v007 AS TRUE, v008 AS TRUE, v009 AS TRUE, v010 AS TRUE, v011 AS TRUE, v012 AS TRUE, v013 AS TRUE, v014 AS TRUE, v015 AS TRUE, v016 AS TRUE, v017 AS TRUE, v018 AS TRUE, v019 AS TRUE, v020 AS TRUE, v021 AS TRUE, v022 AS TRUE, v023 AS TRUE, v024 AS TRUE, v025 AS TRUE, v026 AS TRUE, v027 AS TRUE, v028 AS TRUE, v029 AS TRUE, v030 AS TRUE, v031 AS TRUE, v032 AS TRUE, v033 AS TRUE, v034 AS TRUE, v035 AS TRUE, v036 AS TRUE, v037 AS TRUE, v038 AS TRUE, v039 AS TRUE, v040 AS TRUE, v041 AS TRUE, v042 AS TRUE, v043 AS TRUE, v044 AS TRUE, v045 AS TRUE, v046 AS TRUE, v047 AS TRUE, v048 AS TRUE, v049 AS TRUE, v050 AS TRUE, v051 AS TRUE, v052 AS TRUE, v053 AS TRUE, v054 AS TRUE, v055 AS TRUE, v056 AS TRUE, v057 AS TRUE, v058 AS TRUE, v059 AS TRUE, v060 AS TRUE, v061 AS TRUE, v062 AS TRUE, v063 AS TRUE, v064 AS TRUE, v065 AS TRUE, v066 AS TRUE, v067 AS TRUE, v068 AS TRUE, v069 AS TRUE, v070 AS TRUE, v071 AS TRUE, v072 AS TRUE, v073 AS TRUE, v074 AS TRUE, v075 AS TRUE, v076 AS TRUE, v077 AS TRUE, v078 AS TRUE, v079 AS TRUE, v080 AS TRUE, v081 AS TRUE, v082 AS TRUE, v083 AS TRUE, v084 AS TRUE, v085 AS TRUE, v086 AS TRUE, v087 AS TRUE, v088 AS TRUE, v089 AS TRUE, v090 AS TRUE, v091 AS TRUE, v092 AS TRUE, v093 AS TRUE, v094 AS TRUE, v095 AS TRUE, v096 AS TRUE, v097 AS TRUE, v098 AS TRUE, v099 AS TRUE, v100 AS TRUE, v101 AS TRUE, v102 AS TRUE, v103 AS TRUE, v104 AS TRUE, v105 AS TRUE, v106 AS TRUE, v107 AS TRUE, v108 AS TRUE, v109 AS TRUE, v110 AS TRUE, v111 AS TRUE, v112 AS TRUE, v113 AS TRUE, v114 AS TRUE, v115 AS TRUE, v116 AS TRUE, v117 AS TRUE, v118 AS TRUE, v119 AS TRUE, v120 AS TRUE, v121 AS TRUE, v122 AS TRUE, v123 AS TRUE, v124 AS TRUE, v125 AS TRUE, v126 AS TRUE, v127 AS TRUE, v128 AS TRUE, v129 AS TRUE, v130 AS TRUE, v131 AS TRUE, v132 AS TRUE, v133 AS TRUE, v134 AS TRUE, v135 AS TRUE, v136 AS TRUE, v137 AS TRUE, v138 AS TRUE, v139 AS TRUE, v140 AS TRUE, v141 AS TRUE, v142 AS TRUE, v143 AS TRUE, v144 AS TRUE, v145 AS TRUE, v146 AS TRUE, v147 AS TRUE, v148 AS TRUE, v149 AS TRUE, v150 AS TRUE, v151 AS TRUE, v152 AS TRUE, v153 AS TRUE, v154 AS TRUE, v155 AS TRUE, v156 AS TRUE, v157 AS TRUE, v158 AS TRUE, v159 AS TRUE, v160 AS TRUE, v161 AS TRUE, v162 AS TRUE, v163 AS TRUE, v164 AS TRUE, v165 AS TRUE, v166 AS TRUE, v167 AS TRUE, v168 AS TRUE, v169 AS TRUE, v170 AS TRUE, v171 AS TRUE, v172 AS TRUE, v173 AS TRUE, v174 AS TRUE, v175 AS TRUE, v176 AS TRUE, v177 AS TRUE, v178 AS TRUE, v179 AS TRUE, v180 AS TRUE, v181 AS TRUE, v182 AS TRUE, v183 AS TRUE, v184 AS TRUE, v185 AS TRUE, v186 AS TRUE, v187 AS TRUE, v188 AS TRUE, v189 AS TRUE, v190 AS TRUE, v191 AS TRUE, v192 AS TRUE, v193 AS TRUE, v194 AS TRUE, v195 AS TRUE, v196 AS TRUE, v197 AS TRUE, v198 AS TRUE, v199 AS TRUE, v200 AS TRUE, v201 AS TRUE, v202 AS TRUE, v203 AS TRUE, v204 AS TRUE, v205 AS TRUE, v206 AS TRUE, v207 AS TRUE, v208 AS TRUE, v209 AS TRUE, v210 AS TRUE, v211 AS TRUE, v212 AS TRUE, v213 AS TRUE, v214 AS TRUE, v215 AS TRUE, v216 AS TRUE, v217 AS TRUE, v218 AS TRUE, v219 AS TRUE, v220 AS TRUE, v221 AS TRUE, v222 AS TRUE, v223 AS TRUE, v224 AS TRUE, v225 AS TRUE, v226 AS TRUE, v227 AS TRUE, v228 AS TRUE, v229 AS TRUE, v230 AS TRUE, v231 AS TRUE, v232 AS TRUE, v233 AS TRUE, v234 AS TRUE, v235 AS TRUE, v236 AS TRUE, v237 AS TRUE, v238 AS TRUE, v239 AS TRUE, v240 AS TRUE, v241 AS TRUE, v242 AS TRUE, v243 AS TRUE, v244 AS TRUE, v245 AS TRUE, v246 AS TRUE, v247 AS TRUE, v248 AS TRUE, v249 AS TRUE, v250 AS TRUE, v251 AS TRUE, v252 AS TRUE)"
+PL/pgSQL function inline_code_block line 17 at EXECUTE
+-- Error: 253 variables exceeds limit of 251
 DO $$
 DECLARE
     pattern_vars text;
@@ -2766,7 +2773,7 @@ BEGIN
 END;
 $$;
 ERROR:  too many pattern variables
-DETAIL:  Maximum is 252.
+DETAIL:  Maximum is 251.
 CONTEXT:  SQL statement "SELECT * FROM (SELECT 1 AS x) t WINDOW w AS (
         ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
         PATTERN (v001 v002 v003 v004 v005 v006 v007 v008 v009 v010 v011 v012 v013 v014 v015 v016 v017 v018 v019 v020 v021 v022 v023 v024 v025 v026 v027 v028 v029 v030 v031 v032 v033 v034 v035 v036 v037 v038 v039 v040 v041 v042 v043 v044 v045 v046 v047 v048 v049 v050 v051 v052 v053 v054 v055 v056 v057 v058 v059 v060 v061 v062 v063 v064 v065 v066 v067 v068 v069 v070 v071 v072 v073 v074 v075 v076 v077 v078 v079 v080 v081 v082 v083 v084 v085 v086 v087 v088 v089 v090 v091 v092 v093 v094 v095 v096 v097 v098 v099 v100 v101 v102 v103 v104 v105 v106 v107 v108 v109 v110 v111 v112 v113 v114 v115 v116 v117 v118 v119 v120 v121 v122 v123 v124 v125 v126 v127 v128 v129 v130 v131 v132 v133 v134 v135 v136 v137 v138 v139 v140 v141 v142 v143 v144 v145 v146 v147 v148 v149 v150 v151 v152 v153 v154 v155 v156 v157 v158 v159 v160 v161 v162 v163 v164 v165 v166 v167 v168 v169 v170 v171 v172 v173 v174 v175 v176 v177 v178 v179 v180 v181 v182 v183 v184 v185 v186 v187 v188 v189 v190 v191 v192 v193 v194 v195 v196 v197 v198 v199 v200 v201 v202 v203 v204 v205 v206 v207 v208 v209 v210 v211 v212 v213 v214 v215 v216 v217 v218 v219 v220 v221 v222 v223 v224 v225 v226 v227 v228 v229 v230 v231 v232 v233 v234 v235 v236 v237 v238 v239 v240 v241 v242 v243 v244 v245 v246 v247 v248 v249 v250 v251 v252 v253)
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index 23851c5a11c..c269ab99651 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -2833,6 +2833,23 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
          ->  Seq Scan on rpr_plan
 (6 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,}"
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(6 rows)
+
 -- Consecutive GROUP merge with finite quantifiers: ((A B){5}) ((A B){10}) -> merged
 EXPLAIN (COSTS OFF)
 SELECT COUNT(*) OVER w FROM rpr_plan
@@ -2863,6 +2880,23 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
          ->  Seq Scan on rpr_plan
 (6 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,}"
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(6 rows)
+
 -- PREFIX merge: A B (A B)+ -> (a b){2,}
 EXPLAIN (COSTS OFF)
 SELECT COUNT(*) OVER w FROM rpr_plan
@@ -5057,13 +5091,13 @@ WINDOW w AS (
      0
 (2 rows)
 
--- Test: 252 variables in PATTERN, 253 in DEFINE (boundary - should succeed)
+-- Test: 251 variables in PATTERN, 252 in DEFINE (boundary - should succeed)
 -- Expected: Success - unused DEFINE variables are filtered out
 SELECT COUNT(*) OVER w FROM rpr_errors
 WINDOW w AS (
     ORDER BY id
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    PATTERN (V1 V2 V3 V4 V5 V6 V7 V8 V9 V10 V11 V12 V13 V14 V15 V16 V17 V18 V19 V20 V21 V22 V23 V24 V25 V26 V27 V28 V29 V30 V31 V32 V33 V34 V35 V36 V37 V38 V39 V40 V41 V42 V43 V44 V45 V46 V47 V48 V49 V50 V51 V52 V53 V54 V55 V56 V57 V58 V59 V60 V61 V62 V63 V64 V65 V66 V67 V68 V69 V70 V71 V72 V73 V74 V75 V76 V77 V78 V79 V80 V81 V82 V83 V84 V85 V86 V87 V88 V89 V90 V91 V92 V93 V94 V95 V96 V97 V98 V99 V100 V101 V102 V103 V104 V105 V106 V107 V108 V109 V110 V111 V112 V113 V114 V115 V116 V117 V118 V119 V120 V121 V122 V123 V124 V125 V126 V127 V128 V129 V130 V131 V132 V133 V134 V135 V136 V137 V138 V139 V140 V141 V142 V143 V144 V145 V146 V147 V148 V149 V150 V151 V152 V153 V154 V155 V156 V157 V158 V159 V160 V161 V162 V163 V164 V165 V166 V167 V168 V169 V170 V171 V172 V173 V174 V175 V176 V177 V178 V179 V180 V181 V182 V183 V184 V185 V186 V187 V188 V189 V190 V191 V192 V193 V194 V195 V196 V197 V198 V199 V200 V201 V202 V203 V204 V205 V206 V207 V208 V209 V210 V211 V212 V213 V214 V215 V216 V217 V218 V219 V220 V221 V222 V223 V224 V225 V226 V227 V228 V229 V230 V231 V232 V233 V234 V235 V236 V237 V238 V239 V240 V241 V242 V243 V244 V245 V246 V247 V248 V249 V250 V251 V252)
+    PATTERN (V1 V2 V3 V4 V5 V6 V7 V8 V9 V10 V11 V12 V13 V14 V15 V16 V17 V18 V19 V20 V21 V22 V23 V24 V25 V26 V27 V28 V29 V30 V31 V32 V33 V34 V35 V36 V37 V38 V39 V40 V41 V42 V43 V44 V45 V46 V47 V48 V49 V50 V51 V52 V53 V54 V55 V56 V57 V58 V59 V60 V61 V62 V63 V64 V65 V66 V67 V68 V69 V70 V71 V72 V73 V74 V75 V76 V77 V78 V79 V80 V81 V82 V83 V84 V85 V86 V87 V88 V89 V90 V91 V92 V93 V94 V95 V96 V97 V98 V99 V100 V101 V102 V103 V104 V105 V106 V107 V108 V109 V110 V111 V112 V113 V114 V115 V116 V117 V118 V119 V120 V121 V122 V123 V124 V125 V126 V127 V128 V129 V130 V131 V132 V133 V134 V135 V136 V137 V138 V139 V140 V141 V142 V143 V144 V145 V146 V147 V148 V149 V150 V151 V152 V153 V154 V155 V156 V157 V158 V159 V160 V161 V162 V163 V164 V165 V166 V167 V168 V169 V170 V171 V172 V173 V174 V175 V176 V177 V178 V179 V180 V181 V182 V183 V184 V185 V186 V187 V188 V189 V190 V191 V192 V193 V194 V195 V196 V197 V198 V199 V200 V201 V202 V203 V204 V205 V206 V207 V208 V209 V210 V211 V212 V213 V214 V215 V216 V217 V218 V219 V220 V221 V222 V223 V224 V225 V226 V227 V228 V229 V230 V231 V232 V233 V234 V235 V236 V237 V238 V239 V240 V241 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,
@@ -5090,7 +5124,7 @@ WINDOW w AS (
     V221 AS val > 0, V222 AS val > 0, V223 AS val > 0, V224 AS val > 0, V225 AS val > 0, V226 AS val > 0, V227 AS val > 0, V228 AS val > 0, V229 AS val > 0, V230 AS val > 0,
     V231 AS val > 0, V232 AS val > 0, V233 AS val > 0, V234 AS val > 0, V235 AS val > 0, V236 AS val > 0, V237 AS val > 0, V238 AS val > 0, V239 AS val > 0, V240 AS val > 0,
     V241 AS val > 0, V242 AS val > 0, V243 AS val > 0, V244 AS val > 0, V245 AS val > 0, V246 AS val > 0, V247 AS val > 0, V248 AS val > 0, V249 AS val > 0, V250 AS val > 0,
-    V251 AS val > 0, V252 AS val > 0, V253 AS val > 0
+    V251 AS val > 0, V252 AS val > 0
 );
  count 
 -------
@@ -5098,13 +5132,13 @@ WINDOW w AS (
      0
 (2 rows)
 
--- Test: 253 variables in PATTERN, 252 in DEFINE (exceeds limit with implicit TRUE)
--- Expected: ERROR - too many pattern variables (Maximum is 252)
+-- Test: 252 variables in PATTERN, 251 in DEFINE (exceeds limit with implicit TRUE)
+-- Expected: ERROR - too many pattern variables (Maximum is 251)
 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 V253)
+    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,
@@ -5131,18 +5165,18 @@ WINDOW w AS (
     V221 AS val > 0, V222 AS val > 0, V223 AS val > 0, V224 AS val > 0, V225 AS val > 0, V226 AS val > 0, V227 AS val > 0, V228 AS val > 0, V229 AS val > 0, V230 AS val > 0,
     V231 AS val > 0, V232 AS val > 0, V233 AS val > 0, V234 AS val > 0, V235 AS val > 0, V236 AS val > 0, V237 AS val > 0, V238 AS val > 0, V239 AS val > 0, V240 AS val > 0,
     V241 AS val > 0, V242 AS val > 0, V243 AS val > 0, V244 AS val > 0, V245 AS val > 0, V246 AS val > 0, V247 AS val > 0, V248 AS val > 0, V249 AS val > 0, V250 AS val > 0,
-    V251 AS val > 0, V252 AS val > 0
+    V251 AS val > 0
 );
 ERROR:  too many pattern variables
-DETAIL:  Maximum is 252.
--- Test: Pattern nesting at maximum depth (depth 254)
+DETAIL:  Maximum is 251.
+-- Test: Pattern nesting at maximum depth (depth 253)
 -- Expected: Should succeed
--- Note: 254 nested GROUP{3,7} quantifiers produce depth 254 after optimization
+-- 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}){3,7})
+    PATTERN ((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((A{3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7})
     DEFINE A AS val > 0
 );
  id | val | count 
@@ -5151,18 +5185,18 @@ WINDOW w AS (
   2 |  20 |     0
 (2 rows)
 
--- Test: Pattern nesting depth exceeds maximum (depth 255)
+-- Test: Pattern nesting depth exceeds maximum (depth 254)
 -- Expected: ERROR - pattern nesting too deep
--- Note: 255 nested GROUP{3,7} quantifiers produce depth 255 after optimization
+-- 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}){3,7})
+    PATTERN (((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((A{3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7})
     DEFINE A AS val > 0
 );
 ERROR:  pattern nesting too deep
-DETAIL:  Pattern nesting depth 255 exceeds maximum 254.
+DETAIL:  Pattern nesting depth 254 exceeds maximum 253.
 DROP TABLE rpr_errors;
 -- ============================================================
 -- Jacob's Patterns
@@ -5481,6 +5515,29 @@ WINDOW w AS (
  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;
 -- ============================================================
 -- End of rpr_base.sql
diff --git a/src/test/regress/expected/rpr_explain.out b/src/test/regress/expected/rpr_explain.out
index b5ceaae53b5..ea75d62718e 100644
--- a/src/test/regress/expected/rpr_explain.out
+++ b/src/test/regress/expected/rpr_explain.out
@@ -1,13 +1,42 @@
+-- ============================================================
+-- RPR EXPLAIN Tests
+-- Tests for Row Pattern Recognition EXPLAIN output
+-- ============================================================
 --
--- Test: EXPLAIN ANALYZE output for Row Pattern Recognition NFA statistics
+-- This test suite validates EXPLAIN output for RPR queries,
+-- including NFA statistics shown in EXPLAIN ANALYZE:
+--   - NFA States: peak, total, merged
+--   - NFA Contexts: peak, total, absorbed, skipped
+--   - NFA: matched (len min/max/avg), mismatched (len min/max/avg)
+--   - Pattern deparse formatting
+--   - Multiple output formats (text, JSON, XML)
 --
--- This file tests the NFA statistics shown in EXPLAIN ANALYZE output:
--- - NFA States: peak, total, merged
--- - NFA Contexts: peak, total, absorbed, skipped
--- - NFA: matched (len min/max/avg), mismatched (len min/max/avg)
---
--- Filter function to normalize Storage memory values only (not NFA statistics)
--- Works for text, JSON, and XML formats
+-- 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
+-- ============================================================
+-- Filter function to normalize Storage memory values only (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
 $$
@@ -69,10 +98,25 @@ VALUES
     (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');
---
--- Section 1: Basic NFA Statistics Tests
---
--- Test 1.1: Simple pattern - should show basic statistics
+-- ============================================================
+-- Basic NFA Statistics Tests
+-- ============================================================
+-- Simple pattern - should show basic statistics
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM nfa_test
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B)
+    DEFINE A AS cat = 'A', B AS cat = 'B'
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+       line       
+------------------
+   PATTERN (a b) 
+(1 row)
+
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -95,7 +139,23 @@ WINDOW w AS (
    ->  Seq Scan on nfa_test (actual rows=100.00 loops=1)
 (8 rows)
 
--- Test 1.2: Pattern with no matches - 0 matched
+DROP VIEW rpr_v;
+-- Pattern with no matches - 0 matched
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM nfa_test
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (X Y Z)
+    DEFINE X AS cat = 'X', Y AS cat = 'Y', Z AS cat = 'Z'
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+        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
@@ -118,7 +178,23 @@ WINDOW w AS (
    ->  Seq Scan on nfa_test (actual rows=100.00 loops=1)
 (8 rows)
 
--- Test 1.3: Pattern matching every row - high match count
+DROP VIEW rpr_v;
+-- Pattern matching every row - high match count
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM nfa_test
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (R)
+    DEFINE R AS TRUE
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+      line      
+----------------
+   PATTERN (r) 
+(1 row)
+
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -141,10 +217,95 @@ WINDOW w AS (
    ->  Seq Scan on nfa_test (actual rows=100.00 loops=1)
 (8 rows)
 
---
--- Section 2: State Statistics Tests (peak, total, merged)
---
--- Test 2.1: Simple quantifier pattern - A+ with short matches (no merging)
+DROP VIEW rpr_v;
+-- Regression test: Space before parenthesis in pattern deparse
+-- Verifies that "A (B | C)" correctly outputs as "a (b | c)" with space
+CREATE TEMP VIEW rpr_v AS
+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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+          line          
+------------------------
+   PATTERN (a (b | c)) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (COSTS 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
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a (b | c)
+   ->  Function Scan on generate_series s
+(4 rows)
+
+DROP VIEW rpr_v;
+-- Regression test: Sequential alternations at same depth
+-- Verifies that "((B | C) (D | E))" correctly outputs as "(b | c) (d | e)"
+-- Previously failed due to missing parentheses on ALT depth decrease
+CREATE TEMP VIEW rpr_v AS
+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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+               line                
+-----------------------------------
+   PATTERN (a ((b | c) (d | e))*) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (COSTS 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
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a ((b | c) (d | e))*
+   ->  Function Scan on generate_series s
+(4 rows)
+
+DROP VIEW rpr_v;
+-- ============================================================
+-- State Statistics Tests (peak, total, merged)
+-- ============================================================
+-- Simple quantifier pattern - A+ with short matches (no merging)
+CREATE TEMP VIEW rpr_v AS
+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_v'), 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
@@ -167,7 +328,25 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
 (8 rows)
 
--- Test 2.2: Alternation pattern - multiple state branches
+DROP VIEW rpr_v;
+-- Alternation pattern - multiple state branches
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM 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_v'), 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
@@ -184,7 +363,7 @@ WINDOW w AS (
 -------------------------------------------------------------------
  WindowAgg (actual rows=100.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: (a | b | c d | e)
+   Pattern: (a | b | c) (d | e)
    Storage: Memory  Maximum Storage: NkB
    NFA States: 5 peak, 363 total, 0 merged
    NFA Contexts: 3 peak, 101 total, 40 pruned
@@ -192,7 +371,26 @@ WINDOW w AS (
    ->  Seq Scan on nfa_test (actual rows=100.00 loops=1)
 (8 rows)
 
--- Test 2.3: Complex pattern with high state count
+DROP VIEW rpr_v;
+-- Complex pattern with high state count
+CREATE TEMP VIEW rpr_v 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_v'), 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
@@ -218,7 +416,23 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=100.00 loops=1)
 (8 rows)
 
--- Test 2.4: Grouped pattern with quantifier - state merging
+DROP VIEW rpr_v;
+-- Grouped pattern with quantifier - state merging
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+        line         
+---------------------
+   PATTERN ((a b)+) 
+(1 row)
+
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -242,8 +456,24 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=60.00 loops=1)
 (9 rows)
 
--- Test 2.5: State explosion pattern - many alternations
+DROP VIEW rpr_v;
+-- State explosion pattern - many alternations
 -- Pattern (A|B)(A|B)(A|B)(A|B) can create many parallel states
+CREATE TEMP VIEW rpr_v AS
+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_v'), 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
@@ -258,7 +488,7 @@ WINDOW w AS (
 -----------------------------------------------------------------------
  WindowAgg (actual rows=100.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: (a | b a | b a | b a | b a | b a | b a | b a | b)
+   Pattern: (a | b){8}
    Storage: Memory  Maximum Storage: NkB
    NFA States: 17 peak, 632 total, 0 merged
    NFA Contexts: 9 peak, 101 total, 1 pruned
@@ -267,7 +497,130 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=100.00 loops=1)
 (9 rows)
 
--- Test 2.6: High state merging - alternation with plus quantifier
+DROP VIEW rpr_v;
+-- Consecutive ALT merge followed by different ALT
+-- Tests mergeConsecutiveAlts flush on ALT change: (A|B){2} (C|D)
+CREATE TEMP VIEW rpr_v AS
+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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+                 line                 
+--------------------------------------
+   PATTERN ((a | b) (a | b) (c | d)) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (COSTS 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
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a | b){2} (c | d)
+   ->  Function Scan on generate_series s
+(4 rows)
+
+DROP VIEW rpr_v;
+-- Consecutive ALT merge followed by non-ALT element
+-- Tests mergeConsecutiveAlts flush on non-ALT: (A|B){2} c
+CREATE TEMP VIEW rpr_v AS
+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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+              line              
+--------------------------------
+   PATTERN ((a | b) (a | b) c) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (COSTS 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
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a | b){2} c
+   ->  Function Scan on generate_series s
+(4 rows)
+
+DROP VIEW rpr_v;
+-- ALT prefix/suffix absorbed into GROUP: (A|B) (A|B)+ (A|B) -> (A|B){3,}
+CREATE TEMP VIEW rpr_v AS
+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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+                 line                  
+---------------------------------------
+   PATTERN ((a | b) (a | b)+ (a | b)) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (COSTS 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
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a | b){3,}
+   ->  Function Scan on generate_series s
+(4 rows)
+
+DROP VIEW rpr_v;
+-- High state merging - alternation with plus quantifier
+CREATE TEMP VIEW rpr_v 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_v'), 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
@@ -291,7 +644,23 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=100.00 loops=1)
 (9 rows)
 
--- Test 2.7: Nested quantifiers causing state growth
+DROP VIEW rpr_v;
+-- Nested quantifiers causing state growth
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+           line           
+--------------------------
+   PATTERN (((a | b)+)+) 
+(1 row)
+
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -306,19 +675,35 @@ WINDOW w AS (
 ------------------------------------------------------------------------
  WindowAgg (actual rows=1000.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: ((a | b)+)+
+   Pattern: (a | b)+
    Storage: Memory  Maximum Storage: NkB
-   NFA States: 16 peak, 7334 total, 0 merged
+   NFA States: 8 peak, 4002 total, 0 merged
    NFA Contexts: 4 peak, 1001 total, 333 pruned
    NFA: 334 matched (len 1/2/2.0), 0 mismatched
    NFA: 0 absorbed, 333 skipped (len 2/2/2.0)
    ->  Function Scan on generate_series s (actual rows=1000.00 loops=1)
 (9 rows)
 
---
--- Section 3: Context Statistics Tests (peak, total, absorbed, skipped)
---
--- Test 3.1: Context absorption with unbounded quantifier at start
+DROP VIEW rpr_v;
+-- ============================================================
+-- Context Statistics Tests (peak, total, absorbed, skipped)
+-- ============================================================
+-- Context absorption with unbounded quantifier at start
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+       line        
+-------------------
+   PATTERN (a+ b) 
+(1 row)
+
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -342,7 +727,23 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
 (9 rows)
 
--- Test 3.2: No absorption - bounded quantifier
+DROP VIEW rpr_v;
+-- No absorption - bounded quantifier
+CREATE TEMP VIEW rpr_v 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_v'), 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
@@ -366,7 +767,23 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
 (9 rows)
 
--- Test 3.3: Contexts skipped by SKIP PAST LAST ROW
+DROP VIEW rpr_v;
+-- Contexts skipped by SKIP PAST LAST ROW
+CREATE TEMP VIEW rpr_v 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_v'), 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
@@ -389,7 +806,23 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=100.00 loops=1)
 (8 rows)
 
--- Test 3.4: High context absorption - unbounded group
+DROP VIEW rpr_v;
+-- High context absorption - unbounded group
+CREATE TEMP VIEW rpr_v 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_v'), 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
@@ -412,12 +845,12 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=100.00 loops=1)
 (8 rows)
 
---
--- Section 4: Match Length Statistics Tests
---
--- Test 4.1: Fixed length matches - all same length
-SELECT rpr_explain_filter('
-EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+DROP VIEW rpr_v;
+-- ============================================================
+-- Match Length Statistics Tests
+-- ============================================================
+-- Fixed length matches - all same length
+CREATE TEMP VIEW rpr_v AS
 SELECT count(*) OVER w
 FROM nfa_test
 WINDOW w AS (
@@ -425,12 +858,30 @@ WINDOW w AS (
     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)
+        A AS cat = 'A', B AS cat = 'B', C AS cat = 'C',
+        D AS cat = 'D', E AS cat = 'E'
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+          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 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
    Storage: Memory  Maximum Storage: NkB
@@ -440,7 +891,23 @@ WINDOW w AS (
    ->  Seq Scan on nfa_test (actual rows=100.00 loops=1)
 (8 rows)
 
--- Test 4.2: Variable length matches - min/max/avg differ
+DROP VIEW rpr_v;
+-- Variable length matches - min/max/avg differ
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+       line        
+-------------------
+   PATTERN (a+ b) 
+(1 row)
+
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -464,7 +931,23 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=100.00 loops=1)
 (9 rows)
 
--- Test 4.3: Very long matches
+DROP VIEW rpr_v;
+-- Very long matches
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+       line        
+-------------------
+   PATTERN (a+ b) 
+(1 row)
+
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -488,7 +971,25 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=200.00 loops=1)
 (9 rows)
 
--- Test 4.4: Mix of short and long matches
+DROP VIEW rpr_v;
+-- Mix of short and long matches
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+       line        
+-------------------
+   PATTERN (a+ b) 
+(1 row)
+
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -514,11 +1015,34 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=100.00 loops=1)
 (9 rows)
 
---
--- Section 5: Mismatch Length Statistics Tests
---
--- Test 5.1: Pattern that causes mismatches with length > 1
+DROP VIEW rpr_v;
+-- ============================================================
+-- Mismatch Length Statistics Tests
+-- ============================================================
+-- Pattern that causes mismatches with length > 1
 -- Mismatch happens when partial match fails after processing multiple rows
+CREATE TEMP VIEW rpr_v 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_v'), 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
@@ -549,7 +1073,35 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=100.00 loops=1)
 (9 rows)
 
--- Test 5.2: Long partial matches that fail
+DROP VIEW rpr_v;
+-- Long partial matches that fail
+CREATE TEMP VIEW rpr_v 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_v'), 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
@@ -585,10 +1137,26 @@ WINDOW w AS (
    ->  Function Scan on generate_series i (actual rows=60.00 loops=1)
 (9 rows)
 
---
--- Section 6: JSON Format Tests
---
--- Test 6.1: JSON format output with all statistics
+DROP VIEW rpr_v;
+-- ============================================================
+-- JSON Format Tests
+-- ============================================================
+-- JSON format output with all statistics
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+        line        
+--------------------
+   PATTERN (a+ b+) 
+(1 row)
+
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF, FORMAT JSON)
 SELECT count(*) OVER w
@@ -647,7 +1215,23 @@ WINDOW w AS (
  ]
 (1 row)
 
--- Test 6.2: JSON format with match length statistics
+DROP VIEW rpr_v;
+-- JSON format with match length statistics
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+       line        
+-------------------
+   PATTERN (a+ b) 
+(1 row)
+
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF, FORMAT JSON)
 SELECT count(*) OVER w
@@ -709,10 +1293,26 @@ WINDOW w AS (
  ]
 (1 row)
 
---
--- Section 7: XML Format Tests
---
--- Test 7.1: XML format output
+DROP VIEW rpr_v;
+-- ============================================================
+-- XML Format Tests
+-- ============================================================
+-- XML format output
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B)
+    DEFINE A AS v % 2 = 1, B AS v % 2 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+       line       
+------------------
+   PATTERN (a b) 
+(1 row)
+
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF, FORMAT XML)
 SELECT count(*) OVER w
@@ -771,10 +1371,191 @@ WINDOW w AS (
  </explain>
 (1 row)
 
---
--- Section 8: Multiple Partitions Tests
---
--- Test 8.1: Statistics across multiple partitions
+DROP VIEW rpr_v;
+-- JSON format with mismatch statistics
+-- Pattern A B C expects 1,2,3 but gets 1,2,4 twice causing mismatches
+CREATE TEMP VIEW rpr_v AS
+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_v'), 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",                                                 +
+       "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": 0,                                          +
+       "NFA Contexts Pruned": 6,                                           +
+       "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,                                     +
+       "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)
+
+DROP VIEW rpr_v;
+-- JSON format with skipped context statistics
+-- Alternation pattern with SKIP PAST LAST ROW causes many contexts to be skipped
+CREATE TEMP VIEW rpr_v AS
+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_v'), 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}",                                            +
+       "Storage": "Memory",                                                +
+       "Maximum Storage": 0,                                               +
+       "NFA States Peak": 17,                                              +
+       "NFA States Total": 632,                                            +
+       "NFA States Merged": 0,                                             +
+       "NFA Contexts Peak": 9,                                             +
+       "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)
+
+DROP VIEW rpr_v;
+-- ============================================================
+-- Multiple Partitions Tests
+-- ============================================================
+-- Statistics across multiple partitions
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+       line        
+-------------------
+   PATTERN (a+ b) 
+(1 row)
+
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -808,7 +1589,29 @@ WINDOW w AS (
                ->  Function Scan on generate_series v (actual rows=30.00 loops=3)
 (14 rows)
 
--- Test 8.2: Different pattern behavior per partition
+DROP VIEW rpr_v;
+-- Different pattern behavior per partition
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+       line        
+-------------------
+   PATTERN (a+ b) 
+(1 row)
+
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -841,12 +1644,12 @@ WINDOW w AS (
          ->  Function Scan on generate_series v (actual rows=50.00 loops=1)
 (12 rows)
 
---
--- Section 9: Edge Cases
---
--- Test 9.1: Empty result set
-SELECT rpr_explain_filter('
-EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+DROP VIEW rpr_v;
+-- ============================================================
+-- Edge Cases
+-- ============================================================
+-- Empty result set
+CREATE TEMP VIEW rpr_v AS
 SELECT count(*) OVER w
 FROM generate_series(1, 0) AS s(v)
 WINDOW w AS (
@@ -854,7 +1657,23 @@ WINDOW w AS (
     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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+       line       
+------------------
+   PATTERN (a b) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+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)
@@ -863,7 +1682,23 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=0.00 loops=1)
 (4 rows)
 
--- Test 9.2: Single row
+DROP VIEW rpr_v;
+-- Single row
+CREATE TEMP VIEW rpr_v 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_v'), 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
@@ -886,7 +1721,25 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=1.00 loops=1)
 (8 rows)
 
--- Test 9.3: Pattern longer than data
+DROP VIEW rpr_v;
+-- Pattern longer than data
+CREATE TEMP VIEW rpr_v 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_v'), 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
@@ -911,7 +1764,23 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=5.00 loops=1)
 (8 rows)
 
--- Test 9.4: All rows match as single match
+DROP VIEW rpr_v;
+-- All rows match as single match
+CREATE TEMP VIEW rpr_v 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_v'), 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
@@ -935,10 +1804,26 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
 (9 rows)
 
---
--- Section 10: Complex Pattern Tests
---
--- Test 10.1: Nested groups
+DROP VIEW rpr_v;
+-- ============================================================
+-- Complex Pattern Tests
+-- ============================================================
+-- Nested groups
+CREATE TEMP VIEW rpr_v 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_v'), 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
@@ -962,7 +1847,25 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=60.00 loops=1)
 (9 rows)
 
--- Test 10.2: Multiple alternations
+DROP VIEW rpr_v;
+-- Multiple alternations
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM 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_v'), 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
@@ -979,7 +1882,7 @@ WINDOW w AS (
 -------------------------------------------------------------------
  WindowAgg (actual rows=100.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: (a | b c | d | e)
+   Pattern: (a | b) (c | d | e)
    Storage: Memory  Maximum Storage: NkB
    NFA States: 5 peak, 282 total, 0 merged
    NFA Contexts: 3 peak, 101 total, 60 pruned
@@ -987,7 +1890,23 @@ WINDOW w AS (
    ->  Seq Scan on nfa_test (actual rows=100.00 loops=1)
 (8 rows)
 
--- Test 10.3: Optional elements
+DROP VIEW rpr_v;
+-- Optional elements
+CREATE TEMP VIEW rpr_v 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_v'), 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
@@ -1010,7 +1929,23 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
 (8 rows)
 
--- Test 10.4: Bounded quantifiers
+DROP VIEW rpr_v;
+-- Bounded quantifiers
+CREATE TEMP VIEW rpr_v 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_v'), 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
@@ -1034,7 +1969,23 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=100.00 loops=1)
 (9 rows)
 
--- Test 10.5: Star quantifier
+DROP VIEW rpr_v;
+-- Star quantifier
+CREATE TEMP VIEW rpr_v 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_v'), 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
@@ -1057,10 +2008,26 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
 (8 rows)
 
---
--- Section 11: Real-world Pattern Examples
---
--- Test 11.1: Stock price pattern - V-shape (down then up)
+DROP VIEW rpr_v;
+-- ============================================================
+-- Real-world Pattern Examples
+-- ============================================================
+-- Stock price pattern - V-shape (down then up)
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM nfa_complex
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (D+ U+)
+    DEFINE D AS trend = 'D', U AS trend = 'U'
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+        line        
+--------------------
+   PATTERN (d+ u+) 
+(1 row)
+
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1084,7 +2051,23 @@ WINDOW w AS (
    ->  Seq Scan on nfa_complex (actual rows=30.00 loops=1)
 (9 rows)
 
--- Test 11.2: Stock price pattern - peak (up, stable, down)
+DROP VIEW rpr_v;
+-- Stock price pattern - peak (up, stable, down)
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM nfa_complex
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (U+ S* D+)
+    DEFINE U AS trend = 'U', S AS trend = 'S', D AS trend = 'D'
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+         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
@@ -1108,7 +2091,23 @@ WINDOW w AS (
    ->  Seq Scan on nfa_complex (actual rows=30.00 loops=1)
 (9 rows)
 
--- Test 11.3: Consecutive increasing values (using PREV)
+DROP VIEW rpr_v;
+-- Consecutive increasing values (using PREV)
+CREATE TEMP VIEW rpr_v 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_v'), 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
@@ -1132,10 +2131,26 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
 (9 rows)
 
---
--- Section 12: Performance-oriented Tests
---
--- Test 12.1: Large dataset with simple pattern
+DROP VIEW rpr_v;
+-- ============================================================
+-- Performance-oriented Tests
+-- ============================================================
+-- Large dataset with simple pattern
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+       line       
+------------------
+   PATTERN (a b) 
+(1 row)
+
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1158,7 +2173,23 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=1000.00 loops=1)
 (8 rows)
 
--- Test 12.2: Large dataset with absorption
+DROP VIEW rpr_v;
+-- Large dataset with absorption
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+       line        
+-------------------
+   PATTERN (a+ b) 
+(1 row)
+
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1182,7 +2213,23 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=1000.00 loops=1)
 (9 rows)
 
--- Test 12.3: High state merge ratio
+DROP VIEW rpr_v;
+-- High state merge ratio
+CREATE TEMP VIEW rpr_v 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_v'), 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
@@ -1206,10 +2253,27 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=500.00 loops=1)
 (9 rows)
 
---
--- Section 13: INITIAL vs no INITIAL comparison
---
--- Test 13.1: With INITIAL keyword
+DROP VIEW rpr_v;
+-- ============================================================
+-- INITIAL vs no INITIAL comparison
+-- ============================================================
+-- With INITIAL keyword
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+       line        
+-------------------
+   PATTERN (a+ b) 
+(1 row)
+
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1234,7 +2298,23 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
 (9 rows)
 
--- Test 13.2: Without INITIAL keyword (same behavior currently)
+DROP VIEW rpr_v;
+-- Without INITIAL keyword (same behavior currently)
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+       line        
+-------------------
+   PATTERN (a+ b) 
+(1 row)
+
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1258,10 +2338,26 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
 (9 rows)
 
---
--- Section 14: Quantifier Variations
---
--- Test 14.1: Plus quantifier
+DROP VIEW rpr_v;
+-- ============================================================
+-- Quantifier Variations
+-- ============================================================
+-- Plus quantifier
+CREATE TEMP VIEW rpr_v 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_v'), 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
@@ -1285,7 +2381,23 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=40.00 loops=1)
 (9 rows)
 
--- Test 14.2: Star quantifier (zero or more)
+DROP VIEW rpr_v;
+-- Star quantifier (zero or more)
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+       line        
+-------------------
+   PATTERN (a* b) 
+(1 row)
+
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1309,7 +2421,23 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=40.00 loops=1)
 (9 rows)
 
--- Test 14.3: Question mark (zero or one)
+DROP VIEW rpr_v;
+-- Question mark (zero or one)
+CREATE TEMP VIEW rpr_v 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_v'), 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
@@ -1333,7 +2461,23 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=40.00 loops=1)
 (9 rows)
 
--- Test 14.4: Exact count {n}
+DROP VIEW rpr_v;
+-- Exact count {n}
+CREATE TEMP VIEW rpr_v 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_v'), 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
@@ -1356,7 +2500,23 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
 (8 rows)
 
--- Test 14.5: Range {n,m}
+DROP VIEW rpr_v;
+-- Range {n,m}
+CREATE TEMP VIEW rpr_v 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_v'), 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
@@ -1380,7 +2540,23 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
 (9 rows)
 
--- Test 14.6: At least {n,}
+DROP VIEW rpr_v;
+-- At least {n,}
+CREATE TEMP VIEW rpr_v 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_v'), 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
@@ -1404,11 +2580,27 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
 (9 rows)
 
---
--- Section 15: Regression Tests for Statistics Accuracy
---
--- Test 15.1: Verify state count accuracy
+DROP VIEW rpr_v;
+-- ============================================================
+-- Regression Tests for Statistics Accuracy
+-- ============================================================
+-- Verify state count accuracy
 -- Pattern A+ B with 20 rows should show predictable state behavior
+CREATE TEMP VIEW rpr_v AS
+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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+       line        
+-------------------
+   PATTERN (a+ b) 
+(1 row)
+
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1432,7 +2624,23 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=20.00 loops=1)
 (9 rows)
 
--- Test 15.2: Verify context count with known absorption
+DROP VIEW rpr_v;
+-- Verify context count with known absorption
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A+ B C)
+    DEFINE A AS v % 10 IN (1,2,3,4,5,6,7), B AS v % 10 = 8, C AS v % 10 = 9
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+        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
@@ -1456,7 +2664,23 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=30.00 loops=1)
 (9 rows)
 
--- Test 15.3: Verify match length with fixed-length pattern
+DROP VIEW rpr_v;
+-- Verify match length with fixed-length pattern
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B C)
+    DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+        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
@@ -1479,10 +2703,26 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=30.00 loops=1)
 (8 rows)
 
---
--- Section 16: Alternation Pattern Tests
---
--- Test 16.1: Simple alternation
+DROP VIEW rpr_v;
+-- ============================================================
+-- Alternation Pattern Tests
+-- ============================================================
+-- Simple alternation
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM nfa_test
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A | B) C)
+    DEFINE A AS cat = 'A', B AS cat = 'B', C AS cat = 'C'
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+          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
@@ -1505,7 +2745,25 @@ WINDOW w AS (
    ->  Seq Scan on nfa_test (actual rows=100.00 loops=1)
 (8 rows)
 
--- Test 16.2: Multiple items in alternation
+DROP VIEW rpr_v;
+-- Multiple items in alternation
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM 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_v'), 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
@@ -1530,7 +2788,23 @@ WINDOW w AS (
    ->  Seq Scan on nfa_test (actual rows=100.00 loops=1)
 (8 rows)
 
--- Test 16.3: Alternation with quantifiers
+DROP VIEW rpr_v;
+-- Alternation with quantifiers
+CREATE TEMP VIEW rpr_v 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_v'), 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
@@ -1554,10 +2828,259 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
 (9 rows)
 
---
--- Section 17: Group Pattern Tests
---
--- Test 17.1: Simple group
+DROP VIEW rpr_v;
+-- Multiple alternatives (4+)
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+              line              
+--------------------------------
+   PATTERN (a | b | c | d | e) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (COSTS 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
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a | b | c | d | e)
+   ->  Function Scan on generate_series s
+(4 rows)
+
+DROP VIEW rpr_v;
+-- Alternation at start
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+           line           
+--------------------------
+   PATTERN ((a | b) c d) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (COSTS 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
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a | b) c d
+   ->  Function Scan on generate_series s
+(4 rows)
+
+DROP VIEW rpr_v;
+-- Multiple sequential alternations
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+               line               
+----------------------------------
+   PATTERN ((a | b) c (d | e) f) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (COSTS 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
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a | b) c (d | e) f
+   ->  Function Scan on generate_series s
+(4 rows)
+
+DROP VIEW rpr_v;
+-- Quantified alternatives
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+           line           
+--------------------------
+   PATTERN ((a+ | b+) c) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (COSTS 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
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a+" | b+") c
+   ->  Function Scan on generate_series s
+(4 rows)
+
+DROP VIEW rpr_v;
+-- Alternation at end
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+           line           
+--------------------------
+   PATTERN (a b (c | d)) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (COSTS 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
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a b (c | d)
+   ->  Function Scan on generate_series s
+(4 rows)
+
+DROP VIEW rpr_v;
+-- Nested ALT at start of branch inside outer ALT
+-- Pattern: (A ((B | C) D | E)) - preceding VAR + inner ALT as first branch element
+CREATE TEMP VIEW rpr_v AS
+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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+              line              
+--------------------------------
+   PATTERN (a ((b | c) d | e)) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (COSTS 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
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a ((b | c) d | e)
+   ->  Function Scan on generate_series s
+(4 rows)
+
+DROP VIEW rpr_v;
+-- Nested ALT at end of branch inside outer ALT
+-- Pattern: (C (A | B) | D) - inner ALT is last element in outer branch
+CREATE TEMP VIEW rpr_v AS
+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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+            line            
+----------------------------
+   PATTERN (c (a | b) | d) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (COSTS 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
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (c (a | b) | d)
+   ->  Function Scan on generate_series s
+(4 rows)
+
+DROP VIEW rpr_v;
+-- ============================================================
+-- Group Pattern Tests
+-- ============================================================
+-- Simple group
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+        line         
+---------------------
+   PATTERN ((a b)+) 
+(1 row)
+
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1581,7 +3104,23 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=40.00 loops=1)
 (9 rows)
 
--- Test 17.2: Group with bounded quantifier
+DROP VIEW rpr_v;
+-- Group with bounded quantifier
+CREATE TEMP VIEW rpr_v 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_v'), 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
@@ -1605,7 +3144,23 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=40.00 loops=1)
 (9 rows)
 
--- Test 17.3: Nested groups
+DROP VIEW rpr_v;
+-- Nested groups
+CREATE TEMP VIEW rpr_v 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_v'), 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
@@ -1629,10 +3184,158 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=60.00 loops=1)
 (9 rows)
 
---
--- Section 18: Window Function Combinations
---
--- Test 18.1: count(*) with pattern
+DROP VIEW rpr_v;
+-- Deep nesting (3+ levels)
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+            line             
+-----------------------------
+   PATTERN ((((a | b)+)+)+) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (COSTS 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
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a | b)+
+   ->  Function Scan on generate_series s
+(4 rows)
+
+DROP VIEW rpr_v;
+-- Bounded quantifier on alternation
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+            line             
+-----------------------------
+   PATTERN ((a | b){2,3} c) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (COSTS 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
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a | b){2,3} c
+   ->  Function Scan on generate_series s
+(4 rows)
+
+DROP VIEW rpr_v;
+-- Nested groups with quantifiers
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+           line           
+--------------------------
+   PATTERN (((a b)+ c)*) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (COSTS 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
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: ((a' b')+" c)*
+   ->  Function Scan on generate_series s
+(4 rows)
+
+DROP VIEW rpr_v;
+-- Partial nested quantification
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+           line           
+--------------------------
+   PATTERN ((a (b c)+)*) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (COSTS 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
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a (b c)+)*
+   ->  Function Scan on generate_series s
+(4 rows)
+
+DROP VIEW rpr_v;
+-- ============================================================
+-- Window Function Combinations
+-- ============================================================
+-- count(*) with pattern
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A+ B)
+    DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+       line        
+-------------------
+   PATTERN (a+ b) 
+(1 row)
+
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1656,7 +3359,23 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=30.00 loops=1)
 (9 rows)
 
--- Test 18.2: first_value with pattern
+DROP VIEW rpr_v;
+-- first_value with pattern
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+       line        
+-------------------
+   PATTERN (a+ b) 
+(1 row)
+
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT first_value(v) OVER w
@@ -1680,7 +3399,23 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=30.00 loops=1)
 (9 rows)
 
--- Test 18.3: last_value with pattern
+DROP VIEW rpr_v;
+-- last_value with pattern
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+       line        
+-------------------
+   PATTERN (a+ b) 
+(1 row)
+
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT last_value(v) OVER w
@@ -1704,7 +3439,26 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=30.00 loops=1)
 (9 rows)
 
--- Test 18.4: Multiple window functions
+DROP VIEW rpr_v;
+-- Multiple window functions
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+       line        
+-------------------
+   PATTERN (a+ b) 
+(1 row)
+
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT
@@ -1731,10 +3485,28 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=30.00 loops=1)
 (9 rows)
 
---
--- Section 19: DEFINE Expression Variations
---
--- Test 19.1: Complex boolean expressions
+DROP VIEW rpr_v;
+-- ============================================================
+-- DEFINE Expression Variations
+-- ============================================================
+-- Complex boolean expressions
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+       line        
+-------------------
+   PATTERN (a+ b) 
+(1 row)
+
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1760,7 +3532,26 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
 (9 rows)
 
--- Test 19.2: Using PREV function
+DROP VIEW rpr_v;
+-- Using PREV function
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (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_v'), 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
@@ -1786,7 +3577,26 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=30.00 loops=1)
 (8 rows)
 
--- Test 19.3: Using NULL comparisons
+DROP VIEW rpr_v;
+-- Using NULL comparisons
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+       line        
+-------------------
+   PATTERN (a+ b) 
+(1 row)
+
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1813,10 +3623,26 @@ WINDOW w AS (
    ->  Function Scan on generate_series v (actual rows=30.00 loops=1)
 (9 rows)
 
---
--- Section 20: Large Scale Statistics Verification
---
--- Test 20.1: 500 rows - verify statistics scale correctly
+DROP VIEW rpr_v;
+-- ============================================================
+-- Large Scale Statistics Verification
+-- ============================================================
+-- 500 rows - verify statistics scale correctly
+CREATE TEMP VIEW rpr_v 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_v'), 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
@@ -1840,7 +3666,23 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=500.00 loops=1)
 (9 rows)
 
--- Test 20.2: High match count scenario
+DROP VIEW rpr_v;
+-- High match count scenario
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+       line       
+------------------
+   PATTERN (a b) 
+(1 row)
+
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1863,7 +3705,28 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=500.00 loops=1)
 (8 rows)
 
--- Test 20.3: High skip count scenario
+DROP VIEW rpr_v;
+-- High skip count scenario
+CREATE TEMP VIEW rpr_v 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_v'), 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
@@ -1891,6 +3754,7 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=500.00 loops=1)
 (8 rows)
 
+DROP VIEW rpr_v;
 -- Cleanup
 DROP TABLE nfa_test;
 DROP TABLE nfa_complex;
diff --git a/src/test/regress/sql/rpr.sql b/src/test/regress/sql/rpr.sql
index 788a77e5279..1071dacd687 100644
--- a/src/test/regress/sql/rpr.sql
+++ b/src/test/regress/sql/rpr.sql
@@ -1233,9 +1233,9 @@ SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER
   DOWN AS price < PREV(1)
 );
 
--- Maximum pattern variables is 252 (RPR_VARID_MAX)
+-- Maximum pattern variables is 251 (RPR_VARID_MAX)
 
--- Ok: 252 variables (maximum allowed)
+-- Error: 252 variables exceeds limit of 251
 DO $$
 DECLARE
     pattern_vars text;
@@ -1256,7 +1256,7 @@ BEGIN
 END;
 $$;
 
--- Error: 253 variables exceeds limit of 252
+-- Error: 253 variables exceeds limit of 251
 DO $$
 DECLARE
     pattern_vars text;
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index a5a66d2ca81..6f38cd1ae92 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -2019,6 +2019,14 @@ 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
@@ -2031,6 +2039,14 @@ 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
@@ -3350,13 +3366,13 @@ WINDOW w AS (
     V251 AS val > 0, V252 AS val > 0, V253 AS val > 0
 );
 
--- Test: 252 variables in PATTERN, 253 in DEFINE (boundary - should succeed)
+-- Test: 251 variables in PATTERN, 252 in DEFINE (boundary - should succeed)
 -- Expected: Success - unused DEFINE variables are filtered out
 SELECT COUNT(*) OVER w FROM rpr_errors
 WINDOW w AS (
     ORDER BY id
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    PATTERN (V1 V2 V3 V4 V5 V6 V7 V8 V9 V10 V11 V12 V13 V14 V15 V16 V17 V18 V19 V20 V21 V22 V23 V24 V25 V26 V27 V28 V29 V30 V31 V32 V33 V34 V35 V36 V37 V38 V39 V40 V41 V42 V43 V44 V45 V46 V47 V48 V49 V50 V51 V52 V53 V54 V55 V56 V57 V58 V59 V60 V61 V62 V63 V64 V65 V66 V67 V68 V69 V70 V71 V72 V73 V74 V75 V76 V77 V78 V79 V80 V81 V82 V83 V84 V85 V86 V87 V88 V89 V90 V91 V92 V93 V94 V95 V96 V97 V98 V99 V100 V101 V102 V103 V104 V105 V106 V107 V108 V109 V110 V111 V112 V113 V114 V115 V116 V117 V118 V119 V120 V121 V122 V123 V124 V125 V126 V127 V128 V129 V130 V131 V132 V133 V134 V135 V136 V137 V138 V139 V140 V141 V142 V143 V144 V145 V146 V147 V148 V149 V150 V151 V152 V153 V154 V155 V156 V157 V158 V159 V160 V161 V162 V163 V164 V165 V166 V167 V168 V169 V170 V171 V172 V173 V174 V175 V176 V177 V178 V179 V180 V181 V182 V183 V184 V185 V186 V187 V188 V189 V190 V191 V192 V193 V194 V195 V196 V197 V198 V199 V200 V201 V202 V203 V204 V205 V206 V207 V208 V209 V210 V211 V212 V213 V214 V215 V216 V217 V218 V219 V220 V221 V222 V223 V224 V225 V226 V227 V228 V229 V230 V231 V232 V233 V234 V235 V236 V237 V238 V239 V240 V241 V242 V243 V244 V245 V246 V247 V248 V249 V250 V251 V252)
+    PATTERN (V1 V2 V3 V4 V5 V6 V7 V8 V9 V10 V11 V12 V13 V14 V15 V16 V17 V18 V19 V20 V21 V22 V23 V24 V25 V26 V27 V28 V29 V30 V31 V32 V33 V34 V35 V36 V37 V38 V39 V40 V41 V42 V43 V44 V45 V46 V47 V48 V49 V50 V51 V52 V53 V54 V55 V56 V57 V58 V59 V60 V61 V62 V63 V64 V65 V66 V67 V68 V69 V70 V71 V72 V73 V74 V75 V76 V77 V78 V79 V80 V81 V82 V83 V84 V85 V86 V87 V88 V89 V90 V91 V92 V93 V94 V95 V96 V97 V98 V99 V100 V101 V102 V103 V104 V105 V106 V107 V108 V109 V110 V111 V112 V113 V114 V115 V116 V117 V118 V119 V120 V121 V122 V123 V124 V125 V126 V127 V128 V129 V130 V131 V132 V133 V134 V135 V136 V137 V138 V139 V140 V141 V142 V143 V144 V145 V146 V147 V148 V149 V150 V151 V152 V153 V154 V155 V156 V157 V158 V159 V160 V161 V162 V163 V164 V165 V166 V167 V168 V169 V170 V171 V172 V173 V174 V175 V176 V177 V178 V179 V180 V181 V182 V183 V184 V185 V186 V187 V188 V189 V190 V191 V192 V193 V194 V195 V196 V197 V198 V199 V200 V201 V202 V203 V204 V205 V206 V207 V208 V209 V210 V211 V212 V213 V214 V215 V216 V217 V218 V219 V220 V221 V222 V223 V224 V225 V226 V227 V228 V229 V230 V231 V232 V233 V234 V235 V236 V237 V238 V239 V240 V241 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,
@@ -3383,17 +3399,17 @@ WINDOW w AS (
     V221 AS val > 0, V222 AS val > 0, V223 AS val > 0, V224 AS val > 0, V225 AS val > 0, V226 AS val > 0, V227 AS val > 0, V228 AS val > 0, V229 AS val > 0, V230 AS val > 0,
     V231 AS val > 0, V232 AS val > 0, V233 AS val > 0, V234 AS val > 0, V235 AS val > 0, V236 AS val > 0, V237 AS val > 0, V238 AS val > 0, V239 AS val > 0, V240 AS val > 0,
     V241 AS val > 0, V242 AS val > 0, V243 AS val > 0, V244 AS val > 0, V245 AS val > 0, V246 AS val > 0, V247 AS val > 0, V248 AS val > 0, V249 AS val > 0, V250 AS val > 0,
-    V251 AS val > 0, V252 AS val > 0, V253 AS val > 0
+    V251 AS val > 0, V252 AS val > 0
 );
 
 
--- Test: 253 variables in PATTERN, 252 in DEFINE (exceeds limit with implicit TRUE)
--- Expected: ERROR - too many pattern variables (Maximum is 252)
+-- Test: 252 variables in PATTERN, 251 in DEFINE (exceeds limit with implicit TRUE)
+-- Expected: ERROR - too many pattern variables (Maximum is 251)
 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 V253)
+    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,
@@ -3420,28 +3436,28 @@ WINDOW w AS (
     V221 AS val > 0, V222 AS val > 0, V223 AS val > 0, V224 AS val > 0, V225 AS val > 0, V226 AS val > 0, V227 AS val > 0, V228 AS val > 0, V229 AS val > 0, V230 AS val > 0,
     V231 AS val > 0, V232 AS val > 0, V233 AS val > 0, V234 AS val > 0, V235 AS val > 0, V236 AS val > 0, V237 AS val > 0, V238 AS val > 0, V239 AS val > 0, V240 AS val > 0,
     V241 AS val > 0, V242 AS val > 0, V243 AS val > 0, V244 AS val > 0, V245 AS val > 0, V246 AS val > 0, V247 AS val > 0, V248 AS val > 0, V249 AS val > 0, V250 AS val > 0,
-    V251 AS val > 0, V252 AS val > 0
+    V251 AS val > 0
 );
 
--- Test: Pattern nesting at maximum depth (depth 254)
+-- Test: Pattern nesting at maximum depth (depth 253)
 -- Expected: Should succeed
--- Note: 254 nested GROUP{3,7} quantifiers produce depth 254 after optimization
+-- 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}){3,7})
+    PATTERN ((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((A{3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7})
     DEFINE A AS val > 0
 );
 
--- Test: Pattern nesting depth exceeds maximum (depth 255)
+-- Test: Pattern nesting depth exceeds maximum (depth 254)
 -- Expected: ERROR - pattern nesting too deep
--- Note: 255 nested GROUP{3,7} quantifiers produce depth 255 after optimization
+-- 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}){3,7})
+    PATTERN (((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((A{3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7})
     DEFINE A AS val > 0
 );
 
@@ -3611,6 +3627,24 @@ WINDOW w AS (
     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
index 8156121b3cd..80088ab18e2 100644
--- a/src/test/regress/sql/rpr_explain.sql
+++ b/src/test/regress/sql/rpr_explain.sql
@@ -1,14 +1,43 @@
+-- ============================================================
+-- RPR EXPLAIN Tests
+-- Tests for Row Pattern Recognition EXPLAIN output
+-- ============================================================
 --
--- Test: EXPLAIN ANALYZE output for Row Pattern Recognition NFA statistics
+-- This test suite validates EXPLAIN output for RPR queries,
+-- including NFA statistics shown in EXPLAIN ANALYZE:
+--   - NFA States: peak, total, merged
+--   - NFA Contexts: peak, total, absorbed, skipped
+--   - NFA: matched (len min/max/avg), mismatched (len min/max/avg)
+--   - Pattern deparse formatting
+--   - Multiple output formats (text, JSON, XML)
 --
--- This file tests the NFA statistics shown in EXPLAIN ANALYZE output:
--- - NFA States: peak, total, merged
--- - NFA Contexts: peak, total, absorbed, skipped
--- - NFA: matched (len min/max/avg), mismatched (len min/max/avg)
---
-
--- Filter function to normalize Storage memory values only (not NFA statistics)
--- Works for text, JSON, and XML formats
+-- 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
+-- ============================================================
+
+-- Filter function to normalize Storage memory values only (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
 $$
@@ -75,11 +104,21 @@ VALUES
     (121, 'U'), (123, 'U'), (125, 'U'), (127, 'U'), (129, 'U'),
     (131, 'U'), (133, 'U'), (130, 'D'), (127, 'D'), (124, 'D');
 
---
--- Section 1: Basic NFA Statistics Tests
---
+-- ============================================================
+-- Basic NFA Statistics Tests
+-- ============================================================
 
--- Test 1.1: Simple pattern - should show basic statistics
+-- Simple pattern - should show basic statistics
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM nfa_test
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B)
+    DEFINE A AS cat = 'A', B AS cat = 'B'
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -90,8 +129,19 @@ WINDOW w AS (
     PATTERN (A B)
     DEFINE A AS cat = ''A'', B AS cat = ''B''
 )');
+DROP VIEW rpr_v;
 
--- Test 1.2: Pattern with no matches - 0 matched
+-- Pattern with no matches - 0 matched
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM nfa_test
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (X Y Z)
+    DEFINE X AS cat = 'X', Y AS cat = 'Y', Z AS cat = 'Z'
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -102,8 +152,19 @@ WINDOW w AS (
     PATTERN (X Y Z)
     DEFINE X AS cat = ''X'', Y AS cat = ''Y'', Z AS cat = ''Z''
 );');
+DROP VIEW rpr_v;
 
--- Test 1.3: Pattern matching every row - high match count
+-- Pattern matching every row - high match count
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM nfa_test
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (R)
+    DEFINE R AS TRUE
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -114,12 +175,68 @@ WINDOW w AS (
     PATTERN (R)
     DEFINE R AS TRUE
 );');
+DROP VIEW rpr_v;
 
---
--- Section 2: State Statistics Tests (peak, total, merged)
---
+-- Regression test: Space before parenthesis in pattern deparse
+-- Verifies that "A (B | C)" correctly outputs as "a (b | c)" with space
+CREATE TEMP VIEW rpr_v AS
+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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (COSTS 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
+);');
+DROP VIEW rpr_v;
+
+-- Regression test: Sequential alternations at same depth
+-- Verifies that "((B | C) (D | E))" correctly outputs as "(b | c) (d | e)"
+-- Previously failed due to missing parentheses on ALT depth decrease
+CREATE TEMP VIEW rpr_v AS
+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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (COSTS 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
+);');
+DROP VIEW rpr_v;
+
+-- ============================================================
+-- State Statistics Tests (peak, total, merged)
+-- ============================================================
 
--- Test 2.1: Simple quantifier pattern - A+ with short matches (no merging)
+-- Simple quantifier pattern - A+ with short matches (no merging)
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -130,8 +247,21 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS v % 2 = 1
 );');
+DROP VIEW rpr_v;
 
--- Test 2.2: Alternation pattern - multiple state branches
+-- Alternation pattern - multiple state branches
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -144,8 +274,22 @@ WINDOW w AS (
         A AS cat = ''A'', B AS cat = ''B'', C AS cat = ''C'',
         D AS cat = ''D'', E AS cat = ''E''
 );');
+DROP VIEW rpr_v;
 
--- Test 2.3: Complex pattern with high state count
+-- Complex pattern with high state count
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -159,8 +303,19 @@ WINDOW w AS (
         B AS v % 3 = 2,
         C AS v % 3 = 0
 );');
+DROP VIEW rpr_v;
 
--- Test 2.4: Grouped pattern with quantifier - state merging
+-- Grouped pattern with quantifier - state merging
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -171,9 +326,20 @@ WINDOW w AS (
     PATTERN ((A B)+)
     DEFINE A AS v % 2 = 1, B AS v % 2 = 0
 );');
+DROP VIEW rpr_v;
 
--- Test 2.5: State explosion pattern - many alternations
+-- State explosion pattern - many alternations
 -- Pattern (A|B)(A|B)(A|B)(A|B) can create many parallel states
+CREATE TEMP VIEW rpr_v AS
+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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -184,8 +350,90 @@ WINDOW w AS (
     PATTERN ((A | B) (A | B) (A | B) (A | B) (A | B) (A | B) (A | B) (A | B))
     DEFINE A AS v % 2 = 1, B AS v % 2 = 0
 );');
+DROP VIEW rpr_v;
+
+-- Consecutive ALT merge followed by different ALT
+-- Tests mergeConsecutiveAlts flush on ALT change: (A|B){2} (C|D)
+CREATE TEMP VIEW rpr_v AS
+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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (COSTS 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
+);');
+DROP VIEW rpr_v;
+
+-- Consecutive ALT merge followed by non-ALT element
+-- Tests mergeConsecutiveAlts flush on non-ALT: (A|B){2} c
+CREATE TEMP VIEW rpr_v AS
+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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (COSTS 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
+);');
+DROP VIEW rpr_v;
+
+-- ALT prefix/suffix absorbed into GROUP: (A|B) (A|B)+ (A|B) -> (A|B){3,}
+CREATE TEMP VIEW rpr_v AS
+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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (COSTS 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
+);');
+DROP VIEW rpr_v;
 
--- Test 2.6: High state merging - alternation with plus quantifier
+-- High state merging - alternation with plus quantifier
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -196,8 +444,19 @@ WINDOW w AS (
     PATTERN ((A | B | C)+ D)
     DEFINE A AS v % 4 = 1, B AS v % 4 = 2, C AS v % 4 = 3, D AS v % 4 = 0
 );');
+DROP VIEW rpr_v;
 
--- Test 2.7: Nested quantifiers causing state growth
+-- Nested quantifiers causing state growth
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -208,12 +467,23 @@ WINDOW w AS (
     PATTERN (((A | B)+)+)
     DEFINE A AS v % 3 = 1, B AS v % 3 = 2
 );');
+DROP VIEW rpr_v;
 
---
--- Section 3: Context Statistics Tests (peak, total, absorbed, skipped)
---
+-- ============================================================
+-- Context Statistics Tests (peak, total, absorbed, skipped)
+-- ============================================================
 
--- Test 3.1: Context absorption with unbounded quantifier at start
+-- Context absorption with unbounded quantifier at start
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -224,8 +494,19 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );');
+DROP VIEW rpr_v;
 
--- Test 3.2: No absorption - bounded quantifier
+-- No absorption - bounded quantifier
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -236,8 +517,19 @@ WINDOW w AS (
     PATTERN (A{2,4} B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );');
+DROP VIEW rpr_v;
 
--- Test 3.3: Contexts skipped by SKIP PAST LAST ROW
+-- Contexts skipped by SKIP PAST LAST ROW
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -248,8 +540,19 @@ WINDOW w AS (
     PATTERN (A B C)
     DEFINE A AS v % 10 = 1, B AS v % 10 = 2, C AS v % 10 = 3
 );');
+DROP VIEW rpr_v;
 
--- Test 3.4: High context absorption - unbounded group
+-- High context absorption - unbounded group
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -260,12 +563,25 @@ WINDOW w AS (
     PATTERN ((A B)+ C)
     DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
 );');
+DROP VIEW rpr_v;
 
---
--- Section 4: Match Length Statistics Tests
---
+-- ============================================================
+-- Match Length Statistics Tests
+-- ============================================================
 
--- Test 4.1: Fixed length matches - all same length
+-- Fixed length matches - all same length
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -278,8 +594,19 @@ WINDOW w AS (
         A AS cat = ''A'', B AS cat = ''B'', C AS cat = ''C'',
         D AS cat = ''D'', E AS cat = ''E''
 );');
+DROP VIEW rpr_v;
 
--- Test 4.2: Variable length matches - min/max/avg differ
+-- Variable length matches - min/max/avg differ
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -290,8 +617,19 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 10 <> 0, B AS v % 10 = 0
 );');
+DROP VIEW rpr_v;
 
--- Test 4.3: Very long matches
+-- Very long matches
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -302,8 +640,21 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v <= 195, B AS v > 195
 );');
+DROP VIEW rpr_v;
 
--- Test 4.4: Mix of short and long matches
+-- Mix of short and long matches
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -316,13 +667,31 @@ WINDOW w AS (
         A AS (v % 20 <> 0) AND (v % 20 <= 10 OR v % 20 > 15),
         B AS v % 20 = 0
 );');
+DROP VIEW rpr_v;
 
---
--- Section 5: Mismatch Length Statistics Tests
---
+-- ============================================================
+-- Mismatch Length Statistics Tests
+-- ============================================================
 
--- Test 5.1: Pattern that causes mismatches with length > 1
+-- Pattern that causes mismatches with length > 1
 -- Mismatch happens when partial match fails after processing multiple rows
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -340,8 +709,31 @@ WINDOW w AS (
     PATTERN (A+ B+ C)
     DEFINE A AS cat = ''A'', B AS cat = ''B'', C AS cat = ''C''
 );');
+DROP VIEW rpr_v;
 
--- Test 5.2: Long partial matches that fail
+-- Long partial matches that fail
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -364,14 +756,14 @@ WINDOW w AS (
     PATTERN (A+ B+ C)
     DEFINE A AS cat = ''A'', B AS cat = ''B'', C AS cat = ''C''
 );');
+DROP VIEW rpr_v;
 
---
--- Section 6: JSON Format Tests
---
+-- ============================================================
+-- JSON Format Tests
+-- ============================================================
 
--- Test 6.1: JSON format output with all statistics
-SELECT rpr_explain_filter('
-EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF, FORMAT JSON)
+-- JSON format output with all statistics
+CREATE TEMP VIEW rpr_v AS
 SELECT count(*) OVER w
 FROM generate_series(1, 50) AS s(v)
 WINDOW w AS (
@@ -379,9 +771,31 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (A+ B+)
     DEFINE A AS v % 3 = 1, B AS v % 3 = 2
-)');
-
--- Test 6.2: JSON format with match length statistics
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF, FORMAT 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
+)');
+DROP VIEW rpr_v;
+
+-- JSON format with match length statistics
+CREATE TEMP VIEW rpr_v 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_v'), 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
@@ -392,12 +806,23 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 10 <> 0, B AS v % 10 = 0
 )');
+DROP VIEW rpr_v;
 
---
--- Section 7: XML Format Tests
---
+-- ============================================================
+-- XML Format Tests
+-- ============================================================
 
--- Test 7.1: XML format output
+-- XML format output
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B)
+    DEFINE A AS v % 2 = 1, B AS v % 2 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF, FORMAT XML)
 SELECT count(*) OVER w
@@ -408,12 +833,76 @@ WINDOW w AS (
     PATTERN (A B)
     DEFINE A AS v % 2 = 1, B AS v % 2 = 0
 )');
+DROP VIEW rpr_v;
 
---
--- Section 8: Multiple Partitions Tests
---
+-- JSON format with mismatch statistics
+-- Pattern A B C expects 1,2,3 but gets 1,2,4 twice causing mismatches
+CREATE TEMP VIEW rpr_v AS
+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_v'), 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
+)');
+DROP VIEW rpr_v;
+
+-- JSON format with skipped context statistics
+-- Alternation pattern with SKIP PAST LAST ROW causes many contexts to be skipped
+CREATE TEMP VIEW rpr_v AS
+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_v'), 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
+)');
+DROP VIEW rpr_v;
 
--- Test 8.1: Statistics across multiple partitions
+-- ============================================================
+-- Multiple Partitions Tests
+-- ============================================================
+
+-- Statistics across multiple partitions
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -429,8 +918,25 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );');
+DROP VIEW rpr_v;
 
--- Test 8.2: Different pattern behavior per partition
+-- Different pattern behavior per partition
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -447,12 +953,23 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS val < 5, B AS val >= 5
 );');
+DROP VIEW rpr_v;
 
---
--- Section 9: Edge Cases
---
+-- ============================================================
+-- Edge Cases
+-- ============================================================
 
--- Test 9.1: Empty result set
+-- Empty result set
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -463,8 +980,19 @@ WINDOW w AS (
     PATTERN (A B)
     DEFINE A AS v = 1, B AS v = 2
 );');
+DROP VIEW rpr_v;
 
--- Test 9.2: Single row
+-- Single row
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -475,8 +1003,21 @@ WINDOW w AS (
     PATTERN (A)
     DEFINE A AS TRUE
 );');
+DROP VIEW rpr_v;
 
--- Test 9.3: Pattern longer than data
+-- Pattern longer than data
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -489,8 +1030,19 @@ WINDOW w AS (
         A AS v = 1, B AS v = 2, C AS v = 3, D AS v = 4, E AS v = 5,
         F AS v = 6, G AS v = 7, H AS v = 8, I AS v = 9, J AS v = 10
 );');
+DROP VIEW rpr_v;
 
--- Test 9.4: All rows match as single match
+-- All rows match as single match
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -501,12 +1053,23 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS TRUE
 );');
+DROP VIEW rpr_v;
 
---
--- Section 10: Complex Pattern Tests
---
+-- ============================================================
+-- Complex Pattern Tests
+-- ============================================================
 
--- Test 10.1: Nested groups
+-- Nested groups
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -517,8 +1080,21 @@ WINDOW w AS (
     PATTERN (((A B) C)+)
     DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
 );');
+DROP VIEW rpr_v;
 
--- Test 10.2: Multiple alternations
+-- Multiple alternations
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -531,8 +1107,19 @@ WINDOW w AS (
         A AS cat = ''A'', B AS cat = ''B'', C AS cat = ''C'',
         D AS cat = ''D'', E AS cat = ''E''
 );');
+DROP VIEW rpr_v;
 
--- Test 10.3: Optional elements
+-- Optional elements
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -543,8 +1130,19 @@ WINDOW w AS (
     PATTERN (A B? C)
     DEFINE A AS v % 4 = 1, B AS v % 4 = 2, C AS v % 4 = 3
 );');
+DROP VIEW rpr_v;
 
--- Test 10.4: Bounded quantifiers
+-- Bounded quantifiers
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -555,8 +1153,19 @@ WINDOW w AS (
     PATTERN (A{2,5} B)
     DEFINE A AS v % 10 <> 0, B AS v % 10 = 0
 );');
+DROP VIEW rpr_v;
 
--- Test 10.5: Star quantifier
+-- Star quantifier
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -567,12 +1176,23 @@ WINDOW w AS (
     PATTERN (A B* C)
     DEFINE A AS v % 10 = 1, B AS v % 10 IN (2,3,4,5,6,7,8), C AS v % 10 = 9
 );');
+DROP VIEW rpr_v;
 
---
--- Section 11: Real-world Pattern Examples
---
+-- ============================================================
+-- Real-world Pattern Examples
+-- ============================================================
 
--- Test 11.1: Stock price pattern - V-shape (down then up)
+-- Stock price pattern - V-shape (down then up)
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM nfa_complex
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (D+ U+)
+    DEFINE D AS trend = 'D', U AS trend = 'U'
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -583,8 +1203,19 @@ WINDOW w AS (
     PATTERN (D+ U+)
     DEFINE D AS trend = ''D'', U AS trend = ''U''
 );');
+DROP VIEW rpr_v;
 
--- Test 11.2: Stock price pattern - peak (up, stable, down)
+-- Stock price pattern - peak (up, stable, down)
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM nfa_complex
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (U+ S* D+)
+    DEFINE U AS trend = 'U', S AS trend = 'S', D AS trend = 'D'
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -595,8 +1226,19 @@ WINDOW w AS (
     PATTERN (U+ S* D+)
     DEFINE U AS trend = ''U'', S AS trend = ''S'', D AS trend = ''D''
 );');
+DROP VIEW rpr_v;
 
--- Test 11.3: Consecutive increasing values (using PREV)
+-- Consecutive increasing values (using PREV)
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -607,12 +1249,23 @@ WINDOW w AS (
     PATTERN (A{3,})
     DEFINE A AS v > PREV(v) OR PREV(v) IS NULL
 );');
+DROP VIEW rpr_v;
 
---
--- Section 12: Performance-oriented Tests
---
+-- ============================================================
+-- Performance-oriented Tests
+-- ============================================================
 
--- Test 12.1: Large dataset with simple pattern
+-- Large dataset with simple pattern
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -623,8 +1276,19 @@ WINDOW w AS (
     PATTERN (A B)
     DEFINE A AS v % 2 = 1, B AS v % 2 = 0
 );');
+DROP VIEW rpr_v;
 
--- Test 12.2: Large dataset with absorption
+-- Large dataset with absorption
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -635,8 +1299,19 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 100 <> 0, B AS v % 100 = 0
 );');
+DROP VIEW rpr_v;
 
--- Test 12.3: High state merge ratio
+-- High state merge ratio
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -647,12 +1322,24 @@ WINDOW w AS (
     PATTERN ((A | B)+ C)
     DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
 );');
+DROP VIEW rpr_v;
 
---
--- Section 13: INITIAL vs no INITIAL comparison
---
+-- ============================================================
+-- INITIAL vs no INITIAL comparison
+-- ============================================================
 
--- Test 13.1: With INITIAL keyword
+-- With INITIAL keyword
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -664,8 +1351,19 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );');
+DROP VIEW rpr_v;
 
--- Test 13.2: Without INITIAL keyword (same behavior currently)
+-- Without INITIAL keyword (same behavior currently)
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -676,12 +1374,23 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );');
+DROP VIEW rpr_v;
 
---
--- Section 14: Quantifier Variations
---
+-- ============================================================
+-- Quantifier Variations
+-- ============================================================
 
--- Test 14.1: Plus quantifier
+-- Plus quantifier
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -692,10 +1401,10 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS v % 4 <> 0
 );');
+DROP VIEW rpr_v;
 
--- Test 14.2: Star quantifier (zero or more)
-SELECT rpr_explain_filter('
-EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+-- Star quantifier (zero or more)
+CREATE TEMP VIEW rpr_v AS
 SELECT count(*) OVER w
 FROM generate_series(1, 40) AS s(v)
 WINDOW w AS (
@@ -703,9 +1412,8 @@ WINDOW w AS (
     AFTER MATCH SKIP PAST LAST ROW
     PATTERN (A* B)
     DEFINE A AS v % 4 IN (1, 2), B AS v % 4 = 3
-);');
-
--- Test 14.3: Question mark (zero or one)
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -713,11 +1421,45 @@ 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
+    PATTERN (A* B)
+    DEFINE A AS v % 4 IN (1, 2), B AS v % 4 = 3
 );');
+DROP VIEW rpr_v;
 
--- Test 14.4: Exact count {n}
+-- Question mark (zero or one)
+CREATE TEMP VIEW rpr_v 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_v'), E'\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
+);');
+DROP VIEW rpr_v;
+
+-- Exact count {n}
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -728,8 +1470,19 @@ WINDOW w AS (
     PATTERN (A{3} B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );');
+DROP VIEW rpr_v;
 
--- Test 14.5: Range {n,m}
+-- Range {n,m}
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -740,8 +1493,19 @@ WINDOW w AS (
     PATTERN (A{2,4} B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );');
+DROP VIEW rpr_v;
 
--- Test 14.6: At least {n,}
+-- At least {n,}
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -752,13 +1516,24 @@ WINDOW w AS (
     PATTERN (A{3,} B)
     DEFINE A AS v % 10 <> 0, B AS v % 10 = 0
 );');
+DROP VIEW rpr_v;
 
---
--- Section 15: Regression Tests for Statistics Accuracy
---
+-- ============================================================
+-- Regression Tests for Statistics Accuracy
+-- ============================================================
 
--- Test 15.1: Verify state count accuracy
+-- Verify state count accuracy
 -- Pattern A+ B with 20 rows should show predictable state behavior
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -769,8 +1544,19 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );');
+DROP VIEW rpr_v;
 
--- Test 15.2: Verify context count with known absorption
+-- Verify context count with known absorption
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A+ B C)
+    DEFINE A AS v % 10 IN (1,2,3,4,5,6,7), B AS v % 10 = 8, C AS v % 10 = 9
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -781,8 +1567,19 @@ WINDOW w AS (
     PATTERN (A+ B C)
     DEFINE A AS v % 10 IN (1,2,3,4,5,6,7), B AS v % 10 = 8, C AS v % 10 = 9
 );');
+DROP VIEW rpr_v;
 
--- Test 15.3: Verify match length with fixed-length pattern
+-- Verify match length with fixed-length pattern
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A B C)
+    DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -793,12 +1590,23 @@ WINDOW w AS (
     PATTERN (A B C)
     DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
 );');
+DROP VIEW rpr_v;
 
---
--- Section 16: Alternation Pattern Tests
---
+-- ============================================================
+-- Alternation Pattern Tests
+-- ============================================================
 
--- Test 16.1: Simple alternation
+-- Simple alternation
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM nfa_test
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A | B) C)
+    DEFINE A AS cat = 'A', B AS cat = 'B', C AS cat = 'C'
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -809,8 +1617,21 @@ WINDOW w AS (
     PATTERN ((A | B) C)
     DEFINE A AS cat = ''A'', B AS cat = ''B'', C AS cat = ''C''
 );');
+DROP VIEW rpr_v;
 
--- Test 16.2: Multiple items in alternation
+-- Multiple items in alternation
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -823,8 +1644,19 @@ WINDOW w AS (
         A AS cat = ''A'', B AS cat = ''B'', C AS cat = ''C'',
         D AS cat = ''D'', E AS cat = ''E''
 );');
+DROP VIEW rpr_v;
 
--- Test 16.3: Alternation with quantifiers
+-- Alternation with quantifiers
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -835,12 +1667,172 @@ WINDOW w AS (
     PATTERN ((A | B)+ C)
     DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
 );');
+DROP VIEW rpr_v;
 
---
--- Section 17: Group Pattern Tests
---
+-- Multiple alternatives (4+)
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (COSTS 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
+);');
+DROP VIEW rpr_v;
+
+-- Alternation at start
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (COSTS 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
+);');
+DROP VIEW rpr_v;
+
+-- Multiple sequential alternations
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (COSTS 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
+);');
+DROP VIEW rpr_v;
+
+-- Quantified alternatives
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (COSTS 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
+);');
+DROP VIEW rpr_v;
+
+-- Alternation at end
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (COSTS 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
+);');
+DROP VIEW rpr_v;
+
+-- Nested ALT at start of branch inside outer ALT
+-- Pattern: (A ((B | C) D | E)) - preceding VAR + inner ALT as first branch element
+CREATE TEMP VIEW rpr_v AS
+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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (COSTS 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
+);');
+DROP VIEW rpr_v;
+
+-- Nested ALT at end of branch inside outer ALT
+-- Pattern: (C (A | B) | D) - inner ALT is last element in outer branch
+CREATE TEMP VIEW rpr_v AS
+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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (COSTS 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
+);');
+DROP VIEW rpr_v;
 
--- Test 17.1: Simple group
+-- ============================================================
+-- Group Pattern Tests
+-- ============================================================
+
+-- Simple group
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -851,8 +1843,19 @@ WINDOW w AS (
     PATTERN ((A B)+)
     DEFINE A AS v % 2 = 1, B AS v % 2 = 0
 );');
+DROP VIEW rpr_v;
 
--- Test 17.2: Group with bounded quantifier
+-- Group with bounded quantifier
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -863,8 +1866,19 @@ WINDOW w AS (
     PATTERN ((A B){2,4})
     DEFINE A AS v % 2 = 1, B AS v % 2 = 0
 );');
+DROP VIEW rpr_v;
 
--- Test 17.3: Nested groups
+-- Nested groups
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -875,12 +1889,107 @@ WINDOW w AS (
     PATTERN (((A B){2})+)
     DEFINE A AS v % 2 = 1, B AS v % 2 = 0
 );');
+DROP VIEW rpr_v;
 
---
--- Section 18: Window Function Combinations
---
+-- Deep nesting (3+ levels)
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (COSTS 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
+);');
+DROP VIEW rpr_v;
 
--- Test 18.1: count(*) with pattern
+-- Bounded quantifier on alternation
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (COSTS 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
+);');
+DROP VIEW rpr_v;
+
+-- Nested groups with quantifiers
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (COSTS 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
+);');
+DROP VIEW rpr_v;
+
+-- Partial nested quantification
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (COSTS 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
+);');
+DROP VIEW rpr_v;
+
+-- ============================================================
+-- Window Function Combinations
+-- ============================================================
+
+-- count(*) with pattern
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A+ B)
+    DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -891,8 +2000,19 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );');
+DROP VIEW rpr_v;
 
--- Test 18.2: first_value with pattern
+-- first_value with pattern
+CREATE TEMP VIEW rpr_v 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_v'), 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
@@ -903,8 +2023,19 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );');
+DROP VIEW rpr_v;
 
--- Test 18.3: last_value with pattern
+-- last_value with pattern
+CREATE TEMP VIEW rpr_v 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_v'), 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
@@ -915,8 +2046,22 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );');
+DROP VIEW rpr_v;
 
--- Test 18.4: Multiple window functions
+-- Multiple window functions
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT
@@ -930,12 +2075,25 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
 );');
+DROP VIEW rpr_v;
 
---
--- Section 19: DEFINE Expression Variations
---
+-- ============================================================
+-- DEFINE Expression Variations
+-- ============================================================
 
--- Test 19.1: Complex boolean expressions
+-- Complex boolean expressions
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -948,8 +2106,22 @@ WINDOW w AS (
         A AS (v % 5 <> 0) AND (v % 3 <> 0),
         B AS (v % 5 = 0) OR (v % 3 = 0)
 );');
+DROP VIEW rpr_v;
 
--- Test 19.2: Using PREV function
+-- Using PREV function
+CREATE TEMP VIEW rpr_v AS
+SELECT count(*) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -963,8 +2135,22 @@ WINDOW w AS (
         U AS v > PREV(v),
         D AS v < PREV(v)
 );');
+DROP VIEW rpr_v;
 
--- Test 19.3: Using NULL comparisons
+-- Using NULL comparisons
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -978,12 +2164,23 @@ WINDOW w AS (
     PATTERN (A+ B)
     DEFINE A AS v IS NOT NULL, B AS v IS NULL
 );');
+DROP VIEW rpr_v;
 
---
--- Section 20: Large Scale Statistics Verification
---
+-- ============================================================
+-- Large Scale Statistics Verification
+-- ============================================================
 
--- Test 20.1: 500 rows - verify statistics scale correctly
+-- 500 rows - verify statistics scale correctly
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -994,8 +2191,19 @@ WINDOW w AS (
     PATTERN (A+ B C)
     DEFINE A AS v % 10 < 7, B AS v % 10 = 7, C AS v % 10 = 8
 );');
+DROP VIEW rpr_v;
 
--- Test 20.2: High match count scenario
+-- High match count scenario
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1006,8 +2214,24 @@ WINDOW w AS (
     PATTERN (A B)
     DEFINE A AS v % 2 = 1, B AS v % 2 = 0
 );');
+DROP VIEW rpr_v;
 
--- Test 20.3: High skip count scenario
+-- High skip count scenario
+CREATE TEMP VIEW rpr_v 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_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
 EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
 SELECT count(*) OVER w
@@ -1023,6 +2247,7 @@ WINDOW w AS (
         D AS v % 100 = 4,
         E AS v % 100 = 5
 );');
+DROP VIEW rpr_v;
 
 -- Cleanup
 DROP TABLE nfa_test;
-- 
2.50.1 (Apple Git-155)


From 74205caae63e95c8d237491a821ba07356ac18b2 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Sat, 14 Feb 2026 22:34:39 +0900
Subject: [PATCH 1/1] Run pgindent on RPR source files

---
 src/backend/executor/nodeWindowAgg.c | 32 +++++++++++++++-------------
 1 file changed, 17 insertions(+), 15 deletions(-)

diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index b7b87f65b47..9b2c4b6a1d7 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -5108,9 +5108,9 @@ nfa_process_row(WindowAggState *winstate, int64 currentPos,
 
 		/*
 		 * 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.
+		 * mismatch (nfa_match with NULL), which removes all states (all
+		 * states are at VAR positions after advance). So any surviving
+		 * context here must be within its frame boundary.
 		 */
 		Assert(!hasLimitedFrame ||
 			   currentPos < ctx->matchStartRow + frameOffset + 1);
@@ -5235,7 +5235,7 @@ nfa_states_equal(WindowAggState *winstate, RPRNFAState *s1, RPRNFAState *s2)
 
 	/* Compare counts up to current element's depth */
 	elem = &pattern->elements[s1->elemIdx];
-	compareDepth = elem->depth + 1;		/* depth 0 needs 1 count, etc. */
+	compareDepth = elem->depth + 1; /* depth 0 needs 1 count, etc. */
 
 	if (compareDepth > 0 &&
 		memcmp(s1->counts, s2->counts, sizeof(int32) * compareDepth) != 0)
@@ -5328,14 +5328,14 @@ nfa_add_matched_state(WindowAggState *winstate, RPRNFAContext *ctx,
 		state->next = NULL;
 		ctx->matchEndRow = matchEndRow;
 
-		/*
+		/*----------
 		 * SKIP PAST LAST ROW: eagerly prune contexts within match range.
 		 *
 		 * This function is called whenever a FIN state is reached, including
-		 * during greedy matching when intermediate (shorter) matches are found.
-		 * Each time we update matchEndRow (whether extending a greedy match or
-		 * finding a new match), we can prune pending contexts that started
-		 * within the current match range.
+		 * during greedy matching when intermediate (shorter) matches are
+		 * found. Each time we update matchEndRow (whether extending a greedy
+		 * match or finding a new match), we can prune pending contexts that
+		 * started within the current match range.
 		 *
 		 * SKIP PAST LAST ROW uses lexical order (matchStartRow). Therefore,
 		 * any pending context that started at or before matchEndRow can never
@@ -5349,6 +5349,7 @@ nfa_add_matched_state(WindowAggState *winstate, RPRNFAContext *ctx,
 		 *     - Matches START UP (rows 1-2) → matchEndRow=2 → prune Context B(row 2)
 		 *     - Matches START UP UP (rows 1-3) → matchEndRow=3 → prune Context C(row 3)
 		 *     - Continues greedy extension while pruning incrementally
+		 *----------
 		 */
 		if (winstate->rpSkipTo == ST_PAST_LAST_ROW)
 		{
@@ -5553,8 +5554,8 @@ nfa_get_head_context(WindowAggState *winstate, int64 pos)
 	RPRNFAContext *ctx = winstate->nfaContext;
 
 	/*
-	 * Contexts are sorted by matchStartRow ascending.  If the head
-	 * context doesn't match pos, no context exists for this position.
+	 * 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;
@@ -6075,9 +6076,9 @@ nfa_match(WindowAggState *winstate, RPRNFAContext *ctx, bool *varMatched)
 
 					/*
 					 * END's max can never be exceeded here because
-					 * nfa_advance_end only loops when count < max,
-					 * so endCount entering inline advance is at most
-					 * max-1, and incrementing yields at most max.
+					 * nfa_advance_end only loops when count < max, so
+					 * endCount entering inline advance is at most max-1, and
+					 * incrementing yields at most max.
 					 */
 					Assert(endElem->max == RPR_QUANTITY_INF ||
 						   endCount <= endElem->max);
@@ -6251,7 +6252,7 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
 	else if ((elem->max != RPR_QUANTITY_INF && count >= elem->max) ||
 			 (count == 0 && elem->min == 0))
 	{
-		/*
+		/*----------
 		 * Must exit: either reached max iterations, or group matched empty.
 		 *
 		 * FIXME: The (count == 0 && min == 0) condition is insufficient for
@@ -6265,6 +6266,7 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
 		 * Currently, cycles are silently prevented by nfa_add_state_unique
 		 * detecting duplicate states, but this is implicit and not guaranteed
 		 * for all code paths. Explicit cycle detection is needed.
+		 *----------
 		 */
 		RPRPatternElement *nextElem;
 
-- 
2.50.1 (Apple Git-155)


From 9d2796ca6cc434ef323e31f60d4a1c9976bfeb1c Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Mon, 9 Feb 2026 12:55:01 +0900
Subject: [PATCH 1/1] Review NFA executor and add comprehensive runtime tests

Review the RPR NFA executor code, fix issues found, and add a
comprehensive runtime test suite (rpr_nfa.sql) to validate NFA
behavior at pattern matching boundaries.

Code review fixes in nodeWindowAgg.c:

- Extract nfa_process_row() with three clear phases: match
  (convergence), absorb, and advance (divergence).  Previously these
  phases were interleaved in update_reduced_frame().

- Simplify update_reduced_frame() result registration by separating
  match and mismatch paths into early-return branches, removing the
  combined if-else block.

- Remove nfa_remove_contexts_up_to().  Context cleanup is now handled
  uniformly by nfa_context_free() for both SKIP PAST LAST ROW and
  SKIP TO NEXT ROW modes.

- Rename nfa_state_clone -> nfa_state_create,
  nfa_find_context_for_pos -> nfa_get_head_context for clarity.

- Add nfa_record_context_success/skipped/absorbed statistics helpers.

- Remove unused RPRPattern parameter from nfa_update_absorption_flags().

- Document two FIXME issues found during review:
  (1) altPriority tracks only the last ALT choice, so repeated or
  nested ALTs like (A|B)+ cannot correctly implement SQL standard
  lexical ordering.  A full-path classifier structure is needed.
  (2) Cycle prevention condition (count == 0 && min == 0) is
  insufficient for patterns like (A*)* where cycles occur at
  count > 0.  Currently relies on implicit duplicate detection in
  nfa_add_state_unique.

Code review fixes in rpr.c:

- Remove parentDepth parameter from isUnboundedStart() and
  computeAbsorbabilityRecursive().  Scope boundaries are now
  determined by element depth comparison instead of an explicit
  parent depth parameter.

- Simplify isUnboundedStart(): check simple VAR case first, then
  GROUP case with a depth-bounded loop instead of a FIN-terminated
  traversal with multiple break conditions.

Code review fixes in parse_rpr.c:

- Remove dead frame-type determination code.  RANGE and GROUPS
  frames are already rejected before the start-position check, so
  frameType is always "ROWS".

Test changes:

- Add rpr_nfa.sql: comprehensive NFA runtime tests covering
  quantifier boundaries, alternation priority, nested patterns,
  frame boundary variations, INITIAL mode, pathological patterns,
  context absorption, and FIXME reproduction cases.

- Add nth_value out-of-frame tests, ReScan/LATERAL test, and
  nth_value IGNORE NULLS test to rpr.sql.

- Fix stale comments across rpr.sql, rpr_base.sql, and rpr_nfa.sql:
  remove resolved BUG annotations, update error messages to match
  actual output, correct optimization result descriptions, and
  standardize Expected comment placement to after SQL statements.
---
 src/backend/commands/explain.c            |   12 +-
 src/backend/executor/nodeWindowAgg.c      |  609 ++---
 src/backend/optimizer/plan/rpr.c          |  193 +-
 src/backend/parser/parse_rpr.c            |   10 +-
 src/include/optimizer/rpr.h               |    2 +-
 src/test/regress/expected/rpr.out         |  142 +-
 src/test/regress/expected/rpr_base.out    |   96 +-
 src/test/regress/expected/rpr_explain.out |  549 +++--
 src/test/regress/expected/rpr_nfa.out     | 2524 +++++++++++++++++++++
 src/test/regress/parallel_schedule        |    2 +-
 src/test/regress/sql/rpr.sql              |   88 +-
 src/test/regress/sql/rpr_base.sql         |   92 +-
 src/test/regress/sql/rpr_explain.sql      |   32 +-
 src/test/regress/sql/rpr_nfa.sql          | 1865 +++++++++++++++
 14 files changed, 5445 insertions(+), 771 deletions(-)
 create mode 100644 src/test/regress/expected/rpr_nfa.out
 create mode 100644 src/test/regress/sql/rpr_nfa.sql

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 96542e9538d..c6baa6ca990 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -2986,7 +2986,7 @@ deparse_rpr_elements(RPRPattern *pattern, int *idx, StringInfoData *buf,
 					 RPRDepth groupDepth, RPRDepth *prevDepth,
 					 bool *needSpace)
 {
-	List	   *altSeps = NIL;		/* pending alternation separator indices */
+	List	   *altSeps = NIL;	/* pending alternation separator indices */
 
 	while (*idx < pattern->numElements)
 	{
@@ -3047,9 +3047,9 @@ deparse_rpr_group(RPRPattern *pattern, int *idx, StringInfoData *buf,
 	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.
+	 * 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]))
@@ -3080,7 +3080,7 @@ deparse_rpr_group(RPRPattern *pattern, int *idx, StringInfoData *buf,
 		*needSpace = false;
 	}
 	*prevDepth = childDepth;
-	(*idx)++;						/* consume BEGIN */
+	(*idx)++;					/* consume BEGIN */
 
 	/* Process group children; stops at matching END */
 	deparse_rpr_elements(pattern, idx, buf, begin->depth,
@@ -3101,7 +3101,7 @@ deparse_rpr_group(RPRPattern *pattern, int *idx, StringInfoData *buf,
 	append_rpr_quantifier(buf, end);
 	*prevDepth = end->depth;
 	*needSpace = true;
-	(*idx)++;						/* consume END */
+	(*idx)++;					/* consume END */
 }
 
 /*
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index b2874bf6e9d..b7b87f65b47 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -256,13 +256,17 @@ static void update_reduced_frame(WindowObject winobj, int64 pos);
 static void check_rpr_navigation(Node *node, bool is_prev);
 static bool rpr_navigation_walker(Node *node, void *context);
 
+/* Forward declarations - NFA row processing */
+static void nfa_process_row(WindowAggState *winstate, int64 currentPos,
+							bool hasLimitedFrame, int64 frameOffset);
+
 /* Forward declarations - NFA state management */
 static RPRNFAState *nfa_state_alloc(WindowAggState *winstate);
 static void nfa_state_free(WindowAggState *winstate, RPRNFAState *state);
 static void nfa_state_free_list(WindowAggState *winstate, RPRNFAState *list);
-static RPRNFAState *nfa_state_clone(WindowAggState *winstate, int16 elemIdx,
-									int16 altPriority, int32 *counts,
-									bool sourceAbsorbable);
+static RPRNFAState *nfa_state_create(WindowAggState *winstate, int16 elemIdx,
+									 int16 altPriority, 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,
@@ -275,28 +279,30 @@ static RPRNFAContext *nfa_context_alloc(WindowAggState *winstate);
 static void nfa_unlink_context(WindowAggState *winstate, RPRNFAContext *ctx);
 static void nfa_context_free(WindowAggState *winstate, RPRNFAContext *ctx);
 static RPRNFAContext *nfa_start_context(WindowAggState *winstate, int64 startPos);
-static RPRNFAContext *nfa_find_context_for_pos(WindowAggState *winstate, int64 pos);
+static RPRNFAContext *nfa_get_head_context(WindowAggState *winstate, int64 pos);
 
 /* Forward declarations - NFA statistics */
 static void nfa_update_length_stats(int64 count, NFALengthStats *stats, int64 newLen);
+static void nfa_record_context_success(WindowAggState *winstate, int64 matchLen);
 static void nfa_record_context_failure(WindowAggState *winstate, int64 failedLen);
+static void nfa_record_context_skipped(WindowAggState *winstate, int64 skippedLen);
+static void nfa_record_context_absorbed(WindowAggState *winstate, int64 absorbedLen);
 
 /* Forward declarations - NFA row evaluation */
 static bool nfa_evaluate_row(WindowObject winobj, int64 pos, bool *varMatched);
 
 /* Forward declarations - NFA context lifecycle */
-static void nfa_remove_contexts_up_to(WindowAggState *winstate, int64 endPos,
-									  RPRNFAContext *excludeCtx);
 static void nfa_cleanup_dead_contexts(WindowAggState *winstate, RPRNFAContext *excludeCtx);
+static void nfa_finalize_all_contexts(WindowAggState *winstate, int64 lastPos);
 
 /* Forward declarations - NFA absorption */
-static void nfa_update_absorption_flags(RPRNFAContext *ctx, RPRPattern *pattern);
+static void nfa_update_absorption_flags(RPRNFAContext *ctx);
 static bool nfa_states_covered(RPRPattern *pattern, RPRNFAContext *older,
 							   RPRNFAContext *newer);
 static bool nfa_try_absorb_context(WindowAggState *winstate, RPRNFAContext *ctx);
 static void nfa_absorb_contexts(WindowAggState *winstate);
 
-/* Forward declarations - NFA execution */
+/* Forward declarations - NFA match and advance */
 static inline bool nfa_eval_var_match(WindowAggState *winstate,
 									  RPRPatternElement *elem, bool *varMatched);
 static void nfa_match(WindowAggState *winstate, RPRNFAContext *ctx,
@@ -309,6 +315,9 @@ static void nfa_route_to_elem(WindowAggState *winstate, RPRNFAContext *ctx,
 static void nfa_advance_alt(WindowAggState *winstate, RPRNFAContext *ctx,
 							RPRNFAState *state, RPRPatternElement *elem,
 							int64 currentPos, bool initialAdvance);
+static void nfa_advance_begin(WindowAggState *winstate, RPRNFAContext *ctx,
+							  RPRNFAState *state, RPRPatternElement *elem,
+							  int64 currentPos, bool initialAdvance);
 static void nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
 							RPRNFAState *state, RPRPatternElement *elem,
 							int64 currentPos, bool initialAdvance);
@@ -317,9 +326,6 @@ static void nfa_advance_var(WindowAggState *winstate, RPRNFAContext *ctx,
 							int64 currentPos, bool initialAdvance);
 static void nfa_advance(WindowAggState *winstate, RPRNFAContext *ctx,
 						int64 currentPos, bool initialAdvance);
-static void nfa_process_row(WindowAggState *winstate, int64 currentPos,
-							bool hasLimitedFrame, int64 frameOffset);
-static void nfa_finalize_all_contexts(WindowAggState *winstate, int64 lastPos);
 
 /*
  * Not null info bit array consists of 2-bit items
@@ -4813,6 +4819,7 @@ update_reduced_frame(WindowObject winobj, int64 pos)
 	int			frameOptions = winstate->frameOptions;
 	bool		hasLimitedFrame;
 	int64		frameOffset = 0;
+	int64		matchLen;
 
 	/*
 	 * Check if we have a limited frame (ROWS ... N FOLLOWING). Each context
@@ -4839,7 +4846,7 @@ update_reduced_frame(WindowObject winobj, int64 pos)
 	/*
 	 * Case 2: Find existing context for this pos, or create new one.
 	 */
-	targetCtx = nfa_find_context_for_pos(winstate, pos);
+	targetCtx = nfa_get_head_context(winstate, pos);
 	if (targetCtx == NULL)
 	{
 		/*
@@ -4920,10 +4927,6 @@ update_reduced_frame(WindowObject winobj, int64 pos)
 		 * appropriately as pruned or mismatched.
 		 */
 		nfa_cleanup_dead_contexts(winstate, targetCtx);
-
-		/* Check if target context is now complete */
-		if (targetCtx->states == NULL)
-			break;
 	}
 
 register_result:
@@ -4934,43 +4937,26 @@ register_result:
 	 */
 	if (targetCtx->matchEndRow < targetCtx->matchStartRow)
 	{
-		int64		mismatchLen = targetCtx->lastProcessedRow - targetCtx->matchStartRow + 1;
+		matchLen = targetCtx->lastProcessedRow - targetCtx->matchStartRow + 1;
 
 		register_reduced_frame_map(winstate, targetCtx->matchStartRow, RF_UNMATCHED);
-		nfa_record_context_failure(winstate, mismatchLen);
+		nfa_record_context_failure(winstate, matchLen);
+		nfa_context_free(winstate, targetCtx);
+		return;
 	}
-	else
-	{
-		int64		matchLen = targetCtx->matchEndRow - targetCtx->matchStartRow + 1;
 
-		register_reduced_frame_map(winstate, targetCtx->matchStartRow, RF_FRAME_HEAD);
-		for (int64 i = targetCtx->matchStartRow + 1; i <= targetCtx->matchEndRow; i++)
-		{
-			register_reduced_frame_map(winstate, i, RF_SKIPPED);
-		}
-		winstate->nfaMatchesSucceeded++;
-		nfa_update_length_stats(winstate->nfaMatchesSucceeded,
-								&winstate->nfaMatchLen,
-								matchLen);
-	}
+	/* Match succeeded - register frame map and record statistics */
+	matchLen = targetCtx->matchEndRow - targetCtx->matchStartRow + 1;
 
-	/*
-	 * Cleanup contexts based on SKIP mode.
-	 */
-	if (targetCtx->matchEndRow >= targetCtx->matchStartRow &&
-		winstate->rpSkipTo == ST_PAST_LAST_ROW)
-	{
-		/*
-		 * Remove all contexts with start <= matchEnd, excluding targetCtx
-		 * from skip count
-		 */
-		nfa_remove_contexts_up_to(winstate, targetCtx->matchEndRow, targetCtx);
-	}
-	else
+	register_reduced_frame_map(winstate, targetCtx->matchStartRow, RF_FRAME_HEAD);
+	for (int64 i = targetCtx->matchStartRow + 1; i <= targetCtx->matchEndRow; i++)
 	{
-		/* SKIP TO NEXT ROW or no match: just remove the target context */
-		nfa_context_free(winstate, targetCtx);
+		register_reduced_frame_map(winstate, i, RF_SKIPPED);
 	}
+	nfa_record_context_success(winstate, matchLen);
+
+	/* Remove the matched context */
+	nfa_context_free(winstate, targetCtx);
 }
 
 /*
@@ -5057,6 +5043,82 @@ register_result:
  * allows absorption even when Ctx1 has extra non-absorbable states.
  */
 
+/*
+ * nfa_process_row
+ *
+ * Process all contexts for one row:
+ *   1. Match all contexts (convergence) - evaluate VARs, prune dead states
+ *   2. Absorb redundant contexts - ideal timing after convergence
+ *   3. Advance all contexts (divergence) - create new states for next row
+ */
+static void
+nfa_process_row(WindowAggState *winstate, int64 currentPos,
+				bool hasLimitedFrame, int64 frameOffset)
+{
+	RPRNFAContext *ctx;
+	bool	   *varMatched = winstate->nfaVarMatched;
+
+	/*
+	 * Phase 1: Match all contexts (convergence) Evaluate VAR elements, update
+	 * counts, remove dead states.
+	 */
+	for (ctx = winstate->nfaContext; ctx != NULL; ctx = ctx->next)
+	{
+		if (ctx->states == NULL)
+			continue;
+
+		/* Check frame boundary - finalize if exceeded */
+		if (hasLimitedFrame)
+		{
+			int64		ctxFrameEnd = ctx->matchStartRow + frameOffset + 1;
+
+			if (currentPos >= ctxFrameEnd)
+			{
+				/* Frame boundary exceeded: force mismatch */
+				nfa_match(winstate, ctx, NULL);
+				continue;
+			}
+		}
+
+		nfa_match(winstate, ctx, varMatched);
+		ctx->lastProcessedRow = currentPos;
+	}
+
+	/*
+	 * Phase 2: Absorb redundant contexts After match phase, states have
+	 * converged - ideal for absorption. First update absorption flags that
+	 * may have changed due to state removal.
+	 */
+	if (winstate->rpPattern->isAbsorbable)
+	{
+		for (ctx = winstate->nfaContext; ctx != NULL; ctx = ctx->next)
+			nfa_update_absorption_flags(ctx);
+
+		nfa_absorb_contexts(winstate);
+	}
+
+	/*
+	 * Phase 3: Advance all contexts (divergence) Create new states
+	 * (loop/exit) from surviving matched states.
+	 */
+	for (ctx = winstate->nfaContext; ctx != NULL; ctx = ctx->next)
+	{
+		if (ctx->states == NULL)
+			continue;
+
+		/*
+		 * Phase 1 already handled frame boundary exceeded contexts by forcing
+		 * mismatch (nfa_match with NULL), which removes all states (all states
+		 * are at VAR positions after advance). So any surviving context here
+		 * must be within its frame boundary.
+		 */
+		Assert(!hasLimitedFrame ||
+			   currentPos < ctx->matchStartRow + frameOffset + 1);
+
+		nfa_advance(winstate, ctx, currentPos, false);
+	}
+}
+
 /*
  * nfa_state_alloc
  *
@@ -5109,33 +5171,32 @@ nfa_state_free(WindowAggState *winstate, RPRNFAState *state)
 /*
  * nfa_state_free_list
  *
- * Free all states in a list using pfree.
+ * Return all states in a list to the free list.
  */
 static void
 nfa_state_free_list(WindowAggState *winstate, RPRNFAState *list)
 {
-	RPRNFAState *state;
+	RPRNFAState *next;
 
-	while (list != NULL)
+	for (; list != NULL; list = next)
 	{
-		state = list;
-		list = list->next;
-		nfa_state_free(winstate, state);
+		next = list->next;
+		nfa_state_free(winstate, list);
 	}
 }
 
 /*
- * nfa_state_clone
+ * nfa_state_create
  *
- * Clone a state with given elemIdx, altPriority and counts.
+ * Create a new state with given elemIdx, altPriority 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_clone(WindowAggState *winstate, int16 elemIdx, int16 altPriority,
-				int32 *counts, bool sourceAbsorbable)
+nfa_state_create(WindowAggState *winstate, int16 elemIdx, int16 altPriority,
+				 int32 *counts, bool sourceAbsorbable)
 {
 	RPRPattern *pattern = winstate->rpPattern;
 	int			maxDepth = pattern->maxDepth;
@@ -5165,13 +5226,19 @@ nfa_state_clone(WindowAggState *winstate, int16 elemIdx, int16 altPriority,
 static bool
 nfa_states_equal(WindowAggState *winstate, RPRNFAState *s1, RPRNFAState *s2)
 {
-	int			maxDepth = winstate->rpPattern->maxDepth;
+	RPRPattern *pattern = winstate->rpPattern;
+	RPRPatternElement *elem;
+	int			compareDepth;
 
 	if (s1->elemIdx != s2->elemIdx)
 		return false;
 
-	if (maxDepth > 0 &&
-		memcmp(s1->counts, s2->counts, sizeof(int32) * maxDepth) != 0)
+	/* Compare counts up to current element's depth */
+	elem = &pattern->elements[s1->elemIdx];
+	compareDepth = elem->depth + 1;		/* depth 0 needs 1 count, etc. */
+
+	if (compareDepth > 0 &&
+		memcmp(s1->counts, s2->counts, sizeof(int32) * compareDepth) != 0)
 		return false;
 
 	return true;
@@ -5222,6 +5289,19 @@ nfa_add_state_unique(WindowAggState *winstate, RPRNFAContext *ctx, RPRNFAState *
  * Record a matched state following SQL standard semantics.
  * Lexical order (lower altPriority) wins first. Among same lexical order,
  * longer match wins (greedy).
+ *
+ * FIXME: altPriority is a single value that only tracks the last ALT choice.
+ * For patterns with repeated or nested ALTs like (A|B)+, this cannot correctly
+ * implement SQL standard lexical order, which requires comparing the full path
+ * from left to right. For example:
+ *   Pattern: (A | B)+
+ *   Path "A B A" vs "B A B"
+ *   Current: compares last choice (A vs B) → altPriority 10 vs 20
+ *   Correct: should compare first choice (A < B) → "A B A" wins
+ *
+ * A classifier structure tracking the entire ALT path is required for correct
+ * implementation. Without it, patterns with repeated or nested ALTs will
+ * produce incorrect match selection.
  */
 static void
 nfa_add_matched_state(WindowAggState *winstate, RPRNFAContext *ctx,
@@ -5247,6 +5327,49 @@ nfa_add_matched_state(WindowAggState *winstate, RPRNFAContext *ctx,
 		ctx->matchedState = state;
 		state->next = NULL;
 		ctx->matchEndRow = matchEndRow;
+
+		/*
+		 * SKIP PAST LAST ROW: eagerly prune contexts within match range.
+		 *
+		 * This function is called whenever a FIN state is reached, including
+		 * during greedy matching when intermediate (shorter) matches are found.
+		 * Each time we update matchEndRow (whether extending a greedy match or
+		 * finding a new match), we can prune pending contexts that started
+		 * within the current match range.
+		 *
+		 * SKIP PAST LAST ROW uses lexical order (matchStartRow). Therefore,
+		 * any pending context that started at or before matchEndRow can never
+		 * produce a valid output row - it would be skipped anyway per SQL
+		 * standard.
+		 *
+		 * Example (greedy matching in progress):
+		 *   Pattern: START UP+
+		 *   Rows: 1 2 3 4 5
+		 *   Context A starts at row 1:
+		 *     - Matches START UP (rows 1-2) → matchEndRow=2 → prune Context B(row 2)
+		 *     - Matches START UP UP (rows 1-3) → matchEndRow=3 → prune Context C(row 3)
+		 *     - Continues greedy extension while pruning incrementally
+		 */
+		if (winstate->rpSkipTo == ST_PAST_LAST_ROW)
+		{
+			RPRNFAContext *nextCtx;
+			int64		skippedLen;
+
+			while (ctx->next != NULL &&
+				   ctx->next->matchStartRow <= matchEndRow)
+			{
+				nextCtx = ctx->next;
+				ctx->next = ctx->next->next;
+
+				Assert(nextCtx->lastProcessedRow >= nextCtx->matchStartRow);
+				skippedLen = nextCtx->lastProcessedRow - nextCtx->matchStartRow + 1;
+				nfa_record_context_skipped(winstate, skippedLen);
+
+				nfa_context_free(winstate, nextCtx);
+			}
+			if (ctx->next == NULL)
+				winstate->nfaContextTail = ctx;
+		}
 	}
 	else
 	{
@@ -5258,7 +5381,7 @@ nfa_add_matched_state(WindowAggState *winstate, RPRNFAContext *ctx,
 /*
  * nfa_context_alloc
  *
- * Allocate an NFA context from free list or palloc.
+ * Allocate an NFA context, reusing from free list if available.
  */
 static RPRNFAContext *
 nfa_context_alloc(WindowAggState *winstate)
@@ -5351,8 +5474,9 @@ nfa_context_free(WindowAggState *winstate, RPRNFAContext *ctx)
  * nfa_start_context
  *
  * Start a new match context at given position.
- * Initializes context and state absorption flags.
- * Adds context to winstate->nfaContext list and returns the new context.
+ * Initializes context, state absorption flags, and performs initial advance
+ * to expand epsilon transitions (ALT branches, optional elements).
+ * Adds context to the tail of winstate->nfaContext list.
  */
 static RPRNFAContext *
 nfa_start_context(WindowAggState *winstate, int64 startPos)
@@ -5418,28 +5542,24 @@ nfa_start_context(WindowAggState *winstate, int64 startPos)
 }
 
 /*
- * nfa_find_context_for_pos
+ * nfa_get_head_context
  *
- * Find a context with the given start position.
- * Returns NULL if not found.
+ * Return the head context if its start position matches pos.
+ * Returns NULL if no context exists or head doesn't match pos.
  */
 static RPRNFAContext *
-nfa_find_context_for_pos(WindowAggState *winstate, int64 pos)
+nfa_get_head_context(WindowAggState *winstate, int64 pos)
 {
-	RPRNFAContext *ctx;
+	RPRNFAContext *ctx = winstate->nfaContext;
 
 	/*
-	 * List is sorted by matchStartRow ascending (oldest/smallest at head).
-	 * Stop early if we pass the target position.
+	 * Contexts are sorted by matchStartRow ascending.  If the head
+	 * context doesn't match pos, no context exists for this position.
 	 */
-	for (ctx = winstate->nfaContext; ctx != NULL; ctx = ctx->next)
-	{
-		if (ctx->matchStartRow == pos)
-			return ctx;
-		if (ctx->matchStartRow > pos)
-			break;				/* won't find it, list is sorted */
-	}
-	return NULL;
+	if (ctx == NULL || ctx->matchStartRow != pos)
+		return NULL;
+
+	return ctx;
 }
 
 /*
@@ -5466,6 +5586,20 @@ nfa_update_length_stats(int64 count, NFALengthStats *stats, int64 newLen)
 	stats->total += newLen;
 }
 
+/*
+ * nfa_record_context_success
+ *
+ * Record a successful context in statistics.
+ */
+static void
+nfa_record_context_success(WindowAggState *winstate, int64 matchLen)
+{
+	winstate->nfaMatchesSucceeded++;
+	nfa_update_length_stats(winstate->nfaMatchesSucceeded,
+							&winstate->nfaMatchLen,
+							matchLen);
+}
+
 /*
  * nfa_record_context_failure
  *
@@ -5489,6 +5623,34 @@ nfa_record_context_failure(WindowAggState *winstate, int64 failedLen)
 	}
 }
 
+/*
+ * 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_evaluate_row
  *
@@ -5557,47 +5719,6 @@ nfa_evaluate_row(WindowObject winobj, int64 pos, bool *varMatched)
 	return true;				/* Row exists */
 }
 
-/*
- * nfa_remove_contexts_up_to
- *
- * Remove all contexts with matchStartRow <= endPos.
- * Used by SKIP PAST LAST ROW to discard contexts within matched frame.
- *
- * excludeCtx: if not NULL, this context should not be counted in statistics
- * (typically the matched context that triggered this removal).
- */
-static void
-nfa_remove_contexts_up_to(WindowAggState *winstate, int64 endPos,
-						  RPRNFAContext *excludeCtx)
-{
-	RPRNFAContext *ctx;
-	RPRNFAContext *next;
-
-	/* Contexts are sorted by matchStartRow ascending, so we can stop early */
-	for (ctx = winstate->nfaContext; ctx != NULL; ctx = next)
-	{
-		next = ctx->next;
-		if (ctx->matchStartRow > endPos)
-			break;
-
-		/*
-		 * Track skipped context length statistics, excluding the matched
-		 * context
-		 */
-		if (ctx != excludeCtx && ctx->lastProcessedRow >= ctx->matchStartRow)
-		{
-			int64		skippedLen = ctx->lastProcessedRow - ctx->matchStartRow + 1;
-
-			winstate->nfaContextsSkipped++;
-			nfa_update_length_stats(winstate->nfaContextsSkipped,
-									&winstate->nfaSkippedLen,
-									skippedLen);
-		}
-
-		nfa_context_free(winstate, ctx);
-	}
-}
-
 /*
  * nfa_cleanup_dead_contexts
  *
@@ -5640,6 +5761,27 @@ nfa_cleanup_dead_contexts(WindowAggState *winstate, RPRNFAContext *excludeCtx)
 	}
 }
 
+/*
+ * nfa_finalize_all_contexts
+ *
+ * Finalize all active contexts when partition ends.
+ * Match with NULL to force mismatch, then advance to process epsilon transitions.
+ */
+static void
+nfa_finalize_all_contexts(WindowAggState *winstate, int64 lastPos)
+{
+	RPRNFAContext *ctx;
+
+	for (ctx = winstate->nfaContext; ctx != NULL; ctx = ctx->next)
+	{
+		if (ctx->states != NULL)
+		{
+			nfa_match(winstate, ctx, NULL);
+			nfa_advance(winstate, ctx, lastPos, false);
+		}
+	}
+}
+
 /*
  * nfa_update_absorption_flags
  *
@@ -5657,7 +5799,7 @@ nfa_cleanup_dead_contexts(WindowAggState *winstate, RPRNFAContext *excludeCtx)
  * permanently, so we skip recalculation.
  */
 static void
-nfa_update_absorption_flags(RPRNFAContext *ctx, RPRPattern *pattern)
+nfa_update_absorption_flags(RPRNFAContext *ctx)
 {
 	RPRNFAState *state;
 	bool		hasAbsorbable = false;
@@ -5681,9 +5823,6 @@ nfa_update_absorption_flags(RPRNFAContext *ctx, RPRPattern *pattern)
 		return;
 	}
 
-	if (pattern == NULL)
-		return;
-
 	/*
 	 * Iterate through all states to check absorption status. Uses
 	 * state->isAbsorbable which tracks if state is in absorbable region. This
@@ -5800,10 +5939,7 @@ nfa_try_absorb_context(WindowAggState *winstate, RPRNFAContext *ctx)
 			int64		absorbedLen = ctx->lastProcessedRow - ctx->matchStartRow + 1;
 
 			nfa_context_free(winstate, ctx);
-			winstate->nfaContextsAbsorbed++;
-			nfa_update_length_stats(winstate->nfaContextsAbsorbed,
-									&winstate->nfaAbsorbedLen,
-									absorbedLen);
+			nfa_record_context_absorbed(winstate, absorbedLen);
 			return true;
 		}
 	}
@@ -5853,6 +5989,9 @@ static inline 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))
@@ -5884,17 +6023,19 @@ nfa_match(WindowAggState *winstate, RPRNFAContext *ctx, bool *varMatched)
 	RPRPatternElement *elements = pattern->elements;
 	RPRNFAState **prevPtr = &ctx->states;
 	RPRNFAState *state;
+	RPRNFAState *nextState;
 
 	/*
 	 * Evaluate VAR elements against current row. For simple VARs with END
 	 * next, advance to END and update group count inline so absorb phase can
 	 * compare states properly.
 	 */
-	for (state = ctx->states; state != NULL;)
+	for (state = ctx->states; state != NULL; state = nextState)
 	{
-		RPRNFAState *nextState = state->next;
 		RPRPatternElement *elem = &elements[state->elemIdx];
 
+		nextState = state->next;
+
 		if (RPRElemIsVar(elem))
 		{
 			bool		matched;
@@ -5909,15 +6050,8 @@ nfa_match(WindowAggState *winstate, RPRNFAContext *ctx, bool *varMatched)
 				if (count < RPR_COUNT_MAX)
 					count++;
 
-				/* Check max constraint */
-				if (elem->max != RPR_QUANTITY_INF && count > elem->max)
-				{
-					/* Exceeded max - remove state */
-					*prevPtr = nextState;
-					nfa_state_free(winstate, state);
-					state = nextState;
-					continue;
-				}
+				/* Max constraint should not be exceeded */
+				Assert(elem->max == RPR_QUANTITY_INF || count <= elem->max);
 
 				state->counts[depth] = count;
 
@@ -5926,33 +6060,30 @@ nfa_match(WindowAggState *winstate, RPRNFAContext *ctx, bool *varMatched)
 				 * and update group count inline. This keeps state in place,
 				 * preserving lexical order.
 				 */
-				if (elem->min == 1 && elem->max == 1 && count == 1 &&
+				if (elem->min == 1 && elem->max == 1 &&
 					RPRElemIsEnd(&elements[elem->next]))
 				{
 					RPRPatternElement *endElem = &elements[elem->next];
 					int			endDepth = endElem->depth;
 					int32		endCount = state->counts[endDepth];
 
+					Assert(count == 1);
+
 					/* Increment group count with overflow protection */
 					if (endCount < RPR_COUNT_MAX)
 						endCount++;
 
-					/* Check END's max constraint */
-					if (endElem->max != RPR_QUANTITY_INF && endCount > endElem->max)
-					{
-						/* Exceeded END's max - remove state */
-						*prevPtr = nextState;
-						nfa_state_free(winstate, state);
-						state = nextState;
-						continue;
-					}
+					/*
+					 * END's max can never be exceeded here because
+					 * nfa_advance_end only loops when count < max,
+					 * so endCount entering inline advance is at most
+					 * max-1, and incrementing yields at most max.
+					 */
+					Assert(endElem->max == RPR_QUANTITY_INF ||
+						   endCount <= endElem->max);
 
 					state->elemIdx = elem->next;
 					state->counts[endDepth] = endCount;
-
-					/* Clear deeper counts */
-					for (int d = endDepth + 1; d < pattern->maxDepth; d++)
-						state->counts[d] = 0;
 				}
 				/* else: stay at VAR for advance phase */
 			}
@@ -5964,14 +6095,12 @@ nfa_match(WindowAggState *winstate, RPRNFAContext *ctx, bool *varMatched)
 				 */
 				*prevPtr = nextState;
 				nfa_state_free(winstate, state);
-				state = nextState;
 				continue;
 			}
 		}
 		/* Non-VAR elements: keep as-is for advance phase */
 
 		prevPtr = &state->next;
-		state = nextState;
 	}
 }
 
@@ -5993,9 +6122,9 @@ nfa_route_to_elem(WindowAggState *winstate, RPRNFAContext *ctx,
 		{
 			RPRNFAState *skipState;
 
-			skipState = nfa_state_clone(winstate, nextElem->next,
-										state->altPriority, state->counts,
-										state->isAbsorbable);
+			skipState = nfa_state_create(winstate, nextElem->next,
+										 state->altPriority, state->counts,
+										 state->isAbsorbable);
 			nfa_advance_state(winstate, ctx, skipState, currentPos, initialAdvance);
 		}
 	}
@@ -6026,6 +6155,10 @@ nfa_advance_alt(WindowAggState *winstate, RPRNFAContext *ctx,
 		RPRPatternElement *altElem = &elements[altIdx];
 		RPRNFAState *newState;
 
+		/* Stop if element is outside ALT scope (not a branch) */
+		if (altElem->depth <= elem->depth)
+			break;
+
 		if (first)
 		{
 			state->elemIdx = altIdx;
@@ -6035,8 +6168,8 @@ nfa_advance_alt(WindowAggState *winstate, RPRNFAContext *ctx,
 		}
 		else
 		{
-			newState = nfa_state_clone(winstate, altIdx, altIdx,
-									   state->counts, state->isAbsorbable);
+			newState = nfa_state_create(winstate, altIdx, altIdx,
+										state->counts, state->isAbsorbable);
 		}
 
 		/* Recursively process this branch before next */
@@ -6044,8 +6177,8 @@ nfa_advance_alt(WindowAggState *winstate, RPRNFAContext *ctx,
 		altIdx = altElem->jump;
 	}
 
-	if (first)
-		nfa_state_free(winstate, state);
+	/* ALT must have at least one branch */
+	Assert(!first);
 }
 
 /*
@@ -6063,24 +6196,28 @@ nfa_advance_begin(WindowAggState *winstate, RPRNFAContext *ctx,
 {
 	RPRPattern *pattern = winstate->rpPattern;
 	RPRPatternElement *elements = pattern->elements;
+	RPRNFAState *skipState = NULL;
 
 	state->counts[elem->depth] = 0;
 
-	/* Optional group: create skip path */
+	/* Optional group: create skip path (but don't route yet) */
 	if (elem->min == 0)
 	{
-		RPRNFAState *skipState;
-
-		skipState = nfa_state_clone(winstate, elem->jump, state->altPriority,
+		skipState = nfa_state_create(winstate, elem->jump, state->altPriority,
 									 state->counts, state->isAbsorbable);
-		nfa_route_to_elem(winstate, ctx, skipState,
-						  &elements[elem->jump], currentPos, initialAdvance);
 	}
 
-	/* Enter group: route to first child */
+	/* Enter group: route to first child (lexically first) */
 	state->elemIdx = elem->next;
 	nfa_route_to_elem(winstate, ctx, state,
 					  &elements[state->elemIdx], currentPos, initialAdvance);
+
+	/* Now route skip path (lexically second) */
+	if (skipState != NULL)
+	{
+		nfa_route_to_elem(winstate, ctx, skipState,
+						  &elements[elem->jump], currentPos, initialAdvance);
+	}
 }
 
 /*
@@ -6117,11 +6254,17 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
 		/*
 		 * Must exit: either reached max iterations, or group matched empty.
 		 *
-		 * The second condition (count == 0 && min == 0) prevents infinite
-		 * recursion with patterns like (A*)* where the inner group can match
-		 * empty.  If the group completed without consuming any input
-		 * (count=0) and is optional (min=0), looping back would just repeat
-		 * the empty match forever.  So we force exit instead.
+		 * FIXME: The (count == 0 && min == 0) condition is insufficient for
+		 * cycle prevention. Cycles can occur at any count value when loop back
+		 * happens without consuming rows. For example:
+		 *   Pattern: (A*)*
+		 *   After matching 3 A's (count=3), loop back at a B row
+		 *   Inner A* matches 0 times (skip path) → same (elemIdx, count=3)
+		 *   Infinite cycle at count=3, not count=0
+		 *
+		 * Currently, cycles are silently prevented by nfa_add_state_unique
+		 * detecting duplicate states, but this is implicit and not guaranteed
+		 * for all code paths. Explicit cycle detection is needed.
 		 */
 		RPRPatternElement *nextElem;
 
@@ -6155,8 +6298,8 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
 		 * Create exit state first (need original counts before modifying
 		 * state)
 		 */
-		exitState = nfa_state_clone(winstate, elem->next, exitAltPriority,
-									state->counts, state->isAbsorbable);
+		exitState = nfa_state_create(winstate, elem->next, exitAltPriority,
+									 state->counts, state->isAbsorbable);
 		exitState->counts[depth] = 0;
 		nextElem = &elements[exitState->elemIdx];
 
@@ -6165,7 +6308,6 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
 			exitState->counts[nextElem->depth]++;
 
 		/* Route loop state first (earlier in pattern = lexical order) */
-		state->counts[depth] = count;
 		for (int d = depth + 1; d < pattern->maxDepth; d++)
 			state->counts[d] = 0;
 		state->elemIdx = elem->jump;
@@ -6196,14 +6338,17 @@ nfa_advance_var(WindowAggState *winstate, RPRNFAContext *ctx,
 	bool		canLoop = (elem->max == RPR_QUANTITY_INF || count < elem->max);
 	bool		canExit = (count >= elem->min);
 
+	/* After a successful match, count >= 1, so at least one must be true */
+	Assert(canLoop || canExit);
+
 	if (canLoop && canExit)
 	{
 		/* Both: clone for loop, modify original for exit */
 		RPRNFAState *loopState;
 		RPRPatternElement *nextElem;
 
-		loopState = nfa_state_clone(winstate, state->elemIdx, state->altPriority,
-									state->counts, state->isAbsorbable);
+		loopState = nfa_state_create(winstate, state->elemIdx, state->altPriority,
+									 state->counts, state->isAbsorbable);
 		nfa_add_state_unique(winstate, ctx, loopState);
 
 		/* Exit: advance to next element */
@@ -6220,7 +6365,7 @@ nfa_advance_var(WindowAggState *winstate, RPRNFAContext *ctx,
 	}
 	else if (canExit)
 	{
-		/* Exit only: modify state */
+		/* Exit only: advance to next element */
 		RPRPatternElement *nextElem;
 
 		state->counts[depth] = 0;
@@ -6229,11 +6374,6 @@ nfa_advance_var(WindowAggState *winstate, RPRNFAContext *ctx,
 
 		nfa_route_to_elem(winstate, ctx, state, nextElem, currentPos, initialAdvance);
 	}
-	else
-	{
-		/* Neither - shouldn't happen, free state */
-		nfa_state_free(winstate, state);
-	}
 }
 
 /*
@@ -6311,106 +6451,3 @@ nfa_advance(WindowAggState *winstate, RPRNFAContext *ctx, int64 currentPos,
 		nfa_advance_state(winstate, ctx, state, currentPos, initialAdvance);
 	}
 }
-
-/*
- * nfa_process_row
- *
- * Process all contexts for one row using the new flow:
- *   1. Match all contexts (convergence) - evaluate VARs, prune dead states
- *   2. Absorb redundant contexts - ideal timing after convergence
- *   3. Advance all contexts (divergence) - create new states for next row
- */
-static void
-nfa_process_row(WindowAggState *winstate, int64 currentPos,
-				bool hasLimitedFrame, int64 frameOffset)
-{
-	RPRNFAContext *ctx;
-	bool	   *varMatched = winstate->nfaVarMatched;
-
-	/*
-	 * Phase 1: Match all contexts (convergence) Evaluate VAR elements, update
-	 * counts, remove dead states.
-	 */
-	for (ctx = winstate->nfaContext; ctx != NULL; ctx = ctx->next)
-	{
-		if (ctx->states == NULL)
-			continue;
-
-		/* Check frame boundary - finalize if exceeded */
-		if (hasLimitedFrame)
-		{
-			int64		ctxFrameEnd = ctx->matchStartRow + frameOffset + 1;
-
-			if (currentPos >= ctxFrameEnd)
-			{
-				/*
-				 * Frame boundary: match with NULL (force mismatch), then
-				 * advance
-				 */
-				nfa_match(winstate, ctx, NULL);
-				nfa_advance(winstate, ctx, ctxFrameEnd - 1, false);
-				continue;
-			}
-		}
-
-		nfa_match(winstate, ctx, varMatched);
-		ctx->lastProcessedRow = currentPos;
-	}
-
-	/*
-	 * Phase 2: Absorb redundant contexts After match phase, states have
-	 * converged - ideal for absorption. First update absorption flags that
-	 * may have changed due to state removal.
-	 */
-	if (winstate->rpPattern->isAbsorbable)
-	{
-		RPRPattern *pattern = winstate->rpPattern;
-
-		for (ctx = winstate->nfaContext; ctx != NULL; ctx = ctx->next)
-			nfa_update_absorption_flags(ctx, pattern);
-
-		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;
-
-		/* Skip contexts already finalized in phase 1 */
-		if (hasLimitedFrame)
-		{
-			int64		ctxFrameEnd = ctx->matchStartRow + frameOffset + 1;
-
-			if (currentPos >= ctxFrameEnd)
-				continue;
-		}
-
-		nfa_advance(winstate, ctx, currentPos, false);
-	}
-}
-
-/*
- * nfa_finalize_all_contexts
- *
- * Finalize all active contexts when partition ends.
- * Match with NULL to force mismatch, then advance to process epsilon transitions.
- */
-static void
-nfa_finalize_all_contexts(WindowAggState *winstate, int64 lastPos)
-{
-	RPRNFAContext *ctx;
-
-	for (ctx = winstate->nfaContext; ctx != NULL; ctx = ctx->next)
-	{
-		if (ctx->states != NULL)
-		{
-			nfa_match(winstate, ctx, NULL);
-			nfa_advance(winstate, ctx, lastPos, false);
-		}
-	}
-}
diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index 67710a94a0d..112ed034fe2 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -89,12 +89,10 @@ static void fillRPRPattern(RPRPatternNode *node, RPRPattern *pat,
 static void finalizeRPRPattern(RPRPattern *result);
 
 /* Forward declarations - context absorption */
-static bool isUnboundedStart(RPRPattern *pattern, RPRElemIdx idx,
-							 RPRDepth parentDepth);
+static bool isUnboundedStart(RPRPattern *pattern, RPRElemIdx idx);
 static void computeAbsorbabilityRecursive(RPRPattern *pattern,
 										  RPRElemIdx startIdx,
-										  bool *hasAbsorbable,
-										  RPRDepth parentDepth);
+										  bool *hasAbsorbable);
 static void computeAbsorbability(RPRPattern *pattern);
 
 /*
@@ -1184,13 +1182,13 @@ fillRPRPatternGroup(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth de
 		endElem->min = node->min;
 		endElem->max = (node->max == INT_MAX) ? RPR_QUANTITY_INF : node->max;
 		endElem->next = RPR_ELEMIDX_INVALID;
-		endElem->jump = groupStartIdx;		/* loop to first child */
+		endElem->jump = groupStartIdx;	/* loop to first child */
 		if (node->reluctant >= 0)
 			endElem->flags |= RPR_ELEM_RELUCTANT;
 		(*idx)++;
 
 		/* Set BEGIN skip pointer (next is set by finalize) */
-		beginElem->jump = *idx;				/* skip: go to after END */
+		beginElem->jump = *idx; /* skip: go to after END */
 	}
 }
 
@@ -1439,121 +1437,79 @@ finalizeRPRPattern(RPRPattern *result)
 
 /*
  * isUnboundedStart
- *		Check if the element at idx starts an unbounded sequence.
+ *		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) so that we can "shift" by decrementing count.
+ * For context absorption to work, the sequence starting at idx must be:
+ *   - Unbounded (max = infinity)
+ *   - Greedy (not reluctant)
+ *   - At the start of current scope
  *
  * Algorithm:
- *   - Traverse elements within current scope (bounded by parentDepth)
- *   - For GROUP: must be unbounded AND contain only simple {1,1} VARs
- *   - Check if sequence starts with unbounded element (VAR or GROUP END)
+ *   - Traverse elements within current scope (parentDepth to startDepth)
+ *   - For GROUP: must be unbounded greedy AND contain only simple {1,1} VARs
+ *   - Sets ABSORBABLE and ABSORBABLE_BRANCH flags on matching elements
  *
  * Two cases are handled:
- *   1. Simple var: A+ B C - first element A has max=INF
- *   2. Group: (A B){2,} C - group END has max=INF, and all elements
- *      inside the group must be simple {1,1} vars (no nested complexity)
+ *   1. Simple VAR: A+ B C - A has max=INF, gets both flags
+ *   2. Group: (A B)+ C - END has max=INF, all children are {1,1} VARs
+ *      A,B,END get ABSORBABLE_BRANCH, only END gets ABSORBABLE
  *
  * Returns false for patterns where absorption cannot work:
  *   - A B+ (unbounded not at start)
- *   - (A | B){2,} (ALT inside group)
- *   - (A B+){2,} (unbounded element inside group)
- *   - ((A B){2,} C){3,} (nested groups)
+ *   - A+? B (reluctant quantifier)
+ *   - (A | B)+ (ALT inside group)
+ *   - (A B+)+ (unbounded element inside group)
+ *   - ((A B)+ C)+ (nested unbounded groups)
  */
 static bool
-isUnboundedStart(RPRPattern *pattern, RPRElemIdx idx, RPRDepth parentDepth)
+isUnboundedStart(RPRPattern *pattern, RPRElemIdx idx)
 {
 	RPRPatternElement *elem = &pattern->elements[idx];
 	RPRDepth	startDepth = elem->depth;
 	RPRPatternElement *nextElem;
 	RPRPatternElement *e;
 
+	/* Case 1: Simple unbounded VAR at start (greedy only) */
+	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;
+	}
+
 	/*
-	 * Traverse elements until we exit the current scope. For groups, check if
-	 * all elements inside are simple {1,1} vars. Groups with complex
-	 * quantifiers (e.g., (A+ B)+) are not absorbable. Uses parentDepth as
-	 * upper boundary to detect scope exit.
+	 * Case 2: Unbounded GROUP - traverse siblings at startDepth and check if
+	 * they're all simple {1,1} VARs, then check if END at startDepth - 1 is
+	 * unbounded greedy.
 	 */
-	for (e = elem; !RPRElemIsFin(e); e = nextElem)
+	for (e = elem; e->depth == startDepth; e = nextElem)
 	{
-		/* Check for unbounded END at enclosing depth */
-		if (e->depth < startDepth)
-		{
-			/* Case 2: Unbounded group - END element points back to idx */
-			if (e->depth == startDepth - 1 &&
-				RPRElemIsEnd(e) && e->max == RPR_QUANTITY_INF &&
-				!RPRElemIsReluctant(e))
-			{
-				Assert(e->jump == idx); /* END-based break ensures this */
-
-				/*
-				 * Set ABSORBABLE_BRANCH on all elements, ABSORBABLE on END
-				 * only
-				 */
-				for (e = elem; !RPRElemIsEnd(e); e = &pattern->elements[e->next])
-					e->flags |= RPR_ELEM_ABSORBABLE_BRANCH;
-				e->flags |= RPR_ELEM_ABSORBABLE_BRANCH | RPR_ELEM_ABSORBABLE;
-				return true;
-			}
-			break;
-		}
-
-		/* Element must be simple {1,1} VAR */
-		if (e->depth == startDepth &&
-			(!RPRElemIsVar(e) || e->min != 1 || e->max != 1))
-			break;
+		/* Must be simple {1,1} VAR */
+		if (!RPRElemIsVar(e) || e->min != 1 || e->max != 1)
+			return false;
 
-		/* Next must be valid (will break before reaching FIN) */
 		Assert(e->next != RPR_ELEMIDX_INVALID);
 		nextElem = &pattern->elements[e->next];
-
-		/* Break if next element exits current scope */
-		if (nextElem->depth < parentDepth || nextElem->depth > startDepth)
-			break;
 	}
 
-	/* Case 1: Simple unbounded var at start (greedy only) */
-	if (RPRElemIsVar(elem) && elem->max == RPR_QUANTITY_INF &&
-		!RPRElemIsReluctant(elem))
+	/* Now e should be END at startDepth - 1 */
+	if (e->depth == startDepth - 1 &&
+		RPRElemIsEnd(e) && e->max == RPR_QUANTITY_INF &&
+		!RPRElemIsReluctant(e))
 	{
-		/* Set both flags on first element */
-		elem->flags |= RPR_ELEM_ABSORBABLE_BRANCH | RPR_ELEM_ABSORBABLE;
+		Assert(e->jump == idx); /* END points back to first child */
+
+		/* Set ABSORBABLE_BRANCH on all children, ABSORBABLE on END only */
+		for (e = elem; !RPRElemIsEnd(e); e = &pattern->elements[e->next])
+			e->flags |= RPR_ELEM_ABSORBABLE_BRANCH;
+		e->flags |= RPR_ELEM_ABSORBABLE_BRANCH | RPR_ELEM_ABSORBABLE;
 		return true;
 	}
 
 	return false;
 }
 
-/*
- * 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
- */
-
 /*
  * computeAbsorbabilityRecursive
  *		Recursively check absorbability starting from given index.
@@ -1562,14 +1518,14 @@ isUnboundedStart(RPRPattern *pattern, RPRElemIdx idx, RPRDepth parentDepth)
  * Each branch gets its own absorbability status, and if any branch is absorbable,
  * the ALT element itself is marked with RPR_ELEM_ABSORBABLE_BRANCH.
  *
- * Otherwise, checks if the element starts an unbounded sequence via isUnboundedStart.
+ * If BEGIN, skips to first child.
  *
- * parentDepth acts as a scope boundary, preventing checks from crossing into
- * enclosing contexts.
+ * Otherwise (VAR), checks if the element starts an unbounded sequence via
+ * isUnboundedStart.
  */
 static void
 computeAbsorbabilityRecursive(RPRPattern *pattern, RPRElemIdx startIdx,
-							  bool *hasAbsorbable, RPRDepth parentDepth)
+							  bool *hasAbsorbable)
 {
 	RPRPatternElement *elem = &pattern->elements[startIdx];
 
@@ -1587,9 +1543,12 @@ computeAbsorbabilityRecursive(RPRPattern *pattern, RPRElemIdx startIdx,
 			Assert(branchIdx < pattern->numElements);
 			branchFirst = &pattern->elements[branchIdx];
 
-			/* Recursively check this branch with ALT depth + 1 as boundary */
-			computeAbsorbabilityRecursive(pattern, branchIdx, &branchAbsorbable,
-										  elem->depth + 1);
+			/* 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;
@@ -1605,7 +1564,7 @@ computeAbsorbabilityRecursive(RPRPattern *pattern, RPRElemIdx startIdx,
 	else if (RPRElemIsBegin(elem))
 	{
 		/* BEGIN: skip to first child and check that */
-		computeAbsorbabilityRecursive(pattern, elem->next, hasAbsorbable, parentDepth);
+		computeAbsorbabilityRecursive(pattern, elem->next, hasAbsorbable);
 
 		/* Mark BEGIN element if contents are absorbable */
 		if (*hasAbsorbable)
@@ -1613,14 +1572,46 @@ computeAbsorbabilityRecursive(RPRPattern *pattern, RPRElemIdx startIdx,
 	}
 	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, parentDepth))
+		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)
 {
@@ -1629,8 +1620,8 @@ computeAbsorbability(RPRPattern *pattern)
 	/* Parser always produces at least one element + FIN */
 	Assert(pattern->numElements >= 2);
 
-	/* Start recursion with parentDepth=0 (top level) */
-	computeAbsorbabilityRecursive(pattern, 0, &hasAbsorbable, 0);
+	/* Start recursion from first element */
+	computeAbsorbabilityRecursive(pattern, 0, &hasAbsorbable);
 	pattern->isAbsorbable = hasAbsorbable;
 }
 
diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index 80ebf3c33f8..9e1fa228759 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -90,17 +90,9 @@ transformRPR(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 	/* Frame must start at current row */
 	if ((wc->frameOptions & FRAMEOPTION_START_CURRENT_ROW) == 0)
 	{
-		const char *frameType = "GROUPS";
+		const char *frameType = "ROWS";
 		const char *startBound = "unknown";
 
-		/* Determine frame type */
-		if (wc->frameOptions & FRAMEOPTION_ROWS)
-			frameType = "ROWS";
-		else if (wc->frameOptions & FRAMEOPTION_RANGE)
-			frameType = "RANGE";
-		else
-			frameType = "GROUPS";
-
 		/* Determine current start bound */
 		if (wc->frameOptions & FRAMEOPTION_START_UNBOUNDED_PRECEDING)
 			startBound = "UNBOUNDED PRECEDING";
diff --git a/src/include/optimizer/rpr.h b/src/include/optimizer/rpr.h
index 8e1bc47643c..7a8938dbebd 100644
--- a/src/include/optimizer/rpr.h
+++ b/src/include/optimizer/rpr.h
@@ -22,7 +22,7 @@
 #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_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) */
diff --git a/src/test/regress/expected/rpr.out b/src/test/regress/expected/rpr.out
index 8c8ff90e1cd..c921badb006 100644
--- a/src/test/regress/expected/rpr.out
+++ b/src/test/regress/expected/rpr.out
@@ -826,6 +826,46 @@ SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER
  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
@@ -1165,6 +1205,33 @@ count(*) OVER w
  07-07-2023 |  1100 |             |     0
 (14 rows)
 
+-- ReScan test: LATERAL join forces WindowAgg rescan with RPR
+-- Tests ExecReScanWindowAgg clearing prev_slot/next_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;
@@ -1352,6 +1419,46 @@ WITH data AS (
   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)
+
 -- View and pg_get_viewdef tests.
 CREATE TEMP VIEW v_window AS
 SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w,
@@ -2882,11 +2989,10 @@ WINDOW w AS (
 --   Row 2: B C D (2-4) <- ends first!
 --   Row 3: C D E F (3-6) <- ends last!
 -- Test 1b: Longer pattern FAILS, shorter pattern should survive
--- Pattern: A+ B C D | A+ B (greedy at front for absorption)
--- Data: A B C X (D expected but X found)
--- A+ B C D fails at row 4 (X instead of D)
--- A+ B succeeds at row 2
--- Result should be match 1-2 (A+ B)
+-- Pattern: A+ B C D E | B+ C
+-- A+ B C D E fails (no E found in sequence)
+-- B+ C matches at rows 2-3
+-- Result: match 2-3 (B+ C)
 WITH test_overlap1b AS (
     SELECT * FROM (VALUES
         (1, ARRAY['A']),
@@ -3179,8 +3285,7 @@ WINDOW w AS (
 (5 rows)
 
 -- Pattern A+ is absorbable (unbounded first element, only one unbounded)
--- Without absorption: 4 matches (1-4, 2-4, 3-4, 4-4)
--- With absorption: Row 1 match (1-4), rows 2-4 absorbed
+-- 4 matches: (1-4, 2-4, 3-4, 4-4)
 -- Test absorption 2: A+ B pattern - absorption with fixed suffix
 WITH test_absorb_suffix AS (
     SELECT * FROM (VALUES
@@ -3213,7 +3318,7 @@ WINDOW w AS (
 
 -- Pattern A+ B is absorbable (A+ unbounded first, B bounded suffix)
 -- All potential matches end at same row (row 4 with B)
--- With absorption: Row 1 match (1-4), rows 2-3 absorbed
+-- 3 matches: (1-4, 2-4, 3-4)
 -- Test absorption 3: Per-branch absorption with ALT (B+ C | B+ D)
 WITH test_absorb_alt AS (
     SELECT * FROM (VALUES
@@ -3246,8 +3351,7 @@ WINDOW w AS (
 (5 rows)
 
 -- Both branches B+ C and B+ D are absorbable (B+ unbounded first)
--- B+ D branch matches: potential 1-4, 2-4, 3-4
--- Row 1 (1-4) absorbs Row 2 (2-4) and Row 3 (3-4) - same endpoint
+-- B+ D branch matches: 3 matches (1-4, 2-4, 3-4)
 -- Test absorption 4: Non-absorbable pattern (A B+ - unbounded not first)
 WITH test_no_absorb AS (
     SELECT * FROM (VALUES
@@ -3315,8 +3419,7 @@ WINDOW w AS (
 (7 rows)
 
 -- Pattern optimized: (A B) (A B)+ -> (A B){2,}
--- Potential matches: 1-6 (3 reps), 3-6 (2 reps), 5-6 needs 2 reps (fail)
--- Row 1 (1-6) absorbs Row 3 (3-6) - same endpoint, top-level unbounded GROUP
+-- 2 matches: 1-6 (3 reps), 3-6 (2 reps)
 -- Test absorption 6: Multiple unbounded - first element unbounded enables absorption
 WITH test_multi_unbounded AS (
     SELECT * FROM (VALUES
@@ -3347,7 +3450,7 @@ WINDOW w AS (
   5 | {X}   |             |          
 (5 rows)
 
--- Row 1: 1-4, Row 2: absorbed (same endpoint 4)
+-- 2 matches: 1-4, 2-4 (same endpoint 4)
 -- ============================================
 -- Jacob's RPR Patterns (from jacob branch)
 -- ============================================
@@ -3671,7 +3774,6 @@ WINDOW w AS (
 -- Alternation with quantifiers (BUG cases from Jacob's tests)
 -- ============================================
 -- Test: (A | B)+ C - alternation inside quantified group followed by C
--- BUG: Currently returns NULL instead of matching 1-4
 WITH jacob_alt_quant AS (
     SELECT * FROM (VALUES
         (1, ARRAY['A']),
@@ -3702,7 +3804,6 @@ WINDOW w AS (
 
 -- Expected: 1-4 (A B A C)
 -- Test: ((A | B) C)+ - alternation inside group with outer quantifier
--- Currently returns two separate matches (1-2, 3-4) instead of single 1-4
 WITH jacob_alt_group AS (
     SELECT * FROM (VALUES
         (1, ARRAY['A']),
@@ -3737,8 +3838,7 @@ WINDOW w AS (
 -- ============================================
 -- RELUCTANT quantifiers (not yet supported)
 -- ============================================
--- Test: A+? B (reluctant) - CURRENTLY ACTS LIKE A+ (GREEDY)
--- Parser accepts the syntax but executor doesn't differentiate
+-- Test: A+? B (reluctant) - parser rejects with ERROR
 WITH jacob_reluctant AS (
     SELECT * FROM (VALUES
         (1, ARRAY['A']),
@@ -3761,9 +3861,8 @@ WINDOW w AS (
 ERROR:  reluctant quantifiers are not yet supported
 LINE 15:     PATTERN (A+? B)
                         ^
--- Current: 1-4 (A A A B) - greedy behavior
--- Expected when RELUCTANT implemented: 3-4 (A B) - shortest A before B
--- Test: A{1,3}? B (reluctant bounded)
+-- Expected: ERROR (reluctant quantifiers not yet supported)
+-- Test: A{1,3}? B (reluctant bounded) - parser rejects with ERROR
 WITH jacob_reluctant_bounded AS (
     SELECT * FROM (VALUES
         (1, ARRAY['A']),
@@ -3786,8 +3885,7 @@ WINDOW w AS (
 ERROR:  reluctant quantifiers are not yet supported
 LINE 15:     PATTERN (A{1,3}? B)
                             ^
--- Current: 1-4 (greedy, takes max A before B)
--- Expected when RELUCTANT implemented: 3-4 (takes min A before B)
+-- Expected: ERROR (reluctant quantifiers not yet supported)
 -- ============================================
 -- Nested quantifiers (pathological patterns)
 -- ============================================
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index a1f11bd61ce..ec67a099ee6 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -233,7 +233,7 @@ WINDOW w AS (
 ERROR:  row pattern definition variable name "a" appears more than once in DEFINE clause
 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
+-- 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);
@@ -438,7 +438,7 @@ ERROR:  FRAME option RANGE is not permitted when row pattern recognition is used
 LINE 5:     RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWIN...
             ^
 HINT:  Use: ROWS instead
--- Expected: ERROR: FRAME must start at current row when row pattern recognition is used
+-- Expected: ERROR: FRAME option RANGE is not permitted when row pattern recognition is used
 -- GROUPS frame not starting at CURRENT ROW
 SELECT COUNT(*) OVER w
 FROM rpr_frame
@@ -452,7 +452,7 @@ ERROR:  FRAME option GROUP is not permitted when row pattern recognition is used
 LINE 5:     GROUPS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWI...
             ^
 HINT:  Use: ROWS instead
--- Expected: ERROR: FRAME must start at current row when row pattern recognition is used
+-- Expected: ERROR: FRAME option GROUP is not permitted when row pattern recognition is used
 -- Starting with N PRECEDING
 SELECT COUNT(*) OVER w
 FROM rpr_frame
@@ -496,7 +496,7 @@ WINDOW w AS (
 ERROR:  frame starting from current row cannot have preceding rows
 LINE 5:     ROWS BETWEEN CURRENT ROW AND 1 PRECEDING
                                          ^
--- Expected: ERROR: frame end cannot be before frame start
+-- 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
@@ -509,7 +509,7 @@ WINDOW w AS (
 ERROR:  frame end cannot be UNBOUNDED PRECEDING
 LINE 5:     ROWS BETWEEN CURRENT ROW AND UNBOUNDED PRECEDING
                                          ^
--- Expected: ERROR: frame end cannot be before frame start
+-- 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
@@ -594,14 +594,7 @@ ORDER BY id;
   6 |  30 |   1
 (6 rows)
 
--- RANGE: includes all rows with same ORDER BY value
--- Expected result: Includes peer rows (same val) in range calculation
--- id=1 (val=10): range [10,20] includes all val=10 and val=20 peers -> cnt=2 (only first in peer group gets match)
--- id=2,3 (val=10): already matched by id=1 -> cnt=0
--- id=4 (val=20): range [20,30] includes all val=20 and val=30 peers -> cnt=2
--- id=5 (val=20): already matched by id=4 -> cnt=0
--- id=6 (val=30): range [30,40] includes only val=30 -> cnt=1
--- Result: [2,0,0,2,0,1]
+-- RANGE frame with RPR (not permitted)
 SELECT id, val, COUNT(*) OVER w as cnt
 FROM rpr_frame
 WINDOW w AS (
@@ -616,14 +609,8 @@ ERROR:  FRAME option RANGE is not permitted when row pattern recognition is used
 LINE 5:     RANGE BETWEEN CURRENT ROW AND 10 FOLLOWING
             ^
 HINT:  Use: ROWS instead
--- GROUPS: treats rows with same value as one group (1 FOLLOWING = next group)
--- Expected result: 1 FOLLOWING means current group + 1 next group
--- id=1 (val=10): groups [val=10, val=20] -> cnt=2 (only first in group gets match)
--- id=2,3 (val=10): already matched by id=1 -> cnt=0
--- id=4 (val=20): groups [val=20, val=30] -> cnt=2
--- id=5 (val=20): already matched by id=4 -> cnt=0
--- id=6 (val=30): groups [val=30] (no next group) -> cnt=1
--- Result: [2,0,0,2,0,1]
+-- Expected: ERROR: FRAME option RANGE is not permitted when row pattern recognition is used
+-- GROUPS frame with RPR (not permitted)
 SELECT id, val, COUNT(*) OVER w as cnt
 FROM rpr_frame
 WINDOW w AS (
@@ -638,6 +625,7 @@ ERROR:  FRAME option GROUP is not permitted when row pattern recognition is used
 LINE 5:     GROUPS BETWEEN CURRENT ROW AND 1 FOLLOWING
             ^
 HINT:  Use: ROWS instead
+-- Expected: ERROR: FRAME option GROUP is not permitted when row pattern recognition is used
 DROP TABLE rpr_frame;
 -- ============================================================
 -- PARTITION BY + FRAME Tests
@@ -648,7 +636,6 @@ 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
--- Expected: Pattern matching should reset for each partition
 SELECT id, grp, val, COUNT(*) OVER w as cnt
 FROM rpr_partition
 WINDOW w AS (
@@ -670,8 +657,8 @@ ORDER BY id;
   6 |   2 |  35 |   0
 (6 rows)
 
+-- Expected: Pattern matching should reset for each partition
 -- PARTITION BY with RANGE frame
--- Expected: Each partition processed independently
 SELECT id, grp, val, COUNT(*) OVER w as cnt
 FROM rpr_partition
 WINDOW w AS (
@@ -687,6 +674,7 @@ ERROR:  FRAME option RANGE is not permitted when row pattern recognition is used
 LINE 6:     RANGE BETWEEN CURRENT ROW AND 10 FOLLOWING
             ^
 HINT:  Use: ROWS instead
+-- Expected: ERROR: FRAME option RANGE is not permitted when row pattern recognition is used
 DROP TABLE rpr_partition;
 -- ============================================================
 -- PATTERN Syntax Tests
@@ -2659,7 +2647,7 @@ WINDOW w AS (
 ERROR:  invalid input syntax for type integer: "string"
 LINE 7:     DEFINE A AS val > 'string'
                               ^
--- Expected: ERROR: operator does not exist or type mismatch
+-- Expected: ERROR: invalid input syntax for type integer: "string"
 -- Aggregate function in DEFINE (if not allowed)
 SELECT COUNT(*) OVER w
 FROM rpr_err
@@ -2922,7 +2910,7 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
          ->  Seq Scan on rpr_plan
 (6 rows)
 
--- ALT flatten: (A | (B | C)) -> (a | b | c)
+-- 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
@@ -3154,7 +3142,7 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
          ->  Seq Scan on rpr_plan
 (6 rows)
 
--- Combined optimization: A A (B B)+ B B C C C -> a{2} (b{2}){3,} c{3}
+-- 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
@@ -3170,7 +3158,7 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
          ->  Seq Scan on rpr_plan
 (6 rows)
 
--- Consecutive GROUP merge with unbounded: (A+){2} -> (a+){2}
+-- 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
@@ -3355,8 +3343,8 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
          ->  Seq Scan on rpr_plan
 (6 rows)
 
--- Unwrap GROUP{1,1}: (A | B | C) -> A | B | C
--- Tests tryUnwrapGroup removing redundant GROUP
+-- 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
@@ -4040,7 +4028,6 @@ ORDER BY id;
 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)
--- Expected: Fallback - pattern not merged due to min overflow (4000000000 > INT32_MAX)
 EXPLAIN (COSTS OFF)
 SELECT COUNT(*) OVER w FROM rpr_fallback
 WINDOW w AS (
@@ -4059,8 +4046,8 @@ WINDOW w AS (
          ->  Seq Scan on rpr_fallback
 (6 rows)
 
+-- Expected: Fallback - pattern not merged due to min overflow (4000000000 > INT32_MAX)
 -- Test: max-only quantifier overflow causes optimization fallback
--- Expected: Fallback - min OK (2*1=2), but max overflow (2*2000000000 > INT32_MAX)
 EXPLAIN (COSTS OFF)
 SELECT COUNT(*) OVER w FROM rpr_fallback
 WINDOW w AS (
@@ -4079,8 +4066,8 @@ WINDOW w AS (
          ->  Seq Scan on rpr_fallback
 (6 rows)
 
--- Test: min quantifier overflow causes optimization fallback (min != max case)
--- Expected: Fallback - pattern not merged due to min overflow
+-- 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 (
@@ -4092,8 +4079,8 @@ WINDOW w AS (
 ERROR:  quantifier bounds must be between 0 and 2147483646 with max >= 1
 LINE 6:     PATTERN ((A{2000000000,2147483647}){2})
                         ^
+-- Expected: ERROR at parse time before optimization
 -- Test: nested unbounded with large min causes overflow fallback
--- Expected: Fallback - min overflow (2000000000 * 2000000000 > INT32_MAX)
 EXPLAIN (COSTS OFF)
 SELECT COUNT(*) OVER w FROM rpr_fallback
 WINDOW w AS (
@@ -4112,8 +4099,8 @@ WINDOW w AS (
          ->  Seq Scan on rpr_fallback
 (6 rows)
 
+-- Expected: Fallback - min overflow (2000000000 * 2000000000 > INT32_MAX)
 -- Test: prefix mismatch causes optimization fallback
--- Expected: Fallback - prefix elements don't match GROUP content
 EXPLAIN (COSTS OFF)
 SELECT COUNT(*) OVER w FROM rpr_fallback
 WINDOW w AS (
@@ -4132,6 +4119,7 @@ WINDOW w AS (
          ->  Seq Scan on rpr_fallback
 (6 rows)
 
+-- Expected: Fallback - prefix elements don't match GROUP content
 DROP TABLE rpr_fallback;
 -- ============================================================
 -- Planner Integration Tests
@@ -4288,6 +4276,7 @@ ORDER BY category;
 ERROR:  syntax error at or near "GROUP"
 LINE 12: GROUP BY category
          ^
+-- Expected: ERROR (GROUP BY with window RPR not supported)
 -- ============================================================
 -- Subquery and CTE Tests
 -- Files: planner.c, prepjointree.c
@@ -5015,7 +5004,6 @@ DROP TABLE rpr_stress;
 CREATE TABLE rpr_errors (id INT, val INT);
 INSERT INTO rpr_errors VALUES (1, 10), (2, 20);
 -- Test: PATTERN variable without DEFINE (A), DEFINE variable not in PATTERN (B)
--- Expected: Success - A is implicitly TRUE, B is filtered out
 SELECT id, val, COUNT(*) OVER w FROM rpr_errors
 WINDOW w AS (
     ORDER BY id
@@ -5030,8 +5018,8 @@ WINDOW w AS (
   2 |  20 |     0
 (2 rows)
 
+-- Expected: Success - A is implicitly TRUE, B is filtered out
 -- Test: 3 variables in PATTERN, 253 in DEFINE (DEFINE filtering test)
--- Expected: Success - unused DEFINE variables are filtered out
 SELECT COUNT(*) OVER w FROM rpr_errors
 WINDOW w AS (
     ORDER BY id
@@ -5071,8 +5059,8 @@ WINDOW w AS (
      0
 (2 rows)
 
--- Test: 251 variables in PATTERN, 252 in DEFINE (boundary - should succeed)
 -- Expected: Success - unused DEFINE variables are filtered out
+-- Test: 251 variables in PATTERN, 252 in DEFINE (boundary - should succeed)
 SELECT COUNT(*) OVER w FROM rpr_errors
 WINDOW w AS (
     ORDER BY id
@@ -5112,8 +5100,8 @@ WINDOW w AS (
      0
 (2 rows)
 
+-- Expected: Success - unused DEFINE variables are filtered out
 -- Test: 252 variables in PATTERN, 251 in DEFINE (exceeds limit with implicit TRUE)
--- Expected: ERROR - too many pattern variables (Maximum is 251)
 SELECT COUNT(*) OVER w FROM rpr_errors
 WINDOW w AS (
     ORDER BY id
@@ -5149,8 +5137,8 @@ WINDOW w AS (
 );
 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)
--- Expected: Should succeed
 -- Note: 253 nested GROUP{3,7} quantifiers produce depth 253 after optimization
 SELECT id, val, COUNT(*) OVER w FROM rpr_errors
 WINDOW w AS (
@@ -5165,8 +5153,8 @@ WINDOW w AS (
   2 |  20 |     0
 (2 rows)
 
+-- Expected: Should succeed
 -- Test: Pattern nesting depth exceeds maximum (depth 254)
--- Expected: ERROR - pattern nesting too deep
 -- Note: 254 nested GROUP{3,7} quantifiers produce depth 254 after optimization
 SELECT id, val, COUNT(*) OVER w FROM rpr_errors
 WINDOW w AS (
@@ -5177,6 +5165,7 @@ WINDOW w AS (
 );
 ERROR:  pattern nesting too deep
 DETAIL:  Pattern nesting depth 254 exceeds maximum 253.
+-- Expected: ERROR - pattern nesting too deep
 DROP TABLE rpr_errors;
 -- ============================================================
 -- Jacob's Patterns
@@ -5398,6 +5387,31 @@ WINDOW w AS (
  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
 -- ============================================================
diff --git a/src/test/regress/expected/rpr_explain.out b/src/test/regress/expected/rpr_explain.out
index ea75d62718e..f23d06f6d59 100644
--- a/src/test/regress/expected/rpr_explain.out
+++ b/src/test/regress/expected/rpr_explain.out
@@ -134,10 +134,11 @@ WINDOW w AS (
    Pattern: a b
    Storage: Memory  Maximum Storage: NkB
    NFA States: 2 peak, 101 total, 0 merged
-   NFA Contexts: 3 peak, 101 total, 80 pruned
+   NFA Contexts: 2 peak, 101 total, 60 pruned
    NFA: 20 matched (len 2/2/2.0), 0 mismatched
+   NFA: 0 absorbed, 20 skipped (len 1/1/1.0)
    ->  Seq Scan on nfa_test (actual rows=100.00 loops=1)
-(8 rows)
+(9 rows)
 
 DROP VIEW rpr_v;
 -- Pattern with no matches - 0 matched
@@ -235,7 +236,7 @@ SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line
 (1 row)
 
 SELECT rpr_explain_filter('
-EXPLAIN (COSTS OFF)
+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 (
@@ -243,13 +244,18 @@ WINDOW w AS (
     PATTERN (A (B | C))
     DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
 );');
-                        rpr_explain_filter                         
--------------------------------------------------------------------
- WindowAgg
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ WindowAgg (actual rows=20.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a (b | c)
-   ->  Function Scan on generate_series s
-(4 rows)
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 3 peak, 28 total, 0 merged
+   NFA Contexts: 2 peak, 21 total, 6 pruned
+   NFA: 7 matched (len 2/2/2.0), 0 mismatched
+   NFA: 0 absorbed, 7 skipped (len 1/1/1.0)
+   ->  Function Scan on generate_series s (actual rows=20.00 loops=1)
+(9 rows)
 
 DROP VIEW rpr_v;
 -- Regression test: Sequential alternations at same depth
@@ -270,7 +276,7 @@ SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line
 (1 row)
 
 SELECT rpr_explain_filter('
-EXPLAIN (COSTS OFF)
+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 (
@@ -278,13 +284,17 @@ WINDOW w AS (
     PATTERN (A ((B | C) (D | E))*)
     DEFINE A AS v % 5 = 1, B AS v % 5 = 2, C AS v % 5 = 3, D AS v % 5 = 4, E AS v % 5 = 0
 );');
-                        rpr_explain_filter                         
--------------------------------------------------------------------
- WindowAgg
+                          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))*
-   ->  Function Scan on generate_series s
-(4 rows)
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 4 peak, 49 total, 0 merged
+   NFA Contexts: 3 peak, 31 total, 24 pruned
+   NFA: 6 matched (len 1/1/1.0), 0 mismatched
+   ->  Function Scan on generate_series s (actual rows=30.00 loops=1)
+(8 rows)
 
 DROP VIEW rpr_v;
 -- ============================================================
@@ -366,10 +376,11 @@ WINDOW w AS (
    Pattern: (a | b | c) (d | e)
    Storage: Memory  Maximum Storage: NkB
    NFA States: 5 peak, 363 total, 0 merged
-   NFA Contexts: 3 peak, 101 total, 40 pruned
+   NFA Contexts: 3 peak, 101 total, 20 pruned
    NFA: 20 matched (len 2/2/2.0), 40 mismatched (len 2/2/2.0)
+   NFA: 0 absorbed, 20 skipped (len 1/1/1.0)
    ->  Seq Scan on nfa_test (actual rows=100.00 loops=1)
-(8 rows)
+(9 rows)
 
 DROP VIEW rpr_v;
 -- Complex pattern with high state count
@@ -411,10 +422,11 @@ WINDOW w AS (
    Pattern: a+" b* c+
    Storage: Memory  Maximum Storage: NkB
    NFA States: 5 peak, 235 total, 0 merged
-   NFA Contexts: 3 peak, 101 total, 67 pruned
+   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)
-(8 rows)
+(9 rows)
 
 DROP VIEW rpr_v;
 -- Grouped pattern with quantifier - state merging
@@ -450,9 +462,9 @@ WINDOW w AS (
    Pattern: (a' b')+"
    Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 91 total, 0 merged
-   NFA Contexts: 3 peak, 61 total, 30 pruned
+   NFA Contexts: 2 peak, 61 total, 0 pruned
    NFA: 1 matched (len 60/60/60.0), 0 mismatched
-   NFA: 29 absorbed (len 1/1/1.0), 0 skipped
+   NFA: 29 absorbed (len 1/1/1.0), 30 skipped (len 1/1/1.0)
    ->  Function Scan on generate_series s (actual rows=60.00 loops=1)
 (9 rows)
 
@@ -490,8 +502,8 @@ WINDOW w AS (
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a | b){8}
    Storage: Memory  Maximum Storage: NkB
-   NFA States: 17 peak, 632 total, 0 merged
-   NFA Contexts: 9 peak, 101 total, 1 pruned
+   NFA States: 16 peak, 548 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)
@@ -516,7 +528,7 @@ SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line
 (1 row)
 
 SELECT rpr_explain_filter('
-EXPLAIN (COSTS OFF)
+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 (
@@ -525,13 +537,18 @@ WINDOW w AS (
     PATTERN ((A | B) (A | B) (C | D))
     DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
 );');
-                        rpr_explain_filter                         
--------------------------------------------------------------------
- WindowAgg
+                          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)
-   ->  Function Scan on generate_series s
-(4 rows)
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 6 peak, 111 total, 0 merged
+   NFA Contexts: 3 peak, 41 total, 12 pruned
+   NFA: 9 matched (len 3/3/3.0), 1 mismatched (len 2/2/2.0)
+   NFA: 0 absorbed, 18 skipped (len 1/2/1.5)
+   ->  Function Scan on generate_series s (actual rows=40.00 loops=1)
+(9 rows)
 
 DROP VIEW rpr_v;
 -- Consecutive ALT merge followed by non-ALT element
@@ -552,7 +569,7 @@ SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line
 (1 row)
 
 SELECT rpr_explain_filter('
-EXPLAIN (COSTS OFF)
+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 (
@@ -561,13 +578,18 @@ WINDOW w AS (
     PATTERN ((A | B) (A | B) C)
     DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2
 );');
-                        rpr_explain_filter                         
--------------------------------------------------------------------
- WindowAgg
+                          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
-   ->  Function Scan on generate_series s
-(4 rows)
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 5 peak, 109 total, 0 merged
+   NFA Contexts: 3 peak, 41 total, 2 pruned
+   NFA: 12 matched (len 3/3/3.0), 2 mismatched (len 2/2/2.0)
+   NFA: 0 absorbed, 24 skipped (len 1/2/1.5)
+   ->  Function Scan on generate_series s (actual rows=40.00 loops=1)
+(9 rows)
 
 DROP VIEW rpr_v;
 -- ALT prefix/suffix absorbed into GROUP: (A|B) (A|B)+ (A|B) -> (A|B){3,}
@@ -587,7 +609,7 @@ SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line
 (1 row)
 
 SELECT rpr_explain_filter('
-EXPLAIN (COSTS OFF)
+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 (
@@ -596,13 +618,18 @@ WINDOW w AS (
     PATTERN ((A | B) (A | B)+ (A | B))
     DEFINE A AS v % 2 = 0, B AS v % 2 = 1
 );');
-                        rpr_explain_filter                         
--------------------------------------------------------------------
- WindowAgg
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ WindowAgg (actual rows=40.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a | b){3,}
-   ->  Function Scan on generate_series s
-(4 rows)
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 6 peak, 161 total, 0 merged
+   NFA Contexts: 3 peak, 41 total, 0 pruned
+   NFA: 1 matched (len 40/40/40.0), 0 mismatched
+   NFA: 0 absorbed, 39 skipped (len 1/2/1.0)
+   ->  Function Scan on generate_series s (actual rows=40.00 loops=1)
+(9 rows)
 
 DROP VIEW rpr_v;
 -- High state merging - alternation with plus quantifier
@@ -638,9 +665,9 @@ WINDOW w AS (
    Pattern: (a | b | c)+ d
    Storage: Memory  Maximum Storage: NkB
    NFA States: 15 peak, 753 total, 0 merged
-   NFA Contexts: 5 peak, 101 total, 25 pruned
+   NFA Contexts: 4 peak, 101 total, 0 pruned
    NFA: 25 matched (len 4/4/4.0), 0 mismatched
-   NFA: 0 absorbed, 50 skipped (len 2/3/2.5)
+   NFA: 0 absorbed, 75 skipped (len 1/3/2.0)
    ->  Function Scan on generate_series s (actual rows=100.00 loops=1)
 (9 rows)
 
@@ -677,10 +704,10 @@ WINDOW w AS (
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a | b)+
    Storage: Memory  Maximum Storage: NkB
-   NFA States: 8 peak, 4002 total, 0 merged
-   NFA Contexts: 4 peak, 1001 total, 333 pruned
+   NFA States: 5 peak, 3336 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 2/2/2.0)
+   NFA: 0 absorbed, 333 skipped (len 1/1/1.0)
    ->  Function Scan on generate_series s (actual rows=1000.00 loops=1)
 (9 rows)
 
@@ -721,9 +748,9 @@ WINDOW w AS (
    Pattern: a+" b
    Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 91 total, 0 merged
-   NFA Contexts: 3 peak, 51 total, 10 pruned
+   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), 0 skipped
+   NFA: 30 absorbed (len 1/1/1.0), 10 skipped (len 1/1/1.0)
    ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
 (9 rows)
 
@@ -761,9 +788,9 @@ WINDOW w AS (
    Pattern: a{2,4} b
    Storage: Memory  Maximum Storage: NkB
    NFA States: 7 peak, 101 total, 0 merged
-   NFA Contexts: 6 peak, 51 total, 10 pruned
-   NFA: 10 matched (len 5/5/5.0), 10 mismatched (len 2/2/2.0)
-   NFA: 0 absorbed, 20 skipped (len 3/4/3.5)
+   NFA Contexts: 5 peak, 51 total, 0 pruned
+   NFA: 10 matched (len 5/5/5.0), 0 mismatched
+   NFA: 0 absorbed, 40 skipped (len 1/4/2.5)
    ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
 (9 rows)
 
@@ -801,10 +828,11 @@ WINDOW w AS (
    Pattern: a b c
    Storage: Memory  Maximum Storage: NkB
    NFA States: 2 peak, 101 total, 0 merged
-   NFA Contexts: 3 peak, 101 total, 90 pruned
+   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)
-(8 rows)
+(9 rows)
 
 DROP VIEW rpr_v;
 -- High context absorption - unbounded group
@@ -840,10 +868,11 @@ WINDOW w AS (
    Pattern: (a' b')+" c
    Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 134 total, 0 merged
-   NFA Contexts: 3 peak, 101 total, 67 pruned
+   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)
-(8 rows)
+(9 rows)
 
 DROP VIEW rpr_v;
 -- ============================================================
@@ -886,10 +915,11 @@ WINDOW w AS (
    Pattern: a b c d e
    Storage: Memory  Maximum Storage: NkB
    NFA States: 2 peak, 101 total, 0 merged
-   NFA Contexts: 3 peak, 101 total, 80 pruned
+   NFA Contexts: 3 peak, 101 total, 60 pruned
    NFA: 20 matched (len 5/5/5.0), 0 mismatched
+   NFA: 0 absorbed, 20 skipped (len 1/1/1.0)
    ->  Seq Scan on nfa_test (actual rows=100.00 loops=1)
-(8 rows)
+(9 rows)
 
 DROP VIEW rpr_v;
 -- Variable length matches - min/max/avg differ
@@ -925,9 +955,9 @@ WINDOW w AS (
    Pattern: a+" b
    Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 191 total, 0 merged
-   NFA Contexts: 3 peak, 101 total, 10 pruned
+   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), 0 skipped
+   NFA: 80 absorbed (len 1/1/1.0), 10 skipped (len 1/1/1.0)
    ->  Function Scan on generate_series s (actual rows=100.00 loops=1)
 (9 rows)
 
@@ -965,9 +995,9 @@ WINDOW w AS (
    Pattern: a+" b
    Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 396 total, 0 merged
-   NFA Contexts: 3 peak, 201 total, 5 pruned
+   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), 0 skipped
+   NFA: 194 absorbed (len 1/1/1.0), 1 skipped (len 1/1/1.0)
    ->  Function Scan on generate_series s (actual rows=200.00 loops=1)
 (9 rows)
 
@@ -1009,9 +1039,9 @@ WINDOW w AS (
    Pattern: a+" b
    Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 171 total, 0 merged
-   NFA Contexts: 3 peak, 101 total, 30 pruned
+   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), 0 skipped
+   NFA: 60 absorbed (len 1/1/1.0), 5 skipped (len 1/1/1.0)
    ->  Function Scan on generate_series s (actual rows=100.00 loops=1)
 (9 rows)
 
@@ -1067,9 +1097,9 @@ WINDOW w AS (
    Pattern: a+" b+ c
    Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 151 total, 0 merged
-   NFA Contexts: 3 peak, 101 total, 70 pruned
+   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), 0 skipped
+   NFA: 20 absorbed (len 1/1/1.0), 10 skipped (len 1/1/1.0)
    ->  Function Scan on generate_series s (actual rows=100.00 loops=1)
 (9 rows)
 
@@ -1131,9 +1161,9 @@ WINDOW w AS (
    Pattern: a+" b+ c
    Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 115 total, 0 merged
-   NFA Contexts: 3 peak, 61 total, 16 pruned
+   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), 0 skipped
+   NFA: 42 absorbed (len 1/1/1.0), 1 skipped (len 1/1/1.0)
    ->  Function Scan on generate_series i (actual rows=60.00 loops=1)
 (9 rows)
 
@@ -1188,13 +1218,16 @@ WINDOW w AS (
        "NFA Contexts Peak": 3,                                             +
        "NFA Contexts Total": 51,                                           +
        "NFA Contexts Absorbed": 0,                                         +
-       "NFA Contexts Skipped": 0,                                          +
-       "NFA Contexts Pruned": 33,                                          +
+       "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",                                   +
@@ -1260,11 +1293,11 @@ WINDOW w AS (
        "NFA States Peak": 3,                                               +
        "NFA States Total": 191,                                            +
        "NFA States Merged": 0,                                             +
-       "NFA Contexts Peak": 3,                                             +
+       "NFA Contexts Peak": 2,                                             +
        "NFA Contexts Total": 101,                                          +
        "NFA Contexts Absorbed": 80,                                        +
-       "NFA Contexts Skipped": 0,                                          +
-       "NFA Contexts Pruned": 10,                                          +
+       "NFA Contexts Skipped": 10,                                         +
+       "NFA Contexts Pruned": 0,                                           +
        "NFA Matched": 10,                                                  +
        "NFA Mismatched": 0,                                                +
        "NFA Match Length Min": 10,                                         +
@@ -1273,6 +1306,9 @@ WINDOW w AS (
        "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",                                   +
@@ -1341,16 +1377,19 @@ WINDOW w AS (
        <NFA-States-Peak>2</NFA-States-Peak>                                    +
        <NFA-States-Total>31</NFA-States-Total>                                 +
        <NFA-States-Merged>0</NFA-States-Merged>                                +
-       <NFA-Contexts-Peak>3</NFA-Contexts-Peak>                                +
+       <NFA-Contexts-Peak>2</NFA-Contexts-Peak>                                +
        <NFA-Contexts-Total>31</NFA-Contexts-Total>                             +
        <NFA-Contexts-Absorbed>0</NFA-Contexts-Absorbed>                        +
-       <NFA-Contexts-Skipped>0</NFA-Contexts-Skipped>                          +
-       <NFA-Contexts-Pruned>15</NFA-Contexts-Pruned>                           +
+       <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>                                +
@@ -1420,8 +1459,8 @@ WINDOW w AS (
        "NFA Contexts Peak": 3,                                             +
        "NFA Contexts Total": 10,                                           +
        "NFA Contexts Absorbed": 0,                                         +
-       "NFA Contexts Skipped": 0,                                          +
-       "NFA Contexts Pruned": 6,                                           +
+       "NFA Contexts Skipped": 1,                                          +
+       "NFA Contexts Pruned": 5,                                           +
        "NFA Matched": 1,                                                   +
        "NFA Mismatched": 2,                                                +
        "NFA Match Length Min": 3,                                          +
@@ -1430,6 +1469,9 @@ WINDOW w AS (
        "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",                                     +
@@ -1492,10 +1534,10 @@ WINDOW w AS (
        "Pattern": "(a | b){8}",                                            +
        "Storage": "Memory",                                                +
        "Maximum Storage": 0,                                               +
-       "NFA States Peak": 17,                                              +
-       "NFA States Total": 632,                                            +
+       "NFA States Peak": 16,                                              +
+       "NFA States Total": 548,                                            +
        "NFA States Merged": 0,                                             +
-       "NFA Contexts Peak": 9,                                             +
+       "NFA Contexts Peak": 8,                                             +
        "NFA Contexts Total": 101,                                          +
        "NFA Contexts Absorbed": 0,                                         +
        "NFA Contexts Skipped": 84,                                         +
@@ -1578,9 +1620,9 @@ WINDOW w AS (
    Pattern: a+" b
    Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 165 total, 0 merged
-   NFA Contexts: 3 peak, 93 total, 18 pruned
+   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), 0 skipped
+   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: 27kB
@@ -1635,9 +1677,9 @@ WINDOW w AS (
    Pattern: a+" b
    Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 77 total, 0 merged
-   NFA Contexts: 3 peak, 52 total, 26 pruned
+   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), 0 skipped
+   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: 26kB
@@ -1841,9 +1883,9 @@ WINDOW w AS (
    Pattern: (a' b' c')+"
    Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 81 total, 0 merged
-   NFA Contexts: 3 peak, 61 total, 40 pruned
+   NFA Contexts: 3 peak, 61 total, 20 pruned
    NFA: 1 matched (len 60/60/60.0), 0 mismatched
-   NFA: 19 absorbed (len 1/1/1.0), 0 skipped
+   NFA: 19 absorbed (len 1/1/1.0), 20 skipped (len 1/1/1.0)
    ->  Function Scan on generate_series s (actual rows=60.00 loops=1)
 (9 rows)
 
@@ -1885,10 +1927,11 @@ WINDOW w AS (
    Pattern: (a | b) (c | d | e)
    Storage: Memory  Maximum Storage: NkB
    NFA States: 5 peak, 282 total, 0 merged
-   NFA Contexts: 3 peak, 101 total, 60 pruned
+   NFA Contexts: 3 peak, 101 total, 40 pruned
    NFA: 20 matched (len 2/2/2.0), 20 mismatched (len 2/2/2.0)
+   NFA: 0 absorbed, 20 skipped (len 1/1/1.0)
    ->  Seq Scan on nfa_test (actual rows=100.00 loops=1)
-(8 rows)
+(9 rows)
 
 DROP VIEW rpr_v;
 -- Optional elements
@@ -1924,10 +1967,11 @@ WINDOW w AS (
    Pattern: a b? c
    Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 64 total, 0 merged
-   NFA Contexts: 3 peak, 51 total, 37 pruned
+   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)
-(8 rows)
+(9 rows)
 
 DROP VIEW rpr_v;
 -- Bounded quantifiers
@@ -1963,9 +2007,9 @@ WINDOW w AS (
    Pattern: a{2,5} b
    Storage: Memory  Maximum Storage: NkB
    NFA States: 9 peak, 311 total, 0 merged
-   NFA Contexts: 7 peak, 101 total, 10 pruned
-   NFA: 10 matched (len 6/6/6.0), 50 mismatched (len 2/6/5.2)
-   NFA: 0 absorbed, 30 skipped (len 3/5/4.0)
+   NFA Contexts: 7 peak, 101 total, 0 pruned
+   NFA: 10 matched (len 6/6/6.0), 40 mismatched (len 6/6/6.0)
+   NFA: 0 absorbed, 50 skipped (len 1/5/3.0)
    ->  Function Scan on generate_series s (actual rows=100.00 loops=1)
 (9 rows)
 
@@ -2003,10 +2047,11 @@ WINDOW w AS (
    Pattern: a b* c
    Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 91 total, 0 merged
-   NFA Contexts: 3 peak, 51 total, 45 pruned
+   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)
-(8 rows)
+(9 rows)
 
 DROP VIEW rpr_v;
 -- ============================================================
@@ -2045,9 +2090,9 @@ WINDOW w AS (
    Pattern: d+" u+
    Storage: Memory  Maximum Storage: NkB
    NFA States: 4 peak, 58 total, 0 merged
-   NFA Contexts: 3 peak, 31 total, 17 pruned
+   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), 0 skipped
+   NFA: 9 absorbed (len 1/1/1.0), 14 skipped (len 1/1/1.0)
    ->  Seq Scan on nfa_complex (actual rows=30.00 loops=1)
 (9 rows)
 
@@ -2085,9 +2130,9 @@ WINDOW w AS (
    Pattern: u+" s* d+
    Storage: Memory  Maximum Storage: NkB
    NFA States: 5 peak, 76 total, 0 merged
-   NFA Contexts: 3 peak, 31 total, 14 pruned
+   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), 0 skipped
+   NFA: 12 absorbed (len 1/1/1.0), 13 skipped (len 1/1/1.0)
    ->  Seq Scan on nfa_complex (actual rows=30.00 loops=1)
 (9 rows)
 
@@ -2168,10 +2213,11 @@ WINDOW w AS (
    Pattern: a b
    Storage: Memory  Maximum Storage: NkB
    NFA States: 2 peak, 1001 total, 0 merged
-   NFA Contexts: 3 peak, 1001 total, 500 pruned
+   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)
-(8 rows)
+(9 rows)
 
 DROP VIEW rpr_v;
 -- Large dataset with absorption
@@ -2207,9 +2253,9 @@ WINDOW w AS (
    Pattern: a+" b
    Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 1991 total, 0 merged
-   NFA Contexts: 3 peak, 1001 total, 10 pruned
+   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), 0 skipped
+   NFA: 980 absorbed (len 1/1/1.0), 10 skipped (len 1/1/1.0)
    ->  Function Scan on generate_series s (actual rows=1000.00 loops=1)
 (9 rows)
 
@@ -2247,9 +2293,9 @@ WINDOW w AS (
    Pattern: (a | b)+ c
    Storage: Memory  Maximum Storage: NkB
    NFA States: 8 peak, 2004 total, 0 merged
-   NFA Contexts: 4 peak, 501 total, 167 pruned
+   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, 166 skipped (len 2/2/2.0)
+   NFA: 0 absorbed, 332 skipped (len 1/2/1.5)
    ->  Function Scan on generate_series s (actual rows=500.00 loops=1)
 (9 rows)
 
@@ -2292,9 +2338,9 @@ WINDOW w AS (
    Pattern: a+" b
    Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 91 total, 0 merged
-   NFA Contexts: 3 peak, 51 total, 10 pruned
+   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), 0 skipped
+   NFA: 30 absorbed (len 1/1/1.0), 10 skipped (len 1/1/1.0)
    ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
 (9 rows)
 
@@ -2332,9 +2378,9 @@ WINDOW w AS (
    Pattern: a+" b
    Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 91 total, 0 merged
-   NFA Contexts: 3 peak, 51 total, 10 pruned
+   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), 0 skipped
+   NFA: 30 absorbed (len 1/1/1.0), 10 skipped (len 1/1/1.0)
    ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
 (9 rows)
 
@@ -2455,9 +2501,9 @@ WINDOW w AS (
    Pattern: a? b c
    Storage: Memory  Maximum Storage: NkB
    NFA States: 4 peak, 82 total, 0 merged
-   NFA Contexts: 4 peak, 41 total, 20 pruned
+   NFA Contexts: 3 peak, 41 total, 10 pruned
    NFA: 10 matched (len 3/3/3.0), 0 mismatched
-   NFA: 0 absorbed, 10 skipped (len 2/2/2.0)
+   NFA: 0 absorbed, 20 skipped (len 1/2/1.5)
    ->  Function Scan on generate_series s (actual rows=40.00 loops=1)
 (9 rows)
 
@@ -2495,10 +2541,11 @@ WINDOW w AS (
    Pattern: a{3} b
    Storage: Memory  Maximum Storage: NkB
    NFA States: 4 peak, 51 total, 0 merged
-   NFA Contexts: 5 peak, 51 total, 10 pruned
-   NFA: 10 matched (len 4/4/4.0), 30 mismatched (len 2/4/3.0)
+   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)
-(8 rows)
+(9 rows)
 
 DROP VIEW rpr_v;
 -- Range {n,m}
@@ -2534,9 +2581,9 @@ WINDOW w AS (
    Pattern: a{2,4} b
    Storage: Memory  Maximum Storage: NkB
    NFA States: 7 peak, 101 total, 0 merged
-   NFA Contexts: 6 peak, 51 total, 10 pruned
-   NFA: 10 matched (len 5/5/5.0), 10 mismatched (len 2/2/2.0)
-   NFA: 0 absorbed, 20 skipped (len 3/4/3.5)
+   NFA Contexts: 5 peak, 51 total, 0 pruned
+   NFA: 10 matched (len 5/5/5.0), 0 mismatched
+   NFA: 0 absorbed, 40 skipped (len 1/4/2.5)
    ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
 (9 rows)
 
@@ -2574,9 +2621,9 @@ WINDOW w AS (
    Pattern: a{3,}" b
    Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 86 total, 0 merged
-   NFA Contexts: 3 peak, 51 total, 5 pruned
+   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), 0 skipped
+   NFA: 40 absorbed (len 1/1/1.0), 5 skipped (len 1/1/1.0)
    ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
 (9 rows)
 
@@ -2618,9 +2665,9 @@ WINDOW w AS (
    Pattern: a+" b
    Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 37 total, 0 merged
-   NFA Contexts: 3 peak, 21 total, 4 pruned
+   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), 0 skipped
+   NFA: 12 absorbed (len 1/1/1.0), 4 skipped (len 1/1/1.0)
    ->  Function Scan on generate_series s (actual rows=20.00 loops=1)
 (9 rows)
 
@@ -2658,9 +2705,9 @@ WINDOW w AS (
    Pattern: a+" b c
    Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 52 total, 0 merged
-   NFA Contexts: 3 peak, 31 total, 9 pruned
+   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), 0 skipped
+   NFA: 18 absorbed (len 1/1/1.0), 3 skipped (len 1/1/1.0)
    ->  Function Scan on generate_series s (actual rows=30.00 loops=1)
 (9 rows)
 
@@ -2698,10 +2745,11 @@ WINDOW w AS (
    Pattern: a b c
    Storage: Memory  Maximum Storage: NkB
    NFA States: 2 peak, 31 total, 0 merged
-   NFA Contexts: 3 peak, 31 total, 20 pruned
+   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)
-(8 rows)
+(9 rows)
 
 DROP VIEW rpr_v;
 -- ============================================================
@@ -2740,10 +2788,11 @@ WINDOW w AS (
    Pattern: (a | b) c
    Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 202 total, 0 merged
-   NFA Contexts: 3 peak, 101 total, 60 pruned
+   NFA Contexts: 3 peak, 101 total, 40 pruned
    NFA: 20 matched (len 2/2/2.0), 20 mismatched (len 2/2/2.0)
+   NFA: 0 absorbed, 20 skipped (len 1/1/1.0)
    ->  Seq Scan on nfa_test (actual rows=100.00 loops=1)
-(8 rows)
+(9 rows)
 
 DROP VIEW rpr_v;
 -- Multiple items in alternation
@@ -2783,10 +2832,11 @@ WINDOW w AS (
    Pattern: (a | b | c | d) e
    Storage: Memory  Maximum Storage: NkB
    NFA States: 5 peak, 404 total, 0 merged
-   NFA Contexts: 3 peak, 101 total, 20 pruned
+   NFA Contexts: 3 peak, 101 total, 0 pruned
    NFA: 20 matched (len 2/2/2.0), 60 mismatched (len 2/2/2.0)
+   NFA: 0 absorbed, 20 skipped (len 1/1/1.0)
    ->  Seq Scan on nfa_test (actual rows=100.00 loops=1)
-(8 rows)
+(9 rows)
 
 DROP VIEW rpr_v;
 -- Alternation with quantifiers
@@ -2822,9 +2872,9 @@ WINDOW w AS (
    Pattern: (a | b)+ c
    Storage: Memory  Maximum Storage: NkB
    NFA States: 8 peak, 204 total, 0 merged
-   NFA Contexts: 4 peak, 51 total, 17 pruned
+   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, 16 skipped (len 2/2/2.0)
+   NFA: 0 absorbed, 32 skipped (len 1/2/1.5)
    ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
 (9 rows)
 
@@ -2845,7 +2895,7 @@ SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line
 (1 row)
 
 SELECT rpr_explain_filter('
-EXPLAIN (COSTS OFF)
+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 (
@@ -2853,13 +2903,17 @@ WINDOW w AS (
     PATTERN (A | B | C | D | E)
     DEFINE A AS v % 5 = 0, B AS v % 5 = 1, C AS v % 5 = 2, D AS v % 5 = 3, E AS v % 5 = 4
 );');
-                        rpr_explain_filter                         
--------------------------------------------------------------------
- WindowAgg
+                          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)
-   ->  Function Scan on generate_series s
-(4 rows)
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 6 peak, 505 total, 0 merged
+   NFA Contexts: 2 peak, 101 total, 0 pruned
+   NFA: 100 matched (len 1/1/1.0), 0 mismatched
+   ->  Function Scan on generate_series s (actual rows=100.00 loops=1)
+(8 rows)
 
 DROP VIEW rpr_v;
 -- Alternation at start
@@ -2878,7 +2932,7 @@ SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line
 (1 row)
 
 SELECT rpr_explain_filter('
-EXPLAIN (COSTS OFF)
+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 (
@@ -2886,13 +2940,18 @@ WINDOW w AS (
     PATTERN ((A | B) C D)
     DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
 );');
-                        rpr_explain_filter                         
--------------------------------------------------------------------
- WindowAgg
+                          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
-   ->  Function Scan on generate_series s
-(4 rows)
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 3 peak, 122 total, 0 merged
+   NFA Contexts: 3 peak, 61 total, 16 pruned
+   NFA: 15 matched (len 3/3/3.0), 14 mismatched (len 2/2/2.0)
+   NFA: 0 absorbed, 15 skipped (len 1/1/1.0)
+   ->  Function Scan on generate_series s (actual rows=60.00 loops=1)
+(9 rows)
 
 DROP VIEW rpr_v;
 -- Multiple sequential alternations
@@ -2911,7 +2970,7 @@ SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line
 (1 row)
 
 SELECT rpr_explain_filter('
-EXPLAIN (COSTS OFF)
+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 (
@@ -2919,13 +2978,17 @@ WINDOW w AS (
     PATTERN ((A | B) C (D | E) F)
     DEFINE A AS v % 6 = 0, B AS v % 6 = 1, C AS v % 6 = 2, D AS v % 6 = 3, E AS v % 6 = 4, F AS v % 6 = 5
 );');
-                        rpr_explain_filter                         
--------------------------------------------------------------------
- WindowAgg
+                          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
-   ->  Function Scan on generate_series s
-(4 rows)
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 4 peak, 219 total, 0 merged
+   NFA Contexts: 3 peak, 101 total, 67 pruned
+   NFA: 0 matched, 33 mismatched (len 2/4/3.0)
+   ->  Function Scan on generate_series s (actual rows=100.00 loops=1)
+(8 rows)
 
 DROP VIEW rpr_v;
 -- Quantified alternatives
@@ -2944,7 +3007,7 @@ SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line
 (1 row)
 
 SELECT rpr_explain_filter('
-EXPLAIN (COSTS OFF)
+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 (
@@ -2952,13 +3015,18 @@ WINDOW w AS (
     PATTERN ((A+ | B+) C)
     DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2
 );');
-                        rpr_explain_filter                         
--------------------------------------------------------------------
- WindowAgg
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ WindowAgg (actual rows=60.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a+" | b+") c
-   ->  Function Scan on generate_series s
-(4 rows)
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 4 peak, 162 total, 0 merged
+   NFA Contexts: 3 peak, 61 total, 1 pruned
+   NFA: 20 matched (len 2/2/2.0), 19 mismatched (len 2/2/2.0)
+   NFA: 0 absorbed, 20 skipped (len 1/1/1.0)
+   ->  Function Scan on generate_series s (actual rows=60.00 loops=1)
+(9 rows)
 
 DROP VIEW rpr_v;
 -- Alternation at end
@@ -2977,7 +3045,7 @@ SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line
 (1 row)
 
 SELECT rpr_explain_filter('
-EXPLAIN (COSTS OFF)
+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 (
@@ -2985,13 +3053,18 @@ WINDOW w AS (
     PATTERN (A B (C | D))
     DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
 );');
-                        rpr_explain_filter                         
--------------------------------------------------------------------
- WindowAgg
+                          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)
-   ->  Function Scan on generate_series s
-(4 rows)
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 3 peak, 75 total, 0 merged
+   NFA Contexts: 3 peak, 61 total, 32 pruned
+   NFA: 14 matched (len 3/3/3.0), 0 mismatched
+   NFA: 0 absorbed, 14 skipped (len 1/1/1.0)
+   ->  Function Scan on generate_series s (actual rows=60.00 loops=1)
+(9 rows)
 
 DROP VIEW rpr_v;
 -- Nested ALT at start of branch inside outer ALT
@@ -3011,7 +3084,7 @@ SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line
 (1 row)
 
 SELECT rpr_explain_filter('
-EXPLAIN (COSTS OFF)
+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 (
@@ -3019,13 +3092,17 @@ WINDOW w AS (
     PATTERN (A ((B | C) D | E))
     DEFINE A AS v % 5 = 0, B AS v % 5 = 1, C AS v % 5 = 2, D AS v % 5 = 3, E AS v % 5 = 4
 );');
-                        rpr_explain_filter                         
--------------------------------------------------------------------
- WindowAgg
+                          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)
-   ->  Function Scan on generate_series s
-(4 rows)
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 4 peak, 29 total, 0 merged
+   NFA Contexts: 3 peak, 21 total, 17 pruned
+   NFA: 0 matched, 3 mismatched (len 3/3/3.0)
+   ->  Function Scan on generate_series s (actual rows=20.00 loops=1)
+(8 rows)
 
 DROP VIEW rpr_v;
 -- Nested ALT at end of branch inside outer ALT
@@ -3045,7 +3122,7 @@ SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line
 (1 row)
 
 SELECT rpr_explain_filter('
-EXPLAIN (COSTS OFF)
+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 (
@@ -3053,13 +3130,17 @@ WINDOW w AS (
     PATTERN (C (A | B) | D)
     DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
 );');
-                        rpr_explain_filter                         
--------------------------------------------------------------------
- WindowAgg
+                          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)
-   ->  Function Scan on generate_series s
-(4 rows)
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 4 peak, 47 total, 0 merged
+   NFA Contexts: 3 peak, 21 total, 10 pruned
+   NFA: 5 matched (len 1/1/1.0), 5 mismatched (len 2/2/2.0)
+   ->  Function Scan on generate_series s (actual rows=20.00 loops=1)
+(8 rows)
 
 DROP VIEW rpr_v;
 -- ============================================================
@@ -3098,9 +3179,9 @@ WINDOW w AS (
    Pattern: (a' b')+"
    Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 61 total, 0 merged
-   NFA Contexts: 3 peak, 41 total, 20 pruned
+   NFA Contexts: 2 peak, 41 total, 0 pruned
    NFA: 1 matched (len 40/40/40.0), 0 mismatched
-   NFA: 19 absorbed (len 1/1/1.0), 0 skipped
+   NFA: 19 absorbed (len 1/1/1.0), 20 skipped (len 1/1/1.0)
    ->  Function Scan on generate_series s (actual rows=40.00 loops=1)
 (9 rows)
 
@@ -3137,10 +3218,10 @@ WINDOW w AS (
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a b){2,4}
    Storage: Memory  Maximum Storage: NkB
-   NFA States: 7 peak, 66 total, 0 merged
-   NFA Contexts: 6 peak, 41 total, 20 pruned
+   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, 15 skipped (len 2/6/4.0)
+   NFA: 0 absorbed, 30 skipped (len 1/2/1.5)
    ->  Function Scan on generate_series s (actual rows=40.00 loops=1)
 (9 rows)
 
@@ -3177,10 +3258,10 @@ WINDOW w AS (
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: ((a b){2})+
    Storage: Memory  Maximum Storage: NkB
-   NFA States: 60 peak, 286 total, 0 merged
-   NFA Contexts: 32 peak, 61 total, 30 pruned
-   NFA: 1 matched (len 60/60/60.0), 1 mismatched (len 2/2/2.0)
-   NFA: 0 absorbed, 28 skipped (len 4/58/31.0)
+   NFA States: 5 peak, 76 total, 0 merged
+   NFA Contexts: 4 peak, 61 total, 15 pruned
+   NFA: 1 matched (len 60/60/60.0), 0 mismatched
+   NFA: 0 absorbed, 44 skipped (len 1/4/2.3)
    ->  Function Scan on generate_series s (actual rows=60.00 loops=1)
 (9 rows)
 
@@ -3201,7 +3282,7 @@ SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line
 (1 row)
 
 SELECT rpr_explain_filter('
-EXPLAIN (COSTS OFF)
+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 (
@@ -3209,13 +3290,18 @@ WINDOW w AS (
     PATTERN ((((A | B)+)+)+)
     DEFINE A AS v % 2 = 0, B AS v % 2 = 1
 );');
-                        rpr_explain_filter                         
--------------------------------------------------------------------
- WindowAgg
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ WindowAgg (actual rows=40.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a | b)+
-   ->  Function Scan on generate_series s
-(4 rows)
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 5 peak, 162 total, 0 merged
+   NFA Contexts: 2 peak, 41 total, 0 pruned
+   NFA: 1 matched (len 40/40/40.0), 0 mismatched
+   NFA: 0 absorbed, 39 skipped (len 1/1/1.0)
+   ->  Function Scan on generate_series s (actual rows=40.00 loops=1)
+(9 rows)
 
 DROP VIEW rpr_v;
 -- Bounded quantifier on alternation
@@ -3234,7 +3320,7 @@ SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line
 (1 row)
 
 SELECT rpr_explain_filter('
-EXPLAIN (COSTS OFF)
+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 (
@@ -3242,13 +3328,18 @@ WINDOW w AS (
     PATTERN ((A | B){2,3} C)
     DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2
 );');
-                        rpr_explain_filter                         
--------------------------------------------------------------------
- WindowAgg
+                          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
-   ->  Function Scan on generate_series s
-(4 rows)
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 7 peak, 200 total, 0 merged
+   NFA Contexts: 3 peak, 61 total, 2 pruned
+   NFA: 19 matched (len 3/3/3.0), 1 mismatched (len 2/2/2.0)
+   NFA: 0 absorbed, 38 skipped (len 1/2/1.5)
+   ->  Function Scan on generate_series s (actual rows=60.00 loops=1)
+(9 rows)
 
 DROP VIEW rpr_v;
 -- Nested groups with quantifiers
@@ -3267,7 +3358,7 @@ SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line
 (1 row)
 
 SELECT rpr_explain_filter('
-EXPLAIN (COSTS OFF)
+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 (
@@ -3275,13 +3366,18 @@ WINDOW w AS (
     PATTERN (((A B)+ C)*)
     DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2
 );');
-                        rpr_explain_filter                         
--------------------------------------------------------------------
- WindowAgg
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ WindowAgg (actual rows=60.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: ((a' b')+" c)*
-   ->  Function Scan on generate_series s
-(4 rows)
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 7 peak, 178 total, 0 merged
+   NFA Contexts: 4 peak, 61 total, 22 pruned
+   NFA: 1 matched (len 57/57/57.0), 0 mismatched
+   NFA: 0 absorbed, 37 skipped (len 1/3/2.0)
+   ->  Function Scan on generate_series s (actual rows=60.00 loops=1)
+(9 rows)
 
 DROP VIEW rpr_v;
 -- Partial nested quantification
@@ -3300,7 +3396,7 @@ SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line
 (1 row)
 
 SELECT rpr_explain_filter('
-EXPLAIN (COSTS OFF)
+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 (
@@ -3308,13 +3404,18 @@ WINDOW w AS (
     PATTERN ((A (B C)+)*)
     DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2
 );');
-                        rpr_explain_filter                         
--------------------------------------------------------------------
- WindowAgg
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ WindowAgg (actual rows=60.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: (a (b c)+)*
-   ->  Function Scan on generate_series s
-(4 rows)
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 5 peak, 160 total, 0 merged
+   NFA Contexts: 4 peak, 61 total, 22 pruned
+   NFA: 1 matched (len 57/57/57.0), 0 mismatched
+   NFA: 0 absorbed, 37 skipped (len 1/3/2.0)
+   ->  Function Scan on generate_series s (actual rows=60.00 loops=1)
+(9 rows)
 
 DROP VIEW rpr_v;
 -- ============================================================
@@ -3353,9 +3454,9 @@ WINDOW w AS (
    Pattern: a+" b
    Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 55 total, 0 merged
-   NFA Contexts: 3 peak, 31 total, 6 pruned
+   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), 0 skipped
+   NFA: 18 absorbed (len 1/1/1.0), 6 skipped (len 1/1/1.0)
    ->  Function Scan on generate_series s (actual rows=30.00 loops=1)
 (9 rows)
 
@@ -3393,9 +3494,9 @@ WINDOW w AS (
    Pattern: a+" b
    Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 55 total, 0 merged
-   NFA Contexts: 3 peak, 31 total, 6 pruned
+   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), 0 skipped
+   NFA: 18 absorbed (len 1/1/1.0), 6 skipped (len 1/1/1.0)
    ->  Function Scan on generate_series s (actual rows=30.00 loops=1)
 (9 rows)
 
@@ -3433,9 +3534,9 @@ WINDOW w AS (
    Pattern: a+" b
    Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 55 total, 0 merged
-   NFA Contexts: 3 peak, 31 total, 6 pruned
+   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), 0 skipped
+   NFA: 18 absorbed (len 1/1/1.0), 6 skipped (len 1/1/1.0)
    ->  Function Scan on generate_series s (actual rows=30.00 loops=1)
 (9 rows)
 
@@ -3479,9 +3580,9 @@ WINDOW w AS (
    Pattern: a+" b
    Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 55 total, 0 merged
-   NFA Contexts: 3 peak, 31 total, 6 pruned
+   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), 0 skipped
+   NFA: 18 absorbed (len 1/1/1.0), 6 skipped (len 1/1/1.0)
    ->  Function Scan on generate_series s (actual rows=30.00 loops=1)
 (9 rows)
 
@@ -3526,9 +3627,9 @@ WINDOW w AS (
    Pattern: a+" b
    Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 78 total, 0 merged
-   NFA Contexts: 3 peak, 51 total, 23 pruned
+   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), 0 skipped
+   NFA: 10 absorbed (len 1/1/1.0), 17 skipped (len 1/1/1.0)
    ->  Function Scan on generate_series s (actual rows=50.00 loops=1)
 (9 rows)
 
@@ -3617,9 +3718,9 @@ WINDOW w AS (
    Pattern: a+" b
    Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 55 total, 0 merged
-   NFA Contexts: 3 peak, 31 total, 6 pruned
+   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), 0 skipped
+   NFA: 18 absorbed (len 1/1/1.0), 6 skipped (len 1/1/1.0)
    ->  Function Scan on generate_series v (actual rows=30.00 loops=1)
 (9 rows)
 
@@ -3660,9 +3761,9 @@ WINDOW w AS (
    Pattern: a+" b c
    Storage: Memory  Maximum Storage: NkB
    NFA States: 3 peak, 851 total, 0 merged
-   NFA Contexts: 3 peak, 501 total, 151 pruned
+   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), 0 skipped
+   NFA: 299 absorbed (len 1/1/1.0), 50 skipped (len 1/1/1.0)
    ->  Function Scan on generate_series s (actual rows=500.00 loops=1)
 (9 rows)
 
@@ -3700,10 +3801,11 @@ WINDOW w AS (
    Pattern: a b
    Storage: Memory  Maximum Storage: NkB
    NFA States: 2 peak, 501 total, 0 merged
-   NFA Contexts: 3 peak, 501 total, 250 pruned
+   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)
-(8 rows)
+(9 rows)
 
 DROP VIEW rpr_v;
 -- High skip count scenario
@@ -3749,10 +3851,11 @@ WINDOW w AS (
    Pattern: a b c d e
    Storage: Memory  Maximum Storage: NkB
    NFA States: 2 peak, 501 total, 0 merged
-   NFA Contexts: 3 peak, 501 total, 495 pruned
+   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)
-(8 rows)
+(9 rows)
 
 DROP VIEW rpr_v;
 -- Cleanup
diff --git a/src/test/regress/expected/rpr_nfa.out b/src/test/regress/expected/rpr_nfa.out
new file mode 100644
index 00000000000..46a463c2597
--- /dev/null
+++ b/src/test/regress/expected/rpr_nfa.out
@@ -0,0 +1,2524 @@
+-- ============================================================
+-- 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
+--   FIXME Issues (Known Limitations)
+--
+-- 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)
+
+-- ============================================================
+-- 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)
+
+-- ============================================================
+-- 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)
+
+-- ============================================================
+-- 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)
+
+-- ============================================================
+-- 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)
+
+-- 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)
+
+-- 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)
+
+-- ============================================================
+-- 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)
+
+-- ============================================================
+-- 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)
+
+-- ============================================================
+-- 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)
+
+-- ============================================================
+-- 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)
+
+-- ============================================================
+-- INITIAL Mode (Runtime)
+-- ============================================================
+-- 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)
+
+-- ============================================================
+-- 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)
+
+-- ============================================================
+-- FIXME Issues - Known Limitations
+-- ============================================================
+-- FIXME 1 - altPriority lexical order
+WITH test_alt_priority_repeated AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A','B']),  -- Both A and B match
+        (2, ARRAY['A','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_priority_repeated
+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 |         3
+  2 | {A,B} |           2 |         3
+  3 | {A,B} |           3 |         3
+(3 rows)
+
+-- FIXME 1 - Nested ALT lexical order
+WITH test_alt_priority_nested AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A','B']),
+        (2, ARRAY['C','D']),
+        (3, ARRAY['A','B']),
+        (4, 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_alt_priority_nested
+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 |         4
+  2 | {C,D} |             |          
+  3 | {A,B} |           3 |         4
+  4 | {C,D} |             |          
+(4 rows)
+
+-- FIXME 2 - Cycle prevention at count > 0
+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)
+
+-- FIXME 2 - Cycle with mixed nullables
+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 |         2
+  2 | {B}   |           2 |         2
+  3 | {A}   |           3 |         3
+  4 | {C}   |             |          
+(4 rows)
+
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index fc61d90ebaf..2bf141ff7d5 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -107,7 +107,7 @@ test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combo
 # ----------
 # Row Pattern Recognition tests
 # ----------
-test: rpr rpr_base rpr_explain
+test: rpr rpr_base rpr_explain rpr_nfa
 
 # ----------
 # Another group of parallel tests (JSON related)
diff --git a/src/test/regress/sql/rpr.sql b/src/test/regress/sql/rpr.sql
index 1071dacd687..7690c5611c0 100644
--- a/src/test/regress/sql/rpr.sql
+++ b/src/test/regress/sql/rpr.sql
@@ -323,6 +323,23 @@ SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER
   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
@@ -499,6 +516,25 @@ count(*) OVER w
   DOWN AS price < PREV(price)
 );
 
+-- ReScan test: LATERAL join forces WindowAgg rescan with RPR
+-- Tests ExecReScanWindowAgg clearing prev_slot/next_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;
@@ -624,6 +660,23 @@ WITH data AS (
    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)
+);
+
 -- View and pg_get_viewdef tests.
 CREATE TEMP VIEW v_window AS
 SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w,
@@ -1358,11 +1411,10 @@ WINDOW w AS (
 --   Row 3: C D E F (3-6) <- ends last!
 
 -- Test 1b: Longer pattern FAILS, shorter pattern should survive
--- Pattern: A+ B C D | A+ B (greedy at front for absorption)
--- Data: A B C X (D expected but X found)
--- A+ B C D fails at row 4 (X instead of D)
--- A+ B succeeds at row 2
--- Result should be match 1-2 (A+ B)
+-- Pattern: A+ B C D E | B+ C
+-- A+ B C D E fails (no E found in sequence)
+-- B+ C matches at rows 2-3
+-- Result: match 2-3 (B+ C)
 WITH test_overlap1b AS (
     SELECT * FROM (VALUES
         (1, ARRAY['A']),
@@ -1585,8 +1637,7 @@ WINDOW w AS (
     DEFINE A AS 'A' = ANY(flags)
 );
 -- Pattern A+ is absorbable (unbounded first element, only one unbounded)
--- Without absorption: 4 matches (1-4, 2-4, 3-4, 4-4)
--- With absorption: Row 1 match (1-4), rows 2-4 absorbed
+-- 4 matches: (1-4, 2-4, 3-4, 4-4)
 
 -- Test absorption 2: A+ B pattern - absorption with fixed suffix
 WITH test_absorb_suffix AS (
@@ -1611,7 +1662,7 @@ WINDOW w AS (
 );
 -- Pattern A+ B is absorbable (A+ unbounded first, B bounded suffix)
 -- All potential matches end at same row (row 4 with B)
--- With absorption: Row 1 match (1-4), rows 2-3 absorbed
+-- 3 matches: (1-4, 2-4, 3-4)
 
 -- Test absorption 3: Per-branch absorption with ALT (B+ C | B+ D)
 WITH test_absorb_alt AS (
@@ -1636,8 +1687,7 @@ WINDOW w AS (
         D AS 'D' = ANY(flags)
 );
 -- Both branches B+ C and B+ D are absorbable (B+ unbounded first)
--- B+ D branch matches: potential 1-4, 2-4, 3-4
--- Row 1 (1-4) absorbs Row 2 (2-4) and Row 3 (3-4) - same endpoint
+-- B+ D branch matches: 3 matches (1-4, 2-4, 3-4)
 
 -- Test absorption 4: Non-absorbable pattern (A B+ - unbounded not first)
 WITH test_no_absorb AS (
@@ -1687,8 +1737,7 @@ WINDOW w AS (
         B AS 'B' = ANY(flags)
 );
 -- Pattern optimized: (A B) (A B)+ -> (A B){2,}
--- Potential matches: 1-6 (3 reps), 3-6 (2 reps), 5-6 needs 2 reps (fail)
--- Row 1 (1-6) absorbs Row 3 (3-6) - same endpoint, top-level unbounded GROUP
+-- 2 matches: 1-6 (3 reps), 3-6 (2 reps)
 
 -- Test absorption 6: Multiple unbounded - first element unbounded enables absorption
 WITH test_multi_unbounded AS (
@@ -1711,7 +1760,7 @@ WINDOW w AS (
         A AS 'A' = ANY(flags),
         B AS 'B' = ANY(flags)
 );
--- Row 1: 1-4, Row 2: absorbed (same endpoint 4)
+-- 2 matches: 1-4, 2-4 (same endpoint 4)
 
 -- ============================================
 -- Jacob's RPR Patterns (from jacob branch)
@@ -1963,7 +2012,6 @@ WINDOW w AS (
 -- ============================================
 
 -- Test: (A | B)+ C - alternation inside quantified group followed by C
--- BUG: Currently returns NULL instead of matching 1-4
 WITH jacob_alt_quant AS (
     SELECT * FROM (VALUES
         (1, ARRAY['A']),
@@ -1987,7 +2035,6 @@ WINDOW w AS (
 -- Expected: 1-4 (A B A C)
 
 -- Test: ((A | B) C)+ - alternation inside group with outer quantifier
--- Currently returns two separate matches (1-2, 3-4) instead of single 1-4
 WITH jacob_alt_group AS (
     SELECT * FROM (VALUES
         (1, ARRAY['A']),
@@ -2015,8 +2062,7 @@ WINDOW w AS (
 -- RELUCTANT quantifiers (not yet supported)
 -- ============================================
 
--- Test: A+? B (reluctant) - CURRENTLY ACTS LIKE A+ (GREEDY)
--- Parser accepts the syntax but executor doesn't differentiate
+-- Test: A+? B (reluctant) - parser rejects with ERROR
 WITH jacob_reluctant AS (
     SELECT * FROM (VALUES
         (1, ARRAY['A']),
@@ -2036,10 +2082,9 @@ WINDOW w AS (
         A AS 'A' = ANY(flags),
         B AS 'B' = ANY(flags)
 );
--- Current: 1-4 (A A A B) - greedy behavior
--- Expected when RELUCTANT implemented: 3-4 (A B) - shortest A before B
+-- Expected: ERROR (reluctant quantifiers not yet supported)
 
--- Test: A{1,3}? B (reluctant bounded)
+-- Test: A{1,3}? B (reluctant bounded) - parser rejects with ERROR
 WITH jacob_reluctant_bounded AS (
     SELECT * FROM (VALUES
         (1, ARRAY['A']),
@@ -2059,8 +2104,7 @@ WINDOW w AS (
         A AS 'A' = ANY(flags),
         B AS 'B' = ANY(flags)
 );
--- Current: 1-4 (greedy, takes max A before B)
--- Expected when RELUCTANT implemented: 3-4 (takes min A before B)
+-- Expected: ERROR (reluctant quantifiers not yet supported)
 
 -- ============================================
 -- Nested quantifiers (pathological patterns)
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index 6f38cd1ae92..879f5fe48a2 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -188,7 +188,7 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS id > 0, A AS id < 10
 );
--- Expected: ERROR: row pattern definition variable name "A" appears more than once in DEFINE clause
+-- Expected: ERROR: row pattern definition variable name "a" appears more than once in DEFINE clause
 
 DROP TABLE rpr_dup;
 
@@ -355,7 +355,7 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: FRAME must start at current row when row pattern recognition is used
+-- Expected: ERROR: FRAME option RANGE is not permitted when row pattern recognition is used
 
 -- GROUPS frame not starting at CURRENT ROW
 SELECT COUNT(*) OVER w
@@ -366,7 +366,7 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: FRAME must start at current row when row pattern recognition is used
+-- Expected: ERROR: FRAME option GROUP is not permitted when row pattern recognition is used
 
 -- Starting with N PRECEDING
 SELECT COUNT(*) OVER w
@@ -401,7 +401,7 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: frame end cannot be before frame start
+-- Expected: ERROR: frame starting from current row cannot have preceding rows
 
 -- End before start: CURRENT ROW AND UNBOUNDED PRECEDING
 SELECT COUNT(*) OVER w
@@ -412,7 +412,7 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: frame end cannot be before frame start
+-- Expected: ERROR: frame end cannot be UNBOUNDED PRECEDING
 
 -- Single row frame: CURRENT ROW AND CURRENT ROW
 SELECT id, val, COUNT(*) OVER w as cnt
@@ -462,14 +462,7 @@ WINDOW w AS (
 )
 ORDER BY id;
 
--- RANGE: includes all rows with same ORDER BY value
--- Expected result: Includes peer rows (same val) in range calculation
--- id=1 (val=10): range [10,20] includes all val=10 and val=20 peers -> cnt=2 (only first in peer group gets match)
--- id=2,3 (val=10): already matched by id=1 -> cnt=0
--- id=4 (val=20): range [20,30] includes all val=20 and val=30 peers -> cnt=2
--- id=5 (val=20): already matched by id=4 -> cnt=0
--- id=6 (val=30): range [30,40] includes only val=30 -> cnt=1
--- Result: [2,0,0,2,0,1]
+-- RANGE frame with RPR (not permitted)
 SELECT id, val, COUNT(*) OVER w as cnt
 FROM rpr_frame
 WINDOW w AS (
@@ -480,15 +473,9 @@ WINDOW w AS (
     DEFINE A AS val >= 0, B AS val >= 0
 )
 ORDER BY id;
+-- Expected: ERROR: FRAME option RANGE is not permitted when row pattern recognition is used
 
--- GROUPS: treats rows with same value as one group (1 FOLLOWING = next group)
--- Expected result: 1 FOLLOWING means current group + 1 next group
--- id=1 (val=10): groups [val=10, val=20] -> cnt=2 (only first in group gets match)
--- id=2,3 (val=10): already matched by id=1 -> cnt=0
--- id=4 (val=20): groups [val=20, val=30] -> cnt=2
--- id=5 (val=20): already matched by id=4 -> cnt=0
--- id=6 (val=30): groups [val=30] (no next group) -> cnt=1
--- Result: [2,0,0,2,0,1]
+-- GROUPS frame with RPR (not permitted)
 SELECT id, val, COUNT(*) OVER w as cnt
 FROM rpr_frame
 WINDOW w AS (
@@ -499,6 +486,7 @@ WINDOW w AS (
     DEFINE A AS val >= 0, B AS val >= 0
 )
 ORDER BY id;
+-- Expected: ERROR: FRAME option GROUP is not permitted when row pattern recognition is used
 
 DROP TABLE rpr_frame;
 
@@ -513,7 +501,6 @@ INSERT INTO rpr_partition VALUES
     (4, 2, 15), (5, 2, 25), (6, 2, 35);
 
 -- PARTITION BY with ROWS frame
--- Expected: Pattern matching should reset for each partition
 SELECT id, grp, val, COUNT(*) OVER w as cnt
 FROM rpr_partition
 WINDOW w AS (
@@ -525,9 +512,9 @@ WINDOW w AS (
     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
--- Expected: Each partition processed independently
 SELECT id, grp, val, COUNT(*) OVER w as cnt
 FROM rpr_partition
 WINDOW w AS (
@@ -539,6 +526,7 @@ WINDOW w AS (
     DEFINE A AS val >= 10, B AS val >= 20
 )
 ORDER BY id;
+-- Expected: ERROR: FRAME option RANGE is not permitted when row pattern recognition is used
 
 DROP TABLE rpr_partition;
 
@@ -1911,7 +1899,7 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > 'string'
 );
--- Expected: ERROR: operator does not exist or type mismatch
+-- Expected: ERROR: invalid input syntax for type integer: "string"
 
 -- Aggregate function in DEFINE (if not allowed)
 SELECT COUNT(*) OVER w
@@ -2065,7 +2053,7 @@ 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 flatten: (A | (B | C)) -> (a | b | c)
+-- 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
@@ -2162,14 +2150,14 @@ 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}){3,} c{3}
+-- 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+){2} -> (a+){2}
+-- 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
@@ -2255,8 +2243,8 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
              PATTERN ((A B*)+ A B*)
              DEFINE A AS val <= 50, B AS val > 50);
 
--- Unwrap GROUP{1,1}: (A | B | C) -> A | B | C
--- Tests tryUnwrapGroup removing redundant GROUP
+-- 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
@@ -2611,7 +2599,6 @@ 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)
--- Expected: Fallback - pattern not merged due to min overflow (4000000000 > INT32_MAX)
 EXPLAIN (COSTS OFF)
 SELECT COUNT(*) OVER w FROM rpr_fallback
 WINDOW w AS (
@@ -2620,9 +2607,9 @@ WINDOW w AS (
     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
--- Expected: Fallback - min OK (2*1=2), but max overflow (2*2000000000 > INT32_MAX)
 EXPLAIN (COSTS OFF)
 SELECT COUNT(*) OVER w FROM rpr_fallback
 WINDOW w AS (
@@ -2631,9 +2618,9 @@ WINDOW w AS (
     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: min quantifier overflow causes optimization fallback (min != max case)
--- Expected: Fallback - pattern not merged due to min overflow
+-- 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 (
@@ -2642,9 +2629,9 @@ WINDOW w AS (
     PATTERN ((A{2000000000,2147483647}){2})
     DEFINE A AS val > 0
 );
+-- Expected: ERROR at parse time before optimization
 
 -- Test: nested unbounded with large min causes overflow fallback
--- Expected: Fallback - min overflow (2000000000 * 2000000000 > INT32_MAX)
 EXPLAIN (COSTS OFF)
 SELECT COUNT(*) OVER w FROM rpr_fallback
 WINDOW w AS (
@@ -2653,9 +2640,9 @@ WINDOW w AS (
     PATTERN ((A{2000000000,}){2000000000,})
     DEFINE A AS val > 0
 );
+-- Expected: Fallback - min overflow (2000000000 * 2000000000 > INT32_MAX)
 
 -- Test: prefix mismatch causes optimization fallback
--- Expected: Fallback - prefix elements don't match GROUP content
 EXPLAIN (COSTS OFF)
 SELECT COUNT(*) OVER w FROM rpr_fallback
 WINDOW w AS (
@@ -2664,6 +2651,7 @@ WINDOW w AS (
     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;
 
@@ -2752,7 +2740,6 @@ FROM rpr_planner
 ORDER BY id;
 
 -- Window with Aggregate Functions
-
 SELECT category,
        COUNT(*) OVER w as window_cnt,
        COUNT(*) as agg_cnt
@@ -2766,6 +2753,7 @@ WINDOW w AS (
 )
 GROUP BY category
 ORDER BY category;
+-- Expected: ERROR (GROUP BY with window RPR not supported)
 
 -- ============================================================
 -- Subquery and CTE Tests
@@ -3320,7 +3308,6 @@ CREATE TABLE rpr_errors (id INT, val INT);
 INSERT INTO rpr_errors VALUES (1, 10), (2, 20);
 
 -- Test: PATTERN variable without DEFINE (A), DEFINE variable not in PATTERN (B)
--- Expected: Success - A is implicitly TRUE, B is filtered out
 SELECT id, val, COUNT(*) OVER w FROM rpr_errors
 WINDOW w AS (
     ORDER BY id
@@ -3329,9 +3316,9 @@ WINDOW w AS (
     DEFINE
       B AS TRUE
 );
+-- Expected: Success - A is implicitly TRUE, B is filtered out
 
 -- Test: 3 variables in PATTERN, 253 in DEFINE (DEFINE filtering test)
--- Expected: Success - unused DEFINE variables are filtered out
 SELECT COUNT(*) OVER w FROM rpr_errors
 WINDOW w AS (
     ORDER BY id
@@ -3365,9 +3352,9 @@ WINDOW w AS (
     V241 AS val > 0, V242 AS val > 0, V243 AS val > 0, V244 AS val > 0, V245 AS val > 0, V246 AS val > 0, V247 AS val > 0, V248 AS val > 0, V249 AS val > 0, V250 AS val > 0,
     V251 AS val > 0, V252 AS val > 0, V253 AS val > 0
 );
+-- Expected: Success - unused DEFINE variables are filtered out
 
 -- Test: 251 variables in PATTERN, 252 in DEFINE (boundary - should succeed)
--- Expected: Success - unused DEFINE variables are filtered out
 SELECT COUNT(*) OVER w FROM rpr_errors
 WINDOW w AS (
     ORDER BY id
@@ -3401,10 +3388,9 @@ WINDOW w AS (
     V241 AS val > 0, V242 AS val > 0, V243 AS val > 0, V244 AS val > 0, V245 AS val > 0, V246 AS val > 0, V247 AS val > 0, V248 AS val > 0, V249 AS val > 0, V250 AS val > 0,
     V251 AS val > 0, V252 AS val > 0
 );
-
+-- Expected: Success - unused DEFINE variables are filtered out
 
 -- Test: 252 variables in PATTERN, 251 in DEFINE (exceeds limit with implicit TRUE)
--- Expected: ERROR - too many pattern variables (Maximum is 251)
 SELECT COUNT(*) OVER w FROM rpr_errors
 WINDOW w AS (
     ORDER BY id
@@ -3438,9 +3424,9 @@ WINDOW w AS (
     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)
--- Expected: Should succeed
 -- Note: 253 nested GROUP{3,7} quantifiers produce depth 253 after optimization
 SELECT id, val, COUNT(*) OVER w FROM rpr_errors
 WINDOW w AS (
@@ -3449,9 +3435,9 @@ WINDOW w AS (
     PATTERN ((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((A{3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7})
     DEFINE A AS val > 0
 );
+-- Expected: Should succeed
 
 -- Test: Pattern nesting depth exceeds maximum (depth 254)
--- Expected: ERROR - pattern nesting too deep
 -- Note: 254 nested GROUP{3,7} quantifiers produce depth 254 after optimization
 SELECT id, val, COUNT(*) OVER w FROM rpr_errors
 WINDOW w AS (
@@ -3460,6 +3446,7 @@ WINDOW w AS (
     PATTERN (((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((A{3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7})
     DEFINE A AS val > 0
 );
+-- Expected: ERROR - pattern nesting too deep
 
 DROP TABLE rpr_errors;
 
@@ -3567,6 +3554,25 @@ WINDOW w AS (
     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
 -- ============================================================
diff --git a/src/test/regress/sql/rpr_explain.sql b/src/test/regress/sql/rpr_explain.sql
index 80088ab18e2..f8c8f62e594 100644
--- a/src/test/regress/sql/rpr_explain.sql
+++ b/src/test/regress/sql/rpr_explain.sql
@@ -189,7 +189,7 @@ WINDOW w AS (
 );
 SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
-EXPLAIN (COSTS OFF)
+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 (
@@ -212,7 +212,7 @@ WINDOW w AS (
 );
 SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
-EXPLAIN (COSTS OFF)
+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 (
@@ -365,7 +365,7 @@ WINDOW w AS (
 );
 SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
-EXPLAIN (COSTS OFF)
+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 (
@@ -389,7 +389,7 @@ WINDOW w AS (
 );
 SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
-EXPLAIN (COSTS OFF)
+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 (
@@ -412,7 +412,7 @@ WINDOW w AS (
 );
 SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
-EXPLAIN (COSTS OFF)
+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 (
@@ -1680,7 +1680,7 @@ WINDOW w AS (
 );
 SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
-EXPLAIN (COSTS OFF)
+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 (
@@ -1701,7 +1701,7 @@ WINDOW w AS (
 );
 SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
-EXPLAIN (COSTS OFF)
+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 (
@@ -1722,7 +1722,7 @@ WINDOW w AS (
 );
 SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
-EXPLAIN (COSTS OFF)
+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 (
@@ -1743,7 +1743,7 @@ WINDOW w AS (
 );
 SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
-EXPLAIN (COSTS OFF)
+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 (
@@ -1764,7 +1764,7 @@ WINDOW w AS (
 );
 SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
-EXPLAIN (COSTS OFF)
+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 (
@@ -1786,7 +1786,7 @@ WINDOW w AS (
 );
 SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
-EXPLAIN (COSTS OFF)
+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 (
@@ -1808,7 +1808,7 @@ WINDOW w AS (
 );
 SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
-EXPLAIN (COSTS OFF)
+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 (
@@ -1902,7 +1902,7 @@ WINDOW w AS (
 );
 SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
-EXPLAIN (COSTS OFF)
+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 (
@@ -1923,7 +1923,7 @@ WINDOW w AS (
 );
 SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
-EXPLAIN (COSTS OFF)
+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 (
@@ -1944,7 +1944,7 @@ WINDOW w AS (
 );
 SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
-EXPLAIN (COSTS OFF)
+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 (
@@ -1965,7 +1965,7 @@ WINDOW w AS (
 );
 SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_v'), E'\n')) AS line WHERE line ~ 'PATTERN';
 SELECT rpr_explain_filter('
-EXPLAIN (COSTS OFF)
+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 (
diff --git a/src/test/regress/sql/rpr_nfa.sql b/src/test/regress/sql/rpr_nfa.sql
new file mode 100644
index 00000000000..9573d1dab3b
--- /dev/null
+++ b/src/test/regress/sql/rpr_nfa.sql
@@ -0,0 +1,1865 @@
+-- ============================================================
+-- 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
+--   FIXME Issues (Known Limitations)
+--
+-- 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)
+);
+
+-- ============================================================
+-- 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)
+);
+
+-- ============================================================
+-- 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)
+);
+
+-- ============================================================
+-- 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)
+);
+
+-- ============================================================
+-- 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)
+);
+
+-- 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)
+);
+
+-- 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)
+);
+
+-- ============================================================
+-- 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)
+);
+
+-- ============================================================
+-- 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)
+);
+
+-- ============================================================
+-- 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)
+);
+
+-- ============================================================
+-- 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;
+
+-- ============================================================
+-- INITIAL Mode (Runtime)
+-- ============================================================
+
+-- 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)
+);
+
+-- ============================================================
+-- 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)
+);
+
+-- ============================================================
+-- FIXME Issues - Known Limitations
+-- ============================================================
+
+-- FIXME 1 - altPriority lexical order
+WITH test_alt_priority_repeated AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A','B']),  -- Both A and B match
+        (2, ARRAY['A','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_priority_repeated
+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)
+);
+
+-- FIXME 1 - Nested ALT lexical order
+WITH test_alt_priority_nested AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A','B']),
+        (2, ARRAY['C','D']),
+        (3, ARRAY['A','B']),
+        (4, 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_alt_priority_nested
+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)
+);
+
+-- FIXME 2 - Cycle prevention at count > 0
+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)
+);
+
+-- FIXME 2 - Cycle with mixed nullables
+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)
+);
-- 
2.50.1 (Apple Git-155)


From 5b47d3a2a88f7a62f5a4c6662c4a1b8e03e5348e Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Sat, 14 Feb 2026 23:56:15 +0900
Subject: [PATCH 8/8] Fix RPR documentation: correct depth limit and EXPLAIN
 marker examples

---
 doc/src/sgml/advanced.sgml   | 9 +++++----
 doc/src/sgml/ref/select.sgml | 2 +-
 2 files changed, 6 insertions(+), 5 deletions(-)

diff --git a/doc/src/sgml/advanced.sgml b/doc/src/sgml/advanced.sgml
index 241c7e03a5a..a76fb263a94 100644
--- a/doc/src/sgml/advanced.sgml
+++ b/doc/src/sgml/advanced.sgml
@@ -647,7 +647,7 @@ FROM stock
     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 255-level depth limit rarely encountered.
+    nesting depth, making the 253-level depth limit rarely encountered.
     They are applied transparently and can be observed
     in <command>EXPLAIN</command> output.
    </para>
@@ -667,9 +667,10 @@ FROM stock
     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 A can be absorbed, while <literal>(A' B')+"</literal>
-    shows that both A and B within the group are absorbable.
+    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>
diff --git a/doc/src/sgml/ref/select.sgml b/doc/src/sgml/ref/select.sgml
index 7ec7760f472..f0676bf6f2c 100644
--- a/doc/src/sgml/ref/select.sgml
+++ b/doc/src/sgml/ref/select.sgml
@@ -1155,7 +1155,7 @@ DEFINE <replaceable class="parameter">definition_variable_name</replaceable> AS
     used in <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 255 levels.
+    (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.
-- 
2.50.1 (Apple Git-155)



Attachments:

  [text/plain] 0001-Fix-non-ASCII-characters-in-RPR-code-and-comments.txt (13.5K, 3-0001-Fix-non-ASCII-characters-in-RPR-code-and-comments.txt)
  download | inline diff:
From 1cc2ebd59597c672b752d7b6912dedc2bde88d66 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Tue, 3 Feb 2026 17:30:06 +0900
Subject: [PATCH 1/1] Fix non-ASCII characters in RPR code and comments

---
 src/backend/executor/nodeWindowAgg.c   | 10 ++++-----
 src/backend/optimizer/plan/rpr.c       |  2 +-
 src/include/nodes/execnodes.h          |  8 ++++----
 src/test/regress/expected/rpr.out      | 14 ++++++-------
 src/test/regress/expected/rpr_base.out | 28 +++++++++++++-------------
 src/test/regress/sql/rpr.sql           | 14 ++++++-------
 src/test/regress/sql/rpr_base.sql      | 28 +++++++++++++-------------
 7 files changed, 52 insertions(+), 52 deletions(-)

diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index bcdf8b6db81..1176df04b2c 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -4977,7 +4977,7 @@ register_result:
  * These functions implement direct NFA execution using the compiled
  * RPRPattern structure, avoiding regex compilation overhead.
  *
- * Execution Flow: match → absorb → advance
+ * Execution Flow: match -> absorb -> advance
  * -----------------------------------------
  * The NFA execution follows a three-phase cycle for each row:
  *
@@ -4997,7 +4997,7 @@ register_result:
  *
  * Key Design Decisions:
  * ---------------------
- * - VAR→END transition in match phase: When a simple VAR (max=1) matches
+ * - 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
@@ -5008,7 +5008,7 @@ register_result:
  *   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->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.
@@ -6092,7 +6092,7 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
 		state->elemIdx = elem->next;
 		nextElem = &elements[state->elemIdx];
 
-		/* END→END: increment outer END's count */
+		/* END->END: increment outer END's count */
 		if (RPRElemIsEnd(nextElem) && state->counts[nextElem->depth] < RPR_COUNT_MAX)
 			state->counts[nextElem->depth]++;
 
@@ -6123,7 +6123,7 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
 		exitState->counts[depth] = 0;
 		nextElem = &elements[exitState->elemIdx];
 
-		/* END→END: increment outer END's count */
+		/* END->END: increment outer END's count */
 		if (RPRElemIsEnd(nextElem) && exitState->counts[nextElem->depth] < RPR_COUNT_MAX)
 			exitState->counts[nextElem->depth]++;
 
diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index 230c545a631..50043c416c6 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -1379,7 +1379,7 @@ isUnboundedStart(RPRPattern *pattern, RPRElemIdx idx, RPRDepth parentDepth)
  *
  * Context absorption eliminates redundant match searches by absorbing
  * newer contexts that cannot produce longer matches than older contexts.
- * This achieves O(n²) → O(n) performance improvement.
+ * 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
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index b6d6d942de9..e0704742d16 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -2545,11 +2545,11 @@ typedef struct 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
+ *   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)
+ *     - Dynamic: can change false->true (when non-absorbable states die)
  *     - Used to determine if this context is eligible for absorption
  */
 typedef struct RPRNFAContext
@@ -2564,7 +2564,7 @@ typedef struct RPRNFAContext
 	RPRNFAState *matchedState;	/* FIN state for greedy fallback (cloned) */
 
 	/* Two-flag absorption optimization */
-	bool		hasAbsorbableState; /* can absorb others (≥1 absorbable
+	bool		hasAbsorbableState; /* can absorb others (>=1 absorbable
 									 * state) */
 	bool		allStatesAbsorbable;	/* can be absorbed (ALL states
 										 * absorbable) */
diff --git a/src/test/regress/expected/rpr.out b/src/test/regress/expected/rpr.out
index c4794c5cd88..d4298860865 100644
--- a/src/test/regress/expected/rpr.out
+++ b/src/test/regress/expected/rpr.out
@@ -3099,11 +3099,11 @@ WINDOW w AS (
   5 | {E,_} |             |          
 (5 rows)
 
--- Row 1: A=T, B=T → matches A
--- Row 2: B=T, C=T → matches B
--- Row 3: C=T, D=T → matches C
--- Row 4: D=T, E=T → matches D
--- Row 5: E=T      → matches E
+-- Row 1: A=T, B=T -> matches A
+-- Row 2: B=T, C=T -> matches B
+-- Row 3: C=T, D=T -> matches C
+-- Row 4: D=T, E=T -> matches D
+-- Row 5: E=T      -> matches E
 -- Result: match 1-5 (A B C D E)
 -- Test 6: Diagonal pattern with multi-TRUE (shifted overlap)
 WITH test_diagonal AS (
@@ -3138,8 +3138,8 @@ WINDOW w AS (
 (5 rows)
 
 -- Possible matches:
---   Start Row 1: A(1) B(2) C(3) D(4) → 1-4
---   Start Row 2: A(2) B(3) C(4) D(5) → 2-5 (because Row 2 has A too!)
+--   Start Row 1: A(1) B(2) C(3) D(4) -> 1-4
+--   Start Row 2: A(2) B(3) C(4) D(5) -> 2-5 (because Row 2 has A too!)
 -- ===================================================================
 -- Context Absorption Tests
 -- ===================================================================
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index b34b9e59d25..23851c5a11c 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -336,9 +336,9 @@ INSERT INTO rpr_frame VALUES
 -- 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
+-- 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
@@ -598,11 +598,11 @@ ORDER BY id;
 
 -- RANGE: includes all rows with same ORDER BY value
 -- Expected result: Includes peer rows (same val) in range calculation
--- id=1 (val=10): range [10,20] includes all val=10 and val=20 peers → cnt=2 (only first in peer group gets match)
--- id=2,3 (val=10): already matched by id=1 → cnt=0
--- id=4 (val=20): range [20,30] includes all val=20 and val=30 peers → cnt=2
--- id=5 (val=20): already matched by id=4 → cnt=0
--- id=6 (val=30): range [30,40] includes only val=30 → cnt=1
+-- id=1 (val=10): range [10,20] includes all val=10 and val=20 peers -> cnt=2 (only first in peer group gets match)
+-- id=2,3 (val=10): already matched by id=1 -> cnt=0
+-- id=4 (val=20): range [20,30] includes all val=20 and val=30 peers -> cnt=2
+-- id=5 (val=20): already matched by id=4 -> cnt=0
+-- id=6 (val=30): range [30,40] includes only val=30 -> cnt=1
 -- Result: [2,0,0,2,0,1]
 SELECT id, val, COUNT(*) OVER w as cnt
 FROM rpr_frame
@@ -626,11 +626,11 @@ ORDER BY id;
 
 -- GROUPS: treats rows with same value as one group (1 FOLLOWING = next group)
 -- Expected result: 1 FOLLOWING means current group + 1 next group
--- id=1 (val=10): groups [val=10, val=20] → cnt=2 (only first in group gets match)
--- id=2,3 (val=10): already matched by id=1 → cnt=0
--- id=4 (val=20): groups [val=20, val=30] → cnt=2
--- id=5 (val=20): already matched by id=4 → cnt=0
--- id=6 (val=30): groups [val=30] (no next group) → cnt=1
+-- id=1 (val=10): groups [val=10, val=20] -> cnt=2 (only first in group gets match)
+-- id=2,3 (val=10): already matched by id=1 -> cnt=0
+-- id=4 (val=20): groups [val=20, val=30] -> cnt=2
+-- id=5 (val=20): already matched by id=4 -> cnt=0
+-- id=6 (val=30): groups [val=30] (no next group) -> cnt=1
 -- Result: [2,0,0,2,0,1]
 SELECT id, val, COUNT(*) OVER w as cnt
 FROM rpr_frame
@@ -3549,7 +3549,7 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND 10 FOLLOWING
 -- ============================================================
 -- Absorption Analysis Tests
 -- ============================================================
--- Tests context absorption optimization (O(n²) → O(n))
+-- Tests context absorption optimization (O(n^2) -> O(n))
 -- Files: rpr.c (computeAbsorbability)
 -- Simple Absorbable Pattern: A+ B
 -- Pattern starts with unbounded VAR
diff --git a/src/test/regress/sql/rpr.sql b/src/test/regress/sql/rpr.sql
index 8ab1daf87d6..788a77e5279 100644
--- a/src/test/regress/sql/rpr.sql
+++ b/src/test/regress/sql/rpr.sql
@@ -1527,11 +1527,11 @@ WINDOW w AS (
         D AS 'D' = ANY(flags),
         E AS 'E' = ANY(flags)
 );
--- Row 1: A=T, B=T → matches A
--- Row 2: B=T, C=T → matches B
--- Row 3: C=T, D=T → matches C
--- Row 4: D=T, E=T → matches D
--- Row 5: E=T      → matches E
+-- Row 1: A=T, B=T -> matches A
+-- Row 2: B=T, C=T -> matches B
+-- Row 3: C=T, D=T -> matches C
+-- Row 4: D=T, E=T -> matches D
+-- Row 5: E=T      -> matches E
 -- Result: match 1-5 (A B C D E)
 
 -- Test 6: Diagonal pattern with multi-TRUE (shifted overlap)
@@ -1558,8 +1558,8 @@ WINDOW w AS (
         D AS 'D' = ANY(flags)
 );
 -- Possible matches:
---   Start Row 1: A(1) B(2) C(3) D(4) → 1-4
---   Start Row 2: A(2) B(3) C(4) D(5) → 2-5 (because Row 2 has A too!)
+--   Start Row 1: A(1) B(2) C(3) D(4) -> 1-4
+--   Start Row 2: A(2) B(3) C(4) D(5) -> 2-5 (because Row 2 has A too!)
 
 -- ===================================================================
 -- Context Absorption Tests
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index 88f0a2c2083..a5a66d2ca81 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -280,9 +280,9 @@ INSERT INTO rpr_frame VALUES
 
 -- 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
+-- 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
@@ -464,11 +464,11 @@ ORDER BY id;
 
 -- RANGE: includes all rows with same ORDER BY value
 -- Expected result: Includes peer rows (same val) in range calculation
--- id=1 (val=10): range [10,20] includes all val=10 and val=20 peers → cnt=2 (only first in peer group gets match)
--- id=2,3 (val=10): already matched by id=1 → cnt=0
--- id=4 (val=20): range [20,30] includes all val=20 and val=30 peers → cnt=2
--- id=5 (val=20): already matched by id=4 → cnt=0
--- id=6 (val=30): range [30,40] includes only val=30 → cnt=1
+-- id=1 (val=10): range [10,20] includes all val=10 and val=20 peers -> cnt=2 (only first in peer group gets match)
+-- id=2,3 (val=10): already matched by id=1 -> cnt=0
+-- id=4 (val=20): range [20,30] includes all val=20 and val=30 peers -> cnt=2
+-- id=5 (val=20): already matched by id=4 -> cnt=0
+-- id=6 (val=30): range [30,40] includes only val=30 -> cnt=1
 -- Result: [2,0,0,2,0,1]
 SELECT id, val, COUNT(*) OVER w as cnt
 FROM rpr_frame
@@ -483,11 +483,11 @@ ORDER BY id;
 
 -- GROUPS: treats rows with same value as one group (1 FOLLOWING = next group)
 -- Expected result: 1 FOLLOWING means current group + 1 next group
--- id=1 (val=10): groups [val=10, val=20] → cnt=2 (only first in group gets match)
--- id=2,3 (val=10): already matched by id=1 → cnt=0
--- id=4 (val=20): groups [val=20, val=30] → cnt=2
--- id=5 (val=20): already matched by id=4 → cnt=0
--- id=6 (val=30): groups [val=30] (no next group) → cnt=1
+-- id=1 (val=10): groups [val=10, val=20] -> cnt=2 (only first in group gets match)
+-- id=2,3 (val=10): already matched by id=1 -> cnt=0
+-- id=4 (val=20): groups [val=20, val=30] -> cnt=2
+-- id=5 (val=20): already matched by id=4 -> cnt=0
+-- id=6 (val=30): groups [val=30] (no next group) -> cnt=1
 -- Result: [2,0,0,2,0,1]
 SELECT id, val, COUNT(*) OVER w as cnt
 FROM rpr_frame
@@ -2331,7 +2331,7 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND 10 FOLLOWING
 -- ============================================================
 -- Absorption Analysis Tests
 -- ============================================================
--- Tests context absorption optimization (O(n²) → O(n))
+-- Tests context absorption optimization (O(n^2) -> O(n))
 -- Files: rpr.c (computeAbsorbability)
 
 -- Simple Absorbable Pattern: A+ B
-- 
2.50.1 (Apple Git-155)



  [text/plain] 0002-Initialize-NFA-per-partition-counters-in-ExecInitWindowAgg.txt (892B, 4-0002-Initialize-NFA-per-partition-counters-in-ExecInitWindowAgg.txt)
  download | inline diff:
From d06525db781c3432d488706759c653d00a5e1980 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Thu, 5 Feb 2026 09:41:03 +0900
Subject: [PATCH 1/1] Initialize NFA per-partition counters in
 ExecInitWindowAgg

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

diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 1176df04b2c..1e088615d19 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -3054,6 +3054,8 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
 	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
-- 
2.50.1 (Apple Git-155)



  [text/plain] 0005-Disallow-RANGE-and-GROUPS-frame-types-with-RPR.txt (4.7K, 5-0005-Disallow-RANGE-and-GROUPS-frame-types-with-RPR.txt)
  download | inline diff:
From be455295ab82ccc2ae80d6cb633b114a7c345ae4 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Fri, 13 Feb 2026 21:57:59 +0900
Subject: [PATCH 1/1] Disallow RANGE and GROUPS frame types with row pattern
 recognition

---
 src/backend/parser/parse_rpr.c         | 19 ++++++++++
 src/test/regress/expected/rpr_base.out | 52 ++++++++------------------
 2 files changed, 35 insertions(+), 36 deletions(-)

diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index 048e84bd7bd..80ebf3c33f8 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -68,6 +68,25 @@ transformRPR(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 		return;
 
 	/* Check Frame options */
+
+	/* Frame type must be "ROW" */
+	if (wc->frameOptions & FRAMEOPTION_GROUPS)
+		ereport(ERROR,
+				(errcode(ERRCODE_SYNTAX_ERROR),
+				 errmsg("FRAME option GROUP is not permitted when row pattern recognition is used"),
+				 errhint("Use: ROWS instead"),
+				 parser_errposition(pstate,
+									windef->frameLocation >= 0 ?
+									windef->frameLocation : windef->location)));
+	if (wc->frameOptions & FRAMEOPTION_RANGE)
+		ereport(ERROR,
+				(errcode(ERRCODE_SYNTAX_ERROR),
+				 errmsg("FRAME option RANGE is not permitted when row pattern recognition is used"),
+				 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)
 	{
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index c269ab99651..a1f11bd61ce 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -434,11 +434,10 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > 0
 );
-ERROR:  FRAME must start at CURRENT ROW when row pattern recognition is used
+ERROR:  FRAME option RANGE is not permitted when row pattern recognition is used
 LINE 5:     RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWIN...
             ^
-DETAIL:  Current frame starts with UNBOUNDED PRECEDING.
-HINT:  Use: RANGE BETWEEN CURRENT ROW AND ...
+HINT:  Use: ROWS instead
 -- Expected: ERROR: FRAME must start at current row when row pattern recognition is used
 -- GROUPS frame not starting at CURRENT ROW
 SELECT COUNT(*) OVER w
@@ -449,11 +448,10 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > 0
 );
-ERROR:  FRAME must start at CURRENT ROW when row pattern recognition is used
+ERROR:  FRAME option GROUP is not permitted when row pattern recognition is used
 LINE 5:     GROUPS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWI...
             ^
-DETAIL:  Current frame starts with UNBOUNDED PRECEDING.
-HINT:  Use: GROUPS BETWEEN CURRENT ROW AND ...
+HINT:  Use: ROWS instead
 -- Expected: ERROR: FRAME must start at current row when row pattern recognition is used
 -- Starting with N PRECEDING
 SELECT COUNT(*) OVER w
@@ -614,16 +612,10 @@ WINDOW w AS (
     DEFINE A AS val >= 0, B AS val >= 0
 )
 ORDER BY id;
- id | val | cnt 
-----+-----+-----
-  1 |  10 |   2
-  2 |  10 |   0
-  3 |  10 |   0
-  4 |  20 |   2
-  5 |  20 |   0
-  6 |  30 |   1
-(6 rows)
-
+ERROR:  FRAME option RANGE is not permitted when row pattern recognition is used
+LINE 5:     RANGE BETWEEN CURRENT ROW AND 10 FOLLOWING
+            ^
+HINT:  Use: ROWS instead
 -- GROUPS: treats rows with same value as one group (1 FOLLOWING = next group)
 -- Expected result: 1 FOLLOWING means current group + 1 next group
 -- id=1 (val=10): groups [val=10, val=20] -> cnt=2 (only first in group gets match)
@@ -642,16 +634,10 @@ WINDOW w AS (
     DEFINE A AS val >= 0, B AS val >= 0
 )
 ORDER BY id;
- id | val | cnt 
-----+-----+-----
-  1 |  10 |   2
-  2 |  10 |   0
-  3 |  10 |   0
-  4 |  20 |   2
-  5 |  20 |   0
-  6 |  30 |   1
-(6 rows)
-
+ERROR:  FRAME option GROUP is not permitted when row pattern recognition is used
+LINE 5:     GROUPS BETWEEN CURRENT ROW AND 1 FOLLOWING
+            ^
+HINT:  Use: ROWS instead
 DROP TABLE rpr_frame;
 -- ============================================================
 -- PARTITION BY + FRAME Tests
@@ -697,16 +683,10 @@ WINDOW w AS (
     DEFINE A AS val >= 10, B AS val >= 20
 )
 ORDER BY id;
- id | grp | val | cnt 
-----+-----+-----+-----
-  1 |   1 |  10 |   2
-  2 |   1 |  20 |   2
-  3 |   1 |  30 |   1
-  4 |   2 |  15 |   2
-  5 |   2 |  25 |   2
-  6 |   2 |  35 |   1
-(6 rows)
-
+ERROR:  FRAME option RANGE is not permitted when row pattern recognition is used
+LINE 6:     RANGE BETWEEN CURRENT ROW AND 10 FOLLOWING
+            ^
+HINT:  Use: ROWS instead
 DROP TABLE rpr_partition;
 -- ============================================================
 -- PATTERN Syntax Tests
-- 
2.50.1 (Apple Git-155)



  [text/plain] 0003-Remove-FIXME-and-normalize-Storage-values.txt (175.1K, 6-0003-Remove-FIXME-and-normalize-Storage-values.txt)
  download

  [text/plain] 0004-Fix-RPR-pattern-compilation-crash-and-refactor-EXPLAIN-deparse.txt (256.9K, 7-0004-Fix-RPR-pattern-compilation-crash-and-refactor-EXPLAIN-deparse.txt)
  download

  [text/plain] 0007-Run-pgindent-on-RPR-source-files.txt (4.7K, 8-0007-Run-pgindent-on-RPR-source-files.txt)
  download | inline diff:
From 74205caae63e95c8d237491a821ba07356ac18b2 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Sat, 14 Feb 2026 22:34:39 +0900
Subject: [PATCH 1/1] Run pgindent on RPR source files

---
 src/backend/executor/nodeWindowAgg.c | 32 +++++++++++++++-------------
 1 file changed, 17 insertions(+), 15 deletions(-)

diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index b7b87f65b47..9b2c4b6a1d7 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -5108,9 +5108,9 @@ nfa_process_row(WindowAggState *winstate, int64 currentPos,
 
 		/*
 		 * 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.
+		 * mismatch (nfa_match with NULL), which removes all states (all
+		 * states are at VAR positions after advance). So any surviving
+		 * context here must be within its frame boundary.
 		 */
 		Assert(!hasLimitedFrame ||
 			   currentPos < ctx->matchStartRow + frameOffset + 1);
@@ -5235,7 +5235,7 @@ nfa_states_equal(WindowAggState *winstate, RPRNFAState *s1, RPRNFAState *s2)
 
 	/* Compare counts up to current element's depth */
 	elem = &pattern->elements[s1->elemIdx];
-	compareDepth = elem->depth + 1;		/* depth 0 needs 1 count, etc. */
+	compareDepth = elem->depth + 1; /* depth 0 needs 1 count, etc. */
 
 	if (compareDepth > 0 &&
 		memcmp(s1->counts, s2->counts, sizeof(int32) * compareDepth) != 0)
@@ -5328,14 +5328,14 @@ nfa_add_matched_state(WindowAggState *winstate, RPRNFAContext *ctx,
 		state->next = NULL;
 		ctx->matchEndRow = matchEndRow;
 
-		/*
+		/*----------
 		 * SKIP PAST LAST ROW: eagerly prune contexts within match range.
 		 *
 		 * This function is called whenever a FIN state is reached, including
-		 * during greedy matching when intermediate (shorter) matches are found.
-		 * Each time we update matchEndRow (whether extending a greedy match or
-		 * finding a new match), we can prune pending contexts that started
-		 * within the current match range.
+		 * during greedy matching when intermediate (shorter) matches are
+		 * found. Each time we update matchEndRow (whether extending a greedy
+		 * match or finding a new match), we can prune pending contexts that
+		 * started within the current match range.
 		 *
 		 * SKIP PAST LAST ROW uses lexical order (matchStartRow). Therefore,
 		 * any pending context that started at or before matchEndRow can never
@@ -5349,6 +5349,7 @@ nfa_add_matched_state(WindowAggState *winstate, RPRNFAContext *ctx,
 		 *     - Matches START UP (rows 1-2) → matchEndRow=2 → prune Context B(row 2)
 		 *     - Matches START UP UP (rows 1-3) → matchEndRow=3 → prune Context C(row 3)
 		 *     - Continues greedy extension while pruning incrementally
+		 *----------
 		 */
 		if (winstate->rpSkipTo == ST_PAST_LAST_ROW)
 		{
@@ -5553,8 +5554,8 @@ nfa_get_head_context(WindowAggState *winstate, int64 pos)
 	RPRNFAContext *ctx = winstate->nfaContext;
 
 	/*
-	 * Contexts are sorted by matchStartRow ascending.  If the head
-	 * context doesn't match pos, no context exists for this position.
+	 * 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;
@@ -6075,9 +6076,9 @@ nfa_match(WindowAggState *winstate, RPRNFAContext *ctx, bool *varMatched)
 
 					/*
 					 * END's max can never be exceeded here because
-					 * nfa_advance_end only loops when count < max,
-					 * so endCount entering inline advance is at most
-					 * max-1, and incrementing yields at most max.
+					 * nfa_advance_end only loops when count < max, so
+					 * endCount entering inline advance is at most max-1, and
+					 * incrementing yields at most max.
 					 */
 					Assert(endElem->max == RPR_QUANTITY_INF ||
 						   endCount <= endElem->max);
@@ -6251,7 +6252,7 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
 	else if ((elem->max != RPR_QUANTITY_INF && count >= elem->max) ||
 			 (count == 0 && elem->min == 0))
 	{
-		/*
+		/*----------
 		 * Must exit: either reached max iterations, or group matched empty.
 		 *
 		 * FIXME: The (count == 0 && min == 0) condition is insufficient for
@@ -6265,6 +6266,7 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
 		 * Currently, cycles are silently prevented by nfa_add_state_unique
 		 * detecting duplicate states, but this is implicit and not guaranteed
 		 * for all code paths. Explicit cycle detection is needed.
+		 *----------
 		 */
 		RPRPatternElement *nextElem;
 
-- 
2.50.1 (Apple Git-155)



  [text/plain] 0006-Review-NFA-executor-and-add-comprehensive-runtime-tests.txt (281.7K, 9-0006-Review-NFA-executor-and-add-comprehensive-runtime-tests.txt)
  download

  [text/plain] 0008-Fix-RPR-documentation-correct-depth-limit-and-EXPLAI.txt (2.5K, 10-0008-Fix-RPR-documentation-correct-depth-limit-and-EXPLAI.txt)
  download | inline diff:
From 5b47d3a2a88f7a62f5a4c6662c4a1b8e03e5348e Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Sat, 14 Feb 2026 23:56:15 +0900
Subject: [PATCH 8/8] Fix RPR documentation: correct depth limit and EXPLAIN
 marker examples

---
 doc/src/sgml/advanced.sgml   | 9 +++++----
 doc/src/sgml/ref/select.sgml | 2 +-
 2 files changed, 6 insertions(+), 5 deletions(-)

diff --git a/doc/src/sgml/advanced.sgml b/doc/src/sgml/advanced.sgml
index 241c7e03a5a..a76fb263a94 100644
--- a/doc/src/sgml/advanced.sgml
+++ b/doc/src/sgml/advanced.sgml
@@ -647,7 +647,7 @@ FROM stock
     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 255-level depth limit rarely encountered.
+    nesting depth, making the 253-level depth limit rarely encountered.
     They are applied transparently and can be observed
     in <command>EXPLAIN</command> output.
    </para>
@@ -667,9 +667,10 @@ FROM stock
     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 A can be absorbed, while <literal>(A' B')+"</literal>
-    shows that both A and B within the group are absorbable.
+    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>
diff --git a/doc/src/sgml/ref/select.sgml b/doc/src/sgml/ref/select.sgml
index 7ec7760f472..f0676bf6f2c 100644
--- a/doc/src/sgml/ref/select.sgml
+++ b/doc/src/sgml/ref/select.sgml
@@ -1155,7 +1155,7 @@ DEFINE <replaceable class="parameter">definition_variable_name</replaceable> AS
     used in <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 255 levels.
+    (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.
-- 
2.50.1 (Apple Git-155)



  [application/zip] coverage.zip (233.6K, 11-coverage.zip)
  download

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]
  Subject: Re: Row pattern recognition
  In-Reply-To: <CAAAe_zB+W+xBJpYNzRLzh6BH9HrFycTjoPUnVvnU1BT_7RR8Bw@mail.gmail.com>

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

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