0001-Refactor-NFA-absorption-logic.txt

text/plain

Filename: 0001-Refactor-NFA-absorption-logic.txt
Type: text/plain
Part: 0
Message: Re: Row pattern recognition
From 1bd8eb3ee0faf11b23689143f251de2704d3327c Mon Sep 17 00:00:00 2001
From: Henson Choi <assam258@gmail.com>
Date: Mon, 19 Jan 2026 21:22:14 +0900
Subject: [PATCH] Row pattern recognition: Refactor NFA absorption logic

---
 src/backend/commands/explain.c          |   24 +-
 src/backend/executor/nodeWindowAgg.c    | 1473 ++++++++++++++---------
 src/backend/optimizer/plan/createplan.c |    2 +-
 src/backend/optimizer/plan/rpr.c        |  263 +++-
 src/backend/parser/gram.y               |    5 +
 src/include/nodes/execnodes.h           |   21 +
 src/include/optimizer/rpr.h             |   14 +-
 src/test/regress/expected/rpr.out       |   60 +-
 src/test/regress/sql/rpr.sql            |   32 +-
 9 files changed, 1247 insertions(+), 647 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index ba114bd491c..3cbf2ec235b 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -3675,32 +3675,32 @@ show_windowagg_info(WindowAggState *winstate, ExplainState *es)
 			{
 				ExplainPropertyInteger("NFA Match Length Min", NULL, winstate->nfaMatchLen.min, es);
 				ExplainPropertyInteger("NFA Match Length Max", NULL, winstate->nfaMatchLen.max, es);
-				ExplainPropertyInteger("NFA Match Length Avg", NULL,
-									   (int64) ((winstate->nfaMatchLen.total + winstate->nfaMatchesSucceeded / 2) / winstate->nfaMatchesSucceeded),
+				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);
-				ExplainPropertyInteger("NFA Mismatch Length Avg", NULL,
-									   (int64) ((winstate->nfaFailLen.total + winstate->nfaMatchesFailed / 2) / winstate->nfaMatchesFailed),
+				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);
-				ExplainPropertyInteger("NFA Absorbed Length Avg", NULL,
-									   (int64) ((winstate->nfaAbsorbedLen.total + winstate->nfaContextsAbsorbed / 2) / winstate->nfaContextsAbsorbed),
+				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);
-				ExplainPropertyInteger("NFA Skipped Length Avg", NULL,
-									   (int64) ((winstate->nfaSkippedLen.total + winstate->nfaContextsSkipped / 2) / winstate->nfaContextsSkipped),
+				ExplainPropertyFloat("NFA Skipped Length Avg", NULL,
+									   (double) winstate->nfaSkippedLen.total / winstate->nfaContextsSkipped, 1,
 									   es);
 			}
 		}
@@ -3724,7 +3724,7 @@ show_windowagg_info(WindowAggState *winstate, ExplainState *es)
 			{
 				double		avgLen = (double) winstate->nfaMatchLen.total / winstate->nfaMatchesSucceeded;
 				appendStringInfo(es->str,
-								 INT64_FORMAT " matched (len " INT64_FORMAT "/" INT64_FORMAT "/%.0f)",
+								 INT64_FORMAT " matched (len " INT64_FORMAT "/" INT64_FORMAT "/%.1f)",
 								 winstate->nfaMatchesSucceeded,
 								 winstate->nfaMatchLen.min,
 								 winstate->nfaMatchLen.max,
@@ -3738,7 +3738,7 @@ show_windowagg_info(WindowAggState *winstate, ExplainState *es)
 			{
 				double		avgFail = (double) winstate->nfaFailLen.total / winstate->nfaMatchesFailed;
 				appendStringInfo(es->str,
-								 ", " INT64_FORMAT " mismatched (len " INT64_FORMAT "/" INT64_FORMAT "/%.0f)",
+								 ", " INT64_FORMAT " mismatched (len " INT64_FORMAT "/" INT64_FORMAT "/%.1f)",
 								 winstate->nfaMatchesFailed,
 								 winstate->nfaFailLen.min,
 								 winstate->nfaFailLen.max,
@@ -3760,7 +3760,7 @@ show_windowagg_info(WindowAggState *winstate, ExplainState *es)
 				{
 					double		avgAbsorbed = (double) winstate->nfaAbsorbedLen.total / winstate->nfaContextsAbsorbed;
 					appendStringInfo(es->str,
-									 INT64_FORMAT " absorbed (len " INT64_FORMAT "/" INT64_FORMAT "/%.0f)",
+									 INT64_FORMAT " absorbed (len " INT64_FORMAT "/" INT64_FORMAT "/%.1f)",
 									 winstate->nfaContextsAbsorbed,
 									 winstate->nfaAbsorbedLen.min,
 									 winstate->nfaAbsorbedLen.max,
@@ -3775,7 +3775,7 @@ show_windowagg_info(WindowAggState *winstate, ExplainState *es)
 				{
 					double		avgSkipped = (double) winstate->nfaSkippedLen.total / winstate->nfaContextsSkipped;
 					appendStringInfo(es->str,
-									 ", " INT64_FORMAT " skipped (len " INT64_FORMAT "/" INT64_FORMAT "/%.0f)",
+									 ", " INT64_FORMAT " skipped (len " INT64_FORMAT "/" INT64_FORMAT "/%.1f)",
 									 winstate->nfaContextsSkipped,
 									 winstate->nfaSkippedLen.min,
 									 winstate->nfaSkippedLen.max,
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 6002ca05c90..5185ad40237 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -256,31 +256,70 @@ 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);
 
-/* NFA-based pattern matching functions */
+/* 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);
-static bool nfa_evaluate_row(WindowObject winobj, int64 pos, bool *varMatched);
+									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,
+								 RPRNFAState *state);
+static void nfa_add_matched_state(WindowAggState *winstate, RPRNFAContext *ctx,
+								  RPRNFAState *state, int64 matchEndRow);
+
+/* Forward declarations - NFA context management */
 static RPRNFAContext *nfa_context_alloc(WindowAggState *winstate);
 static void nfa_unlink_context(WindowAggState *winstate, RPRNFAContext *ctx);
 static void nfa_context_free(WindowAggState *winstate, RPRNFAContext *ctx);
 static RPRNFAContext *nfa_start_context(WindowAggState *winstate, int64 startPos);
-static void nfa_step(WindowAggState *winstate, RPRNFAContext *ctx,
-					 bool *varMatched, int64 pos);
-static void nfa_finalize_all_contexts(WindowAggState *winstate, int64 lastPos);
-static void nfa_process_context(WindowAggState *winstate, RPRNFAContext *ctx,
-								int64 currentPos, bool hasLimitedFrame, int64 frameOffset);
-static void nfa_step_single(WindowAggState *winstate, RPRNFAContext *ctx,
-							RPRNFAState *state, bool *varMatched, int64 currentPos);
 static RPRNFAContext *nfa_find_context_for_pos(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_failure(WindowAggState *winstate, int64 failedLen);
+
+/* 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);
+									  RPRNFAContext *excludeCtx);
 static void nfa_cleanup_dead_contexts(WindowAggState *winstate, RPRNFAContext *excludeCtx);
-static void nfa_absorb_contexts(WindowAggState *winstate, RPRNFAContext *excludeCtx, int64 currentPos);
-static void update_nfa_length_stats(int64 count, NFALengthStats *stats, int64 newLen);
-static void record_nfa_context_failure(WindowAggState *winstate, int64 failedLen);
+
+/* Forward declarations - NFA absorption */
+static void nfa_update_absorption_flags(RPRNFAContext *ctx, RPRPattern *pattern);
+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 */
+static inline bool nfa_eval_var_match(WindowAggState *winstate,
+									  RPRPatternElement *elem, bool *varMatched);
+static void nfa_match(WindowAggState *winstate, RPRNFAContext *ctx,
+					  bool *varMatched);
+static void nfa_advance_state(WindowAggState *winstate, RPRNFAContext *ctx,
+							  RPRNFAState *state, int64 currentPos, bool initialAdvance);
+static void nfa_route_to_elem(WindowAggState *winstate, RPRNFAContext *ctx,
+							  RPRNFAState *state, RPRPatternElement *nextElem,
+							  int64 currentPos, bool initialAdvance);
+static void nfa_advance_alt(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);
+static void nfa_advance_var(WindowAggState *winstate, RPRNFAContext *ctx,
+							RPRNFAState *state, RPRPatternElement *elem,
+							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
@@ -4834,7 +4873,6 @@ update_reduced_frame(WindowObject winobj, int64 pos)
 	for (currentPos = startPos; targetCtx->states != NULL; currentPos++)
 	{
 		bool		rowExists;
-		RPRNFAContext *ctx;
 
 		/* Evaluate variables for this row - done only once, shared by all contexts */
 		rowExists = nfa_evaluate_row(winobj, currentPos, winstate->nfaVarMatched);
@@ -4846,18 +4884,23 @@ update_reduced_frame(WindowObject winobj, int64 pos)
 			/* Clean up dead contexts from finalization */
 			nfa_cleanup_dead_contexts(winstate, targetCtx);
 			/* Absorb contexts at partition boundary */
-			if (winstate->rpSkipTo == ST_PAST_LAST_ROW && !hasLimitedFrame &&
-				winstate->rpPattern->isAbsorbable)
-				nfa_absorb_contexts(winstate, NULL, currentPos - 1);
+			if (winstate->rpPattern->isAbsorbable)
+			{
+				nfa_absorb_contexts(winstate);
+			}
 			break;
 		}
 
 		/* Update last processed row */
 		winstate->nfaLastProcessedRow = currentPos;
 
-		/* Process each active context with this row's evaluation results */
-		for (ctx = winstate->nfaContext; ctx != NULL; ctx = ctx->next)
-			nfa_process_context(winstate, ctx, currentPos, hasLimitedFrame, frameOffset);
+		/*
+		 * Process all contexts for this row:
+		 *   1. Match all (convergence)
+		 *   2. Absorb redundant
+		 *   3. Advance all (divergence)
+		 */
+		nfa_process_row(winstate, currentPos, hasLimitedFrame, frameOffset);
 
 		/*
 		 * Create a new context for the next potential start position.
@@ -4872,14 +4915,6 @@ update_reduced_frame(WindowObject winobj, int64 pos)
 		 */
 		nfa_cleanup_dead_contexts(winstate, targetCtx);
 
-		/*
-		 * Absorb redundant contexts using simplified algorithm.
-		 * Only compares absorbable element counts (e.g., A+ or (A B)+).
-		 */
-		if (winstate->rpSkipTo == ST_PAST_LAST_ROW && !hasLimitedFrame &&
-			winstate->rpPattern->isAbsorbable)
-			nfa_absorb_contexts(winstate, targetCtx, currentPos);
-
 		/* Check if target context is now complete */
 		if (targetCtx->states == NULL)
 			break;
@@ -4896,7 +4931,7 @@ register_result:
 		int64		mismatchLen = targetCtx->lastProcessedRow - targetCtx->matchStartRow + 1;
 
 		register_reduced_frame_map(winstate, targetCtx->matchStartRow, RF_UNMATCHED);
-		record_nfa_context_failure(winstate, mismatchLen);
+		nfa_record_context_failure(winstate, mismatchLen);
 	}
 	else
 	{
@@ -4908,7 +4943,7 @@ register_result:
 			register_reduced_frame_map(winstate, i, RF_SKIPPED);
 		}
 		winstate->nfaMatchesSucceeded++;
-		update_nfa_length_stats(winstate->nfaMatchesSucceeded,
+		nfa_update_length_stats(winstate->nfaMatchesSucceeded,
 								&winstate->nfaMatchLen,
 								matchLen);
 	}
@@ -4934,6 +4969,83 @@ register_result:
  *
  * These functions implement direct NFA execution using the compiled
  * RPRPattern structure, avoiding regex compilation overhead.
+ *
+ * Execution Flow: match → absorb → advance
+ * -----------------------------------------
+ * The NFA execution follows a three-phase cycle for each row:
+ *
+ * 1. MATCH (convergence): Evaluate all waiting states against current row.
+ *    States on VAR elements are checked against their defining conditions.
+ *    Failed matches are removed, successful ones may transition forward.
+ *    This is a "convergence" phase - the number of states tends to decrease.
+ *
+ * 2. ABSORB: After matching, check if any context can absorb another.
+ *    Context absorption is an optimization that merges equivalent contexts.
+ *    A context can only be absorbed if ALL its states are absorbable.
+ *
+ * 3. ADVANCE (divergence): Expand states through epsilon transitions.
+ *    States advance through ALT (alternation), END (group end), and
+ *    optional elements until reaching VAR or FIN elements where they wait.
+ *    This is a "divergence" phase - ALT creates multiple branch states.
+ *
+ * Key Design Decisions:
+ * ---------------------
+ * - VAR→END transition in match phase: When a simple VAR (max=1) matches
+ *   and the next element is END, we transition immediately in the match
+ *   phase rather than waiting for advance. This is necessary for correct
+ *   absorption: states must be at END to be marked absorbable before the
+ *   absorption check occurs.
+ *
+ * - Optional VAR skip paths: When advance lands on a VAR with min=0,
+ *   we create both a waiting state AND a skip state (like ALT branches).
+ *   This ensures patterns like "A B? C" work correctly - we need a state
+ *   waiting for B AND a state that has already skipped to C.
+ *
+ * - END→END count increment: When transitioning from one END to another
+ *   END within advance, we must increment the outer END's count. This
+ *   handles nested groups like "((A|B)+)+" correctly - exiting the inner
+ *   group counts as one iteration of the outer group.
+ *
+ * - initialAdvance flag: The first advance after context creation must
+ *   skip FIN recording. Reaching FIN without evaluating any VAR would
+ *   create a zero-length match, which is invalid.
+ *
+ * Context Absorption Runtime:
+ * ---------------------------
+ * Absorption uses flags computed at planning time (in rpr.c) and two
+ * context-level flags maintained at runtime:
+ *
+ * State-level:
+ *   state.isAbsorbable: true if state is in the absorbable region.
+ *     - Set at creation: elem->flags & RPR_ELEM_ABSORBABLE_BRANCH
+ *     - At transition: prevAbsorbable && (newElem->flags & ABSORBABLE_BRANCH)
+ *     - Monotonic: once false, stays false forever
+ *
+ * Context-level:
+ *   ctx.hasAbsorbableState: can this context absorb others?
+ *     - True if at least one state has isAbsorbable=true
+ *     - Monotonic: true->false only (optimization: skip recalc when false)
+ *
+ *   ctx.allStatesAbsorbable: can this context be absorbed?
+ *     - True if ALL states have isAbsorbable=true
+ *     - Dynamic: can change false->true (when non-absorbable states die)
+ *
+ * Absorption Algorithm:
+ *   For each pair (older Ctx1, newer Ctx2):
+ *   1. Pre-check: Ctx1.hasAbsorbableState && Ctx2.allStatesAbsorbable
+ *      -> If false, skip (fast filter)
+ *   2. Coverage check: For each Ctx2 state with isAbsorbable=true,
+ *      find Ctx1 state with same elemIdx and count >= Ctx2.count
+ *   3. If all Ctx2 absorbable states are covered, absorb Ctx2
+ *
+ * Example: Pattern A+ B
+ *   Row 1: Ctx1 at A (count=1)
+ *   Row 2: Ctx1 at A (count=2), Ctx2 at A (count=1)
+ *   -> Both at same elemIdx (A), Ctx1.count >= Ctx2.count
+ *   -> Ctx2 absorbed
+ *
+ * The asymmetric design (Ctx1 needs hasAbsorbable, Ctx2 needs allAbsorbable)
+ * allows absorption even when Ctx1 has extra non-absorbable states.
  */
 
 /*
@@ -5003,6 +5115,39 @@ nfa_state_free_list(WindowAggState *winstate, RPRNFAState *list)
     }
 }
 
+/*
+ * nfa_state_clone
+ *
+ * Clone a 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)
+{
+	RPRPattern *pattern = winstate->rpPattern;
+	int			maxDepth = pattern->maxDepth;
+	RPRNFAState *state = nfa_state_alloc(winstate);
+	RPRPatternElement *elem = &pattern->elements[elemIdx];
+
+	state->elemIdx = elemIdx;
+	state->altPriority = altPriority;
+	if (counts != NULL && maxDepth > 0)
+		memcpy(state->counts, counts, sizeof(int32) * maxDepth);
+
+	/*
+	 * Compute isAbsorbable immediately at transition time.
+	 * isAbsorbable = sourceAbsorbable && (elem->flags & ABSORBABLE_BRANCH)
+	 * Monotonic: once false, stays false (can't re-enter absorbable region).
+	 */
+	state->isAbsorbable = sourceAbsorbable && RPRElemIsAbsorbableBranch(elem);
+
+	return state;
+}
+
 /*
  * nfa_states_equal
  *
@@ -5059,28 +5204,6 @@ nfa_add_state_unique(WindowAggState *winstate, RPRNFAContext *ctx, RPRNFAState *
 	return true;
 }
 
-/*
- * nfa_state_clone
- *
- * Clone a state with given elemIdx, altPriority and counts.
- * Caller is responsible for linking the returned state.
- */
-static RPRNFAState *
-nfa_state_clone(WindowAggState *winstate, int16 elemIdx, int16 altPriority,
-				int32 *counts)
-{
-	RPRPattern *pattern = winstate->rpPattern;
-	int			maxDepth = pattern->maxDepth;
-	RPRNFAState *state = nfa_state_alloc(winstate);
-
-	state->elemIdx = elemIdx;
-	state->altPriority = altPriority;
-	if (counts != NULL && maxDepth > 0)
-		memcpy(state->counts, counts, sizeof(int32) * maxDepth);
-
-	return state;
-}
-
 /*
  * nfa_add_matched_state
  *
@@ -5120,74 +5243,6 @@ nfa_add_matched_state(WindowAggState *winstate, RPRNFAContext *ctx,
 	}
 }
 
-/*
- * nfa_evaluate_row
- *
- * Evaluate all DEFINE variables for current row.
- * Returns true if the row exists, false if out of partition.
- * If row exists, fills varMatched array.
- * varMatched[i] = true if variable i matched at current row.
- */
-static bool
-nfa_evaluate_row(WindowObject winobj, int64 pos, bool *varMatched)
-{
-	WindowAggState *winstate = winobj->winstate;
-	ExprContext *econtext = winstate->ss.ps.ps_ExprContext;
-	int			numDefineVars = list_length(winstate->defineVariableList);
-	ListCell   *lc;
-	int			varIdx = 0;
-	TupleTableSlot *slot;
-
-	/*
-	 * Set up slots for current, previous, and next rows.
-	 * We don't call get_slots() here to avoid recursion through
-	 * row_is_in_frame -> update_reduced_frame -> nfa_match_pattern.
-	 */
-
-	/* Current row -> ecxt_outertuple */
-	slot = winstate->temp_slot_1;
-	if (!window_gettupleslot(winobj, pos, slot))
-		return false;	/* No row exists */
-	econtext->ecxt_outertuple = slot;
-
-	/* Previous row -> ecxt_scantuple (for PREV) */
-	if (pos > 0)
-	{
-		slot = winstate->prev_slot;
-		if (!window_gettupleslot(winobj, pos - 1, slot))
-			econtext->ecxt_scantuple = winstate->null_slot;
-		else
-			econtext->ecxt_scantuple = slot;
-	}
-	else
-		econtext->ecxt_scantuple = winstate->null_slot;
-
-	/* Next row -> ecxt_innertuple (for NEXT) */
-	slot = winstate->next_slot;
-	if (!window_gettupleslot(winobj, pos + 1, slot))
-		econtext->ecxt_innertuple = winstate->null_slot;
-	else
-		econtext->ecxt_innertuple = slot;
-
-	foreach(lc, winstate->defineClauseList)
-	{
-		ExprState  *exprState = (ExprState *) lfirst(lc);
-		Datum		result;
-		bool		isnull;
-
-		/* Evaluate DEFINE expression */
-		result = ExecEvalExpr(exprState, econtext, &isnull);
-
-		varMatched[varIdx] = (!isnull && DatumGetBool(result));
-
-		varIdx++;
-		if (varIdx >= numDefineVars)
-			break;
-	}
-
-	return true;	/* Row exists */
-}
-
 /*
  * nfa_context_alloc
  *
@@ -5216,6 +5271,11 @@ nfa_context_alloc(WindowAggState *winstate)
 	ctx->matchEndRow = -1;
 	ctx->lastProcessedRow = -1;
 	ctx->matchedState = NULL;
+	/* Initialize two-flag absorption design based on pattern */
+	ctx->hasAbsorbableState = (winstate->rpPattern != NULL &&
+							   winstate->rpPattern->isAbsorbable);
+	ctx->allStatesAbsorbable = (winstate->rpPattern != NULL &&
+								winstate->rpPattern->isAbsorbable);
 
 	/* Update statistics */
 	winstate->nfaContextsActive++;
@@ -5279,17 +5339,50 @@ 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.
  */
 static RPRNFAContext *
 nfa_start_context(WindowAggState *winstate, int64 startPos)
 {
 	RPRNFAContext *ctx;
+	RPRPattern *pattern = winstate->rpPattern;
 
 	ctx = nfa_context_alloc(winstate);
 	ctx->matchStartRow = startPos;
 	ctx->states = nfa_state_alloc(winstate);	/* initial state at elem 0 */
 
+	/*
+	 * Initialize two-flag absorption design:
+	 *   hasAbsorbableState: can this context absorb others? (≥1 absorbable state)
+	 *   allStatesAbsorbable: can this context be absorbed? (ALL states absorbable)
+	 * Both initialized from pattern->isAbsorbable at context start.
+	 */
+	ctx->hasAbsorbableState = (pattern != NULL && pattern->isAbsorbable);
+	ctx->allStatesAbsorbable = (pattern != NULL && pattern->isAbsorbable);
+
+	if (ctx->states != NULL && pattern != NULL && pattern->numElements > 0)
+	{
+		RPRPatternElement *elem = &pattern->elements[0];
+
+		/*
+		 * Initial state at element 0.
+		 * Check if element 0 is in absorbable branch.
+		 */
+		if (RPRElemIsAbsorbableBranch(elem))
+		{
+			/* Element 0 is in absorbable branch - flags stay true */
+			ctx->states->isAbsorbable = true;
+		}
+		else
+		{
+			/* Element 0 is NOT in absorbable branch - turn flags OFF */
+			ctx->hasAbsorbableState = false;
+			ctx->allStatesAbsorbable = false;
+			ctx->states->isAbsorbable = false;
+		}
+	}
+
 	/* Add to tail of active context list (doubly-linked, oldest-first) */
 	ctx->prev = winstate->nfaContextTail;
 	ctx->next = NULL;
@@ -5299,6 +5392,16 @@ nfa_start_context(WindowAggState *winstate, int64 startPos)
 		winstate->nfaContext = ctx;	/* first context becomes head */
 	winstate->nfaContextTail = ctx;
 
+	/*
+	 * Initial advance (divergence): expand ALT branches and create exit
+	 * states for VAR elements with min=0. This prepares the context for
+	 * the first row's match phase.
+	 *
+	 * Pass initialAdvance=true to prevent recording zero-length matches
+	 * when optional patterns can skip all VARs to reach FIN immediately.
+	 */
+	nfa_advance(winstate, ctx, startPos, true);
+
 	return ctx;
 }
 
@@ -5328,13 +5431,13 @@ nfa_find_context_for_pos(WindowAggState *winstate, int64 pos)
 }
 
 /*
- * update_nfa_length_stats
+ * nfa_update_length_stats
  *
  * Helper function to update min/max/total length statistics.
  * Called when tracking match/mismatch/absorbed/skipped lengths.
  */
 static void
-update_nfa_length_stats(int64 count, NFALengthStats *stats, int64 newLen)
+nfa_update_length_stats(int64 count, NFALengthStats *stats, int64 newLen)
 {
 	if (count == 1)
 	{
@@ -5352,14 +5455,14 @@ update_nfa_length_stats(int64 count, NFALengthStats *stats, int64 newLen)
 }
 
 /*
- * record_nfa_context_failure
+ * nfa_record_context_failure
  *
  * Record a failed context in statistics.
  * If failedLen == 1, count as pruned (failed on first row).
  * If failedLen > 1, count as mismatched and update length stats.
  */
 static void
-record_nfa_context_failure(WindowAggState *winstate, int64 failedLen)
+nfa_record_context_failure(WindowAggState *winstate, int64 failedLen)
 {
 	if (failedLen == 1)
 	{
@@ -5368,12 +5471,80 @@ record_nfa_context_failure(WindowAggState *winstate, int64 failedLen)
 	else
 	{
 		winstate->nfaMatchesFailed++;
-		update_nfa_length_stats(winstate->nfaMatchesFailed,
+		nfa_update_length_stats(winstate->nfaMatchesFailed,
 								&winstate->nfaFailLen,
 								failedLen);
 	}
 }
 
+/*
+ * nfa_evaluate_row
+ *
+ * Evaluate all DEFINE variables for current row.
+ * Returns true if the row exists, false if out of partition.
+ * If row exists, fills varMatched array.
+ * varMatched[i] = true if variable i matched at current row.
+ */
+static bool
+nfa_evaluate_row(WindowObject winobj, int64 pos, bool *varMatched)
+{
+	WindowAggState *winstate = winobj->winstate;
+	ExprContext *econtext = winstate->ss.ps.ps_ExprContext;
+	int			numDefineVars = list_length(winstate->defineVariableList);
+	ListCell   *lc;
+	int			varIdx = 0;
+	TupleTableSlot *slot;
+
+	/*
+	 * Set up slots for current, previous, and next rows.
+	 * We don't call get_slots() here to avoid recursion through
+	 * row_is_in_frame -> update_reduced_frame -> nfa_process_row.
+	 */
+
+	/* Current row -> ecxt_outertuple */
+	slot = winstate->temp_slot_1;
+	if (!window_gettupleslot(winobj, pos, slot))
+		return false;	/* No row exists */
+	econtext->ecxt_outertuple = slot;
+
+	/* Previous row -> ecxt_scantuple (for PREV) */
+	if (pos > 0)
+	{
+		slot = winstate->prev_slot;
+		if (!window_gettupleslot(winobj, pos - 1, slot))
+			econtext->ecxt_scantuple = winstate->null_slot;
+		else
+			econtext->ecxt_scantuple = slot;
+	}
+	else
+		econtext->ecxt_scantuple = winstate->null_slot;
+
+	/* Next row -> ecxt_innertuple (for NEXT) */
+	slot = winstate->next_slot;
+	if (!window_gettupleslot(winobj, pos + 1, slot))
+		econtext->ecxt_innertuple = winstate->null_slot;
+	else
+		econtext->ecxt_innertuple = slot;
+
+	foreach(lc, winstate->defineClauseList)
+	{
+		ExprState  *exprState = (ExprState *) lfirst(lc);
+		Datum		result;
+		bool		isnull;
+
+		/* Evaluate DEFINE expression */
+		result = ExecEvalExpr(exprState, econtext, &isnull);
+
+		varMatched[varIdx] = (!isnull && DatumGetBool(result));
+
+		varIdx++;
+		if (varIdx >= numDefineVars)
+			break;
+	}
+
+	return true;	/* Row exists */
+}
+
 /*
  * nfa_remove_contexts_up_to
  *
@@ -5403,7 +5574,7 @@ nfa_remove_contexts_up_to(WindowAggState *winstate, int64 endPos,
 			int64 skippedLen = ctx->lastProcessedRow - ctx->matchStartRow + 1;
 
 			winstate->nfaContextsSkipped++;
-			update_nfa_length_stats(winstate->nfaContextsSkipped,
+			nfa_update_length_stats(winstate->nfaContextsSkipped,
 									&winstate->nfaSkippedLen,
 									skippedLen);
 		}
@@ -5445,7 +5616,7 @@ nfa_cleanup_dead_contexts(WindowAggState *winstate, RPRNFAContext *excludeCtx)
 		if (ctx->lastProcessedRow >= ctx->matchStartRow)
 		{
 			int64 failedLen = ctx->lastProcessedRow - ctx->matchStartRow + 1;
-			record_nfa_context_failure(winstate, failedLen);
+			nfa_record_context_failure(winstate, failedLen);
 		}
 		/* else: context was never processed (beyond-partition), just remove */
 
@@ -5454,573 +5625,713 @@ nfa_cleanup_dead_contexts(WindowAggState *winstate, RPRNFAContext *excludeCtx)
 }
 
 /*
- * nfa_absorb_contexts
+ * nfa_update_absorption_flags
  *
- * Absorb newer contexts into older ones when states are fully covered.
- * For pattern like A+, if older context (started earlier) has count >= newer
- * context's count at the same element, the newer context produces subset matches.
+ * Update context's absorption flags after state changes.
  *
- * Absorption condition:
- * - For unbounded quantifiers (max=INT32_MAX): older.counts >= newer.counts
- * - For bounded quantifiers: older.counts == newer.counts
+ * Two flags control absorption behavior:
+ *   hasAbsorbableState: true if context has at least one absorbable state.
+ *     This flag is monotonic (true -> false only). Once all absorbable states
+ *     die, no new absorbable states can be created through transitions.
+ *   allStatesAbsorbable: true if ALL states in context are absorbable.
+ *     This flag is dynamic and can change false -> true when non-absorbable
+ *     states die off.
  *
- * Note: List is oldest-first (head=oldest, tail=newest), so we start from
- * tail (newest) and check if older contexts (via prev) can absorb it.
+ * Optimization: Once hasAbsorbableState becomes false, both flags remain false
+ * permanently, so we skip recalculation.
  */
 static void
-nfa_absorb_contexts(WindowAggState *winstate, RPRNFAContext *excludeCtx, int64 currentPos)
+nfa_update_absorption_flags(RPRNFAContext *ctx, RPRPattern *pattern)
 {
-	RPRPattern *pattern = winstate->rpPattern;
-	RPRNFAContext *ctx;
+	RPRNFAState *state;
+	bool		hasAbsorbable = false;
+	bool		allAbsorbable = true;
 
-	/* Need at least 2 contexts for absorption */
-	if (winstate->nfaContext == NULL || winstate->nfaContext->next == NULL)
+	/*
+	 * Optimization: Once hasAbsorbableState becomes false, it stays false.
+	 * No need to recalculate - both flags remain false permanently.
+	 */
+	if (!ctx->hasAbsorbableState)
+	{
+		ctx->allStatesAbsorbable = false;
 		return;
+	}
 
-	if (pattern == NULL)
+	/* No states means no absorbable states */
+	if (ctx->states == NULL)
+	{
+		ctx->hasAbsorbableState = false;
+		ctx->allStatesAbsorbable = false;
 		return;
+	}
 
-	/*
-	 * Only absorb for patterns marked as absorbable during planning.
-	 * See computeAbsorbability() in rpr.c for criteria.
-	 */
-	if (!pattern->isAbsorbable)
+	if (pattern == NULL)
 		return;
 
 	/*
-	 * Context absorption: A later context (started at higher row) can be
-	 * absorbed by an earlier context if ALL states in the later context
-	 * are "covered" by states in the earlier context.
-	 *
-	 * A later state is covered by an earlier state if:
-	 * 1. They are at the same element
-	 * 2. For unbounded elements (max == INT32_MAX): earlier.counts[d] >= later.counts[d]
-	 *    for all depths d
-	 * 3. For bounded elements: counts must be exactly equal at all depths
-	 *
-	 * List is oldest-first (head = lowest matchStartRow, tail = highest).
-	 * We iterate from tail (newest) via prev and check if older contexts can absorb it.
+	 * Iterate through all states to check absorption status.
+	 * Uses state->isAbsorbable which tracks if state is in absorbable region.
+	 * This is different from RPRElemIsAbsorbable(elem) which checks judgment point.
 	 */
-	ctx = winstate->nfaContextTail;
-
-	while (ctx != NULL)
+	for (state = ctx->states; state != NULL; state = state->next)
 	{
-		RPRNFAContext *nextCtx = ctx->prev;	/* traverse toward older (head) */
-		RPRNFAContext *older;
-		bool		absorbed = false;
+		if (state->isAbsorbable)
+			hasAbsorbable = true;
+		else
+			allAbsorbable = false;
+	}
 
-		/* Never absorb the excluded context (it's the target being completed) */
-		if (ctx == excludeCtx)
-		{
-			ctx = nextCtx;
-			continue;
-		}
+	ctx->hasAbsorbableState = hasAbsorbable;
+	ctx->allStatesAbsorbable = allAbsorbable;
+}
 
-		/* Skip contexts that haven't started processing yet (just created for future row) */
-		if (ctx->matchStartRow > currentPos)
-		{
-			ctx = nextCtx;
-			continue;
-		}
-
-		/*
-		 * Handle completed contexts (states == NULL) separately.
-		 * A completed context can be absorbed by an older completed context
-		 * if the older one has the same or later matchEndRow.
-		 */
-		if (ctx->states == NULL)
-		{
-			/* Only completed contexts with valid matchEndRow can be absorbed */
-			if (ctx->matchEndRow < 0)
-			{
-				ctx = nextCtx;
-				continue;
-			}
-
-			/* Check if any older completed context can absorb this one */
-			for (older = ctx->prev; older != NULL && !absorbed; older = older->prev)
-			{
-				/* Must have started earlier */
-				if (older->matchStartRow >= ctx->matchStartRow)
-					continue;
-
-				/* Older must also be completed with valid matchEndRow */
-				if (older->states != NULL || older->matchEndRow < 0)
-					continue;
+/*
+ * nfa_states_covered
+ *
+ * Check if all states in newer context are "covered" by older context.
+ *
+ * A newer state is covered when older context has an absorbable state at the
+ * same pattern element (elemIdx) with count >= newer's count at that depth.
+ * The covering state must be absorbable because only absorbable states can
+ * guarantee to produce superset matches.
+ *
+ * If all newer states are covered, newer context's eventual matches will be
+ * a subset of older context's matches, making newer redundant.
+ */
+static bool
+nfa_states_covered(RPRPattern *pattern, RPRNFAContext *older, RPRNFAContext *newer)
+{
+	RPRNFAState *newerState;
 
-				/*
-				 * Older context absorbs newer if it has the same or later
-				 * matchEndRow, meaning all matches from newer are subsets.
-				 */
-				if (older->matchEndRow >= ctx->matchEndRow)
-				{
-					/* Track absorbed context length statistics */
-					int64 absorbedLen = ctx->matchEndRow - ctx->matchStartRow + 1;
-
-					/* Absorb: remove ctx (newer) */
-					nfa_context_free(winstate, ctx);
-					winstate->nfaContextsAbsorbed++;
-					update_nfa_length_stats(winstate->nfaContextsAbsorbed,
-											&winstate->nfaAbsorbedLen,
-											absorbedLen);
-					absorbed = true;
-				}
-			}
+	for (newerState = newer->states; newerState != NULL; newerState = newerState->next)
+	{
+		RPRNFAState *olderState;
+		RPRPatternElement *elem;
+		int			depth;
+		bool		found = false;
 
-			ctx = nextCtx;
-			continue;
-		}
+		/* All states are absorbable (caller checks allStatesAbsorbable) */
+		elem = &pattern->elements[newerState->elemIdx];
+		depth = elem->depth;
 
-		/*
-		 * SIMPLIFIED ABSORPTION: Compare counts at depth 0 only.
-		 *
-		 * For absorbable patterns (A+ or (A B)+), we use a simple rule:
-		 * If older context has count[0] >= newer context's count[0] for
-		 * ANY state, older can absorb newer.
-		 *
-		 * This works because:
-		 * - A+: count[0] tracks how many A's matched
-		 * - (A B)+: count[0] tracks how many groups matched
-		 *
-		 * If older is ahead or equal, newer's future matches are subsets.
-		 */
-		for (older = ctx->prev; older != NULL && !absorbed; older = older->prev)
+		for (olderState = older->states; olderState != NULL; olderState = olderState->next)
 		{
-			int32		newerMaxCount = -1;
-			int32		olderMaxCount = -1;
-			RPRNFAState *s;
-
-			/* Skip if not started earlier */
-			if (older->matchStartRow >= ctx->matchStartRow)
-				continue;
-
-			/* Skip contexts that haven't started processing yet */
-			if (older->matchStartRow > currentPos)
-				continue;
-
-			/* For in-progress ctx, older must also be in-progress */
-			if (older->states == NULL)
-				continue;
-
-			/* Find maximum count[0] in newer context */
-			for (s = ctx->states; s != NULL; s = s->next)
+			/* Covering state must also be absorbable */
+			if (olderState->isAbsorbable &&
+				olderState->elemIdx == newerState->elemIdx &&
+				olderState->counts[depth] >= newerState->counts[depth])
 			{
-				if (s->counts[0] > newerMaxCount)
-					newerMaxCount = s->counts[0];
-			}
-
-			/* Find maximum count[0] in older context */
-			for (s = older->states; s != NULL; s = s->next)
-			{
-				if (s->counts[0] > olderMaxCount)
-					olderMaxCount = s->counts[0];
-			}
-
-			/* If older is ahead or equal, absorb newer */
-			if (olderMaxCount >= 0 && newerMaxCount >= 0 &&
-				olderMaxCount >= newerMaxCount)
-			{
-				int64 absorbedLen = ctx->lastProcessedRow - ctx->matchStartRow + 1;
-
-				nfa_context_free(winstate, ctx);
-				winstate->nfaContextsAbsorbed++;
-				update_nfa_length_stats(winstate->nfaContextsAbsorbed,
-										&winstate->nfaAbsorbedLen,
-										absorbedLen);
-				absorbed = true;
+				found = true;
+				break;
 			}
 		}
 
-		ctx = nextCtx;
+		if (!found)
+			return false;
 	}
+
+	return true;
 }
 
 /*
- * nfa_step
+ * nfa_try_absorb_context
+ *
+ * Try to absorb ctx (newer) into an older in-progress context.
+ * Returns true if ctx was absorbed and freed.
  *
- * Process all states in context through NFA for one row.
- * Calls nfa_step_single for each state.
+ * Absorption requires three conditions:
+ *   1. ctx must have all states absorbable (allStatesAbsorbable).
+ *      If ctx has any non-absorbable state, it may produce unique matches.
+ *   2. older must have at least one absorbable state (hasAbsorbableState).
+ *      Without absorbable states, older cannot cover newer's states.
+ *   3. All ctx states must be covered by older's absorbable states.
+ *      This ensures older will produce all matches that ctx would produce.
+ *
+ * Context list is ordered by creation time (oldest first via prev chain).
+ * Each row creates at most one context, so earlier contexts have smaller
+ * matchStartRow values.
  */
-static void
-nfa_step(WindowAggState *winstate, RPRNFAContext *ctx, bool *varMatched, int64 pos)
+static bool
+nfa_try_absorb_context(WindowAggState *winstate, RPRNFAContext *ctx)
 {
-	RPRNFAState *states = ctx->states;
-	RPRNFAState *state;
-	RPRNFAState *nextState;
-
-	ctx->states = NULL;
+	RPRPattern *pattern = winstate->rpPattern;
+	RPRNFAContext *older;
 
-	/* Track last processed row for failed match statistics */
-	if (pos > ctx->lastProcessedRow)
-		ctx->lastProcessedRow = pos;
+	/* Early exit: ctx must have all states absorbable */
+	if (!ctx->allStatesAbsorbable)
+		return false;
 
-	for (state = states; state != NULL; state = nextState)
+	for (older = ctx->prev; older != NULL; older = older->prev)
 	{
-		nextState = state->next;
-		state->next = NULL;
-		nfa_step_single(winstate, ctx, state, varMatched, pos);
+		/*
+		 * By invariant: ctx->prev chain is in creation order (oldest first),
+		 * and each row creates at most one context. So all contexts in this
+		 * chain have matchStartRow < ctx->matchStartRow.
+		 */
+
+		/* Older must also be in-progress */
+		if (older->states == NULL)
+			continue;
+
+		/* Older must have at least one absorbable state */
+		if (!older->hasAbsorbableState)
+			continue;
+
+		/* Check if all newer states are covered by older */
+		if (nfa_states_covered(pattern, older, ctx))
+		{
+			int64	absorbedLen = ctx->lastProcessedRow - ctx->matchStartRow + 1;
+
+			nfa_context_free(winstate, ctx);
+			winstate->nfaContextsAbsorbed++;
+			nfa_update_length_stats(winstate->nfaContextsAbsorbed,
+									&winstate->nfaAbsorbedLen,
+									absorbedLen);
+			return true;
+		}
 	}
+
+	return false;
 }
 
 /*
- * nfa_finalize_all_contexts
+ * nfa_absorb_contexts
  *
- * Finalize all active contexts when partition ends.
- * Calls nfa_step with NULL varMatched to complete without new row data.
+ * Absorb redundant contexts to reduce memory usage and computation.
+ *
+ * For patterns like A+, newer contexts starting later will produce subset
+ * matches of older contexts with higher counts. By absorbing these redundant
+ * contexts early, we avoid duplicate work.
+ *
+ * Iterates from tail (newest) toward head (oldest) via prev chain.
+ * Only in-progress contexts (states != NULL) are candidates for absorption;
+ * completed contexts represent valid match results.
  */
 static void
-nfa_finalize_all_contexts(WindowAggState *winstate, int64 lastPos)
+nfa_absorb_contexts(WindowAggState *winstate)
 {
 	RPRNFAContext *ctx;
+	RPRNFAContext *nextCtx;
 
-	for (ctx = winstate->nfaContext; ctx != NULL; ctx = ctx->next)
+	for (ctx = winstate->nfaContextTail; ctx != NULL; ctx = nextCtx)
 	{
+		nextCtx = ctx->prev;
+
+		/* Only absorb in-progress contexts; completed contexts are valid results */
 		if (ctx->states != NULL)
-			nfa_step(winstate, ctx, NULL, lastPos);
+			nfa_try_absorb_context(winstate, ctx);
 	}
 }
 
 /*
- * nfa_process_context
+ * nfa_eval_var_match
  *
- * Process one context for the current row.
- * Handles frame boundary check and NFA step.
+ * Evaluate if a VAR element matches the current row.
+ * Undefined variables (varId >= defineVariableList length) default to TRUE.
  */
-static void
-nfa_process_context(WindowAggState *winstate, RPRNFAContext *ctx,
-					int64 currentPos, bool hasLimitedFrame, int64 frameOffset)
+static inline bool
+nfa_eval_var_match(WindowAggState *winstate, RPRPatternElement *elem,
+				   bool *varMatched)
 {
-	/* Skip already-completed contexts */
-	if (ctx->states == NULL)
-		return;
-
-	/* Check frame boundary */
-	if (hasLimitedFrame)
-	{
-		int64 ctxFrameEnd = ctx->matchStartRow + frameOffset + 1;
-		if (currentPos >= ctxFrameEnd)
-		{
-			nfa_step(winstate, ctx, NULL, ctxFrameEnd - 1);
-			return;
-		}
-	}
-
-	/* Process states for this context */
-	nfa_step(winstate, ctx, winstate->nfaVarMatched, currentPos);
+	if (varMatched == NULL)
+		return false;
+	if (elem->varId >= list_length(winstate->defineVariableList))
+		return true;
+	return varMatched[elem->varId];
 }
 
 /*
- * nfa_step_single
+ * nfa_match
  *
- * Process one state through NFA for one row.
- * New states are added to ctx->states.
- * Match (FIN) is recorded in ctx->matchedState.
- * When FIN is reached by matching (not skipping), matchEndRow is updated.
+ * Match phase (convergence): evaluate VAR elements against current row.
+ * Only updates counts and removes dead states. Minimal transitions.
+ *
+ * For VAR elements:
+ *   - matched: count++, keep state (unless count > max)
+ *   - not matched: remove state (exit alternatives already exist from
+ *     previous advance when count >= min was satisfied)
+ *
+ * For simple VARs (min=max=1) followed by END:
+ *   - Advance to END and update group count before absorb phase
+ *   - This ensures absorption can compare states by group completion
+ *
+ * Non-VAR elements (ALT, END, FIN) are kept as-is for advance phase.
  */
 static void
-nfa_step_single(WindowAggState *winstate, RPRNFAContext *ctx,
-				RPRNFAState *state, bool *varMatched, int64 currentPos)
+nfa_match(WindowAggState *winstate, RPRNFAContext *ctx, bool *varMatched)
 {
 	RPRPattern *pattern = winstate->rpPattern;
 	RPRPatternElement *elements = pattern->elements;
-	RPRNFAState *pending = state;		/* states to process in current row */
-	RPRNFAState *pending_tail = state;	/* tail for FIFO append */
+	RPRNFAState **prevPtr = &ctx->states;
+	RPRNFAState *state;
 
-	while (pending != NULL)
+	/*
+	 * 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; )
 	{
-		RPRPatternElement *elem;
-		bool		matched;
-		int32		count;
-		int			depth;
-
-		/* Pop from head */
-		state = pending;
-		pending = pending->next;
-		if (pending == NULL)
-			pending_tail = NULL;
-		state->next = NULL;
-
-		Assert(state->elemIdx >= 0 && state->elemIdx < pattern->numElements);
-		elem = &elements[state->elemIdx];
-		depth = elem->depth;
+		RPRNFAState *nextState = state->next;
+		RPRPatternElement *elem = &elements[state->elemIdx];
 
 		if (RPRElemIsVar(elem))
 		{
-			/*
-			 * Variable: check if it matches current row.
-			 * If varMatched is NULL (boundary), all variables are forced to false.
-			 * Otherwise, varId < numDefines uses DEFINE expr,
-			 * varId >= numDefines defaults to TRUE.
-			 */
-			if (varMatched == NULL)
-				matched = false;
-			else
-			{
-				int numDefines = list_length(winstate->defineVariableList);
-
-				if (elem->varId >= numDefines)
-					matched = true;		/* Not defined in DEFINE, defaults to TRUE */
-				else
-					matched = varMatched[elem->varId];
-			}
+			bool	matched;
+			int		depth = elem->depth;
+			int32	count = state->counts[depth];
 
-			count = state->counts[depth];
+			matched = nfa_eval_var_match(winstate, elem, varMatched);
 
 			if (matched)
 			{
-				/*
-				 * Clamp count to prevent overflow with very large partitions.
-				 * Once count reaches RPR_COUNT_MAX, we stop incrementing.
-				 * This is safe because for unbounded quantifiers, only the
-				 * comparison with min matters, and for bounded quantifiers,
-				 * max is always < RPR_COUNT_MAX.
-				 */
+				/* Increment count */
 				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;
 				}
 
 				state->counts[depth] = count;
 
-				/* Can repeat more? Clone for staying */
-				if (elem->max == RPR_QUANTITY_INF || count < elem->max)
-				{
-					RPRNFAState *clone = nfa_state_clone(winstate, state->elemIdx,
-														 state->altPriority,
-														 state->counts);
-					nfa_add_state_unique(winstate, ctx, clone);
-				}
-
-				/* Satisfied? Advance to next element */
-				if (count >= elem->min)
+				/*
+				 * For simple VAR (min=max=1) with END next, advance to END
+				 * and update group count inline. This keeps state in place,
+				 * preserving lexical order.
+				 */
+				if (elem->min == 1 && elem->max == 1 && count == 1 &&
+					RPRElemIsEnd(&elements[elem->next]))
 				{
-					RPRPatternElement *nextElem;
+					RPRPatternElement *endElem = &elements[elem->next];
+					int		endDepth = endElem->depth;
+					int32	endCount = state->counts[endDepth];
 
-					state->counts[depth] = 0;
-					state->elemIdx = elem->next;
-					nextElem = &elements[state->elemIdx];
+					/* Increment group count with overflow protection */
+					if (endCount < RPR_COUNT_MAX)
+						endCount++;
 
-					if (RPRElemIsFin(nextElem))
-					{
-						/* Match ends at current row since we matched */
-						nfa_add_matched_state(winstate, ctx, state, currentPos);
-					}
-					else if (RPRElemIsEnd(nextElem))
+					/* Check END's max constraint */
+					if (endElem->max != RPR_QUANTITY_INF && endCount > endElem->max)
 					{
-						/*
-						 * END is epsilon transition - process immediately on same row.
-						 * This ensures match end position is recorded at the row where
-						 * the last VAR matched, not the next row.
-						 */
-						state->next = NULL;
-						if (pending_tail)
-							pending_tail->next = state;
-						else
-							pending = state;
-						pending_tail = state;
-					}
-					else
-					{
-						/*
-						 * VAR, ALT - wait for next row. ALT dispatches to VARs that
-						 * need input, so it must wait for the next row after VAR
-						 * consumed the current row.
-						 */
-						nfa_add_state_unique(winstate, ctx, state);
+						/* Exceeded END's max - remove state */
+						*prevPtr = nextState;
+						nfa_state_free(winstate, state);
+						state = nextState;
+						continue;
 					}
+
+					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
-				{
-					nfa_state_free(winstate, state);
-				}
+				/* else: stay at VAR for advance phase */
 			}
 			else
 			{
-				/* Not matched: can we skip? */
-				if (count >= elem->min)
-				{
-					RPRPatternElement *nextElem;
+				/*
+				 * Not matched - remove state.
+				 * Exit alternatives were already created by advance phase
+				 * when count >= min was satisfied.
+				 */
+				*prevPtr = nextState;
+				nfa_state_free(winstate, state);
+				state = nextState;
+				continue;
+			}
+		}
+		/* Non-VAR elements: keep as-is for advance phase */
 
-					state->counts[depth] = 0;
-					state->elemIdx = elem->next;
-					nextElem = &elements[state->elemIdx];
+		prevPtr = &state->next;
+		state = nextState;
+	}
+}
 
-					if (RPRElemIsFin(nextElem))
-					{
-						/* Match ends at previous row since current didn't match */
-						nfa_add_matched_state(winstate, ctx, state, currentPos - 1);
-					}
-					else if (RPRElemIsVar(nextElem))
-					{
-						/*
-						 * Current row was NOT consumed (skip case), so next VAR
-						 * must be tried on the SAME row via pending list
-						 */
-						state->next = NULL;
-						if (pending_tail)
-							pending_tail->next = state;
-						else
-							pending = state;
-						pending_tail = state;
-					}
-					else
-					{
-						state->next = NULL;
-						if (pending_tail)
-							pending_tail->next = state;
-						else
-							pending = state;
-						pending_tail = state;
-					}
-				}
-				else
-				{
-					nfa_state_free(winstate, state);
-				}
-			}
+/*
+ * nfa_route_to_elem
+ *
+ * Route state to next element. If VAR, add to ctx->states and process
+ * skip path if optional. Otherwise, continue epsilon expansion via recursion.
+ */
+static void
+nfa_route_to_elem(WindowAggState *winstate, RPRNFAContext *ctx,
+				  RPRNFAState *state, RPRPatternElement *nextElem,
+				  int64 currentPos, bool initialAdvance)
+{
+	if (RPRElemIsVar(nextElem))
+	{
+		nfa_add_state_unique(winstate, ctx, state);
+		if (RPRElemCanSkip(nextElem))
+		{
+			RPRNFAState *skipState;
+			skipState = nfa_state_clone(winstate, nextElem->next,
+										state->altPriority, state->counts,
+										state->isAbsorbable);
+			nfa_advance_state(winstate, ctx, skipState, currentPos, initialAdvance);
 		}
-		else if (RPRElemIsFin(elem))
+	}
+	else
+	{
+		nfa_advance_state(winstate, ctx, state, currentPos, initialAdvance);
+	}
+}
+
+/*
+ * nfa_advance_alt
+ *
+ * Handle ALT element: expand all branches in lexical order (DFS).
+ * Sets altPriority to element index to preserve lexical order for match selection.
+ */
+static void
+nfa_advance_alt(WindowAggState *winstate, RPRNFAContext *ctx,
+				RPRNFAState *state, RPRPatternElement *elem,
+				int64 currentPos, bool initialAdvance)
+{
+	RPRPattern *pattern = winstate->rpPattern;
+	RPRPatternElement *elements = pattern->elements;
+	RPRElemIdx	altIdx = elem->next;
+	bool		first = true;
+
+	while (altIdx >= 0 && altIdx < pattern->numElements)
+	{
+		RPRPatternElement *altElem = &elements[altIdx];
+		RPRNFAState *newState;
+
+		if (first)
 		{
-			/* Already at FIN - match ends at current row */
-			nfa_add_matched_state(winstate, ctx, state, currentPos);
+			state->elemIdx = altIdx;
+			state->altPriority = altIdx;
+			newState = state;
+			first = false;
 		}
-		else if (RPRElemIsAlt(elem))
+		else
 		{
-			RPRElemIdx	altIdx = elem->next;
-			bool		first = true;
+			newState = nfa_state_clone(winstate, altIdx, altIdx,
+									   state->counts, state->isAbsorbable);
+		}
 
-			/*
-			 * ALT doesn't consume a row - it's just a dispatch point.
-			 * All branches should be evaluated on the CURRENT row.
-			 * Set altPriority to branch's elemIdx for lexical order tracking.
-			 * Append to pending_tail to maintain lexical order.
-			 */
-			while (altIdx >= 0 && altIdx < pattern->numElements)
-			{
-				RPRPatternElement *altElem = &elements[altIdx];
-				RPRNFAState *newState;
+		/* Recursively process this branch before next */
+		nfa_advance_state(winstate, ctx, newState, currentPos, initialAdvance);
+		altIdx = altElem->jump;
+	}
 
-				if (first)
-				{
-					state->elemIdx = altIdx;
-					state->altPriority = altIdx;
-					newState = state;
-					first = false;
-				}
-				else
-				{
-					newState = nfa_state_clone(winstate, altIdx, altIdx,
-											   state->counts);
-				}
+	if (first)
+		nfa_state_free(winstate, state);
+}
 
-				/* Append to tail for lexical order */
-				newState->next = NULL;
-				if (pending_tail)
-					pending_tail->next = newState;
-				else
-					pending = newState;
-				pending_tail = newState;
+/*
+ * nfa_advance_end
+ *
+ * Handle END element: group repetition logic.
+ * Decides whether to loop back or exit based on count vs min/max.
+ */
+static void
+nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
+				RPRNFAState *state, RPRPatternElement *elem,
+				int64 currentPos, bool initialAdvance)
+{
+	RPRPattern *pattern = winstate->rpPattern;
+	RPRPatternElement *elements = pattern->elements;
+	int		depth = elem->depth;
+	int32	count = state->counts[depth];
 
-				altIdx = altElem->jump;
-			}
+	if (count < elem->min)
+	{
+		/* Must loop back */
+		RPRPatternElement *jumpElem;
 
-			if (first)
-				nfa_state_free(winstate, state);
-		}
-		else if (RPRElemIsEnd(elem))
-		{
-			count = state->counts[depth] + 1;
+		for (int d = depth + 1; d < pattern->maxDepth; d++)
+			state->counts[d] = 0;
+		state->elemIdx = elem->jump;
+		jumpElem = &elements[state->elemIdx];
 
-			if (count < elem->min)
-			{
-				/*
-				 * Haven't reached minimum yet - must loop back.
-				 * Add to ctx->states (next row) not pending (same row).
-				 */
-				state->counts[depth] = count;
-				for (int d = depth + 1; d < pattern->maxDepth; d++)
-					state->counts[d] = 0;
-				state->elemIdx = elem->jump;
-				nfa_add_state_unique(winstate, ctx, state);
-			}
-			else if (elem->max != RPR_QUANTITY_INF && count >= elem->max)
-			{
-				/* Reached maximum - must exit, continue processing */
-				state->counts[depth] = 0;
-				state->elemIdx = elem->next;
-				state->next = NULL;
-				if (pending_tail)
-					pending_tail->next = state;
-				else
-					pending = state;
-				pending_tail = state;
-			}
+		nfa_route_to_elem(winstate, ctx, state, jumpElem, currentPos, initialAdvance);
+	}
+	else if (elem->max != RPR_QUANTITY_INF && count >= elem->max)
+	{
+		/* Must exit */
+		RPRPatternElement *nextElem;
+
+		state->counts[depth] = 0;
+		state->elemIdx = elem->next;
+		nextElem = &elements[state->elemIdx];
+
+		/* END→END: increment outer END's count */
+		if (RPRElemIsEnd(nextElem) && state->counts[nextElem->depth] < RPR_COUNT_MAX)
+			state->counts[nextElem->depth]++;
+
+		nfa_route_to_elem(winstate, ctx, state, nextElem, currentPos, initialAdvance);
+	}
+	else
+	{
+		/* Between min and max - can exit or loop */
+		RPRElemIdx	exitAltPriority;
+		RPRNFAState *exitState;
+		RPRPatternElement *jumpElem;
+		RPRPatternElement *nextElem;
+
+		/* Preserve altPriority for greedy extension */
+		exitAltPriority = state->altPriority;
+		if (ctx->matchedState != NULL)
+			exitAltPriority = ctx->matchedState->altPriority;
+
+		/* Create exit state first (need original counts before modifying state) */
+		exitState = nfa_state_clone(winstate, elem->next, exitAltPriority,
+									state->counts, state->isAbsorbable);
+		exitState->counts[depth] = 0;
+		nextElem = &elements[exitState->elemIdx];
+
+		/* END→END: increment outer END's count */
+		if (RPRElemIsEnd(nextElem) && exitState->counts[nextElem->depth] < RPR_COUNT_MAX)
+			exitState->counts[nextElem->depth]++;
+
+		/* 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;
+		jumpElem = &elements[state->elemIdx];
+
+		nfa_route_to_elem(winstate, ctx, state, jumpElem, currentPos, initialAdvance);
+
+		/* Then route exit state */
+		nfa_route_to_elem(winstate, ctx, exitState, nextElem, currentPos, initialAdvance);
+	}
+}
+
+/*
+ * nfa_advance_var
+ *
+ * Handle VAR element: loop/exit transitions.
+ * After match phase, all VAR states have matched - decide next action.
+ */
+static void
+nfa_advance_var(WindowAggState *winstate, RPRNFAContext *ctx,
+				RPRNFAState *state, RPRPatternElement *elem,
+				int64 currentPos, bool initialAdvance)
+{
+	RPRPattern *pattern = winstate->rpPattern;
+	RPRPatternElement *elements = pattern->elements;
+	int		depth = elem->depth;
+	int32	count = state->counts[depth];
+	bool	canLoop = (elem->max == RPR_QUANTITY_INF || count < elem->max);
+	bool	canExit = (count >= elem->min);
+
+	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);
+		nfa_add_state_unique(winstate, ctx, loopState);
+
+		/* Exit: advance to next element */
+		state->counts[depth] = 0;
+		state->elemIdx = elem->next;
+		nextElem = &elements[state->elemIdx];
+
+		nfa_route_to_elem(winstate, ctx, state, nextElem, currentPos, initialAdvance);
+	}
+	else if (canLoop)
+	{
+		/* Loop only: keep state as-is */
+		nfa_add_state_unique(winstate, ctx, state);
+	}
+	else if (canExit)
+	{
+		/* Exit only: modify state */
+		RPRPatternElement *nextElem;
+
+		state->counts[depth] = 0;
+		state->elemIdx = elem->next;
+		nextElem = &elements[state->elemIdx];
+
+		nfa_route_to_elem(winstate, ctx, state, nextElem, currentPos, initialAdvance);
+	}
+	else
+	{
+		/* Neither - shouldn't happen, free state */
+		nfa_state_free(winstate, state);
+	}
+}
+
+/*
+ * nfa_advance_state
+ *
+ * Recursively process a single state through epsilon transitions.
+ * Uses DFS traversal to maintain lexical order: lower altPriority paths
+ * are fully processed before higher altPriority paths, ensuring states
+ * are added to ctx->states in lexical order.
+ */
+static void
+nfa_advance_state(WindowAggState *winstate, RPRNFAContext *ctx,
+				  RPRNFAState *state, int64 currentPos, bool initialAdvance)
+{
+	RPRPattern *pattern = winstate->rpPattern;
+	RPRPatternElement *elem;
+
+	Assert(state->elemIdx >= 0 && state->elemIdx < pattern->numElements);
+	elem = &pattern->elements[state->elemIdx];
+
+	switch (elem->varId)
+	{
+		case RPR_VARID_FIN:
+			/* FIN: record match (skip for initial advance) */
+			if (!initialAdvance)
+				nfa_add_matched_state(winstate, ctx, state, currentPos);
 			else
-			{
-				/*
-				 * Between min and max - can exit or continue.
-				 * Exit state handling depends on what comes next:
-				 * - If next is FIN: process on same row (match ends here)
-				 * - If next is VAR/ALT: wait for next row (needs new input)
-				 * Loop state always waits for next row.
-				 */
-				RPRPatternElement *nextElem = &elements[elem->next];
-				RPRElemIdx exitAltPriority;
-				RPRNFAState *exitState;
+				nfa_state_free(winstate, state);
+			break;
 
-				/*
-				 * For greedy extension: if context already has a match recorded,
-				 * preserve its altPriority. This ensures that iterations of a
-				 * quantified group don't compete on lexical order - the first
-				 * iteration's choice sets the preference, and subsequent
-				 * iterations can extend it if they produce a longer match.
-				 */
-				exitAltPriority = state->altPriority;
-				if (ctx->matchedState != NULL)
-					exitAltPriority = ctx->matchedState->altPriority;
+		case RPR_VARID_ALT:
+			nfa_advance_alt(winstate, ctx, state, elem, currentPos, initialAdvance);
+			break;
 
-				exitState = nfa_state_clone(winstate, elem->next,
-											exitAltPriority,
-											state->counts);
-				exitState->counts[depth] = 0;
+		case RPR_VARID_END:
+			nfa_advance_end(winstate, ctx, state, elem, currentPos, initialAdvance);
+			break;
 
-				if (RPRElemIsFin(nextElem))
-				{
-					/* Match ends at current row - append to pending */
-					exitState->next = NULL;
-					if (pending_tail)
-						pending_tail->next = exitState;
-					else
-						pending = exitState;
-					pending_tail = exitState;
-				}
-				else
-				{
-					/* Next element (VAR/ALT) needs new row input */
-					nfa_add_state_unique(winstate, ctx, exitState);
-				}
+		default:
+			/* VAR element */
+			nfa_advance_var(winstate, ctx, state, elem, currentPos, initialAdvance);
+			break;
+	}
+}
 
-				state->counts[depth] = count;
-				for (int d = depth + 1; d < pattern->maxDepth; d++)
-					state->counts[d] = 0;
-				state->elemIdx = elem->jump;
-				nfa_add_state_unique(winstate, ctx, state);
+/*
+ * nfa_advance
+ *
+ * Advance phase (divergence): transition from all surviving states.
+ * Called after match phase with matched VAR states, or at context creation
+ * for initial epsilon expansion (initialAdvance=true skips FIN matches).
+ *
+ * Processes states in order, using recursive DFS to maintain lexical order.
+ */
+static void
+nfa_advance(WindowAggState *winstate, RPRNFAContext *ctx, int64 currentPos,
+			bool initialAdvance)
+{
+	RPRNFAState *states = ctx->states;
+	RPRNFAState *state;
+
+	ctx->states = NULL;	/* Will rebuild */
+
+	/* Process each state in order */
+	while (states != NULL)
+	{
+		state = states;
+		states = states->next;
+		state->next = NULL;
+
+		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;
 			}
 		}
-		else
+
+		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)
 		{
-			state->elemIdx = elem->next;
-			state->next = NULL;
-			if (pending_tail)
-				pending_tail->next = state;
-			else
-				pending = state;
-			pending_tail = state;
+			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/createplan.c b/src/backend/optimizer/plan/createplan.c
index 99180ba339e..00dbcaed72c 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -2556,7 +2556,7 @@ create_windowagg_plan(PlannerInfo *root, WindowAggPath *best_path)
 							  ordCollations,
 							  best_path->runCondition,
 							  wc->rpSkipTo,
-							  buildRPRPattern(wc->rpPattern, defineVariableList),
+							  buildRPRPattern(wc->rpPattern, defineVariableList, wc->rpSkipTo, wc->frameOptions),
 							  wc->defineClause,
 							  best_path->qual,
 							  best_path->topwindow,
diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index 9b847c5e8b1..6883bc5466d 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -6,6 +6,25 @@
  * This file contains functions for optimizing RPR pattern AST and
  * compiling it to bytecode for execution by WindowAgg.
  *
+ * Key components:
+ *   1. Pattern Optimization: Simplifies patterns before compilation
+ *      (e.g., flatten nested SEQ/ALT, merge consecutive vars)
+ *   2. Pattern Compilation: Converts AST to flat element array for NFA
+ *   3. Absorption Analysis: Computes flags for O(n^2)->O(n) optimization
+ *
+ * Context Absorption Optimization:
+ *   When a pattern starts with an unbounded element (e.g., A+ or (A B)+),
+ *   newer contexts cannot produce longer matches than older contexts.
+ *   By absorbing (eliminating) redundant newer contexts, we reduce
+ *   complexity from O(n^2) to O(n) for patterns like A+ B.
+ *
+ *   The absorption analysis uses two element flags:
+ *   - RPR_ELEM_ABSORBABLE: marks WHERE to compare (judgment point)
+ *   - RPR_ELEM_ABSORBABLE_BRANCH: marks the absorbable region
+ *
+ *   See computeAbsorbability() and the detailed comments before
+ *   isUnboundedStart() for the full design explanation.
+ *
  * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
  * Portions Copyright (c) 1994, Regents of the University of California
  *
@@ -1124,14 +1143,13 @@ finalizeRPRPattern(RPRPattern *result)
 	int			i;
 	RPRPatternElement *finElem;
 
-	/* Set up next pointers for elements that don't have one yet */
+	/* Set up next pointers for elements that don't have one */
 	for (i = 0; i < finIdx; i++)
 	{
-		if (result->elements[i].next == RPR_ELEMIDX_INVALID)
-		{
-			/* Point to next element, or to FIN if this is the last */
-			result->elements[i].next = (i < finIdx - 1) ? i + 1 : finIdx;
-		}
+		RPRPatternElement *elem = &result->elements[i];
+
+		if (elem->next == RPR_ELEMIDX_INVALID)
+			elem->next = (i < finIdx - 1) ? i + 1 : finIdx;
 	}
 
 	/* Add FIN marker at the end */
@@ -1145,6 +1163,87 @@ finalizeRPRPattern(RPRPattern *result)
 	finElem->jump = RPR_ELEMIDX_INVALID;
 }
 
+/*-------------------------------------------------------------------------
+ * CONTEXT ABSORPTION: TWO-FLAG DESIGN
+ *-------------------------------------------------------------------------
+ *
+ * Context absorption eliminates redundant match searches by absorbing newer
+ * contexts that cannot produce longer matches than older contexts. This
+ * achieves O(n^2) -> O(n) performance improvement for patterns like A+ B.
+ *
+ * Core Insight:
+ *   For pattern A+ B, if Ctx1 starts at row 0 and Ctx2 starts at row 1,
+ *   both matching A continuously, Ctx1 will always have more A matches.
+ *   When B finally appears, Ctx1's match (0 to current) is always longer
+ *   than Ctx2's match (1 to current). So Ctx2 can be safely eliminated.
+ *
+ * Two Flags:
+ *   1. RPR_ELEM_ABSORBABLE - "Absorption judgment point"
+ *      WHERE contexts can be compared for absorption.
+ *      - Simple unbounded VAR (A+): the VAR element itself
+ *      - Unbounded GROUP ((A B)+): the END element only
+ *
+ *   2. RPR_ELEM_ABSORBABLE_BRANCH - "Absorbable region marker"
+ *      ALL elements within the absorbable region.
+ *      - Used for tracking state.isAbsorbable at runtime
+ *      - States leaving this region become non-absorbable permanently
+ *
+ * Why Two Flags?
+ *   For pattern "(A B)+", contexts at different positions (one at A,
+ *   another at B) cannot be compared - they must synchronize at END.
+ *
+ *   Example: "(A B)+" with input A B A B A B...
+ *     Row 0 (A): Ctx1 starts, matches A
+ *     Row 1 (B): Ctx1 matches B -> END (count=1)
+ *     Row 2 (A): Ctx1 loops to A, Ctx2 starts at A
+ *     Row 3 (B): Ctx1 at END (count=2), Ctx2 at END (count=1)
+ *                -> Both at END, comparable! Ctx1 absorbs Ctx2.
+ *
+ *   Contexts synchronize at END every group-length rows. Therefore:
+ *   - ABSORBABLE marks END as judgment point (where to compare)
+ *   - ABSORBABLE_BRANCH keeps state.isAbsorbable=true through A->B->END
+ *
+ * Pattern Examples:
+ *
+ *   Pattern: A+ B
+ *   Element 0 (A): ABSORBABLE | ABSORBABLE_BRANCH  <- judgment point
+ *   Element 1 (B): (none)
+ *   -> Compare at A every row. When contexts move to B, absorption stops.
+ *
+ *   Pattern: (A B)+ C
+ *   Element 0 (A): ABSORBABLE_BRANCH
+ *   Element 1 (B): ABSORBABLE_BRANCH
+ *   Element 2 (END): ABSORBABLE | ABSORBABLE_BRANCH  <- judgment point
+ *   Element 3 (C): (none)
+ *   -> Compare at END every 2 rows. When contexts move to C, absorption stops.
+ *
+ *   Pattern: (A+ B+)+ C
+ *   Element 0 (A): ABSORBABLE | ABSORBABLE_BRANCH  <- only first A+ flagged
+ *   Element 1 (B): (none)
+ *   Element 2 (END): (none)
+ *   Element 3 (C): (none)
+ *   -> Only first unbounded portion (A+) gets flags. Absorption happens
+ *      at A during first iteration. After moving to B+, absorption stops.
+ *
+ * First Unbounded Portion Strategy:
+ *   The algorithm only flags the FIRST unbounded portion starting from
+ *   element 0. This is sufficient because:
+ *   - Absorption in first portion already achieves O(n) complexity
+ *   - Later portions have different synchronization characteristics
+ *   - Nested unbounded patterns are too complex for simple absorption
+ *   - Complex patterns (nested groups, etc.) naturally die from mismatch
+ *
+ * Runtime Usage (in nodeWindowAgg.c):
+ *   - state.isAbsorbable = (previous && elem.ABSORBABLE_BRANCH)
+ *   - Monotonic: once false, stays false (cannot re-enter region)
+ *   - context.hasAbsorbableState: can absorb others (>=1 absorbable state)
+ *   - context.allStatesAbsorbable: can be absorbed (ALL states absorbable)
+ *   - Absorption check: if Ctx1.hasAbsorbable && Ctx2.allAbsorbable,
+ *     compare counts at same elemIdx, absorb if Ctx1.count >= Ctx2.count
+ *
+ *-------------------------------------------------------------------------
+ */
+
 /*
  * isUnboundedStart
  *		Check if the element at idx starts an unbounded sequence.
@@ -1152,6 +1251,11 @@ finalizeRPRPattern(RPRPattern *result)
  * For context absorption to work, the sequence starting at idx must be
  * unbounded (max = infinity) so that we can "shift" by decrementing count.
  *
+ * Algorithm:
+ *   - Descend through ALT/GROUP structures to find first actual VAR
+ *   - For GROUP: must be unbounded AND contain only simple {1,1} VARs
+ *   - Check if that VAR is unbounded
+ *
  * 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
@@ -1180,7 +1284,7 @@ isUnboundedStart(RPRPattern *pattern, RPRElemIdx idx)
 		e = &pattern->elements[i];
 
 		/* Exit when depth drops (left the group) or reached FIN */
-		if (e->depth < startDepth || e->varId == RPR_VARID_FIN)
+		if (e->depth < startDepth || RPRElemIsFin(e))
 		{
 			lastElem = e;
 			break;
@@ -1202,7 +1306,7 @@ isUnboundedStart(RPRPattern *pattern, RPRElemIdx idx)
 	Assert(lastElem != NULL);
 
 	/* Case 2: Unbounded group - END element points back to idx */
-	if (lastElem->varId == RPR_VARID_END &&
+	if (RPRElemIsEnd(lastElem) &&
 		lastElem->jump == idx &&
 		lastElem->max == RPR_QUANTITY_INF)
 	{
@@ -1219,9 +1323,9 @@ isUnboundedStart(RPRPattern *pattern, RPRElemIdx idx)
 		{
 			RPRPatternElement *e = &pattern->elements[j];
 
-			if (e->varId == RPR_VARID_FIN)
+			if (RPRElemIsFin(e))
 				break;			/* Reached end, no outer nesting */
-			if (e->varId == RPR_VARID_END && e->depth < lastElem->depth)
+			if (RPRElemIsEnd(e) && e->depth < lastElem->depth)
 				return false;	/* Found outer END, nested structure */
 		}
 
@@ -1232,24 +1336,99 @@ isUnboundedStart(RPRPattern *pattern, RPRElemIdx idx)
 	return RPRElemIsVar(elem) && elem->max == RPR_QUANTITY_INF;
 }
 
+/*
+ * setAbsorbabilityFlagsForBranch
+ *		Set absorption flags for a single branch.
+ *
+ * For unbounded GROUP (e.g., (A B)+):
+ *   - ABSORBABLE_BRANCH on all elements (A, B, END)
+ *   - ABSORBABLE on END only (judgment point)
+ *
+ * For simple unbounded VAR (e.g., A+):
+ *   - Both flags on first element only
+ */
+static void
+setAbsorbabilityFlagsForBranch(RPRPattern *pattern, RPRElemIdx branchIdx)
+{
+	RPRPatternElement *branchFirst = &pattern->elements[branchIdx];
+	RPRDepth	startDepth = branchFirst->depth;
+	bool		isUnboundedGroup = false;
+	RPRElemIdx	endIdx = RPR_ELEMIDX_INVALID;
+	RPRElemIdx	i;
+
+	/*
+	 * First pass: detect if this is an unbounded GROUP pattern.
+	 * Look for END element with unbounded max that jumps back to branch start.
+	 * Note: END is at parent depth (less than content depth), so check END
+	 * before the depth-based break condition.
+	 */
+	for (i = branchIdx; i < pattern->numElements; i++)
+	{
+		RPRPatternElement *e = &pattern->elements[i];
+
+		/* Check for unbounded END first (END is at lower depth than content) */
+		if (RPRElemIsEnd(e) &&
+			e->jump == branchIdx &&
+			e->max == RPR_QUANTITY_INF)
+		{
+			isUnboundedGroup = true;
+			endIdx = i;
+			break;
+		}
+
+		if (e->depth < startDepth || RPRElemIsFin(e))
+			break;
+	}
+
+	/*
+	 * Set flags based on pattern type.
+	 */
+	if (isUnboundedGroup)
+	{
+		/*
+		 * Unbounded GROUP: set ABSORBABLE_BRANCH on all elements from
+		 * branchIdx to endIdx, and ABSORBABLE on END only.
+		 */
+		for (i = branchIdx; i <= endIdx; i++)
+		{
+			RPRPatternElement *e = &pattern->elements[i];
+
+			e->flags |= RPR_ELEM_ABSORBABLE_BRANCH;
+			if (i == endIdx)
+				e->flags |= RPR_ELEM_ABSORBABLE;
+		}
+	}
+	else
+	{
+		/*
+		 * Simple unbounded VAR: set both flags on first element only.
+		 */
+		branchFirst->flags |= RPR_ELEM_ABSORBABLE_BRANCH | RPR_ELEM_ABSORBABLE;
+	}
+}
+
 /*
  * computeAbsorbability
  *		Determine if pattern supports context absorption optimization.
  *
- * Context absorption allows the executor to reuse NFA match states from
- * previous row positions. When a match at position P can be "shifted" to
- * position P+1 by decrementing the count of the first unbounded element,
- * we avoid re-running the NFA from scratch.
+ * 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 function sets RPR_ELEM_ABSORBABLE on elements that can be shifted,
- * and pattern->isAbsorbable if any element is absorbable.
+ * 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 at same depth as unbounded start
  *
  * Examples:
- *   A+ B C      - absorbable (unbounded var at start)
- *   (A B){2,} C - absorbable (unbounded group)
- *   A B+        - NOT absorbable (unbounded not at start)
- *   A+ | B+ C   - both branches absorbable (checked independently)
- *   A+ B | C D  - first branch only
+ *   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+)+       - 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)
  */
 static void
 computeAbsorbability(RPRPattern *pattern)
@@ -1261,10 +1440,11 @@ computeAbsorbability(RPRPattern *pattern)
 	if (pattern->numElements < 2)	/* At minimum: one element + FIN */
 		return;
 
-	if (first->varId == RPR_VARID_ALT)
+	if (RPRElemIsAlt(first))
 	{
 		/* ALT: check each branch */
 		RPRElemIdx	branchIdx = first->next;
+		bool		hasAbsorbableBranch = false;
 
 		while (branchIdx != RPR_ELEMIDX_INVALID &&
 			   branchIdx < pattern->numElements - 1)
@@ -1273,20 +1453,28 @@ computeAbsorbability(RPRPattern *pattern)
 
 			if (isUnboundedStart(pattern, branchIdx))
 			{
-				branchFirst->flags |= RPR_ELEM_ABSORBABLE;
 				pattern->isAbsorbable = true;
+				hasAbsorbableBranch = true;
+				setAbsorbabilityFlagsForBranch(pattern, branchIdx);
 			}
 
 			branchIdx = branchFirst->jump;
 		}
+
+		/*
+		 * If any branch is absorbable, mark ALT element too.
+		 * This allows efficient branch-level flag management.
+		 */
+		if (hasAbsorbableBranch)
+			first->flags |= RPR_ELEM_ABSORBABLE_BRANCH;
 	}
 	else
 	{
 		/* Non-ALT: check first element */
 		if (isUnboundedStart(pattern, 0))
 		{
-			first->flags |= RPR_ELEM_ABSORBABLE;
 			pattern->isAbsorbable = true;
+			setAbsorbabilityFlagsForBranch(pattern, 0);
 		}
 	}
 }
@@ -1307,7 +1495,8 @@ computeAbsorbability(RPRPattern *pattern)
  * Returns NULL if pattern is NULL.
  */
 RPRPattern *
-buildRPRPattern(RPRPatternNode *pattern, List *defineVariableList)
+buildRPRPattern(RPRPatternNode *pattern, List *defineVariableList,
+				RPSkipTo rpSkipTo, int frameOptions)
 {
 	RPRPattern *result;
 	RPRPatternNode *optimized;
@@ -1316,6 +1505,7 @@ buildRPRPattern(RPRPatternNode *pattern, List *defineVariableList)
 	int			numElements;
 	RPRDepth	maxDepth;
 	int			idx;
+	bool		hasLimitedFrame;
 
 	if (pattern == NULL)
 		return NULL;
@@ -1336,11 +1526,28 @@ buildRPRPattern(RPRPatternNode *pattern, List *defineVariableList)
 	idx = 0;
 	fillRPRPattern(optimized, result, &idx, 0);
 
-	/* Finalize: set up next pointers and add FIN marker */
+	/* Finalize: set up next pointers, flags, and add FIN marker */
 	finalizeRPRPattern(result);
 
-	/* Compute context absorption eligibility */
-	computeAbsorbability(result);
+	/*
+	 * Compute context absorption eligibility.
+	 * Absorption requires both structural absorbability and runtime conditions.
+	 * Check runtime conditions first to avoid unnecessary pattern analysis.
+	 */
+	hasLimitedFrame = (frameOptions & FRAMEOPTION_ROWS) &&
+					  !(frameOptions & FRAMEOPTION_END_UNBOUNDED_FOLLOWING);
+
+	if (rpSkipTo == ST_PAST_LAST_ROW && !hasLimitedFrame)
+	{
+		/* Runtime conditions met - check structural absorbability */
+		computeAbsorbability(result);
+	}
+	else
+	{
+		/* Runtime conditions not met - skip pattern analysis */
+		result->isAbsorbable = false;
+	}
 
 	return result;
 }
+
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 984316d9a61..a0fbdb196d4 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -20324,6 +20324,11 @@ makeRPRQuantifier(int min, int max, bool reluctant)
 {
 	RPRPatternNode *n = makeNode(RPRPatternNode);
 
+	if (reluctant)
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("reluctant quantifiers are not yet supported")));
+
 	n->min = min;
 	n->max = max;
 	n->reluctant = reluctant;
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index a25d7642d3a..8bc51e3750b 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -2523,17 +2523,34 @@ typedef enum WindowAggStatus
  *
  * counts[] tracks repetition counts at each nesting depth.
  * altPriority tracks lexical order for alternation (lower = earlier alternative).
+ *
+ * isAbsorbable tracks if state is in absorbable region (ABSORBABLE_BRANCH).
+ * Monotonic property: once false, stays false (can't re-enter region).
+ *
+ * Absorption comparison uses elemIdx and counts[depth] directly because:
+ *   - VAR elements consume a row, forcing states to wait for next row
+ *   - END loop puts states at group start with iteration count preserved
+ *   - At row end, comparable states are at the same position (elemIdx)
  */
 typedef struct RPRNFAState
 {
 	struct RPRNFAState *next;	/* next state in linked list */
 	int16		elemIdx;		/* current pattern element index */
 	int16		altPriority;	/* lexical order priority (lower = preferred) */
+	bool		isAbsorbable;	/* true if state is in absorbable region */
 	int32		counts[FLEXIBLE_ARRAY_MEMBER];	/* repetition counts by depth */
 } RPRNFAState;
 
 /*
  * RPRNFAContext - context for NFA pattern matching execution
+ *
+ * Two-flag absorption design:
+ *   hasAbsorbableState: can this context absorb others? (≥1 absorbable state)
+ *     - Monotonic: true→false only, cannot recover once false
+ *     - Used to skip absorption attempts once all absorbable states are gone
+ *   allStatesAbsorbable: can this context be absorbed? (ALL states absorbable)
+ *     - Dynamic: can change false→true (when non-absorbable states die)
+ *     - Used to determine if this context is eligible for absorption
  */
 typedef struct RPRNFAContext
 {
@@ -2545,6 +2562,10 @@ typedef struct RPRNFAContext
 	int64		matchEndRow;	/* row where match ended (-1 = no match) */
 	int64		lastProcessedRow;	/* last row processed (for fail depth) */
 	RPRNFAState *matchedState;	/* FIN state for greedy fallback (cloned) */
+
+	/* Two-flag absorption optimization */
+	bool		hasAbsorbableState;		/* can absorb others (≥1 absorbable state) */
+	bool		allStatesAbsorbable;	/* can be absorbed (ALL states absorbable) */
 } RPRNFAContext;
 
 /*
diff --git a/src/include/optimizer/rpr.h b/src/include/optimizer/rpr.h
index 4846ad6f2b7..be2ebb02c33 100644
--- a/src/include/optimizer/rpr.h
+++ b/src/include/optimizer/rpr.h
@@ -18,7 +18,6 @@
 
 /* Limits and special values */
 #define RPR_VARID_MAX		252			/* max pattern variables: 252 */
-#define RPR_DEPTH_MAX		UINT8_MAX	/* max nesting depth: 255 */
 #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 */
@@ -30,18 +29,21 @@
 #define RPR_VARID_FIN		((RPRVarId) 255)	/* pattern finish */
 
 /* Element flags */
-#define RPR_ELEM_RELUCTANT	0x01	/* reluctant (non-greedy) quantifier */
-#define RPR_ELEM_ABSORBABLE	0x02	/* branch supports context absorption */
+#define RPR_ELEM_RELUCTANT			0x01	/* reluctant (non-greedy) quantifier */
+#define RPR_ELEM_ABSORBABLE_BRANCH	0x02	/* element in absorbable region */
+#define RPR_ELEM_ABSORBABLE			0x04	/* absorption judgment point */
 
 /* Accessor macros for RPRPatternElement */
-#define RPRElemIsReluctant(e)	((e)->flags & RPR_ELEM_RELUCTANT)
-#define RPRElemIsAbsorbable(e)	((e)->flags & RPR_ELEM_ABSORBABLE)
+#define RPRElemIsReluctant(e)			((e)->flags & RPR_ELEM_RELUCTANT)
+#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 RPRElemIsEnd(e)			((e)->varId == RPR_VARID_END)
 #define RPRElemIsFin(e)			((e)->varId == RPR_VARID_FIN)
 #define RPRElemCanSkip(e)		((e)->min == 0)
 
-extern RPRPattern *buildRPRPattern(RPRPatternNode *pattern, List *defineVariableList);
+extern RPRPattern *buildRPRPattern(RPRPatternNode *pattern, List *defineVariableList,
+								   RPSkipTo rpSkipTo, int frameOptions);
 
 #endif							/* OPTIMIZER_RPR_H */
diff --git a/src/test/regress/expected/rpr.out b/src/test/regress/expected/rpr.out
index dad454b858b..5a96ced4b52 100644
--- a/src/test/regress/expected/rpr.out
+++ b/src/test/regress/expected/rpr.out
@@ -2623,7 +2623,7 @@ SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER
   UP AS price > PREV(1),
   DOWN AS price < PREV(1)
 );
-ERROR:  row pattern navigation operation's argument must include at least one column reference
+ERROR:  reluctant quantifiers are not yet supported
 -- Maximum pattern variables is 252 (RPR_VARID_MAX)
 -- Ok: 252 variables (maximum allowed)
 DO $$
@@ -2776,6 +2776,44 @@ WINDOW w AS (
 --   Row 1: A B C D E (1-5)
 --   Row 2: B C D (2-4) <- ends first!
 --   Row 3: C D E F (3-6) <- ends last!
+-- Test 1b: Longer pattern FAILS, shorter pattern should survive
+-- Pattern: A+ B C D | 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)
+WITH test_overlap1b AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['B']),
+        (3, ARRAY['C']),
+        (4, ARRAY['D']),
+        (5, ARRAY['X'])
+    ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_overlap1b
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A+ B C D E | B+ C)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags),
+        C AS 'C' = ANY(flags),
+        D AS 'D' = ANY(flags),
+        E AS 'E' = ANY(flags)
+);
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {A}   |             |          
+  2 | {B}   |           2 |         3
+  3 | {C}   |             |          
+  4 | {D}   |             |          
+  5 | {X}   |             |          
+(5 rows)
+
 -- Test 2: A B+ C | B+ D - long B sequence with different endings
 WITH test_overlap2 AS (
     SELECT * FROM (VALUES
@@ -3592,7 +3630,7 @@ WINDOW w AS (
 
 -- Expected: 1-4 (A C B C)
 -- ============================================
--- RELUCTANT quantifiers (parser only - executor TODO)
+-- RELUCTANT quantifiers (not yet supported)
 -- ============================================
 -- Test: A+? B (reluctant) - CURRENTLY ACTS LIKE A+ (GREEDY)
 -- Parser accepts the syntax but executor doesn't differentiate
@@ -3615,14 +3653,7 @@ WINDOW w AS (
         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}   |             |          
-(4 rows)
-
+ERROR:  reluctant quantifiers are not yet supported
 -- 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)
@@ -3645,13 +3676,6 @@ WINDOW w AS (
         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}   |             |          
-(4 rows)
-
+ERROR:  reluctant quantifiers are not yet supported
 -- Current: 1-4 (greedy, takes max A before B)
 -- Expected when RELUCTANT implemented: 3-4 (takes min A before B)
diff --git a/src/test/regress/sql/rpr.sql b/src/test/regress/sql/rpr.sql
index 4586eb4b04e..f9c161a791a 100644
--- a/src/test/regress/sql/rpr.sql
+++ b/src/test/regress/sql/rpr.sql
@@ -1320,6 +1320,36 @@ 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)
+WITH test_overlap1b AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['B']),
+        (3, ARRAY['C']),
+        (4, ARRAY['D']),
+        (5, ARRAY['X'])
+    ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_overlap1b
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A+ B C D E | B+ C)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags),
+        C AS 'C' = ANY(flags),
+        D AS 'D' = ANY(flags),
+        E AS 'E' = ANY(flags)
+);
+
 -- Test 2: A B+ C | B+ D - long B sequence with different endings
 WITH test_overlap2 AS (
     SELECT * FROM (VALUES
@@ -1945,7 +1975,7 @@ WINDOW w AS (
 -- Expected: 1-4 (A C B C)
 
 -- ============================================
--- RELUCTANT quantifiers (parser only - executor TODO)
+-- RELUCTANT quantifiers (not yet supported)
 -- ============================================
 
 -- Test: A+? B (reluctant) - CURRENTLY ACTS LIKE A+ (GREEDY)
-- 
2.50.1 (Apple Git-155)