From 4ae891ef6bf6a4564b66cecba8a705ebfed95023 Mon Sep 17 00:00:00 2001
From: Tatsuo Ishii <ishii@postgresql.org>
Date: Sun, 30 Aug 2026 07:20:31 +0900
Subject: [PATCH v51 4/9] Row pattern recognition patch (planner).

---
 src/backend/nodes/nodeFuncs.c             |   68 +
 src/backend/optimizer/path/allpaths.c     |   89 +
 src/backend/optimizer/path/costsize.c     |   16 +
 src/backend/optimizer/plan/Makefile       |    1 +
 src/backend/optimizer/plan/createplan.c   |  161 +-
 src/backend/optimizer/plan/meson.build    |    1 +
 src/backend/optimizer/plan/planner.c      |   33 +-
 src/backend/optimizer/plan/rpr.c          | 2137 +++++++++++++++++++++
 src/backend/optimizer/plan/setrefs.c      |   28 +
 src/backend/optimizer/plan/subselect.c    |    2 +
 src/backend/optimizer/prep/prepjointree.c |    7 +
 src/include/nodes/plannodes.h             |  104 +
 src/include/optimizer/rpr.h               |   87 +
 13 files changed, 2730 insertions(+), 4 deletions(-)
 create mode 100644 src/backend/optimizer/plan/rpr.c
 create mode 100644 src/include/optimizer/rpr.h

diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index 2a2e00b372e..6ab227b2b60 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -69,6 +69,9 @@ exprType(const Node *expr)
 		case T_MergeSupportFunc:
 			type = ((const MergeSupportFunc *) expr)->msftype;
 			break;
+		case T_RPRNavExpr:
+			type = ((const RPRNavExpr *) expr)->resulttype;
+			break;
 		case T_SubscriptingRef:
 			type = ((const SubscriptingRef *) expr)->refrestype;
 			break;
@@ -389,6 +392,9 @@ exprTypmod(const Node *expr)
 			return ((const ArrayCoerceExpr *) expr)->resulttypmod;
 		case T_CollateExpr:
 			return exprTypmod((Node *) ((const CollateExpr *) expr)->arg);
+		case T_RPRNavExpr:
+			/* result has the same type/typmod as the argument expression */
+			return exprTypmod((Node *) ((const RPRNavExpr *) expr)->arg);
 		case T_CaseExpr:
 			{
 				/*
@@ -853,6 +859,9 @@ exprCollation(const Node *expr)
 		case T_MergeSupportFunc:
 			coll = ((const MergeSupportFunc *) expr)->msfcollid;
 			break;
+		case T_RPRNavExpr:
+			coll = ((const RPRNavExpr *) expr)->resultcollid;
+			break;
 		case T_SubscriptingRef:
 			coll = ((const SubscriptingRef *) expr)->refcollid;
 			break;
@@ -1162,6 +1171,9 @@ exprSetCollation(Node *expr, Oid collation)
 		case T_MergeSupportFunc:
 			((MergeSupportFunc *) expr)->msfcollid = collation;
 			break;
+		case T_RPRNavExpr:
+			((RPRNavExpr *) expr)->resultcollid = collation;
+			break;
 		case T_SubscriptingRef:
 			((SubscriptingRef *) expr)->refcollid = collation;
 			break;
@@ -1437,6 +1449,12 @@ exprLocation(const Node *expr)
 		case T_MergeSupportFunc:
 			loc = ((const MergeSupportFunc *) expr)->location;
 			break;
+		case T_RPRNavExpr:
+			loc = ((const RPRNavExpr *) expr)->location;
+			break;
+		case T_RPRPatternNode:
+			loc = ((const RPRPatternNode *) expr)->location;
+			break;
 		case T_SubscriptingRef:
 			/* just use container argument's location */
 			loc = exprLocation((Node *) ((const SubscriptingRef *) expr)->refexpr);
@@ -2199,6 +2217,18 @@ expression_tree_walker_impl(Node *node,
 					return true;
 			}
 			break;
+		case T_RPRNavExpr:
+			{
+				RPRNavExpr *expr = (RPRNavExpr *) node;
+
+				if (WALK(expr->arg))
+					return true;
+				if (expr->offset_arg && WALK(expr->offset_arg))
+					return true;
+				if (expr->compound_offset_arg && WALK(expr->compound_offset_arg))
+					return true;
+			}
+			break;
 		case T_SubscriptingRef:
 			{
 				SubscriptingRef *sbsref = (SubscriptingRef *) node;
@@ -2432,6 +2462,8 @@ expression_tree_walker_impl(Node *node,
 					return true;
 				if (WALK(wc->endOffset))
 					return true;
+				if (WALK(wc->defineClause))
+					return true;
 			}
 			break;
 		case T_CTECycleClause:
@@ -2826,6 +2858,8 @@ query_tree_walker_impl(Query *query,
 				return true;
 			if (WALK(wc->endOffset))
 				return true;
+			if (WALK(wc->defineClause))
+				return true;
 		}
 	}
 
@@ -3148,6 +3182,18 @@ expression_tree_mutator_impl(Node *node,
 				return (Node *) newnode;
 			}
 			break;
+		case T_RPRNavExpr:
+			{
+				RPRNavExpr *nav = (RPRNavExpr *) node;
+				RPRNavExpr *newnode;
+
+				FLATCOPY(newnode, nav, RPRNavExpr);
+				MUTATE(newnode->arg, nav->arg, Expr *);
+				MUTATE(newnode->offset_arg, nav->offset_arg, Expr *);
+				MUTATE(newnode->compound_offset_arg, nav->compound_offset_arg, Expr *);
+				return (Node *) newnode;
+			}
+			break;
 		case T_SubscriptingRef:
 			{
 				SubscriptingRef *sbsref = (SubscriptingRef *) node;
@@ -3570,6 +3616,7 @@ expression_tree_mutator_impl(Node *node,
 				MUTATE(newnode->orderClause, wc->orderClause, List *);
 				MUTATE(newnode->startOffset, wc->startOffset, Node *);
 				MUTATE(newnode->endOffset, wc->endOffset, Node *);
+				MUTATE(newnode->defineClause, wc->defineClause, List *);
 				return (Node *) newnode;
 			}
 			break;
@@ -3943,6 +3990,7 @@ query_tree_mutator_impl(Query *query,
 			FLATCOPY(newnode, wc, WindowClause);
 			MUTATE(newnode->startOffset, wc->startOffset, Node *);
 			MUTATE(newnode->endOffset, wc->endOffset, Node *);
+			MUTATE(newnode->defineClause, wc->defineClause, List *);
 
 			resultlist = lappend(resultlist, (Node *) newnode);
 		}
@@ -4599,6 +4647,8 @@ raw_expression_tree_walker_impl(Node *node,
 					return true;
 				if (WALK(wd->endOffset))
 					return true;
+				if (WALK(wd->rpCommonSyntax))
+					return true;
 			}
 			break;
 		case T_RangeSubselect:
@@ -4854,6 +4904,24 @@ raw_expression_tree_walker_impl(Node *node,
 					return true;
 			}
 			break;
+		case T_RPCommonSyntax:
+			{
+				RPCommonSyntax *rc = (RPCommonSyntax *) node;
+
+				if (WALK(rc->rpPattern))
+					return true;
+				if (WALK(rc->rpDefs))
+					return true;
+			}
+			break;
+		case T_RPRPatternNode:
+			{
+				RPRPatternNode *rp = (RPRPatternNode *) node;
+
+				if (WALK(rp->children))
+					return true;
+			}
+			break;
 		default:
 			elog(ERROR, "unrecognized node type: %d",
 				 (int) nodeTag(node));
diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c
index 24a6a8d11dd..62db28c0a42 100644
--- a/src/backend/optimizer/path/allpaths.c
+++ b/src/backend/optimizer/path/allpaths.c
@@ -2467,6 +2467,17 @@ find_window_run_conditions(Query *subquery, AttrNumber attno,
 	wclause = (WindowClause *) list_nth(subquery->windowClause,
 										wfunc->winref - 1);
 
+	/*
+	 * If a DEFINE clause exists, we cannot push down a run condition. In the
+	 * case, a window partition (or frame) is divided into multiple reduced
+	 * frames and each frame should be evaluated to the end of the partition
+	 * (or full frame end). This means we cannot apply the run condition
+	 * optimization because it stops evaluation window functions in certain
+	 * cases.
+	 */
+	if (wclause->defineClause != NIL)
+		return false;
+
 	req.type = T_SupportRequestWFuncMonotonic;
 	req.window_func = wfunc;
 	req.window_clause = wclause;
@@ -4918,6 +4929,84 @@ remove_unused_subquery_outputs(Query *subquery, RelOptInfo *rel,
 		if (contain_volatile_functions(texpr))
 			continue;
 
+		/*
+		 * If any RPR (Row Pattern Recognition) window clause references this
+		 * column in its DEFINE clause, don't remove it.  The DEFINE
+		 * expression needs these columns in the tuplestore slot for pattern
+		 * matching evaluation, even if the outer query doesn't reference
+		 * them.
+		 */
+		if (IsA(texpr, Var))
+		{
+			Var		   *var = (Var *) texpr;
+			bool		needed_by_define = false;
+
+			foreach_node(WindowClause, wc, subquery->windowClause)
+			{
+				if (wc->defineClause != NIL)
+				{
+					/*
+					 * flags == 0 is safe: DEFINE rejects aggregates, window
+					 * functions and subqueries at parse time, and this runs
+					 * before any PlaceHolderVar could be planted.
+					 */
+					List	   *vars = pull_var_clause((Node *) wc->defineClause, 0);
+
+					foreach_node(Var, dvar, vars)
+					{
+
+						/*
+						 * Match varno as well as varattno: a Var pulled from
+						 * a DEFINE clause can share an attribute number with
+						 * an unrelated output column of a different relation,
+						 * which would otherwise be over-retained.  Checking
+						 * varlevelsup is just paranoia, since outer
+						 * references in DEFINE are rejected during parse
+						 * analysis.
+						 */
+						if (dvar->varno == var->varno &&
+							dvar->varattno == var->varattno &&
+							dvar->varlevelsup == var->varlevelsup)
+						{
+							needed_by_define = true;
+							break;
+						}
+					}
+					list_free(vars);
+					if (needed_by_define)
+						break;
+				}
+			}
+			if (needed_by_define)
+				continue;
+		}
+
+		/*
+		 * If it's a window function referencing a window clause with RPR,
+		 * don't remove it.  Even when the window function result is unused by
+		 * the outer query, the RPR pattern matching (frame reduction via
+		 * DEFINE/PATTERN) must still execute.  Replacing this with NULL would
+		 * leave no active window functions for the WindowClause, causing the
+		 * planner to omit the WindowAgg node entirely.
+		 */
+		if (IsA(texpr, WindowFunc))
+		{
+			bool		is_rpr = false;
+			WindowFunc *wfunc = (WindowFunc *) texpr;
+
+			foreach_node(WindowClause, wc, subquery->windowClause)
+			{
+				if (wc->winref == wfunc->winref && wc->defineClause != NIL)
+				{
+					is_rpr = true;
+					break;
+				}
+			}
+
+			if (is_rpr)
+				continue;
+		}
+
 		/*
 		 * OK, we don't need it.  Replace the expression with a NULL constant.
 		 * Preserve the exposed type of the expression, in case something
diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c
index 7bbddb8bee4..c124415a459 100644
--- a/src/backend/optimizer/path/costsize.c
+++ b/src/backend/optimizer/path/costsize.c
@@ -104,6 +104,7 @@
 #include "optimizer/placeholder.h"
 #include "optimizer/plancat.h"
 #include "optimizer/restrictinfo.h"
+#include "optimizer/rpr.h"
 #include "parser/parsetree.h"
 #include "utils/lsyscache.h"
 #include "utils/selfuncs.h"
@@ -3260,7 +3261,22 @@ cost_windowagg(Path *path, PlannerInfo *root,
 	 * many rows the window function will fetch, it's hard to do better.  In
 	 * any case, it's a good estimate for all the built-in window functions,
 	 * so we'll just do this for now.
+	 *
+	 * Moreover, if row pattern recognition is used, we charge the DEFINE
+	 * expressions once per tuple for each DEFINE variable.
 	 */
+	if (winclause->rpPattern)
+	{
+		QualCost	defcosts;
+
+		foreach_node(TargetEntry, def, winclause->defineClause)
+		{
+			cost_qual_eval_node(&defcosts, (Node *) def->expr, root);
+			startup_cost += defcosts.startup;
+			total_cost += defcosts.per_tuple * input_tuples;
+		}
+	}
+
 	foreach(lc, windowFuncs)
 	{
 		WindowFunc *wfunc = lfirst_node(WindowFunc, lc);
diff --git a/src/backend/optimizer/plan/Makefile b/src/backend/optimizer/plan/Makefile
index 80ef162e484..7e0167789d8 100644
--- a/src/backend/optimizer/plan/Makefile
+++ b/src/backend/optimizer/plan/Makefile
@@ -19,6 +19,7 @@ OBJS = \
 	planagg.o \
 	planmain.o \
 	planner.o \
+	rpr.o \
 	setrefs.o \
 	subselect.o
 
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index 02a888c5996..6dfcd43f018 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -35,6 +35,7 @@
 #include "optimizer/planmain.h"
 #include "optimizer/prep.h"
 #include "optimizer/restrictinfo.h"
+#include "optimizer/rpr.h"
 #include "optimizer/subselect.h"
 #include "optimizer/tlist.h"
 #include "parser/parse_clause.h"
@@ -294,7 +295,10 @@ static Memoize *make_memoize(Plan *lefttree, Oid *hashoperators,
 static WindowAgg *make_windowagg(List *tlist, WindowClause *wc,
 								 int partNumCols, AttrNumber *partColIdx, Oid *partOperators, Oid *partCollations,
 								 int ordNumCols, AttrNumber *ordColIdx, Oid *ordOperators, Oid *ordCollations,
-								 List *runCondition, List *qual, bool topWindow,
+								 List *runCondition,
+								 RPRPattern *compiledPattern,
+								 Bitmapset *defineMatchStartDependent,
+								 List *qual, bool topWindow,
 								 Plan *lefttree);
 static Group *make_group(List *tlist, List *qual, int numGroupCols,
 						 AttrNumber *grpColIdx, Oid *grpOperators, Oid *grpCollations,
@@ -2465,6 +2469,125 @@ create_minmaxagg_plan(PlannerInfo *root, MinMaxAggPath *best_path)
 	return plan;
 }
 
+/*
+ * DefineMetadataContext - context for the DEFINE clause walk below.
+ *
+ * The walk classifies one thing: which DEFINE variables depend on the match
+ * start, which is what buildRPRPattern() needs to decide context absorption.
+ * The trim offsets are not plan-time metadata; the executor records them at
+ * init (build_define_offsets) and settles their values per scan
+ * (resolve_nav_offsets), both in nodeWindowAgg.c.
+ *
+ * The driver sets curVarIdx to the index of the variable being walked before
+ * each invocation; the walker uses it to populate matchStartDependent.
+ */
+typedef struct DefineMetadataContext
+{
+	int			curVarIdx;		/* DEFINE variable currently being walked */
+	int			navno;			/* next RPRNavExpr.navno to assign */
+	Bitmapset  *matchStartDependent;	/* variables that depend on
+										 * match_start */
+} DefineMetadataContext;
+
+/*
+ * compute_matchStartDependent
+ *
+ * per-variable match_start dependency for absorption suppression: outer nav
+ * kinds that reach match_start (FIRST, LAST-with-offset, PREV_FIRST,
+ * NEXT_FIRST, PREV_LAST/NEXT_LAST-with-offset) add curVarIdx to
+ * matchStartDependent.
+ *
+ * Classification uses only the outer nav kind: parser nesting restrictions
+ * prevent FIRST/LAST inside a PREV/NEXT value subexpression.
+ */
+static void
+compute_matchStartDependent(RPRNavExpr *nav, DefineMetadataContext *context)
+{
+	/*
+	 * Parser guarantee: by the time the planner sees a DEFINE expression,
+	 * compound nesting has been flattened into a single RPRNavExpr and any
+	 * other RPRNavExpr nesting has been rejected.  So nav's direct child
+	 * fields are not themselves RPRNavExpr nodes, and outer-kind dispatch
+	 * below is sufficient.
+	 */
+	Assert(nav->arg == NULL || !IsA(nav->arg, RPRNavExpr));
+	Assert(nav->offset_arg == NULL || !IsA(nav->offset_arg, RPRNavExpr));
+	Assert(nav->compound_offset_arg == NULL ||
+		   !IsA(nav->compound_offset_arg, RPRNavExpr));
+
+	/*
+	 * Match-start dependency: classify the outer nav kind.  A constant
+	 * LAST(x, 0) is conservatively included (offset_arg is a non-NULL Const),
+	 * causing a harmless extra re-evaluation; since LAST(x, 0) is the current
+	 * row, its result is independent of the match start.
+	 */
+	if (nav->kind == RPR_NAV_FIRST ||
+		(nav->kind == RPR_NAV_LAST && nav->offset_arg != NULL) ||
+		nav->kind == RPR_NAV_PREV_FIRST ||
+		nav->kind == RPR_NAV_NEXT_FIRST ||
+		((nav->kind == RPR_NAV_PREV_LAST ||
+		  nav->kind == RPR_NAV_NEXT_LAST) &&
+		 nav->offset_arg != NULL))
+		context->matchStartDependent =
+			bms_add_member(context->matchStartDependent,
+						   context->curVarIdx);
+}
+
+static bool
+RPRNavExpr_walker(Node *node, DefineMetadataContext *ctx)
+{
+	if (node == NULL)
+		return false;
+	if (IsA(node, RPRNavExpr))
+	{
+		RPRNavExpr *nav = castNode(RPRNavExpr, node);
+
+		nav->navno = ctx->navno++;
+		compute_matchStartDependent(nav, ctx);
+	}
+
+	return expression_tree_walker(node, RPRNavExpr_walker, ctx);
+}
+
+/*
+ * compute_define_metadata
+ *		Classify which DEFINE variables depend on the match start, and number
+ *		the navigations.
+ *
+ * Walks each DEFINE variable expression once and returns the set of variable
+ * indices whose navigation reaches match_start: those containing FIRST or a
+ * compound PREV_FIRST/NEXT_FIRST, or a LAST that carries an offset of its
+ * own, whether plain or inside a compound PREV_LAST/NEXT_LAST.  Such
+ * variables require per-context re-evaluation during NFA processing, and
+ * their presence disqualifies the pattern from context absorption.
+ *
+ * The same walk assigns RPRNavExpr.navno in visit order, which is the order
+ * the executor builds WindowAggState.rprNavOffsets in.
+ *
+ * Navigation offsets for tuplestore trim are not computed here; they are
+ * built at executor init (build_define_offsets) and settled per scan
+ * (resolve_nav_offsets), which can evaluate non-constant offsets that the
+ * planner cannot fold.
+ */
+static void
+compute_define_metadata(List *defineClause, Bitmapset **matchStartDependent)
+{
+	DefineMetadataContext ctx;
+
+	ctx.curVarIdx = 0;
+	ctx.navno = 0;
+	ctx.matchStartDependent = NULL;
+
+	foreach_node(TargetEntry, te, defineClause)
+	{
+		ctx.curVarIdx = foreach_current_index(te);
+
+		RPRNavExpr_walker((Node *) te->expr, &ctx);
+	}
+
+	*matchStartDependent = ctx.matchStartDependent;
+}
+
 /*
  * create_windowagg_plan
  *
@@ -2489,6 +2612,8 @@ create_windowagg_plan(PlannerInfo *root, WindowAggPath *best_path)
 	Oid		   *ordOperators;
 	Oid		   *ordCollations;
 	ListCell   *lc;
+	RPRPattern *compiledPattern = NULL;
+	Bitmapset  *matchStartDependent = NULL;
 
 	/*
 	 * Choice of tlist here is motivated by the fact that WindowAgg will be
@@ -2539,6 +2664,24 @@ create_windowagg_plan(PlannerInfo *root, WindowAggPath *best_path)
 		ordNumCols++;
 	}
 
+	/* Build RPR pattern */
+	if (wc->rpPattern)
+	{
+		/*
+		 * Classify which DEFINE variables depend on match_start (for
+		 * absorption suppression in buildRPRPattern).  Nav offsets for
+		 * tuplestore trim are resolved later, at executor init.
+		 */
+		compute_define_metadata(wc->defineClause, &matchStartDependent);
+
+		/* Compile and optimize RPR patterns */
+		compiledPattern = buildRPRPattern(wc->rpPattern,
+										  wc->defineClause,
+										  wc->rpSkipTo,
+										  wc->frameOptions,
+										  !bms_is_empty(matchStartDependent));
+	}
+
 	/* And finally we can make the WindowAgg node */
 	plan = make_windowagg(tlist,
 						  wc,
@@ -2551,6 +2694,8 @@ create_windowagg_plan(PlannerInfo *root, WindowAggPath *best_path)
 						  ordOperators,
 						  ordCollations,
 						  best_path->runCondition,
+						  compiledPattern,
+						  matchStartDependent,
 						  best_path->qual,
 						  best_path->topwindow,
 						  subplan);
@@ -6649,7 +6794,10 @@ static WindowAgg *
 make_windowagg(List *tlist, WindowClause *wc,
 			   int partNumCols, AttrNumber *partColIdx, Oid *partOperators, Oid *partCollations,
 			   int ordNumCols, AttrNumber *ordColIdx, Oid *ordOperators, Oid *ordCollations,
-			   List *runCondition, List *qual, bool topWindow, Plan *lefttree)
+			   List *runCondition,
+			   RPRPattern *compiledPattern,
+			   Bitmapset *defineMatchStartDependent,
+			   List *qual, bool topWindow, Plan *lefttree)
 {
 	WindowAgg  *node = makeNode(WindowAgg);
 	Plan	   *plan = &node->plan;
@@ -6676,6 +6824,15 @@ make_windowagg(List *tlist, WindowClause *wc,
 	node->inRangeAsc = wc->inRangeAsc;
 	node->inRangeNullsFirst = wc->inRangeNullsFirst;
 	node->topWindow = topWindow;
+	node->rpSkipTo = wc->rpSkipTo;
+
+	/* Store compiled pattern for NFA execution */
+	node->rpPattern = compiledPattern;
+
+	node->defineClause = wc->defineClause;
+
+	/* Store pre-computed match_start dependency bitmapset */
+	node->defineMatchStartDependent = defineMatchStartDependent;
 
 	plan->targetlist = tlist;
 	plan->lefttree = lefttree;
diff --git a/src/backend/optimizer/plan/meson.build b/src/backend/optimizer/plan/meson.build
index c565b2adbcc..5b2381cd774 100644
--- a/src/backend/optimizer/plan/meson.build
+++ b/src/backend/optimizer/plan/meson.build
@@ -7,6 +7,7 @@ backend_sources += files(
   'planagg.c',
   'planmain.c',
   'planner.c',
+  'rpr.c',
   'setrefs.c',
   'subselect.c',
 )
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index c3c158a253d..4199b657afc 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -1062,6 +1062,21 @@ subquery_planner(PlannerGlobal *glob, Query *parse, char *plan_name,
 												EXPRKIND_LIMIT);
 		wc->endOffset = preprocess_expression(root, wc->endOffset,
 											  EXPRKIND_LIMIT);
+		wc->defineClause = (List *) preprocess_expression(root,
+														  (Node *) wc->defineClause,
+														  EXPRKIND_TARGET);
+
+		/*
+		 * Reject volatile expressions in an RPR DEFINE clause.  This is done
+		 * here, not during parse analysis, to follow the convention of not
+		 * checking expression volatility while parsing.  A subquery the
+		 * planner discards before reaching this point is therefore not
+		 * checked, which is the same rule that lets a volatile fold away.
+		 */
+		if (contain_volatile_functions((Node *) wc->defineClause))
+			ereport(ERROR,
+					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					errmsg("DEFINE clause cannot contain volatile functions"));
 	}
 
 	parse->limitOffset = preprocess_expression(root, parse->limitOffset,
@@ -6083,6 +6098,14 @@ optimize_window_clauses(PlannerInfo *root, WindowFuncLists *wflists)
 		if (wflists->windowFuncs[wc->winref] == NIL)
 			continue;
 
+		/*
+		 * If a DEFINE clause exists, do not let support functions replace the
+		 * frame with a non-RPR-compatible one.  RPR windows require ROWS
+		 * BETWEEN CURRENT ROW AND ...
+		 */
+		if (wc->defineClause != NIL)
+			continue;
+
 		foreach(lc2, wflists->windowFuncs[wc->winref])
 		{
 			SupportRequestOptimizeWindowClause req;
@@ -6160,13 +6183,19 @@ optimize_window_clauses(PlannerInfo *root, WindowFuncLists *wflists)
 
 				/*
 				 * Perform the same duplicate check that is done in
-				 * transformWindowFuncCall.
+				 * transformWindowFuncCall. wc is never an RPR clause here
+				 * (those are skipped above), and an RPR existing_wc differs
+				 * in its frame options anyway, so the RPR-related comparisons
+				 * are a defensive backstop for parity.
 				 */
 				if (equal(wc->partitionClause, existing_wc->partitionClause) &&
 					equal(wc->orderClause, existing_wc->orderClause) &&
 					wc->frameOptions == existing_wc->frameOptions &&
 					equal(wc->startOffset, existing_wc->startOffset) &&
-					equal(wc->endOffset, existing_wc->endOffset))
+					equal(wc->endOffset, existing_wc->endOffset) &&
+					wc->rpSkipTo == existing_wc->rpSkipTo &&
+					equal(wc->defineClause, existing_wc->defineClause) &&
+					equal(wc->rpPattern, existing_wc->rpPattern))
 				{
 					ListCell   *lc4;
 
diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
new file mode 100644
index 00000000000..122aaa67a24
--- /dev/null
+++ b/src/backend/optimizer/plan/rpr.c
@@ -0,0 +1,2137 @@
+/*-------------------------------------------------------------------------
+ *
+ * rpr.c
+ *	  Row Pattern Recognition pattern compilation for planner
+ *
+ * This file contains functions for optimizing the RPR pattern parse tree and
+ * compiling it to a flat element array for NFA execution by WindowAgg.
+ *
+ * Key components:
+ *   1. Pattern Optimization: Simplifies patterns before compilation
+ *      (e.g., flatten nested SEQ/ALT, merge consecutive vars)
+ *   2. Pattern Compilation: Converts parse tree to flat element array for NFA
+ *   3. Absorption Analysis: Computes flags for O(n^2)->O(n) optimization
+ *
+ * Context Absorption Optimization:
+ *   When a pattern starts with a greedy unbounded element (e.g., A+ or (A B)+),
+ *   newer contexts cannot produce longer matches than older contexts.
+ *   By absorbing (eliminating) redundant newer contexts, we reduce
+ *   complexity from O(n^2) to O(n) for patterns like A+ B.
+ *
+ *   The absorption analysis uses two element flags:
+ *   - RPR_ELEM_ABSORBABLE: marks WHERE to compare (comparison point)
+ *   - RPR_ELEM_ABSORBABLE_BRANCH: marks the absorbable region
+ *
+ *   EXPLAIN shows both on the Pattern: line, # for the comparison point and
+ *   ~ for the region.
+ *
+ *   See computeAbsorbability() and the detailed comments before
+ *   isUnboundedStart() for the full design explanation.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/optimizer/plan/rpr.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "common/int.h"
+#include "miscadmin.h"
+#include "optimizer/rpr.h"
+
+/* Forward declarations */
+static bool rprPatternEqual(RPRPatternNode *a, RPRPatternNode *b);
+static bool rprPatternChildrenEqual(List *a, List *b);
+static int64 rprNodeRowCount(RPRPatternNode *node);
+static int64 rprBodyRowCount(List *children);
+static bool rprBodyHasUniformLength(List *children);
+static bool rprChildrenMatchAt(List *children, int start, List *content);
+static List *rprGroupContent(RPRPatternNode *group);
+static bool rprTryAddIteration(RPRPatternNode *group);
+static pg_nodiscard List *flattenSeqChildren(List *children);
+static pg_nodiscard List *mergeConsecutiveVars(List *children);
+static pg_nodiscard List *mergeConsecutiveGroups(List *children);
+static pg_nodiscard List *mergeConsecutiveAlts(List *children);
+static pg_nodiscard List *mergeGroupPrefixSuffix(List *children);
+static RPRPatternNode *optimizeSeqPattern(RPRPatternNode *pattern);
+
+static pg_nodiscard List *flattenAltChildren(List *children);
+static pg_nodiscard List *removeDuplicateAlternatives(List *children);
+static RPRPatternNode *optimizeAltPattern(RPRPatternNode *pattern);
+
+static RPRPatternNode *tryMultiplyQuantifiers(RPRPatternNode *pattern);
+static RPRPatternNode *tryUnwrapGroup(RPRPatternNode *pattern);
+static RPRPatternNode *optimizeGroupPattern(RPRPatternNode *pattern);
+
+static RPRPatternNode *optimizeRPRPattern(RPRPatternNode *pattern);
+static void scanRPRPatternRecursive(RPRPatternNode *node, char **varNames,
+									int *numVars, int *numElements,
+									RPRDepth depth, RPRDepth *maxDepth);
+static void scanRPRPattern(RPRPatternNode *node, char **varNames, int *numVars,
+						   int *numElements, RPRDepth *maxDepth);
+static RPRPattern *makeRPRPattern(int numVars, int numElements,
+								  RPRDepth maxDepth, char **varNamesStack);
+static RPRVarId getVarIdFromPattern(RPRPattern *pat, const char *varName);
+static RPRElemFlags fillRPRPatternVar(RPRPatternNode *node, RPRPattern *pat,
+									  int *idx, RPRDepth depth);
+static RPRElemFlags fillRPRPatternGroup(RPRPatternNode *node, RPRPattern *pat,
+										int *idx, RPRDepth depth);
+static RPRElemFlags fillRPRPatternAlt(RPRPatternNode *node, RPRPattern *pat,
+									  int *idx, RPRDepth depth);
+static RPRElemFlags fillRPRPattern(RPRPatternNode *node, RPRPattern *pat,
+								   int *idx, RPRDepth depth);
+static void finalizeRPRPattern(RPRPattern *result);
+
+static bool isFixedLengthChildren(RPRPattern *pattern, RPRElemIdx idx,
+								  RPRDepth scopeDepth);
+static bool isUnboundedStart(RPRPattern *pattern, RPRElemIdx idx);
+static void computeAbsorbabilityRecursive(RPRPattern *pattern,
+										  RPRElemIdx startIdx,
+										  bool *hasAbsorbable);
+static void computeAbsorbability(RPRPattern *pattern);
+
+/*
+ * rprPatternEqual
+ *		Compare two RPRPatternNode trees for equality.
+ *
+ * Returns true if the trees are structurally identical.
+ */
+static bool
+rprPatternEqual(RPRPatternNode *a, RPRPatternNode *b)
+{
+	/* Pattern nodes in children lists must never be NULL */
+	Assert(a != NULL && b != NULL);
+
+	/* Must have same node type and quantifiers */
+	if (a->nodeType != b->nodeType)
+		return false;
+	if (a->min != b->min || a->max != b->max)
+		return false;
+	if (a->reluctant != b->reluctant)
+		return false;
+
+	switch (a->nodeType)
+	{
+		case RPR_PATTERN_VAR:
+			return strcmp(a->varName, b->varName) == 0;
+
+		case RPR_PATTERN_SEQ:
+		case RPR_PATTERN_ALT:
+		case RPR_PATTERN_GROUP:
+			return rprPatternChildrenEqual(a->children, b->children);
+	}
+
+	pg_unreachable();
+	return false;
+}
+
+/*
+ * rprPatternChildrenEqual
+ *		Compare children lists of two pattern nodes for equality.
+ *
+ * Returns true if the children lists are structurally identical.
+ */
+static bool
+rprPatternChildrenEqual(List *a, List *b)
+{
+	ListCell   *lca,
+			   *lcb;
+
+	if (list_length(a) != list_length(b))
+		return false;
+
+	forboth(lca, a, lcb, b)
+	{
+		if (!rprPatternEqual((RPRPatternNode *) lfirst(lca),
+							 (RPRPatternNode *) lfirst(lcb)))
+			return false;
+	}
+
+	return true;
+}
+
+/*
+ * rprNodeRowCount
+ *		Rows the node always consumes, or -1 if that varies.
+ *
+ * Alternatives of equal length count as fixed: they may pick different
+ * variables, but never different rows.
+ */
+static int64
+rprNodeRowCount(RPRPatternNode *node)
+{
+	int64		len;
+
+	check_stack_depth();
+
+	switch (node->nodeType)
+	{
+		case RPR_PATTERN_VAR:
+			if (node->min != node->max)
+				return -1;
+			return node->min;
+
+		case RPR_PATTERN_ALT:
+			len = -1;
+			foreach_node(RPRPatternNode, branch, node->children)
+			{
+				int64		branchLen = rprNodeRowCount(branch);
+
+				if (branchLen < 0 || (len >= 0 && branchLen != len))
+					return -1;
+				len = branchLen;
+			}
+			return len;
+
+		case RPR_PATTERN_SEQ:
+		case RPR_PATTERN_GROUP:
+			len = rprBodyRowCount(node->children);
+			if (len < 0)
+				return -1;
+			if (node->nodeType == RPR_PATTERN_SEQ)
+				return len;
+			if (node->min != node->max)
+				return -1;
+			len *= node->min;
+			/* A count this large cannot arise from real rows anyway */
+			if (len >= RPR_QUANTITY_INF)
+				return -1;
+			return len;
+	}
+	return -1;
+}
+
+/*
+ * rprBodyRowCount
+ *		Rows the children always consume in sequence, or -1 if that varies.
+ */
+static int64
+rprBodyRowCount(List *children)
+{
+	int64		total = 0;
+
+	foreach_node(RPRPatternNode, child, children)
+	{
+		int64		len = rprNodeRowCount(child);
+
+		if (len < 0)
+			return -1;
+		total += len;
+
+		/* A count this large cannot arise from real rows anyway */
+		if (total >= RPR_QUANTITY_INF)
+			return -1;
+	}
+	return total;
+}
+
+/*
+ * rprBodyHasUniformLength
+ *		Do the children always consume the same number of rows?
+ */
+static bool
+rprBodyHasUniformLength(List *children)
+{
+	return rprBodyRowCount(children) >= 0;
+}
+
+/*
+ * rprChildrenMatchAt
+ *		Do the cells of children at [start, start + list_length(content))
+ *		match content element for element?
+ *
+ * Returns false when that range does not lie inside children, so a caller
+ * walking towards either end of the list can just ask.
+ */
+static bool
+rprChildrenMatchAt(List *children, int start, List *content)
+{
+	int			offset = 0;
+
+	if (start < 0 || start + list_length(content) > list_length(children))
+		return false;
+
+	foreach_node(RPRPatternNode, want, content)
+	{
+		RPRPatternNode *have;
+
+		have = list_nth_node(RPRPatternNode, children, start + offset);
+		if (!rprPatternEqual(have, want))
+			return false;
+		offset++;
+	}
+
+	return true;
+}
+
+/*
+ * rprGroupContent
+ *		The elements a GROUP stands for, as they appear in a sequence.
+ *
+ * A GROUP holds a single child, and a multi-element body arrives wrapped in a
+ * SEQ, so unwrap that to compare against elements of the enclosing sequence:
+ * (A B)+ holds the sequence A B.
+ */
+static List *
+rprGroupContent(RPRPatternNode *group)
+{
+	List	   *content = group->children;
+
+	if (list_length(content) == 1)
+	{
+		RPRPatternNode *inner = linitial_node(RPRPatternNode, content);
+
+		if (inner->nodeType == RPR_PATTERN_SEQ)
+			content = inner->children;
+	}
+
+	Assert(list_length(content) > 0);
+	return content;
+}
+
+/*
+ * rprTryAddIteration
+ *		Raise a GROUP's quantifier by one iteration, if that is representable.
+ *
+ * An unbounded max stands for "no limit", not a count, so it is left alone and
+ * nothing can overflow.  A finite bound has to stay below RPR_QUANTITY_INF:
+ * one landing on the marker would read as unbounded.  Returns false without
+ * touching the node when either bound has no room.
+ */
+static bool
+rprTryAddIteration(RPRPatternNode *group)
+{
+	if (group->min >= RPR_QUANTITY_INF - 1)
+		return false;
+	if (group->max != RPR_QUANTITY_INF &&
+		group->max >= RPR_QUANTITY_INF - 1)
+		return false;
+
+	group->min += 1;
+	if (group->max != RPR_QUANTITY_INF)
+		group->max += 1;
+	return true;
+}
+
+/*
+ * flattenSeqChildren
+ *		Recursively optimize children and flatten nested SEQ.
+ *
+ * Example:
+ *   SEQ(A, SEQ(B, C)) -> SEQ(A, B, C)
+ *
+ * Returns a new list with optimized children, with nested SEQ children
+ * flattened into the parent list.  The helpers in this file follow two
+ * conventions -- this one and flattenAltChildren() build a new list, since
+ * either can end up longer than what it started with, while the rest compact
+ * the cells they already have -- so a caller must always assign the return
+ * value.
+ */
+static List *
+flattenSeqChildren(List *children)
+{
+	List	   *newChildren = NIL;
+
+	foreach_node(RPRPatternNode, child, children)
+	{
+		RPRPatternNode *opt = optimizeRPRPattern(child);
+
+		/*
+		 * GROUP{1,1} should have been unwrapped by optimizeGroupPattern;
+		 * tryUnwrapGroup() does so regardless of reluctance.
+		 */
+		Assert(!(opt->nodeType == RPR_PATTERN_GROUP &&
+				 opt->min == 1 && opt->max == 1));
+
+		if (opt->nodeType == RPR_PATTERN_SEQ)
+		{
+			newChildren = list_concat(newChildren,
+									  list_copy(opt->children));
+		}
+		else
+		{
+			newChildren = lappend(newChildren, opt);
+		}
+	}
+
+	return newChildren;
+}
+
+/*
+ * mergeConsecutiveVars
+ *		Merge consecutive identical VAR nodes.
+ *
+ * Examples:
+ *   A{m1,M1} A{m2,M2} -> A{m1+m2, M1+M2} where INF + x = INF.
+ *
+ * Only merges non-reluctant VAR nodes with the same variable name.
+ */
+static List *
+mergeConsecutiveVars(List *children)
+{
+	int			writepos = 0;
+	int			readpos = 0;
+
+	while (readpos < list_length(children))
+	{
+		RPRPatternNode *node = list_nth_node(RPRPatternNode, children, readpos);
+		int			runlen = 1;
+
+		if (node->nodeType == RPR_PATTERN_VAR && !node->reluctant)
+		{
+			/* Fold the VARs that follow into node while they fit */
+			while (readpos + runlen < list_length(children))
+			{
+				RPRPatternNode *other;
+				int			newmin;
+				int			newmax;
+
+				other = list_nth_node(RPRPatternNode, children, readpos + runlen);
+
+				if (other->nodeType != RPR_PATTERN_VAR)
+					break;
+
+				/*
+				 * A greedy quantifier followed by a reluctant one over the
+				 * same variable is not expressible as a single quantifier:
+				 * the pair settles the first quantifier's count before the
+				 * second one decides, which the standard's
+				 * leftmost-choice-first rule (ISO/IEC TR 19075-5 7.2) makes
+				 * observable.  Merging them would change the preferred match,
+				 * so stop here.
+				 */
+				if (other->reluctant)
+					break;
+
+				if (strcmp(node->varName, other->varName) != 0)
+					break;
+
+				/*
+				 * RPR_QUANTITY_INF means unbounded, not a count: a finite sum
+				 * landing on it is representable, so reject it separately.
+				 */
+				if (node->max == RPR_QUANTITY_INF ||
+					other->max == RPR_QUANTITY_INF)
+					newmax = RPR_QUANTITY_INF;
+				else if (pg_add_s32_overflow(node->max, other->max, &newmax) ||
+						 newmax >= RPR_QUANTITY_INF)
+					break;		/* fallback: leave the pair unmerged */
+
+				if (pg_add_s32_overflow(node->min, other->min, &newmin) ||
+					newmin >= RPR_QUANTITY_INF)
+					break;		/* fallback: leave the pair unmerged */
+
+				node->min = newmin;
+				node->max = newmax;
+				runlen++;
+			}
+		}
+
+		/*
+		 * Survivors are compacted towards the front.  writepos never passes
+		 * readpos, so this cannot overwrite a cell still to be read.
+		 */
+		lfirst(list_nth_cell(children, writepos++)) = node;
+		readpos += runlen;
+	}
+
+	return list_truncate(children, writepos);
+}
+
+/*
+ * mergeConsecutiveGroups
+ *		Merge consecutive identical GROUP nodes.
+ *
+ * Example:
+ *   (A B)+ (A B)+ -> (A B){2,}
+ *
+ * Only merges non-reluctant GROUP nodes with identical children.
+ *
+ * The body must consume a fixed number of rows.  Otherwise the merge changes
+ * which match is preferred: the two groups split the iterations between them,
+ * and that split is a choice point the merged form does not have.  With a
+ * fixed body, rows consumed rise in step with the iteration count, so both
+ * forms reach the same rows in the same order; without one, they need not --
+ * (A | B B)+ (A | B B)+ prefers a four-row match where (A | B B){2,} takes two.
+ */
+static List *
+mergeConsecutiveGroups(List *children)
+{
+	int			writepos = 0;
+	int			readpos = 0;
+
+	while (readpos < list_length(children))
+	{
+		RPRPatternNode *node = list_nth_node(RPRPatternNode, children, readpos);
+		int			runlen = 1;
+
+		if (node->nodeType == RPR_PATTERN_GROUP && !node->reluctant)
+		{
+			/* Fold the GROUPs that follow into node while they fit */
+			while (readpos + runlen < list_length(children))
+			{
+				RPRPatternNode *other;
+				int			newmin;
+				int			newmax;
+
+				other = list_nth_node(RPRPatternNode, children, readpos + runlen);
+
+				if (other->nodeType != RPR_PATTERN_GROUP || other->reluctant)
+					break;
+
+				if (!rprPatternChildrenEqual(node->children, other->children))
+					break;
+
+				/* The body must consume a fixed number of rows; see above */
+				if (!rprBodyHasUniformLength(node->children))
+					break;
+
+				/*
+				 * RPR_QUANTITY_INF means unbounded, not a count: a finite sum
+				 * landing on it is representable, so reject it separately.
+				 */
+				if (node->max == RPR_QUANTITY_INF ||
+					other->max == RPR_QUANTITY_INF)
+					newmax = RPR_QUANTITY_INF;
+				else if (pg_add_s32_overflow(node->max, other->max, &newmax) ||
+						 newmax >= RPR_QUANTITY_INF)
+					break;		/* fallback: leave the pair unmerged */
+
+				if (pg_add_s32_overflow(node->min, other->min, &newmin) ||
+					newmin >= RPR_QUANTITY_INF)
+					break;		/* fallback: leave the pair unmerged */
+
+				node->min = newmin;
+				node->max = newmax;
+				runlen++;
+			}
+		}
+
+		/*
+		 * Survivors are compacted towards the front.  writepos never passes
+		 * readpos, so this cannot overwrite a cell still to be read.
+		 */
+		lfirst(list_nth_cell(children, writepos++)) = node;
+		readpos += runlen;
+	}
+
+	return list_truncate(children, writepos);
+}
+
+/*
+ * 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)
+{
+	int			writepos = 0;
+	int			readpos = 0;
+
+	while (readpos < list_length(children))
+	{
+		RPRPatternNode *node = list_nth_node(RPRPatternNode, children, readpos);
+		int			count = 1;
+
+		/* A quantifier never lands on an ALT, so none of these is reluctant */
+		if (node->nodeType == RPR_PATTERN_ALT)
+		{
+			/* Count the run of ALTs identical to this one */
+			while (readpos + count < list_length(children))
+			{
+				RPRPatternNode *other;
+
+				other = list_nth_node(RPRPatternNode, children, readpos + count);
+
+				if (!rprPatternEqual(node, other))
+					break;
+
+				count++;
+			}
+
+			if (count > 1)
+			{
+				/* Wrap the run into GROUP{count,count}(ALT) */
+				RPRPatternNode *group = makeNode(RPRPatternNode);
+
+				group->nodeType = RPR_PATTERN_GROUP;
+				group->min = count;
+				group->max = count;
+				group->reluctant = false;
+				group->location = -1;
+				group->children = list_make1(node);
+				node = group;
+			}
+		}
+
+		/*
+		 * Survivors are compacted towards the front.  writepos never passes
+		 * readpos, so this cannot overwrite a cell still to be read.
+		 */
+		lfirst(list_nth_cell(children, writepos++)) = node;
+		readpos += count;
+	}
+
+	return list_truncate(children, writepos);
+}
+
+/*
+ * mergeGroupPrefixSuffix
+ *		Merge sequence prefix/suffix into GROUP with matching children.
+ *
+ * When a GROUP's children appear as a prefix before and/or suffix after
+ * the GROUP in a SEQ, merge them by incrementing the GROUP's quantifier.
+ * This runs iteratively: A B A B (A B)+ A B -> (A B){4,}.
+ *
+ * Algorithm:
+ *   For each GROUP encountered in the sequence:
+ *   1. PREFIX phase: compare the last N survivors kept so far against the
+ *      GROUP's children.  On match, drop them and increment the GROUP's
+ *      min/max.  Repeat until no match.
+ *   2. SUFFIX phase: compare the next N elements not yet read against the
+ *      GROUP's children.  On match, skip them and increment min/max.
+ *      Repeat until no match.
+ *
+ * Examples:
+ *   A B (A B)+ -> (A B){2,}
+ *   (A B)+ A B -> (A B){2,}
+ *   A B (A B)+ A B -> (A B){3,}
+ *
+ * The two phases are not equally safe.  A prefix copy is mandatory and comes
+ * before the group, exactly like the leading mandatory iterations it becomes,
+ * so the decision trees stay isomorphic for any content.  A suffix copy comes
+ * after the group has already decided to stop, which the merged form defers
+ * until after the last iteration's own choices.  Merge a suffix only when the
+ * content consumes a fixed number of rows, which leaves it nothing to decide;
+ * see mergeConsecutiveGroups for why that condition is the right one.
+ */
+static List *
+mergeGroupPrefixSuffix(List *children)
+{
+	int			numChildren;
+	int			writepos;
+	int			readpos;
+
+	/*
+	 * PREFIX phase.  Every copy that sits immediately before a GROUP is
+	 * folded into it, over the whole sequence, before any suffix is
+	 * considered.  A copy between two GROUPs is a suffix of the one before it
+	 * and a prefix of the one after, and this order hands it to the second,
+	 * which is the safer of the two rules: a mandatory copy before a group
+	 * already sits where the leading iterations it becomes would sit, so
+	 * folding it as a prefix holds for any content, while folding one as a
+	 * suffix needs a fixed-length body.
+	 *
+	 * A prefix copy is one of the survivors already stored, so folding it is
+	 * a step back of the write cursor.
+	 */
+	writepos = 0;
+	numChildren = list_length(children);
+
+	for (readpos = 0; readpos < numChildren; readpos++)
+	{
+		RPRPatternNode *child = list_nth_node(RPRPatternNode, children, readpos);
+
+		if (child->nodeType == RPR_PATTERN_GROUP && !child->reluctant)
+		{
+			List	   *content = rprGroupContent(child);
+			int			content_len = list_length(content);
+
+			while (rprChildrenMatchAt(children, writepos - content_len, content) &&
+				   rprTryAddIteration(child))
+				writepos -= content_len;
+		}
+
+		/*
+		 * Survivors are compacted towards the front.  writepos never passes
+		 * readpos, so this cannot overwrite a cell still to be read.
+		 */
+		lfirst(list_nth_cell(children, writepos++)) = child;
+	}
+
+	children = list_truncate(children, writepos);
+
+	/*
+	 * SUFFIX phase.  A suffix copy comes after the GROUP has already decided
+	 * to stop, which the merged form defers until after the last iteration's
+	 * own choices, so the body must consume a fixed number of rows; see
+	 * above.
+	 *
+	 * Such a copy is still unread, so folding it is a step forward of the
+	 * read cursor.
+	 */
+	writepos = 0;
+	readpos = 0;
+	numChildren = list_length(children);
+
+	while (readpos < numChildren)
+	{
+		RPRPatternNode *child = list_nth_node(RPRPatternNode, children, readpos);
+		int			runlen = 1;
+
+		if (child->nodeType == RPR_PATTERN_GROUP && !child->reluctant)
+		{
+			List	   *content = rprGroupContent(child);
+			int			content_len = list_length(content);
+
+			while (rprBodyHasUniformLength(content) &&
+				   rprChildrenMatchAt(children, readpos + runlen, content) &&
+				   rprTryAddIteration(child))
+				runlen += content_len;
+		}
+
+		lfirst(list_nth_cell(children, writepos++)) = child;
+		readpos += runlen;
+	}
+
+	return list_truncate(children, writepos);
+}
+
+/*
+ * optimizeSeqPattern
+ *		Optimize SEQ pattern node.
+ *
+ * Optimizations, in the order they run:
+ *   1. Recursively optimize the children and flatten nested SEQ
+ *   2. Merge consecutive identical VAR nodes
+ *   3. Merge consecutive identical GROUP nodes
+ *   4. Merge consecutive identical ALT nodes into GROUP
+ *   5. Merge prefix/suffix into GROUP with matching children
+ *   6. Merge consecutive identical GROUP nodes once more
+ *   7. Unwrap single-item SEQ
+ *
+ * That order carries meaning: 1 is what optimizes the children, so every pass
+ * after it sees a flat list of finished nodes, and 6 runs for the reason
+ * given there.
+ */
+static RPRPatternNode *
+optimizeSeqPattern(RPRPatternNode *pattern)
+{
+	pattern->children = flattenSeqChildren(pattern->children);
+	pattern->children = mergeConsecutiveVars(pattern->children);
+	pattern->children = mergeConsecutiveGroups(pattern->children);
+	pattern->children = mergeConsecutiveAlts(pattern->children);
+	pattern->children = mergeGroupPrefixSuffix(pattern->children);
+
+	/*
+	 * Two identical GROUPs can end up next to each other with nothing having
+	 * put them side by side: the ALT merge wraps a run into a GROUP that may
+	 * land beside an identical one, and folding a prefix or a suffix away can
+	 * close the gap between two.  So the GROUP merge gets a second look.  One
+	 * is enough: it only drops elements and raises quantifiers, so it creates
+	 * no copy for the prefix/suffix pass to fold in turn.
+	 */
+	pattern->children = mergeConsecutiveGroups(pattern->children);
+
+	/* Unwrap single-item SEQ: SEQ[A] -> A */
+	if (list_length(pattern->children) == 1)
+		return (RPRPatternNode *) linitial(pattern->children);
+
+	return pattern;
+}
+
+/*
+ * flattenAltChildren
+ *		Recursively optimize children and flatten nested ALT nodes.
+ *
+ * Example:
+ *   (A | (B | C)) -> (A | B | C)
+ *
+ * Splices each nested ALT's children into the parent list at the position the
+ * ALT occupied, so the flattened alternatives keep their place.  Like
+ * flattenSeqChildren(), this pass can end up longer than what it started
+ * with, so it builds a new list rather than compacting the cells it has; the
+ * caller must assign the result.
+ */
+static List *
+flattenAltChildren(List *children)
+{
+	List	   *flattened = NIL;
+
+	foreach_node(RPRPatternNode, child, children)
+	{
+		RPRPatternNode *optimized = optimizeRPRPattern(child);
+
+		if (optimized->nodeType == RPR_PATTERN_ALT)
+			flattened = list_concat(flattened, optimized->children);
+		else
+			flattened = lappend(flattened, optimized);
+	}
+
+	return flattened;
+}
+
+/*
+ * removeDuplicateAlternatives
+ *		Remove duplicate alternatives from a list.
+ *
+ * Examples:
+ *   (A | B | A) -> (A | B)
+ *   (X | Y | X | Z | Y) -> (X | Y | Z)
+ *
+ * Keeps the first of each and compacts the survivors towards the front of the
+ * list it was given, so the caller must assign the truncated result.
+ */
+static List *
+removeDuplicateAlternatives(List *children)
+{
+	int			writepos = 0;
+
+	for (int readpos = 0; readpos < list_length(children); readpos++)
+	{
+		RPRPatternNode *node = list_nth_node(RPRPatternNode, children, readpos);
+		bool		isDuplicate = false;
+
+		/*
+		 * Survivors are compacted towards the front, so those already kept
+		 * are the cells below writepos.  writepos never passes readpos, so
+		 * the store below cannot overwrite a cell still to be read.
+		 */
+		for (int keptpos = 0; keptpos < writepos; keptpos++)
+		{
+			if (rprPatternEqual(list_nth_node(RPRPatternNode, children, keptpos),
+								node))
+			{
+				isDuplicate = true;
+				break;
+			}
+		}
+
+		if (!isDuplicate)
+			lfirst(list_nth_cell(children, writepos++)) = node;
+	}
+
+	return list_truncate(children, writepos);
+}
+
+/*
+ * optimizeAltPattern
+ *		Optimize ALT pattern node.
+ *
+ * Optimizations:
+ *   1. Flatten nested ALT
+ *   2. Remove duplicate alternatives
+ *   3. Unwrap single-item ALT
+ */
+static RPRPatternNode *
+optimizeAltPattern(RPRPatternNode *pattern)
+{
+	/* Recursively optimize children and flatten nested ALT */
+	pattern->children = flattenAltChildren(pattern->children);
+
+	/* Remove duplicate alternatives */
+	pattern->children = removeDuplicateAlternatives(pattern->children);
+
+	/* Unwrap single-item ALT: ALT[A] -> A */
+	if (list_length(pattern->children) == 1)
+		return (RPRPatternNode *) linitial(pattern->children);
+
+	return pattern;
+}
+
+/*
+ * tryMultiplyQuantifiers
+ *		Try to flatten (child{p,q}){m,n} into child{p*m, q*n}.
+ *
+ * Below, p,q are the child's {min,max} and m,n the outer {min,max}.
+ *
+ * Flattening is valid only when the repetition counts the nested quantifiers
+ * can produce form exactly the contiguous interval [p*m, q*n].  For an outer
+ * iteration count t (m <= t <= n) the child contributes any count in
+ * [t*p, t*q], and t = 0 contributes {0}.  The union of those intervals is
+ * contiguous, hence flattenable, when:
+ *
+ *   - m == n: a single outer count, so the result is just [m*p, m*q]; or
+ *   - p == 0: every interval starts at 0, so they all overlap; or
+ *   - consecutive intervals touch and the zero case (if any) connects:
+ *       p <= Max(m,1)*(q-p) + 1   (touch; trivially true if q is unbounded)
+ *       and (m >= 1 or p <= 1)    (when m == 0, {0} must reach [p,q])
+ *
+ * Otherwise gaps appear and the pattern is left unflattened: (A{2}){2,3}
+ * yields {4,6} (not 4..6), and (A{2,})* yields {0} UNION [2,INF) (not
+ * [0,INF), so A* would wrongly admit a single A).
+ *
+ * Contiguity settles the set of counts, not which count is preferred, so a
+ * further condition is needed; see the comment on safe below.
+ *
+ * Returns the child node with multiplied quantifiers if successful,
+ * otherwise returns the original pattern unchanged.
+ */
+static RPRPatternNode *
+tryMultiplyQuantifiers(RPRPatternNode *pattern)
+{
+	RPRPatternNode *child;
+	bool		safe;
+	int32		newmin;
+	int32		newmax;
+
+	/* Parser always creates GROUP with exactly one child */
+	Assert(list_length(pattern->children) == 1);
+
+	if (pattern->reluctant)
+		return pattern;
+
+	child = (RPRPatternNode *) linitial(pattern->children);
+
+	if ((child->nodeType != RPR_PATTERN_VAR &&
+		 child->nodeType != RPR_PATTERN_GROUP) ||
+		child->reluctant)
+		return pattern;
+
+	/*
+	 * Flattening erases the outer block boundaries, so how the child's
+	 * iterations split across blocks must not matter.  A fixed-length body
+	 * ensures that -- splits with the same total span the same rows -- as
+	 * does an exact child quantifier, which admits only one split.  Otherwise
+	 * preferment shifts: ((A | B B){1,2}){2} would become (A | B B){2,4},
+	 * which stops at two A's where the nested form prefers A (B B) A A.
+	 *
+	 * A VAR child has no body to measure, and its own quantifier already
+	 * settles this, so the test applies to a GROUP child only.
+	 */
+	if (child->min != child->max &&
+		child->nodeType == RPR_PATTERN_GROUP &&
+		!rprBodyHasUniformLength(child->children))
+		return pattern;
+
+	/*
+	 * Decide whether the achievable counts form one contiguous interval.  The
+	 * child quantifier is {child->min, child->max} and the outer one is
+	 * {pattern->min, pattern->max}; either max may be RPR_QUANTITY_INF.
+	 */
+	if (pattern->min == pattern->max || child->min == 0)
+		safe = true;
+	else
+	{
+		bool		touch;
+		bool		zero_ok;
+		bool		order_ok;
+
+		/*
+		 * Consecutive intervals [t*min, t*max] and [(t+1)*min, (t+1)*max]
+		 * touch when (t+1)*min <= t*max + 1, i.e. min <= t*(max-min) + 1.
+		 * This is tightest at the smallest t in play, Max(pattern->min, 1).
+		 * An unbounded child->max makes every interval reach INF, so they
+		 * always touch.
+		 */
+		if (child->max == RPR_QUANTITY_INF)
+			touch = true;
+		else
+			touch = ((int64) child->min <=
+					 (int64) Max(pattern->min, 1) * (child->max - child->min) + 1);
+
+		/*
+		 * A skippable outer (min 0) also needs {0} adjacent to the child
+		 * range.
+		 */
+		zero_ok = (pattern->min >= 1 || child->min <= 1);
+
+		/*
+		 * Contiguity is necessary but not sufficient: the flattened form must
+		 * prefer the same match too.  The nested form settles the first
+		 * iteration's count before iterating again, so it stops early when
+		 * the tail cannot reach the child's lower bound -- (A{2,3}){1,2}
+		 * prefers three rows where the flattened A{2,6} takes four.  Only a
+		 * bounded lower bound >= 2 can be undershot.
+		 */
+		order_ok = (child->min <= 1 || child->max == RPR_QUANTITY_INF);
+
+		safe = touch && zero_ok && order_ok;
+	}
+
+	if (!safe)
+		return pattern;
+
+	/* Flatten the child quantifier, declining the rewrite if it does not fit */
+	if (pg_mul_s32_overflow(pattern->min, child->min, &newmin) ||
+		newmin >= RPR_QUANTITY_INF)
+		return pattern;
+
+	/*
+	 * RPR_QUANTITY_INF means unbounded, not a count: a finite product landing
+	 * on it is representable, so reject it separately.
+	 */
+	if (pattern->max == RPR_QUANTITY_INF || child->max == RPR_QUANTITY_INF)
+		newmax = RPR_QUANTITY_INF;
+	else if (pg_mul_s32_overflow(pattern->max, child->max, &newmax) ||
+			 newmax >= RPR_QUANTITY_INF)
+		return pattern;
+
+	child->min = newmin;
+	child->max = newmax;
+	return child;
+}
+
+/*
+ * tryUnwrapGroup
+ *		Try to unwrap GROUP{1,1} node.
+ *
+ * Examples:
+ *   (A){1,1}   -> A
+ *   (A B){1,1} -> SEQ(A, B)  (unwraps the inner SEQ)
+ *   (A)?       -> A?         (propagate quantifier to single VAR child)
+ *   (A)+?      -> A+?        (propagate quantifier including reluctant)
+ *
+ * If GROUP has min=1, max=1, return the child directly (reluctant on
+ * {1,1} is meaningless).  If GROUP has a single VAR child with default
+ * quantifier {1,1}, propagate the GROUP's quantifier to the child and
+ * unwrap.  Otherwise returns the pattern unchanged.
+ *
+ * Note: Parser always creates GROUP with exactly one child via list_make1().
+ */
+static RPRPatternNode *
+tryUnwrapGroup(RPRPatternNode *pattern)
+{
+	RPRPatternNode *child;
+
+	/* Parser always creates GROUP with single child */
+	Assert(list_length(pattern->children) == 1);
+
+	child = (RPRPatternNode *) linitial(pattern->children);
+
+	/* GROUP{1,1}: unwrap directly (reluctant on {1,1} is meaningless) */
+	if (pattern->min == 1 && pattern->max == 1)
+		return child;
+
+	/*
+	 * Single VAR child with default {1,1}: propagate GROUP's quantifier to
+	 * the child and unwrap.  E.g., (A)?? -> A??, (A)+? -> A+?
+	 */
+	if (child->nodeType == RPR_PATTERN_VAR &&
+		child->min == 1 && child->max == 1)
+	{
+		child->min = pattern->min;
+		child->max = pattern->max;
+		child->reluctant = pattern->reluctant;
+		return child;
+	}
+
+	return pattern;
+}
+
+/*
+ * optimizeGroupPattern
+ *		Optimize GROUP pattern node.
+ *
+ * Optimizations:
+ *   1. Quantifier multiplication: (A{m}){n} -> A{m*n}
+ *   2. Unwrap GROUP{1,1}
+ */
+static RPRPatternNode *
+optimizeGroupPattern(RPRPatternNode *pattern)
+{
+	ListCell   *lc;
+	RPRPatternNode *result;
+
+	/* Recursively optimize children */
+	foreach(lc, pattern->children)
+	{
+		lfirst(lc) = optimizeRPRPattern((RPRPatternNode *) lfirst(lc));
+	}
+
+	/* Try quantifier multiplication */
+	result = tryMultiplyQuantifiers(pattern);
+	if (result != pattern)
+		return result;
+
+	/* Try unwrapping GROUP{1,1} */
+	return tryUnwrapGroup(pattern);
+}
+
+/*
+ * optimizeRPRPattern
+ *		Optimize RPRPatternNode tree (dispatcher).
+ *
+ * Dispatches to type-specific optimization functions.
+ * Returns the optimized pattern (may be a different node).
+ */
+static RPRPatternNode *
+optimizeRPRPattern(RPRPatternNode *pattern)
+{
+	RPRPatternNode *result = pattern;
+
+	/* Pattern nodes from parser are never NULL */
+	Assert(pattern != NULL);
+
+	check_stack_depth();
+
+	/*
+	 * A fixed count leaves reluctance nothing to decide.  Drop it here: the
+	 * merge and multiplication rewrites below decline a reluctant node, so
+	 * {n,n}? would otherwise miss what {n,n} gets.
+	 */
+	if (pattern->min == pattern->max)
+		pattern->reluctant = false;
+
+	switch (pattern->nodeType)
+	{
+		case RPR_PATTERN_VAR:
+			break;
+		case RPR_PATTERN_SEQ:
+			result = optimizeSeqPattern(pattern);
+			break;
+		case RPR_PATTERN_ALT:
+			result = optimizeAltPattern(pattern);
+			break;
+		case RPR_PATTERN_GROUP:
+			result = optimizeGroupPattern(pattern);
+			break;
+	}
+
+	/* Again: a rewrite may have produced a fixed count of its own */
+	if (result->min == result->max)
+		result->reluctant = false;
+
+	return result;
+}
+
+/*
+ * scanRPRPatternRecursive
+ *		Recursively scan pattern parse tree (pass 1 internal).
+ *
+ * Collects unique variable names and counts elements while tracking depth.
+ * Variables from DEFINE clause are already in varNames; this adds any
+ * additional variables found in the pattern.
+ */
+static void
+scanRPRPatternRecursive(RPRPatternNode *node, char **varNames, int *numVars,
+						int *numElements, RPRDepth depth, RPRDepth *maxDepth)
+{
+	int			i;
+
+	/* Pattern nodes from parser are never NULL */
+	Assert(node != NULL);
+
+	check_stack_depth();
+
+	/* Check recursion depth limit before overflow occurs */
+	if (depth >= RPR_DEPTH_MAX)
+		ereport(ERROR,
+				errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+				errmsg("pattern nesting too deep"),
+				errdetail("Pattern nesting depth %d exceeds maximum %d.",
+						  depth, RPR_DEPTH_MAX - 1));
+
+	/* Track maximum depth */
+	*maxDepth = Max(*maxDepth, depth);
+
+	switch (node->nodeType)
+	{
+		case RPR_PATTERN_VAR:
+			/* Count element */
+			(*numElements)++;
+
+			/* Collect variable name if not already present */
+			for (i = 0; i < *numVars; i++)
+			{
+				if (strcmp(varNames[i], node->varName) == 0)
+					return;		/* Already have this variable */
+			}
+
+			/*
+			 * Variable not in DEFINE clause - this is valid per ISO/IEC
+			 * 19075-5 Feature R020.  Such variables are implicitly TRUE. Add
+			 * to varNames so they get a varId >= the number of DEFINE clause
+			 * expressions, which executor treats as TRUE.
+			 */
+			Assert(*numVars <= RPR_VARID_MAX);
+			varNames[(*numVars)++] = node->varName;
+			break;
+
+		case RPR_PATTERN_SEQ:
+			/* Sequence: just recurse into children */
+			foreach_node(RPRPatternNode, child, node->children)
+			{
+				scanRPRPatternRecursive(child, varNames,
+										numVars, numElements, depth, maxDepth);
+			}
+			break;
+
+		case RPR_PATTERN_GROUP:
+
+			/*
+			 * Add BEGIN element if group has non-trivial quantifier (not
+			 * {1,1})
+			 */
+			if (node->min != 1 || node->max != 1)
+				(*numElements)++;
+
+			/* Recurse into children at increased depth */
+			foreach_node(RPRPatternNode, child, node->children)
+			{
+				scanRPRPatternRecursive(child, varNames,
+										numVars, numElements, depth + 1, maxDepth);
+			}
+
+			/* Add END element if group has non-trivial quantifier (not {1,1}) */
+			if (node->min != 1 || node->max != 1)
+				(*numElements)++;
+			break;
+
+		case RPR_PATTERN_ALT:
+			/* Count ALT start element */
+			(*numElements)++;
+
+			/* Recurse into children at increased depth */
+			foreach_node(RPRPatternNode, child, node->children)
+			{
+				/* Each branch is terminated by a SEP branch-separator marker */
+				(*numElements)++;
+				scanRPRPatternRecursive(child, varNames,
+										numVars, numElements, depth + 1, maxDepth);
+			}
+			break;
+	}
+}
+
+/*
+ * scanRPRPattern
+ *		Scan pattern parse tree (pass 1 entry point).
+ *
+ * Collects unique variable names (appending to those from DEFINE clause),
+ * counts total elements (including FIN marker), and tracks maximum depth.
+ * Reports error if element count exceeds RPR_ELEMIDX_MAX.
+ */
+static void
+scanRPRPattern(RPRPatternNode *node, char **varNames, int *numVars,
+			   int *numElements, RPRDepth *maxDepth)
+{
+	*numElements = 0;
+	*maxDepth = 0;
+
+	scanRPRPatternRecursive(node, varNames, numVars, numElements, 0, maxDepth);
+
+	(*numElements)++;			/* +1 for FIN marker */
+
+	if (*numElements > RPR_ELEMIDX_MAX)
+		ereport(ERROR,
+				errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+				errmsg("pattern too complex"),
+				errdetail("Pattern has %d elements, maximum is %d.",
+						  *numElements, RPR_ELEMIDX_MAX));
+}
+
+/*
+ * makeRPRPattern
+ *		Allocate and initialize RPRPattern structure.
+ *
+ * Creates the pattern structure, copies variable names, and allocates
+ * the elements array. The elements array is zero-initialized.
+ */
+static RPRPattern *
+makeRPRPattern(int numVars, int numElements, RPRDepth maxDepth,
+			   char **varNamesStack)
+{
+	RPRPattern *result;
+	int			i;
+
+	result = makeNode(RPRPattern);
+	result->numVars = numVars;
+
+	/* depth < RPR_DEPTH_MAX, so maxDepth + 1 does not exceed RPR_DEPTH_MAX. */
+	Assert(maxDepth < RPR_DEPTH_MAX);
+	result->maxDepth = maxDepth + 1;	/* +1: depth is 0-based */
+	result->numElements = numElements;
+
+	/* Copy varNames (pattern must have at least one variable) */
+	Assert(numVars > 0);
+	result->varNames = palloc_array(char *, numVars);
+	for (i = 0; i < numVars; i++)
+		result->varNames[i] = pstrdup(varNamesStack[i]);
+
+	/* Allocate elements array (zero-init for reserved fields) */
+	Assert(numElements >= 2);
+	result->elements = palloc0_array(RPRPatternElement, numElements);
+
+	return result;
+}
+
+/*
+ * getVarIdFromPattern
+ *		Get variable ID for a variable name from RPRPattern.
+ *
+ * Returns the index of the variable in the varNames array.
+ */
+static RPRVarId
+getVarIdFromPattern(RPRPattern *pat, const char *varName)
+{
+	for (int i = 0; i < pat->numVars; i++)
+	{
+		if (strcmp(pat->varNames[i], varName) == 0)
+			return (RPRVarId) i;
+	}
+
+	/* Should not happen - variable should already be collected */
+	elog(ERROR, "pattern variable \"%s\" not found", varName);
+	pg_unreachable();
+}
+
+/*
+ * fillRPRPatternVar
+ *		Fill a VAR pattern element.
+ *
+ * Returns the empty-match flags for this VAR: RPR_ELEM_EMPTY_LOOP when it is
+ * nullable (min 0, can match zero rows), plus RPR_ELEM_EMPTY_PREFERRED when its
+ * preferred derivation is the empty one.  A bare variable consumes a row; only
+ * a reluctant quantifier that may take zero repetitions prefers to skip it.
+ */
+static RPRElemFlags
+fillRPRPatternVar(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth depth)
+{
+	RPRPatternElement *elem = &pat->elements[*idx];
+	RPRElemFlags flags = 0;
+
+	memset(elem, 0, sizeof(RPRPatternElement));
+	elem->varId = getVarIdFromPattern(pat, node->varName);
+	elem->depth = depth;
+	elem->min = node->min;
+	elem->max = node->max;
+	Assert(elem->min >= 0 && elem->min < RPR_QUANTITY_INF &&
+		   elem->max >= 1 && elem->min <= elem->max);
+	elem->next = RPR_ELEMIDX_INVALID;
+	elem->jump = RPR_ELEMIDX_INVALID;
+	if (node->reluctant)
+		elem->flags |= RPR_ELEM_RELUCTANT;
+	(*idx)++;
+
+	if (node->min == 0)
+	{
+		/* nullable; a reluctant quantifier also prefers the empty match */
+		flags |= RPR_ELEM_EMPTY_LOOP;
+		if (node->reluctant)
+			flags |= RPR_ELEM_EMPTY_PREFERRED;
+	}
+	return flags;
+}
+
+/*
+ * fillRPRPatternGroup
+ *		Fill a GROUP pattern and its children.
+ *
+ * Creates elements for group content at increased depth, plus BEGIN/END
+ * marker pair if the group has a non-trivial quantifier (not {1,1}).
+ *
+ * Element layout for (A B){2,3}:
+ *
+ *   [BEGIN]  [A]  [B]  [END]  [next element...]
+ *     |                  |          ^
+ *     |                  +-- jump --+ (loop back to first child)
+ *     +---- jump -------------------+ (skip to after END)
+ *
+ * BEGIN.jump points past END (the skip path taken when min == 0; a count is
+ * only tested at END, so a BEGIN never takes it for reaching max).
+ * END.jump points to the first child (loop-back path).
+ * BEGIN.next and END.next are set later by finalizeRPRPattern().
+ *
+ * Returns the group's empty-match flags.  RPR_ELEM_EMPTY_LOOP is set when the
+ * group is nullable -- its min is 0 (can be skipped entirely) or its body is
+ * nullable (every path through the body can match zero rows).
+ * RPR_ELEM_EMPTY_PREFERRED is set when the group's preferred derivation is
+ * empty: a reluctant min-0 group prefers to take no iteration, and otherwise
+ * the group follows its body.  The END element inherits the body's bits.
+ */
+static RPRElemFlags
+fillRPRPatternGroup(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth depth)
+{
+	int			groupStartIdx = *idx;
+	int			beginIdx = -1;
+	RPRElemFlags bodyFlags = RPR_ELEM_EMPTY_LOOP | RPR_ELEM_EMPTY_PREFERRED;
+	RPRElemFlags result;
+
+	/* Add BEGIN marker if group has non-trivial quantifier (not {1,1}) */
+	if (node->min != 1 || node->max != 1)
+	{
+		RPRPatternElement *elem = &pat->elements[*idx];
+
+		beginIdx = *idx;
+		memset(elem, 0, sizeof(RPRPatternElement));
+		elem->varId = RPR_VARID_BEGIN;
+		elem->depth = depth;
+		elem->min = node->min;
+		elem->max = node->max;
+		Assert(elem->min >= 0 && elem->min < RPR_QUANTITY_INF &&
+			   elem->max >= 1 && elem->min <= elem->max);
+		elem->next = RPR_ELEMIDX_INVALID;	/* set by finalize */
+		elem->jump = RPR_ELEMIDX_INVALID;	/* set after END */
+		if (node->reluctant)
+			elem->flags |= RPR_ELEM_RELUCTANT;
+		(*idx)++;
+		groupStartIdx = *idx;	/* children start after BEGIN */
+	}
+
+	/* A concatenation is nullable / empty-preferred iff every child is (AND) */
+	foreach_node(RPRPatternNode, child, node->children)
+		bodyFlags &= fillRPRPattern(child, pat, idx, depth + 1);
+
+	/* Add group end marker if group has non-trivial quantifier (not {1,1}) */
+	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;
+		Assert(endElem->min >= 0 && endElem->min < RPR_QUANTITY_INF &&
+			   endElem->max >= 1 && endElem->min <= endElem->max);
+		endElem->next = RPR_ELEMIDX_INVALID;
+		endElem->jump = groupStartIdx;	/* loop to first child */
+		if (node->reluctant)
+			endElem->flags |= RPR_ELEM_RELUCTANT;
+
+		/* The END carries the body's bits, not the group's; see README IV-4b */
+		endElem->flags |= bodyFlags;
+
+		(*idx)++;
+
+		/* Set BEGIN skip pointer (next is set by finalize) */
+		beginElem->jump = *idx; /* skip: go to after END */
+	}
+
+	result = bodyFlags;
+	if (node->min == 0)
+	{
+		/* skippable entirely; if reluctant, the group prefers to skip */
+		result |= RPR_ELEM_EMPTY_LOOP;
+		if (node->reluctant)
+			result |= RPR_ELEM_EMPTY_PREFERRED;
+	}
+	return result;
+}
+
+/*
+ * fillRPRPatternAlt
+ *		Fill an ALT pattern and its alternatives.
+ *
+ * Creates the ALT marker and fills each alternative at increased depth,
+ * terminating every alternative (including the last) with a SEP
+ * branch-separator marker.  The branch link runs from ALT through the SEP
+ * chain, never through the branch content: a branch's first element may
+ * itself be a group BEGIN, whose jump is that group's skip-past-END path.
+ *
+ *   ALT.next  -> first branch content      SEP.next -> next branch content
+ *   ALT.jump  -> first SEP                            (post-ALT on the last)
+ *   SEP.jump  -> next SEP (-1 on the last)
+ *
+ * SEP is a marker, never a state: a branch's tail and a branch-terminal
+ * group's BEGIN skip are redirected past the alternation.
+ *
+ * Returns the alternation's empty-match flags.  RPR_ELEM_EMPTY_LOOP is set if
+ * any branch is nullable (OR: one nullable branch suffices).
+ * RPR_ELEM_EMPTY_PREFERRED follows the first branch alone: lexicographic order
+ * makes it the preferred one, so whether the later branches prefer empty does
+ * not matter.
+ */
+static RPRElemFlags
+fillRPRPatternAlt(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth depth)
+{
+	ListCell   *lc;
+	ListCell   *lc2;
+	RPRPatternElement *elem;
+	int			altIdx = *idx;
+	List	   *altBranchStarts = NIL;
+	List	   *altEndPositions = NIL;
+	int			afterAltIdx;
+	RPRElemFlags altFlags = 0;	/* OR of branch EMPTY_LOOP bits */
+	RPRElemFlags firstFlags = 0;	/* first branch's flags (for
+									 * EMPTY_PREFERRED) */
+	bool		firstBranch = true;
+
+	/* Add alternation start marker */
+	elem = &pat->elements[*idx];
+	memset(elem, 0, sizeof(RPRPatternElement));
+	elem->varId = RPR_VARID_ALT;
+	elem->depth = depth;
+	elem->min = 1;
+	elem->max = 1;
+	elem->next = RPR_ELEMIDX_INVALID;
+	elem->jump = RPR_ELEMIDX_INVALID;
+	(*idx)++;
+
+	/* ALT enters the first branch's content (the contiguous next element) */
+	pat->elements[altIdx].next = *idx;
+
+	/* Fill each alternative, terminated by a SEP branch-separator marker */
+	foreach_node(RPRPatternNode, alt, node->children)
+	{
+		int			branchStart = *idx;
+		RPRPatternElement *sep;
+		RPRElemFlags branchFlags;
+
+		altBranchStarts = lappend_int(altBranchStarts, branchStart);
+		branchFlags = fillRPRPattern(alt, pat, idx, depth + 1);
+
+		/* nullable if ANY branch is; empty-preferred per the FIRST branch */
+		altFlags |= (branchFlags & RPR_ELEM_EMPTY_LOOP);
+		if (firstBranch)
+		{
+			firstFlags = branchFlags;
+			firstBranch = false;
+		}
+		altEndPositions = lappend_int(altEndPositions, *idx - 1);
+
+		/* SEP terminates this branch, so it sits at branch end + 1 */
+		sep = &pat->elements[*idx];
+		memset(sep, 0, sizeof(RPRPatternElement));
+		sep->varId = RPR_VARID_SEP;
+		sep->depth = depth;		/* ALT-level boundary, not branch-level */
+		sep->min = 1;
+		sep->max = 1;
+		sep->next = RPR_ELEMIDX_INVALID;
+		sep->jump = RPR_ELEMIDX_INVALID;
+		(*idx)++;
+	}
+
+	afterAltIdx = *idx;
+
+	/* ALT reaches the first branch's terminating SEP */
+	pat->elements[altIdx].jump = linitial_int(altEndPositions) + 1;
+
+	/*
+	 * Wire the SEP chain, and redirect each branch's exits past the
+	 * alternation.
+	 */
+	forboth(lc, altBranchStarts, lc2, altEndPositions)
+	{
+		int			branchStart = lfirst_int(lc);
+		int			endPos = lfirst_int(lc2);
+		int			sepIdx = endPos + 1;
+		ListCell   *nextEnd = lnext(altEndPositions, lc2);
+		int			elemIdx;
+
+		/* SEP.jump -> next SEP; SEP.next -> next branch content */
+		if (nextEnd != NULL)
+		{
+			pat->elements[sepIdx].jump = lfirst_int(nextEnd) + 1;
+			pat->elements[sepIdx].next = lfirst_int(lnext(altBranchStarts, lc));
+		}
+		else
+		{
+			pat->elements[sepIdx].jump = RPR_ELEMIDX_INVALID;
+			pat->elements[sepIdx].next = afterAltIdx;
+		}
+
+		/*
+		 * Redirect the branch's fall-through exit to after the alternation.
+		 * The natural exit is the element past the branch content, which is
+		 * this branch's SEP; a simple tail leaves next unset (finalize would
+		 * fall it through to the SEP), while an inner ALT already set next to
+		 * that position.
+		 */
+		if (pat->elements[endPos].next != RPR_ELEMIDX_INVALID)
+		{
+			int			oldTarget = pat->elements[endPos].next;
+
+			for (elemIdx = branchStart; elemIdx <= endPos; elemIdx++)
+			{
+				if (pat->elements[elemIdx].next == oldTarget)
+					pat->elements[elemIdx].next = afterAltIdx;
+			}
+		}
+		else
+		{
+			pat->elements[endPos].next = afterAltIdx;
+		}
+
+		/*
+		 * A branch-terminal group's BEGIN skip-past-END path points at the
+		 * element following the group, which is this branch's SEP; redirect
+		 * it past the alternation too so the skip never lands on a SEP.
+		 */
+		for (elemIdx = branchStart; elemIdx <= endPos; elemIdx++)
+		{
+			if (pat->elements[elemIdx].jump == sepIdx)
+				pat->elements[elemIdx].jump = afterAltIdx;
+		}
+	}
+
+	list_free(altBranchStarts);
+	list_free(altEndPositions);
+
+	return altFlags | (firstFlags & RPR_ELEM_EMPTY_PREFERRED);
+}
+
+/*
+ * fillRPRPattern
+ *		Fill pattern elements array from parse tree (pass 2).
+ *
+ * Recursively traverses the parse tree and populates pre-allocated elements
+ * array.
+ * Dispatches to type-specific fill functions.
+ *
+ * Returns the pattern's empty-match flags (RPR_ELEM_EMPTY_LOOP for nullable,
+ * RPR_ELEM_EMPTY_PREFERRED for empty-preferred).  For a SEQ, a concatenation is
+ * nullable / empty-preferred only when every child is, so the children's flags
+ * are AND-ed together.
+ */
+static RPRElemFlags
+fillRPRPattern(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth depth)
+{
+	/* Pattern nodes from parser are never NULL */
+	Assert(node != NULL);
+
+	check_stack_depth();
+
+	switch (node->nodeType)
+	{
+		case RPR_PATTERN_SEQ:
+			{
+				RPRElemFlags flags = RPR_ELEM_EMPTY_LOOP | RPR_ELEM_EMPTY_PREFERRED;
+
+				foreach_node(RPRPatternNode, child, node->children)
+					flags &= fillRPRPattern(child, pat, idx, depth);
+				return flags;
+			}
+
+		case RPR_PATTERN_VAR:
+			return fillRPRPatternVar(node, pat, idx, depth);
+
+		case RPR_PATTERN_GROUP:
+			return fillRPRPatternGroup(node, pat, idx, depth);
+
+		case RPR_PATTERN_ALT:
+			return fillRPRPatternAlt(node, pat, idx, depth);
+	}
+
+	pg_unreachable();
+	return 0;
+}
+
+/*
+ * finalizeRPRPattern
+ *		Finalize pattern structure after filling elements.
+ *
+ * This performs:
+ *   1. Initialize absorption flag to false
+ *   2. Set up next pointers for sequential flow
+ *   3. Add FIN marker at the end
+ */
+static void
+finalizeRPRPattern(RPRPattern *result)
+{
+	int			finIdx = result->numElements - 1;
+	int			i;
+	RPRPatternElement *finElem;
+
+	/* Initialize absorption flag */
+	result->isAbsorbable = false;
+
+	/* Set up next pointers for elements that don't have one */
+	for (i = 0; i < finIdx; i++)
+	{
+		RPRPatternElement *elem = &result->elements[i];
+
+		if (elem->next == RPR_ELEMIDX_INVALID)
+			elem->next = (i < finIdx - 1) ? i + 1 : finIdx;
+
+		/* Verify quantifier range is valid */
+		Assert(elem->min >= 0 && elem->min < RPR_QUANTITY_INF &&
+			   elem->max >= 1 && elem->min <= elem->max);
+	}
+
+	/* Add FIN marker at the end */
+	finElem = &result->elements[finIdx];
+	memset(finElem, 0, sizeof(RPRPatternElement));
+	finElem->varId = RPR_VARID_FIN;
+	finElem->depth = 0;
+	finElem->min = 1;
+	finElem->max = 1;
+	finElem->next = RPR_ELEMIDX_INVALID;
+	finElem->jump = RPR_ELEMIDX_INVALID;
+}
+
+/*-------------------------------------------------------------------------
+ * CONTEXT ABSORPTION: TWO-FLAG DESIGN
+ *-------------------------------------------------------------------------
+ *
+ * Context absorption eliminates redundant match searches by absorbing newer
+ * contexts that cannot produce longer matches than older contexts. This
+ * achieves O(n^2) -> O(n) performance improvement for patterns like A+ B.
+ *
+ * Core Insight:
+ *   For pattern A+ B, if Ctx1 starts at row 0 and Ctx2 starts at row 1,
+ *   both matching A continuously, Ctx1 will always have more A matches.
+ *   When B finally appears, Ctx1's match (0 to current) is always longer
+ *   than Ctx2's match (1 to current). So Ctx2 can be safely eliminated.
+ *
+ * Two Flags:
+ *   1. RPR_ELEM_ABSORBABLE - "Absorption comparison point"
+ *      WHERE contexts can be compared for absorption.
+ *      - Simple unbounded VAR (A+): the VAR element itself
+ *      - Unbounded GROUP ((A B)+): the END element only
+ *
+ *   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 comparison point (where to compare)
+ *   - ABSORBABLE_BRANCH keeps state.isAbsorbable=true through A->B->END
+ *
+ * Pattern Examples:
+ *
+ *   Pattern: A+ B
+ *   Element 0 (A): ABSORBABLE | ABSORBABLE_BRANCH  <- comparison point
+ *   Element 1 (B): (none)
+ *   -> Compare at A every row. When contexts move to B, absorption stops.
+ *
+ *   Pattern: (A B)+ C
+ *   Element 0 (BEGIN): ABSORBABLE_BRANCH
+ *   Element 1 (A): ABSORBABLE_BRANCH
+ *   Element 2 (B): ABSORBABLE_BRANCH
+ *   Element 3 (END): ABSORBABLE | ABSORBABLE_BRANCH  <- comparison point
+ *   Element 4 (C): (none)
+ *   -> Compare at END every 2 rows. When contexts move to C, absorption stops.
+ *
+ *   Pattern: (A+ B+)+ C
+ *   Element 0 (BEGIN): ABSORBABLE_BRANCH
+ *   Element 1 (A): ABSORBABLE | ABSORBABLE_BRANCH  <- comparison point
+ *   Element 2 (B): (none)
+ *   Element 3 (END): (none)
+ *   Element 4 (C): (none)
+ *   -> Compare at A during the first iteration. After moving to B+,
+ *      absorption stops.
+ *
+ * First Unbounded Portion Strategy:
+ *   Along one path the algorithm only flags the FIRST unbounded portion
+ *   starting from element 0; an alternation is walked branch by branch, so
+ *   each branch may contribute one (A+ | B+ gives both). 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 execRPR.c):
+ *   - state.isAbsorbable = (previous && elem.ABSORBABLE_BRANCH)
+ *   - Monotonic: once false, stays false (cannot re-enter region)
+ *   - context.hasAbsorbableState: can absorb others (>=1 absorbable state)
+ *   - context.allStatesAbsorbable: can be absorbed (ALL states absorbable)
+ *   - Absorption check: if Ctx1.hasAbsorbable && Ctx2.allAbsorbable,
+ *     compare counts at same elemIdx, absorb if Ctx1.count >= Ctx2.count
+ *
+ *-------------------------------------------------------------------------
+ */
+
+/*
+ * isFixedLengthChildren
+ *		Check if all children at scopeDepth have fixed-length quantifiers
+ *		(min == max), recursively for nested subgroups.
+ *
+ * A fixed-length group is semantically equivalent to unrolling each child
+ * to {1,1} copies, which is the existing Case 2 already proven correct
+ * for absorption.  This check recognizes fixed-length groups at compile
+ * time without actually unrolling them.
+ *
+ * Traverses the flat element array starting at idx.  For VAR elements,
+ * checks min == max.  For BEGIN elements (nested subgroups), recurses
+ * into the subgroup and also checks the subgroup's END quantifier.
+ * ALT elements are rejected (alternation inside absorbable group is
+ * not supported).
+ *
+ * Returns true if all children are fixed-length, stopping at the END
+ * element at scopeDepth - 1.
+ */
+static bool
+isFixedLengthChildren(RPRPattern *pattern, RPRElemIdx idx, RPRDepth scopeDepth)
+{
+	RPRPatternElement *e = &pattern->elements[idx];
+
+	check_stack_depth();
+
+	while (e->depth == scopeDepth)
+	{
+		if (RPRElemIsVar(e))
+		{
+			if (e->min != e->max)
+				return false;
+		}
+		else if (RPRElemIsBegin(e))
+		{
+			RPRElemIdx	childIdx = e->next;
+
+			/* Recurse into subgroup children at scopeDepth + 1 */
+			if (!isFixedLengthChildren(pattern, childIdx, scopeDepth + 1))
+				return false;
+
+			/* Advance past the subgroup to its END element */
+			e = &pattern->elements[e->next];
+			while (e->depth > scopeDepth)
+				e = &pattern->elements[e->next];
+
+			/* e is now the END at scopeDepth; check its quantifier */
+			Assert(RPRElemIsEnd(e) && e->depth == scopeDepth);
+			if (e->min != e->max)
+				return false;
+		}
+		else
+		{
+			/* ALT inside group: not supported for absorption */
+			return false;
+		}
+
+		Assert(e->next != RPR_ELEMIDX_INVALID);
+		e = &pattern->elements[e->next];
+	}
+
+	return true;
+}
+
+/*
+ * isUnboundedStart
+ *		Check if the element at idx starts an unbounded greedy sequence.
+ *
+ * For context absorption to work, the sequence starting at idx must be:
+ *   - Unbounded (max = infinity)
+ *   - Greedy (not reluctant)
+ *   - At the start of current scope
+ *
+ * Two cases are handled:
+ *   1. Simple VAR: A+ B C - A has max=INF, gets both flags
+ *   2. Unbounded GROUP with fixed-length children: (A B{2})+ C
+ *      All children must have min == max (recursively for nested subgroups).
+ *      This is equivalent to unrolling to {1,1} VARs, e.g., (A B B)+ C.
+ *      All elements within the group get ABSORBABLE_BRANCH.
+ *      Only the unbounded END gets ABSORBABLE (comparison point).
+ *
+ *      In the examples below, "step" is the number of VARs in one fully
+ *      unrolled iteration of the group (its fixed per-iteration length).
+ *      Examples:
+ *        (A B{2})+ C          - B{2} has min==max, step=3
+ *        (A (B C){2} D)+ E    - nested {2} subgroup, step=6
+ *        ((A (B C){2}){2})+   - doubly nested {2}, step=10
+ *        (A ((B C{3}){2} D){2} E)+ F  - deep nesting, step=20
+ *
+ * Returns false for patterns where absorption cannot work:
+ *   - A B+ (unbounded not at start)
+ *   - A+? B (the unbounded quantifier itself is reluctant)
+ *   - (A | B)+ (ALT inside group)
+ *   - (A B+)+ (variable-length element inside group)
+ *   - (A B{2,5})+ (min != max inside group)
+ *
+ * The reluctance test covers only the quantifier examined here.  A
+ * reluctant quantifier on an enclosing group -- (A+)??, where A+ itself
+ * is greedy -- is rejected at that group's BEGIN by
+ * computeAbsorbabilityRecursive(), before this function is reached.
+ */
+static bool
+isUnboundedStart(RPRPattern *pattern, RPRElemIdx idx)
+{
+	RPRPatternElement *elem = &pattern->elements[idx];
+	RPRDepth	startDepth = elem->depth;
+	RPRPatternElement *e;
+
+	/* Case 1: Simple unbounded VAR at start (greedy only) */
+	if (RPRElemIsVar(elem) && elem->max == RPR_QUANTITY_INF &&
+		!RPRElemIsReluctant(elem))
+	{
+		/* Set both flags on first element */
+		elem->flags |= RPR_ELEM_ABSORBABLE_BRANCH | RPR_ELEM_ABSORBABLE;
+		return true;
+	}
+
+	/*
+	 * Case 2: Unbounded GROUP with fixed-length children.  Each child must
+	 * have min == max (recursively for nested subgroups), ensuring a fixed
+	 * step size per iteration so that count-dominance holds.
+	 */
+	if (!isFixedLengthChildren(pattern, idx, startDepth))
+		return false;
+
+	/*
+	 * Find the END that closes the group beginning at idx, at startDepth - 1.
+	 * FIN bounds the walk: depth alone cannot when startDepth is 0, and FIN's
+	 * next is RPR_ELEMIDX_INVALID, so the walk would read outside the array.
+	 * Only a tree optimizeRPRPattern() did not produce reaches it that way.
+	 */
+	e = &pattern->elements[idx];
+	while (e->depth >= startDepth && !RPRElemIsFin(e))
+		e = &pattern->elements[e->next];
+
+	/* END must be unbounded greedy */
+	if (e->depth == startDepth - 1 &&
+		RPRElemIsEnd(e) && e->max == RPR_QUANTITY_INF &&
+		!RPRElemIsReluctant(e))
+	{
+		Assert(e->jump == idx); /* END points back to first child */
+
+		/* Set ABSORBABLE_BRANCH on all children, ABSORBABLE on END only */
+		for (e = elem; !RPRElemIsEnd(e) || e->depth >= startDepth;
+			 e = &pattern->elements[e->next])
+			e->flags |= RPR_ELEM_ABSORBABLE_BRANCH;
+		e->flags |= RPR_ELEM_ABSORBABLE_BRANCH | RPR_ELEM_ABSORBABLE;
+		return true;
+	}
+
+	return false;
+}
+
+/*
+ * computeAbsorbabilityRecursive
+ *		Recursively check absorbability starting from given index.
+ *
+ * If the element at startIdx is ALT, recursively checks each branch
+ * independently.  Each branch gets its own absorbability status, and if
+ * any branch is absorbable, the ALT element itself is marked with
+ * RPR_ELEM_ABSORBABLE_BRANCH.
+ *
+ * If BEGIN, skips to first child -- but only when the group's own
+ * quantifier is greedy.  Absorption assumes an earlier context subsumes a
+ * later one, which a reluctant group inverts; isUnboundedStart() sees only
+ * the quantifier it is handed, so the greedy A+ in (A+)?? needs this check.
+ *
+ * Otherwise (VAR), checks if the element starts an unbounded sequence via
+ * isUnboundedStart.
+ */
+static void
+computeAbsorbabilityRecursive(RPRPattern *pattern, RPRElemIdx startIdx,
+							  bool *hasAbsorbable)
+{
+	RPRPatternElement *elem = &pattern->elements[startIdx];
+
+	check_stack_depth();
+
+	if (RPRElemIsAlt(elem))
+	{
+		/* ALT: recursively check each branch via the SEP chain */
+		RPRElemIdx	branchStart = elem->next;
+		RPRElemIdx	sepIdx = elem->jump;
+
+		while (sepIdx != RPR_ELEMIDX_INVALID)
+		{
+			RPRPatternElement *sepElem;
+			bool		branchAbsorbable = false;
+
+			/* Recursively check this branch's content */
+			computeAbsorbabilityRecursive(pattern, branchStart,
+										  &branchAbsorbable);
+			if (branchAbsorbable)
+				*hasAbsorbable = true;
+
+			Assert(sepIdx >= 0 && sepIdx < pattern->numElements);
+			sepElem = &pattern->elements[sepIdx];
+			Assert(RPRElemIsSep(sepElem));
+
+			/* The last branch's SEP has no link, ending the walk */
+			branchStart = sepElem->next;
+			sepIdx = sepElem->jump;
+		}
+
+		/* Mark ALT element if any branch is absorbable */
+		if (*hasAbsorbable)
+			elem->flags |= RPR_ELEM_ABSORBABLE_BRANCH;
+	}
+	else if (RPRElemIsBegin(elem))
+	{
+		/*
+		 * Not an absorbable region.  The group's quantifier sits on its BEGIN
+		 * as well as its END, so this element answers for the group.
+		 */
+		if (RPRElemIsReluctant(elem))
+			return;
+
+		/*
+		 * BEGIN: first try to treat this BEGIN's children as an unbounded
+		 * group directly (handles nested fixed-length groups like ((A{2}
+		 * B{3}){2})+).  If that fails, skip to first child and recurse as
+		 * before.
+		 */
+		if (isUnboundedStart(pattern, elem->next))
+		{
+			*hasAbsorbable = true;
+			elem->flags |= RPR_ELEM_ABSORBABLE_BRANCH;
+		}
+		else
+		{
+			computeAbsorbabilityRecursive(pattern, elem->next, hasAbsorbable);
+
+			/* Mark BEGIN element if contents are absorbable */
+			if (*hasAbsorbable)
+				elem->flags |= RPR_ELEM_ABSORBABLE_BRANCH;
+		}
+	}
+	else
+	{
+		/* Should never reach END - structural invariant of pattern parse tree */
+		Assert(!RPRElemIsEnd(elem));
+
+		/* Non-ALT, non-BEGIN: check if unbounded start */
+		if (isUnboundedStart(pattern, startIdx))
+			*hasAbsorbable = true;
+	}
+}
+
+/*
+ * computeAbsorbability
+ *		Determine if pattern supports context absorption optimization.
+ *
+ * Context absorption eliminates redundant match searches by absorbing
+ * newer contexts that cannot produce longer matches than older contexts.
+ * This achieves O(n^2) -> O(n) performance improvement.
+ *
+ * Only greedy unbounded quantifiers at pattern start can be absorbable.
+ * Reluctant quantifiers are excluded because they don't maintain monotonic
+ * decrease property required for safe absorption -- both the unbounded
+ * quantifier itself and any group quantifier enclosing it.
+ *
+ * This function sets two flags:
+ *   RPR_ELEM_ABSORBABLE: Absorption comparison point
+ *     - Simple unbounded VAR: the VAR itself (e.g., A in A+)
+ *     - Unbounded GROUP: the END element (e.g., END in (A B)+)
+ *   RPR_ELEM_ABSORBABLE_BRANCH: All elements in absorbable region
+ *     - Simple unbounded VAR: the VAR itself only
+ *     - Unbounded GROUP: the whole body (including nested subgroups) and the
+ *       group's END, plus any enclosing BEGIN/ALT on the path to it
+ *
+ * Examples:
+ *   A+ B C         - absorbable (A gets both flags)
+ *   (A B)+ C       - absorbable (BEGIN,A,B,END get BRANCH, END gets ABSORBABLE)
+ *   A B+           - NOT absorbable (unbounded not at start)
+ *   A+? B C        - NOT absorbable (reluctant quantifier)
+ *   (A+ B+)+       - only first A+ on first iteration (nested unbounded not supported)
+ *   A+ | B+        - both branches absorbable independently
+ *   A+ | C D       - only A+ branch absorbable (C D branch not absorbable)
+ *   ((A+ B) | C) D - nested ALT: A+ branch is absorbable
+ */
+static void
+computeAbsorbability(RPRPattern *pattern)
+{
+	bool		hasAbsorbable = false;
+
+	/* Parser always produces at least one element + FIN */
+	Assert(pattern->numElements >= 2);
+
+	/* Start recursion from first element */
+	computeAbsorbabilityRecursive(pattern, 0, &hasAbsorbable);
+	pattern->isAbsorbable = hasAbsorbable;
+}
+
+/*
+ * buildRPRPattern
+ *		Compile pattern parse tree to flat bytecode array.
+ *
+ * Compilation phases:
+ *   1. Optimize parse tree (flatten, merge, deduplicate)
+ *   2. Scan: collect variables, count elements (pass 1)
+ *   3. Allocate result structure
+ *   4. Fill elements from parse tree (pass 2)
+ *   5. Finalize pattern structure
+ *   6. Compute context absorption eligibility
+ *
+ * Called from createplan.c during plan creation.
+ */
+RPRPattern *
+buildRPRPattern(RPRPatternNode *pattern, List *defineClause,
+				RPSkipTo rpSkipTo, int frameOptions,
+				bool hasMatchStartDependent)
+{
+	RPRPattern *result;
+	RPRPatternNode *optimized;
+	char	   *varNamesStack[RPR_VARID_MAX + 1];
+	int			numVars;
+	int			numElements;
+	RPRDepth	maxDepth;
+	int			idx;
+
+	/* Caller must check for NULL pattern before calling */
+	Assert(pattern != NULL);
+	/* RPR is ROWS-only: transformRPR() rejects RANGE/GROUPS up front */
+	Assert(frameOptions & FRAMEOPTION_ROWS);
+
+	/* Optimize the pattern tree */
+	optimized = optimizeRPRPattern(copyObject(pattern));
+
+	numVars = 0;
+
+	/*
+	 * Populate varNamesStack with the DEFINE variable names in DEFINE order.
+	 * This ensures varId == defineClause index, eliminating runtime mapping.
+	 */
+	foreach_node(TargetEntry, te, defineClause)
+	{
+		/* Parser always assigns a name to each DEFINE entry */
+		Assert(te->resname != NULL);
+
+		varNamesStack[numVars++] = pstrdup(te->resname);
+	}
+
+	/* Scan pattern: collect variables, count elements, validate limits */
+	scanRPRPattern(optimized, varNamesStack, &numVars, &numElements, &maxDepth);
+
+	/*
+	 * numVars may reach RPR_VARID_MAX + 1 (valid varIds are 0 ..
+	 * RPR_VARID_MAX)
+	 */
+	Assert(numVars <= RPR_VARID_MAX + 1);
+
+	/* Allocate result structure */
+	result = makeRPRPattern(numVars, numElements, maxDepth, varNamesStack);
+
+	/* Fill elements (pass 2) */
+	idx = 0;
+	fillRPRPattern(optimized, result, &idx, 0);
+
+	/* Finalize: set up next pointers, flags, and add FIN marker */
+	finalizeRPRPattern(result);
+
+	/*
+	 * Compute context absorption eligibility. Absorption requires both
+	 * structural absorbability and runtime conditions. Check runtime
+	 * conditions first to avoid unnecessary pattern analysis.
+	 *
+	 * Runtime conditions for absorption:
+	 *
+	 * 1. SKIP TO PAST LAST ROW required (not SKIP TO NEXT ROW): with NEXT
+	 * ROW, matches overlap and every row must report its own match, so
+	 * absorption (sharing one result) is not semantically possible.  A
+	 * completed context does linger until its own start row is queried; that
+	 * is the inherent cost of per-row match reporting, not redundancy
+	 * absorption could remove.
+	 *
+	 * 2. Unbounded frame end required (not ROWS with bounded end): With a
+	 * bounded frame (e.g., ROWS BETWEEN CURRENT ROW AND 10 FOLLOWING),
+	 * matches may be truncated at frame boundaries. This changes the
+	 * absorption semantics - older contexts don't necessarily produce longer
+	 * matches when frame limits apply differently to each context.
+	 *
+	 * 3. No DEFINE may depend on match_start: such a variable is evaluated
+	 * against the start of its own match, so two contexts that differ only in
+	 * where they started can classify the same row differently and the older
+	 * one no longer covers the newer.
+	 */
+	if (rpSkipTo == ST_PAST_LAST_ROW &&
+		(frameOptions & FRAMEOPTION_END_UNBOUNDED_FOLLOWING) &&
+		!hasMatchStartDependent)
+	{
+		/* Runtime conditions met - check structural absorbability */
+		computeAbsorbability(result);
+	}
+
+	return result;
+}
diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c
index 8a641402a96..9dc191ad68e 100644
--- a/src/backend/optimizer/plan/setrefs.c
+++ b/src/backend/optimizer/plan/setrefs.c
@@ -2603,6 +2603,34 @@ set_upper_references(PlannerInfo *root, Plan *plan, int rtoffset)
 					   rtoffset,
 					   NUM_EXEC_QUAL(plan));
 
+	/*
+	 * Replace an expression tree in each DEFINE clause so that all Var
+	 * nodes's varno refers to OUTER_VAR.
+	 */
+	if (IsA(plan, WindowAgg))
+	{
+		List	   *new_defineClause = NIL;
+		WindowAgg  *wplan = (WindowAgg *) plan;
+
+		foreach_node(TargetEntry, tle, wplan->defineClause)
+		{
+			TargetEntry *newtle;
+
+			newtle = flatCopyTargetEntry(tle);
+			newtle->expr = (Expr *)
+				fix_upper_expr(root,
+							   (Node *) tle->expr,
+							   subplan_itlist,
+							   OUTER_VAR,
+							   rtoffset,
+							   NUM_EXEC_QUAL(plan));
+
+			new_defineClause = lappend(new_defineClause, newtle);
+		}
+
+		wplan->defineClause = new_defineClause;
+	}
+
 	pfree(subplan_itlist);
 }
 
diff --git a/src/backend/optimizer/plan/subselect.c b/src/backend/optimizer/plan/subselect.c
index 2cf5c15a309..c5ad79f52d5 100644
--- a/src/backend/optimizer/plan/subselect.c
+++ b/src/backend/optimizer/plan/subselect.c
@@ -3054,6 +3054,8 @@ finalize_plan(PlannerInfo *root, Plan *plan,
 							  &context);
 			finalize_primnode(((WindowAgg *) plan)->endOffset,
 							  &context);
+			finalize_primnode((Node *) ((WindowAgg *) plan)->defineClause,
+							  &context);
 			break;
 
 		case T_Gather:
diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c
index 5931501e470..8cc5435a3a1 100644
--- a/src/backend/optimizer/prep/prepjointree.c
+++ b/src/backend/optimizer/prep/prepjointree.c
@@ -2596,6 +2596,13 @@ perform_pullup_replace_vars(PlannerInfo *root,
 	parse->returningList = (List *)
 		pullup_replace_vars((Node *) parse->returningList, rvcontext);
 
+	foreach_node(WindowClause, wc, parse->windowClause)
+	{
+		if (wc->defineClause != NIL)
+			wc->defineClause = (List *)
+				pullup_replace_vars((Node *) wc->defineClause, rvcontext);
+	}
+
 	if (parse->onConflict)
 	{
 		parse->onConflict->onConflictSet = (List *)
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 2fe6b61afaf..2d40d82c524 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -18,6 +18,7 @@
 #include "access/stratnum.h"
 #include "nodes/bitmapset.h"
 #include "nodes/lockoptions.h"
+#include "nodes/parsenodes.h"
 #include "nodes/primnodes.h"
 
 
@@ -1248,6 +1249,91 @@ typedef struct Agg
 	List	   *chain;
 } Agg;
 
+/* ----------------
+ *		Row Pattern Recognition compiled pattern types
+ * ----------------
+ */
+
+/* Type definitions for RPR pattern elements */
+typedef uint8 RPRVarId;			/* pattern variable ID */
+typedef uint8 RPRElemFlags;		/* element flags */
+typedef uint8 RPRDepth;			/* group nesting depth */
+typedef int32 RPRQuantity;		/* quantifier min/max */
+typedef int16 RPRElemIdx;		/* element array index */
+
+/*
+ * RPRPatternElement - flat element for NFA pattern matching (16 bytes)
+ *
+ * Layout optimized for alignment (no padding holes):
+ *   varId(1) + depth(1) + flags(1) + reserved(1) + min(4) + max(4) + next(2) + jump(2)
+ */
+typedef struct RPRPatternElement
+{
+	RPRVarId	varId;			/* variable ID, or special value for control */
+	RPRDepth	depth;			/* group nesting depth */
+	RPRElemFlags flags;			/* flags (reluctant, etc.) */
+	uint8		reserved;		/* reserved padding byte */
+	RPRQuantity min;			/* quantifier minimum */
+	RPRQuantity max;			/* quantifier maximum */
+	RPRElemIdx	next;			/* next element index */
+	RPRElemIdx	jump;			/* ALT/SEP branch link, or GROUP
+								 * skip/loop-back */
+} RPRPatternElement;
+
+/*
+ * RPRPattern - compiled pattern for NFA execution
+ *
+ * Requires custom copy/out/read functions due to elements array.
+ */
+typedef struct RPRPattern
+{
+	/*
+	 * RPRPattern is a plan/exec-only node with arrays that need a
+	 * hand-written copy (custom_copy_equal).  It is never compared with
+	 * equal(): equal() routines are generated only for parse/rewrite-level
+	 * nodes, not for plan nodes, so there is nothing to compare it against
+	 * and equal support is suppressed with no_equal.  It is not reachable
+	 * from a Query either, so query jumbling has nothing to do here and is
+	 * suppressed with no_query_jumble.
+	 */
+	pg_node_attr(custom_copy_equal, custom_read_write, no_equal, no_query_jumble)
+
+	NodeTag		type;			/* T_RPRPattern */
+	int			numVars;		/* number of pattern variables */
+	char	  **varNames;		/* array of variable names (DEFINE order
+								 * first) */
+	RPRDepth	maxDepth;		/* deepest group nesting depth plus one, i.e.
+								 * the length of a state's counts[] */
+	int			numElements;	/* number of elements */
+	RPRPatternElement *elements;	/* array of pattern elements */
+
+	/*----------------
+	 * Context absorption optimization.
+	 *
+	 * Absorption is only safe when later matches are guaranteed to be
+	 * suffixes of earlier matches, which requires the pattern to start with
+	 * an unbounded greedy element.  Phase-1 normalization (consecutive
+	 * variable / group / ALT merging and prefix/suffix merging) rewrites the
+	 * pattern toward that form first -- so e.g. A B (A B)+ is merged to
+	 * (A B){2,} and then judged absorbable.
+	 *
+	 * computeAbsorbability() marks the absorbable cases (see isUnboundedStart):
+	 *   - simple unbounded VAR at the start:                    A+ B C
+	 *   - unbounded GROUP with fixed-length children:           (A B)+, (A B{2})+
+	 *   - greedy GROUP whose body starts with one of those:     (A+ B)+
+	 *   - ALT with independently absorbable branches:           A+ | B+
+	 *     (handled in computeAbsorbabilityRecursive, at any nesting: the
+	 *     branches of (A+ | B)+ are judged the same way)
+	 *
+	 * Not absorbable: an unbounded element not at the start (A B+), a
+	 * reluctant quantifier (A+?), or an alternation no branch of which starts
+	 * with an unbounded greedy element ((A|B)+) -- there different start
+	 * positions yield different match contents, so later matches are not
+	 * suffixes of earlier ones.
+	 */
+	bool		isAbsorbable;	/* true if pattern supports context absorption */
+} RPRPattern;
+
 /* ----------------
  *		window aggregate node
  * ----------------
@@ -1318,6 +1404,24 @@ typedef struct WindowAgg
 	/* nulls sort first for in_range tests? */
 	bool		inRangeNullsFirst;
 
+	/* Row Pattern Recognition AFTER MATCH SKIP clause */
+	RPSkipTo	rpSkipTo;		/* Row Pattern Skip To type */
+
+	/* Compiled Row Pattern for NFA execution */
+	struct RPRPattern *rpPattern;
+
+	/* Row Pattern DEFINE clause (list of TargetEntry) */
+	List	   *defineClause;
+
+	/*
+	 * Bitmapset of DEFINE variable indices whose expressions depend on
+	 * match_start: they contain FIRST or a compound PREV_FIRST/NEXT_FIRST, or
+	 * a LAST that carries an offset of its own, whether plain or inside a
+	 * compound PREV_LAST/NEXT_LAST. Variables in this set require per-context
+	 * re-evaluation during NFA processing.
+	 */
+	Bitmapset  *defineMatchStartDependent;
+
 	/*
 	 * false for all apart from the WindowAgg that's closest to the root of
 	 * the plan
diff --git a/src/include/optimizer/rpr.h b/src/include/optimizer/rpr.h
new file mode 100644
index 00000000000..682ed75b48a
--- /dev/null
+++ b/src/include/optimizer/rpr.h
@@ -0,0 +1,87 @@
+/*-------------------------------------------------------------------------
+ *
+ * rpr.h
+ *	  Row Pattern Recognition pattern compilation for planner
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/optimizer/rpr.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef RPR_H
+#define RPR_H
+
+#include "nodes/parsenodes.h"
+#include "nodes/plannodes.h"
+#include "nodes/primnodes.h"
+
+/* Limits and special values */
+/*
+ * Maximum pattern variable ID.  Pattern variables occupy varId 0 to
+ * RPR_VARID_MAX inclusive (240 distinct variables); any varId with the high
+ * nibble set (0xF0 to 0xFF) is reserved for control elements.  Reserving the
+ * whole high nibble, rather than just the values currently in use, leaves
+ * room for future control elements; this range can only be narrowed safely
+ * before release.
+ */
+#define RPR_VARID_MAX		0xEF	/* pattern variables are 0 to 0xEF */
+
+/*
+ * RPR_COUNT_INF is the value a runtime repetition count saturates at to avoid
+ * int32 overflow (see the count++ guard in nfa_match).  It is defined as
+ * RPR_QUANTITY_INF (from nodes/parsenodes.h, included above) so that a
+ * saturated count compares as "unbounded", just like an unbounded
+ * quantifier's max.
+ */
+#define RPR_COUNT_INF		RPR_QUANTITY_INF
+#define RPR_ELEMIDX_MAX		PG_INT16_MAX	/* max pattern elements */
+#define RPR_ELEMIDX_INVALID	((RPRElemIdx) -1)	/* invalid index */
+#define RPR_DEPTH_MAX		PG_UINT8_MAX	/* 255 levels fit RPRDepth; depth
+											 * is 0-based, so the deepest
+											 * permitted nesting is 254 */
+
+/* Reserved control-element varIds (high nibble 0xF; 0xF0-0xFA spare) */
+#define RPR_VARID_BEGIN		((RPRVarId) 0xFB)	/* group begin */
+#define RPR_VARID_END		((RPRVarId) 0xFC)	/* group end */
+#define RPR_VARID_ALT		((RPRVarId) 0xFD)	/* alternation start */
+#define RPR_VARID_SEP		((RPRVarId) 0xFE)	/* alternation branch
+												 * separator */
+#define RPR_VARID_FIN		((RPRVarId) 0xFF)	/* pattern finish */
+
+/* Element flags */
+#define RPR_ELEM_RELUCTANT			0x01	/* reluctant (non-greedy)
+											 * quantifier */
+#define RPR_ELEM_EMPTY_LOOP			0x02	/* END: group body can produce
+											 * empty match */
+#define RPR_ELEM_EMPTY_PREFERRED	0x04	/* END: group body prefers the
+											 * empty match */
+/*
+ * The two absorption flags below are explained in README.rpr IV-5
+ * ("Absorbability Analysis"), with worked examples in Appendix B; the
+ * analysis that sets them is computeAbsorbability() in
+ * optimizer/plan/rpr.c.
+ */
+#define RPR_ELEM_ABSORBABLE_BRANCH	0x08	/* element in absorbable region */
+#define RPR_ELEM_ABSORBABLE			0x10	/* absorption comparison point */
+
+/* Accessor macros for RPRPatternElement */
+#define RPRElemIsReluctant(e)			(((e)->flags & RPR_ELEM_RELUCTANT) != 0)
+#define RPRElemCanEmptyLoop(e)			(((e)->flags & RPR_ELEM_EMPTY_LOOP) != 0)
+#define RPRElemIsEmptyPreferred(e)		(((e)->flags & RPR_ELEM_EMPTY_PREFERRED) != 0)
+#define RPRElemIsAbsorbableBranch(e)	(((e)->flags & RPR_ELEM_ABSORBABLE_BRANCH) != 0)
+#define RPRElemIsAbsorbable(e)			(((e)->flags & RPR_ELEM_ABSORBABLE) != 0)
+#define RPRElemIsVar(e)			((e)->varId <= RPR_VARID_MAX)
+#define RPRElemIsBegin(e)		((e)->varId == RPR_VARID_BEGIN)
+#define RPRElemIsEnd(e)			((e)->varId == RPR_VARID_END)
+#define RPRElemIsAlt(e)			((e)->varId == RPR_VARID_ALT)
+#define RPRElemIsSep(e)			((e)->varId == RPR_VARID_SEP)
+#define RPRElemIsFin(e)			((e)->varId == RPR_VARID_FIN)
+#define RPRElemCanSkip(e)		((e)->min == 0)
+
+extern RPRPattern *buildRPRPattern(RPRPatternNode *pattern, List *defineClause,
+								   RPSkipTo rpSkipTo, int frameOptions,
+								   bool hasMatchStartDependent);
+
+#endif							/* RPR_H */
-- 
2.43.0

