public inbox for [email protected]  
help / color / mirror / Atom feed
From: Henson Choi <[email protected]>
To: Tatsuo Ishii <[email protected]>
Cc: [email protected]
Cc: [email protected]
Cc: [email protected]
Cc: [email protected]
Cc: [email protected]
Cc: [email protected]
Cc: [email protected]
Cc: [email protected]
Cc: [email protected]
Subject: Re: Row pattern recognition
Date: Tue, 9 Jun 2026 13:31:57 +0900
Message-ID: <CAAAe_zCd1pA6vCaMD7e3hB3Ou+=mZziB1=CO_tBybuAiE5K=vQ@mail.gmail.com> (raw)
In-Reply-To: <CAAAe_zBdwAUDNs_WFdLkFF=ewhkDkv-AqizVEVzhsfremGFb4w@mail.gmail.com>
References: <CAAAe_zBHrBBM2KYKJYSvP=vr=6fv7kFDr8qZZWFd7==sb4VMxg@mail.gmail.com>
	<[email protected]>
	<CAAAe_zA3vXEPkC7=fapx0VCE5F2uSgRjKjur67Yfd+JxtWPCuQ@mail.gmail.com>
	<[email protected]>
	<CAAAe_zBdwAUDNs_WFdLkFF=ewhkDkv-AqizVEVzhsfremGFb4w@mail.gmail.com>

Hi hackers,

This is the v48 fold previewed in my 2026-05-30 post: the incremental
series carried on top of v47 has been folded back, Jian He's round-3
review [1] is applied (my point-by-point reply [2]), and an in-house
static-analysis pass plus the cosmetic clean-up are now included.

Resolved since the last post:

  D3. AST "absorption" rename -- Tatsuo's call.  Renamed to "merging"
      (nocfbot-0031).

  0025 ereport style (Tatsuo, 2026-06-02) [3]: the single-row frame
  rejection used the old (errcode(...)) wrapping; updated in place to the
  new errcode/errmsg style.  (The remaining RPR ereports are tidied up
  tree-wide in nocfbot-0045.)

Deferred (unchanged):

  D2. RPRContext consolidation (Jian's round-2 #1) -- folded into the
      future R010 infrastructure, where a single context can back both the
      R020 window implementation and R010.  We agreed to defer it at the v47
      stage (Tatsuo, 2026-05-31); it is not part of v48.

Attached: the v47 feature series (v47-0001..0009) rebased onto current
master, plus the full incremental series (nocfbot-0001..0068) carried
on top of it.

Base:

  89eafad297a  2026-06-06  Fix tuple deforming with virtual generated
columns

Already posted (nocfbot-0001..0026): 0001..0016 went out in earlier
rounds -- 0001..0011 [4], 0012..0015 [5], and 0016 (sent at the time as
0015) [6]; the 2026-05-30 post [7] renumbered them to run contiguously and
added 0017..0026.  All are rebase only and unchanged, except nocfbot-0025,
whose single-row-frame ereports were updated to the new errcode/errmsg
style (per Tatsuo; see above).  Titles for reference:

  nocfbot-0001  Add DEFINE non-volatile baseline to rpr_integration B9
  nocfbot-0002  Unify RPR DEFINE walkers and reject volatile callees
  nocfbot-0003  Cover RPR empty-match path with EXPLAIN tests; fix stale
                XXX comments
  nocfbot-0004  Reclassify DEFINE qualifier check and reword diagnostic to
                "expression"
  nocfbot-0005  Sync stale comments on DEFINE/PATTERN handling
  nocfbot-0006  Add trailing commas to RPR enum definitions
  nocfbot-0007  Remove optional outer parentheses from ereport() calls in
                RPR files
  nocfbot-0008  Add high-water mark tracking to NFA visited bitmap reset
  nocfbot-0009  Document DEFINE subquery rejection as intentional
                over-rejection
  nocfbot-0010  Remove duplicate #include in nodeWindowAgg.c
  nocfbot-0011  Normalize SQL/RPR standard references
  nocfbot-0012  Add rpr_integration B7 cases for RPR in recursive query
  nocfbot-0013  Reject row pattern recognition in recursive queries
  nocfbot-0014  Enhance README.rpr per Tatsuo Ishii's review
  nocfbot-0015  Round out README.rpr WindowAggState field coverage
  nocfbot-0016  Add raw_expression_tree_walker coverage for RPR raw nodes
  nocfbot-0017  Enhance README.rpr per Jian He's review
  nocfbot-0018  Clarify execRPR.c comments and tighten an Assert
  nocfbot-0019  Change nfa_add_state_unique signature from bool to void
  nocfbot-0020  Add reluctant bounded mid-band test to rpr_nfa
  nocfbot-0021  Define RPR absorption terminology in README.rpr
  nocfbot-0022  Document the get_reduced_frame_status cascade invariant
  nocfbot-0023  Explain the completed-head-context branch in
                update_reduced_frame
  nocfbot-0024  Tighten the RPR frame-boundary check from >= to ==
  nocfbot-0025  Reject single-row window frame in row pattern recognition
  nocfbot-0026  Remove the redundant zero check on the RPR frame ending
                offset

New since the 2026-05-30 post [7] -- nocfbot-0027..0068, in series order.
These apply Jian He's round-3 review [1], an in-house static-analysis
pass, and the v48 cosmetic fold (smallest-first, A..F at the tail).
Patches that change user-visible behavior are tagged [behavior change],
except nocfbot-0042, whose narrow variable-limit change (251 -> 240) is
noted inline.

  nocfbot-0027  Restore findTargetlistEntrySQL99 to static (per Tatsuo)
  nocfbot-0028  Clarify RPR comments
  nocfbot-0029  Rename RPR NFA constructors to make/clone
  nocfbot-0030  Demote RPR nfaVisitedNWords to a local
  nocfbot-0031  Rename the AST-level prefix/suffix rewrite from
                "absorption" to "merging"  (D3)
  nocfbot-0032  Drop non-standard per-group banner labels from RPR
                forward declarations
  nocfbot-0033  Clarify that ExecRPRCleanupDeadContexts always frees the
                failed context
  nocfbot-0034  Correct stale RPR comments and document a defensive
                window check

  nocfbot-0035  Fix unsafe (A{n,})* quantifier flattening
                [behavior change]
      (A{2,})* reaches {0} UNION [2,INF); flattening to A* wrongly admits
      a single A.  Skip multiplication when the outer is skippable and the
      child min >= 2.

  nocfbot-0036  Avoid INF-valued quantifier bound in consecutive-merge
                [behavior change]
      The merge overflow guard used <=, letting a merged min reach the
      RPR_QUANTITY_INF sentinel; tightened to <.

  nocfbot-0037  Fix count slot leak in row pattern recognition absorption
                [behavior change]
      An absorbable leaf VAR exiting inline at max count left its shared
      depth count slot dirty for the sibling group; cleared on exit, with
      the count-clear policy made explicit via Asserts.

  nocfbot-0038  Demote dead runtime checks in the RPR executor to
                assertions

  nocfbot-0039  Fix memory leak in row pattern recognition DEFINE
                evaluation  [behavior change]
      Per-tuple DEFINE evaluation leaked into the wrong context.

  nocfbot-0040  Honor reluctant quantifier for non-leading optional RPR
                variables  [behavior change]
      Reluctant handling existed for leading VARs and groups but not for
      a non-leading optional VAR routed through nfa_route_to_elem; e.g.
      (B A?? C) matched A greedily.  Same skip-first pattern applied.

  nocfbot-0041  Reject column-less compound navigation  [behavior change]
      A compound nav with no column reference is now rejected at parse
      time, instead of being silently accepted with a wrong result.

  nocfbot-0042  Reserve the high varId nibble for RPR control elements
                (the maximum pattern-variable count drops from 251 to 240)

  nocfbot-0043  Generalize quantifier multiplication  [behavior change]
      Replaces the hand-rolled special cases for (child{p,q}){m,n} with a
      general interval-contiguity test, folding cases such as
      (A{2,3}){2,3} -> A{4,9} and (A+){3} -> A{3,} while leaving genuine
      gaps unflattened.

  nocfbot-0044  Tidy up row pattern recognition pattern compilation
  nocfbot-0045  Drop redundant parentheses from row pattern recognition
                ereports
  nocfbot-0046  Tidy up forward declarations and helper placement
  nocfbot-0047  Update the varId documentation
  nocfbot-0048  Reformat the design-decisions chapter in README.rpr
  nocfbot-0049  Point to the absorption-analysis docs from the RPR flag
                definitions

  nocfbot-0050  Fix signed-integer overflow in frame-end clamp
                [behavior change]
      The frame-end clamp computed frameOffset + 1 (int64) before the
      overflow check, so a FOLLOWING offset near PG_INT64_MAX overflowed;
      now added in two checked steps that clamp to PG_INT64_MAX.

  nocfbot-0051  Rework row pattern EXPLAIN deparser to fix grouped
                alternation branches  [EXPLAIN output]
      The bytecode deparser collapsed alternations whose branches were
      quantified groups (e.g. (C | (A B)+ | D)).  Rewritten as recursive
      descent over depth/jump-anchored windows; display-only, execution
      was already correct.

  nocfbot-0052  Handle row pattern navigation nodes in exprTypmod and
                isSimpleNode
  nocfbot-0053  Restore the error cursor for too many row pattern
                variables
  nocfbot-0054  Test deparse of an inline row pattern window
  nocfbot-0055  Fix a mislabeled INITIAL test

  nocfbot-0056  Reject invalid column references in row pattern DEFINE
                clauses  [behavior change]
      Outer-query correlated references and 3-part qualified names in
      DEFINE were reaching an internal "Upper-level Var found where not
      expected" (XX000); both now raise a clean, classified error at
      transformColumnRef.

  nocfbot-0057  Fix shortest match for reluctant nullable quantifiers
                [behavior change]
      An outer reluctant quantifier over a nullable reluctant body, e.g.
      (A??)+?, consumed rows instead of taking the empty match.  The
      count<min branch in nfa_advance_end now mirrors its sibling and
      routes the fast-forward exit first for reluctant elements.

  nocfbot-0058  Compare varno when preserving DEFINE-referenced columns
                [behavior change]
      The allpaths DEFINE-column retention loop compared varattno only;
      it now matches varno/varlevelsup too, dropping over-retention.

  nocfbot-0059  Allow a row pattern quantifier with no space before the
                alternation operator  [behavior change]
      The lexer glues "*|" etc. into one token, so (A*|B) failed.  The
      grammar now recognizes the glued forms via a transient trailing_alt
      flag and splitRPRTrailingAlt() re-splits so "A*|B" == "A* | B".

  nocfbot-0060  Fix outdated function and file references in RPR docs
  nocfbot-0061  Assert that row pattern nesting depth never aliases the
                RPR_DEPTH_NONE sentinel

  nocfbot-0062  Invalidate the row pattern nav slot cache when a window
                partition changes  [behavior change]
      release_partition() now resets nav_slot_pos so a stale cached slot
      position cannot survive a partition boundary.

  nocfbot-0063  Tidy up formatting in row pattern recognition code   (A)
  nocfbot-0064  Modernize idioms in row pattern recognition code     (B)
  nocfbot-0065  Use width-explicit integer limit macros              (C)
                (INT_MAX -> PG_INT32_MAX, the unbounded sentinel)
  nocfbot-0066  Tidy up row pattern recognition regression test comments (D)
  nocfbot-0067  Add row pattern recognition negative and coverage tests (E)
  nocfbot-0068  Use foreach_node and friends in RPR code             (F)
                (foreach + lfirst() -> foreach_node;
                 foreach_current_index, dropping redundant breaks)

Issue-to-patch map (each raised issue -> decision -> landing), by source.
(Earlier incremental cleanup 0001..0016: see [4][5][6].)

Jian He, round 1 (2026-05-26):
  Short-circuit optimization        invited to drive   -> separate series
  Absorption README narrative       accept             -> nocfbot-0017
  DFS expansion                     accept             -> nocfbot-0017
  initialAdvance README mismatch    accept             -> nocfbot-0017
  "absorption" -> "merging" rename  accept (Tatsuo)    -> nocfbot-0031
  Defensive Assert in advance_var   accept             -> nocfbot-0018
  Finalize unnecessary?             keep               -> nocfbot-0018
  Greedy comment label              accept             -> nocfbot-0018
  state->next reset                 decline            -> nocfbot-0018
  visited marking purpose           accept             -> nocfbot-0018
  compareDepth comment              accept             -> nocfbot-0018
  ALT depth invariant Assert        decline            -> nocfbot-0018
  Unused bool return                accept             -> nocfbot-0019
  count >= 3 test coverage          accept             -> nocfbot-0020

Jian He, round 2 (2026-05-28):
  RPRContext consolidation          defer (Tatsuo)     -> D2
  README terminology + typo         accept             -> nocfbot-0021
  get_reduced_frame_status order    keep               -> nocfbot-0022
  states == NULL branch coverage    already reached    -> nocfbot-0023
  currentPos >= -> ==               accept             -> nocfbot-0024
  single-row frame (6.10.2, D1)     reject (Tatsuo)    -> nocfbot-0025
  ExecRPRProcessRow refactor        Datum fix only     -> nocfbot-0026

Jian He, round 3 (2026-05-30) [1] (reply [2]):
  "Tests line N" comments           rephrase           -> nocfbot-0028
  "at the END" wording              accept             -> nocfbot-0028
  uppercase END cleanup             accept             -> nocfbot-0028
  nfa_advance_var count comment     reword             -> nocfbot-0028
  make / clone rename               accept             -> nocfbot-0029
  nfaVisitedNWords field            accept (-> local)  -> nocfbot-0030
  nfaStateSize field                keep               -> (no change)
  drop the elem parameter           keep               -> (no change)

Jian He, off-list (2026-05-15..06-04):
  Per-group banner labels           drop               -> nocfbot-0032
  Dead-context cleanup comment      clarify            -> nocfbot-0033
  rpr.c pg_unreachable refactor     accept             -> nocfbot-0044
  ABSORBABLE doc pointer            accept             -> nocfbot-0049
  has_column_ref necessity          defend             -> (no change)
  CFI dedup                         decline            -> (no change)
  fractional FOLLOWING tests        decline            -> (no change)
  initial/SEEK rename               defer              -> separate series

Tatsuo Ishii + Jian round 4 (2026-05-31..06-02):
  Tatsuo README review              accept             -> nocfbot-0014
  findTargetlistEntrySQL99 static   accept             -> nocfbot-0027
  varId high nibble ("3")           accept             -> nocfbot-0042
  ereport redundant parens          accept             -> nocfbot-0045
  forward-decl / helper placement   accept             -> nocfbot-0046
  varId documentation sync          accept             -> nocfbot-0047
  XII chapter prose reformat        reformat           -> nocfbot-0048
  README 0005 -> 0006 reorder       at submit (Ishii)  -> committer
  double-free question              cannot occur       -> (no change)
  redundant continue                keep               -> (no change)
  non-ASCII section sign            already ASCII      -> (no change)
  empty pattern PATTERN ()          R020 reject right  -> (no change)

in-house static analysis:
  stale comments + window check     fix                -> nocfbot-0034
  unsafe (A{n,})* flattening        fix                -> nocfbot-0035
  INF-valued merge bound            fix                -> nocfbot-0036
  count slot leak                   fix                -> nocfbot-0037
  dead runtime checks -> asserts    fix                -> nocfbot-0038
  DEFINE per-tuple memory leak      fix                -> nocfbot-0039
  reluctant non-leading optional    fix                -> nocfbot-0040
  column-less compound navigation   fix                -> nocfbot-0041
  incomplete quantifier fold (EP-1) generalize         -> nocfbot-0043
  frame-end signed overflow         fix                -> nocfbot-0050
  EXPLAIN deparser grouped-alt      fix                -> nocfbot-0051
  exprTypmod / isSimpleNode nav     fix                -> nocfbot-0052
  errpos for too-many-vars          fix                -> nocfbot-0053
  inline-OVER deparse coverage      fix                -> nocfbot-0054
  INITIAL test mislabel             fix                -> nocfbot-0055
  DEFINE outer / 3-part column ref  fix                -> nocfbot-0056
  reluctant-nullable shortest match fix                -> nocfbot-0057
  allpaths varno comparison         fix                -> nocfbot-0058
  glued quantifier + alternation    fix                -> nocfbot-0059
  nfa_advance / README references   doc                -> nocfbot-0060
  invariant asserts / notes         assert             -> nocfbot-0061
  nav_slot / LAST(x,0) edge cases   defense            -> nocfbot-0062

v48 final fold (off-list sweep + Jian round 5 v47 patch):
  pure formatting                   fold               -> nocfbot-0063
  idioms (palloc_array, Max, ...)   fold               -> nocfbot-0064
  INT_MAX -> PG_INT32_MAX sentinel  fold               -> nocfbot-0065
  regress comment + doc hygiene     fold               -> nocfbot-0066
  new negative / coverage tests     fold               -> nocfbot-0067
  foreach_node + lfirst sweep       fold               -> nocfbot-0068

Please let me know if any of the slicing or the grouping looks off, or
if you would prefer the correctness fixes split out ahead of the
cosmetic fold.

Thanks again to Jian for the careful reading.

Best regards,
Henson

[1] Jian's review, round 3 (2026-05-30):

https://postgr.es/m/CACJufxEsaU8GQ4yeXTWhAO8VjbrZTh5CpvUqz=4a3T0Cwz44pA@mail.gmail.com

[2] My point-by-point reply to the round-3 review (2026-05-30):

https://postgr.es/m/CAAAe_zBi1dOtWb2vnwSvGwuU0-bqAOm_7dOM4u-CmukA8xaV5Q@mail.gmail.com

[3] Tatsuo's note on the 0025 ereport wrapping (2026-06-02):

https://postgr.es/m/[email protected]

[4] Earlier incremental post -- nocfbot-0001..0011 (2026-05-09):

https://postgr.es/m/CAAAe_zCL9UtiYthrSaXCmhFMK6Q3YQ6BQGgae7C9en2k=S9doA@mail.gmail.com

[5] Earlier incremental post -- nocfbot-0012..0015 (2026-05-12):

https://postgr.es/m/CAAAe_zBg7y5frYxReua1dczXkMK-7fk6bkVo5ZQXDYxpxe0cwA@mail.gmail.com

[6] nocfbot-0016, sent at the time as 0015 (2026-05-26):

https://postgr.es/m/CAAAe_zD7vCLCb+vxpO3P-NsDUZ=JcN8EGCV0dz0BNgBKsbOGcQ@mail.gmail.com

[7] The 2026-05-30 post -- renumbered 0001..0016 and added 0017..0026:

https://postgr.es/m/CAAAe_zAuHwqUfqJOD4PDUkWsxTfTytNaandq11Kddw2bfCcpvQ@mail.gmail.com

From c215fec2b4a9575973314b8b046552cb5870645e Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Tue, 5 May 2026 11:36:53 +0900
Subject: [PATCH 04/68] Reclassify DEFINE qualifier check and reword diagnostic
 to "expression"

Split the pre-check into three branches: pattern variable ->
FEATURE_NOT_SUPPORTED, range variable (via refnameNamespaceItem) ->
SYNTAX_ERROR, anything else -> fall through to normal resolution.
Reword "qualified column reference" to "qualified expression" since
the quoted token may include an indirection target on composite
types (e.g. (A.items).amount).
---
 src/backend/parser/parse_expr.c        | 31 +++++++---
 src/test/regress/expected/rpr_base.out | 80 +++++++++++++++++++++++---
 src/test/regress/sql/rpr_base.sql      | 61 ++++++++++++++++++--
 3 files changed, 153 insertions(+), 19 deletions(-)

diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 1e9cee36e35..4221643d9c8 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -628,11 +628,24 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 	if (node != NULL)
 		return node;
 
-	/*
-	 * Qualified column references in DEFINE are not supported.  This covers
-	 * both FROM-clause range variables (prohibited by §6.5) and pattern
-	 * variable qualified names (e.g. UP.price), which are valid per §4.16
-	 * but not yet implemented.
+	/*----------
+	 * Qualified references in DEFINE need a tri-classification:
+	 *
+	 *	  pattern variable qualifier (e.g. UP.price): valid per 19075-5 4.16
+	 *	  but not yet implemented -- raise FEATURE_NOT_SUPPORTED.
+	 *
+	 *	  FROM-clause range variable qualifier: prohibited by 19075-5 6.5
+	 *	  -- raise SYNTAX_ERROR.
+	 *
+	 *	  any other qualifier (typo, undefined name): fall through and let
+	 *	  normal column resolution produce a sensible error.
+	 *
+	 * The quoted text reflects only the ColumnRef portion; a trailing field
+	 * selection on a composite type (e.g. ".amount" in "(A.items).amount")
+	 * lives in the surrounding A_Indirection node and is not included here.
+	 * That can be revisited when MEASURES support adds indirection-aware
+	 * traversal.
+	 *----------
 	 */
 	if (pstate->p_expr_kind == EXPR_KIND_RPR_DEFINE &&
 		list_length(cref->fields) != 1)
@@ -653,15 +666,17 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 		if (is_pattern_var)
 			ereport(ERROR,
 					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-					 errmsg("pattern variable qualified column reference \"%s\" is not supported in DEFINE clause",
+					 errmsg("pattern variable qualified expression \"%s\" is not supported in DEFINE clause",
 							NameListToString(cref->fields)),
 					 parser_errposition(pstate, cref->location)));
-		else
+		else if (refnameNamespaceItem(pstate, NULL, qualifier,
+									  cref->location, NULL) != NULL)
 			ereport(ERROR,
 					(errcode(ERRCODE_SYNTAX_ERROR),
-					 errmsg("range variable qualified column reference \"%s\" is not allowed in DEFINE clause",
+					 errmsg("range variable qualified expression \"%s\" is not allowed in DEFINE clause",
 							NameListToString(cref->fields)),
 					 parser_errposition(pstate, cref->location)));
+		/* else: unknown qualifier -- fall through to normal resolution */
 	}
 
 	/*----------
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index a63211ff364..86abb96c177 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -3074,10 +3074,10 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS A.val > 0
 );
-ERROR:  pattern variable qualified column reference "a.val" is not supported in DEFINE clause
+ERROR:  pattern variable qualified expression "a.val" is not supported in DEFINE clause
 LINE 7:     DEFINE A AS A.val > 0
                         ^
--- Expected: ERROR: pattern variable qualified column reference "a.val" is not supported
+-- Expected: ERROR: pattern variable qualified expression "a.val" is not supported
 -- PATTERN-only variable qualified name: not supported even without DEFINE entry
 SELECT COUNT(*) OVER w
 FROM rpr_err
@@ -3087,10 +3087,10 @@ WINDOW w AS (
     PATTERN (A+ B+)
     DEFINE A AS B.val > 0
 );
-ERROR:  pattern variable qualified column reference "b.val" is not supported in DEFINE clause
+ERROR:  pattern variable qualified expression "b.val" is not supported in DEFINE clause
 LINE 7:     DEFINE A AS B.val > 0
                         ^
--- Expected: ERROR: pattern variable qualified column reference "b.val" is not supported
+-- Expected: ERROR: pattern variable qualified expression "b.val" is not supported
 -- DEFINE-only variable qualified name: still a pattern variable, not a range variable
 SELECT COUNT(*) OVER w
 FROM rpr_err
@@ -3103,7 +3103,7 @@ WINDOW w AS (
 ERROR:  DEFINE variable "b" is not used in PATTERN
 LINE 7:     DEFINE A AS val > 0, B AS B.val > 0
                                  ^
--- Expected: ERROR: pattern variable qualified column reference "b.val" is not supported
+-- Expected: ERROR: pattern variable qualified expression "b.val" is not supported
 -- FROM-clause range variable qualified name: not allowed (prohibited by SQL standard 6.5)
 SELECT COUNT(*) OVER w
 FROM rpr_err
@@ -3113,10 +3113,76 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS rpr_err.val > 0
 );
-ERROR:  range variable qualified column reference "rpr_err.val" is not allowed in DEFINE clause
+ERROR:  range variable qualified expression "rpr_err.val" is not allowed in DEFINE clause
 LINE 7:     DEFINE A AS rpr_err.val > 0
                         ^
--- Expected: ERROR: range variable qualified column reference "rpr_err.val" is not allowed
+-- Expected: ERROR: range variable qualified expression "rpr_err.val" is not allowed
+-- Unknown qualifier (neither pattern var nor range var): the DEFINE pre-check
+-- must fall through so that normal column resolution produces a sensible error.
+SELECT COUNT(*) OVER w
+FROM rpr_err
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS nosuch.val > 0
+);
+ERROR:  missing FROM-clause entry for table "nosuch"
+LINE 7:     DEFINE A AS nosuch.val > 0
+                        ^
+-- Expected: ERROR: missing FROM-clause entry for table "nosuch"
+-- Unqualified composite field access in DEFINE works: no qualifier means no
+-- pattern/range-var navigation, so the pre-check skips and normal resolution
+-- handles "(items).amount" via A_Indirection on the current row.
+CREATE TYPE rpr_item AS (name TEXT, amount INT);
+CREATE TEMP TABLE rpr_composite (id int, items rpr_item);
+INSERT INTO rpr_composite VALUES (1, ROW('a',5)), (2, ROW('b',15)), (3, ROW('c',25));
+SELECT id, (items).amount, COUNT(*) OVER w AS cnt
+FROM rpr_composite
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A+)
+    DEFINE A AS (items).amount > 10
+);
+ id | amount | cnt 
+----+--------+-----
+  1 |      5 |   0
+  2 |     15 |   2
+  3 |     25 |   1
+(3 rows)
+
+-- Expected: rows where (items).amount > 10 form matches; counts reflect frame size
+-- Composite type field selection (qualified forms): the ColumnRef portion ("A.items" or
+-- "rpr_composite.items") is what gets quoted; the trailing ".amount" lives in
+-- the surrounding A_Indirection node and is not visible to the pre-check.
+SELECT COUNT(*) OVER w
+FROM rpr_composite
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS (A.items).amount > 10
+);
+ERROR:  pattern variable qualified expression "a.items" is not supported in DEFINE clause
+LINE 7:     DEFINE A AS (A.items).amount > 10
+                         ^
+-- Expected: ERROR: pattern variable qualified expression "a.items" is not supported
+SELECT COUNT(*) OVER w
+FROM rpr_composite
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS (rpr_composite.items).amount > 10
+);
+ERROR:  range variable qualified expression "rpr_composite.items" is not allowed in DEFINE clause
+LINE 7:     DEFINE A AS (rpr_composite.items).amount > 10
+                         ^
+-- Expected: ERROR: range variable qualified expression "rpr_composite.items" is not allowed
+DROP TABLE rpr_composite;
+DROP TYPE rpr_item;
 -- Semantic errors
 -- Undefined column in DEFINE
 SELECT COUNT(*) OVER w
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index 86ed06fec68..e8c72706720 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -2092,7 +2092,7 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS A.val > 0
 );
--- Expected: ERROR: pattern variable qualified column reference "a.val" is not supported
+-- Expected: ERROR: pattern variable qualified expression "a.val" is not supported
 
 -- PATTERN-only variable qualified name: not supported even without DEFINE entry
 SELECT COUNT(*) OVER w
@@ -2103,7 +2103,7 @@ WINDOW w AS (
     PATTERN (A+ B+)
     DEFINE A AS B.val > 0
 );
--- Expected: ERROR: pattern variable qualified column reference "b.val" is not supported
+-- Expected: ERROR: pattern variable qualified expression "b.val" is not supported
 
 -- DEFINE-only variable qualified name: still a pattern variable, not a range variable
 SELECT COUNT(*) OVER w
@@ -2114,7 +2114,7 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > 0, B AS B.val > 0
 );
--- Expected: ERROR: pattern variable qualified column reference "b.val" is not supported
+-- Expected: ERROR: pattern variable qualified expression "b.val" is not supported
 
 -- FROM-clause range variable qualified name: not allowed (prohibited by SQL standard 6.5)
 SELECT COUNT(*) OVER w
@@ -2125,7 +2125,60 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS rpr_err.val > 0
 );
--- Expected: ERROR: range variable qualified column reference "rpr_err.val" is not allowed
+-- Expected: ERROR: range variable qualified expression "rpr_err.val" is not allowed
+
+-- Unknown qualifier (neither pattern var nor range var): the DEFINE pre-check
+-- must fall through so that normal column resolution produces a sensible error.
+SELECT COUNT(*) OVER w
+FROM rpr_err
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS nosuch.val > 0
+);
+-- Expected: ERROR: missing FROM-clause entry for table "nosuch"
+
+-- Unqualified composite field access in DEFINE works: no qualifier means no
+-- pattern/range-var navigation, so the pre-check skips and normal resolution
+-- handles "(items).amount" via A_Indirection on the current row.
+CREATE TYPE rpr_item AS (name TEXT, amount INT);
+CREATE TEMP TABLE rpr_composite (id int, items rpr_item);
+INSERT INTO rpr_composite VALUES (1, ROW('a',5)), (2, ROW('b',15)), (3, ROW('c',25));
+SELECT id, (items).amount, COUNT(*) OVER w AS cnt
+FROM rpr_composite
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A+)
+    DEFINE A AS (items).amount > 10
+);
+-- Expected: rows where (items).amount > 10 form matches; counts reflect frame size
+
+-- Composite type field selection (qualified forms): the ColumnRef portion ("A.items" or
+-- "rpr_composite.items") is what gets quoted; the trailing ".amount" lives in
+-- the surrounding A_Indirection node and is not visible to the pre-check.
+SELECT COUNT(*) OVER w
+FROM rpr_composite
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS (A.items).amount > 10
+);
+-- Expected: ERROR: pattern variable qualified expression "a.items" is not supported
+SELECT COUNT(*) OVER w
+FROM rpr_composite
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS (rpr_composite.items).amount > 10
+);
+-- Expected: ERROR: range variable qualified expression "rpr_composite.items" is not allowed
+DROP TABLE rpr_composite;
+DROP TYPE rpr_item;
 
 -- Semantic errors
 
-- 
2.50.1 (Apple Git-155)


From 251fbbdeb1fa438337cb4399205246c93dc7aa45 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Mon, 4 May 2026 21:37:59 +0900
Subject: [PATCH 02/68] Unify RPR DEFINE walkers and reject volatile callees

Planner/executor: shared nav_traversal_walker + visit_nav_plan /
visit_nav_exec replace four pre-existing walkers; each DEFINE
variable is walked once per phase.

Parser: single define_walker with phase tag (BODY / NAV_ARG /
NAV_OFFSET) replaces two pre-existing walkers and enforces all rules
in one traversal.  Volatile and NextValueExpr are rejected (RPR's NFA
may re-evaluate predicates during backtracking, making volatile
results non-deterministic; STABLE and IMMUTABLE are accepted).

The constant-offset rule now also catches column references in the
inner offset of compound forms.
---
 src/backend/commands/explain.c                |   8 +-
 src/backend/executor/nodeWindowAgg.c          | 282 +++++-------
 src/backend/optimizer/plan/createplan.c       | 434 +++++++++---------
 src/backend/optimizer/plan/rpr.c              |  34 ++
 src/backend/parser/parse_rpr.c                | 393 ++++++++++------
 src/include/optimizer/rpr.h                   |  22 +
 src/test/regress/expected/rpr.out             |  74 +--
 src/test/regress/expected/rpr_explain.out     |  11 +-
 src/test/regress/expected/rpr_integration.out |  40 +-
 src/test/regress/sql/rpr.sql                  |  35 +-
 src/test/regress/sql/rpr_explain.sql          |   7 +-
 src/test/regress/sql/rpr_integration.sql      |  23 +-
 src/tools/pgindent/typedefs.list              |  10 +-
 13 files changed, 758 insertions(+), 615 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 99de36b57f2..1a754bcdac5 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -3312,8 +3312,12 @@ show_window_def(WindowAggState *planstate, List *ancestors, ExplainState *es)
 											es);
 						break;
 					case RPR_NAV_OFFSET_FIXED:
-						ExplainPropertyInteger("Nav Mark Lookahead", NULL,
-											   firstOffset, es);
+						if (firstOffset == INT64_MAX)
+							ExplainPropertyText("Nav Mark Lookahead", "infinite",
+												es);
+						else
+							ExplainPropertyInteger("Nav Mark Lookahead", NULL,
+												   firstOffset, es);
 						break;
 					default:
 						elog(ERROR, "unrecognized RPR nav offset kind: %d",
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index f1cd9b66098..200d8a49001 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -247,8 +247,7 @@ static void update_reduced_frame(WindowObject winobj, int64 pos);
 static bool nfa_evaluate_row(WindowObject winobj, int64 pos, bool *varMatched);
 
 /* Forward declarations - navigation offset evaluation */
-static void eval_nav_max_offset(WindowAggState *winstate, List *defineClause);
-static void eval_nav_first_offset(WindowAggState *winstate, List *defineClause);
+static void eval_define_offsets(WindowAggState *winstate, List *defineClause);
 
 /*
  * Not null info bit array consists of 2-bit items
@@ -2580,12 +2579,10 @@ ExecWindowAgg(PlanState *pstate)
 			{
 				int64		firstreach;
 
-				if (winstate->navFirstOffset > -winstate->nfaContext->matchStartRow)
-					firstreach = winstate->nfaContext->matchStartRow
-						+ winstate->navFirstOffset;
-				else
-					firstreach = 0;
-				navmarkpos = Min(navmarkpos, firstreach);
+				if (!pg_add_s64_overflow(winstate->nfaContext->matchStartRow,
+										 winstate->navFirstOffset,
+										 &firstreach))
+					navmarkpos = Min(navmarkpos, Max(firstreach, 0));
 			}
 
 			if (navmarkpos > winstate->nav_winobj->markpos)
@@ -3038,17 +3035,13 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
 	winstate->rpSkipTo = node->rpSkipTo;
 	/* Set up row pattern recognition PATTERN clause (compiled NFA) */
 	winstate->rpPattern = node->rpPattern;
-	/* Set up nav offsets for tuplestore trim */
+	/* Set up nav offsets for tuplestore trim; resolve any NEEDS_EVAL kinds */
 	winstate->navMaxOffsetKind = node->navMaxOffsetKind;
 	winstate->navMaxOffset = node->navMaxOffset;
-	if (winstate->navMaxOffsetKind == RPR_NAV_OFFSET_NEEDS_EVAL)
-		eval_nav_max_offset(winstate, node->defineClause);
 	winstate->hasFirstNav = node->hasFirstNav;
 	winstate->navFirstOffsetKind = node->navFirstOffsetKind;
 	winstate->navFirstOffset = node->navFirstOffset;
-	if (winstate->hasFirstNav &&
-		winstate->navFirstOffsetKind == RPR_NAV_OFFSET_NEEDS_EVAL)
-		eval_nav_first_offset(winstate, node->defineClause);
+	eval_define_offsets(winstate, node->defineClause);
 
 	/* Copy match_start dependency bitmapset for per-context evaluation */
 	winstate->defineMatchStartDependent = bms_copy(node->defineMatchStartDependent);
@@ -4020,42 +4013,64 @@ eval_nav_offset_helper(WindowAggState *winstate, Expr *offset_expr,
 typedef struct
 {
 	WindowAggState *winstate;
-	int64		maxOffset;
-	bool		overflow;		/* true if overflow detected */
-} EvalNavMaxContext;
+	int64		maxOffset;		/* max backward-reach offset across all nav
+								 * exprs */
+	bool		maxOverflow;	/* true if backward-reach overflow detected */
+	int64		minFirstOffset; /* min forward-from-match_start offset; may be
+								 * negative (PREV_FIRST: inner - outer < 0) */
+} EvalDefineOffsetsContext;
 
 /*
- * eval_nav_max_offset_walker
- *		Walk expression tree evaluating backward-reach offsets at runtime.
+ * visit_nav_exec
+ *		nav_traversal_walker callback (NavVisitFn) for the executor side.
+ *		At each RPRNavExpr, evaluates the nav's offset expression(s) at
+ *		runtime via eval_nav_offset_helper and accumulates:
+ *
+ *		  - maxOffset (backward reach): PREV, LAST-with-offset, compound
+ *			PREV_LAST (sets maxOverflow on int64 overflow), compound
+ *			NEXT_LAST (= max(inner - outer, 0))
+ *		  - minFirstOffset (forward reach from match_start): FIRST,
+ *			compound PREV_FIRST (= inner - outer, may be negative),
+ *			compound NEXT_FIRST (= inner + outer, clamped to INT64_MAX on
+ *			overflow; always >= 0 so never updates minFirstOffset in practice)
  *
- * Handles simple PREV, LAST-with-offset, and compound PREV_LAST/NEXT_LAST.
+ * Counterpart of visit_nav_plan but using runtime evaluation instead of
+ * Const folding; runs only for offsets the planner marked NEEDS_EVAL.
+ * Match-start dependency is not recomputed here -- the planner's bitmapset
+ * is reused via winstate->defineMatchStartDependent.
  */
-static bool
-eval_nav_max_offset_walker(Node *node, void *ctx)
+static void
+visit_nav_exec(NavTraversal *t, RPRNavExpr *nav)
 {
-	EvalNavMaxContext *context = (EvalNavMaxContext *) ctx;
-
-	if (node == NULL)
-		return false;
+	EvalDefineOffsetsContext *context = (EvalDefineOffsetsContext *) t->data;
 
-	/* Short-circuit if overflow already detected */
-	if (context->overflow)
-		return false;
+	/*
+	 * Parser guarantee (mirrors visit_nav_plan): nav's direct children are
+	 * never RPRNavExpr -- compound nesting is flattened in place and any
+	 * other nesting is rejected.  Outer-kind dispatch is sufficient.
+	 */
+	Assert(nav->arg == NULL || !IsA(nav->arg, RPRNavExpr));
+	Assert(nav->offset_arg == NULL || !IsA(nav->offset_arg, RPRNavExpr));
+	Assert(nav->compound_offset_arg == NULL ||
+		   !IsA(nav->compound_offset_arg, RPRNavExpr));
 
-	if (IsA(node, RPRNavExpr))
+	/* Backward reach: PREV, LAST-with-offset */
+	if (!context->maxOverflow)
 	{
-		RPRNavExpr *nav = (RPRNavExpr *) node;
 		int64		reach = 0;
+		bool		gotReach = false;
 
 		if (nav->kind == RPR_NAV_PREV)
 		{
 			reach = eval_nav_offset_helper(context->winstate,
 										   nav->offset_arg, 1);
+			gotReach = true;
 		}
 		else if (nav->kind == RPR_NAV_LAST && nav->offset_arg != NULL)
 		{
 			reach = eval_nav_offset_helper(context->winstate,
 										   nav->offset_arg, 0);
+			gotReach = true;
 		}
 		else if (nav->kind == RPR_NAV_PREV_LAST ||
 				 nav->kind == RPR_NAV_NEXT_LAST)
@@ -4068,168 +4083,123 @@ eval_nav_max_offset_walker(Node *node, void *ctx)
 			if (nav->kind == RPR_NAV_PREV_LAST)
 			{
 				if (pg_add_s64_overflow(inner, outer, &reach))
-				{
-					context->overflow = true;
-					return false;
-				}
+					context->maxOverflow = true;
+				else
+					gotReach = true;
 			}
 			else
-				reach = (inner > outer) ? inner - outer : 0;
+			{
+				reach = Max(inner - outer, 0);
+				gotReach = true;
+			}
 		}
 
-		context->maxOffset = Max(context->maxOffset, reach);
-
-		return false;			/* don't walk into children */
+		if (gotReach)
+			context->maxOffset = Max(context->maxOffset, reach);
 	}
 
-	return expression_tree_walker(node, eval_nav_max_offset_walker, ctx);
-}
-
-/*
- * eval_nav_max_offset
- *		Evaluate non-constant backward-reach offsets at executor init time.
- *
- * Called when the planner set navMaxOffsetKind to RPR_NAV_OFFSET_NEEDS_EVAL
- * because some offset in PREV, LAST-with-offset, or compound PREV_LAST/
- * NEXT_LAST contains a parameter or non-foldable expression.
- *
- * On overflow, sets navMaxOffsetKind to RPR_NAV_OFFSET_RETAIN_ALL so that
- * tuplestore trim is disabled for backward navigation.
- */
-static void
-eval_nav_max_offset(WindowAggState *winstate, List *defineClause)
-{
-	EvalNavMaxContext ctx;
-	ListCell   *lc;
-
-	ctx.winstate = winstate;
-	ctx.maxOffset = 0;
-	ctx.overflow = false;
-
-	foreach(lc, defineClause)
+	/* Forward reach from match_start: FIRST, compound PREV_FIRST/NEXT_FIRST */
+	if (nav->kind == RPR_NAV_FIRST)
 	{
-		TargetEntry *te = (TargetEntry *) lfirst(lc);
+		int64		reach;
 
-		eval_nav_max_offset_walker((Node *) te->expr, &ctx);
-	}
-
-	if (ctx.overflow)
-	{
-		winstate->navMaxOffsetKind = RPR_NAV_OFFSET_RETAIN_ALL;
-		winstate->navMaxOffset = 0;
+		reach = eval_nav_offset_helper(context->winstate,
+									   nav->offset_arg, 0);
+		context->minFirstOffset = Min(context->minFirstOffset, reach);
 	}
-	else
+	else if (nav->kind == RPR_NAV_PREV_FIRST ||
+			 nav->kind == RPR_NAV_NEXT_FIRST)
 	{
-		winstate->navMaxOffsetKind = RPR_NAV_OFFSET_FIXED;
-		winstate->navMaxOffset = ctx.maxOffset;
-	}
-}
+		int64		inner = eval_nav_offset_helper(context->winstate,
+												   nav->offset_arg, 0);
+		int64		outer = eval_nav_offset_helper(context->winstate,
+												   nav->compound_offset_arg, 1);
+		int64		reach;
 
-typedef struct
-{
-	WindowAggState *winstate;
-	int64		minOffset;
-	bool		found;
-} EvalNavFirstContext;
-
-/*
- * eval_nav_first_offset_walker
- *		Walk expression tree evaluating forward-from-match_start offsets.
- *
- * Handles simple FIRST and compound PREV_FIRST/NEXT_FIRST.
- */
-static bool
-eval_nav_first_offset_walker(Node *node, void *ctx)
-{
-	EvalNavFirstContext *context = (EvalNavFirstContext *) ctx;
-
-	if (node == NULL)
-		return false;
-
-	if (IsA(node, RPRNavExpr))
-	{
-		RPRNavExpr *nav = (RPRNavExpr *) node;
-		int64		combined = INT64_MAX;
-
-		if (nav->kind == RPR_NAV_FIRST)
+		if (nav->kind == RPR_NAV_PREV_FIRST)
 		{
-			context->found = true;
-			combined = eval_nav_offset_helper(context->winstate,
-											  nav->offset_arg, 0);
+			/*
+			 * reach = inner - outer.  Both are non-negative, so the result >=
+			 * -INT64_MAX, which cannot underflow int64.
+			 */
+			reach = inner - outer;
 		}
-		else if (nav->kind == RPR_NAV_PREV_FIRST ||
-				 nav->kind == RPR_NAV_NEXT_FIRST)
+		else
 		{
-			int64		inner = eval_nav_offset_helper(context->winstate,
-													   nav->offset_arg, 0);
-			int64		outer = eval_nav_offset_helper(context->winstate,
-													   nav->compound_offset_arg, 1);
-
-			context->found = true;
-			if (nav->kind == RPR_NAV_PREV_FIRST)
-			{
-				/*
-				 * combined = inner - outer.  Both are non-negative, so the
-				 * result >= -INT64_MAX, which cannot underflow int64.
-				 */
-				combined = inner - outer;
-			}
-			else
-			{
-				/*
-				 * NEXT_FIRST: combined = inner + outer.  This can overflow,
-				 * but the result is always >= 0, so it never updates
-				 * minOffset (which tracks the minimum).  Clamp to INT64_MAX
-				 * on overflow.
-				 */
-				if (pg_add_s64_overflow(inner, outer, &combined))
-					combined = INT64_MAX;
-			}
+			/*
+			 * NEXT_FIRST: reach = inner + outer.  This can overflow, but the
+			 * result is always >= 0, so it never updates minFirstOffset
+			 * (which tracks the minimum).  Clamp to INT64_MAX on overflow.
+			 */
+			if (pg_add_s64_overflow(inner, outer, &reach))
+				reach = INT64_MAX;
 		}
-
-		context->minOffset = Min(context->minOffset, combined);
-
-		return false;
+		context->minFirstOffset = Min(context->minFirstOffset, reach);
 	}
-
-	return expression_tree_walker(node, eval_nav_first_offset_walker, ctx);
 }
 
 /*
- * eval_nav_first_offset
- *		Evaluate non-constant forward-from-match_start offsets at executor
- *		init time.
+ * eval_define_offsets
+ *		Evaluate non-constant nav offsets at executor init time.
+ *
+ * Called when the planner set navMaxOffsetKind and/or navFirstOffsetKind
+ * to RPR_NAV_OFFSET_NEEDS_EVAL because some offset contains a parameter
+ * or non-foldable expression.  Updates only the fields whose kind was
+ * NEEDS_EVAL; FIXED kinds are left unchanged.
  *
- * Called when the planner set navFirstOffsetKind to RPR_NAV_OFFSET_NEEDS_EVAL
- * because some offset in FIRST or compound PREV_FIRST/NEXT_FIRST contains
- * a parameter or non-foldable expression.
+ * On backward-reach overflow, sets navMaxOffsetKind to
+ * RPR_NAV_OFFSET_RETAIN_ALL so that tuplestore trim is disabled for
+ * backward navigation.
  */
 static void
-eval_nav_first_offset(WindowAggState *winstate, List *defineClause)
+eval_define_offsets(WindowAggState *winstate, List *defineClause)
 {
-	EvalNavFirstContext ctx;
+	EvalDefineOffsetsContext ctx;
+	NavTraversal trav;
 	ListCell   *lc;
+	bool		needsMax = (winstate->navMaxOffsetKind == RPR_NAV_OFFSET_NEEDS_EVAL);
+	bool		needsFirst = (winstate->hasFirstNav &&
+							  winstate->navFirstOffsetKind == RPR_NAV_OFFSET_NEEDS_EVAL);
+
+	if (!needsMax && !needsFirst)
+		return;
 
 	ctx.winstate = winstate;
-	ctx.minOffset = INT64_MAX;
-	ctx.found = false;
+	ctx.maxOffset = 0;
+	ctx.maxOverflow = false;
+	ctx.minFirstOffset = INT64_MAX;
+
+	trav.visit = visit_nav_exec;
+	trav.data = &ctx;
 
 	foreach(lc, defineClause)
 	{
 		TargetEntry *te = (TargetEntry *) lfirst(lc);
 
-		eval_nav_first_offset_walker((Node *) te->expr, &ctx);
+		nav_traversal_walker((Node *) te->expr, &trav);
 	}
 
-	if (ctx.found && ctx.minOffset < INT64_MAX)
+	if (needsMax)
 	{
-		winstate->navFirstOffsetKind = RPR_NAV_OFFSET_FIXED;
-		winstate->navFirstOffset = ctx.minOffset;
+		if (ctx.maxOverflow)
+		{
+			winstate->navMaxOffsetKind = RPR_NAV_OFFSET_RETAIN_ALL;
+			winstate->navMaxOffset = 0;
+		}
+		else
+		{
+			winstate->navMaxOffsetKind = RPR_NAV_OFFSET_FIXED;
+			winstate->navMaxOffset = ctx.maxOffset;
+		}
 	}
-	else
+
+	if (needsFirst)
 	{
 		winstate->navFirstOffsetKind = RPR_NAV_OFFSET_FIXED;
-		winstate->navFirstOffset = 0;
+		if (ctx.minFirstOffset < INT64_MAX)
+			winstate->navFirstOffset = ctx.minFirstOffset;
+		else
+			winstate->navFirstOffset = INT64_MAX;
 	}
 }
 
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index 52205cc7159..c8ecaeea7cf 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -294,6 +294,9 @@ static WindowAgg *make_windowagg(List *tlist, WindowClause *wc,
 								 RPRPattern *compiledPattern,
 								 List *defineClause,
 								 Bitmapset *defineMatchStartDependent,
+								 RPRNavOffsetKind navMaxOffsetKind, int64 navMaxOffset,
+								 bool hasFirstNav,
+								 RPRNavOffsetKind navFirstOffsetKind, int64 navFirstOffset,
 								 List *qual, bool topWindow,
 								 Plan *lefttree);
 static Group *make_group(List *tlist, List *qual, int numGroupCols,
@@ -2464,13 +2467,20 @@ create_minmaxagg_plan(PlannerInfo *root, MinMaxAggPath *best_path)
 }
 
 /*
- * NavOffsetContext - context for compute_nav_offsets walker.
+ * DefineMetadataContext - context for compute_define_metadata walker.
  *
- * Collects both backward reach (PREV, LAST-with-offset, compound
- * PREV_LAST/NEXT_LAST) and forward-from-match-start reach (FIRST,
- * compound PREV_FIRST/NEXT_FIRST) in a single tree walk.
+ * Collects three pieces of metadata from the DEFINE clause in a single
+ * tree walk per variable:
+ *   - backward reach (PREV, LAST-with-offset, compound PREV_LAST/NEXT_LAST)
+ *   - forward-from-match-start reach (FIRST, compound PREV_FIRST/NEXT_FIRST)
+ *   - per-variable match_start dependency (variables containing FIRST,
+ *     LAST-with-offset, or compound PREV_FIRST/NEXT_FIRST/PREV_LAST/
+ *     NEXT_LAST-with-offset require per-context re-evaluation)
+ *
+ * The driver sets curVarIdx to the index of the variable being walked
+ * before each invocation; the walker uses it to populate matchStartDependent.
  */
-typedef struct NavOffsetContext
+typedef struct DefineMetadataContext
 {
 	int64		maxOffset;		/* max PREV/LAST backward offset (>= 0) */
 	bool		maxNeedsEval;	/* non-constant PREV/LAST offset found */
@@ -2479,7 +2489,10 @@ typedef struct NavOffsetContext
 								 * PREV_FIRST) */
 	bool		hasFirst;		/* any FIRST node found */
 	bool		firstNeedsEval; /* non-constant FIRST offset found */
-} NavOffsetContext;
+	int			curVarIdx;		/* DEFINE variable currently being walked */
+	Bitmapset  *matchStartDependent;	/* variables that depend on
+										 * match_start */
+} DefineMetadataContext;
 
 /*
  * Helper: extract constant offset from an expression, handling NULL/negative.
@@ -2514,175 +2527,207 @@ extract_const_offset(Expr *expr, int64 defaultOffset, int64 *result)
 }
 
 /*
- * nav_offset_walker
- *		Expression tree walker for compute_nav_offsets.
+ * visit_nav_plan
+ *		nav_traversal_walker callback (NavVisitFn) for the planner side.
+ *		At each RPRNavExpr in a DEFINE expression, computes:
+ *
+ *		  1. backward reach (maxOffset) for tuplestore trim:
+ *			 - PREV(v, N), LAST(v, N)              -> N (default 1)
+ *			 - compound PREV_LAST(v, N, M)         -> N + M (overflow -> maxOverflow)
+ *			 - compound NEXT_LAST(v, N, M)         -> max(N - M, 0)
  *
- * For each RPRNavExpr found, extract its constant offset(s) and update the
- * NavOffsetContext with the maximum backward reach (maxOffset) and minimum
- * forward reach (firstOffset).  Handles simple navigation (PREV, NEXT,
- * FIRST, LAST) and compound forms (PREV_FIRST, NEXT_FIRST, PREV_LAST,
- * NEXT_LAST) by combining inner and outer offsets.
+ *		  2. forward reach (firstOffset) for tuplestore trim:
+ *			 - FIRST(v, N)                         -> N (default 0)
+ *			 - compound PREV_FIRST(v, N, M)        -> N - M (may be negative)
+ *			 - compound NEXT_FIRST(v, N, M)        -> N + M
  *
- * Non-constant offsets set maxNeedsEval or firstNeedsEval.  Overflow sets
- * maxOverflow or firstOverflow for RETAIN_ALL fallback.
+ *		  3. per-variable match_start dependency for absorption suppression:
+ *			 outer nav kinds that reach match_start (FIRST, LAST-with-offset,
+ *			 PREV_FIRST, NEXT_FIRST, PREV_LAST/NEXT_LAST-with-offset) add
+ *			 curVarIdx to matchStartDependent.
+ *
+ * Constant offsets are extracted via extract_const_offset; non-constant
+ * offsets set maxNeedsEval / firstNeedsEval so the executor can resolve
+ * them at init time (see visit_nav_exec).  Classification uses only the
+ * outer nav kind: parser nesting restrictions prevent FIRST/LAST inside
+ * a PREV/NEXT value subexpression.
  */
-static bool
-nav_offset_walker(Node *node, void *ctx)
+static void
+visit_nav_plan(NavTraversal *t, RPRNavExpr *nav)
 {
-	NavOffsetContext *context = (NavOffsetContext *) ctx;
+	DefineMetadataContext *context = (DefineMetadataContext *) t->data;
 
-	if (node == NULL)
-		return false;
+	/*
+	 * Parser guarantee: by the time the planner sees a DEFINE expression,
+	 * compound nesting has been flattened into a single RPRNavExpr and any
+	 * other RPRNavExpr nesting has been rejected.  So nav's direct child
+	 * fields are not themselves RPRNavExpr nodes, and outer-kind dispatch
+	 * below is sufficient.
+	 */
+	Assert(nav->arg == NULL || !IsA(nav->arg, RPRNavExpr));
+	Assert(nav->offset_arg == NULL || !IsA(nav->offset_arg, RPRNavExpr));
+	Assert(nav->compound_offset_arg == NULL ||
+		   !IsA(nav->compound_offset_arg, RPRNavExpr));
 
-	if (IsA(node, RPRNavExpr))
+	/*
+	 * Simple PREV(v, N) and LAST(v, N): backward reach from currentpos. LAST
+	 * without offset = currentpos, no backward reach. NEXT: forward only,
+	 * irrelevant for trim.
+	 */
+	if (nav->kind == RPR_NAV_PREV ||
+		(nav->kind == RPR_NAV_LAST && nav->offset_arg != NULL))
 	{
-		RPRNavExpr *nav = (RPRNavExpr *) node;
-
-		/*
-		 * Simple PREV(v, N) and LAST(v, N): backward reach from currentpos.
-		 * LAST without offset = currentpos, no backward reach. NEXT: forward
-		 * only, irrelevant for trim.
-		 */
-		if (nav->kind == RPR_NAV_PREV ||
-			(nav->kind == RPR_NAV_LAST && nav->offset_arg != NULL))
+		if (!context->maxNeedsEval)
 		{
-			if (!context->maxNeedsEval)
-			{
-				int64		offset;
+			int64		offset;
 
-				if (extract_const_offset(nav->offset_arg, 1, &offset))
-					context->maxOffset = Max(context->maxOffset, offset);
-				else
-					context->maxNeedsEval = true;
-			}
+			if (extract_const_offset(nav->offset_arg, 1, &offset))
+				context->maxOffset = Max(context->maxOffset, offset);
+			else
+				context->maxNeedsEval = true;
 		}
+	}
 
-		/*
-		 * Simple FIRST(v, N): forward reach from match_start. Smaller N means
-		 * older rows needed.
-		 */
-		if (nav->kind == RPR_NAV_FIRST)
-		{
-			context->hasFirst = true;
+	/*
+	 * Simple FIRST(v, N): forward reach from match_start. Smaller N means
+	 * older rows needed.
+	 */
+	if (nav->kind == RPR_NAV_FIRST)
+	{
+		context->hasFirst = true;
 
-			if (!context->firstNeedsEval)
-			{
-				int64		offset;
+		if (!context->firstNeedsEval)
+		{
+			int64		offset;
 
-				if (extract_const_offset(nav->offset_arg, 0, &offset))
-					context->firstOffset = Min(context->firstOffset, offset);
-				else
-					context->firstNeedsEval = true;
-			}
+			if (extract_const_offset(nav->offset_arg, 0, &offset))
+				context->firstOffset = Min(context->firstOffset, offset);
+			else
+				context->firstNeedsEval = true;
 		}
+	}
 
-		/*
-		 * Compound PREV_LAST / NEXT_LAST: base = currentpos. PREV_LAST(v, N,
-		 * M): target = currentpos - N - M -> lookback = N + M NEXT_LAST(v, N,
-		 * M): target = currentpos - N + M -> lookback = max(N - M, 0)
-		 */
-		if (nav->kind == RPR_NAV_PREV_LAST ||
-			nav->kind == RPR_NAV_NEXT_LAST)
+	/*
+	 * Compound PREV_LAST / NEXT_LAST: base = currentpos. PREV_LAST(v, N, M):
+	 * target = currentpos - N - M -> lookback = N + M NEXT_LAST(v, N, M):
+	 * target = currentpos - N + M -> lookback = max(N - M, 0)
+	 */
+	if (nav->kind == RPR_NAV_PREV_LAST ||
+		nav->kind == RPR_NAV_NEXT_LAST)
+	{
+		if (!context->maxNeedsEval)
 		{
-			if (!context->maxNeedsEval)
-			{
-				int64		inner,
-							outer,
-							combined;
+			int64		inner;
+			int64		outer;
+			int64		reach;
 
-				if (extract_const_offset(nav->offset_arg, 0, &inner) &&
-					extract_const_offset(nav->compound_offset_arg, 1, &outer))
+			if (extract_const_offset(nav->offset_arg, 0, &inner) &&
+				extract_const_offset(nav->compound_offset_arg, 1, &outer))
+			{
+				if (nav->kind == RPR_NAV_PREV_LAST)
 				{
-					if (nav->kind == RPR_NAV_PREV_LAST)
-					{
-						if (pg_add_s64_overflow(inner, outer, &combined))
-						{
-							context->maxOverflow = true;
-							return false;
-						}
-					}
+					if (pg_add_s64_overflow(inner, outer, &reach))
+						context->maxOverflow = true;
 					else
-						combined = (inner > outer) ? inner - outer : 0;
-
-					context->maxOffset = Max(context->maxOffset, combined);
+						context->maxOffset = Max(context->maxOffset, reach);
 				}
 				else
-					context->maxNeedsEval = true;
+				{
+					reach = Max(inner - outer, 0);
+					context->maxOffset = Max(context->maxOffset, reach);
+				}
 			}
+			else
+				context->maxNeedsEval = true;
 		}
+	}
 
-		/*
-		 * Compound PREV_FIRST / NEXT_FIRST: base = match_start. PREV_FIRST(v,
-		 * N, M): target = match_start + N - M NEXT_FIRST(v, N, M): target =
-		 * match_start + N + M The combined offset (N+/-M) from match_start
-		 * can be negative, meaning rows before match_start are needed.
-		 */
-		if (nav->kind == RPR_NAV_PREV_FIRST ||
-			nav->kind == RPR_NAV_NEXT_FIRST)
+	/*
+	 * Compound PREV_FIRST / NEXT_FIRST: base = match_start. PREV_FIRST(v, N,
+	 * M): target = match_start + N - M NEXT_FIRST(v, N, M): target =
+	 * match_start + N + M The combined offset (N+/-M) from match_start can be
+	 * negative, meaning rows before match_start are needed.
+	 */
+	if (nav->kind == RPR_NAV_PREV_FIRST ||
+		nav->kind == RPR_NAV_NEXT_FIRST)
+	{
+		context->hasFirst = true;
+
+		if (!context->firstNeedsEval)
 		{
-			context->hasFirst = true;
+			int64		inner;
+			int64		outer;
+			int64		reach;
 
-			if (!context->firstNeedsEval)
+			if (extract_const_offset(nav->offset_arg, 0, &inner) &&
+				extract_const_offset(nav->compound_offset_arg, 1, &outer))
 			{
-				int64		inner,
-							outer,
-							combined;
-
-				if (extract_const_offset(nav->offset_arg, 0, &inner) &&
-					extract_const_offset(nav->compound_offset_arg, 1, &outer))
+				if (nav->kind == RPR_NAV_PREV_FIRST)
 				{
-					if (nav->kind == RPR_NAV_PREV_FIRST)
-					{
-						/*
-						 * combined = inner - outer.  Both are non-negative,
-						 * so the result >= -INT64_MAX, which cannot underflow
-						 * int64.  No overflow check needed.
-						 */
-						combined = inner - outer;
-					}
-					else
-					{
-						/*
-						 * NEXT_FIRST: combined = inner + outer.  This can
-						 * overflow, but the result is always >= 0, so it
-						 * never updates firstOffset (which tracks the
-						 * minimum).  Clamp to INT64_MAX on overflow.
-						 */
-						if (pg_add_s64_overflow(inner, outer, &combined))
-							combined = INT64_MAX;
-					}
-
-					context->firstOffset = Min(context->firstOffset, combined);
+					/*
+					 * reach = inner - outer.  Both are non-negative, so the
+					 * result >= -INT64_MAX, which cannot underflow int64. No
+					 * overflow check needed.
+					 */
+					reach = inner - outer;
 				}
 				else
-					context->firstNeedsEval = true;
+				{
+					/*
+					 * NEXT_FIRST: reach = inner + outer.  This can overflow,
+					 * but the result is always >= 0, so it never updates
+					 * firstOffset (which tracks the minimum).  Clamp to
+					 * INT64_MAX on overflow.
+					 */
+					if (pg_add_s64_overflow(inner, outer, &reach))
+						reach = INT64_MAX;
+				}
+
+				context->firstOffset = Min(context->firstOffset, reach);
 			}
+			else
+				context->firstNeedsEval = true;
 		}
-
-		/* Don't walk into RPRNavExpr children */
-		return false;
 	}
 
-	return expression_tree_walker(node, nav_offset_walker, ctx);
+	/* Match-start dependency: classify the outer nav kind. */
+	if (nav->kind == RPR_NAV_FIRST ||
+		(nav->kind == RPR_NAV_LAST && nav->offset_arg != NULL) ||
+		nav->kind == RPR_NAV_PREV_FIRST ||
+		nav->kind == RPR_NAV_NEXT_FIRST ||
+		((nav->kind == RPR_NAV_PREV_LAST ||
+		  nav->kind == RPR_NAV_NEXT_LAST) &&
+		 nav->offset_arg != NULL))
+		context->matchStartDependent =
+			bms_add_member(context->matchStartDependent,
+						   context->curVarIdx);
 }
 
 /*
- * compute_nav_offsets
- *		Compute navigation offsets for tuplestore trim in a single pass.
+ * compute_define_metadata
+ *		Compute navigation offsets and match_start dependency for the
+ *		DEFINE clause in a single pass per variable.
  *
- * Walks all DEFINE clause expressions once, computing:
+ * Walks each DEFINE variable expression once, computing:
  *   - maxOffset: max backward reach from PREV, LAST-with-offset,
  *     compound PREV_LAST/NEXT_LAST
  *   - hasFirst/firstOffset: min forward-from-match-start reach from
  *     FIRST, compound PREV_FIRST/NEXT_FIRST
+ *   - matchStartDependent: bitmapset of variable indices whose
+ *     expressions contain navigation that depends on match_start
+ *     (FIRST, LAST-with-offset, or compound PREV_FIRST/NEXT_FIRST/
+ *     PREV_LAST/NEXT_LAST-with-offset).  Such variables require
+ *     per-context re-evaluation during NFA processing.
  */
 static void
-compute_nav_offsets(List *defineClause,
-					RPRNavOffsetKind *maxKind, int64 *maxResult,
-					bool *hasFirst,
-					RPRNavOffsetKind *firstKind, int64 *firstResult)
+compute_define_metadata(List *defineClause,
+						RPRNavOffsetKind *maxKind, int64 *maxResult,
+						bool *hasFirst,
+						RPRNavOffsetKind *firstKind, int64 *firstResult,
+						Bitmapset **matchStartDependent)
 {
-	NavOffsetContext ctx;
+	DefineMetadataContext ctx;
+	NavTraversal trav;
 	ListCell   *lc;
 
 	ctx.maxOffset = 0;
@@ -2691,14 +2736,22 @@ compute_nav_offsets(List *defineClause,
 	ctx.firstOffset = INT64_MAX;	/* sentinel: no FIRST found yet */
 	ctx.hasFirst = false;
 	ctx.firstNeedsEval = false;
+	ctx.curVarIdx = 0;
+	ctx.matchStartDependent = NULL;
+
+	trav.visit = visit_nav_plan;
+	trav.data = &ctx;
 
 	foreach(lc, defineClause)
 	{
 		TargetEntry *te = (TargetEntry *) lfirst(lc);
 
-		nav_offset_walker((Node *) te->expr, &ctx);
+		nav_traversal_walker((Node *) te->expr, &trav);
+		ctx.curVarIdx++;
 	}
 
+	*matchStartDependent = ctx.matchStartDependent;
+
 	/* Max backward offset */
 	if (ctx.maxOverflow)
 	{
@@ -2725,15 +2778,11 @@ compute_nav_offsets(List *defineClause,
 			*firstKind = RPR_NAV_OFFSET_NEEDS_EVAL;
 			*firstResult = 0;
 		}
-		else if (ctx.firstOffset == INT64_MAX)
-		{
-			*firstKind = RPR_NAV_OFFSET_FIXED;
-			*firstResult = 0;	/* only implicit FIRST(v) */
-		}
 		else
 		{
 			*firstKind = RPR_NAV_OFFSET_FIXED;
-			*firstResult = ctx.firstOffset; /* may be negative */
+			*firstResult = ctx.firstOffset; /* may be negative; INT64_MAX if
+											 * overflowed */
 		}
 	}
 	else
@@ -2743,83 +2792,6 @@ compute_nav_offsets(List *defineClause,
 	}
 }
 
-/*
- * has_match_start_dependency
- *		Check if an expression tree contains navigation that depends on
- *		match_start: FIRST, LAST-with-offset, or compound PREV_FIRST/
- *		NEXT_FIRST/PREV_LAST/NEXT_LAST with offset.  Such expressions
- *		require per-context re-evaluation during NFA processing.
- *
- * LAST without offset always resolves to currentpos and is
- * match_start-independent.
- */
-static bool
-has_match_start_dependency(Node *node, void *context)
-{
-	if (node == NULL)
-		return false;
-
-	if (IsA(node, RPRNavExpr))
-	{
-		RPRNavExpr *nav = (RPRNavExpr *) node;
-
-		if (nav->kind == RPR_NAV_FIRST)
-			return true;
-		if (nav->kind == RPR_NAV_LAST && nav->offset_arg != NULL)
-			return true;
-
-		/* Compound kinds with FIRST base depend on match_start */
-		if (nav->kind == RPR_NAV_PREV_FIRST ||
-			nav->kind == RPR_NAV_NEXT_FIRST)
-			return true;
-
-		/*
-		 * PREV_LAST/NEXT_LAST: inner is LAST, which uses currentpos.
-		 * match_start-dependent only if inner has offset (clamped to
-		 * match_start).
-		 */
-		if ((nav->kind == RPR_NAV_PREV_LAST ||
-			 nav->kind == RPR_NAV_NEXT_LAST) &&
-			nav->offset_arg != NULL)
-			return true;
-
-		/* Check children (arg may contain further nav expressions) */
-		return has_match_start_dependency((Node *) nav->arg, context);
-	}
-
-	return expression_tree_walker(node, has_match_start_dependency, NULL);
-}
-
-/*
- * compute_match_start_dependent
- *		Build a Bitmapset of DEFINE variable indices whose expressions
- *		depend on match_start (contain FIRST, LAST-with-offset, or
- *		compound PREV_FIRST/NEXT_FIRST/PREV_LAST/NEXT_LAST with offset).
- *
- * Variables in this set require per-context re-evaluation during NFA
- * processing, because different contexts may have different match_start
- * values.
- */
-static Bitmapset *
-compute_match_start_dependent(List *defineClause)
-{
-	Bitmapset  *result = NULL;
-	ListCell   *lc;
-	int			varIdx = 0;
-
-	foreach(lc, defineClause)
-	{
-		TargetEntry *te = (TargetEntry *) lfirst(lc);
-
-		if (has_match_start_dependency((Node *) te->expr, NULL))
-			result = bms_add_member(result, varIdx);
-
-		varIdx++;
-	}
-
-	return result;
-}
-
 /*
  * create_windowagg_plan
  *
@@ -2848,6 +2820,11 @@ create_windowagg_plan(PlannerInfo *root, WindowAggPath *best_path)
 	List	   *filteredDefineClause = NIL;
 	RPRPattern *compiledPattern = NULL;
 	Bitmapset  *matchStartDependent = NULL;
+	RPRNavOffsetKind navMaxOffsetKind = RPR_NAV_OFFSET_FIXED;
+	int64		navMaxOffset = 0;
+	bool		hasFirstNav = false;
+	RPRNavOffsetKind navFirstOffsetKind = RPR_NAV_OFFSET_FIXED;
+	int64		navFirstOffset = 0;
 
 
 	/*
@@ -2910,8 +2887,16 @@ create_windowagg_plan(PlannerInfo *root, WindowAggPath *best_path)
 		buildDefineVariableList(wc->defineClause, &defineVariableList);
 		filteredDefineClause = wc->defineClause;
 
-		/* Identify match_start-dependent DEFINE variables */
-		matchStartDependent = compute_match_start_dependent(wc->defineClause);
+		/*
+		 * Walk DEFINE once: collect nav offsets (for tuplestore trim) and the
+		 * bitmapset of match_start-dependent variables (for absorption
+		 * suppression in buildRPRPattern).
+		 */
+		compute_define_metadata(wc->defineClause,
+								&navMaxOffsetKind, &navMaxOffset,
+								&hasFirstNav,
+								&navFirstOffsetKind, &navFirstOffset,
+								&matchStartDependent);
 
 		/* Compile and optimize RPR patterns */
 		compiledPattern = buildRPRPattern(wc->rpPattern,
@@ -2937,6 +2922,9 @@ create_windowagg_plan(PlannerInfo *root, WindowAggPath *best_path)
 						  compiledPattern,
 						  filteredDefineClause,
 						  matchStartDependent,
+						  navMaxOffsetKind, navMaxOffset,
+						  hasFirstNav,
+						  navFirstOffsetKind, navFirstOffset,
 						  best_path->qual,
 						  best_path->topwindow,
 						  subplan);
@@ -7011,6 +6999,9 @@ make_windowagg(List *tlist, WindowClause *wc,
 			   RPRPattern *compiledPattern,
 			   List *defineClause,
 			   Bitmapset *defineMatchStartDependent,
+			   RPRNavOffsetKind navMaxOffsetKind, int64 navMaxOffset,
+			   bool hasFirstNav,
+			   RPRNavOffsetKind navFirstOffsetKind, int64 navFirstOffset,
 			   List *qual, bool topWindow, Plan *lefttree)
 {
 	WindowAgg  *node = makeNode(WindowAgg);
@@ -7048,11 +7039,12 @@ make_windowagg(List *tlist, WindowClause *wc,
 	/* Store pre-computed match_start dependency bitmapset */
 	node->defineMatchStartDependent = defineMatchStartDependent;
 
-	/* Compute nav offsets for tuplestore trim optimization */
-	compute_nav_offsets(defineClause,
-						&node->navMaxOffsetKind, &node->navMaxOffset,
-						&node->hasFirstNav,
-						&node->navFirstOffsetKind, &node->navFirstOffset);
+	/* Store pre-computed nav offsets for tuplestore trim optimization */
+	node->navMaxOffsetKind = navMaxOffsetKind;
+	node->navMaxOffset = navMaxOffset;
+	node->hasFirstNav = hasFirstNav;
+	node->navFirstOffsetKind = navFirstOffsetKind;
+	node->navFirstOffset = navFirstOffset;
 
 	plan->targetlist = tlist;
 	plan->lefttree = lefttree;
diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index 2543170c374..a817eb4a63f 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -41,6 +41,7 @@
 
 #include "miscadmin.h"
 #include "nodes/makefuncs.h"
+#include "nodes/nodeFuncs.h"
 #include "optimizer/rpr.h"
 
 /* Forward declarations - pattern comparison */
@@ -1991,3 +1992,36 @@ buildRPRPattern(RPRPatternNode *pattern, List *defineVariableList,
 
 	return result;
 }
+
+/*
+ * nav_traversal_walker
+ *		Shared expression-tree walker that locates RPRNavExpr nodes in a
+ *		DEFINE expression and dispatches each one to a caller-supplied
+ *		visitor.  Used by:
+ *		  - planner (visit_nav_plan in createplan.c) to collect tuplestore
+ *		    trim offsets and per-variable match_start dependency
+ *		  - executor (visit_nav_exec in nodeWindowAgg.c) to evaluate
+ *		    non-constant nav offsets at WindowAggState init time
+ *
+ * The driver wraps a mode-specific context in a NavTraversal and passes
+ * it as ctx; the visitor casts t->data to its own context type.  Children
+ * of an RPRNavExpr are not walked: the parser's nesting restrictions
+ * ensure offsets and dependencies are fully captured by the outer nav
+ * kind, so the visitor only needs to inspect the RPRNavExpr itself.
+ */
+bool
+nav_traversal_walker(Node *node, void *ctx)
+{
+	if (node == NULL)
+		return false;
+
+	if (IsA(node, RPRNavExpr))
+	{
+		NavTraversal *t = (NavTraversal *) ctx;
+
+		t->visit(t, (RPRNavExpr *) node);
+		return false;
+	}
+
+	return expression_tree_walker(node, nav_traversal_walker, ctx);
+}
diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index f56b7db5bc8..87411abcbe2 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -25,6 +25,7 @@
 
 #include "postgres.h"
 
+#include "catalog/pg_proc.h"
 #include "miscadmin.h"
 #include "nodes/makefuncs.h"
 #include "nodes/nodeFuncs.h"
@@ -35,14 +36,33 @@
 #include "parser/parse_expr.h"
 #include "parser/parse_rpr.h"
 #include "parser/parse_target.h"
+#include "utils/lsyscache.h"
+
+/* DEFINE clause walker context -- see define_walker for usage. */
+typedef enum
+{
+	DEFINE_PHASE_BODY,			/* top-level DEFINE expression */
+	DEFINE_PHASE_NAV_ARG,		/* inside an outer nav's arg subtree */
+	DEFINE_PHASE_NAV_OFFSET,	/* inside an outer nav's offset_arg /
+								 * compound_offset_arg */
+} DefinePhase;
+
+typedef struct
+{
+	ParseState *pstate;
+	DefinePhase phase;
+	int			nav_count;		/* RPRNavExpr nodes seen in current nav.arg */
+	bool		has_column_ref; /* Var seen in current nav scope */
+	RPRNavKind	inner_kind;		/* kind of first nested nav in current arg */
+} DefineWalkCtx;
 
 /* Forward declarations */
 static void validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
 									   List *rpDefs, List **varNames);
 static List *transformDefineClause(ParseState *pstate, WindowClause *wc,
 								   WindowDef *windef, List **targetlist);
-static void check_rpr_nav_expr(RPRNavExpr *nav, ParseState *pstate);
-static bool check_rpr_nav_nesting_walker(Node *node, void *context);
+static bool define_walker(Node *node, void *context);
+static bool nav_volatile_func_checker(Oid funcid, void *context);
 
 /*
  * transformRPR
@@ -412,9 +432,22 @@ transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 	foreach_ptr(TargetEntry, te, defineClause)
 		te->expr = (Expr *) coerce_to_boolean(pstate, (Node *) te->expr, "DEFINE");
 
-	/* check for nested PREV/NEXT and missing column references */
+	/*
+	 * Validate DEFINE expressions: nested PREV/NEXT, column references,
+	 * compound flatten, volatile callees -- all in a single walk per
+	 * variable.
+	 */
 	foreach_ptr(TargetEntry, te, defineClause)
-		(void) check_rpr_nav_nesting_walker((Node *) te->expr, pstate);
+	{
+		DefineWalkCtx ctx;
+
+		ctx.pstate = pstate;
+		ctx.phase = DEFINE_PHASE_BODY;
+		ctx.nav_count = 0;
+		ctx.has_column_ref = false;
+		ctx.inner_kind = 0;
+		(void) define_walker((Node *) te->expr, &ctx);
+	}
 
 	/* mark column origins */
 	markTargetListOrigins(pstate, defineClause);
@@ -426,169 +459,239 @@ transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 }
 
 /*
- * check_rpr_nav_expr
- *		Validate a single RPRNavExpr node by walking its arg and offset_arg
- *		subtrees in a single pass each.  Check for illegal nesting, missing
- *		column references, and non-constant offset expressions.
+ * Single-pass DEFINE clause validator.
  *
- * Nesting rules (SQL standard 5.6.4):
- *   - PREV/NEXT wrapping FIRST/LAST: allowed (compound navigation)
- *   - FIRST/LAST wrapping PREV/NEXT: prohibited
- *   - Same-category nesting (PREV inside PREV, FIRST inside FIRST, etc.):
- *     prohibited
+ * One walker function (define_walker) visits every node in a DEFINE
+ * expression exactly once and enforces every rule:
+ *   - Volatile callees and NextValueExpr are rejected at parse time
+ *     (RPR's NFA may evaluate the same row's predicate multiple times
+ *     during backtracking, so a volatile result would make matching
+ *     non-deterministic).
+ *   - For each outer RPRNavExpr (per SQL 5.6.4 nesting rules):
+ *     * arg must contain at least one column reference
+ *     * PREV/NEXT wrapping FIRST/LAST flattens to a compound kind
+ *     * Other nestings are rejected (FIRST(PREV()), PREV(PREV()), ...)
+ *     * offset_arg / compound_offset_arg must not contain column refs
+ *
+ * The walker uses a phase tag to know which subtree it is in: DEFINE
+ * body (top-level), inside a nav.arg, or inside a nav.offset_arg /
+ * compound_offset_arg.  When entering an outer nav (PHASE_BODY), it
+ * walks nav.arg in PHASE_NAV_ARG to collect nesting/column-ref state,
+ * applies compound flatten or raises a nesting error, then walks the
+ * (post-flatten) offset(s) in PHASE_NAV_OFFSET to enforce the
+ * constant-offset rule.  No subtree is walked twice.
  */
-typedef struct
+
+/*
+ * nav_volatile_func_checker
+ *		check_functions_in_node callback: true if funcid is VOLATILE.
+ */
+static bool
+nav_volatile_func_checker(Oid funcid, void *context)
 {
-	int			nav_count;		/* number of RPRNavExpr nodes found */
-	bool		has_column_ref; /* Var found */
-	RPRNavKind	inner_kind;		/* kind of first (outermost) nested RPRNavExpr */
-} NavCheckResult;
+	return (func_volatile(funcid) == PROVOLATILE_VOLATILE);
+}
 
+/*
+ * define_walker
+ *		Single-pass DEFINE clause validator.  At each node, enforces:
+ *
+ *		  [1] no volatile callees (and no NextValueExpr) -- anywhere in
+ *			  the tree, regardless of phase
+ *		  [2] for each outer RPRNavExpr (PHASE_BODY -> PHASE_NAV_ARG):
+ *			  - nav.arg must contain at least one column reference
+ *			  - PREV/NEXT wrapping FIRST/LAST is flattened in place
+ *				to a compound kind (PREV_FIRST, PREV_LAST, NEXT_FIRST,
+ *				NEXT_LAST)
+ *			  - any other nesting is rejected (FIRST(PREV()),
+ *				PREV(PREV()), FIRST(FIRST()), three-or-more deep)
+ *		  [3] for each nav offset (PHASE_NAV_OFFSET):
+ *			  - must be a run-time constant (no column references)
+ *
+ * Var sightings feed the column-ref rule for the enclosing nav scope;
+ * RPRNavExpr sightings inside PHASE_NAV_ARG feed the nesting decision.
+ * See the comment block above DefinePhase for the overall design and
+ * how each subtree is walked exactly once.
+ */
 static bool
-nav_check_walker(Node *node, void *context)
+define_walker(Node *node, void *context)
 {
-	NavCheckResult *result = (NavCheckResult *) context;
+	DefineWalkCtx *ctx = (DefineWalkCtx *) context;
 
 	if (node == NULL)
 		return false;
-	if (IsA(node, RPRNavExpr))
-	{
-		if (result->nav_count == 0)
-			result->inner_kind = ((RPRNavExpr *) node)->kind;
-		result->nav_count++;
-	}
-	if (IsA(node, Var))
-		result->has_column_ref = true;
-
-	return expression_tree_walker(node, nav_check_walker, context);
-}
 
-static void
-check_rpr_nav_expr(RPRNavExpr *nav, ParseState *pstate)
-{
-	NavCheckResult result;
-	bool		outer_is_physical = (nav->kind == RPR_NAV_PREV ||
-									 nav->kind == RPR_NAV_NEXT);
+	/*
+	 * Reject volatile callees and sequence operations anywhere in the DEFINE
+	 * clause: they are non-deterministic across the multiple predicate
+	 * evaluations that NFA backtracking and PREV/NEXT navigation may trigger
+	 * for a single row.
+	 */
+	if (check_functions_in_node(node, nav_volatile_func_checker, NULL))
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("volatile functions are not allowed in DEFINE clause"),
+				 parser_errposition(ctx->pstate, exprLocation(node))));
+	if (IsA(node, NextValueExpr))
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("sequence operations are not allowed in DEFINE clause"),
+				 parser_errposition(ctx->pstate, exprLocation(node))));
 
-	/* Check arg subtree: nesting + column reference in one walk */
-	memset(&result, 0, sizeof(result));
-	(void) nav_check_walker((Node *) nav->arg, &result);
+	/* Var sighting feeds the column-ref rule for the enclosing nav scope. */
+	if (IsA(node, Var) &&
+		(ctx->phase == DEFINE_PHASE_NAV_ARG ||
+		 ctx->phase == DEFINE_PHASE_NAV_OFFSET))
+		ctx->has_column_ref = true;
 
-	if (result.nav_count > 0)
+	if (IsA(node, RPRNavExpr))
 	{
-		bool		inner_is_physical = (result.inner_kind == RPR_NAV_PREV ||
-										 result.inner_kind == RPR_NAV_NEXT);
+		RPRNavExpr *nav = (RPRNavExpr *) node;
 
-		if (outer_is_physical && !inner_is_physical)
+		if (ctx->phase == DEFINE_PHASE_NAV_ARG)
 		{
 			/*
-			 * PREV/NEXT wrapping FIRST/LAST: compound navigation per SQL
-			 * standard 5.6.4.  Flatten the nested RPRNavExpr into a single
-			 * compound node.  The inner RPRNavExpr must be the direct arg of
-			 * the outer; expressions like PREV(val + FIRST(v)) are not valid
-			 * compound navigation.
+			 * Nested nav inside an outer nav.arg: record for the outer's
+			 * compound / nesting decision, then keep recursing so deeper Vars
+			 * and volatile callees are still observed.
 			 */
-			RPRNavExpr *inner;
-
-			/* Reject triple-or-deeper nesting (e.g. PREV(FIRST(PREV(x)))) */
-			if (result.nav_count > 1)
-				ereport(ERROR,
-						(errcode(ERRCODE_SYNTAX_ERROR),
-						 errmsg("cannot nest row pattern navigation more than two levels deep"),
-						 errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
-						 parser_errposition(pstate, nav->location)));
-
-			if (!IsA(nav->arg, RPRNavExpr))
-				ereport(ERROR,
-						(errcode(ERRCODE_SYNTAX_ERROR),
-						 errmsg("row pattern navigation operation must be a direct argument of the outer navigation"),
-						 errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
-						 parser_errposition(pstate, nav->location)));
-			inner = (RPRNavExpr *) nav->arg;
-
-			/* Determine compound kind */
-			if (nav->kind == RPR_NAV_PREV && inner->kind == RPR_NAV_FIRST)
-				nav->kind = RPR_NAV_PREV_FIRST;
-			else if (nav->kind == RPR_NAV_PREV && inner->kind == RPR_NAV_LAST)
-				nav->kind = RPR_NAV_PREV_LAST;
-			else if (nav->kind == RPR_NAV_NEXT && inner->kind == RPR_NAV_FIRST)
-				nav->kind = RPR_NAV_NEXT_FIRST;
-			else if (nav->kind == RPR_NAV_NEXT && inner->kind == RPR_NAV_LAST)
-				nav->kind = RPR_NAV_NEXT_LAST;
-
-			/* Move outer offset to compound_offset_arg */
-			nav->compound_offset_arg = nav->offset_arg;
-
-			/* Move inner offset and arg up */
-			nav->offset_arg = inner->offset_arg;
-			nav->arg = inner->arg;
-
-			/* No further nesting check needed - already validated */
-			return;
-		}
-		else if (!outer_is_physical && inner_is_physical)
-		{
-			/* FIRST/LAST wrapping PREV/NEXT: prohibited by standard */
-			ereport(ERROR,
-					(errcode(ERRCODE_SYNTAX_ERROR),
-					 errmsg("FIRST and LAST cannot contain PREV or NEXT"),
-					 errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
-					 parser_errposition(pstate, nav->location)));
+			if (ctx->nav_count == 0)
+				ctx->inner_kind = nav->kind;
+			ctx->nav_count++;
+			return expression_tree_walker(node, define_walker, ctx);
 		}
-		else if (outer_is_physical && inner_is_physical)
+
+		if (ctx->phase == DEFINE_PHASE_NAV_OFFSET)
 		{
-			/* PREV/NEXT wrapping PREV/NEXT: prohibited */
-			ereport(ERROR,
-					(errcode(ERRCODE_SYNTAX_ERROR),
-					 errmsg("PREV and NEXT cannot contain PREV or NEXT"),
-					 errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
-					 parser_errposition(pstate, nav->location)));
+			/*
+			 * Navs inside offset_arg are unusual but not directly banned; the
+			 * constant-offset rule will catch any Var or volatile they
+			 * contain.
+			 */
+			return expression_tree_walker(node, define_walker, ctx);
 		}
-		else
+
+		/*
+		 * PHASE_BODY: this is an outer nav at top level.  Walk arg first to
+		 * collect nesting / column-ref state, then validate and (for compound
+		 * forms) flatten, then walk offset(s).
+		 */
 		{
-			/* FIRST/LAST wrapping FIRST/LAST: prohibited */
-			ereport(ERROR,
-					(errcode(ERRCODE_SYNTAX_ERROR),
-					 errmsg("FIRST and LAST cannot contain FIRST or LAST"),
-					 errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
-					 parser_errposition(pstate, nav->location)));
-		}
-	}
-	if (!result.has_column_ref)
-		ereport(ERROR,
-				(errcode(ERRCODE_SYNTAX_ERROR),
-				 errmsg("argument of row pattern navigation operation must include at least one column reference"),
-				 parser_errposition(pstate, nav->location)));
+			DefineWalkCtx saved = *ctx;
+			bool		outer_phys = (nav->kind == RPR_NAV_PREV ||
+									  nav->kind == RPR_NAV_NEXT);
+			bool		flattened = false;
+
+			ctx->phase = DEFINE_PHASE_NAV_ARG;
+			ctx->nav_count = 0;
+			ctx->has_column_ref = false;
+			ctx->inner_kind = 0;
+			(void) define_walker((Node *) nav->arg, ctx);
+
+			if (ctx->nav_count > 0)
+			{
+				bool		inner_phys = (ctx->inner_kind == RPR_NAV_PREV ||
+										  ctx->inner_kind == RPR_NAV_NEXT);
 
-	/* Check offset_arg: column ref + volatile in one walk */
-	if (nav->offset_arg != NULL)
-	{
-		memset(&result, 0, sizeof(result));
-		(void) nav_check_walker((Node *) nav->offset_arg, &result);
-
-		if (result.has_column_ref ||
-			contain_volatile_functions((Node *) nav->offset_arg))
-			ereport(ERROR,
-					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-					 errmsg("row pattern navigation offset must be a run-time constant"),
-					 parser_errposition(pstate, nav->location)));
-	}
-}
+				if (outer_phys && !inner_phys)
+				{
+					RPRNavExpr *inner;
 
-/*
- * check_rpr_nav_nesting_walker
- *		Walk the DEFINE clause expression tree and validate each RPRNavExpr.
- */
-static bool
-check_rpr_nav_nesting_walker(Node *node, void *context)
-{
-	if (node == NULL)
-		return false;
-	if (IsA(node, RPRNavExpr))
-	{
-		check_rpr_nav_expr((RPRNavExpr *) node, (ParseState *) context);
-		/* don't recurse into arg; nesting already checked above */
-		return false;
+					/* Reject triple-or-deeper nesting */
+					if (ctx->nav_count > 1)
+						ereport(ERROR,
+								(errcode(ERRCODE_SYNTAX_ERROR),
+								 errmsg("cannot nest row pattern navigation more than two levels deep"),
+								 errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
+								 parser_errposition(ctx->pstate, nav->location)));
+
+					if (!IsA(nav->arg, RPRNavExpr))
+						ereport(ERROR,
+								(errcode(ERRCODE_SYNTAX_ERROR),
+								 errmsg("row pattern navigation operation must be a direct argument of the outer navigation"),
+								 errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
+								 parser_errposition(ctx->pstate, nav->location)));
+
+					inner = (RPRNavExpr *) nav->arg;
+
+					if (nav->kind == RPR_NAV_PREV && inner->kind == RPR_NAV_FIRST)
+						nav->kind = RPR_NAV_PREV_FIRST;
+					else if (nav->kind == RPR_NAV_PREV && inner->kind == RPR_NAV_LAST)
+						nav->kind = RPR_NAV_PREV_LAST;
+					else if (nav->kind == RPR_NAV_NEXT && inner->kind == RPR_NAV_FIRST)
+						nav->kind = RPR_NAV_NEXT_FIRST;
+					else if (nav->kind == RPR_NAV_NEXT && inner->kind == RPR_NAV_LAST)
+						nav->kind = RPR_NAV_NEXT_LAST;
+
+					nav->compound_offset_arg = nav->offset_arg;
+					nav->offset_arg = inner->offset_arg;
+					nav->arg = inner->arg;
+					flattened = true;
+				}
+				else if (!outer_phys && inner_phys)
+					ereport(ERROR,
+							(errcode(ERRCODE_SYNTAX_ERROR),
+							 errmsg("FIRST and LAST cannot contain PREV or NEXT"),
+							 errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
+							 parser_errposition(ctx->pstate, nav->location)));
+				else if (outer_phys && inner_phys)
+					ereport(ERROR,
+							(errcode(ERRCODE_SYNTAX_ERROR),
+							 errmsg("PREV and NEXT cannot contain PREV or NEXT"),
+							 errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
+							 parser_errposition(ctx->pstate, nav->location)));
+				else
+					ereport(ERROR,
+							(errcode(ERRCODE_SYNTAX_ERROR),
+							 errmsg("FIRST and LAST cannot contain FIRST or LAST"),
+							 errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
+							 parser_errposition(ctx->pstate, nav->location)));
+			}
+			else if (!ctx->has_column_ref)
+			{
+				ereport(ERROR,
+						(errcode(ERRCODE_SYNTAX_ERROR),
+						 errmsg("argument of row pattern navigation operation must include at least one column reference"),
+						 parser_errposition(ctx->pstate, nav->location)));
+			}
+
+			/*
+			 * Walk offset arg(s) in PHASE_NAV_OFFSET to enforce the
+			 * constant-offset rule.  For compound forms, both the inner
+			 * (post-flatten nav->offset_arg) and outer (compound_offset_arg)
+			 * offsets must be constants; the inner's column-ref status was
+			 * not separately tracked during the PHASE_NAV_ARG walk (which
+			 * only checks that nav.arg as a whole has at least one Var), so
+			 * it is re-walked here to catch column references the inner
+			 * offset would have leaked.
+			 */
+			ctx->phase = DEFINE_PHASE_NAV_OFFSET;
+
+			if (nav->offset_arg != NULL)
+			{
+				ctx->has_column_ref = false;
+				(void) define_walker((Node *) nav->offset_arg, ctx);
+				if (ctx->has_column_ref)
+					ereport(ERROR,
+							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+							 errmsg("row pattern navigation offset must be a run-time constant"),
+							 parser_errposition(ctx->pstate, exprLocation((Node *) nav->offset_arg))));
+			}
+			if (flattened && nav->compound_offset_arg != NULL)
+			{
+				ctx->has_column_ref = false;
+				(void) define_walker((Node *) nav->compound_offset_arg, ctx);
+				if (ctx->has_column_ref)
+					ereport(ERROR,
+							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+							 errmsg("row pattern navigation offset must be a run-time constant"),
+							 parser_errposition(ctx->pstate, exprLocation((Node *) nav->compound_offset_arg))));
+			}
+
+			*ctx = saved;
+			return false;
+		}
 	}
-	return expression_tree_walker(node, check_rpr_nav_nesting_walker, context);
+
+	return expression_tree_walker(node, define_walker, ctx);
 }
diff --git a/src/include/optimizer/rpr.h b/src/include/optimizer/rpr.h
index 0a14cfad79b..63c4b09daff 100644
--- a/src/include/optimizer/rpr.h
+++ b/src/include/optimizer/rpr.h
@@ -62,4 +62,26 @@ extern RPRPattern *buildRPRPattern(RPRPatternNode *pattern, List *defineVariable
 								   RPSkipTo rpSkipTo, int frameOptions,
 								   bool hasMatchStartDependent);
 
+/*
+ * Shared traversal walker for DEFINE clause RPRNavExpr collection.
+ *
+ * Both planner (nav-offset / match_start dependency analysis) and executor
+ * (runtime offset evaluation) need to walk DEFINE expressions and dispatch
+ * per RPRNavExpr.  They differ only in what they do at each nav node, so
+ * the traversal frame is shared (nav_traversal_walker, defined in rpr.c)
+ * and the per-nav action is supplied as a callback.  The driver allocates
+ * a mode-specific context, points NavTraversal.data at it, and casts
+ * inside its visitor.
+ */
+struct NavTraversal;
+typedef void (*NavVisitFn) (struct NavTraversal *t, RPRNavExpr *nav);
+
+typedef struct NavTraversal
+{
+	NavVisitFn	visit;
+	void	   *data;			/* mode-specific context */
+} NavTraversal;
+
+extern bool nav_traversal_walker(Node *node, void *ctx);
+
 #endif							/* OPTIMIZER_RPR_H */
diff --git a/src/test/regress/expected/rpr.out b/src/test/regress/expected/rpr.out
index 85384f6b096..8793dda3cc3 100644
--- a/src/test/regress/expected/rpr.out
+++ b/src/test/regress/expected/rpr.out
@@ -1144,7 +1144,31 @@ WINDOW w AS (
 );
 ERROR:  row pattern navigation offset must be a run-time constant
 LINE 7:     DEFINE A AS PREV(price, price) > 0
-                        ^
+                                    ^
+-- Non-constant offset: column reference in compound inner offset
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS PREV(LAST(price, price), 2) > 0
+);
+ERROR:  row pattern navigation offset must be a run-time constant
+LINE 7:     DEFINE A AS PREV(LAST(price, price), 2) > 0
+                                         ^
+-- Non-constant offset: column reference in compound outer offset
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS PREV(LAST(price, 1), price) > 0
+);
+ERROR:  row pattern navigation offset must be a run-time constant
+LINE 7:     DEFINE A AS PREV(LAST(price, 1), price) > 0
+                                             ^
 -- Non-constant offset: volatile function as offset
 SELECT price FROM stock
 WINDOW w AS (
@@ -1154,9 +1178,9 @@ WINDOW w AS (
     PATTERN (A)
     DEFINE A AS PREV(price, random()::int) > 0
 );
-ERROR:  row pattern navigation offset must be a run-time constant
+ERROR:  volatile functions are not allowed in DEFINE clause
 LINE 7:     DEFINE A AS PREV(price, random()::int) > 0
-                        ^
+                                    ^
 -- Non-constant offset: subquery as offset
 SELECT price FROM stock
 WINDOW w AS (
@@ -1181,7 +1205,7 @@ WINDOW w AS (
 ERROR:  cannot use subquery in DEFINE expression
 LINE 7:     DEFINE A AS PREV(price + (SELECT 1)) > 0
                                      ^
--- First arg: volatile function is allowed (evaluated on target row)
+-- Volatile function inside nav.arg is rejected at parse time
 SELECT company, tdate, price,
        first_value(price) OVER w, last_value(price) OVER w, count(*) OVER w
 FROM stock
@@ -1191,30 +1215,24 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS PREV(price + random() * 0) >= 0
 );
- company  |   tdate    | price | first_value | last_value | count 
-----------+------------+-------+-------------+------------+-------
- company1 | 07-01-2023 |   100 |             |            |     0
- company1 | 07-02-2023 |   200 |         200 |        130 |     9
- company1 | 07-03-2023 |   150 |             |            |     0
- company1 | 07-04-2023 |   140 |             |            |     0
- company1 | 07-05-2023 |   150 |             |            |     0
- company1 | 07-06-2023 |    90 |             |            |     0
- company1 | 07-07-2023 |   110 |             |            |     0
- company1 | 07-08-2023 |   130 |             |            |     0
- company1 | 07-09-2023 |   120 |             |            |     0
- company1 | 07-10-2023 |   130 |             |            |     0
- company2 | 07-01-2023 |    50 |             |            |     0
- company2 | 07-02-2023 |  2000 |        2000 |       1300 |     9
- company2 | 07-03-2023 |  1500 |             |            |     0
- company2 | 07-04-2023 |  1400 |             |            |     0
- company2 | 07-05-2023 |  1500 |             |            |     0
- company2 | 07-06-2023 |    60 |             |            |     0
- company2 | 07-07-2023 |  1100 |             |            |     0
- company2 | 07-08-2023 |  1300 |             |            |     0
- company2 | 07-09-2023 |  1200 |             |            |     0
- company2 | 07-10-2023 |  1300 |             |            |     0
-(20 rows)
-
+ERROR:  volatile functions are not allowed in DEFINE clause
+LINE 8:     DEFINE A AS PREV(price + random() * 0) >= 0
+                                     ^
+-- nextval is volatile (per pg_proc), so it is rejected via the FuncExpr
+-- path with the "volatile functions" message
+CREATE SEQUENCE rpr_seq;
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS price > nextval('rpr_seq')
+);
+ERROR:  volatile functions are not allowed in DEFINE clause
+LINE 7:     DEFINE A AS price > nextval('rpr_seq')
+                                ^
+DROP SEQUENCE rpr_seq;
 --
 -- 2-arg PREV/NEXT: functional tests
 --
diff --git a/src/test/regress/expected/rpr_explain.out b/src/test/regress/expected/rpr_explain.out
index dc3522f930f..0a049d1beba 100644
--- a/src/test/regress/expected/rpr_explain.out
+++ b/src/test/regress/expected/rpr_explain.out
@@ -4744,9 +4744,8 @@ WINDOW w AS (
    ->  Function Scan on generate_series s
 (5 rows)
 
--- Compound NEXT(FIRST(val, N), M): constant lookahead overflow -> no trim impact
--- N + M overflows int64, but target is forward from match_start so it never
--- constrains trim.  Lookahead remains at default (0).
+-- Compound NEXT(FIRST(val, N), M): constant lookahead overflow -> infinite
+-- N + M overflows int64; forward reach is unbounded, displayed as infinite.
 EXPLAIN (COSTS OFF) SELECT count(*) OVER w
 FROM generate_series(1,10) s(v)
 WINDOW w AS (
@@ -4760,7 +4759,7 @@ WINDOW w AS (
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a+
    Nav Mark Lookback: 0
-   Nav Mark Lookahead: 0
+   Nav Mark Lookahead: infinite
    ->  Function Scan on generate_series s
 (6 rows)
 
@@ -4803,7 +4802,7 @@ EXPLAIN (COSTS OFF, ANALYZE, TIMING OFF, SUMMARY OFF)
 
 RESET plan_cache_mode;
 DEALLOCATE test_overflow_lookback;
--- Compound NEXT(FIRST(val, $1), $2): parameter lookahead overflow -> no trim impact
+-- Compound NEXT(FIRST(val, $1), $2): parameter lookahead overflow -> infinite
 PREPARE test_overflow_lookahead(int8, int8) AS
 SELECT count(*) OVER w
 FROM generate_series(1,10) s(v)
@@ -4821,7 +4820,7 @@ EXPLAIN (COSTS OFF, ANALYZE, TIMING OFF, SUMMARY OFF)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    Pattern: a+
    Nav Mark Lookback: 0
-   Nav Mark Lookahead: 0
+   Nav Mark Lookahead: infinite
    Storage: Memory  Maximum Storage: 17kB
    NFA States: 1 peak, 11 total, 0 merged
    NFA Contexts: 2 peak, 11 total, 10 pruned
diff --git a/src/test/regress/expected/rpr_integration.out b/src/test/regress/expected/rpr_integration.out
index ef6a157f45d..905bd3538de 100644
--- a/src/test/regress/expected/rpr_integration.out
+++ b/src/test/regress/expected/rpr_integration.out
@@ -1406,23 +1406,12 @@ DROP INDEX rpr_integ_id_idx;
 -- ============================================================
 -- B9. RPR + Volatile function in DEFINE
 -- ============================================================
--- Records the current behaviour: DEFINE today accepts volatile
--- functions such as random() and the query runs to completion.
--- To keep the expected output deterministic the predicate uses
--- "random() >= 0.0", which is structurally equivalent to TRUE and
--- therefore does not perturb the match result.  The interesting
--- property is that volatile invocation does not crash or short-
--- circuit pattern matching.
---
--- XXX: volatile functions in DEFINE are slated to be rejected at
--- parse time.  Under RPR's NFA engine the same row's DEFINE
--- predicate may be evaluated multiple times (backtracking,
--- PREV/NEXT navigation), so a truly volatile result would make
--- pattern matching non-deterministic.  When the prohibition lands,
--- this test must be replaced with an error-case test that expects
--- random() in DEFINE to be rejected.
+-- Volatile functions in DEFINE are rejected at parse time.  Under
+-- RPR's NFA engine the same row's DEFINE predicate may be evaluated
+-- multiple times (backtracking, PREV/NEXT navigation), so a volatile
+-- result would make pattern matching non-deterministic.  STABLE and
+-- IMMUTABLE callees are accepted.
 -- Baseline: STABLE (to_char) and IMMUTABLE (length) callees are accepted.
--- This locks the boundary of the volatile-only prohibition.
 SELECT id, val, count(*) OVER w AS cnt
 FROM rpr_integ
 WINDOW w AS (ORDER BY id
@@ -1446,7 +1435,7 @@ ORDER BY id;
  10 |  45 |   0
 (10 rows)
 
--- Volatile (random) is the prohibition target; today still accepted.
+-- Volatile (random) is rejected.
 SELECT id, val, count(*) OVER w AS cnt
 FROM rpr_integ
 WINDOW w AS (ORDER BY id
@@ -1454,20 +1443,9 @@ WINDOW w AS (ORDER BY id
     PATTERN (A B+)
     DEFINE B AS val > PREV(val) AND random() >= 0.0)
 ORDER BY id;
- id | val | cnt 
-----+-----+-----
-  1 |  10 |   2
-  2 |  20 |   0
-  3 |  15 |   2
-  4 |  25 |   0
-  5 |   5 |   3
-  6 |  30 |   0
-  7 |  35 |   0
-  8 |  20 |   3
-  9 |  40 |   0
- 10 |  45 |   0
-(10 rows)
-
+ERROR:  volatile functions are not allowed in DEFINE clause
+LINE 6:     DEFINE B AS val > PREV(val) AND random() >= 0.0)
+                                            ^
 -- ============================================================
 -- B10. RPR + Correlated subquery in WHERE
 -- ============================================================
diff --git a/src/test/regress/sql/rpr.sql b/src/test/regress/sql/rpr.sql
index 5563e062cde..e4790f75b0a 100644
--- a/src/test/regress/sql/rpr.sql
+++ b/src/test/regress/sql/rpr.sql
@@ -541,6 +541,26 @@ WINDOW w AS (
     DEFINE A AS PREV(price, price) > 0
 );
 
+-- Non-constant offset: column reference in compound inner offset
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS PREV(LAST(price, price), 2) > 0
+);
+
+-- Non-constant offset: column reference in compound outer offset
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS PREV(LAST(price, 1), price) > 0
+);
+
 -- Non-constant offset: volatile function as offset
 SELECT price FROM stock
 WINDOW w AS (
@@ -571,7 +591,7 @@ WINDOW w AS (
     DEFINE A AS PREV(price + (SELECT 1)) > 0
 );
 
--- First arg: volatile function is allowed (evaluated on target row)
+-- Volatile function inside nav.arg is rejected at parse time
 SELECT company, tdate, price,
        first_value(price) OVER w, last_value(price) OVER w, count(*) OVER w
 FROM stock
@@ -582,6 +602,19 @@ WINDOW w AS (
     DEFINE A AS PREV(price + random() * 0) >= 0
 );
 
+-- nextval is volatile (per pg_proc), so it is rejected via the FuncExpr
+-- path with the "volatile functions" message
+CREATE SEQUENCE rpr_seq;
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS price > nextval('rpr_seq')
+);
+DROP SEQUENCE rpr_seq;
+
 --
 -- 2-arg PREV/NEXT: functional tests
 --
diff --git a/src/test/regress/sql/rpr_explain.sql b/src/test/regress/sql/rpr_explain.sql
index a3789e92631..e123be60aea 100644
--- a/src/test/regress/sql/rpr_explain.sql
+++ b/src/test/regress/sql/rpr_explain.sql
@@ -2699,9 +2699,8 @@ WINDOW w AS (
     DEFINE A AS PREV(LAST(v, 4611686018427387904), 4611686018427387904) IS NOT NULL
 );
 
--- Compound NEXT(FIRST(val, N), M): constant lookahead overflow -> no trim impact
--- N + M overflows int64, but target is forward from match_start so it never
--- constrains trim.  Lookahead remains at default (0).
+-- Compound NEXT(FIRST(val, N), M): constant lookahead overflow -> infinite
+-- N + M overflows int64; forward reach is unbounded, displayed as infinite.
 EXPLAIN (COSTS OFF) SELECT count(*) OVER w
 FROM generate_series(1,10) s(v)
 WINDOW w AS (
@@ -2728,7 +2727,7 @@ EXPLAIN (COSTS OFF, ANALYZE, TIMING OFF, SUMMARY OFF)
 RESET plan_cache_mode;
 DEALLOCATE test_overflow_lookback;
 
--- Compound NEXT(FIRST(val, $1), $2): parameter lookahead overflow -> no trim impact
+-- Compound NEXT(FIRST(val, $1), $2): parameter lookahead overflow -> infinite
 PREPARE test_overflow_lookahead(int8, int8) AS
 SELECT count(*) OVER w
 FROM generate_series(1,10) s(v)
diff --git a/src/test/regress/sql/rpr_integration.sql b/src/test/regress/sql/rpr_integration.sql
index d9748979d54..29b2db2f7bb 100644
--- a/src/test/regress/sql/rpr_integration.sql
+++ b/src/test/regress/sql/rpr_integration.sql
@@ -868,24 +868,13 @@ DROP INDEX rpr_integ_id_idx;
 -- ============================================================
 -- B9. RPR + Volatile function in DEFINE
 -- ============================================================
--- Records the current behaviour: DEFINE today accepts volatile
--- functions such as random() and the query runs to completion.
--- To keep the expected output deterministic the predicate uses
--- "random() >= 0.0", which is structurally equivalent to TRUE and
--- therefore does not perturb the match result.  The interesting
--- property is that volatile invocation does not crash or short-
--- circuit pattern matching.
---
--- XXX: volatile functions in DEFINE are slated to be rejected at
--- parse time.  Under RPR's NFA engine the same row's DEFINE
--- predicate may be evaluated multiple times (backtracking,
--- PREV/NEXT navigation), so a truly volatile result would make
--- pattern matching non-deterministic.  When the prohibition lands,
--- this test must be replaced with an error-case test that expects
--- random() in DEFINE to be rejected.
+-- Volatile functions in DEFINE are rejected at parse time.  Under
+-- RPR's NFA engine the same row's DEFINE predicate may be evaluated
+-- multiple times (backtracking, PREV/NEXT navigation), so a volatile
+-- result would make pattern matching non-deterministic.  STABLE and
+-- IMMUTABLE callees are accepted.
 
 -- Baseline: STABLE (to_char) and IMMUTABLE (length) callees are accepted.
--- This locks the boundary of the volatile-only prohibition.
 SELECT id, val, count(*) OVER w AS cnt
 FROM rpr_integ
 WINDOW w AS (ORDER BY id
@@ -896,7 +885,7 @@ WINDOW w AS (ORDER BY id
                 AND to_char(date '2026-01-01', 'YYYY') = '2026')
 ORDER BY id;
 
--- Volatile (random) is the prohibition target; today still accepted.
+-- Volatile (random) is rejected.
 SELECT id, val, count(*) OVER w AS cnt
 FROM rpr_integ
 WINDOW w AS (ORDER BY id
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index ce587068bca..1970ca5da14 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -662,7 +662,10 @@ DecodingWorkerShared
 DefElem
 DefElemAction
 DefaultACLInfo
+DefineMetadataContext
+DefinePhase
 DefineStmt
+DefineWalkCtx
 DefnDumperPtr
 DeleteStmt
 DependenciesParseState
@@ -761,8 +764,7 @@ ErrorData
 ErrorSaveContext
 EstimateDSMForeignScan_function
 EstimationInfo
-EvalNavFirstContext
-EvalNavMaxContext
+EvalDefineOffsetsContext
 EventTriggerCacheEntry
 EventTriggerCacheItem
 EventTriggerCacheStateType
@@ -1833,8 +1835,8 @@ NamedLWLockTrancheRequest
 NamedTuplestoreScan
 NamedTuplestoreScanState
 NamespaceInfo
-NavCheckResult
-NavOffsetContext
+NavTraversal
+NavVisitFn
 NestLoop
 NestLoopParam
 NestLoopState
-- 
2.50.1 (Apple Git-155)


From d9fc4c58303b6d474168f3168451c2c2e973a848 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Tue, 5 May 2026 11:44:45 +0900
Subject: [PATCH 05/68] Sync stale comments on DEFINE/PATTERN handling

validateRPRPatternVarCount() validates DEFINE names against the
PATTERN list and rejects any not used in PATTERN; the surrounding
comments said it "collected" them.  Align the function header,
inline block comment, and the call-site comment.  Also fix the
matching SELECT doc ("filtered during planning" -> "rejected with
an error") and the XXX note's wording.
---
 doc/src/sgml/ref/select.sgml   |  4 ++--
 src/backend/parser/parse_rpr.c | 28 ++++++++++++++--------------
 2 files changed, 16 insertions(+), 16 deletions(-)

diff --git a/doc/src/sgml/ref/select.sgml b/doc/src/sgml/ref/select.sgml
index 5272d6c0bfa..e4708331439 100644
--- a/doc/src/sgml/ref/select.sgml
+++ b/doc/src/sgml/ref/select.sgml
@@ -1192,8 +1192,8 @@ DEFINE <replaceable class="parameter">definition_variable_name</replaceable> AS
 </synopsis>
 
     Conversely, variables defined in the <literal>DEFINE</literal> clause
-    but not used in the <literal>PATTERN</literal> clause are filtered out
-    during query planning.
+    but not used in the <literal>PATTERN</literal> clause are rejected
+    with an error.
    </para>
 
    <para>
diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index 87411abcbe2..2c6fccebd47 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -183,11 +183,11 @@ transformRPR(ParseState *pstate, WindowClause *wc, WindowDef *windef,
  * Recursively traverses the pattern tree, collecting unique variable names.
  * Throws an error if the number of unique variables exceeds RPR_VARID_MAX.
  *
- * If rpDefs is non-NULL, DEFINE variable names are also collected into
- * varNames so that transformColumnRef can distinguish pattern variable
- * qualifiers from FROM-clause range variables.
- *
- * varNames is both input and output: existing names are preserved, new ones added.
+ * If rpDefs is non-NULL, each DEFINE variable name is also validated against
+ * varNames; any DEFINE name not present in PATTERN is rejected with an error.
+ * varNames itself is not extended by this step -- it carries only PATTERN
+ * variable names, which is what transformColumnRef checks via
+ * p_rpr_pattern_vars to identify pattern variable qualifiers.
  */
 static void
 validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
@@ -244,10 +244,10 @@ validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
 	}
 
 	/*
-	 * After the top-level call, also collect DEFINE variable names that are
-	 * not already in the list.  This is only done once at the outermost
-	 * recursion level, detected by rpDefs being non-NULL (recursive calls
-	 * pass NULL).
+	 * After the top-level call, validate that every DEFINE variable name is
+	 * present in the PATTERN variable list; reject names not used in PATTERN.
+	 * This is only done once at the outermost recursion level, detected by
+	 * rpDefs being non-NULL (recursive calls pass NULL).
 	 */
 	if (rpDefs)
 	{
@@ -293,9 +293,9 @@ validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
  * Note: Variables not in DEFINE are evaluated as TRUE by the executor.
  * Variables in DEFINE but not in PATTERN are rejected as an error.
  *
- * XXX Pattern variable qualified column references in DEFINE (e.g.
- * "A.price") are not yet supported.  Currently rejected by
- * transformColumnRef in parse_expr.c via the p_rpr_pattern_vars check.
+ * XXX Pattern variable qualified expressions in DEFINE (e.g. "A.price")
+ * are not yet supported.  Currently rejected by transformColumnRef in
+ * parse_expr.c via the p_rpr_pattern_vars check.
  */
 static List *
 transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
@@ -317,8 +317,8 @@ transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 	Assert(windef->rpCommonSyntax->rpDefs != NULL);
 
 	/*
-	 * Validate PATTERN variable count and collect all RPR variable names
-	 * (PATTERN + DEFINE) for use in transformColumnRef.
+	 * Validate PATTERN variable count, reject DEFINE variables not used in
+	 * PATTERN, and collect PATTERN variable names for transformColumnRef.
 	 */
 	validateRPRPatternVarCount(pstate, windef->rpCommonSyntax->rpPattern,
 							   windef->rpCommonSyntax->rpDefs,
-- 
2.50.1 (Apple Git-155)


From 31b21e064270f94a8ec27305ea47740ad5b62382 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Thu, 7 May 2026 14:29:14 +0900
Subject: [PATCH 03/68] Cover RPR empty-match path with EXPLAIN tests; fix
 stale XXX comments

The test_728_* cases in rpr_nfa.sql claimed (via XXX) that the visited
bitmap blocks empty iterations.  In fact the NFA finds the empty
matches; window aggregates just return 0 / NULL over the length-0
frame, indistinguishable from "no match" without MATCH_NUMBER().

Replace the XXX comments with the actual behavior, and add paired
EXPLAIN ANALYZE tests in rpr_explain.sql that lock in
"NFA: 3 matched (len 0/0/0.0)" as observable regression coverage.
---
 src/test/regress/expected/rpr_explain.out | 200 ++++++++++++++++++++++
 src/test/regress/expected/rpr_nfa.out     |  29 ++--
 src/test/regress/sql/rpr_explain.sql      | 116 +++++++++++++
 src/test/regress/sql/rpr_nfa.sql          |  29 ++--
 4 files changed, 340 insertions(+), 34 deletions(-)

diff --git a/src/test/regress/expected/rpr_explain.out b/src/test/regress/expected/rpr_explain.out
index 0a049d1beba..c4516d3c756 100644
--- a/src/test/regress/expected/rpr_explain.out
+++ b/src/test/regress/expected/rpr_explain.out
@@ -2102,6 +2102,206 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=0.00 loops=1)
 (5 rows)
 
+-- Empty matches (length 0): mirror the test_728_* cases in rpr_nfa.sql.
+-- Window aggregates over a length-0 frame return 0 / NULL, so the SELECT
+-- result alone cannot distinguish "no match" from "empty match"; the
+-- "NFA: N matched (len 0/0/0.0)" line in EXPLAIN is the only observable
+-- proof that the empty matches were found.
+-- (A?){0,3}: min=0, A never matches -> 3 length-0 matches
+CREATE VIEW rpr_ev_edge_empty_match_min0 AS
+SELECT count(*) OVER w
+FROM generate_series(1, 3) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A?){0,3})
+    DEFINE A AS FALSE
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_edge_empty_match_min0'), E'\n')) AS line WHERE line ~ 'PATTERN';
+          line          
+------------------------
+   PATTERN ((a?){0,3}) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 3) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A?){0,3})
+    DEFINE A AS FALSE
+);');
+                         rpr_explain_filter                          
+---------------------------------------------------------------------
+ WindowAgg (actual rows=3.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a?){0,3}
+   Nav Mark Lookback: 0
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 6 peak, 20 total, 4 merged
+   NFA Contexts: 2 peak, 4 total, 0 pruned
+   NFA: 3 matched (len 0/0/0.0), 0 mismatched
+   ->  Function Scan on generate_series s (actual rows=3.00 loops=1)
+(9 rows)
+
+-- (A?){1,3}: min=1, one empty iteration satisfies min -> 3 length-0 matches
+CREATE VIEW rpr_ev_edge_empty_match_min1 AS
+SELECT count(*) OVER w
+FROM generate_series(1, 3) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A?){1,3})
+    DEFINE A AS FALSE
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_edge_empty_match_min1'), E'\n')) AS line WHERE line ~ 'PATTERN';
+          line          
+------------------------
+   PATTERN ((a?){1,3}) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 3) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A?){1,3})
+    DEFINE A AS FALSE
+);');
+                         rpr_explain_filter                          
+---------------------------------------------------------------------
+ WindowAgg (actual rows=3.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a?){1,3}
+   Nav Mark Lookback: 0
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 5 peak, 16 total, 4 merged
+   NFA Contexts: 2 peak, 4 total, 0 pruned
+   NFA: 3 matched (len 0/0/0.0), 0 mismatched
+   ->  Function Scan on generate_series s (actual rows=3.00 loops=1)
+(9 rows)
+
+-- (A?){2,3}: min=2 (SQL:2016 STR06 = STRE STRE) -> 3 length-0 matches
+CREATE VIEW rpr_ev_edge_empty_match_min2 AS
+SELECT count(*) OVER w
+FROM generate_series(1, 3) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A?){2,3})
+    DEFINE A AS FALSE
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_edge_empty_match_min2'), E'\n')) AS line WHERE line ~ 'PATTERN';
+          line          
+------------------------
+   PATTERN ((a?){2,3}) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 3) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A?){2,3})
+    DEFINE A AS FALSE
+);');
+                         rpr_explain_filter                          
+---------------------------------------------------------------------
+ WindowAgg (actual rows=3.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a?){2,3}
+   Nav Mark Lookback: 0
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 5 peak, 16 total, 4 merged
+   NFA Contexts: 2 peak, 4 total, 0 pruned
+   NFA: 3 matched (len 0/0/0.0), 0 mismatched
+   ->  Function Scan on generate_series s (actual rows=3.00 loops=1)
+(9 rows)
+
+-- (A?){2,3} mixed: rows 1-2 match A (real), rows 3-4 fall back to empty
+CREATE VIEW rpr_ev_edge_empty_match_mixed AS
+SELECT count(*) OVER w
+FROM generate_series(1, 4) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A?){2,3})
+    DEFINE A AS v <= 2
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_edge_empty_match_mixed'), E'\n')) AS line WHERE line ~ 'PATTERN';
+          line          
+------------------------
+   PATTERN ((a?){2,3}) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 4) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A?){2,3})
+    DEFINE A AS v <= 2
+);');
+                         rpr_explain_filter                          
+---------------------------------------------------------------------
+ WindowAgg (actual rows=4.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a?){2,3}
+   Nav Mark Lookback: 0
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 8 peak, 26 total, 5 merged
+   NFA Contexts: 4 peak, 5 total, 1 pruned
+   NFA: 3 matched (len 0/2/1.0), 0 mismatched
+   ->  Function Scan on generate_series s (actual rows=4.00 loops=1)
+(9 rows)
+
+-- (A? B?){2,3}: pure empty multi-element body -> 3 length-0 matches
+CREATE VIEW rpr_ev_edge_empty_match_multi AS
+SELECT count(*) OVER w
+FROM generate_series(1, 3) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A? B?){2,3})
+    DEFINE A AS FALSE, B AS FALSE
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_edge_empty_match_multi'), E'\n')) AS line WHERE line ~ 'PATTERN';
+           line            
+---------------------------
+   PATTERN ((a? b?){2,3}) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 3) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A? B?){2,3})
+    DEFINE A AS FALSE, B AS FALSE
+);');
+                         rpr_explain_filter                          
+---------------------------------------------------------------------
+ WindowAgg (actual rows=3.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a? b?){2,3}
+   Nav Mark Lookback: 0
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 6 peak, 20 total, 0 merged
+   NFA Contexts: 2 peak, 4 total, 0 pruned
+   NFA: 3 matched (len 0/0/0.0), 0 mismatched
+   ->  Function Scan on generate_series s (actual rows=3.00 loops=1)
+(9 rows)
+
 -- Single row
 CREATE VIEW rpr_ev_edge_single_row AS
 SELECT count(*) OVER w
diff --git a/src/test/regress/expected/rpr_nfa.out b/src/test/regress/expected/rpr_nfa.out
index a19b26c3b94..4cff7cfbbd7 100644
--- a/src/test/regress/expected/rpr_nfa.out
+++ b/src/test/regress/expected/rpr_nfa.out
@@ -4395,9 +4395,9 @@ WINDOW w AS (
 (3 rows)
 
 -- (A?){0,3}: min=0, nullable inner.
--- A never matches. A? matches empty, min=0 satisfied immediately.
--- Per standard: empty match expected for every row.
--- XXX: visited bitmap blocks empty iteration -> no match (same as {2,3})
+-- A never matches but A? matches empty, satisfying min=0 immediately.
+-- NFA reports 3 length-0 matches (one per row); first_value / last_value
+-- are NULL because the window frame for an empty match has no rows.
 WITH test_728_min0 AS (
     SELECT * FROM (VALUES
         (1, ARRAY['B']),
@@ -4425,9 +4425,8 @@ WINDOW w AS (
 (3 rows)
 
 -- (A?){1,3}: min=1, nullable inner.
--- A never matches. Need 1 empty iteration to satisfy min=1.
--- Per standard: empty match expected for every row.
--- XXX: visited bitmap blocks empty iteration -> no match (same as {2,3})
+-- A never matches; one empty iteration satisfies min=1.
+-- NFA reports 3 length-0 matches; first/last_value NULL over empty frame.
 WITH test_728_min1 AS (
     SELECT * FROM (VALUES
         (1, ARRAY['B']),
@@ -4454,11 +4453,9 @@ WINDOW w AS (
   3 | {B}   |             |          
 (3 rows)
 
--- (A?){2,3}: min=2, nullable inner.
--- A never matches. Need 2 empty iterations to satisfy min=2.
--- Per standard: STR06=(STRE STRE) is valid for min=2.
--- Expected: empty match for every row
--- XXX: visited bitmap blocks second empty iteration -> match failure
+-- (A?){2,3}: min=2, nullable inner.  Per SQL:2016 STR06 = (STRE STRE)
+-- is valid: two empty iterations satisfy min=2.
+-- NFA reports 3 length-0 matches; first/last_value NULL over empty frame.
 WITH test_728_min2 AS (
     SELECT * FROM (VALUES
         (1, ARRAY['B']),
@@ -4486,9 +4483,8 @@ WINDOW w AS (
 (3 rows)
 
 -- (A?){2,3} mixed: some rows match A, some don't
--- Rows 1-2: A matches, greedy takes 2 -> min satisfied
--- Row 3: A doesn't match, needs 2 empty iterations for min=2
--- XXX: Row 3 fails due to visited bitmap (same as pure empty {2,3})
+-- Rows 1-2: A matches, greedy takes 2 -> min satisfied (real match)
+-- Row 3: A doesn't match, two empty iterations satisfy min=2 (length-0 match)
 -- Row 4: A matches 1 real iter + 1 ff empty exit -> match 4-4
 WITH test_728_min2_mixed AS (
     SELECT * FROM (VALUES
@@ -4559,9 +4555,8 @@ WINDOW w AS (
   6 | {B}   |           6 |         6
 (6 rows)
 
--- (A? B?){2,3}: pure empty body (nothing matches)
--- XXX: All NULL: same issue as test_728_min2 (empty match at context
--- start yields UNMATCHED via startPos-1 initial advance)
+-- (A? B?){2,3}: pure empty body (nothing matches A or B).
+-- NFA reports 3 length-0 matches; first/last_value NULL over empty frame.
 WITH test_728_multi_empty AS (
     SELECT * FROM (VALUES
         (1, ARRAY['C']),
diff --git a/src/test/regress/sql/rpr_explain.sql b/src/test/regress/sql/rpr_explain.sql
index e123be60aea..d339a80a673 100644
--- a/src/test/regress/sql/rpr_explain.sql
+++ b/src/test/regress/sql/rpr_explain.sql
@@ -1178,6 +1178,122 @@ WINDOW w AS (
     DEFINE A AS v = 1, B AS v = 2
 );');
 
+-- Empty matches (length 0): mirror the test_728_* cases in rpr_nfa.sql.
+-- Window aggregates over a length-0 frame return 0 / NULL, so the SELECT
+-- result alone cannot distinguish "no match" from "empty match"; the
+-- "NFA: N matched (len 0/0/0.0)" line in EXPLAIN is the only observable
+-- proof that the empty matches were found.
+
+-- (A?){0,3}: min=0, A never matches -> 3 length-0 matches
+CREATE VIEW rpr_ev_edge_empty_match_min0 AS
+SELECT count(*) OVER w
+FROM generate_series(1, 3) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A?){0,3})
+    DEFINE A AS FALSE
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_edge_empty_match_min0'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 3) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A?){0,3})
+    DEFINE A AS FALSE
+);');
+
+-- (A?){1,3}: min=1, one empty iteration satisfies min -> 3 length-0 matches
+CREATE VIEW rpr_ev_edge_empty_match_min1 AS
+SELECT count(*) OVER w
+FROM generate_series(1, 3) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A?){1,3})
+    DEFINE A AS FALSE
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_edge_empty_match_min1'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 3) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A?){1,3})
+    DEFINE A AS FALSE
+);');
+
+-- (A?){2,3}: min=2 (SQL:2016 STR06 = STRE STRE) -> 3 length-0 matches
+CREATE VIEW rpr_ev_edge_empty_match_min2 AS
+SELECT count(*) OVER w
+FROM generate_series(1, 3) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A?){2,3})
+    DEFINE A AS FALSE
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_edge_empty_match_min2'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 3) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A?){2,3})
+    DEFINE A AS FALSE
+);');
+
+-- (A?){2,3} mixed: rows 1-2 match A (real), rows 3-4 fall back to empty
+CREATE VIEW rpr_ev_edge_empty_match_mixed AS
+SELECT count(*) OVER w
+FROM generate_series(1, 4) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A?){2,3})
+    DEFINE A AS v <= 2
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_edge_empty_match_mixed'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 4) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A?){2,3})
+    DEFINE A AS v <= 2
+);');
+
+-- (A? B?){2,3}: pure empty multi-element body -> 3 length-0 matches
+CREATE VIEW rpr_ev_edge_empty_match_multi AS
+SELECT count(*) OVER w
+FROM generate_series(1, 3) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A? B?){2,3})
+    DEFINE A AS FALSE, B AS FALSE
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_edge_empty_match_multi'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 3) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A? B?){2,3})
+    DEFINE A AS FALSE, B AS FALSE
+);');
+
 -- Single row
 CREATE VIEW rpr_ev_edge_single_row AS
 SELECT count(*) OVER w
diff --git a/src/test/regress/sql/rpr_nfa.sql b/src/test/regress/sql/rpr_nfa.sql
index 1d27e0dc09e..29ec4a9dacb 100644
--- a/src/test/regress/sql/rpr_nfa.sql
+++ b/src/test/regress/sql/rpr_nfa.sql
@@ -3234,9 +3234,9 @@ WINDOW w AS (
 );
 
 -- (A?){0,3}: min=0, nullable inner.
--- A never matches. A? matches empty, min=0 satisfied immediately.
--- Per standard: empty match expected for every row.
--- XXX: visited bitmap blocks empty iteration -> no match (same as {2,3})
+-- A never matches but A? matches empty, satisfying min=0 immediately.
+-- NFA reports 3 length-0 matches (one per row); first_value / last_value
+-- are NULL because the window frame for an empty match has no rows.
 WITH test_728_min0 AS (
     SELECT * FROM (VALUES
         (1, ARRAY['B']),
@@ -3258,9 +3258,8 @@ WINDOW w AS (
 );
 
 -- (A?){1,3}: min=1, nullable inner.
--- A never matches. Need 1 empty iteration to satisfy min=1.
--- Per standard: empty match expected for every row.
--- XXX: visited bitmap blocks empty iteration -> no match (same as {2,3})
+-- A never matches; one empty iteration satisfies min=1.
+-- NFA reports 3 length-0 matches; first/last_value NULL over empty frame.
 WITH test_728_min1 AS (
     SELECT * FROM (VALUES
         (1, ARRAY['B']),
@@ -3281,11 +3280,9 @@ WINDOW w AS (
         A AS 'A' = ANY(flags)
 );
 
--- (A?){2,3}: min=2, nullable inner.
--- A never matches. Need 2 empty iterations to satisfy min=2.
--- Per standard: STR06=(STRE STRE) is valid for min=2.
--- Expected: empty match for every row
--- XXX: visited bitmap blocks second empty iteration -> match failure
+-- (A?){2,3}: min=2, nullable inner.  Per SQL:2016 STR06 = (STRE STRE)
+-- is valid: two empty iterations satisfy min=2.
+-- NFA reports 3 length-0 matches; first/last_value NULL over empty frame.
 WITH test_728_min2 AS (
     SELECT * FROM (VALUES
         (1, ARRAY['B']),
@@ -3307,9 +3304,8 @@ WINDOW w AS (
 );
 
 -- (A?){2,3} mixed: some rows match A, some don't
--- Rows 1-2: A matches, greedy takes 2 -> min satisfied
--- Row 3: A doesn't match, needs 2 empty iterations for min=2
--- XXX: Row 3 fails due to visited bitmap (same as pure empty {2,3})
+-- Rows 1-2: A matches, greedy takes 2 -> min satisfied (real match)
+-- Row 3: A doesn't match, two empty iterations satisfy min=2 (length-0 match)
 -- Row 4: A matches 1 real iter + 1 ff empty exit -> match 4-4
 WITH test_728_min2_mixed AS (
     SELECT * FROM (VALUES
@@ -3364,9 +3360,8 @@ WINDOW w AS (
         B AS 'B' = ANY(flags)
 );
 
--- (A? B?){2,3}: pure empty body (nothing matches)
--- XXX: All NULL: same issue as test_728_min2 (empty match at context
--- start yields UNMATCHED via startPos-1 initial advance)
+-- (A? B?){2,3}: pure empty body (nothing matches A or B).
+-- NFA reports 3 length-0 matches; first/last_value NULL over empty frame.
 WITH test_728_multi_empty AS (
     SELECT * FROM (VALUES
         (1, ARRAY['C']),
-- 
2.50.1 (Apple Git-155)


From 75ca808a06e22c08442b2fde2bf5ae18987c1ce9 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Mon, 4 May 2026 19:09:34 +0900
Subject: [PATCH 01/68] Add DEFINE non-volatile baseline to rpr_integration B9

B9 in src/test/regress/sql/rpr_integration.sql today only exercises a
volatile callee (random()) inside DEFINE.  Add a baseline query in the
same section that uses STABLE (to_char) and IMMUTABLE (length) callees,
which must remain accepted when the upcoming volatile-only prohibition
lands.  This guards against the prohibition being broadened by accident
(e.g. contain_volatile_functions -> contain_mutable_functions); the
volatile case alone would not catch over-rejection.

Ordered baseline-first then volatile, matching other B sections.
---
 src/test/regress/expected/rpr_integration.out | 26 +++++++++++++++++++
 src/test/regress/sql/rpr_integration.sql      | 13 ++++++++++
 2 files changed, 39 insertions(+)

diff --git a/src/test/regress/expected/rpr_integration.out b/src/test/regress/expected/rpr_integration.out
index 0cc79b75601..ef6a157f45d 100644
--- a/src/test/regress/expected/rpr_integration.out
+++ b/src/test/regress/expected/rpr_integration.out
@@ -1421,6 +1421,32 @@ DROP INDEX rpr_integ_id_idx;
 -- pattern matching non-deterministic.  When the prohibition lands,
 -- this test must be replaced with an error-case test that expects
 -- random() in DEFINE to be rejected.
+-- Baseline: STABLE (to_char) and IMMUTABLE (length) callees are accepted.
+-- This locks the boundary of the volatile-only prohibition.
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_integ
+WINDOW w AS (ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE B AS val > PREV(val)
+                AND length('x') = 1
+                AND to_char(date '2026-01-01', 'YYYY') = '2026')
+ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   2
+  2 |  20 |   0
+  3 |  15 |   2
+  4 |  25 |   0
+  5 |   5 |   3
+  6 |  30 |   0
+  7 |  35 |   0
+  8 |  20 |   3
+  9 |  40 |   0
+ 10 |  45 |   0
+(10 rows)
+
+-- Volatile (random) is the prohibition target; today still accepted.
 SELECT id, val, count(*) OVER w AS cnt
 FROM rpr_integ
 WINDOW w AS (ORDER BY id
diff --git a/src/test/regress/sql/rpr_integration.sql b/src/test/regress/sql/rpr_integration.sql
index 6d47728e911..d9748979d54 100644
--- a/src/test/regress/sql/rpr_integration.sql
+++ b/src/test/regress/sql/rpr_integration.sql
@@ -884,6 +884,19 @@ DROP INDEX rpr_integ_id_idx;
 -- this test must be replaced with an error-case test that expects
 -- random() in DEFINE to be rejected.
 
+-- Baseline: STABLE (to_char) and IMMUTABLE (length) callees are accepted.
+-- This locks the boundary of the volatile-only prohibition.
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_integ
+WINDOW w AS (ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE B AS val > PREV(val)
+                AND length('x') = 1
+                AND to_char(date '2026-01-01', 'YYYY') = '2026')
+ORDER BY id;
+
+-- Volatile (random) is the prohibition target; today still accepted.
 SELECT id, val, count(*) OVER w AS cnt
 FROM rpr_integ
 WINDOW w AS (ORDER BY id
-- 
2.50.1 (Apple Git-155)


From 0532539a2509adba38989fc3bf5b1a1281f251ab Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Tue, 5 May 2026 17:27:51 +0900
Subject: [PATCH 07/68] Remove optional outer parentheses from ereport() calls
 in RPR files

Per project coding style, the outer parentheses wrapping errcode/
errmsg/etc. arguments in ereport() are optional and should be omitted.
Applies to parse_rpr.c, optimizer/plan/rpr.c, and gram.y.
---
 src/backend/optimizer/plan/rpr.c |  16 ++--
 src/backend/parser/gram.y        |  72 ++++++++--------
 src/backend/parser/parse_rpr.c   | 140 +++++++++++++++----------------
 3 files changed, 114 insertions(+), 114 deletions(-)

diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index a817eb4a63f..ed8b6c3414c 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -1027,10 +1027,10 @@ scanRPRPatternRecursive(RPRPatternNode *node, char **varNames, int *numVars,
 	/* Check recursion depth limit before overflow occurs */
 	if (depth >= RPR_DEPTH_MAX)
 		ereport(ERROR,
-				(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
-				 errmsg("pattern nesting too deep"),
-				 errdetail("Pattern nesting depth %d exceeds maximum %d.",
-						   depth, RPR_DEPTH_MAX - 1)));
+				errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+				errmsg("pattern nesting too deep"),
+				errdetail("Pattern nesting depth %d exceeds maximum %d.",
+						  depth, RPR_DEPTH_MAX - 1));
 
 	/* Track maximum depth */
 	if (depth > *maxDepth)
@@ -1120,10 +1120,10 @@ scanRPRPattern(RPRPatternNode *node, char **varNames, int *numVars,
 
 	if (*numElements > RPR_ELEMIDX_MAX)
 		ereport(ERROR,
-				(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
-				 errmsg("pattern too complex"),
-				 errdetail("Pattern has %d elements, maximum is %d.",
-						   *numElements, RPR_ELEMIDX_MAX)));
+				errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+				errmsg("pattern too complex"),
+				errdetail("Pattern has %d elements, maximum is %d.",
+						  *numElements, RPR_ELEMIDX_MAX));
 }
 
 /*
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index f3cedfbbb18..aa587e6aced 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -17610,10 +17610,10 @@ opt_row_pattern_initial_or_seek:
 			| SEEK
 				{
 					ereport(ERROR,
-							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-							 errmsg("SEEK is not supported"),
-							 errhint("Use INITIAL instead."),
-							 parser_errposition(@1)));
+							errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+							errmsg("SEEK is not supported"),
+							errhint("Use INITIAL instead."),
+							parser_errposition(@1));
 				}
 			| /*EMPTY*/		{ $$ = true; }
 		;
@@ -17740,40 +17740,40 @@ row_pattern_quantifier_opt:
 						$$ = (Node *) makeRPRQuantifier(0, 1, @1 + 1, @1, yyscanner);
 					else
 						ereport(ERROR,
-								(errcode(ERRCODE_SYNTAX_ERROR),
-								 errmsg("unsupported quantifier \"%s\"", $1),
-								 errhint("Valid quantifiers are: *, +, ?, *?, +?, ??, {n}, {n,}, {,m}, {n,m} and their reluctant versions."),
-								 parser_errposition(@1)));
+								errcode(ERRCODE_SYNTAX_ERROR),
+								errmsg("unsupported quantifier \"%s\"", $1),
+								errhint("Valid quantifiers are: *, +, ?, *?, +?, ??, {n}, {n,}, {,m}, {n,m} and their reluctant versions."),
+								parser_errposition(@1));
 				}
 			/* RELUCTANT quantifiers (when lexer separates tokens) */
 			| '*' Op
 				{
 					if (strcmp($2, "?") != 0)
 						ereport(ERROR,
-								(errcode(ERRCODE_SYNTAX_ERROR),
-								 errmsg("invalid token after \"*\" quantifier"),
-								 errhint("Did you mean \"*?\" for reluctant quantifier?"),
-								 parser_errposition(@2)));
+								errcode(ERRCODE_SYNTAX_ERROR),
+								errmsg("invalid token after \"*\" quantifier"),
+								errhint("Did you mean \"*?\" for reluctant quantifier?"),
+								parser_errposition(@2));
 					$$ = (Node *) makeRPRQuantifier(0, INT_MAX, @2, @1, yyscanner);
 				}
 			| '+' Op
 				{
 					if (strcmp($2, "?") != 0)
 						ereport(ERROR,
-								(errcode(ERRCODE_SYNTAX_ERROR),
-								 errmsg("invalid token after \"+\" quantifier"),
-								 errhint("Did you mean \"+?\" for reluctant quantifier?"),
-								 parser_errposition(@2)));
+								errcode(ERRCODE_SYNTAX_ERROR),
+								errmsg("invalid token after \"+\" quantifier"),
+								errhint("Did you mean \"+?\" for reluctant quantifier?"),
+								parser_errposition(@2));
 					$$ = (Node *) makeRPRQuantifier(1, INT_MAX, @2, @1, yyscanner);
 				}
 			| Op Op
 				{
 					if (strcmp($1, "?") != 0 || strcmp($2, "?") != 0)
 						ereport(ERROR,
-								(errcode(ERRCODE_SYNTAX_ERROR),
-								 errmsg("invalid quantifier combination"),
-								 errhint("Did you mean \"??\" for reluctant quantifier?"),
-								 parser_errposition(@1)));
+								errcode(ERRCODE_SYNTAX_ERROR),
+								errmsg("invalid quantifier combination"),
+								errhint("Did you mean \"??\" for reluctant quantifier?"),
+								parser_errposition(@1));
 					$$ = (Node *) makeRPRQuantifier(0, 1, @2, @1, yyscanner);
 				}
 			/* {n}, {n,}, {,m}, {n,m} quantifiers */
@@ -17823,10 +17823,10 @@ row_pattern_quantifier_opt:
 				{
 					if (strcmp($4, "?") != 0)
 						ereport(ERROR,
-								(errcode(ERRCODE_SYNTAX_ERROR),
-								 errmsg("invalid token after range quantifier"),
-								 errhint("Only \"?\" is allowed after {n} to make it reluctant."),
-								 parser_errposition(@4)));
+								errcode(ERRCODE_SYNTAX_ERROR),
+								errmsg("invalid token after range quantifier"),
+								errhint("Only \"?\" is allowed after {n} to make it reluctant."),
+								parser_errposition(@4));
 					if ($2 <= 0 || $2 >= INT_MAX)
 						ereport(ERROR,
 								errcode(ERRCODE_SYNTAX_ERROR),
@@ -17838,10 +17838,10 @@ row_pattern_quantifier_opt:
 				{
 					if (strcmp($5, "?") != 0)
 						ereport(ERROR,
-								(errcode(ERRCODE_SYNTAX_ERROR),
-								 errmsg("invalid token after range quantifier"),
-								 errhint("Only \"?\" is allowed after {n,} or {,m} to make it reluctant."),
-								 parser_errposition(@5)));
+								errcode(ERRCODE_SYNTAX_ERROR),
+								errmsg("invalid token after range quantifier"),
+								errhint("Only \"?\" is allowed after {n,} or {,m} to make it reluctant."),
+								parser_errposition(@5));
 					if ($2 < 0 || $2 >= INT_MAX)
 						ereport(ERROR,
 								errcode(ERRCODE_SYNTAX_ERROR),
@@ -17853,10 +17853,10 @@ row_pattern_quantifier_opt:
 				{
 					if (strcmp($5, "?") != 0)
 						ereport(ERROR,
-								(errcode(ERRCODE_SYNTAX_ERROR),
-								 errmsg("invalid token after range quantifier"),
-								 errhint("Only \"?\" is allowed after {n,} or {,m} to make it reluctant."),
-								 parser_errposition(@5)));
+								errcode(ERRCODE_SYNTAX_ERROR),
+								errmsg("invalid token after range quantifier"),
+								errhint("Only \"?\" is allowed after {n,} or {,m} to make it reluctant."),
+								parser_errposition(@5));
 					if ($3 <= 0 || $3 >= INT_MAX)
 						ereport(ERROR,
 								errcode(ERRCODE_SYNTAX_ERROR),
@@ -17868,10 +17868,10 @@ row_pattern_quantifier_opt:
 				{
 					if (strcmp($6, "?") != 0)
 						ereport(ERROR,
-								(errcode(ERRCODE_SYNTAX_ERROR),
-								 errmsg("invalid token after range quantifier"),
-								 errhint("Only \"?\" is allowed after {n,m} to make it reluctant."),
-								 parser_errposition(@6)));
+								errcode(ERRCODE_SYNTAX_ERROR),
+								errmsg("invalid token after range quantifier"),
+								errhint("Only \"?\" is allowed after {n,m} to make it reluctant."),
+								parser_errposition(@6));
 					if ($2 < 0 || $4 <= 0 || $2 >= INT_MAX || $4 >= INT_MAX)
 						ereport(ERROR,
 								errcode(ERRCODE_SYNTAX_ERROR),
diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index 2c6fccebd47..bba887f17ce 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -95,20 +95,20 @@ transformRPR(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 	/* Frame type must be "ROW" */
 	if (wc->frameOptions & FRAMEOPTION_GROUPS)
 		ereport(ERROR,
-				(errcode(ERRCODE_WINDOWING_ERROR),
-				 errmsg("cannot use FRAME option GROUPS with row pattern recognition"),
-				 errhint("Use ROWS instead."),
-				 parser_errposition(pstate,
-									windef->frameLocation >= 0 ?
-									windef->frameLocation : windef->location)));
+				errcode(ERRCODE_WINDOWING_ERROR),
+				errmsg("cannot use FRAME option GROUPS with row pattern recognition"),
+				errhint("Use ROWS instead."),
+				parser_errposition(pstate,
+								   windef->frameLocation >= 0 ?
+								   windef->frameLocation : windef->location));
 	if (wc->frameOptions & FRAMEOPTION_RANGE)
 		ereport(ERROR,
-				(errcode(ERRCODE_WINDOWING_ERROR),
-				 errmsg("cannot use FRAME option RANGE with row pattern recognition"),
-				 errhint("Use ROWS instead."),
-				 parser_errposition(pstate,
-									windef->frameLocation >= 0 ?
-									windef->frameLocation : windef->location)));
+				errcode(ERRCODE_WINDOWING_ERROR),
+				errmsg("cannot use FRAME option RANGE with row pattern recognition"),
+				errhint("Use ROWS instead."),
+				parser_errposition(pstate,
+								   windef->frameLocation >= 0 ?
+								   windef->frameLocation : windef->location));
 
 	/* Frame must start at current row */
 	if ((wc->frameOptions & FRAMEOPTION_START_CURRENT_ROW) == 0)
@@ -130,11 +130,11 @@ transformRPR(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 			   (wc->frameOptions & FRAMEOPTION_START_OFFSET_FOLLOWING));
 
 		ereport(ERROR,
-				(errcode(ERRCODE_WINDOWING_ERROR),
-				 errmsg("FRAME must start at CURRENT ROW when using row pattern recognition"),
-				 errdetail("Current frame starts with %s.", startBound),
-				 errhint("Use: %s BETWEEN CURRENT ROW AND ...", frameType),
-				 parser_errposition(pstate, windef->frameLocation >= 0 ? windef->frameLocation : windef->location)));
+				errcode(ERRCODE_WINDOWING_ERROR),
+				errmsg("FRAME must start at CURRENT ROW when using row pattern recognition"),
+				errdetail("Current frame starts with %s.", startBound),
+				errhint("Use: %s BETWEEN CURRENT ROW AND ...", frameType),
+				parser_errposition(pstate, windef->frameLocation >= 0 ? windef->frameLocation : windef->location));
 	}
 
 	/* EXCLUDE options are not permitted */
@@ -156,11 +156,11 @@ transformRPR(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 			   (wc->frameOptions & FRAMEOPTION_EXCLUDE_TIES));
 
 		ereport(ERROR,
-				(errcode(ERRCODE_WINDOWING_ERROR),
-				 errmsg("cannot use EXCLUDE options with row pattern recognition"),
-				 errdetail("Frame definition includes %s.", excludeType),
-				 errhint("Remove the EXCLUDE clause from the window definition."),
-				 parser_errposition(pstate, windef->excludeLocation >= 0 ? windef->excludeLocation : windef->location)));
+				errcode(ERRCODE_WINDOWING_ERROR),
+				errmsg("cannot use EXCLUDE options with row pattern recognition"),
+				errdetail("Frame definition includes %s.", excludeType),
+				errhint("Remove the EXCLUDE clause from the window definition."),
+				parser_errposition(pstate, windef->excludeLocation >= 0 ? windef->excludeLocation : windef->location));
 	}
 
 	/* Transform AFTER MATCH SKIP TO clause */
@@ -220,11 +220,11 @@ validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
 					/* Check against RPR_VARID_MAX before adding */
 					if (list_length(*varNames) >= RPR_VARID_MAX)
 						ereport(ERROR,
-								(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
-								 errmsg("too many pattern variables"),
-								 errdetail("Maximum is %d.", RPR_VARID_MAX),
-								 parser_errposition(pstate,
-													exprLocation((Node *) node))));
+								errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+								errmsg("too many pattern variables"),
+								errdetail("Maximum is %d.", RPR_VARID_MAX),
+								parser_errposition(pstate,
+												   exprLocation((Node *) node)));
 
 					*varNames = lappend(*varNames, makeString(pstrdup(node->varName)));
 				}
@@ -267,10 +267,10 @@ validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
 			}
 			if (!found)
 				ereport(ERROR,
-						(errcode(ERRCODE_SYNTAX_ERROR),
-						 errmsg("DEFINE variable \"%s\" is not used in PATTERN",
-								rt->name),
-						 parser_errposition(pstate, rt->location)));
+						errcode(ERRCODE_SYNTAX_ERROR),
+						errmsg("DEFINE variable \"%s\" is not used in PATTERN",
+							   rt->name),
+						parser_errposition(pstate, rt->location));
 		}
 	}
 }
@@ -347,10 +347,10 @@ transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 
 			if (!strcmp(n, name))
 				ereport(ERROR,
-						(errcode(ERRCODE_SYNTAX_ERROR),
-						 errmsg("DEFINE variable \"%s\" appears more than once",
-								name),
-						 parser_errposition(pstate, exprLocation((Node *) r))));
+						errcode(ERRCODE_SYNTAX_ERROR),
+						errmsg("DEFINE variable \"%s\" appears more than once",
+							   name),
+						parser_errposition(pstate, exprLocation((Node *) r)));
 		}
 
 		restargets = lappend(restargets, restarget);
@@ -529,14 +529,14 @@ define_walker(Node *node, void *context)
 	 */
 	if (check_functions_in_node(node, nav_volatile_func_checker, NULL))
 		ereport(ERROR,
-				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-				 errmsg("volatile functions are not allowed in DEFINE clause"),
-				 parser_errposition(ctx->pstate, exprLocation(node))));
+				errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				errmsg("volatile functions are not allowed in DEFINE clause"),
+				parser_errposition(ctx->pstate, exprLocation(node)));
 	if (IsA(node, NextValueExpr))
 		ereport(ERROR,
-				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-				 errmsg("sequence operations are not allowed in DEFINE clause"),
-				 parser_errposition(ctx->pstate, exprLocation(node))));
+				errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				errmsg("sequence operations are not allowed in DEFINE clause"),
+				parser_errposition(ctx->pstate, exprLocation(node)));
 
 	/* Var sighting feeds the column-ref rule for the enclosing nav scope. */
 	if (IsA(node, Var) &&
@@ -600,17 +600,17 @@ define_walker(Node *node, void *context)
 					/* Reject triple-or-deeper nesting */
 					if (ctx->nav_count > 1)
 						ereport(ERROR,
-								(errcode(ERRCODE_SYNTAX_ERROR),
-								 errmsg("cannot nest row pattern navigation more than two levels deep"),
-								 errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
-								 parser_errposition(ctx->pstate, nav->location)));
+								errcode(ERRCODE_SYNTAX_ERROR),
+								errmsg("cannot nest row pattern navigation more than two levels deep"),
+								errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
+								parser_errposition(ctx->pstate, nav->location));
 
 					if (!IsA(nav->arg, RPRNavExpr))
 						ereport(ERROR,
-								(errcode(ERRCODE_SYNTAX_ERROR),
-								 errmsg("row pattern navigation operation must be a direct argument of the outer navigation"),
-								 errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
-								 parser_errposition(ctx->pstate, nav->location)));
+								errcode(ERRCODE_SYNTAX_ERROR),
+								errmsg("row pattern navigation operation must be a direct argument of the outer navigation"),
+								errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
+								parser_errposition(ctx->pstate, nav->location));
 
 					inner = (RPRNavExpr *) nav->arg;
 
@@ -630,29 +630,29 @@ define_walker(Node *node, void *context)
 				}
 				else if (!outer_phys && inner_phys)
 					ereport(ERROR,
-							(errcode(ERRCODE_SYNTAX_ERROR),
-							 errmsg("FIRST and LAST cannot contain PREV or NEXT"),
-							 errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
-							 parser_errposition(ctx->pstate, nav->location)));
+							errcode(ERRCODE_SYNTAX_ERROR),
+							errmsg("FIRST and LAST cannot contain PREV or NEXT"),
+							errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
+							parser_errposition(ctx->pstate, nav->location));
 				else if (outer_phys && inner_phys)
 					ereport(ERROR,
-							(errcode(ERRCODE_SYNTAX_ERROR),
-							 errmsg("PREV and NEXT cannot contain PREV or NEXT"),
-							 errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
-							 parser_errposition(ctx->pstate, nav->location)));
+							errcode(ERRCODE_SYNTAX_ERROR),
+							errmsg("PREV and NEXT cannot contain PREV or NEXT"),
+							errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
+							parser_errposition(ctx->pstate, nav->location));
 				else
 					ereport(ERROR,
-							(errcode(ERRCODE_SYNTAX_ERROR),
-							 errmsg("FIRST and LAST cannot contain FIRST or LAST"),
-							 errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
-							 parser_errposition(ctx->pstate, nav->location)));
+							errcode(ERRCODE_SYNTAX_ERROR),
+							errmsg("FIRST and LAST cannot contain FIRST or LAST"),
+							errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
+							parser_errposition(ctx->pstate, nav->location));
 			}
 			else if (!ctx->has_column_ref)
 			{
 				ereport(ERROR,
-						(errcode(ERRCODE_SYNTAX_ERROR),
-						 errmsg("argument of row pattern navigation operation must include at least one column reference"),
-						 parser_errposition(ctx->pstate, nav->location)));
+						errcode(ERRCODE_SYNTAX_ERROR),
+						errmsg("argument of row pattern navigation operation must include at least one column reference"),
+						parser_errposition(ctx->pstate, nav->location));
 			}
 
 			/*
@@ -673,9 +673,9 @@ define_walker(Node *node, void *context)
 				(void) define_walker((Node *) nav->offset_arg, ctx);
 				if (ctx->has_column_ref)
 					ereport(ERROR,
-							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-							 errmsg("row pattern navigation offset must be a run-time constant"),
-							 parser_errposition(ctx->pstate, exprLocation((Node *) nav->offset_arg))));
+							errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+							errmsg("row pattern navigation offset must be a run-time constant"),
+							parser_errposition(ctx->pstate, exprLocation((Node *) nav->offset_arg)));
 			}
 			if (flattened && nav->compound_offset_arg != NULL)
 			{
@@ -683,9 +683,9 @@ define_walker(Node *node, void *context)
 				(void) define_walker((Node *) nav->compound_offset_arg, ctx);
 				if (ctx->has_column_ref)
 					ereport(ERROR,
-							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-							 errmsg("row pattern navigation offset must be a run-time constant"),
-							 parser_errposition(ctx->pstate, exprLocation((Node *) nav->compound_offset_arg))));
+							errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+							errmsg("row pattern navigation offset must be a run-time constant"),
+							parser_errposition(ctx->pstate, exprLocation((Node *) nav->compound_offset_arg)));
 			}
 
 			*ctx = saved;
-- 
2.50.1 (Apple Git-155)


From 921232e90b5b9685b87d2b70504077929428563e Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Tue, 5 May 2026 17:21:56 +0900
Subject: [PATCH 06/68] Add trailing commas to RPR enum definitions

RPRNavKind, RPRNavOffsetKind, RPRPatternNodeType lacked a trailing
comma on the last enumerator, contrary to project coding style.
---
 src/include/nodes/parsenodes.h | 4 ++--
 src/include/nodes/primnodes.h  | 2 +-
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 455db2cec61..adefb1d5bad 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -604,7 +604,7 @@ typedef enum RPRNavOffsetKind
 	RPR_NAV_OFFSET_FIXED,		/* resolved constant; use the offset value */
 	RPR_NAV_OFFSET_NEEDS_EVAL,	/* non-constant offset; evaluate at executor
 								 * init */
-	RPR_NAV_OFFSET_RETAIN_ALL	/* cannot determine; retain all rows (no trim) */
+	RPR_NAV_OFFSET_RETAIN_ALL,	/* cannot determine; retain all rows (no trim) */
 } RPRNavOffsetKind;
 
 /*
@@ -615,7 +615,7 @@ typedef enum RPRPatternNodeType
 	RPR_PATTERN_VAR,			/* variable reference */
 	RPR_PATTERN_SEQ,			/* sequence (concatenation) */
 	RPR_PATTERN_ALT,			/* alternation (|) */
-	RPR_PATTERN_GROUP			/* group (parentheses) */
+	RPR_PATTERN_GROUP,			/* group (parentheses) */
 } RPRPatternNodeType;
 
 /*
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index 656c552b0a8..d7138f5141b 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -684,7 +684,7 @@ typedef enum RPRNavKind
 	RPR_NAV_PREV_FIRST,
 	RPR_NAV_PREV_LAST,
 	RPR_NAV_NEXT_FIRST,
-	RPR_NAV_NEXT_LAST
+	RPR_NAV_NEXT_LAST,
 } RPRNavKind;
 
 typedef struct RPRNavExpr
-- 
2.50.1 (Apple Git-155)


From 029c2bf753599736f0469a308d8fe050e4e2b91f Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Mon, 4 May 2026 22:31:06 +0900
Subject: [PATCH 08/68] Add high-water mark tracking to NFA visited bitmap
 reset

Track the min/max bitmapword index touched in nfa_mark_visited so the
per-advance reset memsets only the touched range, not the full
nfaVisitedNWords array.

Each visited mark performs two int16 comparisons.  For single-word
bitmaps this overhead is added with no reset saving; for larger NFAs
the reset walks only the slice the DFS actually touched, which is
where the win comes from.  Applied unconditionally for simplicity.
Semantics unchanged.
---
 src/backend/executor/execRPR.c       | 41 +++++++++++++++++++++++-----
 src/backend/executor/nodeWindowAgg.c |  3 ++
 src/include/nodes/execnodes.h        |  4 +++
 3 files changed, 41 insertions(+), 7 deletions(-)

diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 242ae9c6dcf..1e6196d6960 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -38,6 +38,21 @@
 #define WORDNUM(x)	((x) / BITS_PER_BITMAPWORD)
 #define BITNUM(x)	((x) % BITS_PER_BITMAPWORD)
 
+/*
+ * Set the visited bit for elemIdx and update the high-water marks
+ * (nfaVisitedMin/MaxWord) so that the next reset only has to clear
+ * the touched range instead of the full nfaVisitedNWords array.
+ */
+static inline void
+nfa_mark_visited(WindowAggState *winstate, int16 elemIdx)
+{
+	int16		w = WORDNUM(elemIdx);
+
+	winstate->nfaVisitedElems[w] |= ((bitmapword) 1 << BITNUM(elemIdx));
+	winstate->nfaVisitedMinWord = Min(winstate->nfaVisitedMinWord, w);
+	winstate->nfaVisitedMaxWord = Max(winstate->nfaVisitedMaxWord, w);
+}
+
 /* Forward declarations - NFA state management */
 static RPRNFAState *nfa_state_alloc(WindowAggState *winstate);
 static void nfa_state_free(WindowAggState *winstate, RPRNFAState *state);
@@ -320,8 +335,7 @@ nfa_add_state_unique(WindowAggState *winstate, RPRNFAContext *ctx, RPRNFAState *
 	RPRNFAState *tail = NULL;
 
 	/* Mark VAR in visited before duplicate check to prevent DFS loops */
-	winstate->nfaVisitedElems[WORDNUM(state->elemIdx)] |=
-		((bitmapword) 1 << BITNUM(state->elemIdx));
+	nfa_mark_visited(winstate, state->elemIdx);
 
 	/* Check for duplicate and find tail */
 	for (s = ctx->states; s != NULL; s = s->next)
@@ -1341,8 +1355,7 @@ nfa_advance_state(WindowAggState *winstate, RPRNFAContext *ctx,
 	 * the same VAR in a new iteration.
 	 */
 	if (!RPRElemIsVar(elem))
-		winstate->nfaVisitedElems[WORDNUM(state->elemIdx)] |=
-			((bitmapword) 1 << BITNUM(state->elemIdx));
+		nfa_mark_visited(winstate, state->elemIdx);
 
 	switch (elem->varId)
 	{
@@ -1394,9 +1407,23 @@ nfa_advance(WindowAggState *winstate, RPRNFAContext *ctx, int64 currentPos)
 		CHECK_FOR_INTERRUPTS();
 		savedMatchedState = ctx->matchedState;
 
-		/* Clear visited bitmap before each state's DFS expansion */
-		memset(winstate->nfaVisitedElems, 0,
-			   sizeof(bitmapword) * winstate->nfaVisitedNWords);
+		/*
+		 * Clear visited bitmap before each state's DFS expansion.  Only the
+		 * range touched since the previous reset (tracked via the high-water
+		 * marks updated in nfa_mark_visited) needs to be cleared; for small
+		 * NFAs this is the whole array, but for large NFAs whose DFS only
+		 * reaches a few elements per advance it avoids walking the full
+		 * bitmap.
+		 */
+		if (winstate->nfaVisitedMaxWord >= winstate->nfaVisitedMinWord)
+		{
+			memset(&winstate->nfaVisitedElems[winstate->nfaVisitedMinWord], 0,
+				   sizeof(bitmapword) *
+				   (winstate->nfaVisitedMaxWord -
+					winstate->nfaVisitedMinWord + 1));
+			winstate->nfaVisitedMinWord = INT16_MAX;
+			winstate->nfaVisitedMaxWord = -1;
+		}
 
 		state = states;
 		states = states->next;
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 200d8a49001..188445d779c 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -3055,6 +3055,9 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
 			(node->rpPattern->numElements - 1) / BITS_PER_BITMAPWORD + 1;
 		winstate->nfaVisitedElems = palloc0(sizeof(bitmapword) *
 											winstate->nfaVisitedNWords);
+		/* High-water mark sentinels: no bits set yet. */
+		winstate->nfaVisitedMinWord = INT16_MAX;
+		winstate->nfaVisitedMaxWord = -1;
 	}
 
 	/* Set up row pattern recognition DEFINE clause */
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 0cb01baa949..1fba14b892e 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -2673,6 +2673,10 @@ typedef struct WindowAggState
 									 * detection */
 	int			nfaVisitedNWords;	/* number of bitmapwords in
 									 * nfaVisitedElems */
+	int16		nfaVisitedMinWord;	/* lowest bitmapword index touched since
+									 * last reset (INT16_MAX = none) */
+	int16		nfaVisitedMaxWord;	/* highest bitmapword index touched since
+									 * last reset (-1 = none) */
 	int64		nfaLastProcessedRow;	/* last row processed by NFA (-1 =
 										 * none) */
 
-- 
2.50.1 (Apple Git-155)


From 2aaed5f6126ac08b1c11593568bdcbc5f6f58d91 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Thu, 7 May 2026 21:00:48 +0900
Subject: [PATCH 09/68] Document DEFINE subquery rejection as intentional
 over-rejection

The SubLink switch in transformSubLink() rejects every subquery in
EXPR_KIND_RPR_DEFINE with no inline rationale.  Add an XXX block
recording that SQL/RPR (19075-5 4.18.4 / 6.17.4; R010 /
R020) actually permits a nested subquery in DEFINE provided it
does not itself perform row pattern recognition and does not
reference an outer pattern variable, and that the blanket
rejection here subsumes both restrictions by making the subquery
itself unreachable.

Implementing the case distinction would mean walking the analyzed
subquery Query tree for nested RPR and walking it for ColumnRef
qualifiers matching any ancestor's p_rpr_pattern_vars; both are
doable with the existing infrastructure and are left as future
work, not blocked on any other feature.  Comment-only change.
---
 src/backend/parser/parse_expr.c | 22 ++++++++++++++++++++++
 1 file changed, 22 insertions(+)

diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 4221643d9c8..10edccb8afb 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -1944,6 +1944,28 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
 		case EXPR_KIND_FOR_PORTION:
 			err = _("cannot use subquery in FOR PORTION OF expression");
 			break;
+
+			/*----------
+			 * XXX SQL/RPR (19075-5 4.18.4 / 6.17.4; R010 / R020)
+			 * permits a subquery nested in a DEFINE expression provided
+			 * that:
+			 *   (a) the subquery does not itself perform row pattern
+			 *       recognition, and
+			 *   (b) the subquery does not reference a row pattern variable
+			 *       of the outer query.
+			 *
+			 * We reject all subqueries here for now.  Implementing the
+			 * case distinction would mean walking the analyzed subquery
+			 * Query tree for nested RPR window clauses to enforce (a),
+			 * and walking it for ColumnRef qualifiers matching any
+			 * ancestor's p_rpr_pattern_vars to enforce (b).  Both checks
+			 * are doable with the existing infrastructure -- they are
+			 * left as future work, not blocked on any other feature.
+			 * Until then this blanket rejection is intentional
+			 * over-rejection, not a standard fit; it subsumes both (a)
+			 * and (b) by making the subquery itself unreachable.
+			 *----------
+			 */
 		case EXPR_KIND_RPR_DEFINE:
 			err = _("cannot use subquery in DEFINE expression");
 			break;
-- 
2.50.1 (Apple Git-155)


From a6df6a0af2b523eeff49909cb9e4215b3a435162 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Thu, 7 May 2026 21:37:51 +0900
Subject: [PATCH 10/68] Remove duplicate #include in nodeWindowAgg.c

#include "common/int.h" was included twice; the first occurrence
was also misplaced between two catalog/* headers, breaking the
alphabetical grouping of system header includes.  Drop the
misplaced first occurrence; the second sits in the correct
alphabetical position between catalog/ and executor/ headers.
---
 src/backend/executor/nodeWindowAgg.c | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 188445d779c..99858d22dad 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -36,7 +36,6 @@
 #include "access/htup_details.h"
 #include "catalog/objectaccess.h"
 #include "catalog/pg_aggregate.h"
-#include "common/int.h"
 #include "catalog/pg_proc.h"
 #include "common/int.h"
 #include "executor/executor.h"
-- 
2.50.1 (Apple Git-155)


From 5ac8a338e63200e8ab85317cf9cd095a03596fba Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Sat, 9 May 2026 13:47:49 +0900
Subject: [PATCH 11/68] Normalize SQL/RPR standard references

Make every reference cite ISO/IEC 19075-5 explicitly across RPR code
and regress tests.  Prefix bare "19075-5" / "SQL standard" forms,
pin STR06 to its source (7.2.8), and where a clause is mirrored in
both Chapter 4 (FROM) and Chapter 6 (WINDOW), cite the Chapter 6
subclause first because this implementation targets Feature R020.
Document the citation policy in README.rpr.
---
 src/backend/executor/README.rpr               |  8 +++++++-
 src/backend/executor/execRPR.c                |  5 +++--
 src/backend/optimizer/plan/rpr.c              |  8 ++++----
 src/backend/parser/parse_expr.c               | 11 ++++++-----
 src/backend/parser/parse_rpr.c                |  2 +-
 src/test/regress/expected/rpr_base.out        |  6 +++---
 src/test/regress/expected/rpr_explain.out     |  2 +-
 src/test/regress/expected/rpr_integration.out |  4 ++--
 src/test/regress/expected/rpr_nfa.out         |  4 ++--
 src/test/regress/sql/rpr_base.sql             |  6 +++---
 src/test/regress/sql/rpr_explain.sql          |  2 +-
 src/test/regress/sql/rpr_integration.sql      |  4 ++--
 src/test/regress/sql/rpr_nfa.sql              |  4 ++--
 13 files changed, 37 insertions(+), 29 deletions(-)

diff --git a/src/backend/executor/README.rpr b/src/backend/executor/README.rpr
index 52bcd77390c..e64efe0c7fc 100644
--- a/src/backend/executor/README.rpr
+++ b/src/backend/executor/README.rpr
@@ -38,6 +38,12 @@ What is a Flat-Array Stream NFA?
 Chapter I  Row Pattern Recognition Overview
 ============================================================================
 
+Normative reference: ISO/IEC 19075-5 (SQL Technical Report, Part 5: Row
+pattern recognition in SQL).  Subclause numbers cited throughout this code
+base refer to that document.  Where Chapters 4 (FROM clause) and 6 (WINDOW
+clause) describe parallel material, this implementation cites the Chapter 6
+subclause first because it targets Feature R020.
+
 Row Pattern Recognition (hereafter RPR) is a feature introduced in SQL:2016
 that matches regex-based patterns against ordered row sets.
 
@@ -1033,7 +1039,7 @@ match:
 
 X-3. INITIAL vs SEEK
 
-  Standard definition (section 6.12):
+  Standard definition (ISO/IEC 19075-5 6.12):
   INITIAL: "is used to look for a match whose first row is R."
   SEEK:    "is used to permit a search for the first match anywhere
            from R through the end of the full window frame."
diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 1e6196d6960..e1caa7bb528 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -736,8 +736,9 @@ nfa_absorb_contexts(WindowAggState *winstate)
  * once per row by evaluating all DEFINE expressions.  NULL means no DEFINE
  * clauses exist (only possible during early development/testing).
  *
- * Per SQL:2016 R020, pattern variables not listed in DEFINE are implicitly
- * TRUE -- they match every row.  This is checked via varId >= list_length.
+ * Per ISO/IEC 19075-5 Feature R020, pattern variables not listed in DEFINE
+ * are implicitly TRUE -- they match every row.  This is checked via
+ * varId >= list_length.
  */
 static bool
 nfa_eval_var_match(WindowAggState *winstate, RPRPatternElement *elem,
diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index ed8b6c3414c..c65681463b3 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -1050,10 +1050,10 @@ scanRPRPatternRecursive(RPRPatternNode *node, char **varNames, int *numVars,
 			}
 
 			/*
-			 * Variable not in DEFINE clause - this is valid per SQL standard.
-			 * Such variables are implicitly TRUE. Add to varNames so they get
-			 * a varId >= defineVariableList length, which executor treats as
-			 * TRUE.
+			 * Variable not in DEFINE clause - this is valid per ISO/IEC
+			 * 19075-5 Feature R020.  Such variables are implicitly TRUE. Add
+			 * to varNames so they get a varId >= defineVariableList length,
+			 * which executor treats as TRUE.
 			 */
 			Assert(*numVars < RPR_VARID_MAX);
 			varNames[(*numVars)++] = node->varName;
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 10edccb8afb..78abdc88f86 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -631,11 +631,12 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 	/*----------
 	 * Qualified references in DEFINE need a tri-classification:
 	 *
-	 *	  pattern variable qualifier (e.g. UP.price): valid per 19075-5 4.16
-	 *	  but not yet implemented -- raise FEATURE_NOT_SUPPORTED.
+	 *	  pattern variable qualifier (e.g. UP.price): valid per
+	 *	  ISO/IEC 19075-5 6.15 / 4.16 but not yet implemented --
+	 *	  raise FEATURE_NOT_SUPPORTED.
 	 *
-	 *	  FROM-clause range variable qualifier: prohibited by 19075-5 6.5
-	 *	  -- raise SYNTAX_ERROR.
+	 *	  FROM-clause range variable qualifier: prohibited by
+	 *	  ISO/IEC 19075-5 6.5 -- raise SYNTAX_ERROR.
 	 *
 	 *	  any other qualifier (typo, undefined name): fall through and let
 	 *	  normal column resolution produce a sensible error.
@@ -1946,7 +1947,7 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
 			break;
 
 			/*----------
-			 * XXX SQL/RPR (19075-5 4.18.4 / 6.17.4; R010 / R020)
+			 * XXX SQL/RPR (ISO/IEC 19075-5 6.17.4 / 4.18.4; R020 / R010)
 			 * permits a subquery nested in a DEFINE expression provided
 			 * that:
 			 *   (a) the subquery does not itself perform row pattern
diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index bba887f17ce..d2ed6c14811 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -467,7 +467,7 @@ transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
  *     (RPR's NFA may evaluate the same row's predicate multiple times
  *     during backtracking, so a volatile result would make matching
  *     non-deterministic).
- *   - For each outer RPRNavExpr (per SQL 5.6.4 nesting rules):
+ *   - For each outer RPRNavExpr (per ISO/IEC 19075-5 5.6.4 nesting rules):
  *     * arg must contain at least one column reference
  *     * PREV/NEXT wrapping FIRST/LAST flattens to a compound kind
  *     * Other nestings are rejected (FIRST(PREV()), PREV(PREV()), ...)
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index 86abb96c177..cfd2645bbed 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -1,6 +1,6 @@
 -- ============================================================
 -- RPR Base Tests
--- Tests for Row Pattern Recognition (ISO/IEC 19075-5:2016)
+-- Tests for Row Pattern Recognition (ISO/IEC 19075-5)
 -- ============================================================
 --
 -- Parser Layer:
@@ -3065,7 +3065,7 @@ LINE 6:     DEFINE A AS val > 0
             ^
 -- Expected: Syntax error
 -- Qualified column references (NOT SUPPORTED)
--- Pattern variable qualified name: not supported (valid per SQL standard 4.16, not yet implemented)
+-- Pattern variable qualified name: not supported (valid per ISO/IEC 19075-5 6.15 / 4.16, not yet implemented)
 SELECT COUNT(*) OVER w
 FROM rpr_err
 WINDOW w AS (
@@ -3104,7 +3104,7 @@ ERROR:  DEFINE variable "b" is not used in PATTERN
 LINE 7:     DEFINE A AS val > 0, B AS B.val > 0
                                  ^
 -- Expected: ERROR: pattern variable qualified expression "b.val" is not supported
--- FROM-clause range variable qualified name: not allowed (prohibited by SQL standard 6.5)
+-- FROM-clause range variable qualified name: not allowed (prohibited by ISO/IEC 19075-5 6.5)
 SELECT COUNT(*) OVER w
 FROM rpr_err
 WINDOW w AS (
diff --git a/src/test/regress/expected/rpr_explain.out b/src/test/regress/expected/rpr_explain.out
index c4516d3c756..77079d5e8c9 100644
--- a/src/test/regress/expected/rpr_explain.out
+++ b/src/test/regress/expected/rpr_explain.out
@@ -2185,7 +2185,7 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=3.00 loops=1)
 (9 rows)
 
--- (A?){2,3}: min=2 (SQL:2016 STR06 = STRE STRE) -> 3 length-0 matches
+-- (A?){2,3}: min=2 (ISO/IEC 19075-5 7.2.8 STR06 = STRE STRE) -> 3 length-0 matches
 CREATE VIEW rpr_ev_edge_empty_match_min2 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 3) AS s(v)
diff --git a/src/test/regress/expected/rpr_integration.out b/src/test/regress/expected/rpr_integration.out
index 905bd3538de..7cbeed3347e 100644
--- a/src/test/regress/expected/rpr_integration.out
+++ b/src/test/regress/expected/rpr_integration.out
@@ -1278,8 +1278,8 @@ ORDER BY o.id, r.id;
 -- PostgreSQL restriction, so this is the natural place to exercise
 -- "RPR under Recursive Union").
 --
--- XXX: Whether this case falls under the ISO/IEC 9075-2 4.18.5 /
--- 6.17.5 prohibition is not something I can judge.  If this case
+-- XXX: Whether this case falls under the ISO/IEC 19075-5 6.17.5 /
+-- 4.18.5 prohibition is not something I can judge.  If this case
 -- is not prohibited, the open question is whether a query that
 -- does trigger the prohibition can be constructed at all.
 -- Whether to prohibit this case is left to the community.
diff --git a/src/test/regress/expected/rpr_nfa.out b/src/test/regress/expected/rpr_nfa.out
index 4cff7cfbbd7..fe5bb324df0 100644
--- a/src/test/regress/expected/rpr_nfa.out
+++ b/src/test/regress/expected/rpr_nfa.out
@@ -4083,7 +4083,7 @@ WINDOW w AS (
 
 -- ============================================================
 -- Standard Clause 7: Formal Pattern Matching Rules
--- ISO/IEC 19075-5:2021, Clause 7
+-- ISO/IEC 19075-5, Clause 7
 -- ============================================================
 -- ------------------------------------------------------------
 -- 7.2.2 Alternation: first alternative is preferred
@@ -4453,7 +4453,7 @@ WINDOW w AS (
   3 | {B}   |             |          
 (3 rows)
 
--- (A?){2,3}: min=2, nullable inner.  Per SQL:2016 STR06 = (STRE STRE)
+-- (A?){2,3}: min=2, nullable inner.  Per ISO/IEC 19075-5 7.2.8 STR06 = (STRE STRE)
 -- is valid: two empty iterations satisfy min=2.
 -- NFA reports 3 length-0 matches; first/last_value NULL over empty frame.
 WITH test_728_min2 AS (
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index e8c72706720..fd289d7cf67 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -1,6 +1,6 @@
 -- ============================================================
 -- RPR Base Tests
--- Tests for Row Pattern Recognition (ISO/IEC 19075-5:2016)
+-- Tests for Row Pattern Recognition (ISO/IEC 19075-5)
 -- ============================================================
 --
 -- Parser Layer:
@@ -2083,7 +2083,7 @@ WINDOW w AS (
 
 -- Qualified column references (NOT SUPPORTED)
 
--- Pattern variable qualified name: not supported (valid per SQL standard 4.16, not yet implemented)
+-- Pattern variable qualified name: not supported (valid per ISO/IEC 19075-5 6.15 / 4.16, not yet implemented)
 SELECT COUNT(*) OVER w
 FROM rpr_err
 WINDOW w AS (
@@ -2116,7 +2116,7 @@ WINDOW w AS (
 );
 -- Expected: ERROR: pattern variable qualified expression "b.val" is not supported
 
--- FROM-clause range variable qualified name: not allowed (prohibited by SQL standard 6.5)
+-- FROM-clause range variable qualified name: not allowed (prohibited by ISO/IEC 19075-5 6.5)
 SELECT COUNT(*) OVER w
 FROM rpr_err
 WINDOW w AS (
diff --git a/src/test/regress/sql/rpr_explain.sql b/src/test/regress/sql/rpr_explain.sql
index d339a80a673..a527615849a 100644
--- a/src/test/regress/sql/rpr_explain.sql
+++ b/src/test/regress/sql/rpr_explain.sql
@@ -1228,7 +1228,7 @@ WINDOW w AS (
     DEFINE A AS FALSE
 );');
 
--- (A?){2,3}: min=2 (SQL:2016 STR06 = STRE STRE) -> 3 length-0 matches
+-- (A?){2,3}: min=2 (ISO/IEC 19075-5 7.2.8 STR06 = STRE STRE) -> 3 length-0 matches
 CREATE VIEW rpr_ev_edge_empty_match_min2 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 3) AS s(v)
diff --git a/src/test/regress/sql/rpr_integration.sql b/src/test/regress/sql/rpr_integration.sql
index 29b2db2f7bb..f4267c74645 100644
--- a/src/test/regress/sql/rpr_integration.sql
+++ b/src/test/regress/sql/rpr_integration.sql
@@ -792,8 +792,8 @@ ORDER BY o.id, r.id;
 -- PostgreSQL restriction, so this is the natural place to exercise
 -- "RPR under Recursive Union").
 --
--- XXX: Whether this case falls under the ISO/IEC 9075-2 4.18.5 /
--- 6.17.5 prohibition is not something I can judge.  If this case
+-- XXX: Whether this case falls under the ISO/IEC 19075-5 6.17.5 /
+-- 4.18.5 prohibition is not something I can judge.  If this case
 -- is not prohibited, the open question is whether a query that
 -- does trigger the prohibition can be constructed at all.
 -- Whether to prohibit this case is left to the community.
diff --git a/src/test/regress/sql/rpr_nfa.sql b/src/test/regress/sql/rpr_nfa.sql
index 29ec4a9dacb..7a5b5c41b24 100644
--- a/src/test/regress/sql/rpr_nfa.sql
+++ b/src/test/regress/sql/rpr_nfa.sql
@@ -2976,7 +2976,7 @@ WINDOW w AS (
 
 -- ============================================================
 -- Standard Clause 7: Formal Pattern Matching Rules
--- ISO/IEC 19075-5:2021, Clause 7
+-- ISO/IEC 19075-5, Clause 7
 -- ============================================================
 
 -- ------------------------------------------------------------
@@ -3280,7 +3280,7 @@ WINDOW w AS (
         A AS 'A' = ANY(flags)
 );
 
--- (A?){2,3}: min=2, nullable inner.  Per SQL:2016 STR06 = (STRE STRE)
+-- (A?){2,3}: min=2, nullable inner.  Per ISO/IEC 19075-5 7.2.8 STR06 = (STRE STRE)
 -- is valid: two empty iterations satisfy min=2.
 -- NFA reports 3 length-0 matches; first/last_value NULL over empty frame.
 WITH test_728_min2 AS (
-- 
2.50.1 (Apple Git-155)


From e79f86bb2dddc28457ae0aeb658581457122cf42 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Sun, 10 May 2026 13:51:45 +0900
Subject: [PATCH 12/68] Add rpr_integration B7 cases for RPR in recursive query

Replace the prior B7 test (which asserted that an RPR window works
in the base leg of a recursive CTE) with two cases the recursive-RPR
prohibition needs to cover: WITH RECURSIVE with RPR in the base leg,
and CREATE RECURSIVE VIEW with an RPR window.  Cite ISO/IEC 19075-5
6.17.5 (R020) and 4.18.5 (R010), and the formal rule in ISO/IEC
9075-2:2016 7.17 Syntax Rule 3)e)f), and drop the deferred XXX
comment that left this case open to community input.

Expected output still matches the current (pre-rejection) behavior;
a follow-up patch adds the rejection in parse_cte.c and flips both
queries to ERROR.
---
 src/test/regress/expected/rpr_integration.out | 71 ++++++-------------
 src/test/regress/sql/rpr_integration.sql      | 47 ++++++------
 2 files changed, 43 insertions(+), 75 deletions(-)

diff --git a/src/test/regress/expected/rpr_integration.out b/src/test/regress/expected/rpr_integration.out
index 7cbeed3347e..0b05a826a27 100644
--- a/src/test/regress/expected/rpr_integration.out
+++ b/src/test/regress/expected/rpr_integration.out
@@ -1269,54 +1269,18 @@ ORDER BY o.id, r.id;
 -- ============================================================
 -- B7. RPR + Recursive CTE
 -- ============================================================
--- Verify that an RPR window can appear inside the non-recursive
--- (base) leg of a recursive CTE.  The plan must show the RPR
--- WindowAgg sitting under the Recursive Union as the base-leg
--- child, with the WorkTable Scan feeding the recursive leg above
--- it.  This confirms that RPR output can seed a recursive CTE
--- (window functions cannot appear in the recursive leg itself, a
--- PostgreSQL restriction, so this is the natural place to exercise
--- "RPR under Recursive Union").
---
--- XXX: Whether this case falls under the ISO/IEC 19075-5 6.17.5 /
--- 4.18.5 prohibition is not something I can judge.  If this case
--- is not prohibited, the open question is whether a query that
--- does trigger the prohibition can be constructed at all.
--- Whether to prohibit this case is left to the community.
--- Plan: Recursive Union with the RPR WindowAgg on the base leg and
--- the WorkTable Scan on the recursive leg.
-EXPLAIN (COSTS OFF)
-WITH RECURSIVE seq AS (
-    SELECT id, val, count(*) OVER w AS cnt
-    FROM rpr_integ
-    WINDOW w AS (ORDER BY id
-        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-        PATTERN (A B+)
-        DEFINE B AS val > PREV(val))
-    UNION ALL
-    SELECT id + 100, val, cnt FROM seq WHERE id < 3
-)
-SELECT id, val, cnt FROM seq ORDER BY id;
-                                              QUERY PLAN                                               
--------------------------------------------------------------------------------------------------------
- Sort
-   Sort Key: seq.id
-   CTE seq
-     ->  Recursive Union
-           ->  WindowAgg
-                 Window: w AS (ORDER BY rpr_integ.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-                 Pattern: a b+
-                 Nav Mark Lookback: 1
-                 ->  Sort
-                       Sort Key: rpr_integ.id
-                       ->  Seq Scan on rpr_integ
-           ->  WorkTable Scan on seq seq_1
-                 Filter: (id < 3)
-   ->  CTE Scan on seq
-(14 rows)
-
--- Result: the base leg contributes the RPR match counts; the
--- recursive leg propagates those counts with shifted ids.
+-- Verify that RPR is rejected inside a recursive query.
+-- ISO/IEC 19075-5 6.17.5 (R020) and 4.18.5 (R010) cite CREATE
+-- RECURSIVE VIEW examples and state that "row pattern matching
+-- is prohibited in recursive queries".  The formal rule lives in
+-- ISO/IEC 9075-2:2016 7.17 Syntax Rule 3)f): a potentially
+-- recursive <with list element> shall not contain a <row pattern
+-- measures> or <row pattern common syntax>.  Per 3)e), every
+-- <with list element> under WITH RECURSIVE is "potentially
+-- recursive", so the rejection covers the base (non-recursive)
+-- leg too, not just the self-referencing leg.
+-- WITH RECURSIVE: RPR in the base leg is rejected even though the
+-- base leg never references the recursive CTE name.
 WITH RECURSIVE seq AS (
     SELECT id, val, count(*) OVER w AS cnt
     FROM rpr_integ
@@ -1344,6 +1308,17 @@ SELECT id, val, cnt FROM seq ORDER BY id;
  102 |  20 |   0
 (12 rows)
 
+-- CREATE RECURSIVE VIEW: rewritten by makeRecursiveViewSelect()
+-- into WITH RECURSIVE, so the same rejection applies.  This is
+-- the form ISO/IEC 19075-5 6.17.5 cites verbatim.
+CREATE RECURSIVE VIEW rpr_recv(id, val, cnt) AS
+    SELECT id, val, count(*) OVER w
+    FROM rpr_integ
+    WINDOW w AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val));
+DROP VIEW rpr_recv;
 -- ============================================================
 -- B8. RPR + Incremental sort
 -- ============================================================
diff --git a/src/test/regress/sql/rpr_integration.sql b/src/test/regress/sql/rpr_integration.sql
index f4267c74645..bc8f4712bcb 100644
--- a/src/test/regress/sql/rpr_integration.sql
+++ b/src/test/regress/sql/rpr_integration.sql
@@ -783,24 +783,19 @@ ORDER BY o.id, r.id;
 -- ============================================================
 -- B7. RPR + Recursive CTE
 -- ============================================================
--- Verify that an RPR window can appear inside the non-recursive
--- (base) leg of a recursive CTE.  The plan must show the RPR
--- WindowAgg sitting under the Recursive Union as the base-leg
--- child, with the WorkTable Scan feeding the recursive leg above
--- it.  This confirms that RPR output can seed a recursive CTE
--- (window functions cannot appear in the recursive leg itself, a
--- PostgreSQL restriction, so this is the natural place to exercise
--- "RPR under Recursive Union").
---
--- XXX: Whether this case falls under the ISO/IEC 19075-5 6.17.5 /
--- 4.18.5 prohibition is not something I can judge.  If this case
--- is not prohibited, the open question is whether a query that
--- does trigger the prohibition can be constructed at all.
--- Whether to prohibit this case is left to the community.
-
--- Plan: Recursive Union with the RPR WindowAgg on the base leg and
--- the WorkTable Scan on the recursive leg.
-EXPLAIN (COSTS OFF)
+-- Verify that RPR is rejected inside a recursive query.
+-- ISO/IEC 19075-5 6.17.5 (R020) and 4.18.5 (R010) cite CREATE
+-- RECURSIVE VIEW examples and state that "row pattern matching
+-- is prohibited in recursive queries".  The formal rule lives in
+-- ISO/IEC 9075-2:2016 7.17 Syntax Rule 3)f): a potentially
+-- recursive <with list element> shall not contain a <row pattern
+-- measures> or <row pattern common syntax>.  Per 3)e), every
+-- <with list element> under WITH RECURSIVE is "potentially
+-- recursive", so the rejection covers the base (non-recursive)
+-- leg too, not just the self-referencing leg.
+
+-- WITH RECURSIVE: RPR in the base leg is rejected even though the
+-- base leg never references the recursive CTE name.
 WITH RECURSIVE seq AS (
     SELECT id, val, count(*) OVER w AS cnt
     FROM rpr_integ
@@ -813,19 +808,17 @@ WITH RECURSIVE seq AS (
 )
 SELECT id, val, cnt FROM seq ORDER BY id;
 
--- Result: the base leg contributes the RPR match counts; the
--- recursive leg propagates those counts with shifted ids.
-WITH RECURSIVE seq AS (
-    SELECT id, val, count(*) OVER w AS cnt
+-- CREATE RECURSIVE VIEW: rewritten by makeRecursiveViewSelect()
+-- into WITH RECURSIVE, so the same rejection applies.  This is
+-- the form ISO/IEC 19075-5 6.17.5 cites verbatim.
+CREATE RECURSIVE VIEW rpr_recv(id, val, cnt) AS
+    SELECT id, val, count(*) OVER w
     FROM rpr_integ
     WINDOW w AS (ORDER BY id
         ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
         PATTERN (A B+)
-        DEFINE B AS val > PREV(val))
-    UNION ALL
-    SELECT id + 100, val, cnt FROM seq WHERE id < 3
-)
-SELECT id, val, cnt FROM seq ORDER BY id;
+        DEFINE B AS val > PREV(val));
+DROP VIEW rpr_recv;
 
 -- ============================================================
 -- B8. RPR + Incremental sort
-- 
2.50.1 (Apple Git-155)


From 0de2f3fbe345717b9f91aad79e9ace2398e75c3c Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Tue, 12 May 2026 15:38:03 +0900
Subject: [PATCH 14/68] Enhance README.rpr per Tatsuo Ishii's review

Apply Tatsuo Ishii's enhancement patch on top of v47:

- Make "target audience" and "scope" more descriptive,
  pointing readers to the SQL standard (and Oracle/Trino
  manuals as alternatives)
- Spell out NFA and AST on first use
- Cross-reference the absorbability sections from the
  RPR_ELEM_ABSORBABLE_BRANCH flag description
- List additional WindowAggState fields in V-3
  (nfaVisitedNWords, defineMatchStartDependent,
  nfaLastProcessedRow)
- State the window framing rules that apply with RPR
- Add a References section (SQL standards)
---
 src/backend/executor/README.rpr | 49 ++++++++++++++++++++++++---------
 1 file changed, 36 insertions(+), 13 deletions(-)

diff --git a/src/backend/executor/README.rpr b/src/backend/executor/README.rpr
index e64efe0c7fc..6c2bddab455 100644
--- a/src/backend/executor/README.rpr
+++ b/src/backend/executor/README.rpr
@@ -2,11 +2,15 @@
   PostgreSQL Row Pattern Recognition: Flat-Array Stream NFA Guide
 ============================================================================
 
-  Target audience: Developers with a basic understanding of the PostgreSQL
-                   executor and planner architecture
+  This README's target audience is developers with a basic
+  understanding of the PostgreSQL executor and planner architecture.
+  Also it would be better for them to understand the specification of
+  the row pattern recognition in the SQL standard [1][2]. If you do
+  not have access to the SQL standard, Oracle's manual or Trino's
+  manual can be alternatives for them.
 
-  Scope: The entire process from PATTERN/DEFINE clause parsing to NFA
-         runtime execution
+  This README's scope is the entire process from PATTERN/DEFINE clause
+  parsing to NFA runtime execution.
 
   Related code:
     - src/backend/parser/parse_rpr.c          (parser phase)
@@ -23,10 +27,11 @@
 
 What is a Flat-Array Stream NFA?
 
-  The NFA in this implementation is not a traditional state-transition graph
-  but a flat array of fixed-size 16-byte elements. At runtime, it processes
-  the row stream in a forward-only manner, expanding epsilon transitions
-  eagerly without backtracking.
+  The NFA (Nondeterministic Finite Automaton) in this implementation
+  is not a traditional state-transition graph but a flat array of
+  fixed-size 16-byte elements. At runtime, it processes the row stream
+  in a forward-only manner, expanding epsilon transitions eagerly
+  without backtracking.
 
   - Flat-Array: Pattern compiled into a flat array,
                 not a graph (Chapter IV)
@@ -132,14 +137,14 @@ following:
 
   (3) DEFINE clause transformation (transformDefineClause)
 
-III-2. PATTERN AST
+III-2. PATTERN AST (Abstract Syntax Tree)
 
 The parser transforms the PATTERN clause into an RPRPatternNode tree.
 Each node has one of the following four types:
 
   RPR_PATTERN_VAR    Variable reference. Name stored in varName field.
   RPR_PATTERN_SEQ    Concatenation. Children node list in children.
-  RPR_PATTERN_ALT    Alternation. Branch node list in children.
+  RPR_PATTERN_ALT    Alternation (or). Branch node list in children.
   RPR_PATTERN_GROUP  Group (parentheses). Body node list in children.
 
 All nodes have min/max fields to express quantifiers:
@@ -270,9 +275,11 @@ Element flags (1 byte, bitmask):
         matches. (IV-4b)
 
   0x04  RPR_ELEM_ABSORBABLE_BRANCH  (VAR, BEGIN, END, ALT)
-        Element lies within an absorbable region.  Used at runtime
-        to track whether the current NFA state is in an absorbable
-        context.
+        Element lies within an absorbable region.  Used at runtime to
+        track whether the current NFA state is in an absorbable
+        context. See "IV-5. Absorbability Analysis" and
+        "VIII-2. Solution: Context Absorption" for more details about
+        absorption.
 
   0x08  RPR_ELEM_ABSORBABLE         (VAR, END)
         Absorption judgment point.  Where to compare consecutive
@@ -514,7 +521,10 @@ V-3. RPR Fields of WindowAggState
   nfaStateFree                  Reuse pool for states
   nfaVarMatched                 Per-row cache: varMatched[varId]
   nfaVisitedElems               Bitmap for cycle detection
+  nfaVisitedNWords              Number of bitmapwords in nfaVisitedElems
   nfaStateSize                  Precomputed size of RPRNFAState
+  defineMatchStartDependent     DEFINE vars needing per-context evaluation (match_start-dependent)
+  nfaLastProcessedRow           Last row processed by NFA (-1 = none)
 
 Memory management:
 
@@ -1053,6 +1063,10 @@ X-3. INITIAL vs SEEK
 
 X-4. Bounded Frame Handling
 
+  With RPR, the frame mode is always ROWS and the frame start must be
+  CURRENT ROW. The frame end can be either UNBOUNDED FOLLOWING or n
+  FOLLOWING.
+
   When the frame is bounded (e.g., ROWS BETWEEN CURRENT ROW AND 5
   FOLLOWING), ExecRPRProcessRow receives hasLimitedFrame=true and
   frameOffset indicating the upper bound.  Before the match phase,
@@ -1579,6 +1593,15 @@ C-7. PATTERN ((A+ B | C*)+ D)  -- Per-branch absorption in ALT
   nullable.
   BEGIN and ALT get ABSORBABLE_BRANCH (on the path to absorbable elements).
 
+
+References:
+
+[1] ISO/IEC 19075-5 Information technology - Guidance for the use of
+    database language SQL - Part 5: Row pattern recognition
+
+[2] ISO/IEC 9075-2 Information technology - Database languages - SQL -
+    Part 2: Foundation (SQL/Foundation)
+
 ============================================================================
   End of document
 ============================================================================
-- 
2.50.1 (Apple Git-155)


From e989f94078b49439052bcaeda38afbd74a2fc057 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Tue, 12 May 2026 15:43:49 +0900
Subject: [PATCH 15/68] Round out README.rpr WindowAggState field coverage

Follow-up to the previous commit applying Tatsuo Ishii's
review.  That commit added three WindowAggState fields to
V-3 but left a few related entries out, and Appendix B's
diagram still showed the pre-review field list.

- Add nfaVisitedMinWord and nfaVisitedMaxWord to V-3
- Note that EXPLAIN ANALYZE instrumentation counters are
  omitted from V-3 (see execnodes.h)
- Mirror the V-3 additions in the Appendix B diagram
---
 src/backend/executor/README.rpr | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git a/src/backend/executor/README.rpr b/src/backend/executor/README.rpr
index 6c2bddab455..6ff7f33e62e 100644
--- a/src/backend/executor/README.rpr
+++ b/src/backend/executor/README.rpr
@@ -522,10 +522,15 @@ V-3. RPR Fields of WindowAggState
   nfaVarMatched                 Per-row cache: varMatched[varId]
   nfaVisitedElems               Bitmap for cycle detection
   nfaVisitedNWords              Number of bitmapwords in nfaVisitedElems
+  nfaVisitedMinWord             Lowest bitmapword index touched since last reset
+  nfaVisitedMaxWord             Highest bitmapword index touched since last reset
   nfaStateSize                  Precomputed size of RPRNFAState
   defineMatchStartDependent     DEFINE vars needing per-context evaluation (match_start-dependent)
   nfaLastProcessedRow           Last row processed by NFA (-1 = none)
 
+  EXPLAIN ANALYZE instrumentation counters are omitted here; see
+  execnodes.h for the full list.
+
 Memory management:
 
   States and contexts are managed through their own free lists.
@@ -1480,7 +1485,13 @@ Appendix B. Data Structure Relationship Diagram
     |--- defineVariableList: List<String> (variable names, DEFINE order)
     |--- defineClauseList: List<ExprState>
     |--- nfaVarMatched: bool[] (per-row cache)
+    |--- defineMatchStartDependent: Bitmapset* (match_start-dependent
+    |        DEFINE vars; see VI-4)
     |--- nfaVisitedElems: bitmapword* (cycle detection)
+    |--- nfaVisitedNWords: int (size of nfaVisitedElems)
+    |--- nfaVisitedMinWord / nfaVisitedMaxWord: int16
+    |        (touched-word range for fast reset)
+    |--- nfaLastProcessedRow: int64 (-1 = none)
     |--- nfaStateSize: Size (pre-calculated RPRNFAState allocation size)
     |--- nfaContext <-> nfaContextTail (doubly-linked list)
     |   +--- RPRNFAContext
-- 
2.50.1 (Apple Git-155)


From 476a8f0a9aa2a5fca573ff608d16d3a4d35b360e Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Sun, 10 May 2026 14:42:09 +0900
Subject: [PATCH 13/68] Reject row pattern recognition in recursive queries

Per ISO/IEC 9075-2:2016 7.17 Syntax Rule 3)e)f), every <with list
element> in a WITH RECURSIVE clause is "potentially recursive" and
shall not contain a <row pattern common syntax>.  ISO/IEC 19075-5
6.17.5 (R020) and 4.18.5 (R010) restate the prohibition for CREATE
RECURSIVE VIEW, which makeRecursiveViewSelect() rewrites to WITH
RECURSIVE so the same path catches both forms.

The rejection runs in transformWithClause() against the raw parse
tree, before per-CTE analysis, and reports the PATTERN keyword
position via a new RPCommonSyntax.location field captured in
gram.y.  Flips both rpr_integration B7 cases (added in the
preceding commit) from result rows to the new error.
---
 src/backend/parser/gram.y                     |  1 +
 src/backend/parser/parse_cte.c                | 57 +++++++++++++++++++
 src/include/nodes/parsenodes.h                |  1 +
 src/test/regress/expected/rpr_integration.out | 23 ++------
 src/test/regress/sql/rpr_integration.sql      |  1 -
 src/tools/pgindent/typedefs.list              |  1 +
 6 files changed, 66 insertions(+), 18 deletions(-)

diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index aa587e6aced..a2fafb717cd 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -17585,6 +17585,7 @@ opt_row_pattern_skip_to opt_row_pattern_initial_or_seek
 				n->initial = $2;
 				n->rpPattern = (RPRPatternNode *) $5;
 				n->rpDefs = $8;
+				n->location = @3;
 				$$ = (Node *) n;
 			}
 			| /*EMPTY*/		{ $$ = NULL; }
diff --git a/src/backend/parser/parse_cte.c b/src/backend/parser/parse_cte.c
index ccde199319a..0974b43d028 100644
--- a/src/backend/parser/parse_cte.c
+++ b/src/backend/parser/parse_cte.c
@@ -96,6 +96,14 @@ static void checkWellFormedRecursion(CteState *cstate);
 static bool checkWellFormedRecursionWalker(Node *node, CteState *cstate);
 static void checkWellFormedSelectStmt(SelectStmt *stmt, CteState *cstate);
 
+/* Recursive-WITH RPR rejection */
+typedef struct
+{
+	ParseLoc	location;		/* location of first RPR window, or -1 */
+} ContainRPRContext;
+
+static bool contain_rpr_walker(Node *node, void *context);
+
 
 /*
  * transformWithClause -
@@ -164,6 +172,29 @@ transformWithClause(ParseState *pstate, WithClause *withClause)
 		CteState	cstate;
 		int			i;
 
+		/*
+		 * Per ISO/IEC 9075-2:2016 7.17 Syntax Rule 3)e)f), every <with list
+		 * element> in a WITH RECURSIVE clause is "potentially recursive" and
+		 * shall not contain a <row pattern common syntax>.  (PostgreSQL does
+		 * not implement <row pattern measures>, so only the common syntax
+		 * needs to be checked.)  ISO/IEC 19075-5 6.17.5 (R020) and 4.18.5
+		 * (R010) restate the prohibition for CREATE RECURSIVE VIEW, which is
+		 * rewritten to WITH RECURSIVE by makeRecursiveViewSelect() and so
+		 * flows through here as well.
+		 */
+		foreach(lc, withClause->ctes)
+		{
+			CommonTableExpr *cte = (CommonTableExpr *) lfirst(lc);
+			ContainRPRContext ctx;
+
+			ctx.location = -1;
+			if (contain_rpr_walker(cte->ctequery, &ctx))
+				ereport(ERROR,
+						errcode(ERRCODE_SYNTAX_ERROR),
+						errmsg("cannot use row pattern recognition in a recursive query"),
+						parser_errposition(pstate, ctx.location));
+		}
+
 		cstate.pstate = pstate;
 		cstate.numitems = list_length(withClause->ctes);
 		cstate.items = (CteItem *) palloc0(cstate.numitems * sizeof(CteItem));
@@ -1268,3 +1299,29 @@ checkWellFormedSelectStmt(SelectStmt *stmt, CteState *cstate)
 		}
 	}
 }
+
+
+/*
+ * contain_rpr_walker
+ *	  Returns true if the raw parse tree contains any <row pattern common
+ *	  syntax> -- i.e., any WindowDef with PATTERN/DEFINE attached.  Used
+ *	  by transformWithClause() to enforce ISO/IEC 9075-2:2016 7.17 SR 3)f)
+ *	  on WITH RECURSIVE elements.
+ */
+static bool
+contain_rpr_walker(Node *node, void *context)
+{
+	if (node == NULL)
+		return false;
+	if (IsA(node, WindowDef))
+	{
+		WindowDef  *wd = (WindowDef *) node;
+
+		if (wd->rpCommonSyntax != NULL)
+		{
+			((ContainRPRContext *) context)->location = wd->rpCommonSyntax->location;
+			return true;
+		}
+	}
+	return raw_expression_tree_walker(node, contain_rpr_walker, context);
+}
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index adefb1d5bad..5200182aa46 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -646,6 +646,7 @@ typedef struct RPCommonSyntax
 	RPRPatternNode *rpPattern;	/* PATTERN clause AST */
 	List	   *rpDefs;			/* row pattern definitions clause (list of
 								 * ResTarget) */
+	ParseLoc	location;		/* PATTERN keyword location, or -1 */
 } RPCommonSyntax;
 
 /*
diff --git a/src/test/regress/expected/rpr_integration.out b/src/test/regress/expected/rpr_integration.out
index 0b05a826a27..b598ef95776 100644
--- a/src/test/regress/expected/rpr_integration.out
+++ b/src/test/regress/expected/rpr_integration.out
@@ -1292,22 +1292,9 @@ WITH RECURSIVE seq AS (
     SELECT id + 100, val, cnt FROM seq WHERE id < 3
 )
 SELECT id, val, cnt FROM seq ORDER BY id;
- id  | val | cnt 
------+-----+-----
-   1 |  10 |   2
-   2 |  20 |   0
-   3 |  15 |   2
-   4 |  25 |   0
-   5 |   5 |   3
-   6 |  30 |   0
-   7 |  35 |   0
-   8 |  20 |   3
-   9 |  40 |   0
-  10 |  45 |   0
- 101 |  10 |   2
- 102 |  20 |   0
-(12 rows)
-
+ERROR:  cannot use row pattern recognition in a recursive query
+LINE 6:         PATTERN (A B+)
+                ^
 -- CREATE RECURSIVE VIEW: rewritten by makeRecursiveViewSelect()
 -- into WITH RECURSIVE, so the same rejection applies.  This is
 -- the form ISO/IEC 19075-5 6.17.5 cites verbatim.
@@ -1318,7 +1305,9 @@ CREATE RECURSIVE VIEW rpr_recv(id, val, cnt) AS
         ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
         PATTERN (A B+)
         DEFINE B AS val > PREV(val));
-DROP VIEW rpr_recv;
+ERROR:  cannot use row pattern recognition in a recursive query
+LINE 6:         PATTERN (A B+)
+                ^
 -- ============================================================
 -- B8. RPR + Incremental sort
 -- ============================================================
diff --git a/src/test/regress/sql/rpr_integration.sql b/src/test/regress/sql/rpr_integration.sql
index bc8f4712bcb..5f3853becba 100644
--- a/src/test/regress/sql/rpr_integration.sql
+++ b/src/test/regress/sql/rpr_integration.sql
@@ -818,7 +818,6 @@ CREATE RECURSIVE VIEW rpr_recv(id, val, cnt) AS
         ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
         PATTERN (A B+)
         DEFINE B AS val > PREV(val));
-DROP VIEW rpr_recv;
 
 -- ============================================================
 -- B8. RPR + Incremental sort
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 1970ca5da14..24cf2eb7860 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -532,6 +532,7 @@ Constraint
 ConstraintCategory
 ConstraintInfo
 ConstraintsSetStmt
+ContainRPRContext
 ControlData
 ControlFileData
 ConvInfo
-- 
2.50.1 (Apple Git-155)


From ddf9bbf4d9a03b0377ef06556cf0dbba8f1427bb Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Wed, 27 May 2026 07:02:38 +0900
Subject: [PATCH 16/68] Add raw_expression_tree_walker coverage for RPR raw
 nodes

WindowDef.rpCommonSyntax was not walked, and there were no case
arms for T_RPCommonSyntax or T_RPRPatternNode.  RPR core was
unaffected -- contain_rpr_walker() in parse_cte.c intercepts
WindowDef before delegating -- but debug_raw_expression_coverage_test
silently skipped these subtrees, leaving any future raw-node
omission on the RPR side undetected.
---
 src/backend/nodes/nodeFuncs.c | 20 ++++++++++++++++++++
 1 file changed, 20 insertions(+)

diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index 734bb0554fe..101c03b6ae8 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -4641,6 +4641,8 @@ raw_expression_tree_walker_impl(Node *node,
 					return true;
 				if (WALK(wd->endOffset))
 					return true;
+				if (WALK(wd->rpCommonSyntax))
+					return true;
 			}
 			break;
 		case T_RangeSubselect:
@@ -4896,6 +4898,24 @@ raw_expression_tree_walker_impl(Node *node,
 					return true;
 			}
 			break;
+		case T_RPCommonSyntax:
+			{
+				RPCommonSyntax *rc = (RPCommonSyntax *) node;
+
+				if (WALK(rc->rpPattern))
+					return true;
+				if (WALK(rc->rpDefs))
+					return true;
+			}
+			break;
+		case T_RPRPatternNode:
+			{
+				RPRPatternNode *rp = (RPRPatternNode *) node;
+
+				if (WALK(rp->children))
+					return true;
+			}
+			break;
 		default:
 			elog(ERROR, "unrecognized node type: %d",
 				 (int) nodeTag(node));
-- 
2.50.1 (Apple Git-155)


From e0de323a26a95fb235c053c1a1bbb4d8b809e3f7 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Wed, 27 May 2026 13:34:23 +0900
Subject: [PATCH 17/68] Enhance README.rpr per Jian He's review

- Add an intuition summary at the top of Chapter VIII naming
  what context absorption is and the monotonicity principle
  that makes it safe, so the reader meets the core idea before
  the O(N^2) problem framing.
- Add a worked PATTERN (A+) trace at the end of VIII-2 to make
  the state/count dominance comparison concrete.
- Expand "DFS" to "Depth-First Search (DFS)" at first occurrence.
- Replace the stale "nfa_advance(initialAdvance=true)" reference
  in VI-2 with the current signature.
---
 src/backend/executor/README.rpr | 34 +++++++++++++++++++++++++++++++--
 1 file changed, 32 insertions(+), 2 deletions(-)

diff --git a/src/backend/executor/README.rpr b/src/backend/executor/README.rpr
index 6ff7f33e62e..6d40bd70faa 100644
--- a/src/backend/executor/README.rpr
+++ b/src/backend/executor/README.rpr
@@ -354,7 +354,8 @@ The flag is set on all elements that carry the quantifier:
   Simple VAR (A+?):     RPR_ELEM_RELUCTANT on the VAR element
   Group ((...)+?):      RPR_ELEM_RELUCTANT on BEGIN and END elements
 
-At runtime (nfa_advance), the flag controls DFS exploration order:
+At runtime (nfa_advance), the flag controls Depth-First Search
+(DFS) exploration order:
 
   VAR with quantifier:
     Greedy:    primary path = next (continue), clone = jump (skip)
@@ -582,7 +583,8 @@ Creates a new context and performs the initial advance.
   (2) Set matchStartRow = pos
   (3) Create initial state: elemIdx=0 (first pattern element),
       counts=all zero
-  (4) Call nfa_advance(initialAdvance=true)
+  (4) Call nfa_advance() with currentPos = pos - 1 (no row consumed
+      yet)
 
 The initial advance expands epsilon transitions at the beginning of
 the pattern. For example, the initial advance for PATTERN ((A | B) C):
@@ -737,6 +739,15 @@ Immediate advance for simple VARs:
 Chapter VIII  Phase 2: Absorb (Context Absorption)
 ============================================================================
 
+Absorption is the runtime optimization that collapses contexts which
+have converged on identical future behavior.  Two contexts are
+treated as equivalent when one's bookkeeping (elemIdx and per-depth
+iteration counts) is dominated by another's; the younger one is then
+discarded.  The optimization is safe because pattern matching is
+monotonic -- an earlier context's reachable matches always contain a
+later context's.  This is what reduces the naive O(N^2) state count
+to O(N).
+
 VIII-1. Problem
 
 In the current implementation, a new context is started for each row
@@ -762,6 +773,25 @@ is already contained within Context 1.
 
 Therefore Context 2 can be "absorbed" into Context 1.
 
+Worked example for PATTERN (A+) over 3 rows (each matches A):
+
+  After row 1:
+    Ctx_1 (started row 1): state at A with counts[0] = 1
+
+  After row 2:
+    Ctx_1: state at A with counts[0] = 2
+    Ctx_2 (started row 2): state at A with counts[0] = 1
+    -> Same elemIdx; Ctx_1.count (2) dominates Ctx_2.count (1).
+    -> Ctx_2 absorbed.
+
+  After row 3:
+    Ctx_1: state at A with counts[0] = 3
+    Ctx_3 (started row 3): state at A with counts[0] = 1
+    -> Ctx_1.count (3) dominates Ctx_3.count (1).
+    -> Ctx_3 absorbed.
+
+Total active contexts stays at O(1) instead of growing with N.
+
 VIII-3. Absorption Conditions
 
 Planner-time prerequisites (all must hold for absorption to be enabled):
-- 
2.50.1 (Apple Git-155)


From fe90e09297dbb9e16123ffbdd200ca7c4b6b95d4 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Wed, 27 May 2026 14:02:19 +0900
Subject: [PATCH 19/68] Change nfa_add_state_unique signature from bool to void

The return value is leftover from an earlier design.  All four
callers ignore it, and the duplicate-found case is fully handled
inside the function (the new state is freed and nfaStatesMerged is
incremented).  Drop the return value and update the doc comment.
---
 src/backend/executor/execRPR.c | 12 +++++-------
 1 file changed, 5 insertions(+), 7 deletions(-)

diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 261e1209744..88c59cf3276 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -61,7 +61,7 @@ static RPRNFAState *nfa_state_create(WindowAggState *winstate, int16 elemIdx,
 									 int32 *counts, bool sourceAbsorbable);
 static bool nfa_states_equal(WindowAggState *winstate, RPRNFAState *s1,
 							 RPRNFAState *s2);
-static bool nfa_add_state_unique(WindowAggState *winstate, RPRNFAContext *ctx,
+static void nfa_add_state_unique(WindowAggState *winstate, RPRNFAContext *ctx,
 								 RPRNFAState *state);
 static void nfa_add_matched_state(WindowAggState *winstate, RPRNFAContext *ctx,
 								  RPRNFAState *state, int64 matchEndRow);
@@ -335,10 +335,10 @@ nfa_states_equal(WindowAggState *winstate, RPRNFAState *s1, RPRNFAState *s2)
  * nfa_add_state_unique
  *
  * Add a state to ctx->states at the END, only if no duplicate exists.
- * Returns true if state was added, false if duplicate found (state is freed).
- * Earlier states have better lexical order (DFS traversal order), so existing wins.
+ * Earlier states have better lexical order (DFS traversal order), so existing
+ * wins; the new state is freed when a duplicate is found.
  */
-static bool
+static void
 nfa_add_state_unique(WindowAggState *winstate, RPRNFAContext *ctx, RPRNFAState *state)
 {
 	RPRNFAState *s;
@@ -365,7 +365,7 @@ nfa_add_state_unique(WindowAggState *winstate, RPRNFAContext *ctx, RPRNFAState *
 			 */
 			nfa_state_free(winstate, state);
 			winstate->nfaStatesMerged++;
-			return false;
+			return;
 		}
 		tail = s;
 	}
@@ -376,8 +376,6 @@ nfa_add_state_unique(WindowAggState *winstate, RPRNFAContext *ctx, RPRNFAState *
 		ctx->states = state;
 	else
 		tail->next = state;
-
-	return true;
 }
 
 /*
-- 
2.50.1 (Apple Git-155)


From a0a9d1581e728a564bbd481f2854a4a28209a874 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Wed, 27 May 2026 14:15:09 +0900
Subject: [PATCH 20/68] Add reluctant bounded mid-band test to rpr_nfa

PATTERN (A{3,5}? B) drives the VAR-level count in nfa_advance_var
through 3, 4, 5 within a single match attempt -- a band the
existing A{1,3}? B test does not exercise, because A and B match
the same row there and advance's early-termination path frees the
loop state before nfa_advance_var sees count > 2.

This explicitly exercises the count > 2 && reluctant &&
!isAbsorbable path that absorbability analysis structurally
constrains (reluctant quantifiers are excluded, so isAbsorbable
stays false).
---
 src/test/regress/expected/rpr_nfa.out | 38 +++++++++++++++++++++++++++
 src/test/regress/sql/rpr_nfa.sql      | 29 ++++++++++++++++++++
 2 files changed, 67 insertions(+)

diff --git a/src/test/regress/expected/rpr_nfa.out b/src/test/regress/expected/rpr_nfa.out
index fe5bb324df0..1f494d2db34 100644
--- a/src/test/regress/expected/rpr_nfa.out
+++ b/src/test/regress/expected/rpr_nfa.out
@@ -2237,6 +2237,44 @@ WINDOW w AS (
   4 | {B,_} |             |          
 (4 rows)
 
+-- A{3,5}? B (reluctant bounded mid-band): the VAR-level count in
+-- nfa_advance_var cycles through 3, 4, 5 within a single match
+-- attempt.  Exercises the count > 2 && reluctant && !isAbsorbable
+-- branch (absorbability analysis excludes reluctant quantifiers, so
+-- isAbsorbable stays false for A).
+WITH test_reluctant_mid_band AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),
+        (4, ARRAY['A']),
+        (5, ARRAY['A']),
+        (6, ARRAY['B'])
+    ) AS t(id, flags)
+)
+SELECT id, flags,
+       first_value(id) OVER w AS match_start,
+       last_value(id) OVER w AS match_end
+FROM test_reluctant_mid_band
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A{3,5}? B)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {A}   |           1 |         6
+  2 | {A}   |             |          
+  3 | {A}   |             |          
+  4 | {A}   |             |          
+  5 | {A}   |             |          
+  6 | {B}   |             |          
+(6 rows)
+
 -- ============================================================
 -- Pathological Pattern Runtime Protection
 -- ============================================================
diff --git a/src/test/regress/sql/rpr_nfa.sql b/src/test/regress/sql/rpr_nfa.sql
index 7a5b5c41b24..76dfc4d88bc 100644
--- a/src/test/regress/sql/rpr_nfa.sql
+++ b/src/test/regress/sql/rpr_nfa.sql
@@ -1554,6 +1554,35 @@ WINDOW w AS (
         B AS 'B' = ANY(flags)
 );
 
+-- A{3,5}? B (reluctant bounded mid-band): the VAR-level count in
+-- nfa_advance_var cycles through 3, 4, 5 within a single match
+-- attempt.  Exercises the count > 2 && reluctant && !isAbsorbable
+-- branch (absorbability analysis excludes reluctant quantifiers, so
+-- isAbsorbable stays false for A).
+WITH test_reluctant_mid_band AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),
+        (4, ARRAY['A']),
+        (5, ARRAY['A']),
+        (6, ARRAY['B'])
+    ) AS t(id, flags)
+)
+SELECT id, flags,
+       first_value(id) OVER w AS match_start,
+       last_value(id) OVER w AS match_end
+FROM test_reluctant_mid_band
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A{3,5}? B)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags)
+);
+
 -- ============================================================
 -- Pathological Pattern Runtime Protection
 -- ============================================================
-- 
2.50.1 (Apple Git-155)


From 1a5893d5d23d3787065192fb63bdeec1719169ba Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Wed, 27 May 2026 13:44:55 +0900
Subject: [PATCH 18/68] Clarify execRPR.c comments and tighten an Assert per
 Jian He's review

- nfa_advance_var: add Assert that elem->next is within bounds, as
  any reachable VAR's next pointer must be a valid index.
- nfa_advance: document the state->next reset point as the boundary
  contract for the epsilon-expansion DFS, naming the other linking
  site (nfa_add_state_unique) as the pair.
- nfa_add_state_unique: back-reference the asymmetric visited-marking
  scheme, whose primary explanation sits in nfa_advance_state.
- nfa_states_equal: rewrite the compareDepth comment to explain both
  the +1 slot arithmetic and why deeper slots are excluded.
- nfa_advance_begin: replace the misleading "Greedy: enter first,
  skip second" label with one that covers both greedy-optional and
  non-nullable cases.
- nfa_advance_alt: explain when the depth break actually fires
  (quantified-group last branch) and why <= is the correct relation.
- ExecRPRFinalizeAllContexts: reframe as the partition-end
  classification policy holder, enumerating the three context shapes
  that survive into Finalize and how each is handled.
---
 src/backend/executor/execRPR.c | 75 ++++++++++++++++++++++++++++++----
 1 file changed, 68 insertions(+), 7 deletions(-)

diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index e1caa7bb528..261e1209744 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -311,9 +311,19 @@ nfa_states_equal(WindowAggState *winstate, RPRNFAState *s1, RPRNFAState *s2)
 	if (s1->elemIdx != s2->elemIdx)
 		return false;
 
-	/* Compare counts up to current element's depth */
+	/*
+	 * Compare counts up to current element's depth.  Two states sharing
+	 * elemIdx are equivalent iff every enclosing-or-current depth count
+	 * matches.
+	 *
+	 * The +1 is the slot arithmetic: comparing through depth N requires
+	 * counts[0..N], i.e., N+1 entries.  Deeper slots (counts[d] with d >
+	 * elem->depth) are excluded because they hold scratch state from inner
+	 * groups that gets zeroed on re-entry (see END loop-back in
+	 * nfa_advance_end), and so must not participate in equivalence judgment.
+	 */
 	elem = &pattern->elements[s1->elemIdx];
-	compareDepth = elem->depth + 1; /* depth 0 needs 1 count, etc. */
+	compareDepth = elem->depth + 1;
 
 	if (memcmp(s1->counts, s2->counts, sizeof(int32) * compareDepth) != 0)
 		return false;
@@ -334,7 +344,12 @@ nfa_add_state_unique(WindowAggState *winstate, RPRNFAContext *ctx, RPRNFAState *
 	RPRNFAState *s;
 	RPRNFAState *tail = NULL;
 
-	/* Mark VAR in visited before duplicate check to prevent DFS loops */
+	/*
+	 * Mark VAR in visited before duplicate check to prevent DFS loops. This
+	 * is the deferred half of the asymmetric visited-marking scheme; see
+	 * nfa_advance_state for the non-VAR (END/ALT/BEGIN/FIN) half and the
+	 * rationale for the asymmetry.
+	 */
 	nfa_mark_visited(winstate, state->elemIdx);
 
 	/* Check for duplicate and find tail */
@@ -950,7 +965,16 @@ nfa_advance_alt(WindowAggState *winstate, RPRNFAContext *ctx,
 		RPRPatternElement *altElem = &elements[altIdx];
 		RPRNFAState *newState;
 
-		/* Stop if element is outside ALT scope (not a branch) */
+		/*
+		 * Stop if element is outside ALT scope (not a branch).  The check
+		 * fires when the last branch is a quantified group whose BEGIN.jump
+		 * (set by fillRPRPatternGroup) is preserved -- not overridden by
+		 * fillRPRPatternAlt, which only links non-last branch heads -- and
+		 * leads to a post-ALT element.  Other branch shapes terminate the
+		 * walk earlier via altIdx = RPR_ELEMIDX_INVALID.  Use <=, not <: the
+		 * post-ALT element may sit at the same depth as the ALT when the ALT
+		 * has a sibling at that level.
+		 */
 		if (altElem->depth <= elem->depth)
 			break;
 
@@ -1016,7 +1040,12 @@ nfa_advance_begin(WindowAggState *winstate, RPRNFAContext *ctx,
 	}
 	else
 	{
-		/* Greedy: enter first, skip second */
+		/*
+		 * Greedy-or-non-nullable: route to the first child.  For optional
+		 * groups (skipState != NULL, greedy min=0) additionally create the
+		 * skip path; for non-nullable groups (skipState == NULL, min>0) the
+		 * skip-path action is suppressed by the guard below.
+		 */
 		state->elemIdx = elem->next;
 		nfa_route_to_elem(winstate, ctx, state,
 						  &elements[state->elemIdx], currentPos);
@@ -1205,6 +1234,9 @@ nfa_advance_var(WindowAggState *winstate, RPRNFAContext *ctx,
 	/* After a successful match, count >= 1, so at least one must be true */
 	Assert(canLoop || canExit);
 
+	/* elem->next must be a valid index for any reachable VAR */
+	Assert(elem->next >= 0 && elem->next < pattern->numElements);
+
 	if (canLoop && canExit)
 	{
 		/*
@@ -1428,6 +1460,15 @@ nfa_advance(WindowAggState *winstate, RPRNFAContext *ctx, int64 currentPos)
 
 		state = states;
 		states = states->next;
+
+		/*
+		 * Boundary contract: state->next is reset to NULL here, before
+		 * crossing into nfa_advance_state's epsilon-expansion DFS.  The inner
+		 * branches (nfa_advance_var, nfa_advance_begin/end/alt) treat
+		 * state->next as already-NULL and don't reset it themselves; the
+		 * other linking site is nfa_add_state_unique, which sets it when
+		 * appending to ctx->states.
+		 */
 		state->next = NULL;
 
 		nfa_advance_state(winstate, ctx, state, currentPos);
@@ -1779,8 +1820,28 @@ ExecRPRCleanupDeadContexts(WindowAggState *winstate, RPRNFAContext *excludeCtx)
 /*
  * ExecRPRFinalizeAllContexts
  *
- * Finalize all active contexts when partition ends.
- * Match with NULL to force mismatch, then advance to process epsilon transitions.
+ * Partition-end classification policy: kill any VAR states still pursuing
+ * when rows run out, so cleanup sees a uniform ctx->states == NULL across
+ * every context.  By the time this runs, all genuine FIN reaches have
+ * already been recorded in-flight; three shapes survive here:
+ *
+ *   - Pure pursuit (matchedState == NULL): VAR states waiting for input
+ *     that never arrives (e.g., A+ B mid-pattern at partition end).
+ *   - Empty-match candidate + pursuit (matchedState != NULL,
+ *     matchEndRow < matchStartRow): initial-advance FIN-via-skip recorded
+ *     an empty match while VAR states are still chasing a longer one
+ *     (e.g., greedy A*).
+ *   - Real match + pursuit (matchedState != NULL,
+ *     matchEndRow >= matchStartRow): a match has been recorded and VAR
+ *     states are still looping for a longer one.
+ *
+ * Killing the VAR reclassifies the first two as failures in cleanup
+ * (otherwise they linger without contributing to stats).  The third is
+ * stat-neutral -- cleanup skips it either way -- but goes through the
+ * same uniform path so partition-end classification stays centralized.
+ *
+ * Implementation: nfa_match with NULL forces VAR mismatch; nfa_advance
+ * then drains any remaining epsilon transitions.
  */
 void
 ExecRPRFinalizeAllContexts(WindowAggState *winstate, int64 lastPos)
-- 
2.50.1 (Apple Git-155)


From d74ee1d2a2ad85dcb3198f13352d4dcf231e3c35 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Fri, 29 May 2026 17:45:13 +0900
Subject: [PATCH 21/68] Define RPR absorption terminology in README.rpr per
 Jian He's review

- Define the "match_start dep." column values (none, direct,
  boundary check) in VIII-3; the table listed them without saying
  what "direct" versus "boundary check" actually mean.
- Fix the "boundary chk" typo in that table to "boundary check".
- Name the cover condition "count-dominance" and spell out the
  comparison, linking back to the count-dominance reference in
  VIII-3(c).
- Rename the prose term "match_start-dependent" to
  "match_start_dependent" to match the defineMatchStartDependent
  identifier.
---
 src/backend/executor/README.rpr | 37 ++++++++++++++++++++++++++-------
 1 file changed, 30 insertions(+), 7 deletions(-)

diff --git a/src/backend/executor/README.rpr b/src/backend/executor/README.rpr
index 6d40bd70faa..1d211245a5b 100644
--- a/src/backend/executor/README.rpr
+++ b/src/backend/executor/README.rpr
@@ -526,7 +526,7 @@ V-3. RPR Fields of WindowAggState
   nfaVisitedMinWord             Lowest bitmapword index touched since last reset
   nfaVisitedMaxWord             Highest bitmapword index touched since last reset
   nfaStateSize                  Precomputed size of RPRNFAState
-  defineMatchStartDependent     DEFINE vars needing per-context evaluation (match_start-dependent)
+  defineMatchStartDependent     DEFINE vars needing per-context evaluation (match_start_dependent)
   nfaLastProcessedRow           Last row processed by NFA (-1 = none)
 
   EXPLAIN ANALYZE instrumentation counters are omitted here; see
@@ -631,7 +631,7 @@ the same row.
 
 The varMatched array is referenced later in Phase 1 (Match).
 
-VI-4. Per-Context Re-evaluation (match_start-dependent variables)
+VI-4. Per-Context Re-evaluation (match_start_dependent variables)
 
 DEFINE variables that depend on match_start (those containing FIRST,
 LAST-with-offset, or compound PREV_FIRST/NEXT_FIRST/PREV_LAST/NEXT_LAST)
@@ -801,7 +801,7 @@ Planner-time prerequisites (all must hold for absorption to be enabled):
   (b) Unbounded frame (ROWS BETWEEN CURRENT ROW AND UNBOUNDED
       FOLLOWING).  Limited frames apply differently to each context,
       breaking the monotonicity principle.
-  (c) No match_start-dependent navigation in DEFINE.
+  (c) No match_start_dependent navigation in DEFINE.
 
       Mechanism: each context has a different matchStartRow, so FIRST
       resolves to a different row for each context at the same
@@ -820,7 +820,27 @@ Planner-time prerequisites (all must hold for absorption to be enabled):
       FIRST (any)                     direct            unsafe
       Compound (inner FIRST)          direct            unsafe
       Compound (inner LAST, no off.)  none              safe
-      Compound (inner LAST, w/off.)   boundary chk      unsafe
+      Compound (inner LAST, w/off.)   boundary check    unsafe
+
+  The "match_start dep." column classifies how the navigation ties a
+  DEFINE result to the context's matchStartRow:
+
+    none            Independent of matchStartRow.  The result depends
+                    only on currentpos (or a fixed offset from it), so
+                    every context evaluates it identically.
+    direct          Computed from matchStartRow itself -- FIRST counts
+                    forward from match start -- so the resolved row,
+                    and thus the result, differs per context.
+    boundary check  The resolved row is currentpos-relative (LAST with
+                    a backward offset, or a compound whose inner LAST
+                    carries an offset), but its in-range test is taken
+                    against the match range [matchStartRow, currentpos].
+                    The range bound differs per context, so the result
+                    can too.
+
+  Only "none" is safe for absorption; "direct" and "boundary check"
+  both make an earlier context's result stop subsuming a later one's
+  (see (c) above).
 
 Runtime conditions (evaluated per context pair):
 
@@ -828,10 +848,13 @@ Runtime conditions (evaluated per context pair):
   (2) allStatesAbsorbable of the target context is true
   (3) An earlier context "covers" all states of the target
 
-Cover condition (nfa_states_covered):
+Cover condition (nfa_states_covered) -- "count-dominance":
 
   A state with the same elemIdx exists in the earlier context,
-  and the count at that depth is greater than or equal -- then it is covered.
+  and the count at that depth is greater than or equal -- then it is
+  covered.  The earlier context's per-depth iteration count thus
+  dominates the later one's; this is the count-dominance comparison
+  referenced in VIII-3(c).
 
 VIII-4. Dual-Flag Design
 
@@ -1515,7 +1538,7 @@ Appendix B. Data Structure Relationship Diagram
     |--- defineVariableList: List<String> (variable names, DEFINE order)
     |--- defineClauseList: List<ExprState>
     |--- nfaVarMatched: bool[] (per-row cache)
-    |--- defineMatchStartDependent: Bitmapset* (match_start-dependent
+    |--- defineMatchStartDependent: Bitmapset* (match_start_dependent
     |        DEFINE vars; see VI-4)
     |--- nfaVisitedElems: bitmapword* (cycle detection)
     |--- nfaVisitedNWords: int (size of nfaVisitedElems)
-- 
2.50.1 (Apple Git-155)


From 75355c0e115a1874f8833520ae89bcc165867263 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Fri, 29 May 2026 17:51:37 +0900
Subject: [PATCH 22/68] Document the get_reduced_frame_status cascade invariant
 per Jian He's review

The RF_* classifier is an early-return cascade whose branches are not
mutually exclusive, so reordering them changes the result.  This is the
standard cascade idiom -- each branch is a minimal test premised on the
negations the preceding returns have established -- not a logic flaw, but
the structure left the contract implicit.

update_reduced_frame() records the match as exactly one of three
(rpr_match_matched, rpr_match_length) shapes: (false, 1), (true, 0), or
(true, >= 1).  Spell that out in the header, add the missing RF_EMPTY_MATCH
return value, and annotate each branch with a "by here" note stating the
running invariant -- notably why the empty match must be classified before
the range test.  No behavior change.
---
 src/backend/executor/nodeWindowAgg.c | 28 ++++++++++++++++++++++++++--
 1 file changed, 26 insertions(+), 2 deletions(-)

diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 99858d22dad..4cf1a9ac67b 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -4306,6 +4306,16 @@ clear_reduced_frame(WindowAggState *winstate)
  *   RF_FRAME_HEAD      pos is the start of the current match
  *   RF_SKIPPED         pos is inside the current match but not the start
  *   RF_UNMATCHED       pos is processed but not part of any match
+ *   RF_EMPTY_MATCH     pos is the start of an empty (zero-length) match
+ *
+ * update_reduced_frame() records the current match as exactly one of three
+ * (rpr_match_matched, rpr_match_length) shapes: (false, 1) for unmatched,
+ * (true, 0) for an empty match, and (true, >= 1) for a real match.  The
+ * tests below form a cascade with early returns: each is a minimal check
+ * that relies on the negations the preceding returns have already
+ * established, so their order is significant.  The "by here" notes spell
+ * out the running invariant; reordering a test would misclassify one of
+ * the three shapes.
  */
 static int
 get_reduced_frame_status(WindowAggState *winstate, int64 pos)
@@ -4316,17 +4326,31 @@ get_reduced_frame_status(WindowAggState *winstate, int64 pos)
 	if (!winstate->rpr_match_valid)
 		return RF_NOT_DETERMINED;
 
-	/* Empty match: covers only the start position */
+	/*
+	 * By here the record is valid and holds one of the three shapes above.
+	 *
+	 * The empty match (true, 0) must be classified first: it has length 0, so
+	 * the range test below would compute start + length == start and reject
+	 * its own start position as out of range.
+	 */
 	if (pos == start && winstate->rpr_match_matched && length == 0)
 		return RF_EMPTY_MATCH;
 
-	/* Outside the result range */
+	/*
+	 * By here length >= 1 -- the only zero-length record, the empty match,
+	 * has been handled -- so [start, start + length) is a well-formed range.
+	 */
 	if (pos < start || pos >= start + length)
 		return RF_NOT_DETERMINED;
 
+	/*
+	 * By here pos lies within [start, start + length).  An unmatched record
+	 * is (false, 1), so this returns for its single in-range position.
+	 */
 	if (!winstate->rpr_match_matched)
 		return RF_UNMATCHED;
 
+	/* By here the match is real (true, >= 1) and pos is one of its rows. */
 	if (pos == start)
 		return RF_FRAME_HEAD;
 
-- 
2.50.1 (Apple Git-155)


From 6bf5f93a5c5ce41e15e920c09d702713dd15f7f7 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Fri, 29 May 2026 17:56:02 +0900
Subject: [PATCH 23/68] Explain the completed-head-context branch in
 update_reduced_frame per Jian He's review

ExecRPRGetHeadContext() can return a context whose state list is already
drained, and the branch handling it looked unreachable.  It fires under
SKIP TO NEXT ROW: overlapping contexts let one reach FIN, and record its
result, during an earlier call -- before the call asking about its own
start row arrives.  Comment only; no behavior change.
---
 src/backend/executor/nodeWindowAgg.c | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 4cf1a9ac67b..f16d01e9743 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -4434,7 +4434,12 @@ update_reduced_frame(WindowObject winobj, int64 pos)
 	}
 	else if (targetCtx->states == NULL)
 	{
-		/* Context already completed - skip to result registration */
+		/*
+		 * The head context already completed in an earlier call.  Reachable
+		 * under SKIP TO NEXT ROW, where overlapping contexts let one reach
+		 * FIN -- recording its result -- before the call for its own start
+		 * row arrives.  Register that result.
+		 */
 		goto register_result;
 	}
 
-- 
2.50.1 (Apple Git-155)


From a0dc841399f9eb79ae4d77374382dc21f4183f31 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Fri, 29 May 2026 19:07:40 +0900
Subject: [PATCH 24/68] Tighten the RPR frame-boundary check from >= to == per
 Jian He's review

currentPos advances by exactly one row per call, and a finalized context
is skipped by the states == NULL guard, so it can only ever reach
ctxFrameEnd, never overshoot it; >= and == behave identically here, and
== states the intent.  The >= was a defensive guard against an overshoot
that cannot happen -- move that defense into Assert(currentPos <=
ctxFrameEnd) so a future change that breaks the invariant fails
immediately instead of silently slipping past the boundary, and change
the comment from "exceeded" to "reached".  No behavior change.
---
 src/backend/executor/execRPR.c | 15 ++++++++++++---
 1 file changed, 12 insertions(+), 3 deletions(-)

diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 88c59cf3276..4463cfe0a5c 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -1706,7 +1706,7 @@ ExecRPRProcessRow(WindowAggState *winstate, int64 currentPos,
 		if (ctx->states == NULL)
 			continue;
 
-		/* Check frame boundary - finalize if exceeded */
+		/* Check frame boundary - finalize the context when it is reached */
 		if (hasLimitedFrame)
 		{
 			int64		ctxFrameEnd;
@@ -1716,9 +1716,18 @@ ExecRPRProcessRow(WindowAggState *winstate, int64 currentPos,
 									&ctxFrameEnd))
 				ctxFrameEnd = PG_INT64_MAX;
 
-			if (currentPos >= ctxFrameEnd)
+			/*
+			 * currentPos advances by exactly one per call, and a finalized
+			 * context is skipped by the states == NULL guard above, so it can
+			 * only ever reach ctxFrameEnd, never overshoot it.  The Assert
+			 * turns a future change that broke that invariant into an
+			 * immediate failure rather than a silent slip past the boundary.
+			 */
+			Assert(currentPos <= ctxFrameEnd);
+
+			if (currentPos == ctxFrameEnd)
 			{
-				/* Frame boundary exceeded: force mismatch */
+				/* Frame boundary reached: force mismatch */
 				nfa_match(winstate, ctx, NULL);
 				continue;
 			}
-- 
2.50.1 (Apple Git-155)


From 36da77822da7f6ead31f0a434a245096cac2aa94 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Sat, 30 May 2026 18:57:56 +0900
Subject: [PATCH 25/68] Reject single-row window frame in row pattern
 recognition

The standard allows only UNBOUNDED FOLLOWING or a positive offset
FOLLOWING as the frame end for row pattern recognition.  A CURRENT ROW
end, or a zero offset, reduces the frame to the single current row, which
is not a valid search space for pattern matching.

Reject the CURRENT ROW spelling in transformRPR() at parse time, and a
zero offset in calculate_frame_offsets() at run time, since the offset
need not be a constant -- it may be a parameter, expression, or subquery.
---
 src/backend/executor/README.rpr        |  5 ++--
 src/backend/executor/nodeWindowAgg.c   | 10 +++++++
 src/backend/parser/parse_rpr.c         | 14 +++++++++
 src/test/regress/expected/rpr_base.out | 41 ++++++++++++++++++--------
 src/test/regress/sql/rpr_base.sql      | 26 ++++++++++++++--
 5 files changed, 80 insertions(+), 16 deletions(-)

diff --git a/src/backend/executor/README.rpr b/src/backend/executor/README.rpr
index 1d211245a5b..467cc03ecff 100644
--- a/src/backend/executor/README.rpr
+++ b/src/backend/executor/README.rpr
@@ -1122,8 +1122,9 @@ X-3. INITIAL vs SEEK
 X-4. Bounded Frame Handling
 
   With RPR, the frame mode is always ROWS and the frame start must be
-  CURRENT ROW. The frame end can be either UNBOUNDED FOLLOWING or n
-  FOLLOWING.
+  CURRENT ROW. The frame end must be UNBOUNDED FOLLOWING or a positive
+  offset (n >= 1) FOLLOWING; a CURRENT ROW end or a zero offset is
+  rejected, since it would reduce the frame to the single current row.
 
   When the frame is bounded (e.g., ROWS BETWEEN CURRENT ROW AND 5
   FOLLOWING), ExecRPRProcessRow receives hasLimitedFrame=true and
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index f16d01e9743..0dab18254e0 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -2369,6 +2369,16 @@ calculate_frame_offsets(PlanState *pstate)
 				ereport(ERROR,
 						(errcode(ERRCODE_INVALID_PRECEDING_OR_FOLLOWING_SIZE),
 						 errmsg("frame ending offset must not be negative")));
+
+			/*
+			 * Row pattern recognition forbids a zero-length frame end;
+			 * checked here so a non-constant offset (e.g. a bind parameter)
+			 * is caught, not just a literal 0.
+			 */
+			if (winstate->rpPattern != NULL && offset == 0)
+				ereport(ERROR,
+						errcode(ERRCODE_WINDOWING_ERROR),
+						errmsg("frame ending offset must be positive with row pattern recognition"));
 		}
 	}
 	winstate->all_first = false;
diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index d2ed6c14811..fa8c375f48b 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -163,6 +163,20 @@ transformRPR(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 				parser_errposition(pstate, windef->excludeLocation >= 0 ? windef->excludeLocation : windef->location));
 	}
 
+	/*
+	 * The standard allows only UNBOUNDED FOLLOWING or a positive offset
+	 * FOLLOWING as the frame end.  The equivalent 0 FOLLOWING spelling is
+	 * caught at runtime in calculate_frame_offsets().
+	 */
+	if (wc->frameOptions & FRAMEOPTION_END_CURRENT_ROW)
+		ereport(ERROR,
+				errcode(ERRCODE_WINDOWING_ERROR),
+				errmsg("cannot use CURRENT ROW as frame end with row pattern recognition"),
+				errhint("Use UNBOUNDED FOLLOWING or a positive offset FOLLOWING."),
+				parser_errposition(pstate,
+								   windef->frameLocation >= 0 ?
+								   windef->frameLocation : windef->location));
+
 	/* Transform AFTER MATCH SKIP TO clause */
 	wc->rpSkipTo = windef->rpCommonSyntax->rpSkipTo;
 
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index cfd2645bbed..d8f805c89aa 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -542,7 +542,8 @@ ERROR:  frame end cannot be UNBOUNDED PRECEDING
 LINE 5:     ROWS BETWEEN CURRENT ROW AND UNBOUNDED PRECEDING
                                          ^
 -- Expected: ERROR: frame end cannot be UNBOUNDED PRECEDING
--- Single row frame: CURRENT ROW AND CURRENT ROW
+-- Single row frame: CURRENT ROW AND CURRENT ROW is rejected (the standard
+-- allows only UNBOUNDED FOLLOWING or a positive offset FOLLOWING).
 SELECT id, val, COUNT(*) OVER w as cnt
 FROM rpr_frame
 WINDOW w AS (
@@ -553,17 +554,13 @@ WINDOW w AS (
     DEFINE A AS val > 0
 )
 ORDER BY id;
- id | val | cnt 
-----+-----+-----
-  1 |  10 |   1
-  2 |  10 |   1
-  3 |  10 |   1
-  4 |  20 |   1
-  5 |  20 |   1
-  6 |  30 |   1
-(6 rows)
-
--- Zero offset: CURRENT ROW AND 0 FOLLOWING (equivalent to CURRENT ROW)
+ERROR:  cannot use CURRENT ROW as frame end with row pattern recognition
+LINE 5:     ROWS BETWEEN CURRENT ROW AND CURRENT ROW
+            ^
+HINT:  Use UNBOUNDED FOLLOWING or a positive offset FOLLOWING.
+-- Expected: ERROR: cannot use CURRENT ROW as frame end with row pattern recognition
+-- Zero offset: CURRENT ROW AND 0 FOLLOWING denotes the same one-row frame
+-- and is likewise rejected (caught at execution time).
 SELECT id, val, COUNT(*) OVER w as cnt
 FROM rpr_frame
 WINDOW w AS (
@@ -574,6 +571,22 @@ WINDOW w AS (
     DEFINE A AS val > 0
 )
 ORDER BY id;
+ERROR:  frame ending offset must be positive with row pattern recognition
+-- Expected: ERROR: frame ending offset must be positive with row pattern recognition
+-- A non-constant frame end offset is allowed; a zero value is still rejected,
+-- this time at execution time (a literal cannot exercise that path).
+PREPARE rpr_end_offset(int8) AS
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_frame
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND $1 FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A)
+    DEFINE A AS val > 0
+)
+ORDER BY id;
+EXECUTE rpr_end_offset(2);
  id | val | cnt 
 ----+-----+-----
   1 |  10 |   1
@@ -584,6 +597,10 @@ ORDER BY id;
   6 |  30 |   1
 (6 rows)
 
+EXECUTE rpr_end_offset(0);
+ERROR:  frame ending offset must be positive with row pattern recognition
+-- Expected: ERROR: frame ending offset must be positive with row pattern recognition
+DEALLOCATE rpr_end_offset;
 -- Large offset: CURRENT ROW AND 1000 FOLLOWING
 SELECT id, val, COUNT(*) OVER w as cnt
 FROM rpr_frame
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index fd289d7cf67..6c2365a2d20 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -445,7 +445,8 @@ WINDOW w AS (
 );
 -- Expected: ERROR: frame end cannot be UNBOUNDED PRECEDING
 
--- Single row frame: CURRENT ROW AND CURRENT ROW
+-- Single row frame: CURRENT ROW AND CURRENT ROW is rejected (the standard
+-- allows only UNBOUNDED FOLLOWING or a positive offset FOLLOWING).
 SELECT id, val, COUNT(*) OVER w as cnt
 FROM rpr_frame
 WINDOW w AS (
@@ -456,8 +457,10 @@ WINDOW w AS (
     DEFINE A AS val > 0
 )
 ORDER BY id;
+-- Expected: ERROR: cannot use CURRENT ROW as frame end with row pattern recognition
 
--- Zero offset: CURRENT ROW AND 0 FOLLOWING (equivalent to CURRENT ROW)
+-- Zero offset: CURRENT ROW AND 0 FOLLOWING denotes the same one-row frame
+-- and is likewise rejected (caught at execution time).
 SELECT id, val, COUNT(*) OVER w as cnt
 FROM rpr_frame
 WINDOW w AS (
@@ -468,6 +471,25 @@ WINDOW w AS (
     DEFINE A AS val > 0
 )
 ORDER BY id;
+-- Expected: ERROR: frame ending offset must be positive with row pattern recognition
+
+-- A non-constant frame end offset is allowed; a zero value is still rejected,
+-- this time at execution time (a literal cannot exercise that path).
+PREPARE rpr_end_offset(int8) AS
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_frame
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND $1 FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A)
+    DEFINE A AS val > 0
+)
+ORDER BY id;
+EXECUTE rpr_end_offset(2);
+EXECUTE rpr_end_offset(0);
+-- Expected: ERROR: frame ending offset must be positive with row pattern recognition
+DEALLOCATE rpr_end_offset;
 
 -- Large offset: CURRENT ROW AND 1000 FOLLOWING
 SELECT id, val, COUNT(*) OVER w as cnt
-- 
2.50.1 (Apple Git-155)


From 4df572a905bbb1ce97e425db45e9d9152d53deac Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Sat, 30 May 2026 19:37:11 +0900
Subject: [PATCH 26/68] Remove the redundant zero check on the RPR frame ending
 offset

update_reduced_frame() guarded the frame offset with "endOffsetValue
!= 0", comparing a Datum directly to zero.  Now that a single-row frame
is rejected, a limited frame always carries a real offset, so the guard
is unnecessary; dropping it leaves a plain DatumGetInt64() and removes
the type-confused Datum-vs-zero comparison.

Per Jian He's review.
---
 src/backend/executor/nodeWindowAgg.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 0dab18254e0..bcb3614e96e 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -4399,7 +4399,7 @@ update_reduced_frame(WindowObject winobj, int64 pos)
 	 */
 	hasLimitedFrame = (frameOptions & FRAMEOPTION_ROWS) &&
 		!(frameOptions & FRAMEOPTION_END_UNBOUNDED_FOLLOWING);
-	if (hasLimitedFrame && winstate->endOffsetValue != 0)
+	if (hasLimitedFrame)
 		frameOffset = DatumGetInt64(winstate->endOffsetValue);
 
 	/*
-- 
2.50.1 (Apple Git-155)


From d070b97113883c1f6efbe2aaaf05dd5640cf3b1a Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Mon, 1 Jun 2026 10:14:33 +0900
Subject: [PATCH 27/68] Restore findTargetlistEntrySQL99 to static per Tatsuo
 Ishii's review

The RPR DEFINE handling once called findTargetlistEntrySQL99 from
parse_rpr.c, which required exposing it as extern.  The Var-only targetlist
fix removed that cross-file call, so revert the function to its original
static linkage and drop the prototype from parse_clause.h.
---
 src/backend/parser/parse_clause.c | 4 +++-
 src/include/parser/parse_clause.h | 3 ---
 2 files changed, 3 insertions(+), 4 deletions(-)

diff --git a/src/backend/parser/parse_clause.c b/src/backend/parser/parse_clause.c
index 3201a22d278..550ea4eb9c0 100644
--- a/src/backend/parser/parse_clause.c
+++ b/src/backend/parser/parse_clause.c
@@ -89,6 +89,8 @@ static void checkExprIsVarFree(ParseState *pstate, Node *n,
 							   const char *constructName);
 static TargetEntry *findTargetlistEntrySQL92(ParseState *pstate, Node *node,
 											 List **tlist, ParseExprKind exprKind);
+static TargetEntry *findTargetlistEntrySQL99(ParseState *pstate, Node *node,
+											 List **tlist, ParseExprKind exprKind);
 static int	get_matching_location(int sortgroupref,
 								  List *sortgrouprefs, List *exprs);
 static List *resolve_unique_index_expr(ParseState *pstate, InferClause *infer,
@@ -2310,7 +2312,7 @@ findTargetlistEntrySQL92(ParseState *pstate, Node *node, List **tlist,
  * tlist	the target list (passed by reference so we can append to it)
  * exprKind identifies clause type being processed
  */
-TargetEntry *
+static TargetEntry *
 findTargetlistEntrySQL99(ParseState *pstate, Node *node, List **tlist,
 						 ParseExprKind exprKind)
 {
diff --git a/src/include/parser/parse_clause.h b/src/include/parser/parse_clause.h
index 8aaac881f2b..fe234611007 100644
--- a/src/include/parser/parse_clause.h
+++ b/src/include/parser/parse_clause.h
@@ -52,9 +52,6 @@ extern List *addTargetToSortList(ParseState *pstate, TargetEntry *tle,
 extern Index assignSortGroupRef(TargetEntry *tle, List *tlist);
 extern bool targetIsInSortList(TargetEntry *tle, Oid sortop, List *sortList);
 
-extern TargetEntry *findTargetlistEntrySQL99(ParseState *pstate, Node *node,
-											 List **tlist, ParseExprKind exprKind);
-
 /* functions in parse_jsontable.c */
 extern ParseNamespaceItem *transformJsonTable(ParseState *pstate, JsonTable *jt);
 
-- 
2.50.1 (Apple Git-155)


From 8a00c552f5f3ecd6c889b53b8062304d43b06252 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Mon, 1 Jun 2026 11:16:49 +0900
Subject: [PATCH 29/68] Rename RPR NFA constructors to make/clone per Jian He's
 round-3 review

Rename nfa_context_alloc and nfa_state_alloc to _make (a blank
allocation), and nfa_state_create to nfa_state_clone (build a state from
an existing one's counts).  This follows the makeNode precedent and the
regex engine's clonesuccessorstates/cloneouts.  Also reword the
nfa_state_clone header comment from "Create" to "Clone".
---
 src/backend/executor/README.rpr       |  2 +-
 src/backend/executor/execRPR.c        | 58 +++++++++++++--------------
 src/test/regress/expected/rpr_nfa.out |  2 +-
 src/test/regress/sql/rpr_nfa.sql      |  2 +-
 4 files changed, 32 insertions(+), 32 deletions(-)

diff --git a/src/backend/executor/README.rpr b/src/backend/executor/README.rpr
index 467cc03ecff..9396a569fbd 100644
--- a/src/backend/executor/README.rpr
+++ b/src/backend/executor/README.rpr
@@ -579,7 +579,7 @@ VI-2. Context Creation: ExecRPRStartContext()
 
 Creates a new context and performs the initial advance.
 
-  (1) Allocate context via nfa_context_alloc()
+  (1) Allocate context via nfa_context_make()
   (2) Set matchStartRow = pos
   (3) Create initial state: elemIdx=0 (first pattern element),
       counts=all zero
diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 9bad36239f8..4022ca66e84 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -54,11 +54,11 @@ nfa_mark_visited(WindowAggState *winstate, int16 elemIdx)
 }
 
 /* Forward declarations - NFA state management */
-static RPRNFAState *nfa_state_alloc(WindowAggState *winstate);
+static RPRNFAState *nfa_state_make(WindowAggState *winstate);
 static void nfa_state_free(WindowAggState *winstate, RPRNFAState *state);
 static void nfa_state_free_list(WindowAggState *winstate, RPRNFAState *list);
-static RPRNFAState *nfa_state_create(WindowAggState *winstate, int16 elemIdx,
-									 int32 *counts, bool sourceAbsorbable);
+static RPRNFAState *nfa_state_clone(WindowAggState *winstate, int16 elemIdx,
+									int32 *counts, bool sourceAbsorbable);
 static bool nfa_states_equal(WindowAggState *winstate, RPRNFAState *s1,
 							 RPRNFAState *s2);
 static void nfa_add_state_unique(WindowAggState *winstate, RPRNFAContext *ctx,
@@ -67,7 +67,7 @@ static void nfa_add_matched_state(WindowAggState *winstate, RPRNFAContext *ctx,
 								  RPRNFAState *state, int64 matchEndRow);
 
 /* Forward declarations - NFA context management (internal) */
-static RPRNFAContext *nfa_context_alloc(WindowAggState *winstate);
+static RPRNFAContext *nfa_context_make(WindowAggState *winstate);
 static void nfa_unlink_context(WindowAggState *winstate, RPRNFAContext *ctx);
 
 /* Forward declarations - NFA statistics */
@@ -199,14 +199,14 @@ static void nfa_advance(WindowAggState *winstate, RPRNFAContext *ctx,
  */
 
 /*
- * nfa_state_alloc
+ * nfa_state_make
  *
  * Allocate an NFA state, reusing from freeList if available.
  * freeList is stored in WindowAggState for reuse across match attempts.
  * Uses flexible array member for counts[].
  */
 static RPRNFAState *
-nfa_state_alloc(WindowAggState *winstate)
+nfa_state_make(WindowAggState *winstate)
 {
 	RPRNFAState *state;
 
@@ -265,21 +265,21 @@ nfa_state_free_list(WindowAggState *winstate, RPRNFAState *list)
 }
 
 /*
- * nfa_state_create
+ * nfa_state_clone
  *
- * Create a new state with given elemIdx and counts.
+ * Clone a state from the given elemIdx and counts.
  * isAbsorbable is computed immediately: inherited AND new element's flag.
  * Monotonic property: once false, stays false through all transitions.
  *
  * Caller is responsible for linking the returned state.
  */
 static RPRNFAState *
-nfa_state_create(WindowAggState *winstate, int16 elemIdx,
-				 int32 *counts, bool sourceAbsorbable)
+nfa_state_clone(WindowAggState *winstate, int16 elemIdx,
+				int32 *counts, bool sourceAbsorbable)
 {
 	RPRPattern *pattern = winstate->rpPattern;
 	int			maxDepth = pattern->maxDepth;
-	RPRNFAState *state = nfa_state_alloc(winstate);
+	RPRNFAState *state = nfa_state_make(winstate);
 	RPRPatternElement *elem = &pattern->elements[elemIdx];
 
 	state->elemIdx = elemIdx;
@@ -418,12 +418,12 @@ nfa_add_matched_state(WindowAggState *winstate, RPRNFAContext *ctx,
 }
 
 /*
- * nfa_context_alloc
+ * nfa_context_make
  *
  * Allocate an NFA context, reusing from free list if available.
  */
 static RPRNFAContext *
-nfa_context_alloc(WindowAggState *winstate)
+nfa_context_make(WindowAggState *winstate)
 {
 	RPRNFAContext *ctx;
 
@@ -931,8 +931,8 @@ nfa_route_to_elem(WindowAggState *winstate, RPRNFAContext *ctx,
 
 		/* Create skip state before add_unique, which may free state */
 		if (RPRElemCanSkip(nextElem))
-			skipState = nfa_state_create(winstate, nextElem->next,
-										 state->counts, state->isAbsorbable);
+			skipState = nfa_state_clone(winstate, nextElem->next,
+										state->counts, state->isAbsorbable);
 
 		nfa_add_state_unique(winstate, ctx, state);
 
@@ -978,8 +978,8 @@ nfa_advance_alt(WindowAggState *winstate, RPRNFAContext *ctx,
 			break;
 
 		/* Create independent state for each branch */
-		newState = nfa_state_create(winstate, altIdx,
-									state->counts, state->isAbsorbable);
+		newState = nfa_state_clone(winstate, altIdx,
+								   state->counts, state->isAbsorbable);
 
 		/* Recursively process this branch before next */
 		nfa_advance_state(winstate, ctx, newState, currentPos);
@@ -1011,8 +1011,8 @@ nfa_advance_begin(WindowAggState *winstate, RPRNFAContext *ctx,
 	/* Optional group: create skip path (but don't route yet) */
 	if (elem->min == 0)
 	{
-		skipState = nfa_state_create(winstate, elem->jump,
-									 state->counts, state->isAbsorbable);
+		skipState = nfa_state_clone(winstate, elem->jump,
+									state->counts, state->isAbsorbable);
 	}
 
 	if (skipState != NULL && RPRElemIsReluctant(elem))
@@ -1094,8 +1094,8 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
 		 *----------
 		 */
 		if (RPRElemCanEmptyLoop(elem))
-			ffState = nfa_state_create(winstate, state->elemIdx,
-									   state->counts, state->isAbsorbable);
+			ffState = nfa_state_clone(winstate, state->elemIdx,
+									  state->counts, state->isAbsorbable);
 
 		/* Primary path: loop back for real matches */
 		for (int d = depth + 1; d < pattern->maxDepth; d++)
@@ -1163,8 +1163,8 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
 		 * Create exit state first (need original counts before modifying
 		 * state)
 		 */
-		exitState = nfa_state_create(winstate, elem->next,
-									 state->counts, state->isAbsorbable);
+		exitState = nfa_state_clone(winstate, elem->next,
+									state->counts, state->isAbsorbable);
 		exitState->counts[depth] = 0;
 		nextElem = &elements[exitState->elemIdx];
 
@@ -1255,8 +1255,8 @@ nfa_advance_var(WindowAggState *winstate, RPRNFAContext *ctx,
 			RPRNFAState *savedMatch = ctx->matchedState;
 
 			/* Clone for exit, original stays for loop */
-			cloneState = nfa_state_create(winstate, elem->next,
-										  state->counts, state->isAbsorbable);
+			cloneState = nfa_state_clone(winstate, elem->next,
+										 state->counts, state->isAbsorbable);
 			cloneState->counts[depth] = 0;
 			nextElem = &elements[cloneState->elemIdx];
 
@@ -1287,8 +1287,8 @@ nfa_advance_var(WindowAggState *winstate, RPRNFAContext *ctx,
 		else
 		{
 			/* Clone for loop, original used for exit */
-			cloneState = nfa_state_create(winstate, state->elemIdx,
-										  state->counts, state->isAbsorbable);
+			cloneState = nfa_state_clone(winstate, state->elemIdx,
+										 state->counts, state->isAbsorbable);
 
 			/* Loop first (preferred for greedy) */
 			nfa_add_state_unique(winstate, ctx, cloneState);
@@ -1505,9 +1505,9 @@ ExecRPRStartContext(WindowAggState *winstate, int64 startPos)
 	RPRPattern *pattern = winstate->rpPattern;
 	RPRPatternElement *elem;
 
-	ctx = nfa_context_alloc(winstate);
+	ctx = nfa_context_make(winstate);
 	ctx->matchStartRow = startPos;
-	ctx->states = nfa_state_alloc(winstate);	/* initial state at elem 0 */
+	ctx->states = nfa_state_make(winstate); /* initial state at elem 0 */
 
 	elem = &pattern->elements[0];
 
diff --git a/src/test/regress/expected/rpr_nfa.out b/src/test/regress/expected/rpr_nfa.out
index 1f494d2db34..72dbf080a37 100644
--- a/src/test/regress/expected/rpr_nfa.out
+++ b/src/test/regress/expected/rpr_nfa.out
@@ -3844,7 +3844,7 @@ WINDOW w AS (
 
 -- Non-absorbable context during absorption
 -- Pattern (A B)+ C: A,B in absorbable group, C is not.
--- When END exits to C via nfa_state_create, isAbsorbable becomes false.
+-- When END exits to C via nfa_state_clone, isAbsorbable becomes false.
 WITH test_non_absorbable AS (
     SELECT * FROM (VALUES
         (1, ARRAY['A']),
diff --git a/src/test/regress/sql/rpr_nfa.sql b/src/test/regress/sql/rpr_nfa.sql
index 76dfc4d88bc..128476aa1d1 100644
--- a/src/test/regress/sql/rpr_nfa.sql
+++ b/src/test/regress/sql/rpr_nfa.sql
@@ -2790,7 +2790,7 @@ WINDOW w AS (
 
 -- Non-absorbable context during absorption
 -- Pattern (A B)+ C: A,B in absorbable group, C is not.
--- When END exits to C via nfa_state_create, isAbsorbable becomes false.
+-- When END exits to C via nfa_state_clone, isAbsorbable becomes false.
 WITH test_non_absorbable AS (
     SELECT * FROM (VALUES
         (1, ARRAY['A']),
-- 
2.50.1 (Apple Git-155)


From 686c593a23d5604e48a5f08a84cf525cbda8a072 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Mon, 1 Jun 2026 11:03:57 +0900
Subject: [PATCH 28/68] Clarify RPR comments per Jian He's round-3 review

- Replace fragile "Tests line NNN" comments with the branch and condition
  they exercise, so they survive later code shuffles
- Reword nfa_add_state_unique to describe appending to the list tail
- Disambiguate "END chain" as the chain of END elements
- Restate the nfa_advance_var Assert comment as a structural invariant
  (min <= max), not a post-match claim
---
 src/backend/executor/execRPR.c         | 11 ++++++-----
 src/test/regress/expected/rpr_base.out |  8 ++++----
 src/test/regress/sql/rpr_base.sql      |  8 ++++----
 3 files changed, 14 insertions(+), 13 deletions(-)

diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 4463cfe0a5c..9bad36239f8 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -334,7 +334,8 @@ nfa_states_equal(WindowAggState *winstate, RPRNFAState *s1, RPRNFAState *s2)
 /*
  * nfa_add_state_unique
  *
- * Add a state to ctx->states at the END, only if no duplicate exists.
+ * Add the state to the end of the ctx->states linked list, but only if a
+ * duplicate state is not already present.
  * Earlier states have better lexical order (DFS traversal order), so existing
  * wins; the new state is freed when a duplicate is found.
  */
@@ -779,7 +780,7 @@ nfa_eval_var_match(WindowAggState *winstate, RPRPatternElement *elem,
  *     previous advance when count >= min was satisfied)
  *
  * For VARs that reached max count followed by END:
- *   - Advance through END chain to reach absorption judgment point
+ *   - Advance through the END-element chain to the absorption judgment point
  *   - Only deterministic exits (count >= max, max != INF) are handled
  *   - Chains through END elements while count >= max (must-exit path)
  *
@@ -796,8 +797,8 @@ nfa_match(WindowAggState *winstate, RPRNFAContext *ctx, bool *varMatched)
 
 	/*
 	 * Evaluate VAR elements against current row. For VARs that reach max
-	 * count with END next, advance through END chain inline so absorb phase
-	 * can compare states at judgment points.
+	 * count with END next, advance through the chain of END elements inline
+	 * so absorb phase can compare states at judgment points.
 	 */
 	for (state = ctx->states; state != NULL; state = nextState)
 	{
@@ -1229,7 +1230,7 @@ nfa_advance_var(WindowAggState *winstate, RPRNFAContext *ctx,
 	bool		canLoop = (elem->max == RPR_QUANTITY_INF || count < elem->max);
 	bool		canExit = (count >= elem->min);
 
-	/* After a successful match, count >= 1, so at least one must be true */
+	/* min <= max, so !canExit (count < min) implies canLoop (count < max) */
 	Assert(canLoop || canExit);
 
 	/* elem->next must be a valid index for any reachable VAR */
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index d8f805c89aa..c96a1216cc3 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -3442,8 +3442,8 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
 (7 rows)
 
 -- Consecutive VAR merge: A A+ -> a{2,}
--- Tests line 251: child->max == RPR_QUANTITY_INF branch in mergeConsecutiveVars
--- prev: A{1,1} (finite), child: A+ (infinite) triggers line 251 evaluation
+-- Exercises the child->max == RPR_QUANTITY_INF branch in mergeConsecutiveVars,
+-- where a finite prev (A{1,1}) meets an infinite child (A+).
 EXPLAIN (COSTS OFF)
 SELECT COUNT(*) OVER w FROM rpr_plan
 WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
@@ -3492,8 +3492,8 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
 (7 rows)
 
 -- Consecutive GROUP merge: (A B){2} (A B)+ -> (a b){3,}
--- Tests line 325: child->max == RPR_QUANTITY_INF branch in mergeConsecutiveGroups
--- prev: (A B){2,2} (finite), child: (A B)+ (infinite) triggers line 325 evaluation
+-- Exercises the child->max == RPR_QUANTITY_INF branch in mergeConsecutiveGroups,
+-- where a finite prev ((A B){2,2}) meets an infinite child ((A B)+).
 EXPLAIN (COSTS OFF)
 SELECT COUNT(*) OVER w FROM rpr_plan
 WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index 6c2365a2d20..d7b63cfc690 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -2379,8 +2379,8 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
              PATTERN (A+ A*) DEFINE A AS val > 0);
 
 -- Consecutive VAR merge: A A+ -> a{2,}
--- Tests line 251: child->max == RPR_QUANTITY_INF branch in mergeConsecutiveVars
--- prev: A{1,1} (finite), child: A+ (infinite) triggers line 251 evaluation
+-- Exercises the child->max == RPR_QUANTITY_INF branch in mergeConsecutiveVars,
+-- where a finite prev (A{1,1}) meets an infinite child (A+).
 EXPLAIN (COSTS OFF)
 SELECT COUNT(*) OVER w FROM rpr_plan
 WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
@@ -2399,8 +2399,8 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
              PATTERN ((A B)+ (A B)+) DEFINE A AS val <= 50, B AS val > 50);
 
 -- Consecutive GROUP merge: (A B){2} (A B)+ -> (a b){3,}
--- Tests line 325: child->max == RPR_QUANTITY_INF branch in mergeConsecutiveGroups
--- prev: (A B){2,2} (finite), child: (A B)+ (infinite) triggers line 325 evaluation
+-- Exercises the child->max == RPR_QUANTITY_INF branch in mergeConsecutiveGroups,
+-- where a finite prev ((A B){2,2}) meets an infinite child ((A B)+).
 EXPLAIN (COSTS OFF)
 SELECT COUNT(*) OVER w FROM rpr_plan
 WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-- 
2.50.1 (Apple Git-155)


From 66a754b84526730b31edf03462163ae42aec5f9e Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Mon, 1 Jun 2026 11:38:09 +0900
Subject: [PATCH 30/68] Demote RPR nfaVisitedNWords to a local per Jian He's
 round-3 review

nfaVisitedNWords is read only once, at init, to size the visited bitmap
(the per-row reset clears just the high-water range), so it need not live
in WindowAggState.  nfaStateSize is kept: it is recomputed per state
allocation, the engine's hottest path.
---
 src/backend/executor/README.rpr      | 2 --
 src/backend/executor/execRPR.c       | 2 +-
 src/backend/executor/nodeWindowAgg.c | 6 ++++--
 src/include/nodes/execnodes.h        | 2 --
 4 files changed, 5 insertions(+), 7 deletions(-)

diff --git a/src/backend/executor/README.rpr b/src/backend/executor/README.rpr
index 9396a569fbd..26c1a1ea236 100644
--- a/src/backend/executor/README.rpr
+++ b/src/backend/executor/README.rpr
@@ -522,7 +522,6 @@ V-3. RPR Fields of WindowAggState
   nfaStateFree                  Reuse pool for states
   nfaVarMatched                 Per-row cache: varMatched[varId]
   nfaVisitedElems               Bitmap for cycle detection
-  nfaVisitedNWords              Number of bitmapwords in nfaVisitedElems
   nfaVisitedMinWord             Lowest bitmapword index touched since last reset
   nfaVisitedMaxWord             Highest bitmapword index touched since last reset
   nfaStateSize                  Precomputed size of RPRNFAState
@@ -1542,7 +1541,6 @@ Appendix B. Data Structure Relationship Diagram
     |--- defineMatchStartDependent: Bitmapset* (match_start_dependent
     |        DEFINE vars; see VI-4)
     |--- nfaVisitedElems: bitmapword* (cycle detection)
-    |--- nfaVisitedNWords: int (size of nfaVisitedElems)
     |--- nfaVisitedMinWord / nfaVisitedMaxWord: int16
     |        (touched-word range for fast reset)
     |--- nfaLastProcessedRow: int64 (-1 = none)
diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 4022ca66e84..dca1e45be57 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -41,7 +41,7 @@
 /*
  * Set the visited bit for elemIdx and update the high-water marks
  * (nfaVisitedMin/MaxWord) so that the next reset only has to clear
- * the touched range instead of the full nfaVisitedNWords array.
+ * the touched range instead of the full nfaVisitedElems bitmap.
  */
 static inline void
 nfa_mark_visited(WindowAggState *winstate, int16 elemIdx)
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index bcb3614e96e..0b0196e7e40 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -3058,12 +3058,14 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
 	/* Calculate NFA state size and allocate cycle detection bitmap */
 	if (node->rpPattern != NULL)
 	{
+		int			nfaVisitedNWords;
+
 		winstate->nfaStateSize = offsetof(RPRNFAState, counts) +
 			sizeof(int32) * node->rpPattern->maxDepth;
-		winstate->nfaVisitedNWords =
+		nfaVisitedNWords =
 			(node->rpPattern->numElements - 1) / BITS_PER_BITMAPWORD + 1;
 		winstate->nfaVisitedElems = palloc0(sizeof(bitmapword) *
-											winstate->nfaVisitedNWords);
+											nfaVisitedNWords);
 		/* High-water mark sentinels: no bits set yet. */
 		winstate->nfaVisitedMinWord = INT16_MAX;
 		winstate->nfaVisitedMaxWord = -1;
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 1fba14b892e..4641ed36cee 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -2671,8 +2671,6 @@ typedef struct WindowAggState
 											 * (match_start-dependent) */
 	bitmapword *nfaVisitedElems;	/* elemIdx visited bitmap for cycle
 									 * detection */
-	int			nfaVisitedNWords;	/* number of bitmapwords in
-									 * nfaVisitedElems */
 	int16		nfaVisitedMinWord;	/* lowest bitmapword index touched since
 									 * last reset (INT16_MAX = none) */
 	int16		nfaVisitedMaxWord;	/* highest bitmapword index touched since
-- 
2.50.1 (Apple Git-155)


From 4817be41a812df933f0a461609c6d38f35179aa4 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Mon, 1 Jun 2026 12:08:50 +0900
Subject: [PATCH 31/68] Rename the AST-level prefix/suffix rewrite from
 "absorption" to "merging"

"absorption" now refers only to the runtime context-equivalence collapse.
The Phase-1 AST rewrite in mergeGroupPrefixSuffix is renamed "prefix/suffix
merging" to match the sibling "consecutive variable / group / ALT merging"
rewrites.  Per the naming discussion with Tatsuo Ishii.
---
 src/backend/executor/README.rpr  |  2 +-
 src/backend/optimizer/plan/rpr.c | 17 ++++++++---------
 2 files changed, 9 insertions(+), 10 deletions(-)

diff --git a/src/backend/executor/README.rpr b/src/backend/executor/README.rpr
index 26c1a1ea236..449bf051153 100644
--- a/src/backend/executor/README.rpr
+++ b/src/backend/executor/README.rpr
@@ -221,7 +221,7 @@ applied:
   (d) Consecutive ALT merging: Merge repeated identical ALT nodes
       (A | B) (A | B) (A | B) -> (A | B){3}
 
-  (e) Prefix/suffix absorption: Absorb identical sequences before/after
+  (e) Prefix/suffix merging: Merge identical sequences before/after
       a group
       A B (A B)+ -> (A B){2,INF}
 
diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index c65681463b3..2a1d665c7ee 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -480,7 +480,7 @@ mergeConsecutiveAlts(List *children)
  *		Merge sequence prefix/suffix into GROUP with matching children.
  *
  * When a GROUP's children appear as a prefix before and/or suffix after
- * the GROUP in a SEQ, absorb them by incrementing the GROUP's quantifier.
+ * the GROUP in a SEQ, merge them by incrementing the GROUP's quantifier.
  * This runs iteratively: A B A B (A B)+ A B -> (A B){5,}.
  *
  * Algorithm:
@@ -503,15 +503,15 @@ mergeGroupPrefixSuffix(List *children)
 	List	   *result = NIL;
 	int			numChildren = list_length(children);
 	int			i;
-	int			skipUntil = -1; /* skip suffix elements already absorbed */
+	int			skipUntil = -1; /* skip suffix elements already merged */
 
 	for (i = 0; i < numChildren; i++)
 	{
 		RPRPatternNode *child = (RPRPatternNode *) list_nth(children, i);
 
 		/*
-		 * The suffix absorption logic below adjusts i to skip absorbed
-		 * elements, ensuring we never revisit them. Verify this invariant.
+		 * The suffix merge logic below adjusts i to skip merged elements,
+		 * ensuring we never revisit them. Verify this invariant.
 		 */
 		Assert(i >= skipUntil);
 
@@ -543,7 +543,7 @@ mergeGroupPrefixSuffix(List *children)
 			groupChildCount = list_length(groupContent);
 
 			/*
-			 * PREFIX MERGE: Check if preceding elements match. Keep absorbing
+			 * PREFIX MERGE: Check if preceding elements match. Keep merging
 			 * as long as we have matching prefixes.
 			 */
 			while (prefixLen >= groupChildCount && groupChildCount > 0)
@@ -592,7 +592,7 @@ mergeGroupPrefixSuffix(List *children)
 			}
 
 			/*
-			 * SUFFIX MERGE: Check if following elements match. Keep absorbing
+			 * SUFFIX MERGE: Check if following elements match. Keep merging
 			 * as long as we have matching suffixes.
 			 */
 			while (i + groupChildCount < numChildren && groupChildCount > 0)
@@ -623,7 +623,7 @@ mergeGroupPrefixSuffix(List *children)
 					 child->max < RPR_QUANTITY_INF - 1))
 				{
 					/*
-					 * Match! Absorb suffix by incrementing quantifier and
+					 * Match! Merge suffix by incrementing quantifier and
 					 * skipping.
 					 */
 					child->min += 1;
@@ -632,8 +632,7 @@ mergeGroupPrefixSuffix(List *children)
 					skipUntil = suffixStart + groupChildCount;
 
 					/*
-					 * Update i to continue suffix check after absorbed
-					 * elements
+					 * Update i to continue suffix check after merged elements
 					 */
 					i = skipUntil - 1;
 				}
-- 
2.50.1 (Apple Git-155)


From b5a8065aa1d4e5f4679508c8dbafd6465ef2d6ac Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Mon, 1 Jun 2026 12:49:16 +0900
Subject: [PATCH 32/68] Drop non-standard per-group banner labels from RPR
 forward declarations

The per-group "Forward declarations - ..." labels in execRPR.c are local
to the RPR files; the tree convention is a single banner over the block,
or none.  Consolidate to one "/* Forward declarations */" banner.  Per
Jian He's off-list review.
---
 src/backend/executor/execRPR.c | 6 +-----
 1 file changed, 1 insertion(+), 5 deletions(-)

diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index dca1e45be57..34638409f66 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -53,7 +53,7 @@ nfa_mark_visited(WindowAggState *winstate, int16 elemIdx)
 	winstate->nfaVisitedMaxWord = Max(winstate->nfaVisitedMaxWord, w);
 }
 
-/* Forward declarations - NFA state management */
+/* Forward declarations */
 static RPRNFAState *nfa_state_make(WindowAggState *winstate);
 static void nfa_state_free(WindowAggState *winstate, RPRNFAState *state);
 static void nfa_state_free_list(WindowAggState *winstate, RPRNFAState *list);
@@ -66,23 +66,19 @@ static void nfa_add_state_unique(WindowAggState *winstate, RPRNFAContext *ctx,
 static void nfa_add_matched_state(WindowAggState *winstate, RPRNFAContext *ctx,
 								  RPRNFAState *state, int64 matchEndRow);
 
-/* Forward declarations - NFA context management (internal) */
 static RPRNFAContext *nfa_context_make(WindowAggState *winstate);
 static void nfa_unlink_context(WindowAggState *winstate, RPRNFAContext *ctx);
 
-/* Forward declarations - NFA statistics */
 static void nfa_update_length_stats(int64 count, NFALengthStats *stats, int64 newLen);
 static void nfa_record_context_skipped(WindowAggState *winstate, int64 skippedLen);
 static void nfa_record_context_absorbed(WindowAggState *winstate, int64 absorbedLen);
 
-/* Forward declarations - NFA absorption */
 static void nfa_update_absorption_flags(RPRNFAContext *ctx);
 static bool nfa_states_covered(RPRPattern *pattern, RPRNFAContext *older,
 							   RPRNFAContext *newer);
 static void nfa_try_absorb_context(WindowAggState *winstate, RPRNFAContext *ctx);
 static void nfa_absorb_contexts(WindowAggState *winstate);
 
-/* Forward declarations - NFA match and advance */
 static bool nfa_eval_var_match(WindowAggState *winstate,
 							   RPRPatternElement *elem, bool *varMatched);
 static void nfa_match(WindowAggState *winstate, RPRNFAContext *ctx,
-- 
2.50.1 (Apple Git-155)


From 86949cfa045558fc9754f61211f93483a144872e Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Mon, 1 Jun 2026 12:52:45 +0900
Subject: [PATCH 33/68] Clarify that ExecRPRCleanupDeadContexts always frees
 the failed context

The failure statistic is recorded conditionally, but the context is freed
unconditionally.  A stray "else:" comment before the free made it read as
the conditional branch; reword the comment and drop the "else:".  No code
change.  Per Jian He's off-list review.
---
 src/backend/executor/execRPR.c | 7 +++----
 1 file changed, 3 insertions(+), 4 deletions(-)

diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 34638409f66..16a0f4ae375 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -1805,9 +1805,9 @@ ExecRPRCleanupDeadContexts(WindowAggState *winstate, RPRNFAContext *excludeCtx)
 			continue;
 
 		/*
-		 * This is a failed context - count and remove it. Only count if it
-		 * actually processed its start row. Contexts created for
-		 * beyond-partition rows are silently removed.
+		 * Failed context: always removed below.  Only record the failure
+		 * statistic if it actually processed its start row; contexts created
+		 * for beyond-partition rows are removed without being counted.
 		 */
 		if (ctx->lastProcessedRow >= ctx->matchStartRow)
 		{
@@ -1815,7 +1815,6 @@ ExecRPRCleanupDeadContexts(WindowAggState *winstate, RPRNFAContext *excludeCtx)
 
 			ExecRPRRecordContextFailure(winstate, failedLen);
 		}
-		/* else: context was never processed (beyond-partition), just remove */
 
 		ExecRPRFreeContext(winstate, ctx);
 	}
-- 
2.50.1 (Apple Git-155)


From 57989dcb91d0c0e37d07c4d4b79004c45b2815aa Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Mon, 1 Jun 2026 13:45:48 +0900
Subject: [PATCH 34/68] Correct stale RPR comments and document a defensive
 window check

Static analysis surfaced several comments that no longer match the code:

- plannodes.h: rewrite the RPRPattern.isAbsorbable comment to match the
  implemented cases (isUnboundedStart / computeAbsorbability) and the
  normalization that enables absorption; the old text wrongly called the
  prefix/suffix merge unimplemented
- README.rpr: an unmatched row's reduced frame is empty, not "the row
  itself"; the varId range is 0-250, not 0-251
- advanced.sgml: unmatched rows show NULL window functions and
  initial-value aggregates (count() = 0), not all-NULL
- planner.c: note that the RPR fields in optimize_window_clauses' duplicate
  check are reached but defensive -- RPR clauses are separated by their
  frame options first -- kept for parity with transformWindowFuncCall
---
 doc/src/sgml/advanced.sgml           |  5 +++--
 src/backend/executor/README.rpr      |  6 +++---
 src/backend/optimizer/plan/planner.c |  5 ++++-
 src/include/nodes/plannodes.h        | 21 ++++++++++++++-------
 4 files changed, 24 insertions(+), 13 deletions(-)

diff --git a/doc/src/sgml/advanced.sgml b/doc/src/sgml/advanced.sgml
index 11c2416df51..1410a443609 100644
--- a/doc/src/sgml/advanced.sgml
+++ b/doc/src/sgml/advanced.sgml
@@ -600,8 +600,9 @@ DEFINE
     subsequent rows all window functions are shown as NULL. Aggregates on
     non-starting rows return their initial value: for example,
     <function>count()</function> returns 0 and <function>sum()</function>
-    returns NULL. For rows that do not match the PATTERN, columns are shown
-    as NULL too. Example of a <literal>SELECT</literal> using
+    returns NULL. For rows that do not match the PATTERN, window functions
+    are likewise shown as NULL and aggregates return their initial value.
+    Example of a <literal>SELECT</literal> using
     the <literal>DEFINE</literal> and <literal>PATTERN</literal> clause is as
     follows.
 
diff --git a/src/backend/executor/README.rpr b/src/backend/executor/README.rpr
index 449bf051153..08418588114 100644
--- a/src/backend/executor/README.rpr
+++ b/src/backend/executor/README.rpr
@@ -245,7 +245,7 @@ RPRPatternElement struct (16 bytes):
 
   Field      Size     Description
   ---------------------------------------------------------
-  varId      1B      Variable ID (0-251) or control code (252-255)
+  varId      1B      Variable ID (0-250) or control code (252-255)
   depth      1B      Group nesting depth
   flags      1B      Bit flags (see below)
   reserved   1B      Padding
@@ -1303,11 +1303,11 @@ XI-4. Execution Trace
 
 XI-5. Final Result
 
-  Row 0: unmatched     -> frame = the row itself
+  Row 0: unmatched     -> reduced frame empty (window funcs NULL, count() 0)
   Row 1: match head    -> frame = rows 1 through 3
   Row 2: inside match  -> skipped
   Row 3: inside match  -> skipped
-  Row 4: unmatched     -> frame = the row itself
+  Row 4: unmatched     -> reduced frame empty (window funcs NULL, count() 0)
 
 Chapter XII  Summary of Key Design Decisions
 ============================================================================
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index c6fc868cdca..f43cc0edb37 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -6254,7 +6254,10 @@ optimize_window_clauses(PlannerInfo *root, WindowFuncLists *wflists)
 
 				/*
 				 * Perform the same duplicate check that is done in
-				 * transformWindowFuncCall.
+				 * transformWindowFuncCall. wc is never an RPR clause here
+				 * (those are skipped above), and an RPR existing_wc differs
+				 * in its frame options anyway, so the RPR-related comparisons
+				 * are a defensive backstop for parity.
 				 */
 				if (equal(wc->partitionClause, existing_wc->partitionClause) &&
 					equal(wc->orderClause, existing_wc->orderClause) &&
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index fd4bdf2cb31..d45d93d79a2 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -1296,15 +1296,22 @@ typedef struct RPRPattern
 	 * Context absorption optimization.
 	 *
 	 * Absorption is only safe when later matches are guaranteed to be
-	 * suffixes of earlier matches. This requires simple pattern structure:
+	 * suffixes of earlier matches, which requires the pattern to start with
+	 * an unbounded greedy element.  Phase-1 normalization (consecutive
+	 * variable / group / ALT merging and prefix/suffix merging) rewrites the
+	 * pattern toward that form first -- so e.g. A B (A B)+ is merged to
+	 * (A B){2,} and then judged absorbable.
 	 *
-	 * Case 1: No ALT, single unbounded element (A+, (A B)+)
-	 * Case 2: Top-level ALT with each branch being single unbounded (A+ | B+)
+	 * computeAbsorbability() marks the absorbable cases (see isUnboundedStart):
+	 *   - simple unbounded VAR at the start:                    A+ B C
+	 *   - unbounded GROUP with fixed-length children:           (A B)+, (A B{2})+
+	 *   - top-level ALT with independently absorbable branches: A+ | B+
+	 *     (handled in computeAbsorbabilityRecursive)
 	 *
-	 * Complex patterns like A B (A B)+ could theoretically be transformed to
-	 * (A B){2,} for absorption, but this changes lexical order and is not
-	 * implemented. Similarly, (A|B)+ cannot be absorbed because different
-	 * start positions produce different match contents (not suffix relation).
+	 * Not absorbable: an unbounded element not at the start (A B+), a
+	 * reluctant quantifier (A+?), or an ALT inside a group ((A|B)+) -- there
+	 * different start positions yield different match contents, so later
+	 * matches are not suffixes of earlier ones.
 	 */
 	bool		isAbsorbable;	/* true if pattern supports context absorption */
 } RPRPattern;
-- 
2.50.1 (Apple Git-155)


From 6bfd39fc7cf8b758a3dd910d89c0893d57425407 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Mon, 1 Jun 2026 16:35:45 +0900
Subject: [PATCH 35/68] Fix unsafe (A{n,})* quantifier flattening in row
 pattern recognition

The Phase-1 pattern optimizer's tryMultiplyQuantifiers() collapsed a
nested quantifier (A{k,})* to A* whenever both the inner and outer
bounds were unbounded.  That is only correct when the inner minimum is
0 or 1.  For k >= 2 the reachable repetition counts are {0} together
with [k, INF), so the counts 1..k-1 are unreachable; A* admits them.
As a result an isolated single A matched as length 1 where the pattern
must instead produce an empty match (length 0), widening the matching
language.

Skip the multiplication when the outer quantifier is skippable
(minimum 0) and the inner minimum is at least 2.  The existing safe
cases are unaffected: an inner minimum of 0 or 1 stays contiguous, and
a non-skippable outer such as (A{2,})+ still folds to A{2,}.

Per a report from a static analysis tool.
---
 src/backend/executor/README.rpr           |  1 +
 src/backend/optimizer/plan/rpr.c          | 18 +++++-
 src/test/regress/expected/rpr_explain.out | 41 +++++++++++++
 src/test/regress/expected/rpr_nfa.out     | 72 +++++++++++++++++++++++
 src/test/regress/sql/rpr_explain.sql      | 23 ++++++++
 src/test/regress/sql/rpr_nfa.sql          | 53 +++++++++++++++++
 6 files changed, 207 insertions(+), 1 deletion(-)

diff --git a/src/backend/executor/README.rpr b/src/backend/executor/README.rpr
index 08418588114..3a215f2566b 100644
--- a/src/backend/executor/README.rpr
+++ b/src/backend/executor/README.rpr
@@ -232,6 +232,7 @@ applied:
   (g) Quantifier multiplication: Collapse nested quantifiers when safe
       (A+)+ -> A+
       (A{2,3}){5} -> A{10,15}
+      (A{2,})* stays as-is (count 1 unreachable; A* would be wrong)
 
   (h) Single-child unwrap
       SEQ(A) -> A,  (A){1,1} -> A
diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index 2a1d665c7ee..2a98977b288 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -781,7 +781,8 @@ optimizeAltPattern(RPRPatternNode *pattern)
  *		Try to multiply quantifiers.
  *
  * Multiplication is SAFE when:
- *   1. Both unbounded: (A*)* -> A*, (A+)+ -> A+
+ *   1. Both unbounded, with skipless outer or child->min <= 1:
+ *      (A*)* -> A*, (A+)+ -> A+, (A+)* -> A*, (A{2,})+ -> A{2,}
  *   2. Outer exact: (A{m,n}){k} -> A{m*k, n*k}
  *   3. Outer range + child {1,1}: (A){2,} -> A{2,}
  *
@@ -789,6 +790,9 @@ optimizeAltPattern(RPRPatternNode *pattern)
  *   - Only child unbounded: (A+){3} has different semantics
  *   - Outer range + child not {1,1}: gaps possible
  *     e.g., (A{2}){2,3} yields 4,6 only (not 4,5,6)
+ *   - Skippable outer (min 0) + child->min >= 2: (A{2,})* reaches
+ *     {0} UNION [child->min, INF), so 1..child->min-1 are unreachable
+ *     and A* would wrongly admit them
  *
  * Returns the child node with multiplied quantifiers if successful,
  * otherwise returns the original pattern unchanged.
@@ -816,6 +820,18 @@ tryMultiplyQuantifiers(RPRPatternNode *pattern)
 	/* Case 1: Both unbounded - (A*)* -> A*, (A+)+ -> A+ */
 	if (child->max == RPR_QUANTITY_INF && pattern->max == RPR_QUANTITY_INF)
 	{
+		/*
+		 * A skippable outer (min 0) over a child with min >= 2 reaches
+		 * repetition counts {0} UNION [child->min, INF): the counts
+		 * 1..child->min-1 are unreachable, and no single quantifier can
+		 * express that gap.  Flattening to A{0,INF} = A* would wrongly admit
+		 * them, e.g. (A{2,})* would match a single A.  Multiplication is safe
+		 * here only when child->min <= 1 (the reachable set is then
+		 * contiguous from 0); otherwise leave the pattern unflattened.
+		 */
+		if (pattern->min == 0 && child->min >= 2)
+			return pattern;
+
 		new_min_64 = (int64) child->min * pattern->min;
 		if (new_min_64 >= RPR_QUANTITY_INF)
 			return pattern;		/* overflow, skip optimization */
diff --git a/src/test/regress/expected/rpr_explain.out b/src/test/regress/expected/rpr_explain.out
index 77079d5e8c9..9ba302b11ae 100644
--- a/src/test/regress/expected/rpr_explain.out
+++ b/src/test/regress/expected/rpr_explain.out
@@ -782,6 +782,47 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=1000.00 loops=1)
 (10 rows)
 
+-- (A{2,})* must NOT flatten to a* (H-1): counts {0} UNION [2, INF) leave 1
+-- unreachable.  The planner keeps it as (a{2,})*, not a*.
+CREATE VIEW rpr_ev_nested_quant_no_flatten AS
+SELECT count(*) OVER w
+FROM generate_series(1, 6) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A{2,})*)
+    DEFINE A AS v % 3 <> 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_nested_quant_no_flatten'), E'\n')) AS line WHERE line ~ 'PATTERN';
+         line          
+-----------------------
+   PATTERN ((a{2,})*) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 6) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A{2,})*)
+    DEFINE A AS v % 3 <> 0
+);');
+                         rpr_explain_filter                          
+---------------------------------------------------------------------
+ WindowAgg (actual rows=6.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a{2,}")*
+   Nav Mark Lookback: 0
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 5 peak, 18 total, 0 merged
+   NFA Contexts: 3 peak, 7 total, 2 pruned
+   NFA: 2 matched (len 2/2/2.0), 0 mismatched
+   NFA: 2 absorbed (len 1/1/1.0), 0 skipped
+   ->  Function Scan on generate_series s (actual rows=6.00 loops=1)
+(10 rows)
+
 -- ============================================================
 -- Context Statistics Tests (peak, total, pruned + absorbed/skipped)
 -- ============================================================
diff --git a/src/test/regress/expected/rpr_nfa.out b/src/test/regress/expected/rpr_nfa.out
index 72dbf080a37..59b91ff9aa4 100644
--- a/src/test/regress/expected/rpr_nfa.out
+++ b/src/test/regress/expected/rpr_nfa.out
@@ -2275,6 +2275,78 @@ WINDOW w AS (
   6 | {B}   |             |          
 (6 rows)
 
+-- Nested quantifier flattening must not widen the matching language (H-1).
+-- (A{k,})* with k >= 2 reaches repetition counts {0} UNION [k, INF); the gap
+-- 1..k-1 is unreachable, so it must NOT collapse to A*.  An isolated single A
+-- must yield an EMPTY match (count 0), not a length-1 match.
+WITH test_nested_quant_var AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),  -- isolated A: (A{2,})* matches empty here, not 1
+        (2, ARRAY['_']),
+        (3, ARRAY['A']),
+        (4, ARRAY['A']),  -- run of 2: matched
+        (5, ARRAY['_'])
+    ) AS t(id, flags)
+)
+SELECT id, flags,
+       first_value(id) OVER w AS match_start,
+       last_value(id) OVER w AS match_end,
+       count(*) OVER w AS match_count
+FROM test_nested_quant_var
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A{2,})*)
+    DEFINE A AS 'A' = ANY(flags)
+);
+ id | flags | match_start | match_end | match_count 
+----+-------+-------------+-----------+-------------
+  1 | {A}   |             |           |           0
+  2 | {_}   |             |           |           0
+  3 | {A}   |           3 |         4 |           2
+  4 | {A}   |             |           |           0
+  5 | {_}   |             |           |           0
+(5 rows)
+
+-- Same for a GROUP child: ((A B){2,})* must not collapse to (A B)*.
+-- An isolated single (A B) pair must yield an EMPTY match (count 0).
+WITH test_nested_quant_group AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),  -- isolated (A B) pair: matches empty here
+        (2, ARRAY['B']),
+        (3, ARRAY['_']),
+        (4, ARRAY['A']),
+        (5, ARRAY['B']),
+        (6, ARRAY['A']),
+        (7, ARRAY['B']),  -- run of 2 pairs: matched
+        (8, ARRAY['_'])
+    ) AS t(id, flags)
+)
+SELECT id, flags,
+       first_value(id) OVER w AS match_start,
+       last_value(id) OVER w AS match_end,
+       count(*) OVER w AS match_count
+FROM test_nested_quant_group
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (((A B){2,})*)
+    DEFINE A AS 'A' = ANY(flags), B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end | match_count 
+----+-------+-------------+-----------+-------------
+  1 | {A}   |             |           |           0
+  2 | {B}   |             |           |           0
+  3 | {_}   |             |           |           0
+  4 | {A}   |           4 |         7 |           4
+  5 | {B}   |             |           |           0
+  6 | {A}   |             |           |           0
+  7 | {B}   |             |           |           0
+  8 | {_}   |             |           |           0
+(8 rows)
+
 -- ============================================================
 -- Pathological Pattern Runtime Protection
 -- ============================================================
diff --git a/src/test/regress/sql/rpr_explain.sql b/src/test/regress/sql/rpr_explain.sql
index a527615849a..c8b159e30e6 100644
--- a/src/test/regress/sql/rpr_explain.sql
+++ b/src/test/regress/sql/rpr_explain.sql
@@ -499,6 +499,29 @@ WINDOW w AS (
     DEFINE A AS v % 3 = 1, B AS v % 3 = 2
 );');
 
+-- (A{2,})* must NOT flatten to a* (H-1): counts {0} UNION [2, INF) leave 1
+-- unreachable.  The planner keeps it as (a{2,})*, not a*.
+CREATE VIEW rpr_ev_nested_quant_no_flatten AS
+SELECT count(*) OVER w
+FROM generate_series(1, 6) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A{2,})*)
+    DEFINE A AS v % 3 <> 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_nested_quant_no_flatten'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 6) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A{2,})*)
+    DEFINE A AS v % 3 <> 0
+);');
+
 -- ============================================================
 -- Context Statistics Tests (peak, total, pruned + absorbed/skipped)
 -- ============================================================
diff --git a/src/test/regress/sql/rpr_nfa.sql b/src/test/regress/sql/rpr_nfa.sql
index 128476aa1d1..febf834565d 100644
--- a/src/test/regress/sql/rpr_nfa.sql
+++ b/src/test/regress/sql/rpr_nfa.sql
@@ -1583,6 +1583,59 @@ WINDOW w AS (
         B AS 'B' = ANY(flags)
 );
 
+-- Nested quantifier flattening must not widen the matching language (H-1).
+-- (A{k,})* with k >= 2 reaches repetition counts {0} UNION [k, INF); the gap
+-- 1..k-1 is unreachable, so it must NOT collapse to A*.  An isolated single A
+-- must yield an EMPTY match (count 0), not a length-1 match.
+WITH test_nested_quant_var AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),  -- isolated A: (A{2,})* matches empty here, not 1
+        (2, ARRAY['_']),
+        (3, ARRAY['A']),
+        (4, ARRAY['A']),  -- run of 2: matched
+        (5, ARRAY['_'])
+    ) AS t(id, flags)
+)
+SELECT id, flags,
+       first_value(id) OVER w AS match_start,
+       last_value(id) OVER w AS match_end,
+       count(*) OVER w AS match_count
+FROM test_nested_quant_var
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A{2,})*)
+    DEFINE A AS 'A' = ANY(flags)
+);
+
+-- Same for a GROUP child: ((A B){2,})* must not collapse to (A B)*.
+-- An isolated single (A B) pair must yield an EMPTY match (count 0).
+WITH test_nested_quant_group AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),  -- isolated (A B) pair: matches empty here
+        (2, ARRAY['B']),
+        (3, ARRAY['_']),
+        (4, ARRAY['A']),
+        (5, ARRAY['B']),
+        (6, ARRAY['A']),
+        (7, ARRAY['B']),  -- run of 2 pairs: matched
+        (8, ARRAY['_'])
+    ) AS t(id, flags)
+)
+SELECT id, flags,
+       first_value(id) OVER w AS match_start,
+       last_value(id) OVER w AS match_end,
+       count(*) OVER w AS match_count
+FROM test_nested_quant_group
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (((A B){2,})*)
+    DEFINE A AS 'A' = ANY(flags), B AS 'B' = ANY(flags)
+);
+
 -- ============================================================
 -- Pathological Pattern Runtime Protection
 -- ============================================================
-- 
2.50.1 (Apple Git-155)


From dcfeb7ba74a9acf49d41c4e395fd44952eed8837 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Mon, 1 Jun 2026 17:03:04 +0900
Subject: [PATCH 36/68] Avoid INF-valued quantifier bound in RPR
 consecutive-merge optimization

mergeConsecutiveVars() and mergeConsecutiveGroups() accumulate the min and
max bounds of two adjacent pattern elements, guarding against overflow with
prev->min <= RPR_QUANTITY_INF - child->min (and likewise for max).  The <=
let a sum land exactly on RPR_QUANTITY_INF (INT32_MAX), the value reserved as
the sentinel for an unbounded max.  The merged element then carried
min == INF, which is not a valid finite lower bound: it tripped the Assert in
fillRPRPattern* at plan time, and in a non-assert build silently produced a
pattern that can never match.

Tighten both guards to a strict <, so a sum reaching INF falls back and
leaves the elements unmerged.  This matches the >= INF check already used by
the quantifier-multiply path and the strict bound in mergeGroupPrefixSuffix.

Per a report from a static analysis tool.
---
 src/backend/optimizer/plan/rpr.c       | 22 ++++---
 src/test/regress/expected/rpr_base.out | 83 ++++++++++++++++++++++++++
 src/test/regress/sql/rpr_base.sql      | 43 +++++++++++++
 3 files changed, 140 insertions(+), 8 deletions(-)

diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index 2a98977b288..b989fcc5162 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -245,13 +245,16 @@ mergeConsecutiveVars(List *children)
 			/* ----------------------
 			 * Can merge consecutive VAR nodes if:
 			 * 1. Same variable name
-			 * 2. No min overflow: prev->min + child->min <= INF
-			 * 3. No max overflow: prev->max + child->max <= INF (or either is INF)
+			 * 2. No min overflow: prev->min + child->min < INF
+			 * 3. No max overflow: prev->max + child->max < INF (or either is INF)
+			 *
+			 * Strict <: a sum equal to INF would alias the unbounded sentinel
+			 * (min must stay finite; a finite max must not become INF).
 			 */
 			if (prev != NULL &&
 				strcmp(prev->varName, child->varName) == 0 &&
-				prev->min <= RPR_QUANTITY_INF - child->min &&
-				(prev->max <= RPR_QUANTITY_INF - child->max ||
+				prev->min < RPR_QUANTITY_INF - child->min &&
+				(prev->max < RPR_QUANTITY_INF - child->max ||
 				 prev->max == RPR_QUANTITY_INF ||
 				 child->max == RPR_QUANTITY_INF))
 			{
@@ -319,13 +322,16 @@ mergeConsecutiveGroups(List *children)
 			/* ----------------------
 			 * Can merge consecutive GROUP nodes if:
 			 * 1. Identical children
-			 * 2. No min overflow: prev->min + child->min <= INF
-			 * 3. No max overflow: prev->max + child->max <= INF (or either is INF)
+			 * 2. No min overflow: prev->min + child->min < INF
+			 * 3. No max overflow: prev->max + child->max < INF (or either is INF)
+			 *
+			 * Strict <: a sum equal to INF would alias the unbounded sentinel
+			 * (min must stay finite; a finite max must not become INF).
 			 */
 			if (prev != NULL &&
 				rprPatternChildrenEqual(prev->children, child->children) &&
-				prev->min <= RPR_QUANTITY_INF - child->min &&
-				(prev->max <= RPR_QUANTITY_INF - child->max ||
+				prev->min < RPR_QUANTITY_INF - child->min &&
+				(prev->max < RPR_QUANTITY_INF - child->max ||
 				 prev->max == RPR_QUANTITY_INF ||
 				 child->max == RPR_QUANTITY_INF))
 			{
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index c96a1216cc3..ebc1088018a 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -3459,6 +3459,25 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
          ->  Seq Scan on rpr_plan
 (7 rows)
 
+-- Consecutive VAR merge at the boundary: A{1073741823,} A{1073741823,} ->
+-- a{2147483646,}.  The min sum 2147483646 = INT32_MAX - 1 is the largest
+-- still-finite bound, so the merge proceeds; a sum of exactly INF instead
+-- falls back (see the Optimization Fallback Tests).
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN (A{1073741823,} A{1073741823,}) DEFINE A AS val > 0);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a{2147483646,}"
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 rows)
+
 -- Consecutive GROUP merge with finite quantifiers: ((A B){5}) ((A B){10}) -> merged
 EXPLAIN (COSTS OFF)
 SELECT COUNT(*) OVER w FROM rpr_plan
@@ -3509,6 +3528,25 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
          ->  Seq Scan on rpr_plan
 (7 rows)
 
+-- Consecutive GROUP merge at the boundary: (A B){1073741823,} (A B){1073741823,}
+-- -> (a b){2147483646,}.  The min sum INT32_MAX - 1 is still finite, so the
+-- merge proceeds; a sum of exactly INF instead falls back (see the
+-- Optimization Fallback Tests).
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN ((A B){1073741823,} (A B){1073741823,}) DEFINE A AS val <= 50, B AS val > 50);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a' b'){2147483646,}"
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 rows)
+
 -- PREFIX merge: A B (A B)+ -> (a b){2,}
 EXPLAIN (COSTS OFF)
 SELECT COUNT(*) OVER w FROM rpr_plan
@@ -5269,6 +5307,51 @@ WINDOW w AS (
 (7 rows)
 
 -- Expected: Fallback - prefix elements don't match GROUP content
+-- Test: consecutive VAR merge whose min sum is exactly INF causes fallback.
+-- 1073741824 + 1073741823 = 2147483647 = INT32_MAX = RPR_QUANTITY_INF.
+-- Merging would yield a VAR with min == INF, so the merge must fall back and
+-- leave the two VARs unmerged (mirrors the multiply path's >= INF guard).
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_fallback
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{1073741824,} A{1073741823,})
+    DEFINE A AS val > 0
+);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a{1073741824,}" a{1073741823,}
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_fallback
+(7 rows)
+
+-- Expected: Fallback - VARs not merged (min sum 2147483647 == INF)
+-- Test: consecutive GROUP merge whose min sum is exactly INF causes fallback.
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_fallback
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A B){1073741824,} (A B){1073741823,})
+    DEFINE A AS val > 0, B AS val > 5
+);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a' b'){1073741824,}" (a b){1073741823,}
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_fallback
+(7 rows)
+
+-- Expected: Fallback - GROUPs not merged (min sum 2147483647 == INF)
 DROP TABLE rpr_fallback;
 -- ============================================================
 -- Planner Integration Tests
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index d7b63cfc690..63173615273 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -2386,6 +2386,15 @@ SELECT COUNT(*) OVER w FROM rpr_plan
 WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
              PATTERN (A A+) DEFINE A AS val > 0);
 
+-- Consecutive VAR merge at the boundary: A{1073741823,} A{1073741823,} ->
+-- a{2147483646,}.  The min sum 2147483646 = INT32_MAX - 1 is the largest
+-- still-finite bound, so the merge proceeds; a sum of exactly INF instead
+-- falls back (see the Optimization Fallback Tests).
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN (A{1073741823,} A{1073741823,}) DEFINE A AS val > 0);
+
 -- Consecutive GROUP merge with finite quantifiers: ((A B){5}) ((A B){10}) -> merged
 EXPLAIN (COSTS OFF)
 SELECT COUNT(*) OVER w FROM rpr_plan
@@ -2406,6 +2415,15 @@ SELECT COUNT(*) OVER w FROM rpr_plan
 WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
              PATTERN ((A B){2} (A B)+) DEFINE A AS val <= 50, B AS val > 50);
 
+-- Consecutive GROUP merge at the boundary: (A B){1073741823,} (A B){1073741823,}
+-- -> (a b){2147483646,}.  The min sum INT32_MAX - 1 is still finite, so the
+-- merge proceeds; a sum of exactly INF instead falls back (see the
+-- Optimization Fallback Tests).
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN ((A B){1073741823,} (A B){1073741823,}) DEFINE A AS val <= 50, B AS val > 50);
+
 -- PREFIX merge: A B (A B)+ -> (a b){2,}
 EXPLAIN (COSTS OFF)
 SELECT COUNT(*) OVER w FROM rpr_plan
@@ -3213,6 +3231,31 @@ WINDOW w AS (
 );
 -- Expected: Fallback - prefix elements don't match GROUP content
 
+-- Test: consecutive VAR merge whose min sum is exactly INF causes fallback.
+-- 1073741824 + 1073741823 = 2147483647 = INT32_MAX = RPR_QUANTITY_INF.
+-- Merging would yield a VAR with min == INF, so the merge must fall back and
+-- leave the two VARs unmerged (mirrors the multiply path's >= INF guard).
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_fallback
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{1073741824,} A{1073741823,})
+    DEFINE A AS val > 0
+);
+-- Expected: Fallback - VARs not merged (min sum 2147483647 == INF)
+
+-- Test: consecutive GROUP merge whose min sum is exactly INF causes fallback.
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_fallback
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A B){1073741824,} (A B){1073741823,})
+    DEFINE A AS val > 0, B AS val > 5
+);
+-- Expected: Fallback - GROUPs not merged (min sum 2147483647 == INF)
+
 DROP TABLE rpr_fallback;
 
 -- ============================================================
-- 
2.50.1 (Apple Git-155)


From 176f477a820f40ab31cb34fcf4493de60b004579 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Mon, 1 Jun 2026 21:36:38 +0900
Subject: [PATCH 37/68] Fix count slot leak in row pattern recognition
 absorption

When a leaf variable in an absorbable group reaches its maximum count,
the match-phase inline advance walks it through the enclosing END
elements without clearing the variable's own counts[] slot, unlike the
regular exit in nfa_advance_var.  Sibling elements at the same depth
share a slot, so the stale count leaked into a following sibling group
and tripped the max-count Assert -- or, in a non-assert build, produced
a wrong match count.  PATTERN ((A B)+ (C D)+) is the smallest reproducer.

Clear the leaf variable's count on the inline exit.  Assert the
resulting contract -- each element zeroes its own slot on exit, so a
variable or group is entered with a zero count -- at the entry points,
and drop the now-redundant loop-back body reset.

Per a report from a static analysis tool.
---
 src/backend/executor/execRPR.c        | 55 +++++++++++++++++++++------
 src/test/regress/expected/rpr_nfa.out | 42 ++++++++++++++++++++
 src/test/regress/sql/rpr_nfa.sql      | 31 +++++++++++++++
 3 files changed, 117 insertions(+), 11 deletions(-)

diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 16a0f4ae375..9e45920de9c 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -315,8 +315,9 @@ nfa_states_equal(WindowAggState *winstate, RPRNFAState *s1, RPRNFAState *s2)
 	 * The +1 is the slot arithmetic: comparing through depth N requires
 	 * counts[0..N], i.e., N+1 entries.  Deeper slots (counts[d] with d >
 	 * elem->depth) are excluded because they hold scratch state from inner
-	 * groups that gets zeroed on re-entry (see END loop-back in
-	 * nfa_advance_end), and so must not participate in equivalence judgment.
+	 * groups.  Per the count-clear policy such a slot is zeroed when its
+	 * owning element exits (see nfa_advance_var and the inline fast path in
+	 * nfa_match), so it must not participate in equivalence judgment.
 	 */
 	elem = &pattern->elements[s1->elemIdx];
 	compareDepth = elem->depth + 1;
@@ -858,6 +859,16 @@ nfa_match(WindowAggState *winstate, RPRNFAContext *ctx, bool *varMatched)
 					state->elemIdx = elem->next;
 					state->counts[endDepth] = endCount;
 
+					/*
+					 * Leaf VAR exited (reached max): clear its own count so
+					 * the next occupant enters with zero, as nfa_advance_var
+					 * does on exit (this inline path replaces that exit).
+					 * depth > endDepth, so this leaves the group count just
+					 * written intact.
+					 */
+					Assert(endDepth < depth);
+					state->counts[depth] = 0;
+
 					/*
 					 * Chain through END elements within the absorbable region
 					 * (ABSORBABLE_BRANCH) until reaching the judgment point
@@ -873,7 +884,13 @@ nfa_match(WindowAggState *winstate, RPRNFAContext *ctx, bool *varMatched)
 						int			outerDepth = outerEnd->depth;
 						int32		outerCount = state->counts[outerDepth];
 
-						/* Reset exited group's count */
+						/*
+						 * Exit this intermediate group: clear its own count
+						 * (count-clear policy).  It sits below the absorbable
+						 * judgment point, so it is excluded from the
+						 * dominance comparison; the judgment point where the
+						 * chain stops keeps its count.
+						 */
 						state->counts[endDepth] = 0;
 
 						/* Increment outer group count */
@@ -925,6 +942,15 @@ nfa_route_to_elem(WindowAggState *winstate, RPRNFAContext *ctx,
 	{
 		RPRNFAState *skipState = NULL;
 
+		/*
+		 * Entry-side check of the count-clear policy: a VAR is always routed
+		 * to with a clean slot.  Each element zeroes its own count on exit,
+		 * so a nonzero count here would be a leak from an earlier element
+		 * (see nfa_advance_var / nfa_advance_end exit handling and the inline
+		 * fast path in nfa_match).
+		 */
+		Assert(state->counts[nextElem->depth] == 0);
+
 		/* Create skip state before add_unique, which may free state */
 		if (RPRElemCanSkip(nextElem))
 			skipState = nfa_state_clone(winstate, nextElem->next,
@@ -989,9 +1015,10 @@ nfa_advance_alt(WindowAggState *winstate, RPRNFAContext *ctx,
  * nfa_advance_begin
  *
  * Handle BEGIN element: group entry logic.
- * BEGIN is only visited at initial group entry (count is always 0).
+ * BEGIN is only visited at initial group entry; loop-back from END goes
+ * directly to first child, bypassing BEGIN.  Per the count-clear policy the
+ * group's own count slot is therefore already zero on entry (asserted below).
  * If min=0, creates a skip path past the group.
- * Loop-back from END goes directly to first child, bypassing BEGIN.
  */
 static void
 nfa_advance_begin(WindowAggState *winstate, RPRNFAContext *ctx,
@@ -1002,7 +1029,12 @@ nfa_advance_begin(WindowAggState *winstate, RPRNFAContext *ctx,
 	RPRPatternElement *elements = pattern->elements;
 	RPRNFAState *skipState = NULL;
 
-	state->counts[elem->depth] = 0;
+	/*
+	 * Entry-side check of the count-clear policy: the group's own count slot
+	 * is already zero here.  BEGIN is only visited at initial group entry,
+	 * and the previous occupant of this depth slot cleared it on exit.
+	 */
+	Assert(state->counts[elem->depth] == 0);
 
 	/* Optional group: create skip path (but don't route yet) */
 	if (elem->min == 0)
@@ -1094,8 +1126,6 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
 									  state->counts, state->isAbsorbable);
 
 		/* Primary path: loop back for real matches */
-		for (int d = depth + 1; d < pattern->maxDepth; d++)
-			state->counts[d] = 0;
 		state->elemIdx = elem->jump;
 		jumpElem = &elements[state->elemIdx];
 		nfa_route_to_elem(winstate, ctx, state, jumpElem,
@@ -1112,6 +1142,7 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
 		{
 			RPRPatternElement *nextElem;
 
+			/* Exit the group: clear its own count (count-clear policy) */
 			ffState->counts[depth] = 0;
 			ffState->elemIdx = elem->next;
 			nextElem = &elements[ffState->elemIdx];
@@ -1130,6 +1161,7 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
 		/* Must exit: reached max iterations. */
 		RPRPatternElement *nextElem;
 
+		/* Exit: clear the group's own count (count-clear policy) */
 		state->counts[depth] = 0;
 		state->elemIdx = elem->next;
 		nextElem = &elements[state->elemIdx];
@@ -1161,6 +1193,7 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
 		 */
 		exitState = nfa_state_clone(winstate, elem->next,
 									state->counts, state->isAbsorbable);
+		/* Exit branch: clear the group's own count (count-clear policy) */
 		exitState->counts[depth] = 0;
 		nextElem = &elements[exitState->elemIdx];
 
@@ -1169,8 +1202,6 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
 			exitState->counts[nextElem->depth]++;
 
 		/* Prepare loop state */
-		for (int d = depth + 1; d < pattern->maxDepth; d++)
-			state->counts[d] = 0;
 		state->elemIdx = elem->jump;
 		jumpElem = &elements[state->elemIdx];
 
@@ -1253,6 +1284,7 @@ nfa_advance_var(WindowAggState *winstate, RPRNFAContext *ctx,
 			/* Clone for exit, original stays for loop */
 			cloneState = nfa_state_clone(winstate, elem->next,
 										 state->counts, state->isAbsorbable);
+			/* Exit: clear the VAR's own count (count-clear policy) */
 			cloneState->counts[depth] = 0;
 			nextElem = &elements[cloneState->elemIdx];
 
@@ -1289,7 +1321,7 @@ nfa_advance_var(WindowAggState *winstate, RPRNFAContext *ctx,
 			/* Loop first (preferred for greedy) */
 			nfa_add_state_unique(winstate, ctx, cloneState);
 
-			/* Exit second */
+			/* Exit second: clear the VAR's own count (count-clear policy) */
 			state->counts[depth] = 0;
 			state->elemIdx = elem->next;
 			nextElem = &elements[state->elemIdx];
@@ -1326,6 +1358,7 @@ nfa_advance_var(WindowAggState *winstate, RPRNFAContext *ctx,
 		/* Exit only: advance to next element */
 		RPRPatternElement *nextElem;
 
+		/* Exit: clear the VAR's own count (count-clear policy) */
 		state->counts[depth] = 0;
 		state->elemIdx = elem->next;
 		nextElem = &elements[state->elemIdx];
diff --git a/src/test/regress/expected/rpr_nfa.out b/src/test/regress/expected/rpr_nfa.out
index 59b91ff9aa4..2a0d4f11e74 100644
--- a/src/test/regress/expected/rpr_nfa.out
+++ b/src/test/regress/expected/rpr_nfa.out
@@ -447,6 +447,48 @@ WINDOW w AS (
   7 | {X}   |             |          
 (7 rows)
 
+-- Two consecutive unbounded groups: (A B)+ (C D)+
+-- The leading group (A B)+ is absorbable (unbounded multi-element); (C D)+ is
+-- a distinct sibling group that does not merge with it.  When the leading group
+-- exits into the sibling, its body leaf-VAR count must be cleared so it does
+-- not leak into the sibling's shared depth slot.
+WITH test_absorb_two_groups AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['B']),
+        (3, ARRAY['C']),
+        (4, ARRAY['D']),
+        (5, ARRAY['A']),
+        (6, ARRAY['B']),
+        (7, ARRAY['C']),
+        (8, ARRAY['D'])
+    ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_absorb_two_groups
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A B)+ (C D)+)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags),
+        C AS 'C' = ANY(flags),
+        D AS 'D' = ANY(flags)
+);
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {A}   |           1 |         4
+  2 | {B}   |             |          
+  3 | {C}   |             |          
+  4 | {D}   |             |          
+  5 | {A}   |           5 |         8
+  6 | {B}   |             |          
+  7 | {C}   |             |          
+  8 | {D}   |             |          
+(8 rows)
+
 -- Fixed-length group absorption: (A B{2})+ C
 -- B{2} has min == max, equivalent to unrolling to (A B B)+ C
 WITH test_absorb_fixedlen AS (
diff --git a/src/test/regress/sql/rpr_nfa.sql b/src/test/regress/sql/rpr_nfa.sql
index febf834565d..6362c69f385 100644
--- a/src/test/regress/sql/rpr_nfa.sql
+++ b/src/test/regress/sql/rpr_nfa.sql
@@ -346,6 +346,37 @@ WINDOW w AS (
         B AS 'B' = ANY(flags)
 );
 
+-- Two consecutive unbounded groups: (A B)+ (C D)+
+-- The leading group (A B)+ is absorbable (unbounded multi-element); (C D)+ is
+-- a distinct sibling group that does not merge with it.  When the leading group
+-- exits into the sibling, its body leaf-VAR count must be cleared so it does
+-- not leak into the sibling's shared depth slot.
+WITH test_absorb_two_groups AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['B']),
+        (3, ARRAY['C']),
+        (4, ARRAY['D']),
+        (5, ARRAY['A']),
+        (6, ARRAY['B']),
+        (7, ARRAY['C']),
+        (8, ARRAY['D'])
+    ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_absorb_two_groups
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A B)+ (C D)+)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags),
+        C AS 'C' = ANY(flags),
+        D AS 'D' = ANY(flags)
+);
+
 -- Fixed-length group absorption: (A B{2})+ C
 -- B{2} has min == max, equivalent to unrolling to (A B B)+ C
 WITH test_absorb_fixedlen AS (
-- 
2.50.1 (Apple Git-155)


From cd08e4296f499b9c3db3b7926a625f63455a815e Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Mon, 1 Jun 2026 22:15:04 +0900
Subject: [PATCH 38/68] Demote dead runtime checks in the RPR executor to
 assertions

Three checks in the row pattern recognition NFA executor can never fail,
given how the planner builds the pattern:

- The final arm of nfa_advance_var's loop/exit cascade tested canExit,
  but reaching it implies !canLoop, which with min <= max forces canExit
  true.
- nfa_state_clone guarded the counts memcpy with counts != NULL &&
  maxDepth > 0, but every caller passes a live state's counts and the
  stored maxDepth is always >= 1.
- nfa_advance_alt bounded the branch walk with altIdx < numElements, but
  every jump/next link is -1 or a valid index, and the altIdx >= 0 test
  and the depth break already terminate the walk.

Replace each with an assertion that documents the invariant and drop the
runtime branch.  Also document why the nullable fast-forward path in
nfa_advance_end intentionally omits the isAbsorbable update: it runs only
for EMPTY_LOOP groups, which are never inside an absorbable region.

Per a report from a static analysis tool.
---
 src/backend/executor/execRPR.c | 25 +++++++++++++++++++------
 1 file changed, 19 insertions(+), 6 deletions(-)

diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 9e45920de9c..eac0c04c38d 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -279,8 +279,9 @@ nfa_state_clone(WindowAggState *winstate, int16 elemIdx,
 	RPRPatternElement *elem = &pattern->elements[elemIdx];
 
 	state->elemIdx = elemIdx;
-	if (counts != NULL && maxDepth > 0)
-		memcpy(state->counts, counts, sizeof(int32) * maxDepth);
+	/* Every reachable caller passes a live state's counts; maxDepth >= 1. */
+	Assert(counts != NULL && maxDepth > 0);
+	memcpy(state->counts, counts, sizeof(int32) * maxDepth);
 
 	/*
 	 * Compute isAbsorbable immediately at transition time. isAbsorbable =
@@ -981,11 +982,15 @@ nfa_advance_alt(WindowAggState *winstate, RPRNFAContext *ctx,
 	RPRPatternElement *elements = pattern->elements;
 	RPRElemIdx	altIdx = elem->next;
 
-	while (altIdx >= 0 && altIdx < pattern->numElements)
+	while (altIdx >= 0)
 	{
-		RPRPatternElement *altElem = &elements[altIdx];
+		RPRPatternElement *altElem;
 		RPRNFAState *newState;
 
+		/* Branch jump/next links are always -1 or a valid index */
+		Assert(altIdx < pattern->numElements);
+		altElem = &elements[altIdx];
+
 		/*
 		 * Stop if element is outside ALT scope (not a branch).  The check
 		 * fires when the last branch is a quantified group whose BEGIN.jump
@@ -1147,6 +1152,13 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
 			ffState->elemIdx = elem->next;
 			nextElem = &elements[ffState->elemIdx];
 
+			/*
+			 * Unlike the must-exit path, no isAbsorbable update is needed:
+			 * the fast-forward path runs only for EMPTY_LOOP (nullable)
+			 * groups, which are never inside an absorbable region, so
+			 * isAbsorbable is already false here.
+			 */
+
 			/* END->END: increment outer END's count */
 			if (RPRElemIsEnd(nextElem) &&
 				ffState->counts[nextElem->depth] < RPR_COUNT_MAX)
@@ -1353,11 +1365,12 @@ nfa_advance_var(WindowAggState *winstate, RPRNFAContext *ctx,
 		/* Loop only: keep state as-is */
 		nfa_add_state_unique(winstate, ctx, state);
 	}
-	else if (canExit)
+	else
 	{
-		/* Exit only: advance to next element */
+		/* Exit only: advance to next element (canExit necessarily true) */
 		RPRPatternElement *nextElem;
 
+		Assert(canExit);
 		/* Exit: clear the VAR's own count (count-clear policy) */
 		state->counts[depth] = 0;
 		state->elemIdx = elem->next;
-- 
2.50.1 (Apple Git-155)


From fb200f06d1202c13b85935c40d2a5d80c1da584e Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Mon, 1 Jun 2026 23:49:46 +0900
Subject: [PATCH 40/68] Honor reluctant quantifier for non-leading optional RPR
 variables

A reluctant optional pattern variable that is not the leading element --
for example A?? in PATTERN (B A?? C) -- matched greedily.  nfa_route_to_elem
builds the skip path of a min=0 variable in a fixed order, adding the enter
(match) state before the skip state.  Since the state list order encodes
match preference, the enter path always won, ignoring RPRElemIsReluctant.
Leading variables and optional groups were unaffected: they go through
nfa_advance_var / nfa_advance_begin, which already order the skip path first
when reluctant.

Mirror that handling in nfa_route_to_elem: when the optional variable is
reluctant, route the skip path first and drop the enter state if the skip
path has already reached FIN.

Add regression tests for non-leading reluctant optional variables and groups.

Per a report from a static analysis tool.
---
 src/backend/executor/execRPR.c        | 29 ++++++++++-
 src/test/regress/expected/rpr_nfa.out | 69 +++++++++++++++++++++++++++
 src/test/regress/sql/rpr_nfa.sql      | 56 ++++++++++++++++++++++
 3 files changed, 152 insertions(+), 2 deletions(-)

diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index a2c304da0d1..580b25f398d 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -957,10 +957,35 @@ nfa_route_to_elem(WindowAggState *winstate, RPRNFAContext *ctx,
 			skipState = nfa_state_clone(winstate, nextElem->next,
 										state->counts, state->isAbsorbable);
 
-		nfa_add_state_unique(winstate, ctx, state);
+		if (skipState != NULL && RPRElemIsReluctant(nextElem))
+		{
+			RPRNFAState *savedMatch = ctx->matchedState;
 
-		if (skipState != NULL)
+			/*
+			 * Reluctant optional VAR: prefer skipping.  Explore the skip path
+			 * first so it outranks the enter (match) path; if it reaches FIN
+			 * the shortest match is found and the enter state is dropped.
+			 * This mirrors the reluctant branch of nfa_advance_begin used by
+			 * the leading-position and optional-group paths.
+			 */
 			nfa_advance_state(winstate, ctx, skipState, currentPos);
+
+			if (ctx->matchedState != savedMatch)
+			{
+				nfa_state_free(winstate, state);
+				return;
+			}
+
+			nfa_add_state_unique(winstate, ctx, state);
+		}
+		else
+		{
+			/* Greedy (or non-skippable): enter first, then skip */
+			nfa_add_state_unique(winstate, ctx, state);
+
+			if (skipState != NULL)
+				nfa_advance_state(winstate, ctx, skipState, currentPos);
+		}
 	}
 	else
 	{
diff --git a/src/test/regress/expected/rpr_nfa.out b/src/test/regress/expected/rpr_nfa.out
index 2a0d4f11e74..829e8251aed 100644
--- a/src/test/regress/expected/rpr_nfa.out
+++ b/src/test/regress/expected/rpr_nfa.out
@@ -1222,6 +1222,75 @@ WINDOW w AS (
   3 | {C}   |             |          
 (3 rows)
 
+-- Non-leading reluctant optional VAR: (B A?? C)
+-- Reluctant A?? should prefer to skip, matching B(1) C(2) with A left
+-- unmatched (match_end 2).  The leading/group reluctant cases above go through
+-- the begin path; this exercises the non-leading skip path in
+-- nfa_route_to_elem, which must honor reluctant ordering too.
+WITH test_nonleading_reluctant AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['B']),
+        (2, ARRAY['A', 'C']),
+        (3, ARRAY['C'])
+    ) AS t(id, flags)
+)
+SELECT id, flags,
+       first_value(id) OVER w AS match_start,
+       last_value(id) OVER w AS match_end
+FROM test_nonleading_reluctant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (B A?? C)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags),
+        C AS 'C' = ANY(flags)
+);
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {B}   |           1 |         2
+  2 | {A,C} |             |          
+  3 | {C}   |             |          
+(3 rows)
+
+-- Non-leading reluctant optional GROUP with a follower: (B (A X)?? C)
+-- Like the VAR case above but a multi-element group; it goes through the
+-- begin path (nfa_advance_begin), which already honors reluctant ordering.
+-- Reluctant (A X)?? should skip, matching B(1) C(2), with the group skipped
+-- to the following C (not to FIN).
+WITH test_nonleading_reluctant_group AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['B']),
+        (2, ARRAY['A', 'C']),
+        (3, ARRAY['X']),
+        (4, ARRAY['C'])
+    ) AS t(id, flags)
+)
+SELECT id, flags,
+       first_value(id) OVER w AS match_start,
+       last_value(id) OVER w AS match_end
+FROM test_nonleading_reluctant_group
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (B (A X)?? C)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        X AS 'X' = ANY(flags),
+        B AS 'B' = ANY(flags),
+        C AS 'C' = ANY(flags)
+);
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {B}   |           1 |         2
+  2 | {A,C} |             |          
+  3 | {X}   |             |          
+  4 | {C}   |             |          
+(4 rows)
+
 -- Greedy/reluctant sequence: A+ B+? (greedy A, reluctant B at end)
 -- A consumes greedily, B+? exits to FIN after minimum match
 WITH test_greedy_then_reluctant AS (
diff --git a/src/test/regress/sql/rpr_nfa.sql b/src/test/regress/sql/rpr_nfa.sql
index 6362c69f385..3bbec496279 100644
--- a/src/test/regress/sql/rpr_nfa.sql
+++ b/src/test/regress/sql/rpr_nfa.sql
@@ -860,6 +860,62 @@ WINDOW w AS (
         C AS 'C' = ANY(flags)
 );
 
+-- Non-leading reluctant optional VAR: (B A?? C)
+-- Reluctant A?? should prefer to skip, matching B(1) C(2) with A left
+-- unmatched (match_end 2).  The leading/group reluctant cases above go through
+-- the begin path; this exercises the non-leading skip path in
+-- nfa_route_to_elem, which must honor reluctant ordering too.
+WITH test_nonleading_reluctant AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['B']),
+        (2, ARRAY['A', 'C']),
+        (3, ARRAY['C'])
+    ) AS t(id, flags)
+)
+SELECT id, flags,
+       first_value(id) OVER w AS match_start,
+       last_value(id) OVER w AS match_end
+FROM test_nonleading_reluctant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (B A?? C)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags),
+        C AS 'C' = ANY(flags)
+);
+
+-- Non-leading reluctant optional GROUP with a follower: (B (A X)?? C)
+-- Like the VAR case above but a multi-element group; it goes through the
+-- begin path (nfa_advance_begin), which already honors reluctant ordering.
+-- Reluctant (A X)?? should skip, matching B(1) C(2), with the group skipped
+-- to the following C (not to FIN).
+WITH test_nonleading_reluctant_group AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['B']),
+        (2, ARRAY['A', 'C']),
+        (3, ARRAY['X']),
+        (4, ARRAY['C'])
+    ) AS t(id, flags)
+)
+SELECT id, flags,
+       first_value(id) OVER w AS match_start,
+       last_value(id) OVER w AS match_end
+FROM test_nonleading_reluctant_group
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (B (A X)?? C)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        X AS 'X' = ANY(flags),
+        B AS 'B' = ANY(flags),
+        C AS 'C' = ANY(flags)
+);
+
 -- Greedy/reluctant sequence: A+ B+? (greedy A, reluctant B at end)
 -- A consumes greedily, B+? exits to FIN after minimum match
 WITH test_greedy_then_reluctant AS (
-- 
2.50.1 (Apple Git-155)


From 30e1c3dcea5abe8b0909d047550eb589e8e0a248 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Mon, 1 Jun 2026 22:38:55 +0900
Subject: [PATCH 39/68] Fix memory leak in row pattern recognition DEFINE
 evaluation

The row pattern recognition scan evaluates every DEFINE expression once
per partition row in update_reduced_frame, but did so with the plain
ExecEvalExpr in the long-lived query context and never reset it.  By-ref
scratch -- a NUMERIC DEFINE such as price * 1.1 > 165 is the typical case
-- therefore accumulated for the whole forward scan, growing memory
O(partition size) within a single window.

Evaluate the DEFINE expressions in the per-tuple memory context with
ExecEvalExprSwitchContext, in both nfa_evaluate_row and
nfa_reevaluate_dependent_vars, and reset that context once per processed
row in the scan loop.  The boolean results are consumed immediately, and
the cross-row state -- nfaVarMatched in the node context and the NFA
states in the partition context -- lives elsewhere, so the reset is safe.

Per a report from a static analysis tool.
---
 src/backend/executor/execRPR.c       |  3 ++-
 src/backend/executor/nodeWindowAgg.c | 10 ++++++++--
 2 files changed, 10 insertions(+), 3 deletions(-)

diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index eac0c04c38d..a2c304da0d1 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -1706,7 +1706,8 @@ nfa_reevaluate_dependent_vars(WindowAggState *winstate, RPRNFAContext *ctx,
 			Datum		result;
 			bool		isnull;
 
-			result = ExecEvalExpr(exprState, econtext, &isnull);
+			/* Per-tuple context; scratch freed by the per-row reset */
+			result = ExecEvalExprSwitchContext(exprState, econtext, &isnull);
 			winstate->nfaVarMatched[varIdx] = (!isnull && DatumGetBool(result));
 		}
 
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 0b0196e7e40..408bbc120b7 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -4515,6 +4515,12 @@ update_reduced_frame(WindowObject winobj, int64 pos)
 		 * appropriately as pruned or mismatched.
 		 */
 		ExecRPRCleanupDeadContexts(winstate, targetCtx);
+
+		/*
+		 * Free this row's per-tuple DEFINE-evaluation scratch; cross-row
+		 * state (nfaVarMatched, NFA states) lives in other contexts.
+		 */
+		ResetExprContext(winstate->ss.ps.ps_ExprContext);
 	}
 
 register_result:
@@ -4606,8 +4612,8 @@ nfa_evaluate_row(WindowObject winobj, int64 pos, bool *varMatched)
 		Datum		result;
 		bool		isnull;
 
-		/* Evaluate DEFINE expression */
-		result = ExecEvalExpr(exprState, econtext, &isnull);
+		/* Per-tuple context so by-ref scratch is freed by the per-row reset */
+		result = ExecEvalExprSwitchContext(exprState, econtext, &isnull);
 
 		varMatched[varIdx] = (!isnull && DatumGetBool(result));
 
-- 
2.50.1 (Apple Git-155)


From 8112fe3f2c1bee4fc330df0b12d737abd4d27903 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Tue, 2 Jun 2026 00:05:05 +0900
Subject: [PATCH 41/68] Reject column-less compound navigation in row pattern
 recognition

A row pattern navigation argument must contain at least one column
reference.  The check was applied only to simple navigation; a compound
form such as PREV(FIRST(1)) took the nesting-validation/flatten branch in
parse_rpr.c's define_walker, which never re-checked has_column_ref.  So
column-less compound navigation -- PREV(FIRST(1)), NEXT(LAST(1 + 2)),
PREV(FIRST(1), 2), and so on -- was silently accepted, while the simple
PREV(1) was correctly rejected.

Apply the same has_column_ref check after flattening a compound
navigation, so both forms are rejected consistently.  A compound form
whose argument references a column, such as PREV(FIRST(price)), is
unaffected.

Add regression tests covering column-less compound navigation with the
offset on the inner nav, the outer nav, both, or neither.

Per a report from a static analysis tool.
---
 src/backend/parser/parse_rpr.c    | 10 +++++
 src/test/regress/expected/rpr.out | 62 +++++++++++++++++++++++++++++++
 src/test/regress/sql/rpr.sql      | 52 ++++++++++++++++++++++++++
 3 files changed, 124 insertions(+)

diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index fa8c375f48b..6f44edfc50b 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -641,6 +641,16 @@ define_walker(Node *node, void *context)
 					nav->offset_arg = inner->offset_arg;
 					nav->arg = inner->arg;
 					flattened = true;
+
+					/*
+					 * The flattened argument must include a column reference,
+					 * just like the simple-nav case below.
+					 */
+					if (!ctx->has_column_ref)
+						ereport(ERROR,
+								errcode(ERRCODE_SYNTAX_ERROR),
+								errmsg("argument of row pattern navigation operation must include at least one column reference"),
+								parser_errposition(ctx->pstate, nav->location));
 				}
 				else if (!outer_phys && inner_phys)
 					ereport(ERROR,
diff --git a/src/test/regress/expected/rpr.out b/src/test/regress/expected/rpr.out
index 8793dda3cc3..550113700a9 100644
--- a/src/test/regress/expected/rpr.out
+++ b/src/test/regress/expected/rpr.out
@@ -1133,6 +1133,68 @@ WINDOW w AS (
 ERROR:  argument of row pattern navigation operation must include at least one column reference
 LINE 7:     DEFINE A AS PREV(1, 1) > 0
                         ^
+-- Compound navigation without a column reference must be rejected too,
+-- consistent with the simple forms above.
+-- PREV(FIRST(1)): compound, constant only, no column reference
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS PREV(FIRST(1)) > 0
+);
+ERROR:  argument of row pattern navigation operation must include at least one column reference
+LINE 7:     DEFINE A AS PREV(FIRST(1)) > 0
+                        ^
+-- NEXT(LAST(1 + 2)): compound, constant expression, no column reference
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS NEXT(LAST(1 + 2)) > 0
+);
+ERROR:  argument of row pattern navigation operation must include at least one column reference
+LINE 7:     DEFINE A AS NEXT(LAST(1 + 2)) > 0
+                        ^
+-- PREV(FIRST(1, 2)): compound, two-arg inner, no column reference
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS PREV(FIRST(1, 2)) > 0
+);
+ERROR:  argument of row pattern navigation operation must include at least one column reference
+LINE 7:     DEFINE A AS PREV(FIRST(1, 2)) > 0
+                        ^
+-- PREV(FIRST(1), 2): compound, outer offset only, no column reference
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS PREV(FIRST(1), 2) > 0
+);
+ERROR:  argument of row pattern navigation operation must include at least one column reference
+LINE 7:     DEFINE A AS PREV(FIRST(1), 2) > 0
+                        ^
+-- PREV(FIRST(1, 2), 3): compound, inner and outer offsets, no column reference
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS PREV(FIRST(1, 2), 3) > 0
+);
+ERROR:  argument of row pattern navigation operation must include at least one column reference
+LINE 7:     DEFINE A AS PREV(FIRST(1, 2), 3) > 0
+                        ^
 -- Non-constant offset: column reference as offset
 SELECT price FROM stock
 WINDOW w AS (
diff --git a/src/test/regress/sql/rpr.sql b/src/test/regress/sql/rpr.sql
index e4790f75b0a..0aa17f01e84 100644
--- a/src/test/regress/sql/rpr.sql
+++ b/src/test/regress/sql/rpr.sql
@@ -531,6 +531,58 @@ WINDOW w AS (
     DEFINE A AS PREV(1, 1) > 0
 );
 
+-- Compound navigation without a column reference must be rejected too,
+-- consistent with the simple forms above.
+-- PREV(FIRST(1)): compound, constant only, no column reference
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS PREV(FIRST(1)) > 0
+);
+
+-- NEXT(LAST(1 + 2)): compound, constant expression, no column reference
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS NEXT(LAST(1 + 2)) > 0
+);
+
+-- PREV(FIRST(1, 2)): compound, two-arg inner, no column reference
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS PREV(FIRST(1, 2)) > 0
+);
+
+-- PREV(FIRST(1), 2): compound, outer offset only, no column reference
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS PREV(FIRST(1), 2) > 0
+);
+
+-- PREV(FIRST(1, 2), 3): compound, inner and outer offsets, no column reference
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS PREV(FIRST(1, 2), 3) > 0
+);
+
 -- Non-constant offset: column reference as offset
 SELECT price FROM stock
 WINDOW w AS (
-- 
2.50.1 (Apple Git-155)


From 7f127147ae7fd860dc71d22ac539ea7644a77c6f Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Wed, 3 Jun 2026 20:10:59 +0900
Subject: [PATCH 42/68] Reserve the high varId nibble for row pattern
 recognition control elements

Row pattern recognition packs both pattern variables and pattern control
elements (group begin/end, alternation, finish) into a single uint8 varId.
Reserve the entire high nibble, the 16 values 0xF0-0xFF, for control
elements, so pattern variables use varId 0 to 0xEF and at most 240 distinct
variables are allowed.

Reserving all 16 high-nibble values, rather than only the ones currently
needed, leaves spare encodings for future control elements.  RPR_VARID_MAX
denotes the maximum variable varId (0xEF), and the parser limit check, the
bounds assertions, and the variable-name stack are sized from it.  The
boundary regression test exercises the 240/241 limits.
---
 src/backend/optimizer/plan/rpr.c       |  6 +++---
 src/backend/parser/parse_rpr.c         | 18 ++++++++++++------
 src/include/optimizer/rpr.h            | 20 ++++++++++++--------
 src/test/regress/expected/rpr_base.out | 20 ++++++++------------
 src/test/regress/sql/rpr_base.sql      | 18 +++++++-----------
 5 files changed, 42 insertions(+), 40 deletions(-)

diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index b989fcc5162..4652ca6ebeb 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -1017,7 +1017,7 @@ collectDefineVariables(List *defineVariableList, char **varNames)
 	foreach(lc, defineVariableList)
 	{
 		/* Parser already checked this limit in transformDefineClause */
-		Assert(numVars < RPR_VARID_MAX);
+		Assert(numVars <= RPR_VARID_MAX);
 
 		varNames[numVars++] = strVal(lfirst(lc));
 	}
@@ -1076,7 +1076,7 @@ scanRPRPatternRecursive(RPRPatternNode *node, char **varNames, int *numVars,
 			 * to varNames so they get a varId >= defineVariableList length,
 			 * which executor treats as TRUE.
 			 */
-			Assert(*numVars < RPR_VARID_MAX);
+			Assert(*numVars <= RPR_VARID_MAX);
 			varNames[(*numVars)++] = node->varName;
 			break;
 
@@ -1954,7 +1954,7 @@ buildRPRPattern(RPRPatternNode *pattern, List *defineVariableList,
 {
 	RPRPattern *result;
 	RPRPatternNode *optimized;
-	char	   *varNamesStack[RPR_VARID_MAX];
+	char	   *varNamesStack[RPR_VARID_MAX + 1];
 	int			numVars;
 	int			numElements;
 	RPRDepth	maxDepth;
diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index 6f44edfc50b..3872035110a 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -6,7 +6,7 @@
  * This file transforms RPR-related clauses from raw parse tree to planner
  * structures during query analysis:
  *   - Validates frame options (must start at CURRENT ROW, no EXCLUDE)
- *   - Validates PATTERN variable count (max RPR_VARID_MAX)
+ *   - Validates PATTERN variable count (max RPR_VARID_MAX + 1)
  *   - Transforms DEFINE clause into TargetEntry list
  *   - Stores PATTERN/SKIP TO/INITIAL clauses for planner
  *
@@ -192,10 +192,11 @@ transformRPR(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 
 /*
  * validateRPRPatternVarCount
- *		Validate that PATTERN variables don't exceed RPR_VARID_MAX.
+ *		Validate that PATTERN variable count fits the varId range.
  *
  * Recursively traverses the pattern tree, collecting unique variable names.
- * Throws an error if the number of unique variables exceeds RPR_VARID_MAX.
+ * Throws an error if the number of unique variables would require a varId
+ * greater than RPR_VARID_MAX.
  *
  * If rpDefs is non-NULL, each DEFINE variable name is also validated against
  * varNames; any DEFINE name not present in PATTERN is rejected with an error.
@@ -231,12 +232,17 @@ validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
 				}
 				if (!found)
 				{
-					/* Check against RPR_VARID_MAX before adding */
-					if (list_length(*varNames) >= RPR_VARID_MAX)
+					/*
+					 * Check against RPR_VARID_MAX before adding.  varId
+					 * values run 0 to RPR_VARID_MAX inclusive, so the next
+					 * varId to be assigned (the current list length) must not
+					 * exceed it.
+					 */
+					if (list_length(*varNames) > RPR_VARID_MAX)
 						ereport(ERROR,
 								errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
 								errmsg("too many pattern variables"),
-								errdetail("Maximum is %d.", RPR_VARID_MAX),
+								errdetail("Maximum is %d.", RPR_VARID_MAX + 1),
 								parser_errposition(pstate,
 												   exprLocation((Node *) node)));
 
diff --git a/src/include/optimizer/rpr.h b/src/include/optimizer/rpr.h
index 63c4b09daff..2fa483320ce 100644
--- a/src/include/optimizer/rpr.h
+++ b/src/include/optimizer/rpr.h
@@ -18,10 +18,14 @@
 
 /* Limits and special values */
 /*
- * Maximum number of unique pattern variables (varId 0 to RPR_VARID_MAX - 1).
- * Values from RPR_VARID_BEGIN (252) onward are reserved for control elements.
+ * Maximum pattern variable ID.  Pattern variables occupy varId 0 to
+ * RPR_VARID_MAX inclusive (240 distinct variables); any varId with the high
+ * nibble set (0xF0 to 0xFF) is reserved for control elements.  Reserving the
+ * whole high nibble, rather than just the values currently in use, leaves
+ * room for future control elements; this range can only be narrowed safely
+ * before release.
  */
-#define RPR_VARID_MAX		251
+#define RPR_VARID_MAX		0xEF	/* pattern variables are 0 to 0xEF */
 #define RPR_QUANTITY_INF	INT32_MAX	/* unbounded quantifier */
 #define RPR_COUNT_MAX		INT32_MAX	/* max runtime count (NFA state) */
 #define RPR_ELEMIDX_MAX		INT16_MAX	/* max pattern elements */
@@ -29,11 +33,11 @@
 #define RPR_DEPTH_MAX		(UINT8_MAX - 1) /* max pattern nesting depth: 254 */
 #define RPR_DEPTH_NONE		UINT8_MAX	/* no enclosing group (top-level) */
 
-/* Special varId values for control elements (252-255) */
-#define RPR_VARID_BEGIN		((RPRVarId) 252)	/* group begin */
-#define RPR_VARID_END		((RPRVarId) 253)	/* group end */
-#define RPR_VARID_ALT		((RPRVarId) 254)	/* alternation start */
-#define RPR_VARID_FIN		((RPRVarId) 255)	/* pattern finish */
+/* Reserved control-element varIds (high nibble 0xF; 0xF0-0xFB spare) */
+#define RPR_VARID_BEGIN		((RPRVarId) 0xFC)	/* group begin */
+#define RPR_VARID_END		((RPRVarId) 0xFD)	/* group end */
+#define RPR_VARID_ALT		((RPRVarId) 0xFE)	/* alternation start */
+#define RPR_VARID_FIN		((RPRVarId) 0xFF)	/* pattern finish */
 
 /* Element flags */
 #define RPR_ELEM_RELUCTANT			0x01	/* reluctant (non-greedy)
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index ebc1088018a..e9303af6384 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -6280,12 +6280,12 @@ ERROR:  DEFINE variable "b" is not used in PATTERN
 LINE 7:       B AS TRUE
               ^
 -- Expected: Error - B is not used in PATTERN
--- Test: 251 variables in PATTERN and DEFINE (boundary - should succeed)
+-- Test: 240 variables in PATTERN and DEFINE (boundary - should succeed)
 SELECT COUNT(*) OVER w FROM rpr_errors
 WINDOW w AS (
     ORDER BY id
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    PATTERN (V1 V2 V3 V4 V5 V6 V7 V8 V9 V10 V11 V12 V13 V14 V15 V16 V17 V18 V19 V20 V21 V22 V23 V24 V25 V26 V27 V28 V29 V30 V31 V32 V33 V34 V35 V36 V37 V38 V39 V40 V41 V42 V43 V44 V45 V46 V47 V48 V49 V50 V51 V52 V53 V54 V55 V56 V57 V58 V59 V60 V61 V62 V63 V64 V65 V66 V67 V68 V69 V70 V71 V72 V73 V74 V75 V76 V77 V78 V79 V80 V81 V82 V83 V84 V85 V86 V87 V88 V89 V90 V91 V92 V93 V94 V95 V96 V97 V98 V99 V100 V101 V102 V103 V104 V105 V106 V107 V108 V109 V110 V111 V112 V113 V114 V115 V116 V117 V118 V119 V120 V121 V122 V123 V124 V125 V126 V127 V128 V129 V130 V131 V132 V133 V134 V135 V136 V137 V138 V139 V140 V141 V142 V143 V144 V145 V146 V147 V148 V149 V150 V151 V152 V153 V154 V155 V156 V157 V158 V159 V160 V161 V162 V163 V164 V165 V166 V167 V168 V169 V170 V171 V172 V173 V174 V175 V176 V177 V178 V179 V180 V181 V182 V183 V184 V185 V186 V187 V188 V189 V190 V191 V192 V193 V194 V195 V196 V197 V198 V199 V200 V201 V202 V203 V204 V205 V206 V207 V208 V209 V210 V211 V212 V213 V214 V215 V216 V217 V218 V219 V220 V221 V222 V223 V224 V225 V226 V227 V228 V229 V230 V231 V232 V233 V234 V235 V236 V237 V238 V239 V240 V241 V242 V243 V244 V245 V246 V247 V248 V249 V250 V251)
+    PATTERN (V1 V2 V3 V4 V5 V6 V7 V8 V9 V10 V11 V12 V13 V14 V15 V16 V17 V18 V19 V20 V21 V22 V23 V24 V25 V26 V27 V28 V29 V30 V31 V32 V33 V34 V35 V36 V37 V38 V39 V40 V41 V42 V43 V44 V45 V46 V47 V48 V49 V50 V51 V52 V53 V54 V55 V56 V57 V58 V59 V60 V61 V62 V63 V64 V65 V66 V67 V68 V69 V70 V71 V72 V73 V74 V75 V76 V77 V78 V79 V80 V81 V82 V83 V84 V85 V86 V87 V88 V89 V90 V91 V92 V93 V94 V95 V96 V97 V98 V99 V100 V101 V102 V103 V104 V105 V106 V107 V108 V109 V110 V111 V112 V113 V114 V115 V116 V117 V118 V119 V120 V121 V122 V123 V124 V125 V126 V127 V128 V129 V130 V131 V132 V133 V134 V135 V136 V137 V138 V139 V140 V141 V142 V143 V144 V145 V146 V147 V148 V149 V150 V151 V152 V153 V154 V155 V156 V157 V158 V159 V160 V161 V162 V163 V164 V165 V166 V167 V168 V169 V170 V171 V172 V173 V174 V175 V176 V177 V178 V179 V180 V181 V182 V183 V184 V185 V186 V187 V188 V189 V190 V191 V192 V193 V194 V195 V196 V197 V198 V199 V200 V201 V202 V203 V204 V205 V206 V207 V208 V209 V210 V211 V212 V213 V214 V215 V216 V217 V218 V219 V220 V221 V222 V223 V224 V225 V226 V227 V228 V229 V230 V231 V232 V233 V234 V235 V236 V237 V238 V239 V240)
     DEFINE
     V1 AS val > 0, V2 AS val > 0, V3 AS val > 0, V4 AS val > 0, V5 AS val > 0, V6 AS val > 0, V7 AS val > 0, V8 AS val > 0, V9 AS val > 0, V10 AS val > 0,
     V11 AS val > 0, V12 AS val > 0, V13 AS val > 0, V14 AS val > 0, V15 AS val > 0, V16 AS val > 0, V17 AS val > 0, V18 AS val > 0, V19 AS val > 0, V20 AS val > 0,
@@ -6310,9 +6310,7 @@ WINDOW w AS (
     V201 AS val > 0, V202 AS val > 0, V203 AS val > 0, V204 AS val > 0, V205 AS val > 0, V206 AS val > 0, V207 AS val > 0, V208 AS val > 0, V209 AS val > 0, V210 AS val > 0,
     V211 AS val > 0, V212 AS val > 0, V213 AS val > 0, V214 AS val > 0, V215 AS val > 0, V216 AS val > 0, V217 AS val > 0, V218 AS val > 0, V219 AS val > 0, V220 AS val > 0,
     V221 AS val > 0, V222 AS val > 0, V223 AS val > 0, V224 AS val > 0, V225 AS val > 0, V226 AS val > 0, V227 AS val > 0, V228 AS val > 0, V229 AS val > 0, V230 AS val > 0,
-    V231 AS val > 0, V232 AS val > 0, V233 AS val > 0, V234 AS val > 0, V235 AS val > 0, V236 AS val > 0, V237 AS val > 0, V238 AS val > 0, V239 AS val > 0, V240 AS val > 0,
-    V241 AS val > 0, V242 AS val > 0, V243 AS val > 0, V244 AS val > 0, V245 AS val > 0, V246 AS val > 0, V247 AS val > 0, V248 AS val > 0, V249 AS val > 0, V250 AS val > 0,
-    V251 AS val > 0
+    V231 AS val > 0, V232 AS val > 0, V233 AS val > 0, V234 AS val > 0, V235 AS val > 0, V236 AS val > 0, V237 AS val > 0, V238 AS val > 0, V239 AS val > 0, V240 AS val > 0
 );
  count 
 -------
@@ -6321,12 +6319,12 @@ WINDOW w AS (
 (2 rows)
 
 -- Expected: Success - exactly at RPR_VARID_MAX boundary
--- Test: 252 variables in PATTERN, 251 in DEFINE (exceeds limit with implicit TRUE)
+-- Test: 241 variables in PATTERN, 240 in DEFINE (exceeds limit with implicit TRUE)
 SELECT COUNT(*) OVER w FROM rpr_errors
 WINDOW w AS (
     ORDER BY id
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    PATTERN (V1 V2 V3 V4 V5 V6 V7 V8 V9 V10 V11 V12 V13 V14 V15 V16 V17 V18 V19 V20 V21 V22 V23 V24 V25 V26 V27 V28 V29 V30 V31 V32 V33 V34 V35 V36 V37 V38 V39 V40 V41 V42 V43 V44 V45 V46 V47 V48 V49 V50 V51 V52 V53 V54 V55 V56 V57 V58 V59 V60 V61 V62 V63 V64 V65 V66 V67 V68 V69 V70 V71 V72 V73 V74 V75 V76 V77 V78 V79 V80 V81 V82 V83 V84 V85 V86 V87 V88 V89 V90 V91 V92 V93 V94 V95 V96 V97 V98 V99 V100 V101 V102 V103 V104 V105 V106 V107 V108 V109 V110 V111 V112 V113 V114 V115 V116 V117 V118 V119 V120 V121 V122 V123 V124 V125 V126 V127 V128 V129 V130 V131 V132 V133 V134 V135 V136 V137 V138 V139 V140 V141 V142 V143 V144 V145 V146 V147 V148 V149 V150 V151 V152 V153 V154 V155 V156 V157 V158 V159 V160 V161 V162 V163 V164 V165 V166 V167 V168 V169 V170 V171 V172 V173 V174 V175 V176 V177 V178 V179 V180 V181 V182 V183 V184 V185 V186 V187 V188 V189 V190 V191 V192 V193 V194 V195 V196 V197 V198 V199 V200 V201 V202 V203 V204 V205 V206 V207 V208 V209 V210 V211 V212 V213 V214 V215 V216 V217 V218 V219 V220 V221 V222 V223 V224 V225 V226 V227 V228 V229 V230 V231 V232 V233 V234 V235 V236 V237 V238 V239 V240 V241 V242 V243 V244 V245 V246 V247 V248 V249 V250 V251 V252)
+    PATTERN (V1 V2 V3 V4 V5 V6 V7 V8 V9 V10 V11 V12 V13 V14 V15 V16 V17 V18 V19 V20 V21 V22 V23 V24 V25 V26 V27 V28 V29 V30 V31 V32 V33 V34 V35 V36 V37 V38 V39 V40 V41 V42 V43 V44 V45 V46 V47 V48 V49 V50 V51 V52 V53 V54 V55 V56 V57 V58 V59 V60 V61 V62 V63 V64 V65 V66 V67 V68 V69 V70 V71 V72 V73 V74 V75 V76 V77 V78 V79 V80 V81 V82 V83 V84 V85 V86 V87 V88 V89 V90 V91 V92 V93 V94 V95 V96 V97 V98 V99 V100 V101 V102 V103 V104 V105 V106 V107 V108 V109 V110 V111 V112 V113 V114 V115 V116 V117 V118 V119 V120 V121 V122 V123 V124 V125 V126 V127 V128 V129 V130 V131 V132 V133 V134 V135 V136 V137 V138 V139 V140 V141 V142 V143 V144 V145 V146 V147 V148 V149 V150 V151 V152 V153 V154 V155 V156 V157 V158 V159 V160 V161 V162 V163 V164 V165 V166 V167 V168 V169 V170 V171 V172 V173 V174 V175 V176 V177 V178 V179 V180 V181 V182 V183 V184 V185 V186 V187 V188 V189 V190 V191 V192 V193 V194 V195 V196 V197 V198 V199 V200 V201 V202 V203 V204 V205 V206 V207 V208 V209 V210 V211 V212 V213 V214 V215 V216 V217 V218 V219 V220 V221 V222 V223 V224 V225 V226 V227 V228 V229 V230 V231 V232 V233 V234 V235 V236 V237 V238 V239 V240 V241)
     DEFINE
     V1 AS val > 0, V2 AS val > 0, V3 AS val > 0, V4 AS val > 0, V5 AS val > 0, V6 AS val > 0, V7 AS val > 0, V8 AS val > 0, V9 AS val > 0, V10 AS val > 0,
     V11 AS val > 0, V12 AS val > 0, V13 AS val > 0, V14 AS val > 0, V15 AS val > 0, V16 AS val > 0, V17 AS val > 0, V18 AS val > 0, V19 AS val > 0, V20 AS val > 0,
@@ -6351,13 +6349,11 @@ WINDOW w AS (
     V201 AS val > 0, V202 AS val > 0, V203 AS val > 0, V204 AS val > 0, V205 AS val > 0, V206 AS val > 0, V207 AS val > 0, V208 AS val > 0, V209 AS val > 0, V210 AS val > 0,
     V211 AS val > 0, V212 AS val > 0, V213 AS val > 0, V214 AS val > 0, V215 AS val > 0, V216 AS val > 0, V217 AS val > 0, V218 AS val > 0, V219 AS val > 0, V220 AS val > 0,
     V221 AS val > 0, V222 AS val > 0, V223 AS val > 0, V224 AS val > 0, V225 AS val > 0, V226 AS val > 0, V227 AS val > 0, V228 AS val > 0, V229 AS val > 0, V230 AS val > 0,
-    V231 AS val > 0, V232 AS val > 0, V233 AS val > 0, V234 AS val > 0, V235 AS val > 0, V236 AS val > 0, V237 AS val > 0, V238 AS val > 0, V239 AS val > 0, V240 AS val > 0,
-    V241 AS val > 0, V242 AS val > 0, V243 AS val > 0, V244 AS val > 0, V245 AS val > 0, V246 AS val > 0, V247 AS val > 0, V248 AS val > 0, V249 AS val > 0, V250 AS val > 0,
-    V251 AS val > 0
+    V231 AS val > 0, V232 AS val > 0, V233 AS val > 0, V234 AS val > 0, V235 AS val > 0, V236 AS val > 0, V237 AS val > 0, V238 AS val > 0, V239 AS val > 0, V240 AS val > 0
 );
 ERROR:  too many pattern variables
-DETAIL:  Maximum is 251.
--- Expected: ERROR - too many pattern variables (Maximum is 251)
+DETAIL:  Maximum is 240.
+-- Expected: ERROR - too many pattern variables (Maximum is 240)
 -- Test: Pattern nesting at maximum depth (depth 253)
 -- Note: 253 nested GROUP{3,7} quantifiers produce depth 253 after optimization
 SELECT id, val, COUNT(*) OVER w FROM rpr_errors
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index 63173615273..475541d4550 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -3948,12 +3948,12 @@ WINDOW w AS (
 );
 -- Expected: Error - B is not used in PATTERN
 
--- Test: 251 variables in PATTERN and DEFINE (boundary - should succeed)
+-- Test: 240 variables in PATTERN and DEFINE (boundary - should succeed)
 SELECT COUNT(*) OVER w FROM rpr_errors
 WINDOW w AS (
     ORDER BY id
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    PATTERN (V1 V2 V3 V4 V5 V6 V7 V8 V9 V10 V11 V12 V13 V14 V15 V16 V17 V18 V19 V20 V21 V22 V23 V24 V25 V26 V27 V28 V29 V30 V31 V32 V33 V34 V35 V36 V37 V38 V39 V40 V41 V42 V43 V44 V45 V46 V47 V48 V49 V50 V51 V52 V53 V54 V55 V56 V57 V58 V59 V60 V61 V62 V63 V64 V65 V66 V67 V68 V69 V70 V71 V72 V73 V74 V75 V76 V77 V78 V79 V80 V81 V82 V83 V84 V85 V86 V87 V88 V89 V90 V91 V92 V93 V94 V95 V96 V97 V98 V99 V100 V101 V102 V103 V104 V105 V106 V107 V108 V109 V110 V111 V112 V113 V114 V115 V116 V117 V118 V119 V120 V121 V122 V123 V124 V125 V126 V127 V128 V129 V130 V131 V132 V133 V134 V135 V136 V137 V138 V139 V140 V141 V142 V143 V144 V145 V146 V147 V148 V149 V150 V151 V152 V153 V154 V155 V156 V157 V158 V159 V160 V161 V162 V163 V164 V165 V166 V167 V168 V169 V170 V171 V172 V173 V174 V175 V176 V177 V178 V179 V180 V181 V182 V183 V184 V185 V186 V187 V188 V189 V190 V191 V192 V193 V194 V195 V196 V197 V198 V199 V200 V201 V202 V203 V204 V205 V206 V207 V208 V209 V210 V211 V212 V213 V214 V215 V216 V217 V218 V219 V220 V221 V222 V223 V224 V225 V226 V227 V228 V229 V230 V231 V232 V233 V234 V235 V236 V237 V238 V239 V240 V241 V242 V243 V244 V245 V246 V247 V248 V249 V250 V251)
+    PATTERN (V1 V2 V3 V4 V5 V6 V7 V8 V9 V10 V11 V12 V13 V14 V15 V16 V17 V18 V19 V20 V21 V22 V23 V24 V25 V26 V27 V28 V29 V30 V31 V32 V33 V34 V35 V36 V37 V38 V39 V40 V41 V42 V43 V44 V45 V46 V47 V48 V49 V50 V51 V52 V53 V54 V55 V56 V57 V58 V59 V60 V61 V62 V63 V64 V65 V66 V67 V68 V69 V70 V71 V72 V73 V74 V75 V76 V77 V78 V79 V80 V81 V82 V83 V84 V85 V86 V87 V88 V89 V90 V91 V92 V93 V94 V95 V96 V97 V98 V99 V100 V101 V102 V103 V104 V105 V106 V107 V108 V109 V110 V111 V112 V113 V114 V115 V116 V117 V118 V119 V120 V121 V122 V123 V124 V125 V126 V127 V128 V129 V130 V131 V132 V133 V134 V135 V136 V137 V138 V139 V140 V141 V142 V143 V144 V145 V146 V147 V148 V149 V150 V151 V152 V153 V154 V155 V156 V157 V158 V159 V160 V161 V162 V163 V164 V165 V166 V167 V168 V169 V170 V171 V172 V173 V174 V175 V176 V177 V178 V179 V180 V181 V182 V183 V184 V185 V186 V187 V188 V189 V190 V191 V192 V193 V194 V195 V196 V197 V198 V199 V200 V201 V202 V203 V204 V205 V206 V207 V208 V209 V210 V211 V212 V213 V214 V215 V216 V217 V218 V219 V220 V221 V222 V223 V224 V225 V226 V227 V228 V229 V230 V231 V232 V233 V234 V235 V236 V237 V238 V239 V240)
     DEFINE
     V1 AS val > 0, V2 AS val > 0, V3 AS val > 0, V4 AS val > 0, V5 AS val > 0, V6 AS val > 0, V7 AS val > 0, V8 AS val > 0, V9 AS val > 0, V10 AS val > 0,
     V11 AS val > 0, V12 AS val > 0, V13 AS val > 0, V14 AS val > 0, V15 AS val > 0, V16 AS val > 0, V17 AS val > 0, V18 AS val > 0, V19 AS val > 0, V20 AS val > 0,
@@ -3978,18 +3978,16 @@ WINDOW w AS (
     V201 AS val > 0, V202 AS val > 0, V203 AS val > 0, V204 AS val > 0, V205 AS val > 0, V206 AS val > 0, V207 AS val > 0, V208 AS val > 0, V209 AS val > 0, V210 AS val > 0,
     V211 AS val > 0, V212 AS val > 0, V213 AS val > 0, V214 AS val > 0, V215 AS val > 0, V216 AS val > 0, V217 AS val > 0, V218 AS val > 0, V219 AS val > 0, V220 AS val > 0,
     V221 AS val > 0, V222 AS val > 0, V223 AS val > 0, V224 AS val > 0, V225 AS val > 0, V226 AS val > 0, V227 AS val > 0, V228 AS val > 0, V229 AS val > 0, V230 AS val > 0,
-    V231 AS val > 0, V232 AS val > 0, V233 AS val > 0, V234 AS val > 0, V235 AS val > 0, V236 AS val > 0, V237 AS val > 0, V238 AS val > 0, V239 AS val > 0, V240 AS val > 0,
-    V241 AS val > 0, V242 AS val > 0, V243 AS val > 0, V244 AS val > 0, V245 AS val > 0, V246 AS val > 0, V247 AS val > 0, V248 AS val > 0, V249 AS val > 0, V250 AS val > 0,
-    V251 AS val > 0
+    V231 AS val > 0, V232 AS val > 0, V233 AS val > 0, V234 AS val > 0, V235 AS val > 0, V236 AS val > 0, V237 AS val > 0, V238 AS val > 0, V239 AS val > 0, V240 AS val > 0
 );
 -- Expected: Success - exactly at RPR_VARID_MAX boundary
 
--- Test: 252 variables in PATTERN, 251 in DEFINE (exceeds limit with implicit TRUE)
+-- Test: 241 variables in PATTERN, 240 in DEFINE (exceeds limit with implicit TRUE)
 SELECT COUNT(*) OVER w FROM rpr_errors
 WINDOW w AS (
     ORDER BY id
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    PATTERN (V1 V2 V3 V4 V5 V6 V7 V8 V9 V10 V11 V12 V13 V14 V15 V16 V17 V18 V19 V20 V21 V22 V23 V24 V25 V26 V27 V28 V29 V30 V31 V32 V33 V34 V35 V36 V37 V38 V39 V40 V41 V42 V43 V44 V45 V46 V47 V48 V49 V50 V51 V52 V53 V54 V55 V56 V57 V58 V59 V60 V61 V62 V63 V64 V65 V66 V67 V68 V69 V70 V71 V72 V73 V74 V75 V76 V77 V78 V79 V80 V81 V82 V83 V84 V85 V86 V87 V88 V89 V90 V91 V92 V93 V94 V95 V96 V97 V98 V99 V100 V101 V102 V103 V104 V105 V106 V107 V108 V109 V110 V111 V112 V113 V114 V115 V116 V117 V118 V119 V120 V121 V122 V123 V124 V125 V126 V127 V128 V129 V130 V131 V132 V133 V134 V135 V136 V137 V138 V139 V140 V141 V142 V143 V144 V145 V146 V147 V148 V149 V150 V151 V152 V153 V154 V155 V156 V157 V158 V159 V160 V161 V162 V163 V164 V165 V166 V167 V168 V169 V170 V171 V172 V173 V174 V175 V176 V177 V178 V179 V180 V181 V182 V183 V184 V185 V186 V187 V188 V189 V190 V191 V192 V193 V194 V195 V196 V197 V198 V199 V200 V201 V202 V203 V204 V205 V206 V207 V208 V209 V210 V211 V212 V213 V214 V215 V216 V217 V218 V219 V220 V221 V222 V223 V224 V225 V226 V227 V228 V229 V230 V231 V232 V233 V234 V235 V236 V237 V238 V239 V240 V241 V242 V243 V244 V245 V246 V247 V248 V249 V250 V251 V252)
+    PATTERN (V1 V2 V3 V4 V5 V6 V7 V8 V9 V10 V11 V12 V13 V14 V15 V16 V17 V18 V19 V20 V21 V22 V23 V24 V25 V26 V27 V28 V29 V30 V31 V32 V33 V34 V35 V36 V37 V38 V39 V40 V41 V42 V43 V44 V45 V46 V47 V48 V49 V50 V51 V52 V53 V54 V55 V56 V57 V58 V59 V60 V61 V62 V63 V64 V65 V66 V67 V68 V69 V70 V71 V72 V73 V74 V75 V76 V77 V78 V79 V80 V81 V82 V83 V84 V85 V86 V87 V88 V89 V90 V91 V92 V93 V94 V95 V96 V97 V98 V99 V100 V101 V102 V103 V104 V105 V106 V107 V108 V109 V110 V111 V112 V113 V114 V115 V116 V117 V118 V119 V120 V121 V122 V123 V124 V125 V126 V127 V128 V129 V130 V131 V132 V133 V134 V135 V136 V137 V138 V139 V140 V141 V142 V143 V144 V145 V146 V147 V148 V149 V150 V151 V152 V153 V154 V155 V156 V157 V158 V159 V160 V161 V162 V163 V164 V165 V166 V167 V168 V169 V170 V171 V172 V173 V174 V175 V176 V177 V178 V179 V180 V181 V182 V183 V184 V185 V186 V187 V188 V189 V190 V191 V192 V193 V194 V195 V196 V197 V198 V199 V200 V201 V202 V203 V204 V205 V206 V207 V208 V209 V210 V211 V212 V213 V214 V215 V216 V217 V218 V219 V220 V221 V222 V223 V224 V225 V226 V227 V228 V229 V230 V231 V232 V233 V234 V235 V236 V237 V238 V239 V240 V241)
     DEFINE
     V1 AS val > 0, V2 AS val > 0, V3 AS val > 0, V4 AS val > 0, V5 AS val > 0, V6 AS val > 0, V7 AS val > 0, V8 AS val > 0, V9 AS val > 0, V10 AS val > 0,
     V11 AS val > 0, V12 AS val > 0, V13 AS val > 0, V14 AS val > 0, V15 AS val > 0, V16 AS val > 0, V17 AS val > 0, V18 AS val > 0, V19 AS val > 0, V20 AS val > 0,
@@ -4014,11 +4012,9 @@ WINDOW w AS (
     V201 AS val > 0, V202 AS val > 0, V203 AS val > 0, V204 AS val > 0, V205 AS val > 0, V206 AS val > 0, V207 AS val > 0, V208 AS val > 0, V209 AS val > 0, V210 AS val > 0,
     V211 AS val > 0, V212 AS val > 0, V213 AS val > 0, V214 AS val > 0, V215 AS val > 0, V216 AS val > 0, V217 AS val > 0, V218 AS val > 0, V219 AS val > 0, V220 AS val > 0,
     V221 AS val > 0, V222 AS val > 0, V223 AS val > 0, V224 AS val > 0, V225 AS val > 0, V226 AS val > 0, V227 AS val > 0, V228 AS val > 0, V229 AS val > 0, V230 AS val > 0,
-    V231 AS val > 0, V232 AS val > 0, V233 AS val > 0, V234 AS val > 0, V235 AS val > 0, V236 AS val > 0, V237 AS val > 0, V238 AS val > 0, V239 AS val > 0, V240 AS val > 0,
-    V241 AS val > 0, V242 AS val > 0, V243 AS val > 0, V244 AS val > 0, V245 AS val > 0, V246 AS val > 0, V247 AS val > 0, V248 AS val > 0, V249 AS val > 0, V250 AS val > 0,
-    V251 AS val > 0
+    V231 AS val > 0, V232 AS val > 0, V233 AS val > 0, V234 AS val > 0, V235 AS val > 0, V236 AS val > 0, V237 AS val > 0, V238 AS val > 0, V239 AS val > 0, V240 AS val > 0
 );
--- Expected: ERROR - too many pattern variables (Maximum is 251)
+-- Expected: ERROR - too many pattern variables (Maximum is 240)
 
 -- Test: Pattern nesting at maximum depth (depth 253)
 -- Note: 253 nested GROUP{3,7} quantifiers produce depth 253 after optimization
-- 
2.50.1 (Apple Git-155)


From c0aa0a1ca48f6c520658a50560b86d708118b9a0 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Wed, 3 Jun 2026 23:10:49 +0900
Subject: [PATCH 43/68] Generalize quantifier multiplication in row pattern
 recognition

The planner flattens a nested pattern quantifier (child{p,q}){m,n} into
child{p*m, q*n}.  This is valid only when the repetition counts the nested
quantifiers can produce form exactly one contiguous interval.  Replace the
previous handful of special cases with that general test: fold when the
outer count is exact (m == n), or the child is skippable (p == 0), or the
per-iteration intervals touch (p <= Max(m,1)*(q-p) + 1) and, for a skippable
outer, the zero case connects to the child range (m >= 1 or p <= 1).

This additionally folds contiguous cases that were previously left
unflattened, such as (A{2,3}){2,3} -> a{4,9}, (A+){3} -> a{3,}, and
(A+){2,4} -> a{2,}, while still leaving gap-producing patterns like
(A{2}){2,3} and (A{2,})* untouched.  The int64 overflow guards on the
multiplied bounds are retained.

Expand the quatifier-multiplication regression cases to exercise each
branch with both folding and gap (no-fold) patterns.  The pattern-nesting
depth tests now use reluctant quantifiers, which are not subject to
multiplication, so they still build the intended nesting depth.
---
 src/backend/optimizer/plan/rpr.c          | 130 +++++++++---------
 src/test/regress/expected/rpr_base.out    | 154 +++++++++++++++++++++-
 src/test/regress/expected/rpr_explain.out |  16 +--
 src/test/regress/sql/rpr_base.sql         |  72 +++++++++-
 4 files changed, 284 insertions(+), 88 deletions(-)

diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index 4652ca6ebeb..617a3869948 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -784,21 +784,25 @@ optimizeAltPattern(RPRPatternNode *pattern)
 
 /*
  * tryMultiplyQuantifiers
- *		Try to multiply quantifiers.
- *
- * Multiplication is SAFE when:
- *   1. Both unbounded, with skipless outer or child->min <= 1:
- *      (A*)* -> A*, (A+)+ -> A+, (A+)* -> A*, (A{2,})+ -> A{2,}
- *   2. Outer exact: (A{m,n}){k} -> A{m*k, n*k}
- *   3. Outer range + child {1,1}: (A){2,} -> A{2,}
- *
- * Multiplication is NOT safe when:
- *   - Only child unbounded: (A+){3} has different semantics
- *   - Outer range + child not {1,1}: gaps possible
- *     e.g., (A{2}){2,3} yields 4,6 only (not 4,5,6)
- *   - Skippable outer (min 0) + child->min >= 2: (A{2,})* reaches
- *     {0} UNION [child->min, INF), so 1..child->min-1 are unreachable
- *     and A* would wrongly admit them
+ *		Try to flatten (child{p,q}){m,n} into child{p*m, q*n}.
+ *
+ * Below, p,q are the child's {min,max} and m,n the outer {min,max}.
+ *
+ * Flattening is valid only when the repetition counts the nested quantifiers
+ * can produce form exactly the contiguous interval [p*m, q*n].  For an outer
+ * iteration count t (m <= t <= n) the child contributes any count in
+ * [t*p, t*q], and t = 0 contributes {0}.  The union of those intervals is
+ * contiguous, hence flattenable, when:
+ *
+ *   - m == n: a single outer count, so the result is just [m*p, m*q]; or
+ *   - p == 0: every interval starts at 0, so they all overlap; or
+ *   - consecutive intervals touch and the zero case (if any) connects:
+ *       p <= Max(m,1)*(q-p) + 1   (touch; trivially true if q is unbounded)
+ *       and (m >= 1 or p <= 1)    (when m == 0, {0} must reach [p,q])
+ *
+ * Otherwise gaps appear and the pattern is left unflattened: (A{2}){2,3}
+ * yields {4,6} (not 4..6), and (A{2,})* yields {0} UNION [2,INF) (not
+ * [0,INF), so A* would wrongly admit a single A).
  *
  * Returns the child node with multiplied quantifiers if successful,
  * otherwise returns the original pattern unchanged.
@@ -807,6 +811,7 @@ static RPRPatternNode *
 tryMultiplyQuantifiers(RPRPatternNode *pattern)
 {
 	RPRPatternNode *child;
+	bool		safe;
 	int64		new_min_64;
 	int64		new_max_64;
 
@@ -823,69 +828,60 @@ tryMultiplyQuantifiers(RPRPatternNode *pattern)
 		child->reluctant)
 		return pattern;
 
-	/* Case 1: Both unbounded - (A*)* -> A*, (A+)+ -> A+ */
-	if (child->max == RPR_QUANTITY_INF && pattern->max == RPR_QUANTITY_INF)
+	/*
+	 * Decide whether the achievable counts form one contiguous interval.  The
+	 * child quantifier is {child->min, child->max} and the outer one is
+	 * {pattern->min, pattern->max}; either max may be RPR_QUANTITY_INF.
+	 */
+	if (pattern->min == pattern->max || child->min == 0)
+		safe = true;
+	else
 	{
+		bool		touch;
+		bool		zero_ok;
+
 		/*
-		 * A skippable outer (min 0) over a child with min >= 2 reaches
-		 * repetition counts {0} UNION [child->min, INF): the counts
-		 * 1..child->min-1 are unreachable, and no single quantifier can
-		 * express that gap.  Flattening to A{0,INF} = A* would wrongly admit
-		 * them, e.g. (A{2,})* would match a single A.  Multiplication is safe
-		 * here only when child->min <= 1 (the reachable set is then
-		 * contiguous from 0); otherwise leave the pattern unflattened.
+		 * Consecutive intervals [t*min, t*max] and [(t+1)*min, (t+1)*max]
+		 * touch when (t+1)*min <= t*max + 1, i.e. min <= t*(max-min) + 1.
+		 * This is tightest at the smallest t in play, Max(pattern->min, 1).
+		 * An unbounded child->max makes every interval reach INF, so they
+		 * always touch.
 		 */
-		if (pattern->min == 0 && child->min >= 2)
-			return pattern;
+		if (child->max == RPR_QUANTITY_INF)
+			touch = true;
+		else
+			touch = ((int64) child->min <=
+					 (int64) Max(pattern->min, 1) * (child->max - child->min) + 1);
 
-		new_min_64 = (int64) child->min * pattern->min;
-		if (new_min_64 >= RPR_QUANTITY_INF)
-			return pattern;		/* overflow, skip optimization */
+		/*
+		 * A skippable outer (min 0) also needs {0} adjacent to the child
+		 * range.
+		 */
+		zero_ok = (pattern->min >= 1 || child->min <= 1);
 
-		child->min = (int) new_min_64;
-		child->max = RPR_QUANTITY_INF;
-		return child;
+		safe = touch && zero_ok;
 	}
 
-	/*----------
-	 * Case 2: Outer exact (min == max): (A{2,3}){4} -> A{8,12}.
-	 *         Safe because every iteration produces the same range.
-	 *
-	 * Case 3: Child {1,1}: (A){2,5} -> A{2,5}.
-	 *         Safe because the child contributes exactly one per
-	 *         iteration, so the outer range maps directly.
-	 *
-	 * Unsafe example: (A{2}){2,3} produces counts 4 or 6 only, not the full
-	 * range 4..6, so we cannot flatten when child has a non-trivial range AND
-	 * outer is also a range.
-	 *----------
-	 */
-	if (child->max != RPR_QUANTITY_INF &&
-		(pattern->min == pattern->max ||
-		 (child->min == 1 && child->max == 1)))
-	{
-		new_min_64 = (int64) pattern->min * child->min;
-		if (new_min_64 >= RPR_QUANTITY_INF)
-			return pattern;
-
-		/* Outer unbounded: result is unbounded regardless of child */
-		if (pattern->max == RPR_QUANTITY_INF)
-			new_max_64 = RPR_QUANTITY_INF;
-		else
-		{
-			new_max_64 = (int64) pattern->max * child->max;
+	if (!safe)
+		return pattern;
 
-			if (new_max_64 >= RPR_QUANTITY_INF)
-				return pattern;
-		}
+	/* Flatten the child quantifier, guarding against overflow. */
+	new_min_64 = (int64) pattern->min * child->min;
+	if (new_min_64 >= RPR_QUANTITY_INF)
+		return pattern;			/* overflow, skip optimization */
 
-		child->min = (int) new_min_64;
-		child->max = (int) new_max_64;
-		return child;
+	if (pattern->max == RPR_QUANTITY_INF || child->max == RPR_QUANTITY_INF)
+		new_max_64 = RPR_QUANTITY_INF;
+	else
+	{
+		new_max_64 = (int64) pattern->max * child->max;
+		if (new_max_64 >= RPR_QUANTITY_INF)
+			return pattern;
 	}
 
-	/* Not safe to multiply */
-	return pattern;
+	child->min = (int) new_min_64;
+	child->max = (int) new_max_64;
+	return child;
 }
 
 /*
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index e9303af6384..1410ba75395 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -3785,8 +3785,8 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
          ->  Seq Scan on rpr_plan
 (7 rows)
 
--- Quantifier NO multiply: (A{2,3}){2,3} stays as (a{2,3}){2,3}
--- outer range, child range - gaps possible (e.g., (A{4,5}){2,3} misses 11)
+-- Quantifier multiply: (A{2,3}){2,3} -> a{4,9}
+-- outer range, child range: counts [4,6] U [6,9] = [4,9] are contiguous, so it folds
 EXPLAIN (COSTS OFF)
 SELECT COUNT(*) OVER w FROM rpr_plan
 WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
@@ -3795,7 +3795,24 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
 -------------------------------------------------------------------------------
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: (a{2,3}){2,3}
+   Pattern: a{4,9}
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 rows)
+
+-- Quantifier NO multiply: (A{4,5}){2,3} stays as (a{4,5}){2,3}
+-- outer range, child range with a gap: [8,10] U [12,15] misses 11
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN ((A{4,5}){2,3}) DEFINE A AS val > 0);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a{4,5}){2,3}
    Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
@@ -3850,6 +3867,125 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
          ->  Seq Scan on rpr_plan
 (7 rows)
 
+-- Quantifier multiply with an unbounded child: an exact outer count (m == n)
+-- always folds regardless of the child's max - (A+){3} -> a{3,}
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN ((A+){3}) DEFINE A AS val > 0);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a{3,}"
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 rows)
+
+-- (A{2,}){3} -> a{6,}  (m == n, unbounded child with min 2)
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN ((A{2,}){3}) DEFINE A AS val > 0);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a{6,}"
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 rows)
+
+-- (A+){2,4} -> a{2,}  (outer range, unbounded child: every interval reaches INF,
+-- so they always touch)
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN ((A+){2,4}) DEFINE A AS val > 0);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a{2,}"
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 rows)
+
+-- (A{2,3}){2,4} -> a{4,12}  (outer range x child range, contiguous:
+-- [4,6] U [6,9] U [8,12] = [4,12])
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN ((A{2,3}){2,4}) DEFINE A AS val > 0);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a{4,12}
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 rows)
+
+-- Skippable outer (min 0) folds only when the zero case connects to the child
+-- range: (A{1,3})? -> a{0,3}  (child min <= 1, so {0} U [1,3] = [0,3] is contiguous)
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN ((A{1,3})?) DEFINE A AS val > 0);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a{0,3}
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 rows)
+
+-- Quantifier NO multiply: (A{2,3})? stays as (a{2,3})?
+-- min 0 with child min >= 2: {0} U [2,3] leaves 1 unreachable (intervals touch but
+-- the zero case does not connect)
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN ((A{2,3})?) DEFINE A AS val > 0);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a{2,3})?
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 rows)
+
+-- Quantifier NO multiply: (A{3,4})? stays as (a{3,4})?
+-- min 0 with child min >= 2: {0} U [3,4] leaves 1,2 unreachable
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN ((A{3,4})?) DEFINE A AS val > 0);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a{3,4})?
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 rows)
+
 -- Unwrap GROUP{1,1}: (A) -> a
 EXPLAIN (COSTS OFF)
 SELECT COUNT(*) OVER w FROM rpr_plan
@@ -6355,12 +6491,14 @@ ERROR:  too many pattern variables
 DETAIL:  Maximum is 240.
 -- Expected: ERROR - too many pattern variables (Maximum is 240)
 -- Test: Pattern nesting at maximum depth (depth 253)
--- Note: 253 nested GROUP{3,7} quantifiers produce depth 253 after optimization
+-- Note: 253 nested GROUP{3,7}? quantifiers; reluctant quantifiers are not
+-- subject to quantifier multiplication, so the nesting (and depth 253) is
+-- preserved after optimization.
 SELECT id, val, COUNT(*) OVER w FROM rpr_errors
 WINDOW w AS (
     ORDER BY id
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    PATTERN ((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((A{3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7})
+    PATTERN ((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((A{3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?)
     DEFINE A AS val > 0
 );
  id | val | count 
@@ -6371,12 +6509,14 @@ WINDOW w AS (
 
 -- Expected: Should succeed
 -- Test: Pattern nesting depth exceeds maximum (depth 254)
--- Note: 254 nested GROUP{3,7} quantifiers produce depth 254 after optimization
+-- Note: 254 nested GROUP{3,7}? quantifiers; reluctant quantifiers are not
+-- subject to quantifier multiplication, so the nesting reaches depth 254 and
+-- exceeds the limit.
 SELECT id, val, COUNT(*) OVER w FROM rpr_errors
 WINDOW w AS (
     ORDER BY id
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    PATTERN (((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((A{3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7})
+    PATTERN (((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((A{3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?)
     DEFINE A AS val > 0
 );
 ERROR:  pattern nesting too deep
diff --git a/src/test/regress/expected/rpr_explain.out b/src/test/regress/expected/rpr_explain.out
index 9ba302b11ae..5cddd1a56df 100644
--- a/src/test/regress/expected/rpr_explain.out
+++ b/src/test/regress/expected/rpr_explain.out
@@ -2178,10 +2178,10 @@ WINDOW w AS (
 ---------------------------------------------------------------------
  WindowAgg (actual rows=3.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: (a?){0,3}
+   Pattern: a{0,3}
    Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
-   NFA States: 6 peak, 20 total, 4 merged
+   NFA States: 3 peak, 8 total, 0 merged
    NFA Contexts: 2 peak, 4 total, 0 pruned
    NFA: 3 matched (len 0/0/0.0), 0 mismatched
    ->  Function Scan on generate_series s (actual rows=3.00 loops=1)
@@ -2217,10 +2217,10 @@ WINDOW w AS (
 ---------------------------------------------------------------------
  WindowAgg (actual rows=3.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: (a?){1,3}
+   Pattern: a{0,3}
    Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
-   NFA States: 5 peak, 16 total, 4 merged
+   NFA States: 3 peak, 8 total, 0 merged
    NFA Contexts: 2 peak, 4 total, 0 pruned
    NFA: 3 matched (len 0/0/0.0), 0 mismatched
    ->  Function Scan on generate_series s (actual rows=3.00 loops=1)
@@ -2256,10 +2256,10 @@ WINDOW w AS (
 ---------------------------------------------------------------------
  WindowAgg (actual rows=3.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: (a?){2,3}
+   Pattern: a{0,3}
    Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
-   NFA States: 5 peak, 16 total, 4 merged
+   NFA States: 3 peak, 8 total, 0 merged
    NFA Contexts: 2 peak, 4 total, 0 pruned
    NFA: 3 matched (len 0/0/0.0), 0 mismatched
    ->  Function Scan on generate_series s (actual rows=3.00 loops=1)
@@ -2295,10 +2295,10 @@ WINDOW w AS (
 ---------------------------------------------------------------------
  WindowAgg (actual rows=4.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: (a?){2,3}
+   Pattern: a{0,3}
    Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
-   NFA States: 8 peak, 26 total, 5 merged
+   NFA States: 6 peak, 13 total, 0 merged
    NFA Contexts: 4 peak, 5 total, 1 pruned
    NFA: 3 matched (len 0/2/1.0), 0 mismatched
    ->  Function Scan on generate_series s (actual rows=4.00 loops=1)
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index 475541d4550..53bf090b903 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -2516,13 +2516,20 @@ SELECT COUNT(*) OVER w FROM rpr_plan
 WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
              PATTERN ((A{2}){3,5}) DEFINE A AS val > 0);
 
--- Quantifier NO multiply: (A{2,3}){2,3} stays as (a{2,3}){2,3}
--- outer range, child range - gaps possible (e.g., (A{4,5}){2,3} misses 11)
+-- Quantifier multiply: (A{2,3}){2,3} -> a{4,9}
+-- outer range, child range: counts [4,6] U [6,9] = [4,9] are contiguous, so it folds
 EXPLAIN (COSTS OFF)
 SELECT COUNT(*) OVER w FROM rpr_plan
 WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
              PATTERN ((A{2,3}){2,3}) DEFINE A AS val > 0);
 
+-- Quantifier NO multiply: (A{4,5}){2,3} stays as (a{4,5}){2,3}
+-- outer range, child range with a gap: [8,10] U [12,15] misses 11
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN ((A{4,5}){2,3}) DEFINE A AS val > 0);
+
 -- Nested unbounded: (A*)* -> a*
 EXPLAIN (COSTS OFF)
 SELECT COUNT(*) OVER w FROM rpr_plan
@@ -2541,6 +2548,55 @@ SELECT COUNT(*) OVER w FROM rpr_plan
 WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
              PATTERN ((A+)+) DEFINE A AS val > 0);
 
+-- Quantifier multiply with an unbounded child: an exact outer count (m == n)
+-- always folds regardless of the child's max - (A+){3} -> a{3,}
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN ((A+){3}) DEFINE A AS val > 0);
+
+-- (A{2,}){3} -> a{6,}  (m == n, unbounded child with min 2)
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN ((A{2,}){3}) DEFINE A AS val > 0);
+
+-- (A+){2,4} -> a{2,}  (outer range, unbounded child: every interval reaches INF,
+-- so they always touch)
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN ((A+){2,4}) DEFINE A AS val > 0);
+
+-- (A{2,3}){2,4} -> a{4,12}  (outer range x child range, contiguous:
+-- [4,6] U [6,9] U [8,12] = [4,12])
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN ((A{2,3}){2,4}) DEFINE A AS val > 0);
+
+-- Skippable outer (min 0) folds only when the zero case connects to the child
+-- range: (A{1,3})? -> a{0,3}  (child min <= 1, so {0} U [1,3] = [0,3] is contiguous)
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN ((A{1,3})?) DEFINE A AS val > 0);
+
+-- Quantifier NO multiply: (A{2,3})? stays as (a{2,3})?
+-- min 0 with child min >= 2: {0} U [2,3] leaves 1 unreachable (intervals touch but
+-- the zero case does not connect)
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN ((A{2,3})?) DEFINE A AS val > 0);
+
+-- Quantifier NO multiply: (A{3,4})? stays as (a{3,4})?
+-- min 0 with child min >= 2: {0} U [3,4] leaves 1,2 unreachable
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN ((A{3,4})?) DEFINE A AS val > 0);
+
 -- Unwrap GROUP{1,1}: (A) -> a
 EXPLAIN (COSTS OFF)
 SELECT COUNT(*) OVER w FROM rpr_plan
@@ -4017,23 +4073,27 @@ WINDOW w AS (
 -- Expected: ERROR - too many pattern variables (Maximum is 240)
 
 -- Test: Pattern nesting at maximum depth (depth 253)
--- Note: 253 nested GROUP{3,7} quantifiers produce depth 253 after optimization
+-- Note: 253 nested GROUP{3,7}? quantifiers; reluctant quantifiers are not
+-- subject to quantifier multiplication, so the nesting (and depth 253) is
+-- preserved after optimization.
 SELECT id, val, COUNT(*) OVER w FROM rpr_errors
 WINDOW w AS (
     ORDER BY id
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    PATTERN ((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((A{3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7})
+    PATTERN ((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((A{3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?)
     DEFINE A AS val > 0
 );
 -- Expected: Should succeed
 
 -- Test: Pattern nesting depth exceeds maximum (depth 254)
--- Note: 254 nested GROUP{3,7} quantifiers produce depth 254 after optimization
+-- Note: 254 nested GROUP{3,7}? quantifiers; reluctant quantifiers are not
+-- subject to quantifier multiplication, so the nesting reaches depth 254 and
+-- exceeds the limit.
 SELECT id, val, COUNT(*) OVER w FROM rpr_errors
 WINDOW w AS (
     ORDER BY id
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    PATTERN (((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((A{3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7})
+    PATTERN (((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((A{3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?)
     DEFINE A AS val > 0
 );
 -- Expected: ERROR - pattern nesting too deep
-- 
2.50.1 (Apple Git-155)


From 1e9d66e961bae1e3884be7c844c7faba252c4bed Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Thu, 4 Jun 2026 20:28:51 +0900
Subject: [PATCH 44/68] Tidy up row pattern recognition pattern compilation

Mark the unreachable tails of the three node-type switches
(rprPatternEqual, optimizeRPRPattern, fillRPRPattern) with
pg_unreachable() instead of a "keep compiler quiet" return.
RPRPatternNodeType has only four members and each switch handles
all of them, so the trailing return can never be reached.

In buildRPRPattern(), the absorption-eligibility test guarded
against a ROWS frame redundantly: RPR is ROWS-only because
transformRPR() rejects RANGE and GROUPS up front, so the
FRAMEOPTION_ROWS term was always true.  Drop the hasLimitedFrame
variable, assert the ROWS-only invariant instead, and test
FRAMEOPTION_END_UNBOUNDED_FOLLOWING directly.

No behavior change.

Patch by Jian He, with minor adjustments.
---
 src/backend/optimizer/plan/rpr.c | 18 ++++++++++--------
 1 file changed, 10 insertions(+), 8 deletions(-)

diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index 617a3869948..3205559c03a 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -132,7 +132,8 @@ rprPatternEqual(RPRPatternNode *a, RPRPatternNode *b)
 			return rprPatternChildrenEqual(a->children, b->children);
 	}
 
-	return false;				/* keep compiler quiet */
+	pg_unreachable();
+	return false;
 }
 
 /*
@@ -993,7 +994,8 @@ optimizeRPRPattern(RPRPatternNode *pattern)
 			return optimizeGroupPattern(pattern);
 	}
 
-	return pattern;				/* keep compiler quiet */
+	pg_unreachable();
+	return pattern;
 }
 
 /*
@@ -1450,7 +1452,8 @@ fillRPRPattern(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth depth)
 			return fillRPRPatternAlt(node, pat, idx, depth);
 	}
 
-	return false;				/* unreachable */
+	pg_unreachable();
+	return false;
 }
 
 /*
@@ -1955,10 +1958,11 @@ buildRPRPattern(RPRPatternNode *pattern, List *defineVariableList,
 	int			numElements;
 	RPRDepth	maxDepth;
 	int			idx;
-	bool		hasLimitedFrame;
 
 	/* Caller must check for NULL pattern before calling */
 	Assert(pattern != NULL);
+	/* RPR is ROWS-only: transformRPR() rejects RANGE/GROUPS up front */
+	Assert(frameOptions & FRAMEOPTION_ROWS);
 
 	/* Optimize the pattern tree */
 	optimized = optimizeRPRPattern(copyObject(pattern));
@@ -1997,10 +2001,8 @@ buildRPRPattern(RPRPatternNode *pattern, List *defineVariableList,
 	 * absorption semantics - older contexts don't necessarily produce longer
 	 * matches when frame limits apply differently to each context.
 	 */
-	hasLimitedFrame = (frameOptions & FRAMEOPTION_ROWS) &&
-		!(frameOptions & FRAMEOPTION_END_UNBOUNDED_FOLLOWING);
-
-	if (rpSkipTo == ST_PAST_LAST_ROW && !hasLimitedFrame &&
+	if (rpSkipTo == ST_PAST_LAST_ROW &&
+		(frameOptions & FRAMEOPTION_END_UNBOUNDED_FOLLOWING) &&
 		!hasMatchStartDependent)
 	{
 		/* Runtime conditions met - check structural absorbability */
-- 
2.50.1 (Apple Git-155)


From 427916e331cd97c6ee68e3e09b3ee1ccfcb14cb3 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Fri, 5 Jun 2026 11:29:33 +0900
Subject: [PATCH 45/68] Drop redundant parentheses from row pattern recognition
 ereports

The row pattern recognition patch added a number of ereport() calls
that still wrap their auxiliary functions in the optional outer
parentheses.  The newer style omits them, and most of the patch --
all of parse_rpr.c, for one -- already does.

Convert the remaining calls to the parenthesis-free style so the
feature is internally consistent: the DEFINE-clause checks in
ParseFuncOrColumn() and transformColumnRef(), the navigation offset
checks in execExprInterp.c, and the navigation placeholder window
functions in windowfuncs.c.  Pre-existing core ereport() calls are
left untouched.

No behavior change.
---
 src/backend/executor/execExprInterp.c | 16 +++++++-------
 src/backend/parser/parse_expr.c       | 16 +++++++-------
 src/backend/parser/parse_func.c       |  8 +++----
 src/backend/utils/adt/windowfuncs.c   | 32 +++++++++++++--------------
 4 files changed, 36 insertions(+), 36 deletions(-)

diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c
index 324b9a962a8..805c8583fb2 100644
--- a/src/backend/executor/execExprInterp.c
+++ b/src/backend/executor/execExprInterp.c
@@ -6026,15 +6026,15 @@ rpr_nav_get_compound_offset(ExprEvalStep *op)
 
 	if (op->d.rpr_nav.offset_isnull[1])
 		ereport(ERROR,
-				(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
-				 errmsg("row pattern navigation offset must not be null")));
+				errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
+				errmsg("row pattern navigation offset must not be null"));
 
 	val = DatumGetInt64(op->d.rpr_nav.offset_value[1]);
 
 	if (val < 0)
 		ereport(ERROR,
-				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-				 errmsg("row pattern navigation offset must not be negative")));
+				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				errmsg("row pattern navigation offset must not be negative"));
 
 	return val;
 }
@@ -6071,15 +6071,15 @@ ExecEvalRPRNavSet(ExprState *state, ExprEvalStep *op, ExprContext *econtext)
 	{
 		if (*op->d.rpr_nav.offset_isnull)
 			ereport(ERROR,
-					(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
-					 errmsg("row pattern navigation offset must not be null")));
+					errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
+					errmsg("row pattern navigation offset must not be null"));
 
 		offset = DatumGetInt64(*op->d.rpr_nav.offset_value);
 
 		if (offset < 0)
 			ereport(ERROR,
-					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-					 errmsg("row pattern navigation offset must not be negative")));
+					errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					errmsg("row pattern navigation offset must not be negative"));
 	}
 	else
 	{
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 78abdc88f86..2344aaef9ae 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -666,17 +666,17 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 
 		if (is_pattern_var)
 			ereport(ERROR,
-					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-					 errmsg("pattern variable qualified expression \"%s\" is not supported in DEFINE clause",
-							NameListToString(cref->fields)),
-					 parser_errposition(pstate, cref->location)));
+					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					errmsg("pattern variable qualified expression \"%s\" is not supported in DEFINE clause",
+						   NameListToString(cref->fields)),
+					parser_errposition(pstate, cref->location));
 		else if (refnameNamespaceItem(pstate, NULL, qualifier,
 									  cref->location, NULL) != NULL)
 			ereport(ERROR,
-					(errcode(ERRCODE_SYNTAX_ERROR),
-					 errmsg("range variable qualified expression \"%s\" is not allowed in DEFINE clause",
-							NameListToString(cref->fields)),
-					 parser_errposition(pstate, cref->location)));
+					errcode(ERRCODE_SYNTAX_ERROR),
+					errmsg("range variable qualified expression \"%s\" is not allowed in DEFINE clause",
+						   NameListToString(cref->fields)),
+					parser_errposition(pstate, cref->location));
 		/* else: unknown qualifier -- fall through to normal resolution */
 	}
 
diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c
index 49646b728c6..8cae9ba52b6 100644
--- a/src/backend/parser/parse_func.c
+++ b/src/backend/parser/parse_func.c
@@ -789,10 +789,10 @@ ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs,
 
 		/* Not a column projection -- report error */
 		ereport(ERROR,
-				(errcode(ERRCODE_SYNTAX_ERROR),
-				 errmsg("cannot use %s outside a DEFINE clause",
-						NameListToString(funcname)),
-				 parser_errposition(pstate, location)));
+				errcode(ERRCODE_SYNTAX_ERROR),
+				errmsg("cannot use %s outside a DEFINE clause",
+					   NameListToString(funcname)),
+				parser_errposition(pstate, location));
 	}
 
 	/* build the appropriate output structure */
diff --git a/src/backend/utils/adt/windowfuncs.c b/src/backend/utils/adt/windowfuncs.c
index 46e7a03a666..3869f6c8994 100644
--- a/src/backend/utils/adt/windowfuncs.c
+++ b/src/backend/utils/adt/windowfuncs.c
@@ -739,8 +739,8 @@ Datum
 window_prev(PG_FUNCTION_ARGS)
 {
 	ereport(ERROR,
-			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-			 errmsg("cannot use PREV() outside a DEFINE clause")));
+			errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			errmsg("cannot use PREV() outside a DEFINE clause"));
 	PG_RETURN_NULL();			/* not reached */
 }
 
@@ -753,8 +753,8 @@ Datum
 window_next(PG_FUNCTION_ARGS)
 {
 	ereport(ERROR,
-			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-			 errmsg("cannot use NEXT() outside a DEFINE clause")));
+			errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			errmsg("cannot use NEXT() outside a DEFINE clause"));
 	PG_RETURN_NULL();			/* not reached */
 }
 
@@ -767,8 +767,8 @@ Datum
 window_prev_offset(PG_FUNCTION_ARGS)
 {
 	ereport(ERROR,
-			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-			 errmsg("cannot use PREV() outside a DEFINE clause")));
+			errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			errmsg("cannot use PREV() outside a DEFINE clause"));
 	PG_RETURN_NULL();			/* not reached */
 }
 
@@ -781,8 +781,8 @@ Datum
 window_next_offset(PG_FUNCTION_ARGS)
 {
 	ereport(ERROR,
-			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-			 errmsg("cannot use NEXT() outside a DEFINE clause")));
+			errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			errmsg("cannot use NEXT() outside a DEFINE clause"));
 	PG_RETURN_NULL();			/* not reached */
 }
 
@@ -795,8 +795,8 @@ Datum
 window_first(PG_FUNCTION_ARGS)
 {
 	ereport(ERROR,
-			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-			 errmsg("cannot use FIRST() outside a DEFINE clause")));
+			errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			errmsg("cannot use FIRST() outside a DEFINE clause"));
 	PG_RETURN_NULL();			/* not reached */
 }
 
@@ -809,8 +809,8 @@ Datum
 window_last(PG_FUNCTION_ARGS)
 {
 	ereport(ERROR,
-			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-			 errmsg("cannot use LAST() outside a DEFINE clause")));
+			errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			errmsg("cannot use LAST() outside a DEFINE clause"));
 	PG_RETURN_NULL();			/* not reached */
 }
 
@@ -823,8 +823,8 @@ Datum
 window_first_offset(PG_FUNCTION_ARGS)
 {
 	ereport(ERROR,
-			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-			 errmsg("cannot use FIRST() outside a DEFINE clause")));
+			errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			errmsg("cannot use FIRST() outside a DEFINE clause"));
 	PG_RETURN_NULL();			/* not reached */
 }
 
@@ -837,7 +837,7 @@ Datum
 window_last_offset(PG_FUNCTION_ARGS)
 {
 	ereport(ERROR,
-			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-			 errmsg("cannot use LAST() outside a DEFINE clause")));
+			errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			errmsg("cannot use LAST() outside a DEFINE clause"));
 	PG_RETURN_NULL();			/* not reached */
 }
-- 
2.50.1 (Apple Git-155)


From 178fe9979b1a5c18e06af67a696e12f0ee9da107 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Fri, 5 Jun 2026 12:32:38 +0900
Subject: [PATCH 46/68] Tidy up forward declarations and helper placement for
 row pattern recognition

Bring the row pattern recognition code in line with the surrounding
conventions for static helpers:

- each static helper has a forward declaration;
- static helpers are defined above the "API exposed" banner, public
  entry points below;
- the forward-declaration block uses a single banner, not per-group
  labels;
- forward declarations follow definition order.

Pure code movement and comments, no behavior change.

Per review comments from Tatsuo Ishii and Jian He.
---
 src/backend/executor/execRPR.c       | 110 +++----
 src/backend/executor/nodeWindowAgg.c | 454 +++++++++++++--------------
 src/backend/optimizer/plan/rpr.c     |  12 +-
 src/backend/parser/parse_rpr.c       |   2 +-
 4 files changed, 289 insertions(+), 289 deletions(-)

diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 580b25f398d..56399c0c7fd 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -83,8 +83,6 @@ static bool nfa_eval_var_match(WindowAggState *winstate,
 							   RPRPatternElement *elem, bool *varMatched);
 static void nfa_match(WindowAggState *winstate, RPRNFAContext *ctx,
 					  bool *varMatched);
-static void nfa_advance_state(WindowAggState *winstate, RPRNFAContext *ctx,
-							  RPRNFAState *state, int64 currentPos);
 static void nfa_route_to_elem(WindowAggState *winstate, RPRNFAContext *ctx,
 							  RPRNFAState *state, RPRPatternElement *nextElem,
 							  int64 currentPos);
@@ -100,9 +98,15 @@ static void nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
 static void nfa_advance_var(WindowAggState *winstate, RPRNFAContext *ctx,
 							RPRNFAState *state, RPRPatternElement *elem,
 							int64 currentPos);
+static void nfa_advance_state(WindowAggState *winstate, RPRNFAContext *ctx,
+							  RPRNFAState *state, int64 currentPos);
 static void nfa_advance(WindowAggState *winstate, RPRNFAContext *ctx,
 						int64 currentPos);
 
+static void nfa_reevaluate_dependent_vars(WindowAggState *winstate,
+										  RPRNFAContext *ctx,
+										  int64 currentPos);
+
 /*
  * NFA-based pattern matching implementation
  *
@@ -1552,6 +1556,57 @@ nfa_advance(WindowAggState *winstate, RPRNFAContext *ctx, int64 currentPos)
 	}
 }
 
+/*
+ * nfa_reevaluate_dependent_vars
+ *		Re-evaluate match_start-dependent DEFINE variables for a specific
+ *		context whose matchStartRow differs from the shared evaluation's
+ *		nav_match_start.
+ *
+ * Only variables in defineMatchStartDependent are re-evaluated.  The
+ * current row's slot (ecxt_outertuple) must already be set up by
+ * nfa_evaluate_row().
+ */
+static void
+nfa_reevaluate_dependent_vars(WindowAggState *winstate, RPRNFAContext *ctx,
+							  int64 currentPos)
+{
+	ExprContext *econtext = winstate->ss.ps.ps_ExprContext;
+	int64		saved_match_start = winstate->nav_match_start;
+	int64		saved_pos = winstate->currentpos;
+	int			varIdx = 0;
+	ListCell   *lc;
+
+	/* Temporarily set nav_match_start and currentpos for FIRST/LAST */
+	winstate->nav_match_start = ctx->matchStartRow;
+	winstate->currentpos = currentPos;
+
+	/* Invalidate nav_slot cache since match_start changed */
+	winstate->nav_slot_pos = -1;
+
+	foreach(lc, winstate->defineClauseList)
+	{
+		if (bms_is_member(varIdx, winstate->defineMatchStartDependent))
+		{
+			ExprState  *exprState = (ExprState *) lfirst(lc);
+			Datum		result;
+			bool		isnull;
+
+			/* Per-tuple context; scratch freed by the per-row reset */
+			result = ExecEvalExprSwitchContext(exprState, econtext, &isnull);
+			winstate->nfaVarMatched[varIdx] = (!isnull && DatumGetBool(result));
+		}
+
+		varIdx++;
+		if (varIdx >= list_length(winstate->defineVariableList))
+			break;
+	}
+
+	/* Restore original match_start, currentpos, and invalidate cache */
+	winstate->nav_match_start = saved_match_start;
+	winstate->currentpos = saved_pos;
+	winstate->nav_slot_pos = -1;
+}
+
 
 /***********************************************************************
  * API exposed to nodeWindowAgg.c
@@ -1696,57 +1751,6 @@ ExecRPRRecordContextFailure(WindowAggState *winstate, int64 failedLen)
 	}
 }
 
-/*
- * nfa_reevaluate_dependent_vars
- *		Re-evaluate match_start-dependent DEFINE variables for a specific
- *		context whose matchStartRow differs from the shared evaluation's
- *		nav_match_start.
- *
- * Only variables in defineMatchStartDependent are re-evaluated.  The
- * current row's slot (ecxt_outertuple) must already be set up by
- * nfa_evaluate_row().
- */
-static void
-nfa_reevaluate_dependent_vars(WindowAggState *winstate, RPRNFAContext *ctx,
-							  int64 currentPos)
-{
-	ExprContext *econtext = winstate->ss.ps.ps_ExprContext;
-	int64		saved_match_start = winstate->nav_match_start;
-	int64		saved_pos = winstate->currentpos;
-	int			varIdx = 0;
-	ListCell   *lc;
-
-	/* Temporarily set nav_match_start and currentpos for FIRST/LAST */
-	winstate->nav_match_start = ctx->matchStartRow;
-	winstate->currentpos = currentPos;
-
-	/* Invalidate nav_slot cache since match_start changed */
-	winstate->nav_slot_pos = -1;
-
-	foreach(lc, winstate->defineClauseList)
-	{
-		if (bms_is_member(varIdx, winstate->defineMatchStartDependent))
-		{
-			ExprState  *exprState = (ExprState *) lfirst(lc);
-			Datum		result;
-			bool		isnull;
-
-			/* Per-tuple context; scratch freed by the per-row reset */
-			result = ExecEvalExprSwitchContext(exprState, econtext, &isnull);
-			winstate->nfaVarMatched[varIdx] = (!isnull && DatumGetBool(result));
-		}
-
-		varIdx++;
-		if (varIdx >= list_length(winstate->defineVariableList))
-			break;
-	}
-
-	/* Restore original match_start, currentpos, and invalidate cache */
-	winstate->nav_match_start = saved_match_start;
-	winstate->currentpos = saved_pos;
-	winstate->nav_slot_pos = -1;
-}
-
 /*
  * ExecRPRProcessRow
  *
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 408bbc120b7..86b39bf7f61 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -4627,6 +4627,233 @@ nfa_evaluate_row(WindowObject winobj, int64 pos, bool *varMatched)
 	return true;				/* Row exists */
 }
 
+/*
+ * WinGetSlotInFrame
+ * slot: TupleTableSlot to store the result
+ * relpos: signed rowcount offset from the seek position
+ * seektype: WINDOW_SEEK_HEAD or WINDOW_SEEK_TAIL
+ * set_mark: If the row is found/in frame and set_mark is true, the mark is
+ *		moved to the row as a side-effect.
+ * isnull: output argument, receives isnull status of result
+ * isout: output argument, set to indicate whether target row position
+ *		is out of frame (can pass NULL if caller doesn't care about this)
+ *
+ * Returns 0 if we successfully got the slot, or nonzero if out of frame.
+ * (isout is also set in the latter case.)
+ */
+static int
+WinGetSlotInFrame(WindowObject winobj, TupleTableSlot *slot,
+				  int relpos, int seektype, bool set_mark,
+				  bool *isnull, bool *isout)
+{
+	WindowAggState *winstate;
+	int64		abs_pos;
+	int64		mark_pos;
+	int64		num_reduced_frame;
+
+	Assert(WindowObjectIsValid(winobj));
+	winstate = winobj->winstate;
+
+	switch (seektype)
+	{
+		case WINDOW_SEEK_CURRENT:
+			elog(ERROR, "WINDOW_SEEK_CURRENT is not supported for WinGetFuncArgInFrame");
+			abs_pos = mark_pos = 0; /* keep compiler quiet */
+			break;
+		case WINDOW_SEEK_HEAD:
+			/* rejecting relpos < 0 is easy and simplifies code below */
+			if (relpos < 0)
+				goto out_of_frame;
+			update_frameheadpos(winstate);
+			abs_pos = winstate->frameheadpos + relpos;
+			mark_pos = abs_pos;
+
+			/*
+			 * Account for exclusion option if one is active, but advance only
+			 * abs_pos not mark_pos.  This prevents changes of the current
+			 * row's peer group from resulting in trying to fetch a row before
+			 * some previous mark position.
+			 *
+			 * Note that in some corner cases such as current row being
+			 * outside frame, these calculations are theoretically too simple,
+			 * but it doesn't matter because we'll end up deciding the row is
+			 * out of frame.  We do not attempt to avoid fetching rows past
+			 * end of frame; that would happen in some cases anyway.
+			 */
+			switch (winstate->frameOptions & FRAMEOPTION_EXCLUSION)
+			{
+				case 0:
+					/* no adjustment needed */
+					break;
+				case FRAMEOPTION_EXCLUDE_CURRENT_ROW:
+					if (abs_pos >= winstate->currentpos &&
+						winstate->currentpos >= winstate->frameheadpos)
+						abs_pos++;
+					break;
+				case FRAMEOPTION_EXCLUDE_GROUP:
+					update_grouptailpos(winstate);
+					if (abs_pos >= winstate->groupheadpos &&
+						winstate->grouptailpos > winstate->frameheadpos)
+					{
+						int64		overlapstart = Max(winstate->groupheadpos,
+													   winstate->frameheadpos);
+
+						abs_pos += winstate->grouptailpos - overlapstart;
+					}
+					break;
+				case FRAMEOPTION_EXCLUDE_TIES:
+					update_grouptailpos(winstate);
+					if (abs_pos >= winstate->groupheadpos &&
+						winstate->grouptailpos > winstate->frameheadpos)
+					{
+						int64		overlapstart = Max(winstate->groupheadpos,
+													   winstate->frameheadpos);
+
+						if (abs_pos == overlapstart)
+							abs_pos = winstate->currentpos;
+						else
+							abs_pos += winstate->grouptailpos - overlapstart - 1;
+					}
+					break;
+				default:
+					elog(ERROR, "unrecognized frame option state: 0x%x",
+						 winstate->frameOptions);
+					break;
+			}
+			num_reduced_frame = row_is_in_reduced_frame(winobj,
+														winstate->frameheadpos);
+			if (num_reduced_frame < 0)
+				goto out_of_frame;
+			else if (num_reduced_frame > 0)
+				if (relpos >= num_reduced_frame)
+					goto out_of_frame;
+			break;
+		case WINDOW_SEEK_TAIL:
+			/* rejecting relpos > 0 is easy and simplifies code below */
+			if (relpos > 0)
+				goto out_of_frame;
+
+			/*
+			 * RPR cares about frame head pos. Need to call
+			 * update_frameheadpos
+			 */
+			update_frameheadpos(winstate);
+
+			update_frametailpos(winstate);
+			abs_pos = winstate->frametailpos - 1 + relpos;
+
+			/*
+			 * Account for exclusion option if one is active.  If there is no
+			 * exclusion, we can safely set the mark at the accessed row.  But
+			 * if there is, we can only mark the frame start, because we can't
+			 * be sure how far back in the frame the exclusion might cause us
+			 * to fetch in future.  Furthermore, we have to actually check
+			 * against frameheadpos here, since it's unsafe to try to fetch a
+			 * row before frame start if the mark might be there already.
+			 */
+			switch (winstate->frameOptions & FRAMEOPTION_EXCLUSION)
+			{
+				case 0:
+					/* no adjustment needed */
+					mark_pos = abs_pos;
+					break;
+				case FRAMEOPTION_EXCLUDE_CURRENT_ROW:
+					if (abs_pos <= winstate->currentpos &&
+						winstate->currentpos < winstate->frametailpos)
+						abs_pos--;
+					update_frameheadpos(winstate);
+					if (abs_pos < winstate->frameheadpos)
+						goto out_of_frame;
+					mark_pos = winstate->frameheadpos;
+					break;
+				case FRAMEOPTION_EXCLUDE_GROUP:
+					update_grouptailpos(winstate);
+					if (abs_pos < winstate->grouptailpos &&
+						winstate->groupheadpos < winstate->frametailpos)
+					{
+						int64		overlapend = Min(winstate->grouptailpos,
+													 winstate->frametailpos);
+
+						abs_pos -= overlapend - winstate->groupheadpos;
+					}
+					update_frameheadpos(winstate);
+					if (abs_pos < winstate->frameheadpos)
+						goto out_of_frame;
+					mark_pos = winstate->frameheadpos;
+					break;
+				case FRAMEOPTION_EXCLUDE_TIES:
+					update_grouptailpos(winstate);
+					if (abs_pos < winstate->grouptailpos &&
+						winstate->groupheadpos < winstate->frametailpos)
+					{
+						int64		overlapend = Min(winstate->grouptailpos,
+													 winstate->frametailpos);
+
+						if (abs_pos == overlapend - 1)
+							abs_pos = winstate->currentpos;
+						else
+							abs_pos -= overlapend - 1 - winstate->groupheadpos;
+					}
+					update_frameheadpos(winstate);
+					if (abs_pos < winstate->frameheadpos)
+						goto out_of_frame;
+					mark_pos = winstate->frameheadpos;
+					break;
+				default:
+					elog(ERROR, "unrecognized frame option state: 0x%x",
+						 winstate->frameOptions);
+					mark_pos = 0;	/* keep compiler quiet */
+					break;
+			}
+
+			num_reduced_frame = row_is_in_reduced_frame(winobj,
+														winstate->frameheadpos);
+			if (num_reduced_frame < 0)
+				goto out_of_frame;
+			else if (num_reduced_frame > 0)
+			{
+				if (-relpos >= num_reduced_frame)
+					goto out_of_frame;
+				abs_pos = winstate->frameheadpos + relpos +
+					num_reduced_frame - 1;
+			}
+			break;
+		default:
+			elog(ERROR, "unrecognized window seek type: %d", seektype);
+			abs_pos = mark_pos = 0; /* keep compiler quiet */
+			break;
+	}
+
+	if (!window_gettupleslot(winobj, abs_pos, slot))
+		goto out_of_frame;
+
+	/* The code above does not detect all out-of-frame cases, so check */
+	if (row_is_in_frame(winobj, abs_pos, slot, false) <= 0)
+		goto out_of_frame;
+
+	if (isout)
+		*isout = false;
+	if (set_mark)
+	{
+		/*
+		 * If RPR is enabled and seek type is WINDOW_SEEK_TAIL, we set the
+		 * mark position unconditionally to frameheadpos. In this case the
+		 * frame always starts at CURRENT_ROW and never goes back, thus
+		 * setting the mark at the position is safe.
+		 */
+		if (winstate->rpPattern != NULL && seektype == WINDOW_SEEK_TAIL)
+			mark_pos = winstate->frameheadpos;
+		WinSetMarkPosition(winobj, mark_pos);
+	}
+	return 0;
+
+out_of_frame:
+	if (isout)
+		*isout = true;
+	*isnull = true;
+	return -1;
+}
+
 
 /***********************************************************************
  * API exposed to window functions
@@ -5019,233 +5246,6 @@ WinGetFuncArgInFrame(WindowObject winobj, int argno,
 	return (Datum) 0;
 }
 
-/*
- * WinGetSlotInFrame
- * slot: TupleTableSlot to store the result
- * relpos: signed rowcount offset from the seek position
- * seektype: WINDOW_SEEK_HEAD or WINDOW_SEEK_TAIL
- * set_mark: If the row is found/in frame and set_mark is true, the mark is
- *		moved to the row as a side-effect.
- * isnull: output argument, receives isnull status of result
- * isout: output argument, set to indicate whether target row position
- *		is out of frame (can pass NULL if caller doesn't care about this)
- *
- * Returns 0 if we successfully got the slot, or nonzero if out of frame.
- * (isout is also set in the latter case.)
- */
-static int
-WinGetSlotInFrame(WindowObject winobj, TupleTableSlot *slot,
-				  int relpos, int seektype, bool set_mark,
-				  bool *isnull, bool *isout)
-{
-	WindowAggState *winstate;
-	int64		abs_pos;
-	int64		mark_pos;
-	int64		num_reduced_frame;
-
-	Assert(WindowObjectIsValid(winobj));
-	winstate = winobj->winstate;
-
-	switch (seektype)
-	{
-		case WINDOW_SEEK_CURRENT:
-			elog(ERROR, "WINDOW_SEEK_CURRENT is not supported for WinGetFuncArgInFrame");
-			abs_pos = mark_pos = 0; /* keep compiler quiet */
-			break;
-		case WINDOW_SEEK_HEAD:
-			/* rejecting relpos < 0 is easy and simplifies code below */
-			if (relpos < 0)
-				goto out_of_frame;
-			update_frameheadpos(winstate);
-			abs_pos = winstate->frameheadpos + relpos;
-			mark_pos = abs_pos;
-
-			/*
-			 * Account for exclusion option if one is active, but advance only
-			 * abs_pos not mark_pos.  This prevents changes of the current
-			 * row's peer group from resulting in trying to fetch a row before
-			 * some previous mark position.
-			 *
-			 * Note that in some corner cases such as current row being
-			 * outside frame, these calculations are theoretically too simple,
-			 * but it doesn't matter because we'll end up deciding the row is
-			 * out of frame.  We do not attempt to avoid fetching rows past
-			 * end of frame; that would happen in some cases anyway.
-			 */
-			switch (winstate->frameOptions & FRAMEOPTION_EXCLUSION)
-			{
-				case 0:
-					/* no adjustment needed */
-					break;
-				case FRAMEOPTION_EXCLUDE_CURRENT_ROW:
-					if (abs_pos >= winstate->currentpos &&
-						winstate->currentpos >= winstate->frameheadpos)
-						abs_pos++;
-					break;
-				case FRAMEOPTION_EXCLUDE_GROUP:
-					update_grouptailpos(winstate);
-					if (abs_pos >= winstate->groupheadpos &&
-						winstate->grouptailpos > winstate->frameheadpos)
-					{
-						int64		overlapstart = Max(winstate->groupheadpos,
-													   winstate->frameheadpos);
-
-						abs_pos += winstate->grouptailpos - overlapstart;
-					}
-					break;
-				case FRAMEOPTION_EXCLUDE_TIES:
-					update_grouptailpos(winstate);
-					if (abs_pos >= winstate->groupheadpos &&
-						winstate->grouptailpos > winstate->frameheadpos)
-					{
-						int64		overlapstart = Max(winstate->groupheadpos,
-													   winstate->frameheadpos);
-
-						if (abs_pos == overlapstart)
-							abs_pos = winstate->currentpos;
-						else
-							abs_pos += winstate->grouptailpos - overlapstart - 1;
-					}
-					break;
-				default:
-					elog(ERROR, "unrecognized frame option state: 0x%x",
-						 winstate->frameOptions);
-					break;
-			}
-			num_reduced_frame = row_is_in_reduced_frame(winobj,
-														winstate->frameheadpos);
-			if (num_reduced_frame < 0)
-				goto out_of_frame;
-			else if (num_reduced_frame > 0)
-				if (relpos >= num_reduced_frame)
-					goto out_of_frame;
-			break;
-		case WINDOW_SEEK_TAIL:
-			/* rejecting relpos > 0 is easy and simplifies code below */
-			if (relpos > 0)
-				goto out_of_frame;
-
-			/*
-			 * RPR cares about frame head pos. Need to call
-			 * update_frameheadpos
-			 */
-			update_frameheadpos(winstate);
-
-			update_frametailpos(winstate);
-			abs_pos = winstate->frametailpos - 1 + relpos;
-
-			/*
-			 * Account for exclusion option if one is active.  If there is no
-			 * exclusion, we can safely set the mark at the accessed row.  But
-			 * if there is, we can only mark the frame start, because we can't
-			 * be sure how far back in the frame the exclusion might cause us
-			 * to fetch in future.  Furthermore, we have to actually check
-			 * against frameheadpos here, since it's unsafe to try to fetch a
-			 * row before frame start if the mark might be there already.
-			 */
-			switch (winstate->frameOptions & FRAMEOPTION_EXCLUSION)
-			{
-				case 0:
-					/* no adjustment needed */
-					mark_pos = abs_pos;
-					break;
-				case FRAMEOPTION_EXCLUDE_CURRENT_ROW:
-					if (abs_pos <= winstate->currentpos &&
-						winstate->currentpos < winstate->frametailpos)
-						abs_pos--;
-					update_frameheadpos(winstate);
-					if (abs_pos < winstate->frameheadpos)
-						goto out_of_frame;
-					mark_pos = winstate->frameheadpos;
-					break;
-				case FRAMEOPTION_EXCLUDE_GROUP:
-					update_grouptailpos(winstate);
-					if (abs_pos < winstate->grouptailpos &&
-						winstate->groupheadpos < winstate->frametailpos)
-					{
-						int64		overlapend = Min(winstate->grouptailpos,
-													 winstate->frametailpos);
-
-						abs_pos -= overlapend - winstate->groupheadpos;
-					}
-					update_frameheadpos(winstate);
-					if (abs_pos < winstate->frameheadpos)
-						goto out_of_frame;
-					mark_pos = winstate->frameheadpos;
-					break;
-				case FRAMEOPTION_EXCLUDE_TIES:
-					update_grouptailpos(winstate);
-					if (abs_pos < winstate->grouptailpos &&
-						winstate->groupheadpos < winstate->frametailpos)
-					{
-						int64		overlapend = Min(winstate->grouptailpos,
-													 winstate->frametailpos);
-
-						if (abs_pos == overlapend - 1)
-							abs_pos = winstate->currentpos;
-						else
-							abs_pos -= overlapend - 1 - winstate->groupheadpos;
-					}
-					update_frameheadpos(winstate);
-					if (abs_pos < winstate->frameheadpos)
-						goto out_of_frame;
-					mark_pos = winstate->frameheadpos;
-					break;
-				default:
-					elog(ERROR, "unrecognized frame option state: 0x%x",
-						 winstate->frameOptions);
-					mark_pos = 0;	/* keep compiler quiet */
-					break;
-			}
-
-			num_reduced_frame = row_is_in_reduced_frame(winobj,
-														winstate->frameheadpos);
-			if (num_reduced_frame < 0)
-				goto out_of_frame;
-			else if (num_reduced_frame > 0)
-			{
-				if (-relpos >= num_reduced_frame)
-					goto out_of_frame;
-				abs_pos = winstate->frameheadpos + relpos +
-					num_reduced_frame - 1;
-			}
-			break;
-		default:
-			elog(ERROR, "unrecognized window seek type: %d", seektype);
-			abs_pos = mark_pos = 0; /* keep compiler quiet */
-			break;
-	}
-
-	if (!window_gettupleslot(winobj, abs_pos, slot))
-		goto out_of_frame;
-
-	/* The code above does not detect all out-of-frame cases, so check */
-	if (row_is_in_frame(winobj, abs_pos, slot, false) <= 0)
-		goto out_of_frame;
-
-	if (isout)
-		*isout = false;
-	if (set_mark)
-	{
-		/*
-		 * If RPR is enabled and seek type is WINDOW_SEEK_TAIL, we set the
-		 * mark position unconditionally to frameheadpos. In this case the
-		 * frame always starts at CURRENT_ROW and never goes back, thus
-		 * setting the mark at the position is safe.
-		 */
-		if (winstate->rpPattern != NULL && seektype == WINDOW_SEEK_TAIL)
-			mark_pos = winstate->frameheadpos;
-		WinSetMarkPosition(winobj, mark_pos);
-	}
-	return 0;
-
-out_of_frame:
-	if (isout)
-		*isout = true;
-	*isnull = true;
-	return -1;
-}
-
 /*
  * WinGetFuncArgCurrent
  *		Evaluate a window function's argument expression on the current row.
diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index 3205559c03a..43114088c3f 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -44,14 +44,12 @@
 #include "nodes/nodeFuncs.h"
 #include "optimizer/rpr.h"
 
-/* Forward declarations - pattern comparison */
+/* Forward declarations */
 static bool rprPatternEqual(RPRPatternNode *a, RPRPatternNode *b);
 static bool rprPatternChildrenEqual(List *a, List *b);
 
-/* Forward declarations - pattern optimization (shared) */
 static RPRPatternNode *tryUnwrapSingleChild(RPRPatternNode *pattern);
 
-/* Forward declarations - SEQ optimization */
 static List *flattenSeqChildren(List *children);
 static List *mergeConsecutiveVars(List *children);
 static List *mergeConsecutiveGroups(List *children);
@@ -59,20 +57,16 @@ static List *mergeConsecutiveAlts(List *children);
 static List *mergeGroupPrefixSuffix(List *children);
 static RPRPatternNode *optimizeSeqPattern(RPRPatternNode *pattern);
 
-/* Forward declarations - ALT optimization */
 static List *flattenAltChildren(List *children);
 static List *removeDuplicateAlternatives(List *children);
 static RPRPatternNode *optimizeAltPattern(RPRPatternNode *pattern);
 
-/* Forward declarations - GROUP optimization */
 static RPRPatternNode *tryMultiplyQuantifiers(RPRPatternNode *pattern);
 static RPRPatternNode *tryUnwrapGroup(RPRPatternNode *pattern);
 static RPRPatternNode *optimizeGroupPattern(RPRPatternNode *pattern);
 
-/* Forward declarations - optimization dispatcher */
 static RPRPatternNode *optimizeRPRPattern(RPRPatternNode *pattern);
 
-/* Forward declarations - pattern compilation */
 static int	collectDefineVariables(List *defineVariableList, char **varNames);
 static void scanRPRPatternRecursive(RPRPatternNode *node, char **varNames,
 									int *numVars, int *numElements,
@@ -92,7 +86,6 @@ static bool fillRPRPattern(RPRPatternNode *node, RPRPattern *pat,
 						   int *idx, RPRDepth depth);
 static void finalizeRPRPattern(RPRPattern *result);
 
-/* Forward declarations - context absorption */
 static bool isFixedLengthChildren(RPRPattern *pattern, RPRElemIdx idx,
 								  RPRDepth scopeDepth);
 static bool isUnboundedStart(RPRPattern *pattern, RPRElemIdx idx);
@@ -101,6 +94,9 @@ static void computeAbsorbabilityRecursive(RPRPattern *pattern,
 										  bool *hasAbsorbable);
 static void computeAbsorbability(RPRPattern *pattern);
 
+static void collectPatternVariablesRecursive(RPRPatternNode *node,
+											 List **varNames);
+
 /*
  * rprPatternEqual
  *		Compare two RPRPatternNode trees for equality.
diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index 3872035110a..c9469b56b7b 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -61,8 +61,8 @@ static void validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
 									   List *rpDefs, List **varNames);
 static List *transformDefineClause(ParseState *pstate, WindowClause *wc,
 								   WindowDef *windef, List **targetlist);
-static bool define_walker(Node *node, void *context);
 static bool nav_volatile_func_checker(Oid funcid, void *context);
+static bool define_walker(Node *node, void *context);
 
 /*
  * transformRPR
-- 
2.50.1 (Apple Git-155)


From 04f18c586669af22eef26eab7f7cb383f0ccf6d4 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Fri, 5 Jun 2026 12:45:09 +0900
Subject: [PATCH 47/68] Update the varId documentation for row pattern
 recognition

Reserving the whole high varId nibble (0xF0-0xFF) for control elements
left 240 pattern variables (0x00-0xEF), but the documentation still
described the old 0-250 / 252-255 layout.

Update README.rpr's element table and control-code list to the hex
nibble boundaries, note the 0xF0-0xFB spare range, and correct the
user-facing limit in select.sgml from 251 to 240 unique pattern
variables.

Documentation only.
---
 doc/src/sgml/ref/select.sgml    |  2 +-
 src/backend/executor/README.rpr | 15 ++++++++++-----
 2 files changed, 11 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/ref/select.sgml b/doc/src/sgml/ref/select.sgml
index e4708331439..be5ed814853 100644
--- a/doc/src/sgml/ref/select.sgml
+++ b/doc/src/sgml/ref/select.sgml
@@ -1198,7 +1198,7 @@ DEFINE <replaceable class="parameter">definition_variable_name</replaceable> AS
 
    <para>
     Note that the maximum number of unique pattern variables
-    used in the <literal>PATTERN</literal> clause is 251.
+    used in the <literal>PATTERN</literal> clause is 240.
     If this limit is exceeded, an error will be raised.
     Additionally, the maximum nesting depth of pattern groups
     (parentheses) is 253 levels.
diff --git a/src/backend/executor/README.rpr b/src/backend/executor/README.rpr
index 3a215f2566b..55f899f7fef 100644
--- a/src/backend/executor/README.rpr
+++ b/src/backend/executor/README.rpr
@@ -246,7 +246,7 @@ RPRPatternElement struct (16 bytes):
 
   Field      Size     Description
   ---------------------------------------------------------
-  varId      1B      Variable ID (0-250) or control code (252-255)
+  varId      1B      Variable ID (0-0xEF) or control code (0xFC-0xFF)
   depth      1B      Group nesting depth
   flags      1B      Bit flags (see below)
   reserved   1B      Padding
@@ -255,12 +255,17 @@ RPRPatternElement struct (16 bytes):
   next       2B      Next element index (sequential flow)
   jump       2B      Branch target index (for ALT/GROUP)
 
+Pattern variables occupy varId 0 to RPR_VARID_MAX (0xEF) inclusive,
+giving 240 distinct variables.  Any varId with the high nibble set
+(0xF0-0xFF) is reserved for control elements; 0xF0-0xFB are currently
+spare.
+
 Control codes:
 
-  RPR_VARID_BEGIN (252)  Group start marker
-  RPR_VARID_END   (253)  Group end marker
-  RPR_VARID_ALT   (254)  Alternation start marker
-  RPR_VARID_FIN   (255)  Pattern completion marker
+  RPR_VARID_BEGIN (0xFC)  Group start marker
+  RPR_VARID_END   (0xFD)  Group end marker
+  RPR_VARID_ALT   (0xFE)  Alternation start marker
+  RPR_VARID_FIN   (0xFF)  Pattern completion marker
 
 Element flags (1 byte, bitmask):
 
-- 
2.50.1 (Apple Git-155)


From c2539388101eaf8c9cc5fa198a3b90d7e9346889 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Fri, 5 Jun 2026 13:07:22 +0900
Subject: [PATCH 48/68] Reformat the design-decisions chapter in the row
 pattern recognition README

Chapter XII recorded each key design decision as a "Choice:" line plus a
bulleted "Rationale:" list.  Rewrite XII-1 through XII-4 as prose instead
-- a short paragraph stating the decision, followed by a paragraph for
its rationale -- to match the rest of the file.  No rationale is dropped.

XII-4 also picks up the detail, noted by Jian He, that the state and
context structures live in a partition-lifespan memory context that is
freed in release_partition.

Documentation only.
---
 src/backend/executor/README.rpr | 54 ++++++++++++++++-----------------
 1 file changed, 26 insertions(+), 28 deletions(-)

diff --git a/src/backend/executor/README.rpr b/src/backend/executor/README.rpr
index 55f899f7fef..df574a0a6f4 100644
--- a/src/backend/executor/README.rpr
+++ b/src/backend/executor/README.rpr
@@ -1320,47 +1320,45 @@ Chapter XII  Summary of Key Design Decisions
 
 XII-1. Flat Array vs Tree-Based NFA
 
-  Choice: Flat array (RPRPatternElement[])
+  The compiled pattern is stored as a flat array of fixed-size 16-byte
+  RPRPatternElement structs rather than as a tree.
 
-  Rationale:
-  - Cache-friendly: 16-byte fixed size, contiguous memory
-  - Index-based references: 2-byte indices instead of pointers
-  - Easy to serialize: can use memcpy when passing to plan nodes
+  The array is contiguous and cache-friendly, elements reference each
+  other by 2-byte index instead of by pointer, and the whole structure
+  can be serialized with memcpy when passed to plan nodes.
 
 XII-2. Forward-only Execution vs Backtracking
 
-  Choice: Forward-only (state set tracking)
+  The NFA is simulated forward-only, tracking a set of live states,
+  rather than by backtracking.
 
-  Rationale:
-  - Backtracking takes exponential time in the worst case
-  - NFA simulation guarantees polynomial time
-  - DFS order naturally guarantees preferment.
-    Greedy/reluctant per quantifier requires only reversing the DFS order
-  - Window functions receive sorted rows sequentially.
-    Forward-only fits directly into this pipeline,
-    whereas backtracking requires re-fetching previous rows
-  - DEFINE conditions are SQL expressions (PREV, RUNNING aggregates, etc.)
-    with high re-evaluation cost. Forward-only requires only one evaluation
-    per row
+  Backtracking would take exponential time in the worst case, whereas
+  forward-only NFA simulation is polynomial.  Forward-only also fits the
+  window pipeline, which delivers sorted rows sequentially: it needs no
+  re-fetching of earlier rows, and each row's DEFINE conditions (SQL
+  expressions such as PREV or running aggregates, with high re-evaluation
+  cost) are evaluated only once.  DFS order yields preferment naturally,
+  with greedy or reluctant behavior per quantifier obtained by reversing
+  that order.
 
 XII-3. Per-Context Management
 
-  Choice: Independent context per start row
+  A separate match context is maintained for each start row.
 
-  Rationale:
-  - Supports overlapping matches under SKIP TO NEXT ROW
-  - Determines the frame for each row independently
-  - Absorption optimization can eliminate redundant contexts in O(n)
+  This supports overlapping matches under SKIP TO NEXT ROW, determines
+  each row's frame independently, and lets the absorption optimization
+  eliminate redundant contexts in O(n).
 
 XII-4. Memory Pool Management
 
-  Choice: Custom free list
+  NFA states are managed through a custom free list, and both RPRNFAState
+  and RPRNFAContext are allocated in a partition-lifespan memory context
+  that is freed in release_partition.
 
-  Rationale:
-  - NFA states are created and destroyed in large numbers per row
-  - Avoids palloc/pfree overhead
-  - State size is variable (counts[] array), but within a single query
-    maxDepth is fixed, so all states have the same size
+  NFA states are created and destroyed in large numbers per row, so the
+  free list avoids palloc/pfree overhead.  Their size varies (the
+  counts[] array), but maxDepth is fixed within a single query, so all
+  states have the same size.
 
 XII-5. Execution Optimization Summary
 
-- 
2.50.1 (Apple Git-155)


From acc0ebfda053b8206b6c336a68229c7d724aea8d Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Fri, 5 Jun 2026 13:14:01 +0900
Subject: [PATCH 49/68] Point to the absorption-analysis docs from the RPR flag
 definitions

The RPR_ELEM_ABSORBABLE and RPR_ELEM_ABSORBABLE_BRANCH element flags are
described in README.rpr and computed in rpr.c, but the rpr.h definitions
gave no hint of where to look.  Add a short pointer comment to
README.rpr IV-5 ("Absorbability Analysis"), its Appendix C examples, and
computeAbsorbability(), without restating the explanation.

Documentation only.

Per an off-list review comment from Jian He.
---
 src/include/optimizer/rpr.h | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/src/include/optimizer/rpr.h b/src/include/optimizer/rpr.h
index 2fa483320ce..73c827de2b1 100644
--- a/src/include/optimizer/rpr.h
+++ b/src/include/optimizer/rpr.h
@@ -44,6 +44,12 @@
 											 * quantifier */
 #define RPR_ELEM_EMPTY_LOOP			0x02	/* END: group body can produce
 											 * empty match */
+/*
+ * The two absorption flags below are explained in README.rpr IV-5
+ * ("Absorbability Analysis"), with worked examples in Appendix C; the
+ * analysis that sets them is computeAbsorbability() in
+ * optimizer/plan/rpr.c.
+ */
 #define RPR_ELEM_ABSORBABLE_BRANCH	0x04	/* element in absorbable region */
 #define RPR_ELEM_ABSORBABLE			0x08	/* absorption judgment point */
 
-- 
2.50.1 (Apple Git-155)


From 8e806e30fd8a7dcbeb18e13faccd50cdeff0f911 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Fri, 5 Jun 2026 13:31:20 +0900
Subject: [PATCH 50/68] Fix signed-integer overflow in row pattern recognition
 frame-end clamp

ExecRPRProcessRow() computed "frameOffset + 1" before passing it to
pg_add_s64_overflow(), so a FOLLOWING offset near PG_INT64_MAX made
that add overflow on its own -- undefined behavior that crashed
cassert builds and gave wrong results otherwise.

Add the offset and the +1 in two separately checked steps, clamping to
PG_INT64_MAX on overflow, as core WindowAgg already does.  The clamp is
safe because the partition row count is an int64, so no row ever sits
at that position; a huge FOLLOWING offset thus behaves like UNBOUNDED
FOLLOWING.  Also add a regression test.
---
 src/backend/executor/execRPR.c         | 31 +++++++++++++++++++++-----
 src/test/regress/expected/rpr_base.out | 25 +++++++++++++++++++++
 src/test/regress/sql/rpr_base.sql      | 16 +++++++++++++
 3 files changed, 66 insertions(+), 6 deletions(-)

diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 56399c0c7fd..69e3603adef 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -1784,9 +1784,15 @@ ExecRPRProcessRow(WindowAggState *winstate, int64 currentPos,
 		{
 			int64		ctxFrameEnd;
 
-			/* Clamp to INT64_MAX on overflow */
-			if (pg_add_s64_overflow(ctx->matchStartRow, frameOffset + 1,
-									&ctxFrameEnd))
+			/*
+			 * Clamp to INT64_MAX on overflow.  frameOffset can be as large as
+			 * PG_INT64_MAX (e.g. "ROWS <huge> FOLLOWING"), so add the offset
+			 * and the trailing +1 in two separately checked steps to avoid
+			 * signed-integer overflow in the "frameOffset + 1" subexpression.
+			 */
+			if (pg_add_s64_overflow(ctx->matchStartRow, frameOffset,
+									&ctxFrameEnd) ||
+				pg_add_s64_overflow(ctxFrameEnd, 1, &ctxFrameEnd))
 				ctxFrameEnd = PG_INT64_MAX;
 
 			/*
@@ -1844,10 +1850,23 @@ ExecRPRProcessRow(WindowAggState *winstate, int64 currentPos,
 		 * mismatch (nfa_match with NULL), which removes all states (all
 		 * states are at VAR positions after advance). So any surviving
 		 * context here must be within its frame boundary.
+		 *
+		 * Compute the (clamped) frame end the same way as Phase 1, using two
+		 * separately checked adds so that "frameOffset + 1" cannot overflow
+		 * when frameOffset is near PG_INT64_MAX.
 		 */
-		Assert(!hasLimitedFrame ||
-			   ctx->matchStartRow > PG_INT64_MAX - frameOffset - 1 ||
-			   currentPos < ctx->matchStartRow + frameOffset + 1);
+#ifdef USE_ASSERT_CHECKING
+		if (hasLimitedFrame)
+		{
+			int64		ctxFrameEnd;
+
+			if (pg_add_s64_overflow(ctx->matchStartRow, frameOffset,
+									&ctxFrameEnd) ||
+				pg_add_s64_overflow(ctxFrameEnd, 1, &ctxFrameEnd))
+				ctxFrameEnd = PG_INT64_MAX;
+			Assert(currentPos < ctxFrameEnd);
+		}
+#endif
 
 		nfa_advance(winstate, ctx, currentPos);
 	}
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index 1410ba75395..4fe7360114f 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -643,6 +643,31 @@ ORDER BY id;
   6 |  30 |   1
 (6 rows)
 
+-- int64 frame-end overflow: a huge FOLLOWING offset must clamp to the
+-- partition end (matchStartRow + offset + 1 overflows int64; the clamp makes
+-- it behave like UNBOUNDED FOLLOWING).  Guards against signed-integer overflow
+-- in the "frameOffset + 1" subexpression (undefined behavior).  The cnt values
+-- must match the UNBOUNDED FOLLOWING result for the same data.
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_frame
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND 9223372036854775806 FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A+)
+    DEFINE A AS val > 0
+)
+ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   6
+  2 |  10 |   5
+  3 |  10 |   4
+  4 |  20 |   3
+  5 |  20 |   2
+  6 |  30 |   1
+(6 rows)
+
 -- RANGE frame with RPR (not permitted)
 SELECT id, val, COUNT(*) OVER w as cnt
 FROM rpr_frame
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index 53bf090b903..c6fcfa3e9ff 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -515,6 +515,22 @@ WINDOW w AS (
 )
 ORDER BY id;
 
+-- int64 frame-end overflow: a huge FOLLOWING offset must clamp to the
+-- partition end (matchStartRow + offset + 1 overflows int64; the clamp makes
+-- it behave like UNBOUNDED FOLLOWING).  Guards against signed-integer overflow
+-- in the "frameOffset + 1" subexpression (undefined behavior).  The cnt values
+-- must match the UNBOUNDED FOLLOWING result for the same data.
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_frame
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND 9223372036854775806 FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A+)
+    DEFINE A AS val > 0
+)
+ORDER BY id;
+
 -- RANGE frame with RPR (not permitted)
 SELECT id, val, COUNT(*) OVER w as cnt
 FROM rpr_frame
-- 
2.50.1 (Apple Git-155)


From 8b325c572b63859781879cf645e7eb87521e1118 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Fri, 5 Jun 2026 16:52:38 +0900
Subject: [PATCH 52/68] Handle row pattern navigation nodes in exprTypmod and
 isSimpleNode

RPRNavExpr, the PREV/NEXT/FIRST/LAST navigation node used in a row
pattern DEFINE clause, was missing from two type-helper switches and
fell through to their defaults.

In exprTypmod() the default returns -1, so the argument's typmod was
dropped.  A navigation expression evaluates its argument on another row
without changing its type, so its result has the same type and typmod
as the argument; return the argument's typmod, matching how exprType()
already reports its result type.

In isSimpleNode() the default returns false, which made pretty-printing
wrap a navigation operand in redundant parentheses, for example
val > (PREV(val)).  A navigation expression deparses as a function-like
FUNC(..) call that already delimits itself, so it is simple and needs
no extra parentheses; add it to the function-like group.

This affects display and node completeness only; execution and
re-parsing were already correct, as the extra parentheses were still
valid SQL.  Add a pretty-mode regression using the two-argument
pg_get_viewdef in rpr_base, a path the existing single-argument
navigation tests did not exercise.
---
 src/backend/nodes/nodeFuncs.c          |  3 +++
 src/backend/utils/adt/ruleutils.c      |  1 +
 src/test/regress/expected/rpr_base.out | 27 ++++++++++++++++++++++++++
 src/test/regress/sql/rpr_base.sql      | 13 +++++++++++++
 4 files changed, 44 insertions(+)

diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index 101c03b6ae8..aecf4e4da0a 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -392,6 +392,9 @@ exprTypmod(const Node *expr)
 			return ((const ArrayCoerceExpr *) expr)->resulttypmod;
 		case T_CollateExpr:
 			return exprTypmod((Node *) ((const CollateExpr *) expr)->arg);
+		case T_RPRNavExpr:
+			/* result has the same type/typmod as the argument expression */
+			return exprTypmod((Node *) ((const RPRNavExpr *) expr)->arg);
 		case T_CaseExpr:
 			{
 				/*
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 6237080fb36..2b8439e452e 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -9616,6 +9616,7 @@ isSimpleNode(Node *node, Node *parentNode, int prettyFlags)
 		case T_FuncExpr:
 		case T_JsonConstructorExpr:
 		case T_JsonExpr:
+		case T_RPRNavExpr:
 			/* function-like: name(..) or name[..] */
 			return true;
 
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index 4fe7360114f..fa6d101f874 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -2488,6 +2488,33 @@ SELECT pg_get_viewdef('rpr_serial_nav6'::regclass);
    b AS (NEXT(FIRST(val), (3)::bigint) > 0) );
 (1 row)
 
+-- Pretty deparse: navigation calls are function-like and take no extra parens
+CREATE VIEW rpr_nav_pretty_v AS
+SELECT id, val, count(*) OVER w
+FROM rpr_serial
+WINDOW w AS (ORDER BY id
+             ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN (A B+)
+             DEFINE A AS TRUE,
+                    B AS val > PREV(val) AND PREV(val) IS NOT NULL
+                         AND NEXT(val) > FIRST(val)
+                         AND PREV(FIRST(val)) > 0);
+SELECT pg_get_viewdef('rpr_nav_pretty_v'::regclass, true);
+                                             pg_get_viewdef                                              
+---------------------------------------------------------------------------------------------------------
+  SELECT id,                                                                                            +
+     val,                                                                                               +
+     count(*) OVER w AS count                                                                           +
+    FROM rpr_serial                                                                                     +
+   WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING                            +
+   AFTER MATCH SKIP PAST LAST ROW                                                                       +
+   INITIAL                                                                                              +
+   PATTERN (a b+)                                                                                       +
+   DEFINE                                                                                               +
+   a AS true,                                                                                           +
+   b AS val > PREV(val) AND PREV(val) IS NOT NULL AND NEXT(val) > FIRST(val) AND PREV(FIRST(val)) > 0 );
+(1 row)
+
 -- Reluctant {1}? quantifier deparse through ruleutils
 CREATE VIEW rpr_quant_reluctant_v AS
 SELECT id, val, count(*) OVER w
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index c6fcfa3e9ff..e0af7199629 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -1693,6 +1693,19 @@ WINDOW w AS (ORDER BY id
              DEFINE A AS TRUE, B AS NEXT(FIRST(val), 3) > 0);
 SELECT pg_get_viewdef('rpr_serial_nav6'::regclass);
 
+-- Pretty deparse: navigation calls are function-like and take no extra parens
+CREATE VIEW rpr_nav_pretty_v AS
+SELECT id, val, count(*) OVER w
+FROM rpr_serial
+WINDOW w AS (ORDER BY id
+             ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN (A B+)
+             DEFINE A AS TRUE,
+                    B AS val > PREV(val) AND PREV(val) IS NOT NULL
+                         AND NEXT(val) > FIRST(val)
+                         AND PREV(FIRST(val)) > 0);
+SELECT pg_get_viewdef('rpr_nav_pretty_v'::regclass, true);
+
 -- Reluctant {1}? quantifier deparse through ruleutils
 CREATE VIEW rpr_quant_reluctant_v AS
 SELECT id, val, count(*) OVER w
-- 
2.50.1 (Apple Git-155)


From c33935aecc31bc25961122e69cd96fd65710ea2a Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Sat, 6 Jun 2026 06:58:01 +0900
Subject: [PATCH 54/68] Test deparse of an inline row pattern window

A window spec written inline (OVER (...) with no WINDOW alias) deparses
through a different get_window_function_node branch than a named
window, and no view exercised it.  Add an inline-OVER view to rpr_base
and capture its viewdef, covering that path.  No code change.
---
 src/test/regress/expected/rpr_base.out | 23 +++++++++++++++++++++++
 src/test/regress/sql/rpr_base.sql      | 10 ++++++++++
 2 files changed, 33 insertions(+)

diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index f086ecc9a5d..c50c7a5f6a8 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -2564,6 +2564,29 @@ SELECT pg_get_viewdef('rpr_serial_quoted'::regclass);
    "Up" AS (val > PREV(val)) );
 (1 row)
 
+-- Inline OVER round-trip: inline window spec (no WINDOW alias) deparses inside OVER (...)
+CREATE VIEW rpr_serial_inline_over AS
+SELECT id, val,
+       count(*) OVER (ORDER BY id
+                      ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+                      PATTERN (A B+)
+                      DEFINE A AS val > 10, B AS val > PREV(val)) AS cnt
+FROM rpr_serial;
+SELECT pg_get_viewdef('rpr_serial_inline_over'::regclass);
+                                  pg_get_viewdef                                  
+----------------------------------------------------------------------------------
+  SELECT id,                                                                     +
+     val,                                                                        +
+     count(*) OVER (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
+   AFTER MATCH SKIP PAST LAST ROW                                                +
+   INITIAL                                                                       +
+   PATTERN (a b+)                                                                +
+   DEFINE                                                                        +
+   a AS (val > 10),                                                              +
+   b AS (val > PREV(val)) ) AS cnt                                               +
+    FROM rpr_serial;
+(1 row)
+
 -- Materialized view (if supported)
 CREATE TABLE rpr_mview (id INT, val INT);
 INSERT INTO rpr_mview VALUES (1, 10), (2, 20), (3, 30);
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index e0af7199629..7dfb72f6bfd 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -1727,6 +1727,16 @@ WINDOW w AS (ORDER BY id
              DEFINE "Start" AS TRUE, "Up" AS val > PREV(val));
 SELECT pg_get_viewdef('rpr_serial_quoted'::regclass);
 
+-- Inline OVER round-trip: inline window spec (no WINDOW alias) deparses inside OVER (...)
+CREATE VIEW rpr_serial_inline_over AS
+SELECT id, val,
+       count(*) OVER (ORDER BY id
+                      ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+                      PATTERN (A B+)
+                      DEFINE A AS val > 10, B AS val > PREV(val)) AS cnt
+FROM rpr_serial;
+SELECT pg_get_viewdef('rpr_serial_inline_over'::regclass);
+
 -- Materialized view (if supported)
 
 CREATE TABLE rpr_mview (id INT, val INT);
-- 
2.50.1 (Apple Git-155)


From a17415a855ba345bbd189869fb0b50334752c404 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Sat, 6 Jun 2026 07:18:12 +0900
Subject: [PATCH 55/68] Fix a mislabeled INITIAL test in row pattern
 recognition

The "INITIAL Mode" block in rpr_nfa claimed INITIAL was unimplemented
and produced a syntax error.  INITIAL is the default match mode and is
fully supported; the error was only a clause-order violation -- INITIAL
was written before AFTER MATCH SKIP, which the grammar forbids.

Reorder INITIAL after AFTER MATCH SKIP and correct the comments.  The
queries now run and confirm that explicit INITIAL matches the default
mode.  Test-only change.
---
 src/test/regress/expected/rpr_nfa.out | 31 ++++++++++++++++-----------
 src/test/regress/sql/rpr_nfa.sql      | 10 ++++-----
 2 files changed, 23 insertions(+), 18 deletions(-)

diff --git a/src/test/regress/expected/rpr_nfa.out b/src/test/regress/expected/rpr_nfa.out
index 829e8251aed..02a5e517b0e 100644
--- a/src/test/regress/expected/rpr_nfa.out
+++ b/src/test/regress/expected/rpr_nfa.out
@@ -3524,10 +3524,8 @@ ORDER BY mode, id;
 
 -- ============================================================
 -- INITIAL Mode (Runtime)
--- Placeholder: INITIAL is not yet implemented (syntax error).
--- Kept here so tests convert to runtime tests when implemented.
 -- ============================================================
--- INITIAL mode (not yet supported - produces syntax error)
+-- Explicit INITIAL (after AFTER MATCH SKIP, per the grammar); same as the default
 WITH test_initial_mode AS (
     SELECT * FROM (VALUES
         (1, ARRAY['_']),  -- Unmatched
@@ -3544,15 +3542,21 @@ FROM test_initial_mode
 WINDOW w AS (
     ORDER BY id
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    INITIAL
     AFTER MATCH SKIP TO NEXT ROW
+    INITIAL
     PATTERN (A+)
     DEFINE
         A AS 'A' = ANY(flags)
 );
-ERROR:  syntax error at or near "AFTER"
-LINE 18:     AFTER MATCH SKIP TO NEXT ROW
-             ^
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {_}   |             |          
+  2 | {A}   |           2 |         3
+  3 | {A}   |           3 |         3
+  4 | {_}   |             |          
+  5 | {A}   |           5 |         5
+(5 rows)
+
 -- Default mode (include all rows)
 WITH test_default_mode AS (
     SELECT * FROM (VALUES
@@ -3584,7 +3588,7 @@ WINDOW w AS (
   5 | {A}   |           5 |         5
 (5 rows)
 
--- Mode difference verification (INITIAL not yet supported - produces syntax error)
+-- Mode equivalence verification: explicit INITIAL equals the default mode
 WITH test_mode_diff AS (
     SELECT * FROM (VALUES
         (1, ARRAY['_']),
@@ -3598,8 +3602,8 @@ FROM (
     WINDOW w AS (
         ORDER BY id
         ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-        INITIAL
         AFTER MATCH SKIP TO NEXT ROW
+        INITIAL
         PATTERN (A)
         DEFINE A AS 'A' = ANY(flags)
     )
@@ -3617,9 +3621,12 @@ FROM (
     )
 ) sub
 ORDER BY mode;
-ERROR:  syntax error at or near "AFTER"
-LINE 15:         AFTER MATCH SKIP TO NEXT ROW
-                 ^
+  mode   | row_count 
+---------+-----------
+ DEFAULT |         3
+ INITIAL |         3
+(2 rows)
+
 -- ============================================================
 -- Frame Boundary Variations
 -- ============================================================
diff --git a/src/test/regress/sql/rpr_nfa.sql b/src/test/regress/sql/rpr_nfa.sql
index 3bbec496279..213385f143b 100644
--- a/src/test/regress/sql/rpr_nfa.sql
+++ b/src/test/regress/sql/rpr_nfa.sql
@@ -2530,11 +2530,9 @@ ORDER BY mode, id;
 
 -- ============================================================
 -- INITIAL Mode (Runtime)
--- Placeholder: INITIAL is not yet implemented (syntax error).
--- Kept here so tests convert to runtime tests when implemented.
 -- ============================================================
 
--- INITIAL mode (not yet supported - produces syntax error)
+-- Explicit INITIAL (after AFTER MATCH SKIP, per the grammar); same as the default
 WITH test_initial_mode AS (
     SELECT * FROM (VALUES
         (1, ARRAY['_']),  -- Unmatched
@@ -2551,8 +2549,8 @@ FROM test_initial_mode
 WINDOW w AS (
     ORDER BY id
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    INITIAL
     AFTER MATCH SKIP TO NEXT ROW
+    INITIAL
     PATTERN (A+)
     DEFINE
         A AS 'A' = ANY(flags)
@@ -2581,7 +2579,7 @@ WINDOW w AS (
         A AS 'A' = ANY(flags)
 );
 
--- Mode difference verification (INITIAL not yet supported - produces syntax error)
+-- Mode equivalence verification: explicit INITIAL equals the default mode
 WITH test_mode_diff AS (
     SELECT * FROM (VALUES
         (1, ARRAY['_']),
@@ -2595,8 +2593,8 @@ FROM (
     WINDOW w AS (
         ORDER BY id
         ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-        INITIAL
         AFTER MATCH SKIP TO NEXT ROW
+        INITIAL
         PATTERN (A)
         DEFINE A AS 'A' = ANY(flags)
     )
-- 
2.50.1 (Apple Git-155)


From 9a191e6f099df41bc976728d83ba5f8b3190a5a9 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Fri, 5 Jun 2026 22:30:14 +0900
Subject: [PATCH 53/68] Restore the error cursor for too many row pattern
 variables

The "too many pattern variables" error came out without a LINE/caret
cursor: the check locates the cursor via exprLocation(), which had no
T_RPRPatternNode case and fell through to the default returning -1.

Add the case alongside T_RPRNavExpr to return the node's token
location.  Caller and message text are unchanged; only the cursor is
restored.  Update the existing 241-variable rpr_base regression.
---
 src/backend/nodes/nodeFuncs.c          | 3 +++
 src/test/regress/expected/rpr_base.out | 2 ++
 2 files changed, 5 insertions(+)

diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index aecf4e4da0a..6ab227b2b60 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -1452,6 +1452,9 @@ exprLocation(const Node *expr)
 		case T_RPRNavExpr:
 			loc = ((const RPRNavExpr *) expr)->location;
 			break;
+		case T_RPRPatternNode:
+			loc = ((const RPRPatternNode *) expr)->location;
+			break;
 		case T_SubscriptingRef:
 			/* just use container argument's location */
 			loc = exprLocation((Node *) ((const SubscriptingRef *) expr)->refexpr);
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index fa6d101f874..f086ecc9a5d 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -6540,6 +6540,8 @@ WINDOW w AS (
     V231 AS val > 0, V232 AS val > 0, V233 AS val > 0, V234 AS val > 0, V235 AS val > 0, V236 AS val > 0, V237 AS val > 0, V238 AS val > 0, V239 AS val > 0, V240 AS val > 0
 );
 ERROR:  too many pattern variables
+LINE 5: ...V230 V231 V232 V233 V234 V235 V236 V237 V238 V239 V240 V241)
+                                                                  ^
 DETAIL:  Maximum is 240.
 -- Expected: ERROR - too many pattern variables (Maximum is 240)
 -- Test: Pattern nesting at maximum depth (depth 253)
-- 
2.50.1 (Apple Git-155)


From 2a9b6b156ea929b24148fe617e802c6821716b83 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Fri, 5 Jun 2026 16:37:34 +0900
Subject: [PATCH 51/68] Rework row pattern EXPLAIN deparser to fix grouped
 alternation branches

The EXPLAIN "Pattern:" line reconstructs a pattern string from the
compiled RPRPattern bytecode.  When an alternation branch was a
quantified group, the previous deparser dropped the branch separator
or the enclosing parenthesis, so the displayed pattern no longer
matched the one that was compiled.  For example PATTERN (C | (A B)+ | D)
was shown as (c | (a b)+ d), collapsing a three-way alternation into a
different two-way pattern, and a leading group branch lost the opening
parenthesis of the alternation.

The cause was that branch separators and scope ends were rebuilt from
the jump field, which the compiler overloads as a group's skip target,
and from absolute next values, which it rewrites for branch tails and
nested alternations.

Rewrite the deparser as a recursive descent in which each construct is
deparsed within an inherited [start, limit) window.  Scope ends come
from depth, branch boundaries from a branch-start jump confirmed by the
relative test elem[j-1].next != j, and parentheses from structure plus
a one-step lookahead for a group that wraps a lone alternation.  Each
construct's extent is found by a short forward scan, so the walk is not
a single linear pass; as this runs only while EXPLAIN formats its
output and patterns are small, it favors clarity over a maximally
efficient deparse.

This is display-only; query execution and pg_get_viewdef were already
correct and are unchanged.  Add regression coverage in rpr_explain for
grouped alternation branches and for deeply nested alternations, which
previously had none.
---
 src/backend/commands/explain.c            | 343 ++++----
 src/test/regress/expected/rpr_explain.out | 950 ++++++++++++++++++++++
 src/test/regress/sql/rpr_explain.sql      | 523 ++++++++++++
 3 files changed, 1623 insertions(+), 193 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 1a754bcdac5..7ba0b6df849 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -122,18 +122,13 @@ static void show_window_keys(StringInfo buf, PlanState *planstate,
 							 List *ancestors, ExplainState *es);
 static void append_rpr_quantifier(StringInfo buf, RPRPatternElement *elem);
 static char *deparse_rpr_pattern(RPRPattern *pattern);
-static void deparse_rpr_elements(RPRPattern *pattern, int *idx,
-								 StringInfoData *buf, RPRDepth groupDepth,
-								 RPRDepth *prevDepth, bool *needSpace);
-static void deparse_rpr_group(RPRPattern *pattern, int *idx,
-							  StringInfoData *buf, RPRDepth *prevDepth,
-							  bool *needSpace);
-static void deparse_rpr_alt(RPRPattern *pattern, int *idx,
-							StringInfoData *buf, RPRDepth *prevDepth,
-							bool *needSpace, List **altSeps);
-static void deparse_rpr_var(RPRPattern *pattern, int *idx,
-							StringInfoData *buf, RPRDepth *prevDepth,
-							bool *needSpace, List **altSeps);
+static int	deparse_rpr_seq(RPRPattern *pattern, int start, int limit,
+							StringInfo buf);
+static int	deparse_rpr_node(RPRPattern *pattern, int i, int limit,
+							 StringInfo buf);
+static int	rpr_match_end(RPRPattern *pattern, int beginIdx);
+static int	rpr_alt_scope_end(RPRPattern *pattern, int i);
+static int	rpr_next_branch(RPRPattern *pattern, int b, int altEnd);
 static void show_storage_info(char *maxStorageType, int64 maxSpaceUsed,
 							  ExplainState *es);
 static void show_tablesample(TableSampleClause *tsc, PlanState *planstate,
@@ -2952,244 +2947,206 @@ append_rpr_quantifier(StringInfo buf, RPRPatternElement *elem)
 }
 
 /*
- * Deparse a compiled RPRPattern (bytecode) back to pattern string.
+ * Deparse a compiled RPRPattern (bytecode) back to a pattern string.
  *
- * Walks the flat bytecode array using mutual recursion: deparse_rpr_elements
- * processes sequential elements, and deparse_rpr_group handles BEGIN...END
- * groups by recursing back into deparse_rpr_elements for the group content.
+ * The flat RPRPatternElement[] array is walked by recursive descent.  Each
+ * construct is deparsed within an inherited [start, limit) window: the parent
+ * passes the boundary down, so each construct's extent is fixed by its caller.
+ * Three signals drive the walk:
+ *
+ *   - scope ends (where an ALT or GROUP body finishes) come from depth, via
+ *     rpr_alt_scope_end() and rpr_match_end().
+ *   - branch boundaries (where a "|" goes) come from a branch-start jump,
+ *     confirmed by the relative test elem[j-1].next != j, via
+ *     rpr_next_branch().
+ *   - parentheses come from structure (a BEGIN group, an ALT) plus a one-step
+ *     lookahead for a group that wraps a lone ALT.
+ *
+ * depth and the relative next test are stable across the next/jump values the
+ * compiler assigns to branch tails and nested alternations, which is what makes
+ * them suitable to anchor scope and branch boundaries.
+ *
+ * EXPLAIN parenthesizes every ALT on its own, so a top-level "A | B" deparses
+ * as "(a | b)".  This self-consistent EXPLAIN form is the correctness oracle
+ * here; pg_get_viewdef differs, as its parens come only from an enclosing
+ * GROUP.  Absorption markers (' ") are orthogonal and handled by
+ * append_rpr_quantifier().
+ *
+ * Two compiler invariants hold throughout: {1,1} groups are unwrapped before
+ * bytecode generation (so every BEGIN/END group carries a non-trivial
+ * quantifier, and a lone ALT inside a group always spans to the group's END),
+ * and a group's quantifier is read from its END element (the BEGIN copy is
+ * ignored).
  */
 static char *
 deparse_rpr_pattern(RPRPattern *pattern)
 {
 	StringInfoData buf;
-	int			idx = 0;
-	RPRDepth	prevDepth = 0;
-	bool		needSpace = false;
 
 	Assert(pattern != NULL && pattern->numElements >= 2);
 
 	initStringInfo(&buf);
-
-	deparse_rpr_elements(pattern, &idx, &buf, RPR_DEPTH_NONE,
-						 &prevDepth, &needSpace);
-
-	/* Close remaining open parens */
-	while (prevDepth > 0)
-	{
-		appendStringInfoChar(&buf, ')');
-		prevDepth--;
-	}
-
+	deparse_rpr_seq(pattern, 0, pattern->numElements, &buf);
 	return buf.data;
 }
 
 /*
- * Process pattern elements sequentially until FIN or END at groupDepth.
+ * Deparse a run of sibling elements in [start, limit), separated by spaces.
  *
- * When groupDepth >= 0, stops at the matching END element (leaving idx
- * pointing to it) so the caller (deparse_rpr_group) can consume it.
- * When groupDepth < 0, processes until FIN (top-level call).
+ * Stops at limit or at the FIN terminator (top-level call passes limit =
+ * numElements, where the last element is FIN).  Returns the index reached.
  */
-static void
-deparse_rpr_elements(RPRPattern *pattern, int *idx, StringInfoData *buf,
-					 RPRDepth groupDepth, RPRDepth *prevDepth,
-					 bool *needSpace)
+static int
+deparse_rpr_seq(RPRPattern *pattern, int start, int limit, StringInfo buf)
 {
-	List	   *altSeps = NIL;	/* pending alternation separator indices */
+	int			i = start;
+	bool		first = true;
 
-	while (*idx < pattern->numElements)
+	while (i < limit && !RPRElemIsFin(&pattern->elements[i]))
 	{
-		RPRPatternElement *elem = &pattern->elements[*idx];
-
-		if (RPRElemIsFin(elem))
-			break;
-
-		/* Stop at END matching our group depth; caller handles it */
-		if (RPRElemIsEnd(elem) && elem->depth == groupDepth)
-			break;
-
-		/* Alternation separator */
-		if (list_member_int(altSeps, *idx))
-		{
-			/* Close parens to match separator depth first */
-			while (*prevDepth > elem->depth)
-			{
-				appendStringInfoChar(buf, ')');
-				(*prevDepth)--;
-			}
-			appendStringInfoString(buf, " | ");
-			*needSpace = false;
-			altSeps = list_delete_int(altSeps, *idx);
-		}
-
-		/* Dispatch to element-type handlers */
-		if (RPRElemIsAlt(elem))
-			deparse_rpr_alt(pattern, idx, buf, prevDepth,
-							needSpace, &altSeps);
-		else if (RPRElemIsBegin(elem))
-			deparse_rpr_group(pattern, idx, buf, prevDepth,
-							  needSpace);
-		else if (RPRElemIsVar(elem))
-			deparse_rpr_var(pattern, idx, buf, prevDepth,
-							needSpace, &altSeps);
-	}
-	list_free(altSeps);
+		if (!first)
+			appendStringInfoChar(buf, ' ');
+		first = false;
+		i = deparse_rpr_node(pattern, i, limit, buf);
+	}
+	return i;
 }
 
 /*
- * Process a BEGIN...END group.
+ * Deparse the single construct starting at index i, bounded by the inherited
+ * limit.  Returns the index just past the construct.
  *
- * Consumes BEGIN, recurses into deparse_rpr_elements for group content,
- * then consumes END and outputs the group quantifier.
- *
- * When the group wraps a single ALT with no siblings, the group-level
- * parenthesis is suppressed since the ALT-to-children depth transition
- * already provides it (avoids double parens like "((a | b))+").
+ * A VAR is its name plus quantifier.  A BEGIN opens a group spanning to its
+ * matching END (rpr_match_end); when the group's sole child is an ALT that
+ * runs to the END, the ALT supplies the parentheses and the group only adds
+ * the quantifier, otherwise the group body is wrapped in its own "( )".  An
+ * ALT runs to its depth-determined scope end (capped by the inherited limit)
+ * and emits "( b1 | b2 | ... )", each branch deparsed within the boundary
+ * handed down by rpr_next_branch.
  */
-static void
-deparse_rpr_group(RPRPattern *pattern, int *idx, StringInfoData *buf,
-				  RPRDepth *prevDepth, bool *needSpace)
+static int
+deparse_rpr_node(RPRPattern *pattern, int i, int limit, StringInfo buf)
 {
-	RPRPatternElement *begin = &pattern->elements[*idx];
-	RPRDepth	childDepth = begin->depth + 1;
-	bool		singleAlt = false;
-	RPRPatternElement *end;
+	RPRPatternElement *elem = &pattern->elements[i];
 
-	/*
-	 * Check if this group wraps a single ALT with no siblings. Scan from
-	 * after ALT to END: if no element at childDepth exists, the ALT is the
-	 * sole child.
-	 */
-	if (*idx + 1 < pattern->numElements &&
-		RPRElemIsAlt(&pattern->elements[*idx + 1]))
+	if (RPRElemIsVar(elem))
 	{
-		int			j;
+		Assert(elem->varId < pattern->numVars);
+		appendStringInfoString(buf,
+							   quote_identifier(pattern->varNames[elem->varId]));
+		append_rpr_quantifier(buf, elem);
+		return i + 1;
+	}
 
-		singleAlt = true;
-		for (j = *idx + 2; j < pattern->numElements; j++)
-		{
-			RPRPatternElement *e = &pattern->elements[j];
+	if (RPRElemIsBegin(elem))
+	{
+		int			end = rpr_match_end(pattern, i);
+		bool		loneAlt;
 
-			if (RPRElemIsEnd(e) && e->depth == begin->depth)
-				break;
-			if (e->depth <= childDepth)
-			{
-				singleAlt = false;
-				break;
-			}
+		loneAlt = (i + 1 < end &&
+				   RPRElemIsAlt(&pattern->elements[i + 1]) &&
+				   rpr_alt_scope_end(pattern, i + 1) == end);
+
+		if (loneAlt)
+		{
+			/* The ALT child already parenthesizes the whole group body. */
+			(void) deparse_rpr_node(pattern, i + 1, end, buf);
 		}
+		else
+		{
+			appendStringInfoChar(buf, '(');
+			(void) deparse_rpr_seq(pattern, i + 1, end, buf);
+			appendStringInfoChar(buf, ')');
+		}
+		append_rpr_quantifier(buf, &pattern->elements[end]);
+		return end + 1;
 	}
 
-	/* Open group paren (unless single ALT provides it) */
-	if (!singleAlt)
+	Assert(RPRElemIsAlt(elem));
 	{
-		if (*needSpace)
-			appendStringInfoChar(buf, ' ');
-		appendStringInfoChar(buf, '(');
-		*needSpace = false;
-	}
-	*prevDepth = childDepth;
-	(*idx)++;					/* consume BEGIN */
+		int			altEnd = rpr_alt_scope_end(pattern, i);
+		int			b;
+		bool		first = true;
 
-	/* Process group children; stops at matching END */
-	deparse_rpr_elements(pattern, idx, buf, begin->depth,
-						 prevDepth, needSpace);
+		if (altEnd > limit)
+			altEnd = limit;
 
-	/* Consume END and output quantifier */
-	Assert(*idx < pattern->numElements);
-	end = &pattern->elements[*idx];
-	Assert(RPRElemIsEnd(end) && end->depth == begin->depth);
+		appendStringInfoChar(buf, '(');
+		b = i + 1;
+		while (b < altEnd)
+		{
+			int			nb = rpr_next_branch(pattern, b, altEnd);
 
-	while (*prevDepth > end->depth + 1)
-	{
+			if (!first)
+				appendStringInfoString(buf, " | ");
+			first = false;
+			(void) deparse_rpr_seq(pattern, b, nb, buf);
+			b = nb;
+		}
 		appendStringInfoChar(buf, ')');
-		(*prevDepth)--;
+		return altEnd;
 	}
-	if (!singleAlt)
-		appendStringInfoChar(buf, ')');
-	append_rpr_quantifier(buf, end);
-	*prevDepth = end->depth;
-	*needSpace = true;
-	(*idx)++;					/* consume END */
 }
 
 /*
- * Process an ALT element: adjust depth parens and register separator positions.
+ * Find the END that closes the group opened by the BEGIN at beginIdx: the
+ * first END at the same depth scanning forward.
  */
-static void
-deparse_rpr_alt(RPRPattern *pattern, int *idx, StringInfoData *buf,
-				RPRDepth *prevDepth, bool *needSpace, List **altSeps)
+static int
+rpr_match_end(RPRPattern *pattern, int beginIdx)
 {
-	RPRPatternElement *elem = &pattern->elements[*idx];
-
-	/* Close parens for depth decrease */
-	while (*prevDepth > elem->depth)
-	{
-		appendStringInfoChar(buf, ')');
-		(*prevDepth)--;
-		*needSpace = true;
-	}
-
-	/* Open parens up to ALT's depth */
-	while (*prevDepth < elem->depth)
-	{
-		if (*needSpace)
-			appendStringInfoChar(buf, ' ');
-		appendStringInfoChar(buf, '(');
-		(*prevDepth)++;
-		*needSpace = false;
-	}
+	RPRDepth	d = pattern->elements[beginIdx].depth;
+	int			j;
 
-	/* Register next alternation separator position */
-	if (elem->next != RPR_ELEMIDX_INVALID)
+	for (j = beginIdx + 1; j < pattern->numElements; j++)
 	{
-		RPRPatternElement *firstElem = &pattern->elements[elem->next];
+		RPRPatternElement *e = &pattern->elements[j];
 
-		if (firstElem->jump != RPR_ELEMIDX_INVALID)
-			*altSeps = lappend_int(*altSeps, firstElem->jump);
+		if (RPRElemIsEnd(e) && e->depth == d)
+			return j;
 	}
-	if (elem->jump != RPR_ELEMIDX_INVALID)
-		*altSeps = lappend_int(*altSeps, elem->jump);
-	(*idx)++;
+	pg_unreachable();			/* a BEGIN always has a matching END */
 }
 
 /*
- * Process a VAR element: adjust depth parens and output variable name.
+ * Scope end of the construct at index i: the first following element whose
+ * depth is no greater than i's own.  For an ALT marker this is the index just
+ * past its last branch, since depth stays constant across branch boundaries.
+ * FIN sits at depth 0, so a top-level ALT stops there.
  */
-static void
-deparse_rpr_var(RPRPattern *pattern, int *idx, StringInfoData *buf,
-				RPRDepth *prevDepth, bool *needSpace, List **altSeps)
+static int
+rpr_alt_scope_end(RPRPattern *pattern, int i)
 {
-	RPRPatternElement *elem = &pattern->elements[*idx];
-
-	/* Open parens for depth increase */
-	while (*prevDepth < elem->depth)
-	{
-		if (*needSpace)
-			appendStringInfoChar(buf, ' ');
-		appendStringInfoChar(buf, '(');
-		(*prevDepth)++;
-		*needSpace = false;
-	}
+	RPRDepth	d = pattern->elements[i].depth;
+	int			k;
 
-	/* Close parens for depth decrease */
-	while (*prevDepth > elem->depth)
+	for (k = i + 1; k < pattern->numElements; k++)
 	{
-		appendStringInfoChar(buf, ')');
-		(*prevDepth)--;
+		if (pattern->elements[k].depth <= d)
+			return k;
 	}
+	return pattern->numElements;
+}
 
-	if (*needSpace)
-		appendStringInfoChar(buf, ' ');
-
-	Assert(elem->varId < pattern->numVars);
-	appendStringInfoString(buf, quote_identifier(pattern->varNames[elem->varId]));
-	append_rpr_quantifier(buf, elem);
-	*needSpace = true;
+/*
+ * Boundary of the alternation branch starting at b (i.e. the start of the next
+ * branch, or altEnd if b is the last branch).
+ *
+ * The branch-start element's jump points at the next branch when this is not
+ * the last branch.  jump is overloaded (a group BEGIN also uses it for its
+ * skip path), so confirm a real branch boundary with the relative test
+ * elem[j-1].next != j: at a true boundary the preceding branch's tail has its
+ * next redirected past the alternation, so it does not point at j.
+ */
+static int
+rpr_next_branch(RPRPattern *pattern, int b, int altEnd)
+{
+	int			j = pattern->elements[b].jump;
 
-	if (elem->jump != RPR_ELEMIDX_INVALID)
-		*altSeps = lappend_int(*altSeps, elem->jump);
-	(*idx)++;
+	if (j != RPR_ELEMIDX_INVALID && j < altEnd &&
+		pattern->elements[j - 1].next != j)
+		return j;
+	return altEnd;
 }
 
 /*
diff --git a/src/test/regress/expected/rpr_explain.out b/src/test/regress/expected/rpr_explain.out
index 5cddd1a56df..bcbf4f941ba 100644
--- a/src/test/regress/expected/rpr_explain.out
+++ b/src/test/regress/expected/rpr_explain.out
@@ -3761,6 +3761,956 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=20.00 loops=1)
 (9 rows)
 
+-- Quantified group as the first alternation branch
+-- Pattern: ((A B)+ | C) - leading group branch must open the enclosing paren
+CREATE VIEW rpr_ev_alt_grp_first AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A B)+ | C)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_first'), E'\n')) AS line WHERE line ~ 'PATTERN';
+          line           
+-------------------------
+   PATTERN ((a b)+ | c) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A B)+ | C)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ WindowAgg (actual rows=20.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: ((a' b')+" | c)
+   Nav Mark Lookback: 0
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 5 peak, 67 total, 0 merged
+   NFA Contexts: 3 peak, 21 total, 7 pruned
+   NFA: 9 matched (len 1/2/1.4), 0 mismatched
+   NFA: 0 absorbed, 4 skipped (len 1/1/1.0)
+   ->  Function Scan on generate_series s (actual rows=20.00 loops=1)
+(10 rows)
+
+-- Quantified group as the last alternation branch
+-- Pattern: (C | (A B)+) - trailing group branch, no separator follows
+CREATE VIEW rpr_ev_alt_grp_last AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (C | (A B)+)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_last'), E'\n')) AS line WHERE line ~ 'PATTERN';
+          line           
+-------------------------
+   PATTERN (c | (a b)+) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (C | (A B)+)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ WindowAgg (actual rows=20.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (c | (a' b')+")
+   Nav Mark Lookback: 0
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 5 peak, 67 total, 0 merged
+   NFA Contexts: 3 peak, 21 total, 7 pruned
+   NFA: 9 matched (len 1/2/1.4), 0 mismatched
+   NFA: 0 absorbed, 4 skipped (len 1/1/1.0)
+   ->  Function Scan on generate_series s (actual rows=20.00 loops=1)
+(10 rows)
+
+-- Quantified group as the middle branch of a three-way alternation
+-- Pattern: (C | (A B)+ | D) - separator before D must survive the group branch
+CREATE VIEW rpr_ev_alt_grp_mid AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (C | (A B)+ | D)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_mid'), E'\n')) AS line WHERE line ~ 'PATTERN';
+            line             
+-----------------------------
+   PATTERN (c | (a b)+ | d) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (C | (A B)+ | D)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ WindowAgg (actual rows=20.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (c | (a' b')+" | d)
+   Nav Mark Lookback: 0
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 6 peak, 88 total, 0 merged
+   NFA Contexts: 3 peak, 21 total, 2 pruned
+   NFA: 14 matched (len 1/2/1.3), 0 mismatched
+   NFA: 0 absorbed, 4 skipped (len 1/1/1.0)
+   ->  Function Scan on generate_series s (actual rows=20.00 loops=1)
+(10 rows)
+
+-- Quantified group as the first branch of a three-way alternation
+-- Pattern: ((A B)+ | C | D) - leading group branch with two following branches
+CREATE VIEW rpr_ev_alt_grp_first3 AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A B)+ | C | D)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_first3'), E'\n')) AS line WHERE line ~ 'PATTERN';
+            line             
+-----------------------------
+   PATTERN ((a b)+ | c | d) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A B)+ | C | D)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ WindowAgg (actual rows=20.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: ((a' b')+" | c | d)
+   Nav Mark Lookback: 0
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 6 peak, 88 total, 0 merged
+   NFA Contexts: 3 peak, 21 total, 2 pruned
+   NFA: 14 matched (len 1/2/1.3), 0 mismatched
+   NFA: 0 absorbed, 4 skipped (len 1/1/1.0)
+   ->  Function Scan on generate_series s (actual rows=20.00 loops=1)
+(10 rows)
+
+-- Bounded-quantifier group as the first alternation branch
+-- Pattern: ((A B){2} | C) - leading group branch with a range quantifier
+CREATE VIEW rpr_ev_alt_grp_bounded AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A B){2} | C)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_bounded'), E'\n')) AS line WHERE line ~ 'PATTERN';
+           line            
+---------------------------
+   PATTERN ((a b){2} | c) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A B){2} | C)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ WindowAgg (actual rows=20.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: ((a b){2} | c)
+   Nav Mark Lookback: 0
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 4 peak, 63 total, 0 merged
+   NFA Contexts: 3 peak, 21 total, 11 pruned
+   NFA: 5 matched (len 1/1/1.0), 4 mismatched (len 3/3/3.0)
+   ->  Function Scan on generate_series s (actual rows=20.00 loops=1)
+(9 rows)
+
+-- Two quantified groups in one alternation
+-- Pattern: ((A B)+ | (C D)+) - both branches are groups
+CREATE VIEW rpr_ev_alt_grp_both AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A B)+ | (C D)+)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_both'), E'\n')) AS line WHERE line ~ 'PATTERN';
+             line             
+------------------------------
+   PATTERN ((a b)+ | (c d)+) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A B)+ | (C D)+)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ WindowAgg (actual rows=20.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: ((a' b')+" | (c' d')+")
+   Nav Mark Lookback: 0
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 5 peak, 72 total, 0 merged
+   NFA Contexts: 3 peak, 21 total, 2 pruned
+   NFA: 9 matched (len 2/2/2.0), 0 mismatched
+   NFA: 0 absorbed, 9 skipped (len 1/1/1.0)
+   ->  Function Scan on generate_series s (actual rows=20.00 loops=1)
+(10 rows)
+
+-- Leading group branch in an alternation nested in a sequence
+-- Pattern: (((A B)+ | C) D) - inner alternation opens with a group branch
+CREATE VIEW rpr_ev_alt_grp_seq_head AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (((A B)+ | C) D)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_seq_head'), E'\n')) AS line WHERE line ~ 'PATTERN';
+            line             
+-----------------------------
+   PATTERN (((a b)+ | c) d) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (((A B)+ | C) D)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ WindowAgg (actual rows=20.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: ((a' b')+" | c) d
+   Nav Mark Lookback: 0
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 5 peak, 67 total, 0 merged
+   NFA Contexts: 3 peak, 21 total, 6 pruned
+   NFA: 5 matched (len 2/2/2.0), 4 mismatched (len 3/3/3.0)
+   NFA: 0 absorbed, 5 skipped (len 1/1/1.0)
+   ->  Function Scan on generate_series s (actual rows=20.00 loops=1)
+(10 rows)
+
+-- Trailing group branch in an alternation nested in a sequence
+-- Pattern: ((C | (A B)+) D) - group as last branch, then a sequence element
+CREATE VIEW rpr_ev_alt_grp_seq_tail AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((C | (A B)+) D)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_seq_tail'), E'\n')) AS line WHERE line ~ 'PATTERN';
+            line             
+-----------------------------
+   PATTERN ((c | (a b)+) d) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((C | (A B)+) D)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ WindowAgg (actual rows=20.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (c | (a' b')+") d
+   Nav Mark Lookback: 0
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 5 peak, 67 total, 0 merged
+   NFA Contexts: 3 peak, 21 total, 6 pruned
+   NFA: 5 matched (len 2/2/2.0), 4 mismatched (len 3/3/3.0)
+   NFA: 0 absorbed, 5 skipped (len 1/1/1.0)
+   ->  Function Scan on generate_series s (actual rows=20.00 loops=1)
+(10 rows)
+
+-- Quantified alternation whose first branch is a quantified group
+-- Pattern: (((A B){2} | C)+) - single-ALT group wraps a leading group branch
+CREATE VIEW rpr_ev_alt_grp_quant AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (((A B){2} | C)+)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_quant'), E'\n')) AS line WHERE line ~ 'PATTERN';
+             line             
+------------------------------
+   PATTERN (((a b){2} | c)+) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (((A B){2} | C)+)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ WindowAgg (actual rows=20.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: ((a b){2} | c)+
+   Nav Mark Lookback: 0
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 6 peak, 78 total, 0 merged
+   NFA Contexts: 3 peak, 21 total, 11 pruned
+   NFA: 5 matched (len 1/1/1.0), 4 mismatched (len 3/3/3.0)
+   ->  Function Scan on generate_series s (actual rows=20.00 loops=1)
+(9 rows)
+
+-- Unit (1,1) group as an alternation branch (emits no BEGIN/END)
+-- Pattern: ((A B) | C) - control: takes the variable path, not deparse_rpr_group
+CREATE VIEW rpr_ev_alt_grp_unit AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A B) | C)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_unit'), E'\n')) AS line WHERE line ~ 'PATTERN';
+          line          
+------------------------
+   PATTERN ((a b) | c) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A B) | C)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ WindowAgg (actual rows=20.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a b | c)
+   Nav Mark Lookback: 0
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 4 peak, 63 total, 0 merged
+   NFA Contexts: 2 peak, 21 total, 7 pruned
+   NFA: 9 matched (len 1/2/1.4), 0 mismatched
+   NFA: 0 absorbed, 4 skipped (len 1/1/1.0)
+   ->  Function Scan on generate_series s (actual rows=20.00 loops=1)
+(10 rows)
+
+-- Quantified variable as the first alternation branch
+-- Pattern: (A+ | C) - control: deparse_rpr_var already opens the leading paren
+CREATE VIEW rpr_ev_alt_var_first AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+ | C)
+    DEFINE A AS v % 4 = 0, C AS v % 4 = 2
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_var_first'), E'\n')) AS line WHERE line ~ 'PATTERN';
+        line         
+---------------------
+   PATTERN (a+ | c) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+ | C)
+    DEFINE A AS v % 4 = 0, C AS v % 4 = 2
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ WindowAgg (actual rows=20.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a+" | c)
+   Nav Mark Lookback: 0
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 5 peak, 68 total, 0 merged
+   NFA Contexts: 3 peak, 21 total, 10 pruned
+   NFA: 10 matched (len 1/1/1.0), 0 mismatched
+   ->  Function Scan on generate_series s (actual rows=20.00 loops=1)
+(9 rows)
+
+-- Quantified group as the last branch of a three-way alternation
+-- Pattern: (C | D | (A B)+) - control: trailing group needs no separator
+CREATE VIEW rpr_ev_alt_grp_last3 AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (C | D | (A B)+)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_last3'), E'\n')) AS line WHERE line ~ 'PATTERN';
+            line             
+-----------------------------
+   PATTERN (c | d | (a b)+) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (C | D | (A B)+)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ WindowAgg (actual rows=20.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (c | d | (a' b')+")
+   Nav Mark Lookback: 0
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 6 peak, 88 total, 0 merged
+   NFA Contexts: 3 peak, 21 total, 2 pruned
+   NFA: 14 matched (len 1/2/1.3), 0 mismatched
+   NFA: 0 absorbed, 4 skipped (len 1/1/1.0)
+   ->  Function Scan on generate_series s (actual rows=20.00 loops=1)
+(10 rows)
+
+-- Alternation nested in a leading branch must not swallow the trailing branch
+-- Pattern: (D (A | B) | E) - inherited limit bounds the inner alternation
+CREATE VIEW rpr_ev_alt_inner_bounded AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (D (A | B) | E)
+    DEFINE A AS v % 5 = 0, B AS v % 5 = 1, D AS v % 5 = 2, E AS v % 5 = 3
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_inner_bounded'), E'\n')) AS line WHERE line ~ 'PATTERN';
+            line            
+----------------------------
+   PATTERN (d (a | b) | e) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (D (A | B) | E)
+    DEFINE A AS v % 5 = 0, B AS v % 5 = 1, D AS v % 5 = 2, E AS v % 5 = 3
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ WindowAgg (actual rows=20.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (d (a | b) | e)
+   Nav Mark Lookback: 0
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 5 peak, 71 total, 0 merged
+   NFA Contexts: 3 peak, 21 total, 12 pruned
+   NFA: 4 matched (len 1/1/1.0), 4 mismatched (len 2/2/2.0)
+   ->  Function Scan on generate_series s (actual rows=20.00 loops=1)
+(9 rows)
+
+-- Group mid-branch followed by a sequence element needs no separator before it
+-- Pattern: (C | (A B)+ D) - relative-next blocks a spurious separator at D
+CREATE VIEW rpr_ev_alt_grp_then_seq AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (C | (A B)+ D)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_then_seq'), E'\n')) AS line WHERE line ~ 'PATTERN';
+           line            
+---------------------------
+   PATTERN (c | (a b)+ d) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (C | (A B)+ D)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ WindowAgg (actual rows=20.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (c | (a' b')+" d)
+   Nav Mark Lookback: 0
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 6 peak, 88 total, 0 merged
+   NFA Contexts: 3 peak, 21 total, 6 pruned
+   NFA: 10 matched (len 1/1/1.0), 4 mismatched (len 3/3/3.0)
+   ->  Function Scan on generate_series s (actual rows=20.00 loops=1)
+(9 rows)
+
+-- Quantified group wrapping a lone alternation: the ALT supplies the parens
+-- Pattern: ((A | B)+) - loneAlt path, single pair of parens
+CREATE VIEW rpr_ev_grp_lone_alt AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A | B)+)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_grp_lone_alt'), E'\n')) AS line WHERE line ~ 'PATTERN';
+         line          
+-----------------------
+   PATTERN ((a | b)+) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A | B)+)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ WindowAgg (actual rows=20.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a | b)+
+   Nav Mark Lookback: 0
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 6 peak, 93 total, 0 merged
+   NFA Contexts: 3 peak, 21 total, 10 pruned
+   NFA: 6 matched (len 1/2/1.7), 0 mismatched
+   NFA: 0 absorbed, 4 skipped (len 1/1/1.0)
+   ->  Function Scan on generate_series s (actual rows=20.00 loops=1)
+(10 rows)
+
+-- Quantified group wrapping a sequence whose last element is an alternation
+-- Pattern: ((A (B | C))+) - group paren plus a nested alternation paren
+CREATE VIEW rpr_ev_grp_seq_alt AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A (B | C))+)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_grp_seq_alt'), E'\n')) AS line WHERE line ~ 'PATTERN';
+           line            
+---------------------------
+   PATTERN ((a (b | c))+) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A (B | C))+)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ WindowAgg (actual rows=20.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a (b | c))+
+   Nav Mark Lookback: 0
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 3 peak, 35 total, 0 merged
+   NFA Contexts: 3 peak, 21 total, 12 pruned
+   NFA: 4 matched (len 2/2/2.0), 0 mismatched
+   NFA: 0 absorbed, 4 skipped (len 1/1/1.0)
+   ->  Function Scan on generate_series s (actual rows=20.00 loops=1)
+(10 rows)
+
+-- Quantified group wrapping a sequence whose first element is an alternation
+-- Pattern: (((A | B) C)+) - leading nested alternation inside a group sequence
+CREATE VIEW rpr_ev_grp_alt_seq AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (((A | B) C)+)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_grp_alt_seq'), E'\n')) AS line WHERE line ~ 'PATTERN';
+           line            
+---------------------------
+   PATTERN (((a | b) c)+) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (((A | B) C)+)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ WindowAgg (actual rows=20.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: ((a | b) c)+
+   Nav Mark Lookback: 0
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 6 peak, 78 total, 0 merged
+   NFA Contexts: 3 peak, 21 total, 6 pruned
+   NFA: 5 matched (len 2/2/2.0), 4 mismatched (len 2/2/2.0)
+   NFA: 0 absorbed, 5 skipped (len 1/1/1.0)
+   ->  Function Scan on generate_series s (actual rows=20.00 loops=1)
+(10 rows)
+
+-- Alternation non-last in a non-last branch, stacked three deep: each level's
+-- inherited limit must bound the inner alternation against the next branch
+-- Pattern: (((A | B) C | D) E | F) - three nested inherited-limit boundaries
+CREATE VIEW rpr_ev_alt_stack3 AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (((A | B) C | D) E | F)
+    DEFINE A AS v % 6 = 0, B AS v % 6 = 1, C AS v % 6 = 2,
+           D AS v % 6 = 3, E AS v % 6 = 4, F AS v % 6 = 5
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_stack3'), E'\n')) AS line WHERE line ~ 'PATTERN';
+                line                
+------------------------------------
+   PATTERN (((a | b) c | d) e | f) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (((A | B) C | D) E | F)
+    DEFINE A AS v % 6 = 0, B AS v % 6 = 1, C AS v % 6 = 2,
+           D AS v % 6 = 3, E AS v % 6 = 4, F AS v % 6 = 5
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ WindowAgg (actual rows=20.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (((a | b) c | d) e | f)
+   Nav Mark Lookback: 0
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 6 peak, 147 total, 0 merged
+   NFA Contexts: 3 peak, 21 total, 4 pruned
+   NFA: 6 matched (len 1/2/1.5), 7 mismatched (len 2/3/2.4)
+   NFA: 0 absorbed, 3 skipped (len 1/1/1.0)
+   ->  Function Scan on generate_series s (actual rows=20.00 loops=1)
+(10 rows)
+
+-- Same interaction stacked four deep, to exercise the induction one step further
+-- Pattern: ((((A | B) C | D) E | F) G | H) - four nested inherited-limit boundaries
+CREATE VIEW rpr_ev_alt_stack4 AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((((A | B) C | D) E | F) G | H)
+    DEFINE A AS v % 8 = 0, B AS v % 8 = 1, C AS v % 8 = 2, D AS v % 8 = 3,
+           E AS v % 8 = 4, F AS v % 8 = 5, G AS v % 8 = 6, H AS v % 8 = 7
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_stack4'), E'\n')) AS line WHERE line ~ 'PATTERN';
+                    line                    
+--------------------------------------------
+   PATTERN ((((a | b) c | d) e | f) g | h) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((((A | B) C | D) E | F) G | H)
+    DEFINE A AS v % 8 = 0, B AS v % 8 = 1, C AS v % 8 = 2, D AS v % 8 = 3,
+           E AS v % 8 = 4, F AS v % 8 = 5, G AS v % 8 = 6, H AS v % 8 = 7
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ WindowAgg (actual rows=20.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: ((((a | b) c | d) e | f) g | h)
+   Nav Mark Lookback: 0
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 7 peak, 189 total, 0 merged
+   NFA Contexts: 3 peak, 21 total, 6 pruned
+   NFA: 4 matched (len 1/2/1.5), 8 mismatched (len 2/3/2.6)
+   NFA: 0 absorbed, 2 skipped (len 1/1/1.0)
+   ->  Function Scan on generate_series s (actual rows=20.00 loops=1)
+(10 rows)
+
+-- Three-deep stack whose innermost branch is a quantified group: the group's
+-- skip-target jump must not be mistaken for a branch separator at any depth
+-- Pattern: (((A | B)+ C | D) E | F) - inherited limit plus loneAlt at the base
+CREATE VIEW rpr_ev_alt_stack3_grp AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (((A | B)+ C | D) E | F)
+    DEFINE A AS v % 6 = 0, B AS v % 6 = 1, C AS v % 6 = 2,
+           D AS v % 6 = 3, E AS v % 6 = 4, F AS v % 6 = 5
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_stack3_grp'), E'\n')) AS line WHERE line ~ 'PATTERN';
+                line                 
+-------------------------------------
+   PATTERN (((a | b)+ c | d) e | f) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (((A | B)+ C | D) E | F)
+    DEFINE A AS v % 6 = 0, B AS v % 6 = 1, C AS v % 6 = 2,
+           D AS v % 6 = 3, E AS v % 6 = 4, F AS v % 6 = 5
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ WindowAgg (actual rows=20.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (((a | b)+ c | d) e | f)
+   Nav Mark Lookback: 0
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 11 peak, 177 total, 0 merged
+   NFA Contexts: 4 peak, 21 total, 4 pruned
+   NFA: 6 matched (len 1/2/1.5), 7 mismatched (len 2/4/3.1)
+   NFA: 0 absorbed, 3 skipped (len 1/1/1.0)
+   ->  Function Scan on generate_series s (actual rows=20.00 loops=1)
+(10 rows)
+
+-- Alternation trailing a paren-less sequence (last element of a non-last
+-- branch, no same-depth sibling to bound it), nested three deep
+-- Pattern: (A (B (C | D) | E) | F) - each inner alternation is branch-tail
+CREATE VIEW rpr_ev_alt_tail3 AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A (B (C | D) | E) | F)
+    DEFINE A AS v % 6 = 0, B AS v % 6 = 1, C AS v % 6 = 2,
+           D AS v % 6 = 3, E AS v % 6 = 4, F AS v % 6 = 5
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_tail3'), E'\n')) AS line WHERE line ~ 'PATTERN';
+                line                
+------------------------------------
+   PATTERN (a (b (c | d) | e) | f) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A (B (C | D) | E) | F)
+    DEFINE A AS v % 6 = 0, B AS v % 6 = 1, C AS v % 6 = 2,
+           D AS v % 6 = 3, E AS v % 6 = 4, F AS v % 6 = 5
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ WindowAgg (actual rows=20.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a (b (c | d) | e) | f)
+   Nav Mark Lookback: 0
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 5 peak, 75 total, 0 merged
+   NFA Contexts: 3 peak, 21 total, 11 pruned
+   NFA: 6 matched (len 1/3/2.0), 0 mismatched
+   NFA: 0 absorbed, 3 skipped (len 1/1/1.0)
+   ->  Function Scan on generate_series s (actual rows=20.00 loops=1)
+(10 rows)
+
+-- Same branch-tail alternation nested four deep
+-- Pattern: (A (B (C (D | E) | F) | G) | H) - branch-tail alternation x4
+CREATE VIEW rpr_ev_alt_tail4 AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A (B (C (D | E) | F) | G) | H)
+    DEFINE A AS v % 8 = 0, B AS v % 8 = 1, C AS v % 8 = 2, D AS v % 8 = 3,
+           E AS v % 8 = 4, F AS v % 8 = 5, G AS v % 8 = 6, H AS v % 8 = 7
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_tail4'), E'\n')) AS line WHERE line ~ 'PATTERN';
+                    line                    
+--------------------------------------------
+   PATTERN (a (b (c (d | e) | f) | g) | h) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A (B (C (D | E) | F) | G) | H)
+    DEFINE A AS v % 8 = 0, B AS v % 8 = 1, C AS v % 8 = 2, D AS v % 8 = 3,
+           E AS v % 8 = 4, F AS v % 8 = 5, G AS v % 8 = 6, H AS v % 8 = 7
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ WindowAgg (actual rows=20.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a (b (c (d | e) | f) | g) | h)
+   Nav Mark Lookback: 0
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 5 peak, 75 total, 0 merged
+   NFA Contexts: 3 peak, 21 total, 14 pruned
+   NFA: 4 matched (len 1/4/2.5), 0 mismatched
+   NFA: 0 absorbed, 2 skipped (len 1/1/1.0)
+   ->  Function Scan on generate_series s (actual rows=20.00 loops=1)
+(10 rows)
+
+-- A nested alternation tail neighbouring a multi-element sequence branch: the
+-- branch boundary must split "...branch-tail ALT" from a plain "G A" sequence
+-- Pattern: (A (B (C (D | E) | F) | G A) | H) - seq branch beside an ALT tail
+CREATE VIEW rpr_ev_alt_tail_seqbranch AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A (B (C (D | E) | F) | G A) | H)
+    DEFINE A AS v % 8 = 0, B AS v % 8 = 1, C AS v % 8 = 2, D AS v % 8 = 3,
+           E AS v % 8 = 4, F AS v % 8 = 5, G AS v % 8 = 6, H AS v % 8 = 7
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_tail_seqbranch'), E'\n')) AS line WHERE line ~ 'PATTERN';
+                     line                     
+----------------------------------------------
+   PATTERN (a (b (c (d | e) | f) | g a) | h) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A (B (C (D | E) | F) | G A) | H)
+    DEFINE A AS v % 8 = 0, B AS v % 8 = 1, C AS v % 8 = 2, D AS v % 8 = 3,
+           E AS v % 8 = 4, F AS v % 8 = 5, G AS v % 8 = 6, H AS v % 8 = 7
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ WindowAgg (actual rows=20.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a (b (c (d | e) | f) | g a) | h)
+   Nav Mark Lookback: 0
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 5 peak, 75 total, 0 merged
+   NFA Contexts: 3 peak, 21 total, 14 pruned
+   NFA: 4 matched (len 1/4/2.5), 0 mismatched
+   NFA: 0 absorbed, 2 skipped (len 1/1/1.0)
+   ->  Function Scan on generate_series s (actual rows=20.00 loops=1)
+(10 rows)
+
+-- A nested alternation that is sibling-bounded by a trailing sequence element
+-- at the outer level (the ALT is not the branch tail; G follows it in-branch)
+-- Pattern: ((A (B (C | D) | E) | F) G | H) - ALT bounded by a following element
+CREATE VIEW rpr_ev_alt_mid_seqtail AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A (B (C | D) | E) | F) G | H)
+    DEFINE A AS v % 8 = 0, B AS v % 8 = 1, C AS v % 8 = 2, D AS v % 8 = 3,
+           E AS v % 8 = 4, F AS v % 8 = 5, G AS v % 8 = 6, H AS v % 8 = 7
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_mid_seqtail'), E'\n')) AS line WHERE line ~ 'PATTERN';
+                    line                    
+--------------------------------------------
+   PATTERN ((a (b (c | d) | e) | f) g | h) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A (B (C | D) | E) | F) G | H)
+    DEFINE A AS v % 8 = 0, B AS v % 8 = 1, C AS v % 8 = 2, D AS v % 8 = 3,
+           E AS v % 8 = 4, F AS v % 8 = 5, G AS v % 8 = 6, H AS v % 8 = 7
+);');
+                          rpr_explain_filter                          
+----------------------------------------------------------------------
+ WindowAgg (actual rows=20.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: ((a (b (c | d) | e) | f) g | h)
+   Nav Mark Lookback: 0
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 6 peak, 113 total, 0 merged
+   NFA Contexts: 3 peak, 21 total, 12 pruned
+   NFA: 4 matched (len 1/2/1.5), 2 mismatched (len 4/4/4.0)
+   NFA: 0 absorbed, 2 skipped (len 1/1/1.0)
+   ->  Function Scan on generate_series s (actual rows=20.00 loops=1)
+(10 rows)
+
 -- ============================================================
 -- Group Pattern Tests
 -- ============================================================
diff --git a/src/test/regress/sql/rpr_explain.sql b/src/test/regress/sql/rpr_explain.sql
index c8b159e30e6..aa78ffed260 100644
--- a/src/test/regress/sql/rpr_explain.sql
+++ b/src/test/regress/sql/rpr_explain.sql
@@ -2118,6 +2118,529 @@ WINDOW w AS (
     DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
 );');
 
+-- Quantified group as the first alternation branch
+-- Pattern: ((A B)+ | C) - leading group branch must open the enclosing paren
+CREATE VIEW rpr_ev_alt_grp_first AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A B)+ | C)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_first'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A B)+ | C)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2
+);');
+
+-- Quantified group as the last alternation branch
+-- Pattern: (C | (A B)+) - trailing group branch, no separator follows
+CREATE VIEW rpr_ev_alt_grp_last AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (C | (A B)+)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_last'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (C | (A B)+)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2
+);');
+
+-- Quantified group as the middle branch of a three-way alternation
+-- Pattern: (C | (A B)+ | D) - separator before D must survive the group branch
+CREATE VIEW rpr_ev_alt_grp_mid AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (C | (A B)+ | D)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_mid'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (C | (A B)+ | D)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);');
+
+-- Quantified group as the first branch of a three-way alternation
+-- Pattern: ((A B)+ | C | D) - leading group branch with two following branches
+CREATE VIEW rpr_ev_alt_grp_first3 AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A B)+ | C | D)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_first3'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A B)+ | C | D)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);');
+
+-- Bounded-quantifier group as the first alternation branch
+-- Pattern: ((A B){2} | C) - leading group branch with a range quantifier
+CREATE VIEW rpr_ev_alt_grp_bounded AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A B){2} | C)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_bounded'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A B){2} | C)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2
+);');
+
+-- Two quantified groups in one alternation
+-- Pattern: ((A B)+ | (C D)+) - both branches are groups
+CREATE VIEW rpr_ev_alt_grp_both AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A B)+ | (C D)+)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_both'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A B)+ | (C D)+)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);');
+
+-- Leading group branch in an alternation nested in a sequence
+-- Pattern: (((A B)+ | C) D) - inner alternation opens with a group branch
+CREATE VIEW rpr_ev_alt_grp_seq_head AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (((A B)+ | C) D)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_seq_head'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (((A B)+ | C) D)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);');
+
+-- Trailing group branch in an alternation nested in a sequence
+-- Pattern: ((C | (A B)+) D) - group as last branch, then a sequence element
+CREATE VIEW rpr_ev_alt_grp_seq_tail AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((C | (A B)+) D)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_seq_tail'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((C | (A B)+) D)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);');
+
+-- Quantified alternation whose first branch is a quantified group
+-- Pattern: (((A B){2} | C)+) - single-ALT group wraps a leading group branch
+CREATE VIEW rpr_ev_alt_grp_quant AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (((A B){2} | C)+)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_quant'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (((A B){2} | C)+)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2
+);');
+
+-- Unit (1,1) group as an alternation branch (emits no BEGIN/END)
+-- Pattern: ((A B) | C) - control: takes the variable path, not deparse_rpr_group
+CREATE VIEW rpr_ev_alt_grp_unit AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A B) | C)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_unit'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A B) | C)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2
+);');
+
+-- Quantified variable as the first alternation branch
+-- Pattern: (A+ | C) - control: deparse_rpr_var already opens the leading paren
+CREATE VIEW rpr_ev_alt_var_first AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+ | C)
+    DEFINE A AS v % 4 = 0, C AS v % 4 = 2
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_var_first'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+ | C)
+    DEFINE A AS v % 4 = 0, C AS v % 4 = 2
+);');
+
+-- Quantified group as the last branch of a three-way alternation
+-- Pattern: (C | D | (A B)+) - control: trailing group needs no separator
+CREATE VIEW rpr_ev_alt_grp_last3 AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (C | D | (A B)+)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_last3'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (C | D | (A B)+)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);');
+
+-- Alternation nested in a leading branch must not swallow the trailing branch
+-- Pattern: (D (A | B) | E) - inherited limit bounds the inner alternation
+CREATE VIEW rpr_ev_alt_inner_bounded AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (D (A | B) | E)
+    DEFINE A AS v % 5 = 0, B AS v % 5 = 1, D AS v % 5 = 2, E AS v % 5 = 3
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_inner_bounded'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (D (A | B) | E)
+    DEFINE A AS v % 5 = 0, B AS v % 5 = 1, D AS v % 5 = 2, E AS v % 5 = 3
+);');
+
+-- Group mid-branch followed by a sequence element needs no separator before it
+-- Pattern: (C | (A B)+ D) - relative-next blocks a spurious separator at D
+CREATE VIEW rpr_ev_alt_grp_then_seq AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (C | (A B)+ D)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_then_seq'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (C | (A B)+ D)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
+);');
+
+-- Quantified group wrapping a lone alternation: the ALT supplies the parens
+-- Pattern: ((A | B)+) - loneAlt path, single pair of parens
+CREATE VIEW rpr_ev_grp_lone_alt AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A | B)+)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_grp_lone_alt'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A | B)+)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1
+);');
+
+-- Quantified group wrapping a sequence whose last element is an alternation
+-- Pattern: ((A (B | C))+) - group paren plus a nested alternation paren
+CREATE VIEW rpr_ev_grp_seq_alt AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A (B | C))+)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_grp_seq_alt'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A (B | C))+)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2
+);');
+
+-- Quantified group wrapping a sequence whose first element is an alternation
+-- Pattern: (((A | B) C)+) - leading nested alternation inside a group sequence
+CREATE VIEW rpr_ev_grp_alt_seq AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (((A | B) C)+)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_grp_alt_seq'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (((A | B) C)+)
+    DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2
+);');
+
+-- Alternation non-last in a non-last branch, stacked three deep: each level's
+-- inherited limit must bound the inner alternation against the next branch
+-- Pattern: (((A | B) C | D) E | F) - three nested inherited-limit boundaries
+CREATE VIEW rpr_ev_alt_stack3 AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (((A | B) C | D) E | F)
+    DEFINE A AS v % 6 = 0, B AS v % 6 = 1, C AS v % 6 = 2,
+           D AS v % 6 = 3, E AS v % 6 = 4, F AS v % 6 = 5
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_stack3'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (((A | B) C | D) E | F)
+    DEFINE A AS v % 6 = 0, B AS v % 6 = 1, C AS v % 6 = 2,
+           D AS v % 6 = 3, E AS v % 6 = 4, F AS v % 6 = 5
+);');
+
+-- Same interaction stacked four deep, to exercise the induction one step further
+-- Pattern: ((((A | B) C | D) E | F) G | H) - four nested inherited-limit boundaries
+CREATE VIEW rpr_ev_alt_stack4 AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((((A | B) C | D) E | F) G | H)
+    DEFINE A AS v % 8 = 0, B AS v % 8 = 1, C AS v % 8 = 2, D AS v % 8 = 3,
+           E AS v % 8 = 4, F AS v % 8 = 5, G AS v % 8 = 6, H AS v % 8 = 7
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_stack4'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((((A | B) C | D) E | F) G | H)
+    DEFINE A AS v % 8 = 0, B AS v % 8 = 1, C AS v % 8 = 2, D AS v % 8 = 3,
+           E AS v % 8 = 4, F AS v % 8 = 5, G AS v % 8 = 6, H AS v % 8 = 7
+);');
+
+-- Three-deep stack whose innermost branch is a quantified group: the group's
+-- skip-target jump must not be mistaken for a branch separator at any depth
+-- Pattern: (((A | B)+ C | D) E | F) - inherited limit plus loneAlt at the base
+CREATE VIEW rpr_ev_alt_stack3_grp AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (((A | B)+ C | D) E | F)
+    DEFINE A AS v % 6 = 0, B AS v % 6 = 1, C AS v % 6 = 2,
+           D AS v % 6 = 3, E AS v % 6 = 4, F AS v % 6 = 5
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_stack3_grp'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (((A | B)+ C | D) E | F)
+    DEFINE A AS v % 6 = 0, B AS v % 6 = 1, C AS v % 6 = 2,
+           D AS v % 6 = 3, E AS v % 6 = 4, F AS v % 6 = 5
+);');
+
+-- Alternation trailing a paren-less sequence (last element of a non-last
+-- branch, no same-depth sibling to bound it), nested three deep
+-- Pattern: (A (B (C | D) | E) | F) - each inner alternation is branch-tail
+CREATE VIEW rpr_ev_alt_tail3 AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A (B (C | D) | E) | F)
+    DEFINE A AS v % 6 = 0, B AS v % 6 = 1, C AS v % 6 = 2,
+           D AS v % 6 = 3, E AS v % 6 = 4, F AS v % 6 = 5
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_tail3'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A (B (C | D) | E) | F)
+    DEFINE A AS v % 6 = 0, B AS v % 6 = 1, C AS v % 6 = 2,
+           D AS v % 6 = 3, E AS v % 6 = 4, F AS v % 6 = 5
+);');
+
+-- Same branch-tail alternation nested four deep
+-- Pattern: (A (B (C (D | E) | F) | G) | H) - branch-tail alternation x4
+CREATE VIEW rpr_ev_alt_tail4 AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A (B (C (D | E) | F) | G) | H)
+    DEFINE A AS v % 8 = 0, B AS v % 8 = 1, C AS v % 8 = 2, D AS v % 8 = 3,
+           E AS v % 8 = 4, F AS v % 8 = 5, G AS v % 8 = 6, H AS v % 8 = 7
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_tail4'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A (B (C (D | E) | F) | G) | H)
+    DEFINE A AS v % 8 = 0, B AS v % 8 = 1, C AS v % 8 = 2, D AS v % 8 = 3,
+           E AS v % 8 = 4, F AS v % 8 = 5, G AS v % 8 = 6, H AS v % 8 = 7
+);');
+
+-- A nested alternation tail neighbouring a multi-element sequence branch: the
+-- branch boundary must split "...branch-tail ALT" from a plain "G A" sequence
+-- Pattern: (A (B (C (D | E) | F) | G A) | H) - seq branch beside an ALT tail
+CREATE VIEW rpr_ev_alt_tail_seqbranch AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A (B (C (D | E) | F) | G A) | H)
+    DEFINE A AS v % 8 = 0, B AS v % 8 = 1, C AS v % 8 = 2, D AS v % 8 = 3,
+           E AS v % 8 = 4, F AS v % 8 = 5, G AS v % 8 = 6, H AS v % 8 = 7
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_tail_seqbranch'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A (B (C (D | E) | F) | G A) | H)
+    DEFINE A AS v % 8 = 0, B AS v % 8 = 1, C AS v % 8 = 2, D AS v % 8 = 3,
+           E AS v % 8 = 4, F AS v % 8 = 5, G AS v % 8 = 6, H AS v % 8 = 7
+);');
+
+-- A nested alternation that is sibling-bounded by a trailing sequence element
+-- at the outer level (the ALT is not the branch tail; G follows it in-branch)
+-- Pattern: ((A (B (C | D) | E) | F) G | H) - ALT bounded by a following element
+CREATE VIEW rpr_ev_alt_mid_seqtail AS
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A (B (C | D) | E) | F) G | H)
+    DEFINE A AS v % 8 = 0, B AS v % 8 = 1, C AS v % 8 = 2, D AS v % 8 = 3,
+           E AS v % 8 = 4, F AS v % 8 = 5, G AS v % 8 = 6, H AS v % 8 = 7
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_mid_seqtail'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 20) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A (B (C | D) | E) | F) G | H)
+    DEFINE A AS v % 8 = 0, B AS v % 8 = 1, C AS v % 8 = 2, D AS v % 8 = 3,
+           E AS v % 8 = 4, F AS v % 8 = 5, G AS v % 8 = 6, H AS v % 8 = 7
+);');
+
 -- ============================================================
 -- Group Pattern Tests
 -- ============================================================
-- 
2.50.1 (Apple Git-155)


From 37ae9552adb03842984f4305b9b72bb26af3591d Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Sun, 7 Jun 2026 18:27:16 +0900
Subject: [PATCH 56/68] Reject invalid column references in row pattern DEFINE
 clauses

A DEFINE expression could reference an outer query's column (a correlated
reference), which surfaced as the internal "Upper-level Var found where not
expected" error (XX000) raised by pull_var_clause.  Separately, a schema- or
catalog-qualified column reference (three or more name parts) slipped past
the existing two-part classifier and was wrongly accepted.

Reject both in transformColumnRef, right after the reference is resolved: an
outer-level Var (varlevelsup > 0) with ERRCODE_FEATURE_NOT_SUPPORTED, and a
reference with three or more name parts with ERRCODE_SYNTAX_ERROR.  The latter
uses "qualified expression" wording because a composite-typed qualifier
(e.g. "a.items" in (a.items).amount) is not a column name.
---
 src/backend/parser/parse_expr.c   | 24 ++++++++++
 src/test/regress/expected/rpr.out | 74 +++++++++++++++++++++++++++++++
 src/test/regress/sql/rpr.sql      | 61 +++++++++++++++++++++++++
 3 files changed, 159 insertions(+)

diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 2344aaef9ae..f65a270d20e 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -947,6 +947,30 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 		}
 	}
 
+	/*
+	 * Restrict column references in a row pattern DEFINE clause.  node is now
+	 * a successfully resolved reference, so reject the two forms RPR does not
+	 * allow: a correlated reference to an outer query's column, and a
+	 * schema/catalog-qualified reference (three or more name parts).  Simple
+	 * two-part qualifiers (pattern or range variable) are handled earlier,
+	 * before resolution.
+	 */
+	if (pstate->p_expr_kind == EXPR_KIND_RPR_DEFINE)
+	{
+		if (IsA(node, Var) && ((Var *) node)->varlevelsup > 0)
+			ereport(ERROR,
+					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					errmsg("cannot use outer query column in DEFINE clause"),
+					parser_errposition(pstate, cref->location));
+
+		if (list_length(cref->fields) >= 3)
+			ereport(ERROR,
+					errcode(ERRCODE_SYNTAX_ERROR),
+					errmsg("qualified expression \"%s\" is not allowed in DEFINE clause",
+						   NameListToString(cref->fields)),
+					parser_errposition(pstate, cref->location));
+	}
+
 	return node;
 }
 
diff --git a/src/test/regress/expected/rpr.out b/src/test/regress/expected/rpr.out
index 550113700a9..1b409b923dd 100644
--- a/src/test/regress/expected/rpr.out
+++ b/src/test/regress/expected/rpr.out
@@ -1295,6 +1295,80 @@ ERROR:  volatile functions are not allowed in DEFINE clause
 LINE 7:     DEFINE A AS price > nextval('rpr_seq')
                                 ^
 DROP SEQUENCE rpr_seq;
+-- DEFINE cannot reference an outer query's column.  A correlated outer
+-- reference must produce a clean error, not the internal "Upper-level Var"
+-- elog that pull_var_clause would otherwise raise.
+-- Qualified outer reference (o.threshold):
+SELECT * FROM (VALUES (95)) AS o(threshold),
+LATERAL (
+    SELECT price FROM stock
+    WINDOW w AS (
+        PARTITION BY company
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        INITIAL
+        PATTERN (A)
+        DEFINE A AS price > o.threshold
+    )
+) s;
+ERROR:  cannot use outer query column in DEFINE clause
+LINE 9:         DEFINE A AS price > o.threshold
+                                    ^
+-- Unqualified name resolving to the outer column (threshold):
+SELECT * FROM (VALUES (95)) AS o(threshold),
+LATERAL (
+    SELECT price FROM stock
+    WINDOW w AS (
+        PARTITION BY company
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        INITIAL
+        PATTERN (A)
+        DEFINE A AS price > threshold
+    )
+) s;
+ERROR:  cannot use outer query column in DEFINE clause
+LINE 9:         DEFINE A AS price > threshold
+                                    ^
+-- Outer reference inside a navigation argument is rejected too:
+SELECT * FROM (VALUES (95)) AS o(threshold),
+LATERAL (
+    SELECT price FROM stock
+    WINDOW w AS (
+        PARTITION BY company ORDER BY tdate
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A+)
+        DEFINE A AS PREV(o.threshold, 1) > 0
+    )
+) s;
+ERROR:  cannot use outer query column in DEFINE clause
+LINE 8:         DEFINE A AS PREV(o.threshold, 1) > 0
+                                 ^
+-- DEFINE rejects a schema-qualified column reference (three or more name
+-- parts) once it resolves; the qualified form itself is not allowed.  (stock
+-- is a temp table, so it is qualified with pg_temp here.)
+-- 3-part (schema.table.column):
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS pg_temp.stock.price > 0
+);
+ERROR:  qualified expression "pg_temp.stock.price" is not allowed in DEFINE clause
+LINE 7:     DEFINE A AS pg_temp.stock.price > 0
+                        ^
+-- whole-row variant (schema.table.*):
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS (pg_temp.stock.*) IS NOT NULL
+);
+ERROR:  qualified expression "pg_temp.stock.*" is not allowed in DEFINE clause
+LINE 7:     DEFINE A AS (pg_temp.stock.*) IS NOT NULL
+                         ^
 --
 -- 2-arg PREV/NEXT: functional tests
 --
diff --git a/src/test/regress/sql/rpr.sql b/src/test/regress/sql/rpr.sql
index 0aa17f01e84..56dff9b6725 100644
--- a/src/test/regress/sql/rpr.sql
+++ b/src/test/regress/sql/rpr.sql
@@ -667,6 +667,67 @@ WINDOW w AS (
 );
 DROP SEQUENCE rpr_seq;
 
+-- DEFINE cannot reference an outer query's column.  A correlated outer
+-- reference must produce a clean error, not the internal "Upper-level Var"
+-- elog that pull_var_clause would otherwise raise.
+-- Qualified outer reference (o.threshold):
+SELECT * FROM (VALUES (95)) AS o(threshold),
+LATERAL (
+    SELECT price FROM stock
+    WINDOW w AS (
+        PARTITION BY company
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        INITIAL
+        PATTERN (A)
+        DEFINE A AS price > o.threshold
+    )
+) s;
+-- Unqualified name resolving to the outer column (threshold):
+SELECT * FROM (VALUES (95)) AS o(threshold),
+LATERAL (
+    SELECT price FROM stock
+    WINDOW w AS (
+        PARTITION BY company
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        INITIAL
+        PATTERN (A)
+        DEFINE A AS price > threshold
+    )
+) s;
+-- Outer reference inside a navigation argument is rejected too:
+SELECT * FROM (VALUES (95)) AS o(threshold),
+LATERAL (
+    SELECT price FROM stock
+    WINDOW w AS (
+        PARTITION BY company ORDER BY tdate
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A+)
+        DEFINE A AS PREV(o.threshold, 1) > 0
+    )
+) s;
+
+-- DEFINE rejects a schema-qualified column reference (three or more name
+-- parts) once it resolves; the qualified form itself is not allowed.  (stock
+-- is a temp table, so it is qualified with pg_temp here.)
+-- 3-part (schema.table.column):
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS pg_temp.stock.price > 0
+);
+-- whole-row variant (schema.table.*):
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS (pg_temp.stock.*) IS NOT NULL
+);
+
 --
 -- 2-arg PREV/NEXT: functional tests
 --
-- 
2.50.1 (Apple Git-155)


From fcb8bccafe66cf7f1ded30e5237bb157a94c82e5 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Sun, 7 Jun 2026 20:38:00 +0900
Subject: [PATCH 58/68] Compare varno when preserving DEFINE-referenced columns
 in row pattern recognition

remove_unused_subquery_outputs keeps subquery output columns that an RPR
DEFINE clause references.  The check compared only varattno, so an unrelated
output column of a different relation that happened to share an attribute
number was over-retained and propagated through the plan.

Also compare varno (and, as paranoia, varlevelsup), matching the pattern
already used in set_function_pathlist.  The result was already correct; this
just stops retaining the needless column.
---
 src/backend/optimizer/path/allpaths.c         | 13 ++++++-
 src/test/regress/expected/rpr_integration.out | 35 +++++++++++++++++++
 src/test/regress/sql/rpr_integration.sql      | 16 +++++++++
 3 files changed, 63 insertions(+), 1 deletion(-)

diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c
index fb0506933cd..22339f7491f 100644
--- a/src/backend/optimizer/path/allpaths.c
+++ b/src/backend/optimizer/path/allpaths.c
@@ -4776,7 +4776,18 @@ remove_unused_subquery_outputs(Query *subquery, RelOptInfo *rel,
 					{
 						Var		   *dvar = (Var *) lfirst(vlc);
 
-						if (dvar->varattno == var->varattno)
+						/*
+						 * Match varno as well as varattno: a Var pulled from
+						 * a DEFINE clause can share an attribute number with
+						 * an unrelated output column of a different relation,
+						 * which would otherwise be over-retained.  Checking
+						 * varlevelsup is just paranoia, since outer
+						 * references in DEFINE are rejected during parse
+						 * analysis.
+						 */
+						if (dvar->varno == var->varno &&
+							dvar->varattno == var->varattno &&
+							dvar->varlevelsup == var->varlevelsup)
 						{
 							needed_by_define = true;
 							break;
diff --git a/src/test/regress/expected/rpr_integration.out b/src/test/regress/expected/rpr_integration.out
index b598ef95776..2133e2dfe13 100644
--- a/src/test/regress/expected/rpr_integration.out
+++ b/src/test/regress/expected/rpr_integration.out
@@ -1481,6 +1481,41 @@ ORDER BY o.id;
  10 |  45 |         2
 (10 rows)
 
+-- A column referenced only by DEFINE must not keep an unrelated column that
+-- merely shares its attribute number.  DEFINE references a (rpr_over1); c
+-- (rpr_over2) has the same attno but is unused, so it must be dropped.
+CREATE TABLE rpr_over1 (a int);
+CREATE TABLE rpr_over2 (c int);
+INSERT INTO rpr_over1 VALUES (1),(2),(3);
+INSERT INTO rpr_over2 VALUES (1),(2),(3);
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT cnt FROM (
+  SELECT a AS oa, c AS oc, count(*) OVER w AS cnt
+  FROM rpr_over1 CROSS JOIN rpr_over2
+  WINDOW w AS (ORDER BY a ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+               PATTERN (X+) DEFINE X AS a > 0)
+) s;
+                                          QUERY PLAN                                          
+----------------------------------------------------------------------------------------------
+ Subquery Scan on s
+   Output: s.cnt
+   ->  WindowAgg
+         Output: rpr_over1.a, NULL::integer, count(*) OVER w
+         Window: w AS (ORDER BY rpr_over1.a ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+         Pattern: x+"
+         Nav Mark Lookback: 0
+         ->  Sort
+               Output: rpr_over1.a
+               Sort Key: rpr_over1.a
+               ->  Nested Loop
+                     Output: rpr_over1.a
+                     ->  Seq Scan on public.rpr_over1
+                           Output: rpr_over1.a
+                     ->  Materialize
+                           ->  Seq Scan on public.rpr_over2
+(16 rows)
+
+DROP TABLE rpr_over1, rpr_over2;
 -- Cleanup
 DROP TABLE rpr_integ;
 DROP TABLE rpr_integ2;
diff --git a/src/test/regress/sql/rpr_integration.sql b/src/test/regress/sql/rpr_integration.sql
index 5f3853becba..24b0b1811b9 100644
--- a/src/test/regress/sql/rpr_integration.sql
+++ b/src/test/regress/sql/rpr_integration.sql
@@ -929,6 +929,22 @@ SELECT o.id, o.val,
 FROM rpr_integ o
 ORDER BY o.id;
 
+-- A column referenced only by DEFINE must not keep an unrelated column that
+-- merely shares its attribute number.  DEFINE references a (rpr_over1); c
+-- (rpr_over2) has the same attno but is unused, so it must be dropped.
+CREATE TABLE rpr_over1 (a int);
+CREATE TABLE rpr_over2 (c int);
+INSERT INTO rpr_over1 VALUES (1),(2),(3);
+INSERT INTO rpr_over2 VALUES (1),(2),(3);
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT cnt FROM (
+  SELECT a AS oa, c AS oc, count(*) OVER w AS cnt
+  FROM rpr_over1 CROSS JOIN rpr_over2
+  WINDOW w AS (ORDER BY a ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+               PATTERN (X+) DEFINE X AS a > 0)
+) s;
+DROP TABLE rpr_over1, rpr_over2;
+
 -- Cleanup
 DROP TABLE rpr_integ;
 DROP TABLE rpr_integ2;
-- 
2.50.1 (Apple Git-155)


From 426451992a4a865203f4fed65a5d38839bc1402b Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Sun, 7 Jun 2026 19:38:16 +0900
Subject: [PATCH 57/68] Fix shortest match for reluctant nullable quantifiers
 in row pattern recognition

When a reluctant outer quantifier wrapped a nullable reluctant body, such as
(A??)+?, the match consumed rows instead of producing the required shortest
(empty) match.

nfa_advance_end decides a group's repeat-or-exit by comparing count with the
quantifier's min and max.  The count < min branch always routed the loop-back
(real match) before the fast-forward exit and never suppressed it, so a longer
match could replace the shortest one.  The sibling min <= count < max branch
already handles this correctly for reluctant groups, leaving the two branches
asymmetric.

Split the count < min branch into reluctant and greedy cases, mirroring the
sibling: a reluctant group takes the fast-forward exit first and, if it reaches
FIN, frees the loop-back state so a longer match cannot replace the shortest
one.  Greedy and non-nullable groups keep the existing loop-first behaviour.
---
 src/backend/executor/execRPR.c        | 72 +++++++++++++++++----------
 src/test/regress/expected/rpr_nfa.out | 33 ++++++++++++
 src/test/regress/sql/rpr_nfa.sql      | 26 ++++++++++
 3 files changed, 106 insertions(+), 25 deletions(-)

diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 69e3603adef..b7e3c4a8274 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -1139,43 +1139,32 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
 	{
 		RPRPatternElement *jumpElem;
 		RPRNFAState *ffState = NULL;
+		RPRPatternElement *nextElem = NULL;
 
 		/*----------
-		 * Two paths are explored in parallel when the group body is nullable
+		 * Two paths are explored when the group body is nullable
 		 * (RPR_ELEM_EMPTY_LOOP):
 		 *
-		 * 1. Primary path: loop back and attempt real matches in the
-		 *    next iteration (state, modified below).
+		 * 1. Loop-back path: attempt real matches in the next iteration
+		 *    (state, modified below).
 		 *
-		 * 2. Fast-forward path: skip directly to after the group,
-		 *    treating all remaining required iterations as empty
-		 *    matches (ffState, handled after the primary path).
+		 * 2. Fast-forward path: skip directly to after the group, treating
+		 *    all remaining required iterations as empty matches (ffState).
+		 *    Route to elem->next (not nfa_advance_end) to avoid creating
+		 *    competing greedy/reluctant loop states.
 		 *
-		 * The snapshot must be taken BEFORE modifying state for the loop-back,
-		 * since both paths diverge from the same point.
+		 * Greedy prefers the loop-back first (more iterations); reluctant
+		 * prefers the fast-forward (exit) first and, if it reaches FIN, drops
+		 * the loop-back so a longer match cannot replace the shortest one --
+		 * mirroring the min<=count<max branch below.  The ffState snapshot is
+		 * taken BEFORE modifying state, since both paths diverge from here.
 		 *----------
 		 */
 		if (RPRElemCanEmptyLoop(elem))
+		{
 			ffState = nfa_state_clone(winstate, state->elemIdx,
 									  state->counts, state->isAbsorbable);
 
-		/* Primary path: loop back for real matches */
-		state->elemIdx = elem->jump;
-		jumpElem = &elements[state->elemIdx];
-		nfa_route_to_elem(winstate, ctx, state, jumpElem,
-						  currentPos);
-
-		/*
-		 * Fast-forward path for nullable bodies.  E.g. (A?){2,3} when A
-		 * doesn't match: the primary loop-back produces empty iterations that
-		 * cycle detection would kill.  Instead, exit directly with count
-		 * satisfied.  Route to elem->next (not nfa_advance_end) to avoid
-		 * creating competing greedy/reluctant loop states.
-		 */
-		if (ffState != NULL)
-		{
-			RPRPatternElement *nextElem;
-
 			/* Exit the group: clear its own count (count-clear policy) */
 			ffState->counts[depth] = 0;
 			ffState->elemIdx = elem->next;
@@ -1192,9 +1181,42 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
 			if (RPRElemIsEnd(nextElem) &&
 				ffState->counts[nextElem->depth] < RPR_COUNT_MAX)
 				ffState->counts[nextElem->depth]++;
+		}
+
+		/* Prepare the loop-back state */
+		state->elemIdx = elem->jump;
+		jumpElem = &elements[state->elemIdx];
 
+		if (ffState != NULL && RPRElemIsReluctant(elem))
+		{
+			RPRNFAState *savedMatch = ctx->matchedState;
+
+			/* Reluctant: take the fast-forward (exit) first */
 			nfa_route_to_elem(winstate, ctx, ffState, nextElem,
 							  currentPos);
+
+			/*
+			 * If the exit reached FIN, the shortest match is found.  Skip the
+			 * loop-back to prevent longer matches from replacing it.
+			 */
+			if (ctx->matchedState != savedMatch)
+			{
+				nfa_state_free(winstate, state);
+				return;
+			}
+
+			/* Loop-back second */
+			nfa_route_to_elem(winstate, ctx, state, jumpElem,
+							  currentPos);
+		}
+		else
+		{
+			/* Greedy (or non-nullable): loop-back first, fast-forward second */
+			nfa_route_to_elem(winstate, ctx, state, jumpElem,
+							  currentPos);
+			if (ffState != NULL)
+				nfa_route_to_elem(winstate, ctx, ffState, nextElem,
+								  currentPos);
 		}
 	}
 	else if (elem->max != RPR_QUANTITY_INF && count >= elem->max)
diff --git a/src/test/regress/expected/rpr_nfa.out b/src/test/regress/expected/rpr_nfa.out
index 02a5e517b0e..3b9975a83df 100644
--- a/src/test/regress/expected/rpr_nfa.out
+++ b/src/test/regress/expected/rpr_nfa.out
@@ -1255,6 +1255,39 @@ WINDOW w AS (
   3 | {C}   |             |          
 (3 rows)
 
+-- Reluctant outer quantifier over a nullable reluctant body: SQL/RPR
+-- semantics call for the shortest (empty) match.  The count<min branch of
+-- nfa_advance_end must prefer the fast-forward (exit) path for reluctant
+-- groups and suppress longer matches once exit reaches FIN, mirroring the
+-- sibling min<=count<max branch.  The 2-level greedy/reluctant matrix plus a
+-- min>=2 boundary and single-quantifier controls localize the behaviour: only
+-- the all-reluctant case (rr) should differ.
+WITH t(id, isa) AS (VALUES (1, true), (2, true), (3, true), (4, false))
+SELECT id,
+       count(*) OVER gg  AS gg,     -- (A?)+      greedy / greedy
+       count(*) OVER gr  AS gr,     -- (A??)+     greedy / reluctant
+       count(*) OVER rg  AS rg,     -- (A?)+?     reluctant / greedy
+       count(*) OVER rr  AS rr,     -- (A??)+?    reluctant / reluctant
+       count(*) OVER rr2 AS rr2,    -- (A??){2,}? reluctant, min>=2 boundary
+       count(*) OVER ca  AS ca,     -- A??        single reluctant control
+       count(*) OVER cs  AS cs      -- A*?        single reluctant control
+FROM t
+WINDOW gg  AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A?)+)      DEFINE A AS isa),
+       gr  AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A??)+)     DEFINE A AS isa),
+       rg  AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A?)+?)     DEFINE A AS isa),
+       rr  AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A??)+?)    DEFINE A AS isa),
+       rr2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A??){2,}?) DEFINE A AS isa),
+       ca  AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A??)        DEFINE A AS isa),
+       cs  AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*?)        DEFINE A AS isa)
+ORDER BY id;
+ id | gg | gr | rg | rr | rr2 | ca | cs 
+----+----+----+----+----+-----+----+----
+  1 |  3 |  3 |  1 |  0 |   0 |  0 |  0
+  2 |  0 |  0 |  1 |  0 |   0 |  0 |  0
+  3 |  0 |  0 |  1 |  0 |   0 |  0 |  0
+  4 |  0 |  0 |  0 |  0 |   0 |  0 |  0
+(4 rows)
+
 -- Non-leading reluctant optional GROUP with a follower: (B (A X)?? C)
 -- Like the VAR case above but a multi-element group; it goes through the
 -- begin path (nfa_advance_begin), which already honors reluctant ordering.
diff --git a/src/test/regress/sql/rpr_nfa.sql b/src/test/regress/sql/rpr_nfa.sql
index 213385f143b..61072d1d6f1 100644
--- a/src/test/regress/sql/rpr_nfa.sql
+++ b/src/test/regress/sql/rpr_nfa.sql
@@ -887,6 +887,32 @@ WINDOW w AS (
         C AS 'C' = ANY(flags)
 );
 
+-- Reluctant outer quantifier over a nullable reluctant body: SQL/RPR
+-- semantics call for the shortest (empty) match.  The count<min branch of
+-- nfa_advance_end must prefer the fast-forward (exit) path for reluctant
+-- groups and suppress longer matches once exit reaches FIN, mirroring the
+-- sibling min<=count<max branch.  The 2-level greedy/reluctant matrix plus a
+-- min>=2 boundary and single-quantifier controls localize the behaviour: only
+-- the all-reluctant case (rr) should differ.
+WITH t(id, isa) AS (VALUES (1, true), (2, true), (3, true), (4, false))
+SELECT id,
+       count(*) OVER gg  AS gg,     -- (A?)+      greedy / greedy
+       count(*) OVER gr  AS gr,     -- (A??)+     greedy / reluctant
+       count(*) OVER rg  AS rg,     -- (A?)+?     reluctant / greedy
+       count(*) OVER rr  AS rr,     -- (A??)+?    reluctant / reluctant
+       count(*) OVER rr2 AS rr2,    -- (A??){2,}? reluctant, min>=2 boundary
+       count(*) OVER ca  AS ca,     -- A??        single reluctant control
+       count(*) OVER cs  AS cs      -- A*?        single reluctant control
+FROM t
+WINDOW gg  AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A?)+)      DEFINE A AS isa),
+       gr  AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A??)+)     DEFINE A AS isa),
+       rg  AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A?)+?)     DEFINE A AS isa),
+       rr  AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A??)+?)    DEFINE A AS isa),
+       rr2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A??){2,}?) DEFINE A AS isa),
+       ca  AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A??)        DEFINE A AS isa),
+       cs  AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*?)        DEFINE A AS isa)
+ORDER BY id;
+
 -- Non-leading reluctant optional GROUP with a follower: (B (A X)?? C)
 -- Like the VAR case above but a multi-element group; it goes through the
 -- begin path (nfa_advance_begin), which already honors reluctant ordering.
-- 
2.50.1 (Apple Git-155)


From fcd882526042fa7bec72c0658f02b8dddd2109af Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Mon, 8 Jun 2026 11:23:03 +0900
Subject: [PATCH 59/68] Allow a row pattern quantifier with no space before the
 alternation operator

The scanner lexes an operator such as "*|" as a single token, so a PATTERN
like (A*|B) previously failed with "unsupported quantifier".  Recognize these
glued forms in the quantifier rules and, once the surrounding sequence is
built, re-split it at the affected term so "|" keeps its lowest precedence;
(A*|B) now parses identically to the spaced (A* | B).  This covers the op-char
quantifiers (*| +| ?| *?| +?| ??|), the reluctant range forms ({n}?| and the
like), and mixed spacing such as "A* ?|B".  A dangling "|" with no right-hand
pattern is still rejected.
---
 src/backend/parser/gram.y              | 195 +++++++++++++++++++--
 src/backend/parser/parse_rpr.c         |   6 +
 src/include/nodes/parsenodes.h         |   8 +
 src/test/regress/expected/rpr_base.out | 228 +++++++++++++++++++++++++
 src/test/regress/sql/rpr_base.sql      | 112 ++++++++++++
 5 files changed, 535 insertions(+), 14 deletions(-)

diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index a2fafb717cd..147b5f37293 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -210,6 +210,8 @@ static void preprocess_pub_all_objtype_list(List *all_objects_list,
 static void preprocess_pubobj_list(List *pubobjspec_list,
 								   core_yyscan_t yyscanner);
 static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
+static RPRPatternNode *makeRPRSeqOrSingle(List *children, int location);
+static RPRPatternNode *splitRPRTrailingAlt(RPRPatternNode *node, core_yyscan_t yyscanner);
 static RPRPatternNode *makeRPRQuantifier(int min, int max, ParseLoc reluctant, int location,
 									   core_yyscan_t yyscanner);
 
@@ -17624,23 +17626,30 @@ row_pattern:
 		;
 
 row_pattern_alt:
-			row_pattern_seq						{ $$ = $1; }
+			row_pattern_seq
+				{
+					$$ = (Node *) splitRPRTrailingAlt((RPRPatternNode *) $1,
+													 yyscanner);
+				}
 			| row_pattern_alt '|' row_pattern_seq
 				{
 					RPRPatternNode *n;
+					RPRPatternNode *rhs = splitRPRTrailingAlt((RPRPatternNode *) $3,
+															 yyscanner);
+
 					/* If left side is already ALT, append to it */
 					if (IsA($1, RPRPatternNode) &&
 						((RPRPatternNode *) $1)->nodeType == RPR_PATTERN_ALT)
 					{
 						n = (RPRPatternNode *) $1;
-						n->children = lappend(n->children, $3);
+						n->children = lappend(n->children, rhs);
 						$$ = (Node *) n;
 					}
 					else
 					{
 						n = makeNode(RPRPatternNode);
 						n->nodeType = RPR_PATTERN_ALT;
-						n->children = list_make2($1, $3);
+						n->children = list_make2($1, rhs);
 						n->min = 1;
 						n->max = 1;
 						n->reluctant = false;
@@ -17656,7 +17665,12 @@ row_pattern_seq:
 			| row_pattern_seq row_pattern_term
 				{
 					RPRPatternNode *n;
-					/* If left side is already SEQ, append to it */
+
+					/*
+					 * If left side is already SEQ, append to it.  A glued
+					 * quantifier's trailing_alt stays on the child term;
+					 * row_pattern_alt splits on it once the seq is complete.
+					 */
 					if (IsA($1, RPRPatternNode) &&
 						((RPRPatternNode *) $1)->nodeType == RPR_PATTERN_SEQ)
 					{
@@ -17689,6 +17703,7 @@ row_pattern_term:
 					n->max = q->max;
 					n->reluctant = q->reluctant;
 					n->reluctant_location = q->reluctant_location;
+					n->trailing_alt = q->trailing_alt;
 					$$ = (Node *) n;
 				}
 		;
@@ -17739,6 +17754,36 @@ row_pattern_quantifier_opt:
 						$$ = (Node *) makeRPRQuantifier(1, INT_MAX, @1 + 1, @1, yyscanner);
 					else if (strcmp($1, "??") == 0)
 						$$ = (Node *) makeRPRQuantifier(0, 1, @1 + 1, @1, yyscanner);
+					else if (strcmp($1, "*|") == 0)
+					{
+						$$ = (Node *) makeRPRQuantifier(0, INT_MAX, -1, @1, yyscanner);
+						((RPRPatternNode *) $$)->trailing_alt = true;
+					}
+					else if (strcmp($1, "+|") == 0)
+					{
+						$$ = (Node *) makeRPRQuantifier(1, INT_MAX, -1, @1, yyscanner);
+						((RPRPatternNode *) $$)->trailing_alt = true;
+					}
+					else if (strcmp($1, "?|") == 0)
+					{
+						$$ = (Node *) makeRPRQuantifier(0, 1, -1, @1, yyscanner);
+						((RPRPatternNode *) $$)->trailing_alt = true;
+					}
+					else if (strcmp($1, "*?|") == 0)
+					{
+						$$ = (Node *) makeRPRQuantifier(0, INT_MAX, @1 + 1, @1, yyscanner);
+						((RPRPatternNode *) $$)->trailing_alt = true;
+					}
+					else if (strcmp($1, "+?|") == 0)
+					{
+						$$ = (Node *) makeRPRQuantifier(1, INT_MAX, @1 + 1, @1, yyscanner);
+						((RPRPatternNode *) $$)->trailing_alt = true;
+					}
+					else if (strcmp($1, "??|") == 0)
+					{
+						$$ = (Node *) makeRPRQuantifier(0, 1, @1 + 1, @1, yyscanner);
+						((RPRPatternNode *) $$)->trailing_alt = true;
+					}
 					else
 						ereport(ERROR,
 								errcode(ERRCODE_SYNTAX_ERROR),
@@ -17749,33 +17794,60 @@ row_pattern_quantifier_opt:
 			/* RELUCTANT quantifiers (when lexer separates tokens) */
 			| '*' Op
 				{
-					if (strcmp($2, "?") != 0)
+					if (strcmp($2, "?") == 0)
+						$$ = (Node *) makeRPRQuantifier(0, INT_MAX, @2, @1, yyscanner);
+					else if (strcmp($2, "?|") == 0)
+					{
+						/* "A* ?|B" = reluctant "A*?" plus alternation */
+						$$ = (Node *) makeRPRQuantifier(0, INT_MAX, @2, @1, yyscanner);
+						((RPRPatternNode *) $$)->trailing_alt = true;
+					}
+					else
 						ereport(ERROR,
 								errcode(ERRCODE_SYNTAX_ERROR),
 								errmsg("invalid token after \"*\" quantifier"),
 								errhint("Did you mean \"*?\" for reluctant quantifier?"),
 								parser_errposition(@2));
-					$$ = (Node *) makeRPRQuantifier(0, INT_MAX, @2, @1, yyscanner);
 				}
 			| '+' Op
 				{
-					if (strcmp($2, "?") != 0)
+					if (strcmp($2, "?") == 0)
+						$$ = (Node *) makeRPRQuantifier(1, INT_MAX, @2, @1, yyscanner);
+					else if (strcmp($2, "?|") == 0)
+					{
+						/* "A+ ?|B" = reluctant "A+?" plus alternation */
+						$$ = (Node *) makeRPRQuantifier(1, INT_MAX, @2, @1, yyscanner);
+						((RPRPatternNode *) $$)->trailing_alt = true;
+					}
+					else
 						ereport(ERROR,
 								errcode(ERRCODE_SYNTAX_ERROR),
 								errmsg("invalid token after \"+\" quantifier"),
 								errhint("Did you mean \"+?\" for reluctant quantifier?"),
 								parser_errposition(@2));
-					$$ = (Node *) makeRPRQuantifier(1, INT_MAX, @2, @1, yyscanner);
 				}
 			| Op Op
 				{
-					if (strcmp($1, "?") != 0 || strcmp($2, "?") != 0)
+					if (strcmp($1, "?") != 0)
+						ereport(ERROR,
+								errcode(ERRCODE_SYNTAX_ERROR),
+								errmsg("invalid quantifier combination"),
+								errhint("Did you mean \"??\" for reluctant quantifier?"),
+								parser_errposition(@1));
+					if (strcmp($2, "?") == 0)
+						$$ = (Node *) makeRPRQuantifier(0, 1, @2, @1, yyscanner);
+					else if (strcmp($2, "?|") == 0)
+					{
+						/* "A? ?|B" = reluctant "A??" plus alternation */
+						$$ = (Node *) makeRPRQuantifier(0, 1, @2, @1, yyscanner);
+						((RPRPatternNode *) $$)->trailing_alt = true;
+					}
+					else
 						ereport(ERROR,
 								errcode(ERRCODE_SYNTAX_ERROR),
 								errmsg("invalid quantifier combination"),
 								errhint("Did you mean \"??\" for reluctant quantifier?"),
 								parser_errposition(@1));
-					$$ = (Node *) makeRPRQuantifier(0, 1, @2, @1, yyscanner);
 				}
 			/* {n}, {n,}, {,m}, {n,m} quantifiers */
 			| '{' Iconst '}'
@@ -17822,7 +17894,7 @@ row_pattern_quantifier_opt:
 			/* Reluctant versions: {n}?, {n,}?, {,m}?, {n,m}? */
 			| '{' Iconst '}' Op
 				{
-					if (strcmp($4, "?") != 0)
+					if (strcmp($4, "?") != 0 && strcmp($4, "?|") != 0)
 						ereport(ERROR,
 								errcode(ERRCODE_SYNTAX_ERROR),
 								errmsg("invalid token after range quantifier"),
@@ -17834,10 +17906,12 @@ row_pattern_quantifier_opt:
 								errmsg("quantifier bound must be between 1 and %d", INT_MAX - 1),
 								parser_errposition(@2));
 					$$ = (Node *) makeRPRQuantifier($2, $2, @4, @1, yyscanner);
+					if (strcmp($4, "?|") == 0)
+						((RPRPatternNode *) $$)->trailing_alt = true;
 				}
 			| '{' Iconst ',' '}' Op
 				{
-					if (strcmp($5, "?") != 0)
+					if (strcmp($5, "?") != 0 && strcmp($5, "?|") != 0)
 						ereport(ERROR,
 								errcode(ERRCODE_SYNTAX_ERROR),
 								errmsg("invalid token after range quantifier"),
@@ -17849,10 +17923,12 @@ row_pattern_quantifier_opt:
 								errmsg("quantifier bound must be between 0 and %d", INT_MAX - 1),
 								parser_errposition(@2));
 					$$ = (Node *) makeRPRQuantifier($2, INT_MAX, @5, @1, yyscanner);
+					if (strcmp($5, "?|") == 0)
+						((RPRPatternNode *) $$)->trailing_alt = true;
 				}
 			| '{' ',' Iconst '}' Op
 				{
-					if (strcmp($5, "?") != 0)
+					if (strcmp($5, "?") != 0 && strcmp($5, "?|") != 0)
 						ereport(ERROR,
 								errcode(ERRCODE_SYNTAX_ERROR),
 								errmsg("invalid token after range quantifier"),
@@ -17864,10 +17940,12 @@ row_pattern_quantifier_opt:
 								errmsg("quantifier bound must be between 1 and %d", INT_MAX - 1),
 								parser_errposition(@3));
 					$$ = (Node *) makeRPRQuantifier(0, $3, @5, @1, yyscanner);
+					if (strcmp($5, "?|") == 0)
+						((RPRPatternNode *) $$)->trailing_alt = true;
 				}
 			| '{' Iconst ',' Iconst '}' Op
 				{
-					if (strcmp($6, "?") != 0)
+					if (strcmp($6, "?") != 0 && strcmp($6, "?|") != 0)
 						ereport(ERROR,
 								errcode(ERRCODE_SYNTAX_ERROR),
 								errmsg("invalid token after range quantifier"),
@@ -17884,6 +17962,8 @@ row_pattern_quantifier_opt:
 								errmsg("quantifier minimum bound must not exceed maximum"),
 								parser_errposition(@2));
 					$$ = (Node *) makeRPRQuantifier($2, $4, @6, @1, yyscanner);
+					if (strcmp($6, "?|") == 0)
+						((RPRPatternNode *) $$)->trailing_alt = true;
 				}
 		;
 
@@ -21325,6 +21405,93 @@ makeRPRQuantifier(int min, int max, ParseLoc reluctant_location, int location,
 	return n;
 }
 
+/*
+ * Build a SEQ node from children, or return the lone child unchanged.
+ */
+static RPRPatternNode *
+makeRPRSeqOrSingle(List *children, int location)
+{
+	RPRPatternNode *n;
+
+	if (list_length(children) == 1)
+		return (RPRPatternNode *) linitial(children);
+
+	n = makeNode(RPRPatternNode);
+	n->nodeType = RPR_PATTERN_SEQ;
+	n->children = children;
+	n->min = 1;
+	n->max = 1;
+	n->reluctant = false;
+	n->reluctant_location = -1;
+	n->location = location;
+	return n;
+}
+
+/*
+ * A glued quantifier such as "A*|" leaves trailing_alt set on its term while
+ * the enclosing sequence is built.  Once the sequence is complete, split it at
+ * the flagged term into alt(left, right), where the right operand is the whole
+ * remaining sequence -- this keeps "|" as the lowest-precedence operator, so
+ * "A*|B C" parses as "A* | (B C)", identical to the spaced form.  A flag with
+ * nothing to its right is a dangling "|" and is rejected.
+ */
+static RPRPatternNode *
+splitRPRTrailingAlt(RPRPatternNode *node, core_yyscan_t yyscanner)
+{
+	ListCell   *lc;
+	int			i = 0;
+
+	if (node->nodeType != RPR_PATTERN_SEQ)
+	{
+		if (node->trailing_alt)
+		{
+			node->trailing_alt = false;
+			ereport(ERROR,
+					errcode(ERRCODE_SYNTAX_ERROR),
+					errmsg("alternation operator \"|\" requires a pattern on both sides"),
+					parser_errposition(node->location));
+		}
+		return node;
+	}
+
+	foreach(lc, node->children)
+	{
+		RPRPatternNode *child = (RPRPatternNode *) lfirst(lc);
+
+		if (child->trailing_alt)
+		{
+			List	   *lefthalf = list_copy_head(node->children, i + 1);
+			List	   *righthalf = list_copy_tail(node->children, i + 1);
+			RPRPatternNode *altn;
+			RPRPatternNode *rightnode;
+
+			child->trailing_alt = false;
+			if (righthalf == NIL)
+				ereport(ERROR,
+						errcode(ERRCODE_SYNTAX_ERROR),
+						errmsg("alternation operator \"|\" requires a pattern on both sides"),
+						parser_errposition(node->location));
+
+			/* the right branch starts at its own first element, not the seq start */
+			rightnode = splitRPRTrailingAlt(makeRPRSeqOrSingle(righthalf,
+															   ((RPRPatternNode *) linitial(righthalf))->location),
+											yyscanner);
+			altn = makeNode(RPRPatternNode);
+			altn->nodeType = RPR_PATTERN_ALT;
+			altn->children = list_make2(makeRPRSeqOrSingle(lefthalf, node->location),
+										rightnode);
+			altn->min = 1;
+			altn->max = 1;
+			altn->reluctant = false;
+			altn->reluctant_location = -1;
+			altn->location = node->location;
+			return altn;
+		}
+		i++;
+	}
+	return node;
+}
+
 /* parser_init()
  * Initialize to parse one query string
  */
diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index c9469b56b7b..4e1d2650cf6 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -213,6 +213,12 @@ validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
 	/* Pattern node must exist - parser always provides non-NULL root */
 	Assert(node != NULL);
 
+	/*
+	 * trailing_alt is a transient grammar flag; splitRPRTrailingAlt must have
+	 * cleared it on every node before the pattern reaches parse analysis.
+	 */
+	Assert(!node->trailing_alt);
+
 	check_stack_depth();
 
 	switch (node->nodeType)
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 5200182aa46..e371f04a403 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -632,6 +632,14 @@ typedef struct RPRPatternNode
 	ParseLoc	location;		/* token location, or -1 */
 	char	   *varName;		/* VAR: variable name */
 	List	   *children;		/* SEQ, ALT, GROUP: child nodes */
+
+	/*
+	 * Transient parse flag, cleared by splitRPRTrailingAlt before the tree is
+	 * finalized: a glued quantifier such as "*|" is immediately followed by
+	 * the alternation operator '|'.  It is always false in a finalized tree,
+	 * so query_jumble_ignore keeps it off the pg_stat_statements queryid.
+	 */
+	bool		trailing_alt pg_node_attr(query_jumble_ignore);
 } RPRPatternNode;
 
 /*
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index c50c7a5f6a8..1fcb2ce22f0 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -13,6 +13,7 @@
 --   Navigation Functions Tests
 --   SKIP TO / INITIAL Tests
 --   Serialization/Deserialization Tests (objects kept for pg_upgrade/pg_dump)
+--   Glued Quantifier / Alternation Tests
 --   Error Cases Tests
 --   Window Deduplication Tests
 --
@@ -3083,6 +3084,233 @@ SELECT pg_get_viewdef('rpr_quant_n_plus_v'::regclass);
    a AS (val > 0) );
 (1 row)
 
+-- ============================================================
+-- Glued Quantifier / Alternation Tests
+-- ============================================================
+CREATE TABLE rpr_glue (id INT, val INT);
+INSERT INTO rpr_glue VALUES (1, 5), (2, 8), (3, 9), (4, -1), (5, 6), (6, -2);
+-- Quantifier glued to the alternation operator '|' without a space (0059).
+-- The lexer glues the trailing '|' into one Op token; the grammar reattaches it
+-- as the lowest-precedence alternation once the surrounding sequence is built.
+-- Deparse is canonical, so the glued, spaced, and mixed-spacing forms all
+-- reduce to the same PATTERN -- one deparse per shape proves the parse tree.
+-- Op-char quantifiers (*, +, ?, *?, +?, ??) glued to '|'.
+CREATE VIEW rpr_dp_op AS SELECT
+    count(*) OVER w1 AS w1, count(*) OVER w2 AS w2, count(*) OVER w3 AS w3,
+    count(*) OVER w4 AS w4, count(*) OVER w5 AS w5, count(*) OVER w6 AS w6
+FROM rpr_glue
+WINDOW w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|B) DEFINE A AS val > 0, B AS val <= 0),
+       w2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+|B) DEFINE A AS val > 0, B AS val <= 0),
+       w3 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A?|B) DEFINE A AS val > 0, B AS val <= 0),
+       w4 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*?|B) DEFINE A AS val > 0, B AS val <= 0),
+       w5 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+?|B) DEFINE A AS val > 0, B AS val <= 0),
+       w6 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A??|B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_dp_op'), E'\n')) AS line WHERE line ~ 'PATTERN';
+         line         
+----------------------
+   PATTERN (a* | b) 
+   PATTERN (a+ | b) 
+   PATTERN (a? | b) 
+   PATTERN (a*? | b) 
+   PATTERN (a+? | b) 
+   PATTERN (a?? | b) 
+(6 rows)
+
+DROP VIEW rpr_dp_op;
+-- Spaced reference: the fully-spaced canonical forms.  Identical deparse to the
+-- glued rpr_dp_op w1/w4 above completes the glued = spaced = mixed equivalence.
+CREATE VIEW rpr_dp_spc AS SELECT count(*) OVER w1 AS w1, count(*) OVER w2 AS w2
+FROM rpr_glue
+WINDOW w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A* | B) DEFINE A AS val > 0, B AS val <= 0),
+       w2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*? | B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_dp_spc'), E'\n')) AS line WHERE line ~ 'PATTERN';
+         line         
+----------------------
+   PATTERN (a* | b) 
+   PATTERN (a*? | b) 
+(2 rows)
+
+DROP VIEW rpr_dp_spc;
+-- Range quantifiers glued to '|': non-reluctant {n}| (} + char '|') and
+-- reluctant {n}?| (} + Op "?|").
+CREATE VIEW rpr_dp_rng AS SELECT
+    count(*) OVER w1 AS w1, count(*) OVER w2 AS w2, count(*) OVER w3 AS w3, count(*) OVER w4 AS w4,
+    count(*) OVER w5 AS w5, count(*) OVER w6 AS w6, count(*) OVER w7 AS w7, count(*) OVER w8 AS w8
+FROM rpr_glue
+WINDOW w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2}|B) DEFINE A AS val > 0, B AS val <= 0),
+       w2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2,}|B) DEFINE A AS val > 0, B AS val <= 0),
+       w3 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{,3}|B) DEFINE A AS val > 0, B AS val <= 0),
+       w4 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2,3}|B) DEFINE A AS val > 0, B AS val <= 0),
+       w5 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2}?|B) DEFINE A AS val > 0, B AS val <= 0),
+       w6 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2,}?|B) DEFINE A AS val > 0, B AS val <= 0),
+       w7 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{,3}?|B) DEFINE A AS val > 0, B AS val <= 0),
+       w8 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2,3}?|B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_dp_rng'), E'\n')) AS line WHERE line ~ 'PATTERN';
+           line           
+--------------------------
+   PATTERN (a{2} | b) 
+   PATTERN (a{2,} | b) 
+   PATTERN (a{0,3} | b) 
+   PATTERN (a{2,3} | b) 
+   PATTERN (a{2}? | b) 
+   PATTERN (a{2,}? | b) 
+   PATTERN (a{0,3}? | b) 
+   PATTERN (a{2,3}? | b) 
+(8 rows)
+
+DROP VIEW rpr_dp_rng;
+-- Mixed spacing: a space inside the quantifier with '|' still glued.
+-- "A* ?|B" = '*' + Op"?|" = reluctant "A*?" plus alternation.
+CREATE VIEW rpr_dp_mix AS SELECT count(*) OVER w1 AS w1, count(*) OVER w2 AS w2, count(*) OVER w3 AS w3
+FROM rpr_glue
+WINDOW w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A* ?|B) DEFINE A AS val > 0, B AS val <= 0),
+       w2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+ ?|B) DEFINE A AS val > 0, B AS val <= 0),
+       w3 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A? ?|B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_dp_mix'), E'\n')) AS line WHERE line ~ 'PATTERN';
+         line         
+----------------------
+   PATTERN (a*? | b) 
+   PATTERN (a+? | b) 
+   PATTERN (a?? | b) 
+(3 rows)
+
+DROP VIEW rpr_dp_mix;
+-- Structure: precedence (| is lowest, so its right operand is the whole
+-- following sequence), chaining, concatenation, and grouping.
+CREATE VIEW rpr_dp_struct AS SELECT
+    count(*) OVER w1 AS w1, count(*) OVER w2 AS w2, count(*) OVER w3 AS w3,
+    count(*) OVER w4 AS w4, count(*) OVER w5 AS w5, count(*) OVER w6 AS w6
+FROM rpr_glue
+WINDOW w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|B C) DEFINE A AS val > 0, B AS val <= 0, C AS val < 100),
+       w2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|B*|C) DEFINE A AS val > 0, B AS val <= 0, C AS val < 100),
+       w3 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A B*|C D) DEFINE A AS val > 0, B AS val <= 0, C AS val < 100, D AS val > 5),
+       w4 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A*|B)) DEFINE A AS val > 0, B AS val <= 0),
+       w5 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|(B|C)) DEFINE A AS val > 0, B AS val <= 0, C AS val < 100),
+       w6 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A*|B)+) DEFINE A AS val > 0, B AS val <= 0);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_dp_struct'), E'\n')) AS line WHERE line ~ 'PATTERN';
+           line            
+---------------------------
+   PATTERN (a* | b c) 
+   PATTERN (a* | b* | c) 
+   PATTERN (a b* | c d) 
+   PATTERN ((a* | b)) 
+   PATTERN (a* | (b | c)) 
+   PATTERN ((a* | b)+) 
+(6 rows)
+
+DROP VIEW rpr_dp_struct;
+-- Execution semantics (deparse cannot show reluctant shortest-match).  The
+-- rpr_glue rows -- an A-run followed by B rows -- make the '|B' alternative
+-- reachable: with "*" the greedy form matches the whole run while the
+-- reluctant form matches empty; with "+" the greedy form matches the run and
+-- the reluctant form matches one row, and on a B row (where "A+" fails) the B
+-- alternative fires.
+SELECT id, val,
+       count(*) OVER gs AS gstar, count(*) OVER rs AS rstar,
+       count(*) OVER gp AS gplus, count(*) OVER rp AS rplus
+FROM rpr_glue
+WINDOW gs AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|B) DEFINE A AS val > 0, B AS val <= 0),
+       rs AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*?|B) DEFINE A AS val > 0, B AS val <= 0),
+       gp AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+|B) DEFINE A AS val > 0, B AS val <= 0),
+       rp AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+?|B) DEFINE A AS val > 0, B AS val <= 0)
+ORDER BY id;
+ id | val | gstar | rstar | gplus | rplus 
+----+-----+-------+-------+-------+-------
+  1 |   5 |     3 |     0 |     3 |     1
+  2 |   8 |     0 |     0 |     0 |     1
+  3 |   9 |     0 |     0 |     0 |     1
+  4 |  -1 |     1 |     1 |     1 |     1
+  5 |   6 |     1 |     0 |     1 |     1
+  6 |  -2 |     1 |     1 |     1 |     1
+(6 rows)
+
+-- Patterns that must stay rejected.  "&" is an invalid op; a '|' with an empty
+-- side (leading, trailing, doubled, or alone in a group) has no operand; "||"
+-- and "*||" are doubled pipes; "A* *|B"/"A* *?|B"/"A{2}*?|B" are doubled
+-- quantifiers.
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A&B) DEFINE A AS val > 0);
+ERROR:  unsupported quantifier "&"
+LINE 1: ...EEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A&B) DEFINE...
+                                                             ^
+HINT:  Valid quantifiers are: *, +, ?, *?, +?, ??, {n}, {n,}, {,m}, {n,m} and their reluctant versions.
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|) DEFINE A AS val > 0);
+ERROR:  alternation operator "|" requires a pattern on both sides
+LINE 1: ...WEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|) DEFIN...
+                                                             ^
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*| |B) DEFINE A AS val > 0, B AS val <= 0);
+ERROR:  alternation operator "|" requires a pattern on both sides
+LINE 1: ...WEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*| |B) DE...
+                                                             ^
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*||B) DEFINE A AS val > 0, B AS val <= 0);
+ERROR:  unsupported quantifier "*||"
+LINE 1: ...EEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*||B) DEFI...
+                                                             ^
+HINT:  Valid quantifiers are: *, +, ?, *?, +?, ??, {n}, {n,}, {,m}, {n,m} and their reluctant versions.
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A||B) DEFINE A AS val > 0, B AS val <= 0);
+ERROR:  unsupported quantifier "||"
+LINE 1: ...EEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A||B) DEFIN...
+                                                             ^
+HINT:  Valid quantifiers are: *, +, ?, *?, +?, ??, {n}, {n,}, {,m}, {n,m} and their reluctant versions.
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|B|) DEFINE A AS val > 0, B AS val <= 0);
+ERROR:  syntax error at or near ")"
+LINE 1: ...CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|B|) DEFINE A...
+                                                             ^
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (|A) DEFINE A AS val > 0);
+ERROR:  syntax error at or near "|"
+LINE 1: ...WEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (|A) DEFINE...
+                                                             ^
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A*|)) DEFINE A AS val > 0);
+ERROR:  alternation operator "|" requires a pattern on both sides
+LINE 1: ...EEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A*|)) DEFI...
+                                                             ^
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A* *|B) DEFINE A AS val > 0, B AS val <= 0);
+ERROR:  invalid token after "*" quantifier
+LINE 1: ...N CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A* *|B) DEFIN...
+                                                             ^
+HINT:  Did you mean "*?" for reluctant quantifier?
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A* *?|B) DEFINE A AS val > 0, B AS val <= 0);
+ERROR:  invalid token after "*" quantifier
+LINE 1: ...N CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A* *?|B) DEFI...
+                                                             ^
+HINT:  Did you mean "*?" for reluctant quantifier?
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A? *?|B) DEFINE A AS val > 0, B AS val <= 0);
+ERROR:  invalid quantifier combination
+LINE 1: ...EEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A? *?|B) DE...
+                                                             ^
+HINT:  Did you mean "??" for reluctant quantifier?
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2}*?|B) DEFINE A AS val > 0, B AS val <= 0);
+ERROR:  invalid token after range quantifier
+LINE 1: ... CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2}*?|B) DEFI...
+                                                             ^
+HINT:  Only "?" is allowed after {n} to make it reluctant.
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2} *?|B) DEFINE A AS val > 0, B AS val <= 0);
+ERROR:  invalid token after range quantifier
+LINE 1: ...CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2} *?|B) DEFI...
+                                                             ^
+HINT:  Only "?" is allowed after {n} to make it reluctant.
+-- Doubled op-char quantifiers lex as one Op token and are unsupported, whether
+-- glued to '|' ("**|", "*+|", "???|") or on their own ("**").
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A**|B) DEFINE A AS val > 0, B AS val <= 0);
+ERROR:  unsupported quantifier "**|"
+LINE 1: ...EEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A**|B) DEFI...
+                                                             ^
+HINT:  Valid quantifiers are: *, +, ?, *?, +?, ??, {n}, {n,}, {,m}, {n,m} and their reluctant versions.
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*+|B) DEFINE A AS val > 0, B AS val <= 0);
+ERROR:  unsupported quantifier "*+|"
+LINE 1: ...EEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*+|B) DEFI...
+                                                             ^
+HINT:  Valid quantifiers are: *, +, ?, *?, +?, ??, {n}, {n,}, {,m}, {n,m} and their reluctant versions.
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A???|B) DEFINE A AS val > 0, B AS val <= 0);
+ERROR:  unsupported quantifier "???|"
+LINE 1: ...EEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A???|B) DEF...
+                                                             ^
+HINT:  Valid quantifiers are: *, +, ?, *?, +?, ??, {n}, {n,}, {,m}, {n,m} and their reluctant versions.
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A**B) DEFINE A AS val > 0);
+ERROR:  unsupported quantifier "**"
+LINE 1: ...EEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A**B) DEFIN...
+                                                             ^
+HINT:  Valid quantifiers are: *, +, ?, *?, +?, ??, {n}, {n,}, {,m}, {n,m} and their reluctant versions.
+DROP TABLE rpr_glue;
 -- ============================================================
 -- Error Cases Tests
 -- ============================================================
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index 7dfb72f6bfd..cc79843aeb7 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -13,6 +13,7 @@
 --   Navigation Functions Tests
 --   SKIP TO / INITIAL Tests
 --   Serialization/Deserialization Tests (objects kept for pg_upgrade/pg_dump)
+--   Glued Quantifier / Alternation Tests
 --   Error Cases Tests
 --   Window Deduplication Tests
 --
@@ -2074,6 +2075,117 @@ WINDOW w AS (ORDER BY id
              DEFINE A AS val > 0);
 SELECT pg_get_viewdef('rpr_quant_n_plus_v'::regclass);
 
+-- ============================================================
+-- Glued Quantifier / Alternation Tests
+-- ============================================================
+CREATE TABLE rpr_glue (id INT, val INT);
+INSERT INTO rpr_glue VALUES (1, 5), (2, 8), (3, 9), (4, -1), (5, 6), (6, -2);
+-- Quantifier glued to the alternation operator '|' without a space (0059).
+-- The lexer glues the trailing '|' into one Op token; the grammar reattaches it
+-- as the lowest-precedence alternation once the surrounding sequence is built.
+-- Deparse is canonical, so the glued, spaced, and mixed-spacing forms all
+-- reduce to the same PATTERN -- one deparse per shape proves the parse tree.
+
+-- Op-char quantifiers (*, +, ?, *?, +?, ??) glued to '|'.
+CREATE VIEW rpr_dp_op AS SELECT
+    count(*) OVER w1 AS w1, count(*) OVER w2 AS w2, count(*) OVER w3 AS w3,
+    count(*) OVER w4 AS w4, count(*) OVER w5 AS w5, count(*) OVER w6 AS w6
+FROM rpr_glue
+WINDOW w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|B) DEFINE A AS val > 0, B AS val <= 0),
+       w2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+|B) DEFINE A AS val > 0, B AS val <= 0),
+       w3 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A?|B) DEFINE A AS val > 0, B AS val <= 0),
+       w4 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*?|B) DEFINE A AS val > 0, B AS val <= 0),
+       w5 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+?|B) DEFINE A AS val > 0, B AS val <= 0),
+       w6 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A??|B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_dp_op'), E'\n')) AS line WHERE line ~ 'PATTERN';
+DROP VIEW rpr_dp_op;
+-- Spaced reference: the fully-spaced canonical forms.  Identical deparse to the
+-- glued rpr_dp_op w1/w4 above completes the glued = spaced = mixed equivalence.
+CREATE VIEW rpr_dp_spc AS SELECT count(*) OVER w1 AS w1, count(*) OVER w2 AS w2
+FROM rpr_glue
+WINDOW w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A* | B) DEFINE A AS val > 0, B AS val <= 0),
+       w2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*? | B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_dp_spc'), E'\n')) AS line WHERE line ~ 'PATTERN';
+DROP VIEW rpr_dp_spc;
+-- Range quantifiers glued to '|': non-reluctant {n}| (} + char '|') and
+-- reluctant {n}?| (} + Op "?|").
+CREATE VIEW rpr_dp_rng AS SELECT
+    count(*) OVER w1 AS w1, count(*) OVER w2 AS w2, count(*) OVER w3 AS w3, count(*) OVER w4 AS w4,
+    count(*) OVER w5 AS w5, count(*) OVER w6 AS w6, count(*) OVER w7 AS w7, count(*) OVER w8 AS w8
+FROM rpr_glue
+WINDOW w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2}|B) DEFINE A AS val > 0, B AS val <= 0),
+       w2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2,}|B) DEFINE A AS val > 0, B AS val <= 0),
+       w3 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{,3}|B) DEFINE A AS val > 0, B AS val <= 0),
+       w4 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2,3}|B) DEFINE A AS val > 0, B AS val <= 0),
+       w5 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2}?|B) DEFINE A AS val > 0, B AS val <= 0),
+       w6 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2,}?|B) DEFINE A AS val > 0, B AS val <= 0),
+       w7 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{,3}?|B) DEFINE A AS val > 0, B AS val <= 0),
+       w8 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2,3}?|B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_dp_rng'), E'\n')) AS line WHERE line ~ 'PATTERN';
+DROP VIEW rpr_dp_rng;
+-- Mixed spacing: a space inside the quantifier with '|' still glued.
+-- "A* ?|B" = '*' + Op"?|" = reluctant "A*?" plus alternation.
+CREATE VIEW rpr_dp_mix AS SELECT count(*) OVER w1 AS w1, count(*) OVER w2 AS w2, count(*) OVER w3 AS w3
+FROM rpr_glue
+WINDOW w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A* ?|B) DEFINE A AS val > 0, B AS val <= 0),
+       w2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+ ?|B) DEFINE A AS val > 0, B AS val <= 0),
+       w3 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A? ?|B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_dp_mix'), E'\n')) AS line WHERE line ~ 'PATTERN';
+DROP VIEW rpr_dp_mix;
+-- Structure: precedence (| is lowest, so its right operand is the whole
+-- following sequence), chaining, concatenation, and grouping.
+CREATE VIEW rpr_dp_struct AS SELECT
+    count(*) OVER w1 AS w1, count(*) OVER w2 AS w2, count(*) OVER w3 AS w3,
+    count(*) OVER w4 AS w4, count(*) OVER w5 AS w5, count(*) OVER w6 AS w6
+FROM rpr_glue
+WINDOW w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|B C) DEFINE A AS val > 0, B AS val <= 0, C AS val < 100),
+       w2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|B*|C) DEFINE A AS val > 0, B AS val <= 0, C AS val < 100),
+       w3 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A B*|C D) DEFINE A AS val > 0, B AS val <= 0, C AS val < 100, D AS val > 5),
+       w4 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A*|B)) DEFINE A AS val > 0, B AS val <= 0),
+       w5 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|(B|C)) DEFINE A AS val > 0, B AS val <= 0, C AS val < 100),
+       w6 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A*|B)+) DEFINE A AS val > 0, B AS val <= 0);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_dp_struct'), E'\n')) AS line WHERE line ~ 'PATTERN';
+DROP VIEW rpr_dp_struct;
+-- Execution semantics (deparse cannot show reluctant shortest-match).  The
+-- rpr_glue rows -- an A-run followed by B rows -- make the '|B' alternative
+-- reachable: with "*" the greedy form matches the whole run while the
+-- reluctant form matches empty; with "+" the greedy form matches the run and
+-- the reluctant form matches one row, and on a B row (where "A+" fails) the B
+-- alternative fires.
+SELECT id, val,
+       count(*) OVER gs AS gstar, count(*) OVER rs AS rstar,
+       count(*) OVER gp AS gplus, count(*) OVER rp AS rplus
+FROM rpr_glue
+WINDOW gs AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|B) DEFINE A AS val > 0, B AS val <= 0),
+       rs AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*?|B) DEFINE A AS val > 0, B AS val <= 0),
+       gp AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+|B) DEFINE A AS val > 0, B AS val <= 0),
+       rp AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+?|B) DEFINE A AS val > 0, B AS val <= 0)
+ORDER BY id;
+-- Patterns that must stay rejected.  "&" is an invalid op; a '|' with an empty
+-- side (leading, trailing, doubled, or alone in a group) has no operand; "||"
+-- and "*||" are doubled pipes; "A* *|B"/"A* *?|B"/"A{2}*?|B" are doubled
+-- quantifiers.
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A&B) DEFINE A AS val > 0);
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|) DEFINE A AS val > 0);
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*| |B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*||B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A||B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|B|) DEFINE A AS val > 0, B AS val <= 0);
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (|A) DEFINE A AS val > 0);
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A*|)) DEFINE A AS val > 0);
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A* *|B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A* *?|B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A? *?|B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2}*?|B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2} *?|B) DEFINE A AS val > 0, B AS val <= 0);
+-- Doubled op-char quantifiers lex as one Op token and are unsupported, whether
+-- glued to '|' ("**|", "*+|", "???|") or on their own ("**").
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A**|B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*+|B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A???|B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A**B) DEFINE A AS val > 0);
+DROP TABLE rpr_glue;
+
 -- ============================================================
 -- Error Cases Tests
 -- ============================================================
-- 
2.50.1 (Apple Git-155)


From 071126a8559aceea9299a2801a78f241777071dd Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Mon, 8 Jun 2026 11:46:56 +0900
Subject: [PATCH 60/68] Fix outdated function and file references in row
 pattern recognition docs

Two comments in rpr_nfa.sql attributed nfa_advance_begin/end to
nodeWindowAgg.c, but both are defined in execRPR.c.  README.rpr referred to a
nonexistent compute_nav_offsets; the planner routine that computes the trim
offsets is compute_define_metadata in createplan.c.  These are comment and
documentation fixes only, with no behavior change; the rpr_nfa expected output
is updated to match the corrected comment.
---
 src/backend/executor/README.rpr       | 4 ++--
 src/test/regress/expected/rpr_nfa.out | 4 ++--
 src/test/regress/sql/rpr_nfa.sql      | 4 ++--
 3 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/src/backend/executor/README.rpr b/src/backend/executor/README.rpr
index df574a0a6f4..c1822d8240b 100644
--- a/src/backend/executor/README.rpr
+++ b/src/backend/executor/README.rpr
@@ -667,7 +667,7 @@ VI-5. Tuplestore Mark and Trim (nodeWindowAgg.c)
 
 Navigation functions require access to past rows via the tuplestore.
 To allow tuplestore_trim() to free rows that are no longer reachable,
-the planner computes two offsets (see compute_nav_offsets):
+the planner computes two offsets (see compute_define_metadata):
 
   navMaxOffset (Nav Mark Lookback):
     Maximum backward reach from currentpos.  Contributed by PREV,
@@ -1499,7 +1499,7 @@ Appendix A. Key Function Index
   ExecRPRFinalizeAllContexts    execRPR.c             Partition-end finalize
   ExecRPRRecordContextSuccess   execRPR.c             Stats: match success
   ExecRPRRecordContextFailure   execRPR.c             Stats: match failure
-  compute_nav_offsets           createplan.c          Trim offset computation
+  compute_define_metadata       createplan.c          Trim offset computation
 
 Appendix B. Data Structure Relationship Diagram
 ============================================================================
diff --git a/src/test/regress/expected/rpr_nfa.out b/src/test/regress/expected/rpr_nfa.out
index 3b9975a83df..d7146168f2b 100644
--- a/src/test/regress/expected/rpr_nfa.out
+++ b/src/test/regress/expected/rpr_nfa.out
@@ -1357,7 +1357,7 @@ WINDOW w AS (
 
 -- Reluctant optional group skip-to-FIN
 -- When a reluctant optional group's skip path reaches FIN, the group
--- entry path is abandoned (nodeWindowAgg.c nfa_advance_begin).
+-- entry path is abandoned (execRPR.c nfa_advance_begin).
 -- Pattern: C (A B)?? -- after C matches, the reluctant group (A B)??
 -- prefers to skip.  Skip goes to FIN (group is last element), so
 -- the match completes with just C.
@@ -3364,7 +3364,7 @@ WINDOW w AS (
 -- Nested END->END fast-forward
 -- When an inner group has a nullable body and count < min, the
 -- fast-forward path exits through the outer END, incrementing
--- the outer group's count (nodeWindowAgg.c nfa_advance_end).
+-- the outer group's count (execRPR.c nfa_advance_end).
 -- Pattern: ((A?){2,3}){2,3} -- nested groups, neither collapses
 -- because the optimizer cannot safely multiply non-exact quantifiers.
 -- Data has no A rows, forcing all-empty iterations via fast-forward.
diff --git a/src/test/regress/sql/rpr_nfa.sql b/src/test/regress/sql/rpr_nfa.sql
index 61072d1d6f1..8daa0a73725 100644
--- a/src/test/regress/sql/rpr_nfa.sql
+++ b/src/test/regress/sql/rpr_nfa.sql
@@ -968,7 +968,7 @@ WINDOW w AS (
 
 -- Reluctant optional group skip-to-FIN
 -- When a reluctant optional group's skip path reaches FIN, the group
--- entry path is abandoned (nodeWindowAgg.c nfa_advance_begin).
+-- entry path is abandoned (execRPR.c nfa_advance_begin).
 -- Pattern: C (A B)?? -- after C matches, the reluctant group (A B)??
 -- prefers to skip.  Skip goes to FIN (group is last element), so
 -- the match completes with just C.
@@ -2406,7 +2406,7 @@ WINDOW w AS (
 -- Nested END->END fast-forward
 -- When an inner group has a nullable body and count < min, the
 -- fast-forward path exits through the outer END, incrementing
--- the outer group's count (nodeWindowAgg.c nfa_advance_end).
+-- the outer group's count (execRPR.c nfa_advance_end).
 -- Pattern: ((A?){2,3}){2,3} -- nested groups, neither collapses
 -- because the optimizer cannot safely multiply non-exact quantifiers.
 -- Data has no A rows, forcing all-empty iterations via fast-forward.
-- 
2.50.1 (Apple Git-155)


From 8d43d10030a6fad989e2ed8bbde1d6b036d348ce Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Mon, 8 Jun 2026 12:22:21 +0900
Subject: [PATCH 61/68] Assert that row pattern nesting depth never aliases the
 RPR_DEPTH_NONE sentinel

allocateRPRPattern stores maxDepth + 1, and scanRPRPatternRecursive rejects
nesting at or above RPR_DEPTH_MAX, so the stored value stays <= RPR_DEPTH_MAX
and never reaches the RPR_DEPTH_NONE sentinel (UINT8_MAX).  Assert that bound
so raising RPR_DEPTH_MAX or the depth limit trips immediately.  Also note in
compute_define_metadata that its default offset (1) serves only PREV; the
guarded LAST sub-case never uses it.
---
 src/backend/optimizer/plan/createplan.c | 3 +++
 src/backend/optimizer/plan/rpr.c        | 3 +++
 2 files changed, 6 insertions(+)

diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index c8ecaeea7cf..c5b0857ff20 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -2581,6 +2581,9 @@ visit_nav_plan(NavTraversal *t, RPRNavExpr *nav)
 		{
 			int64		offset;
 
+			/*
+			 * default 1 is for PREV; the guarded LAST sub-case never uses it.
+			 */
 			if (extract_const_offset(nav->offset_arg, 1, &offset))
 				context->maxOffset = Max(context->maxOffset, offset);
 			else
diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index 43114088c3f..e7276c42ca6 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -1157,6 +1157,9 @@ allocateRPRPattern(int numVars, int numElements, RPRDepth maxDepth,
 
 	result = makeNode(RPRPattern);
 	result->numVars = numVars;
+
+	/* depth < RPR_DEPTH_MAX, so maxDepth+1 never aliases RPR_DEPTH_NONE. */
+	Assert(maxDepth < RPR_DEPTH_MAX);
 	result->maxDepth = maxDepth + 1;	/* +1: depth is 0-based */
 	result->numElements = numElements;
 
-- 
2.50.1 (Apple Git-155)


From f55bf199846bb94e42cb3238a2dc92ccff1c50fe Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Mon, 8 Jun 2026 12:44:45 +0900
Subject: [PATCH 62/68] Invalidate the row pattern nav slot cache when a window
 partition changes

release_partition clears the per-partition tuplestore, leaving the cached
nav slot position (which is partition-relative) stale.  This is not a live
bug: nfa_evaluate_row resets nav_slot_pos to -1 before each row's DEFINE
evaluation, the only path that fetches nav slots, so a stale position never
produces a cache hit.  Reset it here too for consistency with the rest of the
per-partition cleanup.  Also note in visit_nav_plan that a constant
LAST(x, 0) is conservatively flagged match-start-dependent, causing a
harmless redundant re-evaluation.
---
 src/backend/executor/nodeWindowAgg.c    | 3 +++
 src/backend/optimizer/plan/createplan.c | 7 ++++++-
 2 files changed, 9 insertions(+), 1 deletion(-)

diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 86b39bf7f61..95f4f06501e 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -1574,6 +1574,9 @@ release_partition(WindowAggState *winstate)
 	winstate->nfaLastProcessedRow = -1;
 	winstate->nfaStatesActive = 0;
 	winstate->nfaContextsActive = 0;
+
+	/* Invalidate the nav slot position cache for the new partition. */
+	winstate->nav_slot_pos = -1;
 }
 
 /*
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index c5b0857ff20..5608d71cbe2 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -2693,7 +2693,12 @@ visit_nav_plan(NavTraversal *t, RPRNavExpr *nav)
 		}
 	}
 
-	/* Match-start dependency: classify the outer nav kind. */
+	/*
+	 * Match-start dependency: classify the outer nav kind.  A constant
+	 * LAST(x, 0) is conservatively included (offset_arg is a non-NULL Const),
+	 * causing a harmless extra re-evaluation; since LAST(x, 0) is the current
+	 * row, its result is independent of the match start.
+	 */
 	if (nav->kind == RPR_NAV_FIRST ||
 		(nav->kind == RPR_NAV_LAST && nav->offset_arg != NULL) ||
 		nav->kind == RPR_NAV_PREV_FIRST ||
-- 
2.50.1 (Apple Git-155)


From a3ef40ae024cd1db787a1c19dc967dbe03bb28f2 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Mon, 8 Jun 2026 13:48:10 +0900
Subject: [PATCH 63/68] Tidy up formatting in row pattern recognition code

Reformat the first three row_pattern_quantifier_opt alternatives as
multiline blocks to match the rest of the rule, change the RF_* reduced
frame macros from a tab to a space after #define, add a trailing comma to
the last RPSkipTo enumerator, and move the RPRNav* entries to their correct
C-collation position in typedefs.list.  These are formatting and alignment
changes only, with no effect on behavior.
---
 src/backend/optimizer/plan/rpr.c |  2 +-
 src/backend/parser/gram.y        | 14 +++++++++++---
 src/include/nodes/execnodes.h    | 10 +++++-----
 src/include/nodes/parsenodes.h   |  2 +-
 src/tools/pgindent/typedefs.list |  6 +++---
 5 files changed, 21 insertions(+), 13 deletions(-)

diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index e7276c42ca6..143ea034fb3 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -484,7 +484,7 @@ mergeConsecutiveAlts(List *children)
  *
  * When a GROUP's children appear as a prefix before and/or suffix after
  * the GROUP in a SEQ, merge them by incrementing the GROUP's quantifier.
- * This runs iteratively: A B A B (A B)+ A B -> (A B){5,}.
+ * This runs iteratively: A B A B (A B)+ A B -> (A B){4,}.
  *
  * Algorithm:
  *   For each GROUP encountered in the sequence:
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 147b5f37293..e0799f35638 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -17740,9 +17740,17 @@ row_pattern_primary:
 row_pattern_quantifier_opt:
 			/* EMPTY - no quantifier means exactly once; @$ is unused since
 			 * min=max=1 never produces an error */
-			{ $$ = (Node *) makeRPRQuantifier(1, 1, -1, @$, yyscanner); }
-			| '*'					{ $$ = (Node *) makeRPRQuantifier(0, INT_MAX, -1, @1, yyscanner); }
-			| '+'					{ $$ = (Node *) makeRPRQuantifier(1, INT_MAX, -1, @1, yyscanner); }
+				{
+					$$ = (Node *) makeRPRQuantifier(1, 1, -1, @$, yyscanner);
+				}
+			| '*'
+				{
+					$$ = (Node *) makeRPRQuantifier(0, INT_MAX, -1, @1, yyscanner);
+				}
+			| '+'
+				{
+					$$ = (Node *) makeRPRQuantifier(1, INT_MAX, -1, @1, yyscanner);
+				}
 			| Op
 				{
 					/* Handle single Op: ? or reluctant quantifiers *?, +?, ?? */
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 4641ed36cee..77d75aca91f 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -2528,11 +2528,11 @@ typedef enum WindowAggStatus
 } WindowAggStatus;
 
 /* RPR reduced frame states returned by get_reduced_frame_status() */
-#define	RF_NOT_DETERMINED	0	/* not yet processed */
-#define	RF_FRAME_HEAD		1	/* start row of a match */
-#define	RF_SKIPPED			2	/* interior row of a match */
-#define	RF_UNMATCHED		3	/* no match at this row */
-#define	RF_EMPTY_MATCH		4	/* empty match (0 rows); treated as unmatched */
+#define RF_NOT_DETERMINED	0	/* not yet processed */
+#define RF_FRAME_HEAD		1	/* start row of a match */
+#define RF_SKIPPED			2	/* interior row of a match */
+#define RF_UNMATCHED		3	/* no match at this row */
+#define RF_EMPTY_MATCH		4	/* empty match (0 rows); treated as unmatched */
 
 /*
  * RPRNFAState - single NFA state for pattern matching
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index e371f04a403..9636a8efca1 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -589,7 +589,7 @@ typedef enum RPSkipTo
 	ST_NONE,					/* no AFTER MATCH clause; default for non-RPR
 								 * windows */
 	ST_NEXT_ROW,				/* SKIP TO NEXT ROW */
-	ST_PAST_LAST_ROW			/* SKIP TO PAST LAST ROW */
+	ST_PAST_LAST_ROW,			/* SKIP TO PAST LAST ROW */
 } RPSkipTo;
 
 /*
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 24cf2eb7860..06cd541be5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2519,9 +2519,6 @@ QuerySource
 QueueBackendStatus
 QueuePosition
 QuitSignalReason
-RPRNavExpr
-RPRNavKind
-RPRNavOffsetKind
 RBTNode
 RBTOrderControl
 RBTree
@@ -2540,6 +2537,9 @@ RPRElemFlags
 RPRElemIdx
 RPRNFAContext
 RPRNFAState
+RPRNavExpr
+RPRNavKind
+RPRNavOffsetKind
 RPRPattern
 RPRPatternElement
 RPRPatternNode
-- 
2.50.1 (Apple Git-155)


From 4f04a1aefc4a5a041e1070f0cbae2f8fa9ff5e40 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Mon, 8 Jun 2026 14:59:00 +0900
Subject: [PATCH 65/68] Use width-explicit integer limit macros in row pattern
 recognition

Replace INT_MAX/INT16_MAX/INT32_MAX/INT64_MAX/UINT8_MAX with the
PG_INT*_MAX/PG_UINT8_MAX equivalents, and type RPRPatternNode.min/max
as int32 to match the plan representation.  Value- and output-preserving.
---
 src/backend/commands/explain.c          |  2 +-
 src/backend/executor/execRPR.c          | 11 +++--
 src/backend/executor/nodeWindowAgg.c    | 16 +++----
 src/backend/optimizer/plan/createplan.c | 14 +++---
 src/backend/optimizer/plan/rpr.c        |  6 +--
 src/backend/parser/gram.y               | 64 ++++++++++++-------------
 src/backend/utils/adt/ruleutils.c       |  6 +--
 src/include/nodes/execnodes.h           |  2 +-
 src/include/nodes/parsenodes.h          |  4 +-
 src/include/optimizer/rpr.h             | 11 +++--
 10 files changed, 69 insertions(+), 67 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 7992829d5c4..70fd7f386a0 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -3270,7 +3270,7 @@ show_window_def(WindowAggState *planstate, List *ancestors, ExplainState *es)
 											es);
 						break;
 					case RPR_NAV_OFFSET_FIXED:
-						if (firstOffset == INT64_MAX)
+						if (firstOffset == PG_INT64_MAX)
 							ExplainPropertyText("Nav Mark Lookahead", "infinite",
 												es);
 						else
diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index b7e3c4a8274..1c89875a306 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -1546,7 +1546,7 @@ nfa_advance(WindowAggState *winstate, RPRNFAContext *ctx, int64 currentPos)
 				   sizeof(bitmapword) *
 				   (winstate->nfaVisitedMaxWord -
 					winstate->nfaVisitedMinWord + 1));
-			winstate->nfaVisitedMinWord = INT16_MAX;
+			winstate->nfaVisitedMinWord = PG_INT16_MAX;
 			winstate->nfaVisitedMaxWord = -1;
 		}
 
@@ -1807,10 +1807,11 @@ ExecRPRProcessRow(WindowAggState *winstate, int64 currentPos,
 			int64		ctxFrameEnd;
 
 			/*
-			 * Clamp to INT64_MAX on overflow.  frameOffset can be as large as
-			 * PG_INT64_MAX (e.g. "ROWS <huge> FOLLOWING"), so add the offset
-			 * and the trailing +1 in two separately checked steps to avoid
-			 * signed-integer overflow in the "frameOffset + 1" subexpression.
+			 * Clamp to PG_INT64_MAX on overflow.  frameOffset can be as large
+			 * as PG_INT64_MAX (e.g. "ROWS <huge> FOLLOWING"), so add the
+			 * offset and the trailing +1 in two separately checked steps to
+			 * avoid signed-integer overflow in the "frameOffset + 1"
+			 * subexpression.
 			 */
 			if (pg_add_s64_overflow(ctx->matchStartRow, frameOffset,
 									&ctxFrameEnd) ||
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 95f4f06501e..13661181986 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -3070,7 +3070,7 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
 		winstate->nfaVisitedElems = palloc0(sizeof(bitmapword) *
 											nfaVisitedNWords);
 		/* High-water mark sentinels: no bits set yet. */
-		winstate->nfaVisitedMinWord = INT16_MAX;
+		winstate->nfaVisitedMinWord = PG_INT16_MAX;
 		winstate->nfaVisitedMaxWord = -1;
 	}
 
@@ -4048,7 +4048,7 @@ typedef struct
  *			NEXT_LAST (= max(inner - outer, 0))
  *		  - minFirstOffset (forward reach from match_start): FIRST,
  *			compound PREV_FIRST (= inner - outer, may be negative),
- *			compound NEXT_FIRST (= inner + outer, clamped to INT64_MAX on
+ *			compound NEXT_FIRST (= inner + outer, clamped to PG_INT64_MAX on
  *			overflow; always >= 0 so never updates minFirstOffset in practice)
  *
  * Counterpart of visit_nav_plan but using runtime evaluation instead of
@@ -4137,7 +4137,7 @@ visit_nav_exec(NavTraversal *t, RPRNavExpr *nav)
 		{
 			/*
 			 * reach = inner - outer.  Both are non-negative, so the result >=
-			 * -INT64_MAX, which cannot underflow int64.
+			 * -PG_INT64_MAX, which cannot underflow int64.
 			 */
 			reach = inner - outer;
 		}
@@ -4146,10 +4146,10 @@ visit_nav_exec(NavTraversal *t, RPRNavExpr *nav)
 			/*
 			 * NEXT_FIRST: reach = inner + outer.  This can overflow, but the
 			 * result is always >= 0, so it never updates minFirstOffset
-			 * (which tracks the minimum).  Clamp to INT64_MAX on overflow.
+			 * (which tracks the minimum).  Clamp to PG_INT64_MAX on overflow.
 			 */
 			if (pg_add_s64_overflow(inner, outer, &reach))
-				reach = INT64_MAX;
+				reach = PG_INT64_MAX;
 		}
 		context->minFirstOffset = Min(context->minFirstOffset, reach);
 	}
@@ -4184,7 +4184,7 @@ eval_define_offsets(WindowAggState *winstate, List *defineClause)
 	ctx.winstate = winstate;
 	ctx.maxOffset = 0;
 	ctx.maxOverflow = false;
-	ctx.minFirstOffset = INT64_MAX;
+	ctx.minFirstOffset = PG_INT64_MAX;
 
 	trav.visit = visit_nav_exec;
 	trav.data = &ctx;
@@ -4213,10 +4213,10 @@ eval_define_offsets(WindowAggState *winstate, List *defineClause)
 	if (needsFirst)
 	{
 		winstate->navFirstOffsetKind = RPR_NAV_OFFSET_FIXED;
-		if (ctx.minFirstOffset < INT64_MAX)
+		if (ctx.minFirstOffset < PG_INT64_MAX)
 			winstate->navFirstOffset = ctx.minFirstOffset;
 		else
-			winstate->navFirstOffset = INT64_MAX;
+			winstate->navFirstOffset = PG_INT64_MAX;
 	}
 }
 
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index 5608d71cbe2..d2e19d61d58 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -2669,8 +2669,8 @@ visit_nav_plan(NavTraversal *t, RPRNavExpr *nav)
 				{
 					/*
 					 * reach = inner - outer.  Both are non-negative, so the
-					 * result >= -INT64_MAX, which cannot underflow int64. No
-					 * overflow check needed.
+					 * result >= -PG_INT64_MAX, which cannot underflow int64.
+					 * No overflow check needed.
 					 */
 					reach = inner - outer;
 				}
@@ -2680,10 +2680,10 @@ visit_nav_plan(NavTraversal *t, RPRNavExpr *nav)
 					 * NEXT_FIRST: reach = inner + outer.  This can overflow,
 					 * but the result is always >= 0, so it never updates
 					 * firstOffset (which tracks the minimum).  Clamp to
-					 * INT64_MAX on overflow.
+					 * PG_INT64_MAX on overflow.
 					 */
 					if (pg_add_s64_overflow(inner, outer, &reach))
-						reach = INT64_MAX;
+						reach = PG_INT64_MAX;
 				}
 
 				context->firstOffset = Min(context->firstOffset, reach);
@@ -2741,7 +2741,7 @@ compute_define_metadata(List *defineClause,
 	ctx.maxOffset = 0;
 	ctx.maxNeedsEval = false;
 	ctx.maxOverflow = false;
-	ctx.firstOffset = INT64_MAX;	/* sentinel: no FIRST found yet */
+	ctx.firstOffset = PG_INT64_MAX; /* sentinel: no FIRST found yet */
 	ctx.hasFirst = false;
 	ctx.firstNeedsEval = false;
 	ctx.curVarIdx = 0;
@@ -2789,8 +2789,8 @@ compute_define_metadata(List *defineClause,
 		else
 		{
 			*firstKind = RPR_NAV_OFFSET_FIXED;
-			*firstResult = ctx.firstOffset; /* may be negative; INT64_MAX if
-											 * overflowed */
+			*firstResult = ctx.firstOffset; /* may be negative; PG_INT64_MAX
+											 * if overflowed */
 		}
 	}
 	else
diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index 9364136b071..d768422deeb 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -1210,7 +1210,7 @@ fillRPRPatternVar(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth dept
 	elem->varId = getVarIdFromPattern(pat, node->varName);
 	elem->depth = depth;
 	elem->min = node->min;
-	elem->max = (node->max == INT_MAX) ? RPR_QUANTITY_INF : node->max;
+	elem->max = (node->max == PG_INT32_MAX) ? RPR_QUANTITY_INF : node->max;
 	Assert(elem->min >= 0 && elem->min < RPR_QUANTITY_INF &&
 		   elem->max >= 1 &&
 		   (elem->max == RPR_QUANTITY_INF || elem->min <= elem->max));
@@ -1263,7 +1263,7 @@ fillRPRPatternGroup(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth de
 		elem->varId = RPR_VARID_BEGIN;
 		elem->depth = depth;
 		elem->min = node->min;
-		elem->max = (node->max == INT_MAX) ? RPR_QUANTITY_INF : node->max;
+		elem->max = (node->max == PG_INT32_MAX) ? RPR_QUANTITY_INF : node->max;
 		Assert(elem->min >= 0 && elem->min < RPR_QUANTITY_INF &&
 			   elem->max >= 1 &&
 			   (elem->max == RPR_QUANTITY_INF || elem->min <= elem->max));
@@ -1291,7 +1291,7 @@ fillRPRPatternGroup(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth de
 		endElem->varId = RPR_VARID_END;
 		endElem->depth = depth;
 		endElem->min = node->min;
-		endElem->max = (node->max == INT_MAX) ? RPR_QUANTITY_INF : node->max;
+		endElem->max = (node->max == PG_INT32_MAX) ? RPR_QUANTITY_INF : node->max;
 		Assert(endElem->min >= 0 && endElem->min < RPR_QUANTITY_INF &&
 			   endElem->max >= 1 &&
 			   (endElem->max == RPR_QUANTITY_INF || endElem->min <= endElem->max));
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index e0799f35638..ac2e5d7914a 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -212,7 +212,7 @@ static void preprocess_pubobj_list(List *pubobjspec_list,
 static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 static RPRPatternNode *makeRPRSeqOrSingle(List *children, int location);
 static RPRPatternNode *splitRPRTrailingAlt(RPRPatternNode *node, core_yyscan_t yyscanner);
-static RPRPatternNode *makeRPRQuantifier(int min, int max, ParseLoc reluctant, int location,
+static RPRPatternNode *makeRPRQuantifier(int32 min, int32 max, ParseLoc reluctant, int location,
 									   core_yyscan_t yyscanner);
 
 %}
@@ -17745,11 +17745,11 @@ row_pattern_quantifier_opt:
 				}
 			| '*'
 				{
-					$$ = (Node *) makeRPRQuantifier(0, INT_MAX, -1, @1, yyscanner);
+					$$ = (Node *) makeRPRQuantifier(0, PG_INT32_MAX, -1, @1, yyscanner);
 				}
 			| '+'
 				{
-					$$ = (Node *) makeRPRQuantifier(1, INT_MAX, -1, @1, yyscanner);
+					$$ = (Node *) makeRPRQuantifier(1, PG_INT32_MAX, -1, @1, yyscanner);
 				}
 			| Op
 				{
@@ -17757,19 +17757,19 @@ row_pattern_quantifier_opt:
 					if (strcmp($1, "?") == 0)
 						$$ = (Node *) makeRPRQuantifier(0, 1, -1, @1, yyscanner);
 					else if (strcmp($1, "*?") == 0)
-						$$ = (Node *) makeRPRQuantifier(0, INT_MAX, @1 + 1, @1, yyscanner);
+						$$ = (Node *) makeRPRQuantifier(0, PG_INT32_MAX, @1 + 1, @1, yyscanner);
 					else if (strcmp($1, "+?") == 0)
-						$$ = (Node *) makeRPRQuantifier(1, INT_MAX, @1 + 1, @1, yyscanner);
+						$$ = (Node *) makeRPRQuantifier(1, PG_INT32_MAX, @1 + 1, @1, yyscanner);
 					else if (strcmp($1, "??") == 0)
 						$$ = (Node *) makeRPRQuantifier(0, 1, @1 + 1, @1, yyscanner);
 					else if (strcmp($1, "*|") == 0)
 					{
-						$$ = (Node *) makeRPRQuantifier(0, INT_MAX, -1, @1, yyscanner);
+						$$ = (Node *) makeRPRQuantifier(0, PG_INT32_MAX, -1, @1, yyscanner);
 						((RPRPatternNode *) $$)->trailing_alt = true;
 					}
 					else if (strcmp($1, "+|") == 0)
 					{
-						$$ = (Node *) makeRPRQuantifier(1, INT_MAX, -1, @1, yyscanner);
+						$$ = (Node *) makeRPRQuantifier(1, PG_INT32_MAX, -1, @1, yyscanner);
 						((RPRPatternNode *) $$)->trailing_alt = true;
 					}
 					else if (strcmp($1, "?|") == 0)
@@ -17779,12 +17779,12 @@ row_pattern_quantifier_opt:
 					}
 					else if (strcmp($1, "*?|") == 0)
 					{
-						$$ = (Node *) makeRPRQuantifier(0, INT_MAX, @1 + 1, @1, yyscanner);
+						$$ = (Node *) makeRPRQuantifier(0, PG_INT32_MAX, @1 + 1, @1, yyscanner);
 						((RPRPatternNode *) $$)->trailing_alt = true;
 					}
 					else if (strcmp($1, "+?|") == 0)
 					{
-						$$ = (Node *) makeRPRQuantifier(1, INT_MAX, @1 + 1, @1, yyscanner);
+						$$ = (Node *) makeRPRQuantifier(1, PG_INT32_MAX, @1 + 1, @1, yyscanner);
 						((RPRPatternNode *) $$)->trailing_alt = true;
 					}
 					else if (strcmp($1, "??|") == 0)
@@ -17803,11 +17803,11 @@ row_pattern_quantifier_opt:
 			| '*' Op
 				{
 					if (strcmp($2, "?") == 0)
-						$$ = (Node *) makeRPRQuantifier(0, INT_MAX, @2, @1, yyscanner);
+						$$ = (Node *) makeRPRQuantifier(0, PG_INT32_MAX, @2, @1, yyscanner);
 					else if (strcmp($2, "?|") == 0)
 					{
 						/* "A* ?|B" = reluctant "A*?" plus alternation */
-						$$ = (Node *) makeRPRQuantifier(0, INT_MAX, @2, @1, yyscanner);
+						$$ = (Node *) makeRPRQuantifier(0, PG_INT32_MAX, @2, @1, yyscanner);
 						((RPRPatternNode *) $$)->trailing_alt = true;
 					}
 					else
@@ -17820,11 +17820,11 @@ row_pattern_quantifier_opt:
 			| '+' Op
 				{
 					if (strcmp($2, "?") == 0)
-						$$ = (Node *) makeRPRQuantifier(1, INT_MAX, @2, @1, yyscanner);
+						$$ = (Node *) makeRPRQuantifier(1, PG_INT32_MAX, @2, @1, yyscanner);
 					else if (strcmp($2, "?|") == 0)
 					{
 						/* "A+ ?|B" = reluctant "A+?" plus alternation */
-						$$ = (Node *) makeRPRQuantifier(1, INT_MAX, @2, @1, yyscanner);
+						$$ = (Node *) makeRPRQuantifier(1, PG_INT32_MAX, @2, @1, yyscanner);
 						((RPRPatternNode *) $$)->trailing_alt = true;
 					}
 					else
@@ -17860,37 +17860,37 @@ row_pattern_quantifier_opt:
 			/* {n}, {n,}, {,m}, {n,m} quantifiers */
 			| '{' Iconst '}'
 				{
-					if ($2 <= 0 || $2 >= INT_MAX)
+					if ($2 <= 0 || $2 >= PG_INT32_MAX)
 						ereport(ERROR,
 								errcode(ERRCODE_SYNTAX_ERROR),
-								errmsg("quantifier bound must be between 1 and %d", INT_MAX - 1),
+								errmsg("quantifier bound must be between 1 and %d", PG_INT32_MAX - 1),
 								parser_errposition(@2));
 					$$ = (Node *) makeRPRQuantifier($2, $2, -1, @1, yyscanner);
 				}
 			| '{' Iconst ',' '}'
 				{
-					if ($2 < 0 || $2 >= INT_MAX)
+					if ($2 < 0 || $2 >= PG_INT32_MAX)
 						ereport(ERROR,
 								errcode(ERRCODE_SYNTAX_ERROR),
-								errmsg("quantifier bound must be between 0 and %d", INT_MAX - 1),
+								errmsg("quantifier bound must be between 0 and %d", PG_INT32_MAX - 1),
 								parser_errposition(@2));
-					$$ = (Node *) makeRPRQuantifier($2, INT_MAX, -1, @1, yyscanner);
+					$$ = (Node *) makeRPRQuantifier($2, PG_INT32_MAX, -1, @1, yyscanner);
 				}
 			| '{' ',' Iconst '}'
 				{
-					if ($3 <= 0 || $3 >= INT_MAX)
+					if ($3 <= 0 || $3 >= PG_INT32_MAX)
 						ereport(ERROR,
 								errcode(ERRCODE_SYNTAX_ERROR),
-								errmsg("quantifier bound must be between 1 and %d", INT_MAX - 1),
+								errmsg("quantifier bound must be between 1 and %d", PG_INT32_MAX - 1),
 								parser_errposition(@3));
 					$$ = (Node *) makeRPRQuantifier(0, $3, -1, @1, yyscanner);
 				}
 			| '{' Iconst ',' Iconst '}'
 				{
-					if ($2 < 0 || $4 <= 0 || $2 >= INT_MAX || $4 >= INT_MAX)
+					if ($2 < 0 || $4 <= 0 || $2 >= PG_INT32_MAX || $4 >= PG_INT32_MAX)
 						ereport(ERROR,
 								errcode(ERRCODE_SYNTAX_ERROR),
-								errmsg("quantifier bounds must be between 0 and %d with max >= 1", INT_MAX - 1),
+								errmsg("quantifier bounds must be between 0 and %d with max >= 1", PG_INT32_MAX - 1),
 								parser_errposition(@2));
 					if ($2 > $4)
 						ereport(ERROR,
@@ -17908,10 +17908,10 @@ row_pattern_quantifier_opt:
 								errmsg("invalid token after range quantifier"),
 								errhint("Only \"?\" is allowed after {n} to make it reluctant."),
 								parser_errposition(@4));
-					if ($2 <= 0 || $2 >= INT_MAX)
+					if ($2 <= 0 || $2 >= PG_INT32_MAX)
 						ereport(ERROR,
 								errcode(ERRCODE_SYNTAX_ERROR),
-								errmsg("quantifier bound must be between 1 and %d", INT_MAX - 1),
+								errmsg("quantifier bound must be between 1 and %d", PG_INT32_MAX - 1),
 								parser_errposition(@2));
 					$$ = (Node *) makeRPRQuantifier($2, $2, @4, @1, yyscanner);
 					if (strcmp($4, "?|") == 0)
@@ -17925,12 +17925,12 @@ row_pattern_quantifier_opt:
 								errmsg("invalid token after range quantifier"),
 								errhint("Only \"?\" is allowed after {n,} or {,m} to make it reluctant."),
 								parser_errposition(@5));
-					if ($2 < 0 || $2 >= INT_MAX)
+					if ($2 < 0 || $2 >= PG_INT32_MAX)
 						ereport(ERROR,
 								errcode(ERRCODE_SYNTAX_ERROR),
-								errmsg("quantifier bound must be between 0 and %d", INT_MAX - 1),
+								errmsg("quantifier bound must be between 0 and %d", PG_INT32_MAX - 1),
 								parser_errposition(@2));
-					$$ = (Node *) makeRPRQuantifier($2, INT_MAX, @5, @1, yyscanner);
+					$$ = (Node *) makeRPRQuantifier($2, PG_INT32_MAX, @5, @1, yyscanner);
 					if (strcmp($5, "?|") == 0)
 						((RPRPatternNode *) $$)->trailing_alt = true;
 				}
@@ -17942,10 +17942,10 @@ row_pattern_quantifier_opt:
 								errmsg("invalid token after range quantifier"),
 								errhint("Only \"?\" is allowed after {n,} or {,m} to make it reluctant."),
 								parser_errposition(@5));
-					if ($3 <= 0 || $3 >= INT_MAX)
+					if ($3 <= 0 || $3 >= PG_INT32_MAX)
 						ereport(ERROR,
 								errcode(ERRCODE_SYNTAX_ERROR),
-								errmsg("quantifier bound must be between 1 and %d", INT_MAX - 1),
+								errmsg("quantifier bound must be between 1 and %d", PG_INT32_MAX - 1),
 								parser_errposition(@3));
 					$$ = (Node *) makeRPRQuantifier(0, $3, @5, @1, yyscanner);
 					if (strcmp($5, "?|") == 0)
@@ -17959,10 +17959,10 @@ row_pattern_quantifier_opt:
 								errmsg("invalid token after range quantifier"),
 								errhint("Only \"?\" is allowed after {n,m} to make it reluctant."),
 								parser_errposition(@6));
-					if ($2 < 0 || $4 <= 0 || $2 >= INT_MAX || $4 >= INT_MAX)
+					if ($2 < 0 || $4 <= 0 || $2 >= PG_INT32_MAX || $4 >= PG_INT32_MAX)
 						ereport(ERROR,
 								errcode(ERRCODE_SYNTAX_ERROR),
-								errmsg("quantifier bounds must be between 0 and %d with max >= 1", INT_MAX - 1),
+								errmsg("quantifier bounds must be between 0 and %d with max >= 1", PG_INT32_MAX - 1),
 								parser_errposition(@2));
 					if ($2 > $4)
 						ereport(ERROR,
@@ -21400,7 +21400,7 @@ makeRecursiveViewSelect(char *relname, List *aliases, Node *query)
  *		Create an RPRPatternNode with specified quantifier bounds.
  */
 static RPRPatternNode *
-makeRPRQuantifier(int min, int max, ParseLoc reluctant_location, int location,
+makeRPRQuantifier(int32 min, int32 max, ParseLoc reluctant_location, int location,
 				  core_yyscan_t yyscanner)
 {
 	RPRPatternNode *n = makeNode(RPRPatternNode);
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index e8087d4316a..d588cd8263d 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -7124,13 +7124,13 @@ append_pattern_quantifier(StringInfo buf, RPRPatternNode *node)
 		/* {1,1} = no quantifier */
 		has_quantifier = false;
 	}
-	else if (node->min == 0 && node->max == INT_MAX)
+	else if (node->min == 0 && node->max == PG_INT32_MAX)
 		appendStringInfoChar(buf, '*');
-	else if (node->min == 1 && node->max == INT_MAX)
+	else if (node->min == 1 && node->max == PG_INT32_MAX)
 		appendStringInfoChar(buf, '+');
 	else if (node->min == 0 && node->max == 1)
 		appendStringInfoChar(buf, '?');
-	else if (node->max == INT_MAX)
+	else if (node->max == PG_INT32_MAX)
 		appendStringInfo(buf, "{%d,}", node->min);
 	else if (node->min == node->max)
 		appendStringInfo(buf, "{%d}", node->min);
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 77d75aca91f..dca0bbc3e30 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -2672,7 +2672,7 @@ typedef struct WindowAggState
 	bitmapword *nfaVisitedElems;	/* elemIdx visited bitmap for cycle
 									 * detection */
 	int16		nfaVisitedMinWord;	/* lowest bitmapword index touched since
-									 * last reset (INT16_MAX = none) */
+									 * last reset (PG_INT16_MAX = none) */
 	int16		nfaVisitedMaxWord;	/* highest bitmapword index touched since
 									 * last reset (-1 = none) */
 	int64		nfaLastProcessedRow;	/* last row processed by NFA (-1 =
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 9636a8efca1..16f31dbe7ff 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -625,8 +625,8 @@ typedef struct RPRPatternNode
 {
 	NodeTag		type;			/* T_RPRPatternNode */
 	RPRPatternNodeType nodeType;	/* VAR, SEQ, ALT, GROUP */
-	int			min;			/* minimum repetitions (0 for *, ?) */
-	int			max;			/* maximum repetitions (INT_MAX for *, +) */
+	int32		min;			/* minimum repetitions (0 for *, ?) */
+	int32		max;			/* maximum repetitions (PG_INT32_MAX for *, +) */
 	bool		reluctant;		/* true for reluctant (non-greedy) */
 	ParseLoc	reluctant_location; /* location of '?' token, or -1 */
 	ParseLoc	location;		/* token location, or -1 */
diff --git a/src/include/optimizer/rpr.h b/src/include/optimizer/rpr.h
index 73c827de2b1..8e0bc7efc53 100644
--- a/src/include/optimizer/rpr.h
+++ b/src/include/optimizer/rpr.h
@@ -26,12 +26,13 @@
  * before release.
  */
 #define RPR_VARID_MAX		0xEF	/* pattern variables are 0 to 0xEF */
-#define RPR_QUANTITY_INF	INT32_MAX	/* unbounded quantifier */
-#define RPR_COUNT_MAX		INT32_MAX	/* max runtime count (NFA state) */
-#define RPR_ELEMIDX_MAX		INT16_MAX	/* max pattern elements */
+#define RPR_QUANTITY_INF	PG_INT32_MAX	/* unbounded quantifier */
+#define RPR_COUNT_MAX		PG_INT32_MAX	/* max runtime count (NFA state) */
+#define RPR_ELEMIDX_MAX		PG_INT16_MAX	/* max pattern elements */
 #define RPR_ELEMIDX_INVALID	((RPRElemIdx) -1)	/* invalid index */
-#define RPR_DEPTH_MAX		(UINT8_MAX - 1) /* max pattern nesting depth: 254 */
-#define RPR_DEPTH_NONE		UINT8_MAX	/* no enclosing group (top-level) */
+#define RPR_DEPTH_MAX		(PG_UINT8_MAX - 1)	/* max pattern nesting depth:
+												 * 254 */
+#define RPR_DEPTH_NONE		PG_UINT8_MAX	/* no enclosing group (top-level) */
 
 /* Reserved control-element varIds (high nibble 0xF; 0xF0-0xFB spare) */
 #define RPR_VARID_BEGIN		((RPRVarId) 0xFC)	/* group begin */
-- 
2.50.1 (Apple Git-155)


From ad3e63eea24c4f80f8d18ef7b5cc40fff14e4828 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Mon, 8 Jun 2026 13:59:43 +0900
Subject: [PATCH 64/68] Modernize idioms in row pattern recognition code

Replace appendStringInfo() with appendStringInfoString() for the eight
constant string literals that contain no format specifiers, avoiding an
unnecessary trip through vsnprintf().

Replace the four palloc(n * sizeof(type)) calls in _copyRPRPattern() and
_readRPRPattern() with palloc_array()/palloc0_array(), which make the
element type and count explicit and guard against size multiplication
overflow.

Replace an open-coded maximum in scanRPRPatternRecursive() with Max().

These are mechanical idiom changes only; behavior, output, and ABI are
unchanged, and the row pattern regression tests pass with no .out diffs.
---
 src/backend/commands/explain.c    | 15 ++++++++-------
 src/backend/nodes/copyfuncs.c     |  4 ++--
 src/backend/nodes/readfuncs.c     |  4 ++--
 src/backend/optimizer/plan/rpr.c  |  3 +--
 src/backend/utils/adt/ruleutils.c |  3 ++-
 5 files changed, 15 insertions(+), 14 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 7ba0b6df849..7992829d5c4 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -2932,7 +2932,8 @@ append_rpr_quantifier(StringInfo buf, RPRPatternElement *elem)
 	if (RPRElemIsReluctant(elem))
 	{
 		if (elem->min == 1 && elem->max == 1)
-			appendStringInfo(buf, "{1}");	/* make reluctant ? unambiguous */
+			appendStringInfoString(buf, "{1}"); /* make reluctant ?
+												 * unambiguous */
 		appendStringInfoChar(buf, '?');
 	}
 
@@ -3929,7 +3930,7 @@ show_rpr_nfa_stats(WindowAggState *winstate, ExplainState *es)
 
 		/* Match/mismatch counts with length min/max/avg */
 		ExplainIndentText(es);
-		appendStringInfo(es->str, "NFA: ");
+		appendStringInfoString(es->str, "NFA: ");
 		if (winstate->nfaMatchesSucceeded > 0)
 		{
 			double		avgLen = (double) winstate->nfaMatchLen.total / winstate->nfaMatchesSucceeded;
@@ -3943,7 +3944,7 @@ show_rpr_nfa_stats(WindowAggState *winstate, ExplainState *es)
 		}
 		else
 		{
-			appendStringInfo(es->str, "0 matched");
+			appendStringInfoString(es->str, "0 matched");
 		}
 		if (winstate->nfaMatchesFailed > 0)
 		{
@@ -3958,7 +3959,7 @@ show_rpr_nfa_stats(WindowAggState *winstate, ExplainState *es)
 		}
 		else
 		{
-			appendStringInfo(es->str, ", 0 mismatched");
+			appendStringInfoString(es->str, ", 0 mismatched");
 		}
 		appendStringInfoChar(es->str, '\n');
 
@@ -3966,7 +3967,7 @@ show_rpr_nfa_stats(WindowAggState *winstate, ExplainState *es)
 		if (winstate->nfaContextsAbsorbed > 0 || winstate->nfaContextsSkipped > 0)
 		{
 			ExplainIndentText(es);
-			appendStringInfo(es->str, "NFA: ");
+			appendStringInfoString(es->str, "NFA: ");
 
 			if (winstate->nfaContextsAbsorbed > 0)
 			{
@@ -3981,7 +3982,7 @@ show_rpr_nfa_stats(WindowAggState *winstate, ExplainState *es)
 			}
 			else
 			{
-				appendStringInfo(es->str, "0 absorbed");
+				appendStringInfoString(es->str, "0 absorbed");
 			}
 
 			if (winstate->nfaContextsSkipped > 0)
@@ -3997,7 +3998,7 @@ show_rpr_nfa_stats(WindowAggState *winstate, ExplainState *es)
 			}
 			else
 			{
-				appendStringInfo(es->str, ", 0 skipped");
+				appendStringInfoString(es->str, ", 0 skipped");
 			}
 
 			appendStringInfoChar(es->str, '\n');
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index e67ad39bdb8..17d45930d7b 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -178,13 +178,13 @@ _copyRPRPattern(const RPRPattern *from)
 
 	/* Deep copy the varNames array (DEFINE clause is required) */
 	Assert(from->numVars > 0);
-	newnode->varNames = palloc0(from->numVars * sizeof(char *));
+	newnode->varNames = palloc0_array(char *, from->numVars);
 	for (int i = 0; i < from->numVars; i++)
 		newnode->varNames[i] = pstrdup(from->varNames[i]);
 
 	/* Deep copy the elements array (always has at least one element + FIN) */
 	Assert(from->numElements >= 2);
-	newnode->elements = palloc(from->numElements * sizeof(RPRPatternElement));
+	newnode->elements = palloc_array(RPRPatternElement, from->numElements);
 	memcpy(newnode->elements, from->elements,
 		   from->numElements * sizeof(RPRPatternElement));
 
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
index 5bbde5bcad2..6c39c6fe06d 100644
--- a/src/backend/nodes/readfuncs.c
+++ b/src/backend/nodes/readfuncs.c
@@ -582,7 +582,7 @@ _readRPRPattern(void)
 	token = pg_strtok(&length); /* get '(' or '<>' */
 	if (local_node->numVars > 0 && token[0] == '(')
 	{
-		local_node->varNames = palloc(local_node->numVars * sizeof(char *));
+		local_node->varNames = palloc_array(char *, local_node->numVars);
 		for (int i = 0; i < local_node->numVars; i++)
 		{
 			token = pg_strtok(&length);
@@ -600,7 +600,7 @@ _readRPRPattern(void)
 	token = pg_strtok(&length); /* get '(' or '<>' */
 	if (local_node->numElements > 0 && token[0] == '(')
 	{
-		local_node->elements = palloc0(local_node->numElements * sizeof(RPRPatternElement));
+		local_node->elements = palloc0_array(RPRPatternElement, local_node->numElements);
 		for (int i = 0; i < local_node->numElements; i++)
 		{
 			RPRPatternElement *elem = &local_node->elements[i];
diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index 143ea034fb3..9364136b071 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -1048,8 +1048,7 @@ scanRPRPatternRecursive(RPRPatternNode *node, char **varNames, int *numVars,
 						  depth, RPR_DEPTH_MAX - 1));
 
 	/* Track maximum depth */
-	if (depth > *maxDepth)
-		*maxDepth = depth;
+	*maxDepth = Max(*maxDepth, depth);
 
 	switch (node->nodeType)
 	{
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 2b8439e452e..e8087d4316a 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -7140,7 +7140,8 @@ append_pattern_quantifier(StringInfo buf, RPRPatternNode *node)
 	if (node->reluctant)
 	{
 		if (!has_quantifier)
-			appendStringInfo(buf, "{1}");	/* make reluctant ? unambiguous */
+			appendStringInfoString(buf, "{1}"); /* make reluctant ?
+												 * unambiguous */
 		appendStringInfoChar(buf, '?');
 	}
 }
-- 
2.50.1 (Apple Git-155)


From 033b6d0bd42a41b2501d34fb413cb889caffa366 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Mon, 8 Jun 2026 16:34:09 +0900
Subject: [PATCH 67/68] Add row pattern recognition negative and coverage tests

Cover the split-token error paths after a quantifier (* / + / Op and
the {n}/{n,}/{,m}/{n,m} range forms), rejection of set-returning and
window functions in a DEFINE clause, and the reluctant-GROUP case where
quantifier multiplication is correctly suppressed.  Tests only.
---
 src/test/regress/expected/rpr_base.out | 91 ++++++++++++++++++++++++--
 src/test/regress/sql/rpr_base.sql      | 37 +++++++++--
 2 files changed, 120 insertions(+), 8 deletions(-)

diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index f1767305d06..2407c455164 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -3283,7 +3283,48 @@ ERROR:  unsupported quantifier "+!"
 LINE 6:     PATTERN (A+!)
                       ^
 HINT:  Valid quantifiers are: *, +, ?, *?, +?, ??, {n}, {n,}, {,m}, {n,m} and their reluctant versions.
--- Expected: Syntax error
+-- none of the following 4 queries should be accepted
+SELECT FROM rpr_err WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A+ !) DEFINE A AS TRUE);
+ERROR:  invalid token after "+" quantifier
+LINE 1: ...S BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A+ !) DEFINE ...
+                                                             ^
+HINT:  Did you mean "+?" for reluctant quantifier?
+SELECT FROM rpr_err WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A+ ?+) DEFINE A AS TRUE);
+ERROR:  invalid token after "+" quantifier
+LINE 1: ...S BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A+ ?+) DEFINE...
+                                                             ^
+HINT:  Did you mean "+?" for reluctant quantifier?
+SELECT FROM rpr_err WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A* ?+) DEFINE A AS TRUE);
+ERROR:  invalid token after "*" quantifier
+LINE 1: ...S BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A* ?+) DEFINE...
+                                                             ^
+HINT:  Did you mean "*?" for reluctant quantifier?
+SELECT FROM rpr_err WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A? ??) DEFINE A AS TRUE);
+ERROR:  invalid quantifier combination
+LINE 1: ...OWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A? ??) DEFI...
+                                                             ^
+HINT:  Did you mean "??" for reluctant quantifier?
+-- none of the following 4 range-quantifier queries should be accepted
+SELECT FROM rpr_err WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A{2} !) DEFINE A AS TRUE);
+ERROR:  invalid token after range quantifier
+LINE 1: ...BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A{2} !) DEFINE ...
+                                                             ^
+HINT:  Only "?" is allowed after {n} to make it reluctant.
+SELECT FROM rpr_err WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A{2,} !) DEFINE A AS TRUE);
+ERROR:  invalid token after range quantifier
+LINE 1: ...ETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A{2,} !) DEFINE ...
+                                                             ^
+HINT:  Only "?" is allowed after {n,} or {,m} to make it reluctant.
+SELECT FROM rpr_err WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A{,3} !) DEFINE A AS TRUE);
+ERROR:  invalid token after range quantifier
+LINE 1: ...ETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A{,3} !) DEFINE ...
+                                                             ^
+HINT:  Only "?" is allowed after {n,} or {,m} to make it reluctant.
+SELECT FROM rpr_err WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A{2,3} !) DEFINE A AS TRUE);
+ERROR:  invalid token after range quantifier
+LINE 1: ...TWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A{2,3} !) DEFINE ...
+                                                             ^
+HINT:  Only "?" is allowed after {n,m} to make it reluctant.
 -- Unmatched parentheses
 SET client_min_messages = NOTICE;
 DO $$
@@ -3310,7 +3351,6 @@ WINDOW w AS (
 ERROR:  syntax error at or near ")"
 LINE 8: );
         ^
--- Expected: Syntax error
 -- ERROR: empty PATTERN not allowed
 SELECT COUNT(*) OVER w
 FROM rpr_err
@@ -3323,7 +3363,6 @@ WINDOW w AS (
 ERROR:  syntax error at or near ")"
 LINE 6:     PATTERN ()
                      ^
--- Expected: Syntax error
 -- ERROR: DEFINE without PATTERN (PATTERN and DEFINE must be used together)
 SELECT COUNT(*) OVER w
 FROM rpr_err
@@ -3335,7 +3374,6 @@ WINDOW w AS (
 ERROR:  syntax error at or near "DEFINE"
 LINE 6:     DEFINE A AS val > 0
             ^
--- Expected: Syntax error
 -- Qualified column references (NOT SUPPORTED)
 -- Pattern variable qualified name: not supported (valid per ISO/IEC 19075-5 6.15 / 4.16, not yet implemented)
 SELECT COUNT(*) OVER w
@@ -3484,6 +3522,18 @@ WINDOW w AS (
 ERROR:  aggregate functions are not allowed in DEFINE
 LINE 7:     DEFINE A AS COUNT(*) > 0
                         ^
+-- ERROR: set-returning function in DEFINE is not supported
+SELECT FROM rpr_err
+WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A+) DEFINE A AS 1 > generate_series(1 ,2));
+ERROR:  set-returning functions are not allowed in DEFINE
+LINE 2: ... ROW AND 1 FOLLOWING PATTERN (A+) DEFINE A AS 1 > generate_s...
+                                                             ^
+-- ERROR: window function in DEFINE is not supported
+SELECT FROM rpr_err
+WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A+) DEFINE A AS 1 > row_number() OVER ());
+ERROR:  window functions are not allowed in DEFINE
+LINE 2: ... ROW AND 1 FOLLOWING PATTERN (A+) DEFINE A AS 1 > row_number...
+                                                             ^
 -- Subquery in DEFINE is not supported
 SELECT COUNT(*) OVER w
 FROM rpr_err
@@ -3925,6 +3975,39 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
          ->  Seq Scan on rpr_plan
 (7 rows)
 
+-- Quantifier NO multiply: reluctant GROUP child (((A B){2}?){3}) stays nested
+-- a reluctant quantifier on a GROUP is not subject to multiplication
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN (((A B){2}?){3}) DEFINE A AS val > 0, B AS val > 0);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: ((a b){2}?){3}
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 rows)
+
+-- Quantifier multiply control: greedy GROUP (((A B){2}){3}) -> (a b){6}
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN (((A B){2}){3}) DEFINE A AS val > 0, B AS val > 0);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a b){6}
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 rows)
+
 -- Quantifier multiply with child range: (A{2,3}){3} -> a{6,9}
 -- outer exact, child range - optimization applies
 EXPLAIN (COSTS OFF)
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index 7dc13232d89..cec7ea1e8db 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -2145,7 +2145,18 @@ WINDOW w AS (
     PATTERN (A+!)
     DEFINE A AS val > 0
 );
--- Expected: Syntax error
+
+-- none of the following 4 queries should be accepted
+SELECT FROM rpr_err WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A+ !) DEFINE A AS TRUE);
+SELECT FROM rpr_err WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A+ ?+) DEFINE A AS TRUE);
+SELECT FROM rpr_err WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A* ?+) DEFINE A AS TRUE);
+SELECT FROM rpr_err WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A? ??) DEFINE A AS TRUE);
+
+-- none of the following 4 range-quantifier queries should be accepted
+SELECT FROM rpr_err WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A{2} !) DEFINE A AS TRUE);
+SELECT FROM rpr_err WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A{2,} !) DEFINE A AS TRUE);
+SELECT FROM rpr_err WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A{,3} !) DEFINE A AS TRUE);
+SELECT FROM rpr_err WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A{2,3} !) DEFINE A AS TRUE);
 
 -- Unmatched parentheses
 SET client_min_messages = NOTICE;
@@ -2170,7 +2181,6 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE
 );
--- Expected: Syntax error
 
 -- ERROR: empty PATTERN not allowed
 SELECT COUNT(*) OVER w
@@ -2181,7 +2191,6 @@ WINDOW w AS (
     PATTERN ()
     DEFINE A AS val > 0
 );
--- Expected: Syntax error
 
 -- ERROR: DEFINE without PATTERN (PATTERN and DEFINE must be used together)
 SELECT COUNT(*) OVER w
@@ -2191,7 +2200,6 @@ WINDOW w AS (
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
     DEFINE A AS val > 0
 );
--- Expected: Syntax error
 
 -- Qualified column references (NOT SUPPORTED)
 
@@ -2315,6 +2323,14 @@ WINDOW w AS (
     DEFINE A AS COUNT(*) > 0
 );
 
+-- ERROR: set-returning function in DEFINE is not supported
+SELECT FROM rpr_err
+WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A+) DEFINE A AS 1 > generate_series(1 ,2));
+
+-- ERROR: window function in DEFINE is not supported
+SELECT FROM rpr_err
+WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A+) DEFINE A AS 1 > row_number() OVER ());
+
 -- Subquery in DEFINE is not supported
 SELECT COUNT(*) OVER w
 FROM rpr_err
@@ -2549,6 +2565,19 @@ SELECT COUNT(*) OVER w FROM rpr_plan
 WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
              PATTERN ((A{2}){3}) DEFINE A AS val > 0);
 
+-- Quantifier NO multiply: reluctant GROUP child (((A B){2}?){3}) stays nested
+-- a reluctant quantifier on a GROUP is not subject to multiplication
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN (((A B){2}?){3}) DEFINE A AS val > 0, B AS val > 0);
+
+-- Quantifier multiply control: greedy GROUP (((A B){2}){3}) -> (a b){6}
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN (((A B){2}){3}) DEFINE A AS val > 0, B AS val > 0);
+
 -- Quantifier multiply with child range: (A{2,3}){3} -> a{6,9}
 -- outer exact, child range - optimization applies
 EXPLAIN (COSTS OFF)
-- 
2.50.1 (Apple Git-155)


From 5221784ac469bb213fae2853beab7f5b2b4562cd Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Mon, 8 Jun 2026 16:22:16 +0900
Subject: [PATCH 66/68] Tidy up row pattern recognition regression test
 comments

Drop the "-- Expected: ERROR: ..." comments that repeat exact error text
(they go stale; the .out already has the real error) and reword comments
that embedded literal limits.  Rename the "Jacob's Patterns" section and
clarify the serialization section's deparse/re-parse round-trip intent.

Test comments only; rpr_base.out regenerated, no output changes.
---
 src/test/regress/expected/rpr_base.out | 169 +++++++----------------
 src/test/regress/sql/rpr_base.sql      | 184 +++++++------------------
 2 files changed, 102 insertions(+), 251 deletions(-)

diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index 1fcb2ce22f0..f1767305d06 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -12,7 +12,7 @@
 --   Quantifiers Tests
 --   Navigation Functions Tests
 --   SKIP TO / INITIAL Tests
---   Serialization/Deserialization Tests (objects kept for pg_upgrade/pg_dump)
+--   Serialization/Deserialization Tests
 --   Glued Quantifier / Alternation Tests
 --   Error Cases Tests
 --   Window Deduplication Tests
@@ -34,7 +34,7 @@
 --   Error Limit Tests
 --
 -- Contributed Tests:
---   Jacob's Patterns
+--   Basic Pattern Matching
 --   Pathological Patterns
 -- ============================================================
 SET client_min_messages = WARNING;
@@ -50,6 +50,7 @@ CREATE TABLE rpr_keywords (
     past INT,        -- PAST keyword
     pattern INT,     -- PATTERN keyword
     seek INT,        -- SEEK keyword
+-- ERROR: SEEK is not supported
     skip INT         -- SKIP keyword (pre-existing)
 );
 INSERT INTO rpr_keywords VALUES (1, 10, 20, 30, 40, 50, 60);
@@ -224,7 +225,7 @@ DROP TABLE rpr_auto;
 -- Duplicate variable names
 CREATE TABLE rpr_dup (id INT);
 INSERT INTO rpr_dup VALUES (1), (2);
--- Duplicate DEFINE entries
+-- Duplicate DEFINE variable name is not allowed
 SELECT COUNT(*) OVER w
 FROM rpr_dup
 WINDOW w AS (
@@ -236,12 +237,11 @@ WINDOW w AS (
 ERROR:  DEFINE variable "a" appears more than once
 LINE 7:     DEFINE A AS id > 0, A AS id < 10
                    ^
--- Expected: ERROR: row pattern definition variable name "a" appears more than once in DEFINE clause
 DROP TABLE rpr_dup;
 -- Boolean coercion
 CREATE TABLE rpr_bool (id INT, flag BOOLEAN);
 INSERT INTO rpr_bool VALUES (1, true), (2, false);
--- Non-boolean expression
+-- DEFINE clause must be a boolean expression
 SELECT COUNT(*) OVER w
 FROM rpr_bool
 WINDOW w AS (
@@ -253,7 +253,6 @@ WINDOW w AS (
 ERROR:  argument of DEFINE must be type boolean, not type integer
 LINE 7:     DEFINE A AS id
                         ^
--- Expected: ERROR: argument of DEFINE must be type boolean
 -- Boolean column reference
 SELECT id, flag, COUNT(*) OVER w as cnt
 FROM rpr_bool
@@ -393,8 +392,7 @@ ORDER BY id;
   6 |  30 |   1
 (6 rows)
 
--- Invalid frame start positions
--- Not starting at CURRENT ROW
+-- ERROR: frame must start at current row when row pattern recognition is used
 SELECT COUNT(*) OVER w
 FROM rpr_frame
 WINDOW w AS (
@@ -408,7 +406,6 @@ LINE 5:     ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
             ^
 DETAIL:  Current frame starts with UNBOUNDED PRECEDING.
 HINT:  Use: ROWS BETWEEN CURRENT ROW AND ...
--- Expected: ERROR: FRAME must start at current row when row pattern recognition is used
 -- EXCLUDE options
 -- EXCLUDE not permitted
 SELECT COUNT(*) OVER w
@@ -425,7 +422,6 @@ LINE 6:     EXCLUDE CURRENT ROW
             ^
 DETAIL:  Frame definition includes EXCLUDE CURRENT ROW.
 HINT:  Remove the EXCLUDE clause from the window definition.
--- Expected: ERROR: cannot use EXCLUDE options with row pattern recognition
 -- EXCLUDE GROUP not permitted
 SELECT COUNT(*) OVER w
 FROM rpr_frame
@@ -441,7 +437,6 @@ LINE 6:     EXCLUDE GROUP
             ^
 DETAIL:  Frame definition includes EXCLUDE GROUP.
 HINT:  Remove the EXCLUDE clause from the window definition.
--- Expected: ERROR: cannot use EXCLUDE options with row pattern recognition
 -- EXCLUDE TIES not permitted
 SELECT COUNT(*) OVER w
 FROM rpr_frame
@@ -457,8 +452,7 @@ LINE 6:     EXCLUDE TIES
             ^
 DETAIL:  Frame definition includes EXCLUDE TIES.
 HINT:  Remove the EXCLUDE clause from the window definition.
--- Expected: ERROR: cannot use EXCLUDE options with row pattern recognition
--- RANGE frame not starting at CURRENT ROW
+-- range frame is not allowed with RPR
 SELECT COUNT(*) OVER w
 FROM rpr_frame
 WINDOW w AS (
@@ -471,8 +465,7 @@ ERROR:  cannot use FRAME option RANGE with row pattern recognition
 LINE 5:     RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWIN...
             ^
 HINT:  Use ROWS instead.
--- Expected: ERROR: cannot use FRAME option RANGE with row pattern recognition
--- GROUPS frame not starting at CURRENT ROW
+-- GROUPS frame is not allowed with RPR
 SELECT COUNT(*) OVER w
 FROM rpr_frame
 WINDOW w AS (
@@ -485,8 +478,7 @@ ERROR:  cannot use FRAME option GROUPS with row pattern recognition
 LINE 5:     GROUPS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWI...
             ^
 HINT:  Use ROWS instead.
--- Expected: ERROR: cannot use FRAME option GROUPS with row pattern recognition
--- Starting with N PRECEDING
+-- ERROR: frame must start at current row when row pattern recognition is used
 SELECT COUNT(*) OVER w
 FROM rpr_frame
 WINDOW w AS (
@@ -500,8 +492,7 @@ LINE 5:     ROWS BETWEEN 1 PRECEDING AND UNBOUNDED FOLLOWING
             ^
 DETAIL:  Current frame starts with offset PRECEDING.
 HINT:  Use: ROWS BETWEEN CURRENT ROW AND ...
--- Expected: ERROR: FRAME must start at current row when row pattern recognition is used
--- Starting with N FOLLOWING
+-- ERROR: frame must start at current row with RPR
 SELECT COUNT(*) OVER w
 FROM rpr_frame
 WINDOW w AS (
@@ -515,9 +506,7 @@ LINE 5:     ROWS BETWEEN 1 FOLLOWING AND UNBOUNDED FOLLOWING
             ^
 DETAIL:  Current frame starts with offset FOLLOWING.
 HINT:  Use: ROWS BETWEEN CURRENT ROW AND ...
--- Expected: ERROR: FRAME must start at current row when row pattern recognition is used
--- Frame end bound edge cases
--- End before start: CURRENT ROW AND 1 PRECEDING
+-- ERROR: end before start: CURRENT ROW AND 1 PRECEDING
 SELECT COUNT(*) OVER w
 FROM rpr_frame
 WINDOW w AS (
@@ -529,8 +518,7 @@ WINDOW w AS (
 ERROR:  frame starting from current row cannot have preceding rows
 LINE 5:     ROWS BETWEEN CURRENT ROW AND 1 PRECEDING
                                          ^
--- Expected: ERROR: frame starting from current row cannot have preceding rows
--- End before start: CURRENT ROW AND UNBOUNDED PRECEDING
+-- ERROR: end before start: CURRENT ROW AND UNBOUNDED PRECEDING
 SELECT COUNT(*) OVER w
 FROM rpr_frame
 WINDOW w AS (
@@ -542,7 +530,6 @@ WINDOW w AS (
 ERROR:  frame end cannot be UNBOUNDED PRECEDING
 LINE 5:     ROWS BETWEEN CURRENT ROW AND UNBOUNDED PRECEDING
                                          ^
--- Expected: ERROR: frame end cannot be UNBOUNDED PRECEDING
 -- Single row frame: CURRENT ROW AND CURRENT ROW is rejected (the standard
 -- allows only UNBOUNDED FOLLOWING or a positive offset FOLLOWING).
 SELECT id, val, COUNT(*) OVER w as cnt
@@ -559,7 +546,6 @@ ERROR:  cannot use CURRENT ROW as frame end with row pattern recognition
 LINE 5:     ROWS BETWEEN CURRENT ROW AND CURRENT ROW
             ^
 HINT:  Use UNBOUNDED FOLLOWING or a positive offset FOLLOWING.
--- Expected: ERROR: cannot use CURRENT ROW as frame end with row pattern recognition
 -- Zero offset: CURRENT ROW AND 0 FOLLOWING denotes the same one-row frame
 -- and is likewise rejected (caught at execution time).
 SELECT id, val, COUNT(*) OVER w as cnt
@@ -573,7 +559,6 @@ WINDOW w AS (
 )
 ORDER BY id;
 ERROR:  frame ending offset must be positive with row pattern recognition
--- Expected: ERROR: frame ending offset must be positive with row pattern recognition
 -- A non-constant frame end offset is allowed; a zero value is still rejected,
 -- this time at execution time (a literal cannot exercise that path).
 PREPARE rpr_end_offset(int8) AS
@@ -600,7 +585,6 @@ EXECUTE rpr_end_offset(2);
 
 EXECUTE rpr_end_offset(0);
 ERROR:  frame ending offset must be positive with row pattern recognition
--- Expected: ERROR: frame ending offset must be positive with row pattern recognition
 DEALLOCATE rpr_end_offset;
 -- Large offset: CURRENT ROW AND 1000 FOLLOWING
 SELECT id, val, COUNT(*) OVER w as cnt
@@ -669,7 +653,7 @@ ORDER BY id;
   6 |  30 |   1
 (6 rows)
 
--- RANGE frame with RPR (not permitted)
+-- range frame is not allowed with RPR
 SELECT id, val, COUNT(*) OVER w as cnt
 FROM rpr_frame
 WINDOW w AS (
@@ -684,7 +668,6 @@ ERROR:  cannot use FRAME option RANGE with row pattern recognition
 LINE 5:     RANGE BETWEEN CURRENT ROW AND 10 FOLLOWING
             ^
 HINT:  Use ROWS instead.
--- Expected: ERROR: cannot use FRAME option RANGE with row pattern recognition
 -- GROUPS frame with RPR (not permitted)
 SELECT id, val, COUNT(*) OVER w as cnt
 FROM rpr_frame
@@ -700,7 +683,6 @@ ERROR:  cannot use FRAME option GROUPS with row pattern recognition
 LINE 5:     GROUPS BETWEEN CURRENT ROW AND 1 FOLLOWING
             ^
 HINT:  Use ROWS instead.
--- Expected: ERROR: cannot use FRAME option GROUPS with row pattern recognition
 DROP TABLE rpr_frame;
 -- ============================================================
 -- PARTITION BY + FRAME Tests
@@ -749,7 +731,6 @@ ERROR:  cannot use FRAME option RANGE with row pattern recognition
 LINE 6:     RANGE BETWEEN CURRENT ROW AND 10 FOLLOWING
             ^
 HINT:  Use ROWS instead.
--- Expected: ERROR: cannot use FRAME option RANGE with row pattern recognition
 DROP TABLE rpr_partition;
 -- ============================================================
 -- PATTERN Syntax Tests
@@ -1011,7 +992,6 @@ ORDER BY id;
 ERROR:  quantifier bound must be between 1 and 2147483646
 LINE 6:     PATTERN (A{0} B)
                        ^
--- Expected: ERROR: quantifier bound must be between 1 and 2147483646
 -- {0,0} is not allowed (max must be >= 1)
 SELECT id, val, COUNT(*) OVER w as cnt
 FROM rpr_quant
@@ -1025,7 +1005,6 @@ ORDER BY id;
 ERROR:  quantifier bounds must be between 0 and 2147483646 with max >= 1
 LINE 6:     PATTERN (A{0,0} B)
                        ^
--- Expected: ERROR: quantifier bounds must be between 0 and 2147483646 with max >= 1
 -- {0,1} (equivalent to ?)
 SELECT id, val, COUNT(*) OVER w as cnt
 FROM rpr_quant
@@ -1155,6 +1134,7 @@ DROP TABLE rpr_quant;
 CREATE TABLE rpr_reluctant (id INT, val INT);
 INSERT INTO rpr_reluctant VALUES (1, 10), (2, 20), (3, 30);
 -- *? (zero or more, reluctant)
+-- Reluctant quantifier: prefer shortest match
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1170,8 +1150,8 @@ WINDOW w AS (
      0
 (3 rows)
 
--- Reluctant quantifier: prefer shortest match
 -- +? (one or more, reluctant)
+-- Reluctant quantifier: prefer shortest match
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1187,8 +1167,8 @@ WINDOW w AS (
      1
 (3 rows)
 
--- Reluctant quantifier: prefer shortest match
 -- ?? (zero or one, reluctant)
+-- Reluctant quantifier: prefer shortest match
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1204,8 +1184,8 @@ WINDOW w AS (
      0
 (3 rows)
 
--- Reluctant quantifier: prefer shortest match
 -- {n,}? (n or more, reluctant)
+-- Reluctant quantifier: prefer shortest match
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1221,8 +1201,8 @@ WINDOW w AS (
      0
 (3 rows)
 
--- Reluctant quantifier: prefer shortest match
 -- {n,m}? (n to m, reluctant)
+-- Reluctant quantifier: prefer shortest match
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1238,8 +1218,8 @@ WINDOW w AS (
      1
 (3 rows)
 
--- Reluctant quantifier: prefer shortest match
 -- {n}? (exactly n, reluctant)
+-- Reluctant quantifier: prefer shortest match
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1255,8 +1235,8 @@ WINDOW w AS (
      0
 (3 rows)
 
--- Reluctant quantifier: prefer shortest match
 -- {,m}? (up to m, reluctant) - COMPLETELY UNTESTED RULE!
+-- Reluctant quantifier: prefer shortest match
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1272,8 +1252,6 @@ WINDOW w AS (
      0
 (3 rows)
 
--- Reluctant quantifier: prefer shortest match
--- Invalid reluctant patterns (wrong token after quantifier)
 -- {2}+ (should be {2}? not {2}+)
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
@@ -1286,7 +1264,6 @@ WINDOW w AS (
 ERROR:  syntax error at or near "+"
 LINE 6:     PATTERN (A{2}+)
                          ^
--- Expected: ERROR: syntax error at or near "+"
 -- {2,}* (should be {2,}? not {2,}*)
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
@@ -1299,7 +1276,6 @@ WINDOW w AS (
 ERROR:  syntax error at or near "*"
 LINE 6:     PATTERN (A{2,}*)
                           ^
--- Expected: ERROR: syntax error at or near "*"
 -- {,3}* (should be {,3}? not {,3}*)
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
@@ -1312,7 +1288,6 @@ WINDOW w AS (
 ERROR:  syntax error at or near "*"
 LINE 6:     PATTERN (A{,3}*)
                           ^
--- Expected: ERROR: syntax error at or near "*"
 -- {1,3}+ (should be {1,3}? not {1,3}+)
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
@@ -1325,9 +1300,8 @@ WINDOW w AS (
 ERROR:  syntax error at or near "+"
 LINE 6:     PATTERN (A{1,3}+)
                            ^
--- Expected: ERROR: syntax error at or near "+"
 -- Boundary errors in reluctant quantifiers
--- {-1}? (negative bound)
+-- negative bound is not allowed
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1339,8 +1313,7 @@ WINDOW w AS (
 ERROR:  syntax error at or near "-"
 LINE 6:     PATTERN (A{-1}?)
                        ^
--- Expected: ERROR: syntax error at or near "-"
--- {2147483647}? (INT_MAX)
+-- ERROR: quantifier bound exceeds limits
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1352,8 +1325,7 @@ WINDOW w AS (
 ERROR:  quantifier bound must be between 1 and 2147483646
 LINE 6:     PATTERN (A{2147483647}?)
                        ^
--- Expected: ERROR: quantifier bound must be between 1 and 2147483646
--- {-1,}? (negative lower bound)
+-- negative lower bound is not allowed
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1365,8 +1337,7 @@ WINDOW w AS (
 ERROR:  syntax error at or near "-"
 LINE 6:     PATTERN (A{-1,}?)
                        ^
--- Expected: ERROR: syntax error at or near "-"
--- {2147483647,}? (INT_MAX lower bound)
+-- ERROR: quantifier lower bound exceeds limits
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1378,8 +1349,7 @@ WINDOW w AS (
 ERROR:  quantifier bound must be between 0 and 2147483646
 LINE 6:     PATTERN (A{2147483647,}?)
                        ^
--- Expected: ERROR: quantifier bound must be between 0 and 2147483646
--- {,0}? (zero upper bound)
+-- zero upper bound is not allowed
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1391,8 +1361,7 @@ WINDOW w AS (
 ERROR:  quantifier bound must be between 1 and 2147483646
 LINE 6:     PATTERN (A{,0}?)
                         ^
--- Expected: ERROR: quantifier bound must be between 1 and 2147483646
--- {,2147483647}? (INT_MAX upper bound)
+-- ERROR: {,2147483647}? (upper bound in range exceeds limits)
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1404,8 +1373,7 @@ WINDOW w AS (
 ERROR:  quantifier bound must be between 1 and 2147483646
 LINE 6:     PATTERN (A{,2147483647}?)
                         ^
--- Expected: ERROR: quantifier bound must be between 1 and 2147483646
--- {-1,3}? (negative lower in range)
+-- ERROR: {-1,3}? (negative lower bound in range is not allowed)
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1417,8 +1385,7 @@ WINDOW w AS (
 ERROR:  syntax error at or near "-"
 LINE 6:     PATTERN (A{-1,3}?)
                        ^
--- Expected: ERROR: syntax error at or near "-"
--- {1,2147483647}? (INT_MAX upper in range)
+-- ERROR: {1,2147483647}? (upper bound in range exceeds limits)
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1430,8 +1397,7 @@ WINDOW w AS (
 ERROR:  quantifier bounds must be between 0 and 2147483646 with max >= 1
 LINE 6:     PATTERN (A{1,2147483647}?)
                        ^
--- Expected: ERROR: quantifier bounds must be between 0 and 2147483646 with max >= 1
--- {5,3}? (min > max)
+-- ERROR: {5,3}? (min > max is not allowed)
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1443,10 +1409,10 @@ WINDOW w AS (
 ERROR:  quantifier minimum bound must not exceed maximum
 LINE 6:     PATTERN (A{5,3}?)
                        ^
--- Expected: ERROR: quantifier minimum bound must not exceed maximum
 -- Token-separated reluctant quantifiers (space between quantifier and ?)
 -- These may be tokenized differently by the lexer
 -- * ? (token separated)
+-- Reluctant quantifier: prefer shortest match
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1462,8 +1428,8 @@ WINDOW w AS (
      0
 (3 rows)
 
--- Reluctant quantifier: prefer shortest match
 -- + ? (token separated)
+-- Reluctant quantifier: prefer shortest match
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1479,8 +1445,8 @@ WINDOW w AS (
      1
 (3 rows)
 
--- Reluctant quantifier: prefer shortest match
 -- {2,} ? (token separated)
+-- Reluctant quantifier: prefer shortest match
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1496,8 +1462,6 @@ WINDOW w AS (
      0
 (3 rows)
 
--- Reluctant quantifier: prefer shortest match
--- Invalid token combinations
 -- * + (invalid combination)
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
@@ -1510,7 +1474,6 @@ WINDOW w AS (
 ERROR:  syntax error at or near "+"
 LINE 6:     PATTERN (A* +)
                         ^
--- Expected: ERROR: syntax error at or near "+"
 -- + * (invalid combination)
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
@@ -1523,8 +1486,8 @@ WINDOW w AS (
 ERROR:  syntax error at or near "*"
 LINE 6:     PATTERN (A+ *)
                         ^
--- Expected: ERROR: syntax error at or near "*"
 -- ? ? (parsed as ?? reluctant quantifier)
+-- Reluctant quantifier: prefer shortest match
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1540,12 +1503,11 @@ WINDOW w AS (
      0
 (3 rows)
 
--- Reluctant quantifier: prefer shortest match
 DROP TABLE rpr_reluctant;
 -- Quantifier boundary conditions
 CREATE TABLE rpr_bounds (id INT);
 INSERT INTO rpr_bounds VALUES (1), (2);
--- min > max
+-- ERROR: quantifier lower bound must not exceed upper bound
 SELECT COUNT(*) OVER w
 FROM rpr_bounds
 WINDOW w AS (
@@ -1557,7 +1519,6 @@ WINDOW w AS (
 ERROR:  quantifier minimum bound must not exceed maximum
 LINE 6:     PATTERN (A{5,3})
                        ^
--- Expected: ERROR: quantifier minimum bound must not exceed maximum
 -- Large bounds
 SELECT COUNT(*) OVER w
 FROM rpr_bounds
@@ -1603,7 +1564,7 @@ WINDOW w AS (
      0
 (2 rows)
 
--- INT_MAX = 2147483647 (over limit)
+-- ERROR: quantifier bound exceeds limits
 SELECT COUNT(*) OVER w
 FROM rpr_bounds
 WINDOW w AS (
@@ -1615,9 +1576,8 @@ WINDOW w AS (
 ERROR:  quantifier bound must be between 1 and 2147483646
 LINE 6:     PATTERN (A{2147483647})
                        ^
--- Expected: ERROR: quantifier bound must be between 1 and 2147483646
 -- {n,} boundary errors
--- Negative lower bound in {n,}
+-- ERROR: negative lower bound in {n,} is not allowed
 SELECT COUNT(*) OVER w
 FROM rpr_bounds
 WINDOW w AS (
@@ -1629,8 +1589,7 @@ WINDOW w AS (
 ERROR:  syntax error at or near "-"
 LINE 6:     PATTERN (A{-1,})
                        ^
--- Expected: ERROR: syntax error at or near "-"
--- INT_MAX in {n,}
+-- ERROR: quantifier lower bound exceeds limits
 SELECT COUNT(*) OVER w
 FROM rpr_bounds
 WINDOW w AS (
@@ -1642,7 +1601,6 @@ WINDOW w AS (
 ERROR:  quantifier bound must be between 0 and 2147483646
 LINE 6:     PATTERN (A{2147483647,})
                        ^
--- Expected: ERROR: quantifier bound must be between 0 and 2147483646
 -- {,m} boundary errors
 -- Zero upper bound in {,m}
 SELECT COUNT(*) OVER w
@@ -1656,8 +1614,7 @@ WINDOW w AS (
 ERROR:  quantifier bound must be between 1 and 2147483646
 LINE 6:     PATTERN (A{,0})
                         ^
--- Expected: ERROR: quantifier bound must be between 1 and 2147483646
--- INT_MAX in {,m}
+-- ERROR: quantifier upper bound exceeds limits
 SELECT COUNT(*) OVER w
 FROM rpr_bounds
 WINDOW w AS (
@@ -1669,7 +1626,6 @@ WINDOW w AS (
 ERROR:  quantifier bound must be between 1 and 2147483646
 LINE 6:     PATTERN (A{,2147483647})
                         ^
--- Expected: ERROR: quantifier bound must be between 1 and 2147483646
 DROP TABLE rpr_bounds;
 -- ============================================================
 -- Navigation Functions Tests (PREV / NEXT / FIRST / LAST)
@@ -1756,7 +1712,6 @@ ORDER BY id;
 ERROR:  cannot use prev outside a DEFINE clause
 LINE 1: SELECT PREV(id), id, val, COUNT(*) OVER w as cnt
                ^
--- Expected: ERROR: cannot use prev outside a DEFINE clause
 -- NEXT function cannot be used other than in DEFINE
 SELECT NEXT(id), id, val, COUNT(*) OVER w as cnt
 FROM rpr_nav
@@ -1772,7 +1727,6 @@ ORDER BY id;
 ERROR:  cannot use next outside a DEFINE clause
 LINE 1: SELECT NEXT(id), id, val, COUNT(*) OVER w as cnt
                ^
--- Expected: ERROR: cannot use next outside a DEFINE clause
 -- FIRST function - reference match_start row
 SELECT id, val, COUNT(*) OVER w as cnt
 FROM rpr_nav
@@ -1841,13 +1795,11 @@ SELECT FIRST(id), id, val FROM rpr_nav;
 ERROR:  cannot use first outside a DEFINE clause
 LINE 1: SELECT FIRST(id), id, val FROM rpr_nav;
                ^
--- Expected: ERROR: cannot use first outside a DEFINE clause
 -- LAST function cannot be used other than in DEFINE
 SELECT LAST(id), id, val FROM rpr_nav;
 ERROR:  cannot use last outside a DEFINE clause
 LINE 1: SELECT LAST(id), id, val FROM rpr_nav;
                ^
--- Expected: ERROR: cannot use last outside a DEFINE clause
 DROP TABLE rpr_nav;
 -- ============================================================
 -- SKIP TO / INITIAL Tests
@@ -2033,12 +1985,13 @@ ERROR:  SEEK is not supported
 LINE 6:     SEEK
             ^
 HINT:  Use INITIAL instead.
--- Expected: ERROR: SEEK is not supported
--- HINT: Use INITIAL instead.
 DROP TABLE rpr_seek;
 -- ============================================================
 -- Serialization/Deserialization Tests
 -- ============================================================
+-- RPR-defining views and tables here are intentionally left in place (not
+-- dropped) so that pg_dump/pg_upgrade exercise the deparse-then-re-parse
+-- round-trip of the RPR window clause.
 -- View creation and deparsing
 CREATE TABLE rpr_serial (id INT, val INT);
 INSERT INTO rpr_serial VALUES
@@ -3317,7 +3270,6 @@ DROP TABLE rpr_glue;
 DROP TABLE IF EXISTS rpr_err;
 CREATE TABLE rpr_err (id INT, val INT);
 INSERT INTO rpr_err VALUES (1, 10), (2, 20);
--- Syntax errors
 -- Invalid quantifier syntax
 SELECT COUNT(*) OVER w
 FROM rpr_err
@@ -3346,7 +3298,7 @@ EXCEPTION
 END $$;
 NOTICE:  Unmatched parentheses: EXPECTED ERROR - syntax error at or near "AS"
 SET client_min_messages = WARNING;
--- Empty DEFINE
+-- ERROR: empty DEFINE not allowed
 SELECT COUNT(*) OVER w
 FROM rpr_err
 WINDOW w AS (
@@ -3359,7 +3311,7 @@ ERROR:  syntax error at or near ")"
 LINE 8: );
         ^
 -- Expected: Syntax error
--- Empty PATTERN
+-- ERROR: empty PATTERN not allowed
 SELECT COUNT(*) OVER w
 FROM rpr_err
 WINDOW w AS (
@@ -3372,7 +3324,7 @@ ERROR:  syntax error at or near ")"
 LINE 6:     PATTERN ()
                      ^
 -- Expected: Syntax error
--- DEFINE without PATTERN (PATTERN and DEFINE must be used together)
+-- ERROR: DEFINE without PATTERN (PATTERN and DEFINE must be used together)
 SELECT COUNT(*) OVER w
 FROM rpr_err
 WINDOW w AS (
@@ -3397,7 +3349,6 @@ WINDOW w AS (
 ERROR:  pattern variable qualified expression "a.val" is not supported in DEFINE clause
 LINE 7:     DEFINE A AS A.val > 0
                         ^
--- Expected: ERROR: pattern variable qualified expression "a.val" is not supported
 -- PATTERN-only variable qualified name: not supported even without DEFINE entry
 SELECT COUNT(*) OVER w
 FROM rpr_err
@@ -3410,7 +3361,6 @@ WINDOW w AS (
 ERROR:  pattern variable qualified expression "b.val" is not supported in DEFINE clause
 LINE 7:     DEFINE A AS B.val > 0
                         ^
--- Expected: ERROR: pattern variable qualified expression "b.val" is not supported
 -- DEFINE-only variable qualified name: still a pattern variable, not a range variable
 SELECT COUNT(*) OVER w
 FROM rpr_err
@@ -3423,7 +3373,6 @@ WINDOW w AS (
 ERROR:  DEFINE variable "b" is not used in PATTERN
 LINE 7:     DEFINE A AS val > 0, B AS B.val > 0
                                  ^
--- Expected: ERROR: pattern variable qualified expression "b.val" is not supported
 -- FROM-clause range variable qualified name: not allowed (prohibited by ISO/IEC 19075-5 6.5)
 SELECT COUNT(*) OVER w
 FROM rpr_err
@@ -3436,7 +3385,6 @@ WINDOW w AS (
 ERROR:  range variable qualified expression "rpr_err.val" is not allowed in DEFINE clause
 LINE 7:     DEFINE A AS rpr_err.val > 0
                         ^
--- Expected: ERROR: range variable qualified expression "rpr_err.val" is not allowed
 -- Unknown qualifier (neither pattern var nor range var): the DEFINE pre-check
 -- must fall through so that normal column resolution produces a sensible error.
 SELECT COUNT(*) OVER w
@@ -3450,7 +3398,6 @@ WINDOW w AS (
 ERROR:  missing FROM-clause entry for table "nosuch"
 LINE 7:     DEFINE A AS nosuch.val > 0
                         ^
--- Expected: ERROR: missing FROM-clause entry for table "nosuch"
 -- Unqualified composite field access in DEFINE works: no qualifier means no
 -- pattern/range-var navigation, so the pre-check skips and normal resolution
 -- handles "(items).amount" via A_Indirection on the current row.
@@ -3488,7 +3435,6 @@ WINDOW w AS (
 ERROR:  pattern variable qualified expression "a.items" is not supported in DEFINE clause
 LINE 7:     DEFINE A AS (A.items).amount > 10
                          ^
--- Expected: ERROR: pattern variable qualified expression "a.items" is not supported
 SELECT COUNT(*) OVER w
 FROM rpr_composite
 WINDOW w AS (
@@ -3500,11 +3446,9 @@ WINDOW w AS (
 ERROR:  range variable qualified expression "rpr_composite.items" is not allowed in DEFINE clause
 LINE 7:     DEFINE A AS (rpr_composite.items).amount > 10
                          ^
--- Expected: ERROR: range variable qualified expression "rpr_composite.items" is not allowed
 DROP TABLE rpr_composite;
 DROP TYPE rpr_item;
--- Semantic errors
--- Undefined column in DEFINE
+-- ERROR: undefined column in DEFINE
 SELECT COUNT(*) OVER w
 FROM rpr_err
 WINDOW w AS (
@@ -3516,8 +3460,7 @@ WINDOW w AS (
 ERROR:  column "nonexistent_column" does not exist
 LINE 7:     DEFINE A AS nonexistent_column > 0
                         ^
--- Expected: ERROR: column "nonexistent_column" does not exist
--- Type mismatch
+-- ERROR: type mismatch
 SELECT COUNT(*) OVER w
 FROM rpr_err
 WINDOW w AS (
@@ -3529,8 +3472,7 @@ WINDOW w AS (
 ERROR:  invalid input syntax for type integer: "string"
 LINE 7:     DEFINE A AS val > 'string'
                               ^
--- Expected: ERROR: invalid input syntax for type integer: "string"
--- Aggregate function in DEFINE (if not allowed)
+-- ERROR: aggregate function in DEFINE is not supported
 SELECT COUNT(*) OVER w
 FROM rpr_err
 WINDOW w AS (
@@ -3542,8 +3484,7 @@ WINDOW w AS (
 ERROR:  aggregate functions are not allowed in DEFINE
 LINE 7:     DEFINE A AS COUNT(*) > 0
                         ^
--- Expected: ERROR: aggregate functions are not allowed in DEFINE
--- Subquery in DEFINE (NOT SUPPORTED)
+-- Subquery in DEFINE is not supported
 SELECT COUNT(*) OVER w
 FROM rpr_err
 WINDOW w AS (
@@ -3555,8 +3496,6 @@ WINDOW w AS (
 ERROR:  cannot use subquery in DEFINE expression
 LINE 7:     DEFINE A AS val > (SELECT max(val) FROM rpr_err)
                               ^
--- Expected: ERROR: cannot use subquery in DEFINE expression
--- Edge cases
 -- Pattern variable not used (should work, extra vars ignored)
 SELECT id, val, COUNT(*) OVER w as cnt
 FROM rpr_err
@@ -5703,7 +5642,6 @@ WINDOW w AS (
 ERROR:  quantifier bounds must be between 0 and 2147483646 with max >= 1
 LINE 6:     PATTERN ((A{2000000000,2147483647}){2})
                         ^
--- Expected: ERROR at parse time before optimization
 -- Test: nested unbounded with large min causes overflow fallback
 EXPLAIN (COSTS OFF)
 SELECT COUNT(*) OVER w FROM rpr_fallback
@@ -5947,7 +5885,6 @@ ORDER BY category;
 ERROR:  syntax error at or near "GROUP"
 LINE 12: GROUP BY category
          ^
--- Expected: ERROR: syntax error at or near "GROUP"
 -- (GROUP BY after WINDOW clause is not valid SQL syntax)
 -- ============================================================
 -- Subquery and CTE Tests
@@ -6427,7 +6364,6 @@ INSERT INTO rpr_sort VALUES
     (1, 'A', 30), (2, 'B', 20), (3, 'A', 10),
     (4, 'B', 40), (5, 'A', 50), (6, 'B', 60);
 -- RPR with GROUP BY (aggregate in DEFINE -> ERROR before GROUP BY interaction)
--- Expected: ERROR: aggregate functions are not allowed in DEFINE
 SELECT category,
        COUNT(*) as group_cnt,
        MAX(val) as max_val,
@@ -6445,7 +6381,6 @@ ERROR:  aggregate functions are not allowed in DEFINE
 LINE 11:     DEFINE A AS COUNT(*) > 0
                          ^
 -- RPR with HAVING (same aggregate-in-DEFINE error)
--- Expected: ERROR: aggregate functions are not allowed in DEFINE
 SELECT category,
        COUNT(*) as group_cnt,
        COUNT(*) OVER w as window_cnt
@@ -6758,7 +6693,7 @@ WINDOW w AS (
 (2 rows)
 
 -- Expected: Success - exactly at RPR_VARID_MAX boundary
--- Test: 241 variables in PATTERN, 240 in DEFINE (exceeds limit with implicit TRUE)
+-- ERROR: 241 variables in PATTERN, 240 in DEFINE (exceeds limit with implicit TRUE)
 SELECT COUNT(*) OVER w FROM rpr_errors
 WINDOW w AS (
     ORDER BY id
@@ -6794,7 +6729,6 @@ ERROR:  too many pattern variables
 LINE 5: ...V230 V231 V232 V233 V234 V235 V236 V237 V238 V239 V240 V241)
                                                                   ^
 DETAIL:  Maximum is 240.
--- Expected: ERROR - too many pattern variables (Maximum is 240)
 -- Test: Pattern nesting at maximum depth (depth 253)
 -- Note: 253 nested GROUP{3,7}? quantifiers; reluctant quantifiers are not
 -- subject to quantifier multiplication, so the nesting (and depth 253) is
@@ -6826,12 +6760,10 @@ WINDOW w AS (
 );
 ERROR:  pattern nesting too deep
 DETAIL:  Pattern nesting depth 254 exceeds maximum 253.
--- Expected: ERROR - pattern nesting too deep
 DROP TABLE rpr_errors;
 -- ============================================================
--- Jacob's Patterns
+-- Basic Pattern Matching
 -- ============================================================
--- Basic pattern matching tests from jacob branch
 -- Test: A? (optional, greedy)
 SELECT id, val, count(*) OVER w AS c
 FROM rpr_plan
@@ -7194,3 +7126,4 @@ FROM (SELECT id, val,
 (2 rows)
 
 DROP TABLE rpr_plan;
+RESET client_min_messages;
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index cc79843aeb7..7dc13232d89 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -12,7 +12,7 @@
 --   Quantifiers Tests
 --   Navigation Functions Tests
 --   SKIP TO / INITIAL Tests
---   Serialization/Deserialization Tests (objects kept for pg_upgrade/pg_dump)
+--   Serialization/Deserialization Tests
 --   Glued Quantifier / Alternation Tests
 --   Error Cases Tests
 --   Window Deduplication Tests
@@ -34,7 +34,7 @@
 --   Error Limit Tests
 --
 -- Contributed Tests:
---   Jacob's Patterns
+--   Basic Pattern Matching
 --   Pathological Patterns
 -- ============================================================
 
@@ -54,6 +54,7 @@ CREATE TABLE rpr_keywords (
     past INT,        -- PAST keyword
     pattern INT,     -- PATTERN keyword
     seek INT,        -- SEEK keyword
+-- ERROR: SEEK is not supported
     skip INT         -- SKIP keyword (pre-existing)
 );
 
@@ -69,7 +70,6 @@ DROP TABLE rpr_keywords;
 -- DEFINE Clause Tests
 -- ============================================================
 
-
 -- Simple column references
 CREATE TABLE stock_price (
     dt DATE,
@@ -182,7 +182,7 @@ DROP TABLE rpr_auto;
 CREATE TABLE rpr_dup (id INT);
 INSERT INTO rpr_dup VALUES (1), (2);
 
--- Duplicate DEFINE entries
+-- Duplicate DEFINE variable name is not allowed
 SELECT COUNT(*) OVER w
 FROM rpr_dup
 WINDOW w AS (
@@ -191,7 +191,6 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS id > 0, A AS id < 10
 );
--- Expected: ERROR: row pattern definition variable name "a" appears more than once in DEFINE clause
 
 DROP TABLE rpr_dup;
 
@@ -199,7 +198,7 @@ DROP TABLE rpr_dup;
 CREATE TABLE rpr_bool (id INT, flag BOOLEAN);
 INSERT INTO rpr_bool VALUES (1, true), (2, false);
 
--- Non-boolean expression
+-- DEFINE clause must be a boolean expression
 SELECT COUNT(*) OVER w
 FROM rpr_bool
 WINDOW w AS (
@@ -208,7 +207,6 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS id
 );
--- Expected: ERROR: argument of DEFINE must be type boolean
 
 -- Boolean column reference
 SELECT id, flag, COUNT(*) OVER w as cnt
@@ -301,7 +299,6 @@ DROP TABLE rpr_unused;
 -- FRAME Options Tests
 -- ============================================================
 
-
 CREATE TABLE rpr_frame (id INT, val INT);
 INSERT INTO rpr_frame VALUES
     (1, 10), (2, 10), (3, 10),  -- Same val: 10
@@ -327,9 +324,7 @@ WINDOW w AS (
 )
 ORDER BY id;
 
--- Invalid frame start positions
-
--- Not starting at CURRENT ROW
+-- ERROR: frame must start at current row when row pattern recognition is used
 SELECT COUNT(*) OVER w
 FROM rpr_frame
 WINDOW w AS (
@@ -338,7 +333,6 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: FRAME must start at current row when row pattern recognition is used
 
 -- EXCLUDE options
 
@@ -352,7 +346,6 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: cannot use EXCLUDE options with row pattern recognition
 
 -- EXCLUDE GROUP not permitted
 SELECT COUNT(*) OVER w
@@ -364,7 +357,6 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: cannot use EXCLUDE options with row pattern recognition
 
 -- EXCLUDE TIES not permitted
 SELECT COUNT(*) OVER w
@@ -376,9 +368,8 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: cannot use EXCLUDE options with row pattern recognition
 
--- RANGE frame not starting at CURRENT ROW
+-- range frame is not allowed with RPR
 SELECT COUNT(*) OVER w
 FROM rpr_frame
 WINDOW w AS (
@@ -387,9 +378,8 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: cannot use FRAME option RANGE with row pattern recognition
 
--- GROUPS frame not starting at CURRENT ROW
+-- GROUPS frame is not allowed with RPR
 SELECT COUNT(*) OVER w
 FROM rpr_frame
 WINDOW w AS (
@@ -398,9 +388,8 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: cannot use FRAME option GROUPS with row pattern recognition
 
--- Starting with N PRECEDING
+-- ERROR: frame must start at current row when row pattern recognition is used
 SELECT COUNT(*) OVER w
 FROM rpr_frame
 WINDOW w AS (
@@ -409,9 +398,8 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: FRAME must start at current row when row pattern recognition is used
 
--- Starting with N FOLLOWING
+-- ERROR: frame must start at current row with RPR
 SELECT COUNT(*) OVER w
 FROM rpr_frame
 WINDOW w AS (
@@ -420,11 +408,8 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: FRAME must start at current row when row pattern recognition is used
 
--- Frame end bound edge cases
-
--- End before start: CURRENT ROW AND 1 PRECEDING
+-- ERROR: end before start: CURRENT ROW AND 1 PRECEDING
 SELECT COUNT(*) OVER w
 FROM rpr_frame
 WINDOW w AS (
@@ -433,9 +418,8 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: frame starting from current row cannot have preceding rows
 
--- End before start: CURRENT ROW AND UNBOUNDED PRECEDING
+-- ERROR: end before start: CURRENT ROW AND UNBOUNDED PRECEDING
 SELECT COUNT(*) OVER w
 FROM rpr_frame
 WINDOW w AS (
@@ -444,7 +428,6 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: frame end cannot be UNBOUNDED PRECEDING
 
 -- Single row frame: CURRENT ROW AND CURRENT ROW is rejected (the standard
 -- allows only UNBOUNDED FOLLOWING or a positive offset FOLLOWING).
@@ -458,7 +441,6 @@ WINDOW w AS (
     DEFINE A AS val > 0
 )
 ORDER BY id;
--- Expected: ERROR: cannot use CURRENT ROW as frame end with row pattern recognition
 
 -- Zero offset: CURRENT ROW AND 0 FOLLOWING denotes the same one-row frame
 -- and is likewise rejected (caught at execution time).
@@ -472,7 +454,6 @@ WINDOW w AS (
     DEFINE A AS val > 0
 )
 ORDER BY id;
--- Expected: ERROR: frame ending offset must be positive with row pattern recognition
 
 -- A non-constant frame end offset is allowed; a zero value is still rejected,
 -- this time at execution time (a literal cannot exercise that path).
@@ -489,7 +470,6 @@ WINDOW w AS (
 ORDER BY id;
 EXECUTE rpr_end_offset(2);
 EXECUTE rpr_end_offset(0);
--- Expected: ERROR: frame ending offset must be positive with row pattern recognition
 DEALLOCATE rpr_end_offset;
 
 -- Large offset: CURRENT ROW AND 1000 FOLLOWING
@@ -532,7 +512,7 @@ WINDOW w AS (
 )
 ORDER BY id;
 
--- RANGE frame with RPR (not permitted)
+-- range frame is not allowed with RPR
 SELECT id, val, COUNT(*) OVER w as cnt
 FROM rpr_frame
 WINDOW w AS (
@@ -543,7 +523,6 @@ WINDOW w AS (
     DEFINE A AS val >= 0, B AS val >= 0
 )
 ORDER BY id;
--- Expected: ERROR: cannot use FRAME option RANGE with row pattern recognition
 
 -- GROUPS frame with RPR (not permitted)
 SELECT id, val, COUNT(*) OVER w as cnt
@@ -556,7 +535,6 @@ WINDOW w AS (
     DEFINE A AS val >= 0, B AS val >= 0
 )
 ORDER BY id;
--- Expected: ERROR: cannot use FRAME option GROUPS with row pattern recognition
 
 DROP TABLE rpr_frame;
 
@@ -596,7 +574,6 @@ WINDOW w AS (
     DEFINE A AS val >= 10, B AS val >= 20
 )
 ORDER BY id;
--- Expected: ERROR: cannot use FRAME option RANGE with row pattern recognition
 
 DROP TABLE rpr_partition;
 
@@ -604,7 +581,6 @@ DROP TABLE rpr_partition;
 -- PATTERN Syntax Tests
 -- ============================================================
 
-
 CREATE TABLE rpr_pattern (id INT, val INT);
 INSERT INTO rpr_pattern VALUES
     (1, 5), (2, 10), (3, 15), (4, 20), (5, 25),
@@ -700,7 +676,6 @@ DROP TABLE rpr_pattern;
 -- Quantifiers Tests
 -- ============================================================
 
-
 CREATE TABLE rpr_quant (id INT, val INT);
 INSERT INTO rpr_quant VALUES
     (1, 10), (2, 20), (3, 30), (4, 40), (5, 50),
@@ -753,7 +728,6 @@ WINDOW w AS (
     DEFINE A AS val > 1000, B AS val > 0
 )
 ORDER BY id;
--- Expected: ERROR: quantifier bound must be between 1 and 2147483646
 
 -- {0,0} is not allowed (max must be >= 1)
 SELECT id, val, COUNT(*) OVER w as cnt
@@ -765,7 +739,6 @@ WINDOW w AS (
     DEFINE A AS val > 1000, B AS val > 0
 )
 ORDER BY id;
--- Expected: ERROR: quantifier bounds must be between 0 and 2147483646 with max >= 1
 
 -- {0,1} (equivalent to ?)
 SELECT id, val, COUNT(*) OVER w as cnt
@@ -837,6 +810,7 @@ CREATE TABLE rpr_reluctant (id INT, val INT);
 INSERT INTO rpr_reluctant VALUES (1, 10), (2, 20), (3, 30);
 
 -- *? (zero or more, reluctant)
+-- Reluctant quantifier: prefer shortest match
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -845,9 +819,9 @@ WINDOW w AS (
     PATTERN (A*?)
     DEFINE A AS val > 0
 );
--- Reluctant quantifier: prefer shortest match
 
 -- +? (one or more, reluctant)
+-- Reluctant quantifier: prefer shortest match
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -856,9 +830,9 @@ WINDOW w AS (
     PATTERN (A+?)
     DEFINE A AS val > 0
 );
--- Reluctant quantifier: prefer shortest match
 
 -- ?? (zero or one, reluctant)
+-- Reluctant quantifier: prefer shortest match
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -867,9 +841,9 @@ WINDOW w AS (
     PATTERN (A??)
     DEFINE A AS val > 0
 );
--- Reluctant quantifier: prefer shortest match
 
 -- {n,}? (n or more, reluctant)
+-- Reluctant quantifier: prefer shortest match
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -878,9 +852,9 @@ WINDOW w AS (
     PATTERN (A{2,}?)
     DEFINE A AS val > 0
 );
--- Reluctant quantifier: prefer shortest match
 
 -- {n,m}? (n to m, reluctant)
+-- Reluctant quantifier: prefer shortest match
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -889,9 +863,9 @@ WINDOW w AS (
     PATTERN (A{1,3}?)
     DEFINE A AS val > 0
 );
--- Reluctant quantifier: prefer shortest match
 
 -- {n}? (exactly n, reluctant)
+-- Reluctant quantifier: prefer shortest match
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -900,9 +874,9 @@ WINDOW w AS (
     PATTERN (A{2}?)
     DEFINE A AS val > 0
 );
--- Reluctant quantifier: prefer shortest match
 
 -- {,m}? (up to m, reluctant) - COMPLETELY UNTESTED RULE!
+-- Reluctant quantifier: prefer shortest match
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -911,9 +885,6 @@ WINDOW w AS (
     PATTERN (A{,3}?)
     DEFINE A AS val > 0
 );
--- Reluctant quantifier: prefer shortest match
-
--- Invalid reluctant patterns (wrong token after quantifier)
 
 -- {2}+ (should be {2}? not {2}+)
 SELECT COUNT(*) OVER w
@@ -924,7 +895,6 @@ WINDOW w AS (
     PATTERN (A{2}+)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: syntax error at or near "+"
 
 -- {2,}* (should be {2,}? not {2,}*)
 SELECT COUNT(*) OVER w
@@ -935,7 +905,6 @@ WINDOW w AS (
     PATTERN (A{2,}*)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: syntax error at or near "*"
 
 -- {,3}* (should be {,3}? not {,3}*)
 SELECT COUNT(*) OVER w
@@ -946,7 +915,6 @@ WINDOW w AS (
     PATTERN (A{,3}*)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: syntax error at or near "*"
 
 -- {1,3}+ (should be {1,3}? not {1,3}+)
 SELECT COUNT(*) OVER w
@@ -957,11 +925,10 @@ WINDOW w AS (
     PATTERN (A{1,3}+)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: syntax error at or near "+"
 
 -- Boundary errors in reluctant quantifiers
 
--- {-1}? (negative bound)
+-- negative bound is not allowed
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -970,9 +937,8 @@ WINDOW w AS (
     PATTERN (A{-1}?)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: syntax error at or near "-"
 
--- {2147483647}? (INT_MAX)
+-- ERROR: quantifier bound exceeds limits
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -981,9 +947,8 @@ WINDOW w AS (
     PATTERN (A{2147483647}?)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: quantifier bound must be between 1 and 2147483646
 
--- {-1,}? (negative lower bound)
+-- negative lower bound is not allowed
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -992,9 +957,8 @@ WINDOW w AS (
     PATTERN (A{-1,}?)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: syntax error at or near "-"
 
--- {2147483647,}? (INT_MAX lower bound)
+-- ERROR: quantifier lower bound exceeds limits
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1003,9 +967,8 @@ WINDOW w AS (
     PATTERN (A{2147483647,}?)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: quantifier bound must be between 0 and 2147483646
 
--- {,0}? (zero upper bound)
+-- zero upper bound is not allowed
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1014,9 +977,8 @@ WINDOW w AS (
     PATTERN (A{,0}?)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: quantifier bound must be between 1 and 2147483646
 
--- {,2147483647}? (INT_MAX upper bound)
+-- ERROR: {,2147483647}? (upper bound in range exceeds limits)
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1025,9 +987,8 @@ WINDOW w AS (
     PATTERN (A{,2147483647}?)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: quantifier bound must be between 1 and 2147483646
 
--- {-1,3}? (negative lower in range)
+-- ERROR: {-1,3}? (negative lower bound in range is not allowed)
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1036,9 +997,8 @@ WINDOW w AS (
     PATTERN (A{-1,3}?)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: syntax error at or near "-"
 
--- {1,2147483647}? (INT_MAX upper in range)
+-- ERROR: {1,2147483647}? (upper bound in range exceeds limits)
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1047,9 +1007,8 @@ WINDOW w AS (
     PATTERN (A{1,2147483647}?)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: quantifier bounds must be between 0 and 2147483646 with max >= 1
 
--- {5,3}? (min > max)
+-- ERROR: {5,3}? (min > max is not allowed)
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1058,12 +1017,12 @@ WINDOW w AS (
     PATTERN (A{5,3}?)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: quantifier minimum bound must not exceed maximum
 
 -- Token-separated reluctant quantifiers (space between quantifier and ?)
 -- These may be tokenized differently by the lexer
 
 -- * ? (token separated)
+-- Reluctant quantifier: prefer shortest match
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1072,9 +1031,9 @@ WINDOW w AS (
     PATTERN (A* ?)
     DEFINE A AS val > 0
 );
--- Reluctant quantifier: prefer shortest match
 
 -- + ? (token separated)
+-- Reluctant quantifier: prefer shortest match
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1083,9 +1042,9 @@ WINDOW w AS (
     PATTERN (A+ ?)
     DEFINE A AS val > 0
 );
--- Reluctant quantifier: prefer shortest match
 
 -- {2,} ? (token separated)
+-- Reluctant quantifier: prefer shortest match
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1094,9 +1053,6 @@ WINDOW w AS (
     PATTERN (A{2,} ?)
     DEFINE A AS val > 0
 );
--- Reluctant quantifier: prefer shortest match
-
--- Invalid token combinations
 
 -- * + (invalid combination)
 SELECT COUNT(*) OVER w
@@ -1107,7 +1063,6 @@ WINDOW w AS (
     PATTERN (A* +)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: syntax error at or near "+"
 
 -- + * (invalid combination)
 SELECT COUNT(*) OVER w
@@ -1118,9 +1073,9 @@ WINDOW w AS (
     PATTERN (A+ *)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: syntax error at or near "*"
 
 -- ? ? (parsed as ?? reluctant quantifier)
+-- Reluctant quantifier: prefer shortest match
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1129,7 +1084,6 @@ WINDOW w AS (
     PATTERN (A? ?)
     DEFINE A AS val > 0
 );
--- Reluctant quantifier: prefer shortest match
 
 DROP TABLE rpr_reluctant;
 
@@ -1138,7 +1092,7 @@ DROP TABLE rpr_reluctant;
 CREATE TABLE rpr_bounds (id INT);
 INSERT INTO rpr_bounds VALUES (1), (2);
 
--- min > max
+-- ERROR: quantifier lower bound must not exceed upper bound
 SELECT COUNT(*) OVER w
 FROM rpr_bounds
 WINDOW w AS (
@@ -1147,7 +1101,6 @@ WINDOW w AS (
     PATTERN (A{5,3})
     DEFINE A AS id > 0
 );
--- Expected: ERROR: quantifier minimum bound must not exceed maximum
 
 -- Large bounds
 SELECT COUNT(*) OVER w
@@ -1179,7 +1132,7 @@ WINDOW w AS (
     DEFINE A AS id > 0
 );
 
--- INT_MAX = 2147483647 (over limit)
+-- ERROR: quantifier bound exceeds limits
 SELECT COUNT(*) OVER w
 FROM rpr_bounds
 WINDOW w AS (
@@ -1188,11 +1141,10 @@ WINDOW w AS (
     PATTERN (A{2147483647})
     DEFINE A AS id > 0
 );
--- Expected: ERROR: quantifier bound must be between 1 and 2147483646
 
 -- {n,} boundary errors
 
--- Negative lower bound in {n,}
+-- ERROR: negative lower bound in {n,} is not allowed
 SELECT COUNT(*) OVER w
 FROM rpr_bounds
 WINDOW w AS (
@@ -1201,9 +1153,8 @@ WINDOW w AS (
     PATTERN (A{-1,})
     DEFINE A AS id > 0
 );
--- Expected: ERROR: syntax error at or near "-"
 
--- INT_MAX in {n,}
+-- ERROR: quantifier lower bound exceeds limits
 SELECT COUNT(*) OVER w
 FROM rpr_bounds
 WINDOW w AS (
@@ -1212,7 +1163,6 @@ WINDOW w AS (
     PATTERN (A{2147483647,})
     DEFINE A AS id > 0
 );
--- Expected: ERROR: quantifier bound must be between 0 and 2147483646
 
 -- {,m} boundary errors
 
@@ -1225,9 +1175,8 @@ WINDOW w AS (
     PATTERN (A{,0})
     DEFINE A AS id > 0
 );
--- Expected: ERROR: quantifier bound must be between 1 and 2147483646
 
--- INT_MAX in {,m}
+-- ERROR: quantifier upper bound exceeds limits
 SELECT COUNT(*) OVER w
 FROM rpr_bounds
 WINDOW w AS (
@@ -1236,7 +1185,6 @@ WINDOW w AS (
     PATTERN (A{,2147483647})
     DEFINE A AS id > 0
 );
--- Expected: ERROR: quantifier bound must be between 1 and 2147483646
 
 DROP TABLE rpr_bounds;
 
@@ -1244,7 +1192,6 @@ DROP TABLE rpr_bounds;
 -- Navigation Functions Tests (PREV / NEXT / FIRST / LAST)
 -- ============================================================
 
-
 CREATE TABLE rpr_nav (id INT, val INT);
 INSERT INTO rpr_nav VALUES
     (1, 10), (2, 20), (3, 15), (4, 25), (5, 30);
@@ -1301,7 +1248,6 @@ WINDOW w AS (
         B AS val > PREV(val)
 )
 ORDER BY id;
--- Expected: ERROR: cannot use prev outside a DEFINE clause
 
 -- NEXT function cannot be used other than in DEFINE
 SELECT NEXT(id), id, val, COUNT(*) OVER w as cnt
@@ -1315,7 +1261,6 @@ WINDOW w AS (
         B AS val > PREV(val)
 )
 ORDER BY id;
--- Expected: ERROR: cannot use next outside a DEFINE clause
 
 -- FIRST function - reference match_start row
 SELECT id, val, COUNT(*) OVER w as cnt
@@ -1358,11 +1303,9 @@ ORDER BY id;
 
 -- FIRST function cannot be used other than in DEFINE
 SELECT FIRST(id), id, val FROM rpr_nav;
--- Expected: ERROR: cannot use first outside a DEFINE clause
 
 -- LAST function cannot be used other than in DEFINE
 SELECT LAST(id), id, val FROM rpr_nav;
--- Expected: ERROR: cannot use last outside a DEFINE clause
 
 DROP TABLE rpr_nav;
 
@@ -1370,7 +1313,6 @@ DROP TABLE rpr_nav;
 -- SKIP TO / INITIAL Tests
 -- ============================================================
 
-
 CREATE TABLE rpr_skip (id INT, val INT);
 INSERT INTO rpr_skip VALUES
     (1, 1), (2, 2), (3, 3), (4, 4), (5, 5),
@@ -1492,15 +1434,15 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: SEEK is not supported
--- HINT: Use INITIAL instead.
 
 DROP TABLE rpr_seek;
 
 -- ============================================================
 -- Serialization/Deserialization Tests
 -- ============================================================
-
+-- RPR-defining views and tables here are intentionally left in place (not
+-- dropped) so that pg_dump/pg_upgrade exercise the deparse-then-re-parse
+-- round-trip of the RPR window clause.
 
 -- View creation and deparsing
 
@@ -2190,13 +2132,10 @@ DROP TABLE rpr_glue;
 -- Error Cases Tests
 -- ============================================================
 
-
 DROP TABLE IF EXISTS rpr_err;
 CREATE TABLE rpr_err (id INT, val INT);
 INSERT INTO rpr_err VALUES (1, 10), (2, 20);
 
--- Syntax errors
-
 -- Invalid quantifier syntax
 SELECT COUNT(*) OVER w
 FROM rpr_err
@@ -2222,7 +2161,7 @@ EXCEPTION
 END $$;
 SET client_min_messages = WARNING;
 
--- Empty DEFINE
+-- ERROR: empty DEFINE not allowed
 SELECT COUNT(*) OVER w
 FROM rpr_err
 WINDOW w AS (
@@ -2233,7 +2172,7 @@ WINDOW w AS (
 );
 -- Expected: Syntax error
 
--- Empty PATTERN
+-- ERROR: empty PATTERN not allowed
 SELECT COUNT(*) OVER w
 FROM rpr_err
 WINDOW w AS (
@@ -2244,7 +2183,7 @@ WINDOW w AS (
 );
 -- Expected: Syntax error
 
--- DEFINE without PATTERN (PATTERN and DEFINE must be used together)
+-- ERROR: DEFINE without PATTERN (PATTERN and DEFINE must be used together)
 SELECT COUNT(*) OVER w
 FROM rpr_err
 WINDOW w AS (
@@ -2265,7 +2204,6 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS A.val > 0
 );
--- Expected: ERROR: pattern variable qualified expression "a.val" is not supported
 
 -- PATTERN-only variable qualified name: not supported even without DEFINE entry
 SELECT COUNT(*) OVER w
@@ -2276,7 +2214,6 @@ WINDOW w AS (
     PATTERN (A+ B+)
     DEFINE A AS B.val > 0
 );
--- Expected: ERROR: pattern variable qualified expression "b.val" is not supported
 
 -- DEFINE-only variable qualified name: still a pattern variable, not a range variable
 SELECT COUNT(*) OVER w
@@ -2287,7 +2224,6 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > 0, B AS B.val > 0
 );
--- Expected: ERROR: pattern variable qualified expression "b.val" is not supported
 
 -- FROM-clause range variable qualified name: not allowed (prohibited by ISO/IEC 19075-5 6.5)
 SELECT COUNT(*) OVER w
@@ -2298,7 +2234,6 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS rpr_err.val > 0
 );
--- Expected: ERROR: range variable qualified expression "rpr_err.val" is not allowed
 
 -- Unknown qualifier (neither pattern var nor range var): the DEFINE pre-check
 -- must fall through so that normal column resolution produces a sensible error.
@@ -2310,7 +2245,6 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS nosuch.val > 0
 );
--- Expected: ERROR: missing FROM-clause entry for table "nosuch"
 
 -- Unqualified composite field access in DEFINE works: no qualifier means no
 -- pattern/range-var navigation, so the pre-check skips and normal resolution
@@ -2340,7 +2274,6 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS (A.items).amount > 10
 );
--- Expected: ERROR: pattern variable qualified expression "a.items" is not supported
 SELECT COUNT(*) OVER w
 FROM rpr_composite
 WINDOW w AS (
@@ -2349,13 +2282,10 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS (rpr_composite.items).amount > 10
 );
--- Expected: ERROR: range variable qualified expression "rpr_composite.items" is not allowed
 DROP TABLE rpr_composite;
 DROP TYPE rpr_item;
 
--- Semantic errors
-
--- Undefined column in DEFINE
+-- ERROR: undefined column in DEFINE
 SELECT COUNT(*) OVER w
 FROM rpr_err
 WINDOW w AS (
@@ -2364,9 +2294,8 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS nonexistent_column > 0
 );
--- Expected: ERROR: column "nonexistent_column" does not exist
 
--- Type mismatch
+-- ERROR: type mismatch
 SELECT COUNT(*) OVER w
 FROM rpr_err
 WINDOW w AS (
@@ -2375,9 +2304,8 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > 'string'
 );
--- Expected: ERROR: invalid input syntax for type integer: "string"
 
--- Aggregate function in DEFINE (if not allowed)
+-- ERROR: aggregate function in DEFINE is not supported
 SELECT COUNT(*) OVER w
 FROM rpr_err
 WINDOW w AS (
@@ -2386,9 +2314,8 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS COUNT(*) > 0
 );
--- Expected: ERROR: aggregate functions are not allowed in DEFINE
 
--- Subquery in DEFINE (NOT SUPPORTED)
+-- Subquery in DEFINE is not supported
 SELECT COUNT(*) OVER w
 FROM rpr_err
 WINDOW w AS (
@@ -2397,9 +2324,6 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > (SELECT max(val) FROM rpr_err)
 );
--- Expected: ERROR: cannot use subquery in DEFINE expression
-
--- Edge cases
 
 -- Pattern variable not used (should work, extra vars ignored)
 SELECT id, val, COUNT(*) OVER w as cnt
@@ -3414,7 +3338,6 @@ WINDOW w AS (
     PATTERN ((A{2000000000,2147483647}){2})
     DEFINE A AS val > 0
 );
--- Expected: ERROR at parse time before optimization
 
 -- Test: nested unbounded with large min causes overflow fallback
 EXPLAIN (COSTS OFF)
@@ -3563,7 +3486,6 @@ WINDOW w AS (
 )
 GROUP BY category
 ORDER BY category;
--- Expected: ERROR: syntax error at or near "GROUP"
 -- (GROUP BY after WINDOW clause is not valid SQL syntax)
 
 -- ============================================================
@@ -3937,7 +3859,6 @@ INSERT INTO rpr_sort VALUES
     (4, 'B', 40), (5, 'A', 50), (6, 'B', 60);
 
 -- RPR with GROUP BY (aggregate in DEFINE -> ERROR before GROUP BY interaction)
--- Expected: ERROR: aggregate functions are not allowed in DEFINE
 
 SELECT category,
        COUNT(*) as group_cnt,
@@ -3954,7 +3875,6 @@ WINDOW w AS (
 ORDER BY category;
 
 -- RPR with HAVING (same aggregate-in-DEFINE error)
--- Expected: ERROR: aggregate functions are not allowed in DEFINE
 
 SELECT category,
        COUNT(*) as group_cnt,
@@ -4189,7 +4109,7 @@ WINDOW w AS (
 );
 -- Expected: Success - exactly at RPR_VARID_MAX boundary
 
--- Test: 241 variables in PATTERN, 240 in DEFINE (exceeds limit with implicit TRUE)
+-- ERROR: 241 variables in PATTERN, 240 in DEFINE (exceeds limit with implicit TRUE)
 SELECT COUNT(*) OVER w FROM rpr_errors
 WINDOW w AS (
     ORDER BY id
@@ -4221,7 +4141,6 @@ WINDOW w AS (
     V221 AS val > 0, V222 AS val > 0, V223 AS val > 0, V224 AS val > 0, V225 AS val > 0, V226 AS val > 0, V227 AS val > 0, V228 AS val > 0, V229 AS val > 0, V230 AS val > 0,
     V231 AS val > 0, V232 AS val > 0, V233 AS val > 0, V234 AS val > 0, V235 AS val > 0, V236 AS val > 0, V237 AS val > 0, V238 AS val > 0, V239 AS val > 0, V240 AS val > 0
 );
--- Expected: ERROR - too many pattern variables (Maximum is 240)
 
 -- Test: Pattern nesting at maximum depth (depth 253)
 -- Note: 253 nested GROUP{3,7}? quantifiers; reluctant quantifiers are not
@@ -4247,14 +4166,12 @@ WINDOW w AS (
     PATTERN (((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((A{3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?)
     DEFINE A AS val > 0
 );
--- Expected: ERROR - pattern nesting too deep
 
 DROP TABLE rpr_errors;
 
 -- ============================================================
--- Jacob's Patterns
+-- Basic Pattern Matching
 -- ============================================================
--- Basic pattern matching tests from jacob branch
 
 -- Test: A? (optional, greedy)
 SELECT id, val, count(*) OVER w AS c
@@ -4453,3 +4370,4 @@ FROM (SELECT id, val,
 ) s;
 
 DROP TABLE rpr_plan;
+RESET client_min_messages;
-- 
2.50.1 (Apple Git-155)


From b010580c9b62244ba323520bb7202aa348c18195 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Mon, 8 Jun 2026 21:27:44 +0900
Subject: [PATCH 68/68] Use foreach_node and friends in row pattern recognition
 code

Replace the hand-written foreach()/lfirst() loops in the row pattern
recognition code with foreach_node(), foreach_ptr(), foreach_current_index()
and forboth(), dropping the now-unneeded ListCell declarations, casts and
manual index counters.  No behavior change.
---
 src/backend/executor/execRPR.c            | 10 +--
 src/backend/executor/nodeWindowAgg.c      | 12 +--
 src/backend/optimizer/path/allpaths.c     |  9 +--
 src/backend/optimizer/path/costsize.c     |  5 +-
 src/backend/optimizer/plan/createplan.c   |  5 +-
 src/backend/optimizer/plan/rpr.c          | 90 +++++++----------------
 src/backend/optimizer/prep/prepjointree.c |  4 +-
 src/backend/parser/parse_cte.c            |  3 +-
 src/backend/parser/parse_expr.c           |  5 +-
 src/backend/parser/parse_rpr.c            | 38 +++-------
 src/backend/utils/adt/ruleutils.c         | 18 ++---
 11 files changed, 62 insertions(+), 137 deletions(-)

diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 1c89875a306..5ae54495968 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -1595,8 +1595,6 @@ nfa_reevaluate_dependent_vars(WindowAggState *winstate, RPRNFAContext *ctx,
 	ExprContext *econtext = winstate->ss.ps.ps_ExprContext;
 	int64		saved_match_start = winstate->nav_match_start;
 	int64		saved_pos = winstate->currentpos;
-	int			varIdx = 0;
-	ListCell   *lc;
 
 	/* Temporarily set nav_match_start and currentpos for FIRST/LAST */
 	winstate->nav_match_start = ctx->matchStartRow;
@@ -1605,11 +1603,12 @@ nfa_reevaluate_dependent_vars(WindowAggState *winstate, RPRNFAContext *ctx,
 	/* Invalidate nav_slot cache since match_start changed */
 	winstate->nav_slot_pos = -1;
 
-	foreach(lc, winstate->defineClauseList)
+	foreach_ptr(ExprState, exprState, winstate->defineClauseList)
 	{
+		int			varIdx = foreach_current_index(exprState);
+
 		if (bms_is_member(varIdx, winstate->defineMatchStartDependent))
 		{
-			ExprState  *exprState = (ExprState *) lfirst(lc);
 			Datum		result;
 			bool		isnull;
 
@@ -1618,8 +1617,7 @@ nfa_reevaluate_dependent_vars(WindowAggState *winstate, RPRNFAContext *ctx,
 			winstate->nfaVarMatched[varIdx] = (!isnull && DatumGetBool(result));
 		}
 
-		varIdx++;
-		if (varIdx >= list_length(winstate->defineVariableList))
+		if (varIdx + 1 >= list_length(winstate->defineVariableList))
 			break;
 	}
 
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 13661181986..15579ae5ea4 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -3084,9 +3084,8 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
 		 * by EEOP_RPR_NAV_SET/RESTORE opcodes emitted during ExecInitExpr, so
 		 * no varno rewriting is needed here.
 		 */
-		foreach(l, node->defineClause)
+		foreach_node(TargetEntry, te, node->defineClause)
 		{
-			TargetEntry *te = lfirst(l);
 			char	   *name = te->resname;
 			Expr	   *expr = te->expr;
 			ExprState  *exps;
@@ -4173,7 +4172,6 @@ eval_define_offsets(WindowAggState *winstate, List *defineClause)
 {
 	EvalDefineOffsetsContext ctx;
 	NavTraversal trav;
-	ListCell   *lc;
 	bool		needsMax = (winstate->navMaxOffsetKind == RPR_NAV_OFFSET_NEEDS_EVAL);
 	bool		needsFirst = (winstate->hasFirstNav &&
 							  winstate->navFirstOffsetKind == RPR_NAV_OFFSET_NEEDS_EVAL);
@@ -4189,10 +4187,8 @@ eval_define_offsets(WindowAggState *winstate, List *defineClause)
 	trav.visit = visit_nav_exec;
 	trav.data = &ctx;
 
-	foreach(lc, defineClause)
+	foreach_node(TargetEntry, te, defineClause)
 	{
-		TargetEntry *te = (TargetEntry *) lfirst(lc);
-
 		nav_traversal_walker((Node *) te->expr, &trav);
 	}
 
@@ -4586,7 +4582,6 @@ nfa_evaluate_row(WindowObject winobj, int64 pos, bool *varMatched)
 	WindowAggState *winstate = winobj->winstate;
 	ExprContext *econtext = winstate->ss.ps.ps_ExprContext;
 	int			numDefineVars = list_length(winstate->defineVariableList);
-	ListCell   *lc;
 	int			varIdx = 0;
 	TupleTableSlot *slot;
 	int64		saved_pos;
@@ -4609,9 +4604,8 @@ nfa_evaluate_row(WindowObject winobj, int64 pos, bool *varMatched)
 	/* Invalidate nav_slot cache so PREV/NEXT re-fetch for new row */
 	winstate->nav_slot_pos = -1;
 
-	foreach(lc, winstate->defineClauseList)
+	foreach_ptr(ExprState, exprState, winstate->defineClauseList)
 	{
-		ExprState  *exprState = (ExprState *) lfirst(lc);
 		Datum		result;
 		bool		isnull;
 
diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c
index 22339f7491f..f3c9f3c0bd6 100644
--- a/src/backend/optimizer/path/allpaths.c
+++ b/src/backend/optimizer/path/allpaths.c
@@ -4760,21 +4760,16 @@ remove_unused_subquery_outputs(Query *subquery, RelOptInfo *rel,
 		if (IsA(texpr, Var))
 		{
 			Var		   *var = (Var *) texpr;
-			ListCell   *wlc;
 			bool		needed_by_define = false;
 
-			foreach(wlc, subquery->windowClause)
+			foreach_node(WindowClause, wc, subquery->windowClause)
 			{
-				WindowClause *wc = lfirst_node(WindowClause, wlc);
-
 				if (wc->defineClause != NIL)
 				{
 					List	   *vars = pull_var_clause((Node *) wc->defineClause, 0);
-					ListCell   *vlc;
 
-					foreach(vlc, vars)
+					foreach_node(Var, dvar, vars)
 					{
-						Var		   *dvar = (Var *) lfirst(vlc);
 
 						/*
 						 * Match varno as well as varattno: a Var pulled from
diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c
index b38cad9f121..82472c3fe96 100644
--- a/src/backend/optimizer/path/costsize.c
+++ b/src/backend/optimizer/path/costsize.c
@@ -3236,14 +3236,13 @@ cost_windowagg(Path *path, PlannerInfo *root,
 	if (winclause->rpPattern)
 	{
 		List	   *pattern_vars;
-		ListCell   *lc2;
 		QualCost	defcosts;
 
 		pattern_vars = collectPatternVariables(winclause->rpPattern);
 
-		foreach(lc2, pattern_vars)
+		foreach_node(String, pv, pattern_vars)
 		{
-			char	   *ptname = strVal(lfirst(lc2));
+			char	   *ptname = strVal(pv);
 
 			foreach_node(TargetEntry, def, winclause->defineClause)
 			{
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index d2e19d61d58..cca4126e511 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -2736,7 +2736,6 @@ compute_define_metadata(List *defineClause,
 {
 	DefineMetadataContext ctx;
 	NavTraversal trav;
-	ListCell   *lc;
 
 	ctx.maxOffset = 0;
 	ctx.maxNeedsEval = false;
@@ -2750,10 +2749,8 @@ compute_define_metadata(List *defineClause,
 	trav.visit = visit_nav_plan;
 	trav.data = &ctx;
 
-	foreach(lc, defineClause)
+	foreach_node(TargetEntry, te, defineClause)
 	{
-		TargetEntry *te = (TargetEntry *) lfirst(lc);
-
 		nav_traversal_walker((Node *) te->expr, &trav);
 		ctx.curVarIdx++;
 	}
diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index d768422deeb..664c942e575 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -191,12 +191,10 @@ tryUnwrapSingleChild(RPRPatternNode *pattern)
 static List *
 flattenSeqChildren(List *children)
 {
-	ListCell   *lc;
 	List	   *newChildren = NIL;
 
-	foreach(lc, children)
+	foreach_node(RPRPatternNode, child, children)
 	{
-		RPRPatternNode *child = (RPRPatternNode *) lfirst(lc);
 		RPRPatternNode *opt = optimizeRPRPattern(child);
 
 		/* GROUP{1,1} should have been unwrapped by optimizeGroupPattern */
@@ -229,14 +227,11 @@ flattenSeqChildren(List *children)
 static List *
 mergeConsecutiveVars(List *children)
 {
-	ListCell   *lc;
 	List	   *mergedChildren = NIL;
 	RPRPatternNode *prev = NULL;
 
-	foreach(lc, children)
+	foreach_node(RPRPatternNode, child, children)
 	{
-		RPRPatternNode *child = (RPRPatternNode *) lfirst(lc);
-
 		if (child->nodeType == RPR_PATTERN_VAR && child->reluctant == false)
 		{
 			/* ----------------------
@@ -306,14 +301,11 @@ mergeConsecutiveVars(List *children)
 static List *
 mergeConsecutiveGroups(List *children)
 {
-	ListCell   *lc;
 	List	   *mergedChildren = NIL;
 	RPRPatternNode *prev = NULL;
 
-	foreach(lc, children)
+	foreach_node(RPRPatternNode, child, children)
 	{
-		RPRPatternNode *child = (RPRPatternNode *) lfirst(lc);
-
 		if (child->nodeType == RPR_PATTERN_GROUP && child->reluctant == false)
 		{
 			/* ----------------------
@@ -385,15 +377,12 @@ mergeConsecutiveGroups(List *children)
 static List *
 mergeConsecutiveAlts(List *children)
 {
-	ListCell   *lc;
 	List	   *mergedChildren = NIL;
 	RPRPatternNode *prev = NULL;
 	int			count = 0;
 
-	foreach(lc, children)
+	foreach_node(RPRPatternNode, child, children)
 	{
-		RPRPatternNode *child = (RPRPatternNode *) lfirst(lc);
-
 		if (child->nodeType == RPR_PATTERN_ALT && child->reluctant == false)
 		{
 			if (prev != NULL &&
@@ -702,12 +691,10 @@ optimizeSeqPattern(RPRPatternNode *pattern)
 static List *
 flattenAltChildren(List *children)
 {
-	ListCell   *lc;
 	List	   *newChildren = NIL;
 
-	foreach(lc, children)
+	foreach_node(RPRPatternNode, child, children)
 	{
-		RPRPatternNode *child = (RPRPatternNode *) lfirst(lc);
 		RPRPatternNode *opt = optimizeRPRPattern(child);
 
 		if (opt->nodeType == RPR_PATTERN_ALT)
@@ -732,18 +719,15 @@ flattenAltChildren(List *children)
 static List *
 removeDuplicateAlternatives(List *children)
 {
-	ListCell   *lc;
-	ListCell   *lc2;
 	List	   *uniqueChildren = NIL;
 
-	foreach(lc, children)
+	foreach_node(RPRPatternNode, child, children)
 	{
-		RPRPatternNode *child = (RPRPatternNode *) lfirst(lc);
 		bool		isDuplicate = false;
 
-		foreach(lc2, uniqueChildren)
+		foreach_node(RPRPatternNode, uchild, uniqueChildren)
 		{
-			if (rprPatternEqual((RPRPatternNode *) lfirst(lc2), child))
+			if (rprPatternEqual(uchild, child))
 			{
 				isDuplicate = true;
 				break;
@@ -940,16 +924,13 @@ tryUnwrapGroup(RPRPatternNode *pattern)
 static RPRPatternNode *
 optimizeGroupPattern(RPRPatternNode *pattern)
 {
-	ListCell   *lc;
 	List	   *newChildren;
 	RPRPatternNode *result;
 
 	/* Recursively optimize children */
 	newChildren = NIL;
-	foreach(lc, pattern->children)
+	foreach_node(RPRPatternNode, child, pattern->children)
 	{
-		RPRPatternNode *child = (RPRPatternNode *) lfirst(lc);
-
 		newChildren = lappend(newChildren, optimizeRPRPattern(child));
 	}
 	pattern->children = newChildren;
@@ -1005,15 +986,14 @@ optimizeRPRPattern(RPRPatternNode *pattern)
 static int
 collectDefineVariables(List *defineVariableList, char **varNames)
 {
-	ListCell   *lc;
 	int			numVars = 0;
 
-	foreach(lc, defineVariableList)
+	foreach_node(String, varname, defineVariableList)
 	{
 		/* Parser already checked this limit in transformDefineClause */
 		Assert(numVars <= RPR_VARID_MAX);
 
-		varNames[numVars++] = strVal(lfirst(lc));
+		varNames[numVars++] = strVal(varname);
 	}
 
 	return numVars;
@@ -1031,7 +1011,6 @@ static void
 scanRPRPatternRecursive(RPRPatternNode *node, char **varNames, int *numVars,
 						int *numElements, RPRDepth depth, RPRDepth *maxDepth)
 {
-	ListCell   *lc;
 	int			i;
 
 	/* Pattern nodes from parser are never NULL */
@@ -1075,9 +1054,9 @@ scanRPRPatternRecursive(RPRPatternNode *node, char **varNames, int *numVars,
 
 		case RPR_PATTERN_SEQ:
 			/* Sequence: just recurse into children */
-			foreach(lc, node->children)
+			foreach_node(RPRPatternNode, child, node->children)
 			{
-				scanRPRPatternRecursive((RPRPatternNode *) lfirst(lc), varNames,
+				scanRPRPatternRecursive(child, varNames,
 										numVars, numElements, depth, maxDepth);
 			}
 			break;
@@ -1088,9 +1067,9 @@ scanRPRPatternRecursive(RPRPatternNode *node, char **varNames, int *numVars,
 				(*numElements)++;
 
 			/* Recurse into children at increased depth */
-			foreach(lc, node->children)
+			foreach_node(RPRPatternNode, child, node->children)
 			{
-				scanRPRPatternRecursive((RPRPatternNode *) lfirst(lc), varNames,
+				scanRPRPatternRecursive(child, varNames,
 										numVars, numElements, depth + 1, maxDepth);
 			}
 
@@ -1104,9 +1083,9 @@ scanRPRPatternRecursive(RPRPatternNode *node, char **varNames, int *numVars,
 			(*numElements)++;
 
 			/* Recurse into children at increased depth */
-			foreach(lc, node->children)
+			foreach_node(RPRPatternNode, child, node->children)
 			{
-				scanRPRPatternRecursive((RPRPatternNode *) lfirst(lc), varNames,
+				scanRPRPatternRecursive(child, varNames,
 										numVars, numElements, depth + 1, maxDepth);
 			}
 			break;
@@ -1248,7 +1227,6 @@ fillRPRPatternVar(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth dept
 static bool
 fillRPRPatternGroup(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth depth)
 {
-	ListCell   *lc;
 	int			groupStartIdx = *idx;
 	int			beginIdx = -1;
 	bool		bodyNullable = true;
@@ -1275,9 +1253,9 @@ fillRPRPatternGroup(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth de
 		groupStartIdx = *idx;	/* children start after BEGIN */
 	}
 
-	foreach(lc, node->children)
+	foreach_node(RPRPatternNode, child, node->children)
 	{
-		if (!fillRPRPattern((RPRPatternNode *) lfirst(lc), pat, idx, depth + 1))
+		if (!fillRPRPattern(child, pat, idx, depth + 1))
 			bodyNullable = false;
 	}
 
@@ -1350,9 +1328,8 @@ fillRPRPatternAlt(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth dept
 	(*idx)++;
 
 	/* Fill each alternative */
-	foreach(lc, node->children)
+	foreach_node(RPRPatternNode, alt, node->children)
 	{
-		RPRPatternNode *alt = (RPRPatternNode *) lfirst(lc);
 		int			branchStart = *idx;
 
 		altBranchStarts = lappend_int(altBranchStarts, branchStart);
@@ -1372,9 +1349,8 @@ fillRPRPatternAlt(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth dept
 
 	/* Set next on last element of each alternative to after the alternation */
 	afterAltIdx = *idx;
-	lc2 = list_head(altBranchStarts);
 
-	foreach(lc, altEndPositions)
+	forboth(lc, altEndPositions, lc2, altBranchStarts)
 	{
 		int			endPos = lfirst_int(lc);
 		int			branchStart = lfirst_int(lc2);
@@ -1399,8 +1375,6 @@ fillRPRPatternAlt(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth dept
 		{
 			pat->elements[endPos].next = afterAltIdx;
 		}
-
-		lc2 = lnext(altBranchStarts, lc2);
 	}
 
 	list_free(altBranchStarts);
@@ -1422,7 +1396,6 @@ fillRPRPatternAlt(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth dept
 static bool
 fillRPRPattern(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth depth)
 {
-	ListCell   *lc;
 	bool		allNullable = true;
 
 	/* Pattern nodes from parser are never NULL */
@@ -1433,9 +1406,9 @@ fillRPRPattern(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth depth)
 	switch (node->nodeType)
 	{
 		case RPR_PATTERN_SEQ:
-			foreach(lc, node->children)
+			foreach_node(RPRPatternNode, child, node->children)
 			{
-				if (!fillRPRPattern((RPRPatternNode *) lfirst(lc), pat, idx, depth))
+				if (!fillRPRPattern(child, pat, idx, depth))
 					allNullable = false;
 			}
 			return allNullable;
@@ -1856,8 +1829,6 @@ computeAbsorbability(RPRPattern *pattern)
 static void
 collectPatternVariablesRecursive(RPRPatternNode *node, List **varNames)
 {
-	ListCell   *lc;
-
 	Assert(node != NULL);
 
 	check_stack_depth();
@@ -1866,9 +1837,9 @@ collectPatternVariablesRecursive(RPRPatternNode *node, List **varNames)
 	{
 		case RPR_PATTERN_VAR:
 			/* Add variable if not already in list */
-			foreach(lc, *varNames)
+			foreach_node(String, varname, *varNames)
 			{
-				if (strcmp(strVal(lfirst(lc)), node->varName) == 0)
+				if (strcmp(strVal(varname), node->varName) == 0)
 					return;		/* Already collected */
 			}
 			*varNames = lappend(*varNames, makeString(pstrdup(node->varName)));
@@ -1877,10 +1848,9 @@ collectPatternVariablesRecursive(RPRPatternNode *node, List **varNames)
 		case RPR_PATTERN_SEQ:
 		case RPR_PATTERN_ALT:
 		case RPR_PATTERN_GROUP:
-			foreach(lc, node->children)
+			foreach_node(RPRPatternNode, child, node->children)
 			{
-				collectPatternVariablesRecursive((RPRPatternNode *) lfirst(lc),
-												 varNames);
+				collectPatternVariablesRecursive(child, varNames);
 			}
 			break;
 	}
@@ -1917,14 +1887,10 @@ collectPatternVariables(RPRPatternNode *pattern)
 void
 buildDefineVariableList(List *defineClause, List **defineVariableList)
 {
-	ListCell   *lc;
-
 	*defineVariableList = NIL;
 
-	foreach(lc, defineClause)
+	foreach_node(TargetEntry, te, defineClause)
 	{
-		TargetEntry *te = (TargetEntry *) lfirst(lc);
-
 		*defineVariableList = lappend(*defineVariableList,
 									  makeString(pstrdup(te->resname)));
 	}
diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c
index 02898a9106b..74dc486b1f5 100644
--- a/src/backend/optimizer/prep/prepjointree.c
+++ b/src/backend/optimizer/prep/prepjointree.c
@@ -2593,10 +2593,8 @@ perform_pullup_replace_vars(PlannerInfo *root,
 	parse->returningList = (List *)
 		pullup_replace_vars((Node *) parse->returningList, rvcontext);
 
-	foreach(lc, parse->windowClause)
+	foreach_node(WindowClause, wc, parse->windowClause)
 	{
-		WindowClause *wc = lfirst_node(WindowClause, lc);
-
 		if (wc->defineClause != NIL)
 			wc->defineClause = (List *)
 				pullup_replace_vars((Node *) wc->defineClause, rvcontext);
diff --git a/src/backend/parser/parse_cte.c b/src/backend/parser/parse_cte.c
index 0974b43d028..3e493beba0b 100644
--- a/src/backend/parser/parse_cte.c
+++ b/src/backend/parser/parse_cte.c
@@ -182,9 +182,8 @@ transformWithClause(ParseState *pstate, WithClause *withClause)
 		 * rewritten to WITH RECURSIVE by makeRecursiveViewSelect() and so
 		 * flows through here as well.
 		 */
-		foreach(lc, withClause->ctes)
+		foreach_node(CommonTableExpr, cte, withClause->ctes)
 		{
-			CommonTableExpr *cte = (CommonTableExpr *) lfirst(lc);
 			ContainRPRContext ctx;
 
 			ctx.location = -1;
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index f65a270d20e..0f7207577b3 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -652,12 +652,11 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 		list_length(cref->fields) != 1)
 	{
 		char	   *qualifier = strVal(linitial(cref->fields));
-		ListCell   *lc;
 		bool		is_pattern_var = false;
 
-		foreach(lc, pstate->p_rpr_pattern_vars)
+		foreach_node(String, pv, pstate->p_rpr_pattern_vars)
 		{
-			if (strcmp(strVal(lfirst(lc)), qualifier) == 0)
+			if (strcmp(strVal(pv), qualifier) == 0)
 			{
 				is_pattern_var = true;
 				break;
diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index 4e1d2650cf6..d12a0d45c94 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -208,8 +208,6 @@ static void
 validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
 						   List *rpDefs, List **varNames)
 {
-	ListCell   *lc;
-
 	/* Pattern node must exist - parser always provides non-NULL root */
 	Assert(node != NULL);
 
@@ -228,9 +226,9 @@ validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
 			{
 				bool		found = false;
 
-				foreach(lc, *varNames)
+				foreach_node(String, varname, *varNames)
 				{
-					if (strcmp(strVal(lfirst(lc)), node->varName) == 0)
+					if (strcmp(strVal(varname), node->varName) == 0)
 					{
 						found = true;
 						break;
@@ -261,10 +259,9 @@ validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
 		case RPR_PATTERN_ALT:
 		case RPR_PATTERN_GROUP:
 			/* Recurse into children */
-			foreach(lc, node->children)
+			foreach_node(RPRPatternNode, child, node->children)
 			{
-				validateRPRPatternVarCount(pstate, (RPRPatternNode *) lfirst(lc),
-										   NULL, varNames);
+				validateRPRPatternVarCount(pstate, child, NULL, varNames);
 			}
 			break;
 	}
@@ -277,15 +274,13 @@ validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
 	 */
 	if (rpDefs)
 	{
-		foreach(lc, rpDefs)
+		foreach_node(ResTarget, rt, rpDefs)
 		{
-			ResTarget  *rt = (ResTarget *) lfirst(lc);
-			ListCell   *lc2;
 			bool		found = false;
 
-			foreach(lc2, *varNames)
+			foreach_node(String, varname, *varNames)
 			{
-				if (strcmp(strVal(lfirst(lc2)), rt->name) == 0)
+				if (strcmp(strVal(varname), rt->name) == 0)
 				{
 					found = true;
 					break;
@@ -327,10 +322,6 @@ static List *
 transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 					  List **targetlist)
 {
-	ListCell   *lc,
-			   *l;
-	ResTarget  *restarget,
-			   *r;
 	List	   *restargets;
 	List	   *defineClause = NIL;
 	char	   *name;
@@ -357,18 +348,16 @@ transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 	 * equivalent.
 	 */
 	restargets = NIL;
-	foreach(lc, windef->rpCommonSyntax->rpDefs)
+	foreach_node(ResTarget, restarget, windef->rpCommonSyntax->rpDefs)
 	{
 		TargetEntry *teDefine;
 
-		restarget = (ResTarget *) lfirst(lc);
 		name = restarget->name;
 
-		foreach(l, restargets)
+		foreach_node(ResTarget, r, restargets)
 		{
 			char	   *n;
 
-			r = (ResTarget *) lfirst(l);
 			n = r->name;
 
 			if (!strcmp(n, name))
@@ -394,7 +383,6 @@ transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 		{
 			Node	   *expr;
 			List	   *vars;
-			ListCell   *lc2;
 
 			expr = transformExpr(pstate, restarget->val,
 								 EXPR_KIND_RPR_DEFINE);
@@ -407,16 +395,12 @@ transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 			 * evaluation.
 			 */
 			vars = pull_var_clause(expr, 0);
-			foreach(lc2, vars)
+			foreach_node(Var, var, vars)
 			{
-				Var		   *var = (Var *) lfirst(lc2);
 				bool		found = false;
-				ListCell   *tl;
 
-				foreach(tl, *targetlist)
+				foreach_node(TargetEntry, tle, *targetlist)
 				{
-					TargetEntry *tle = (TargetEntry *) lfirst(tl);
-
 					if (IsA(tle->expr, Var) &&
 						((Var *) tle->expr)->varno == var->varno &&
 						((Var *) tle->expr)->varattno == var->varattno)
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index d588cd8263d..415da6417d4 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -7153,7 +7153,6 @@ static void
 get_rule_pattern_node(RPRPatternNode *node, deparse_context *context)
 {
 	StringInfo	buf = context->buf;
-	ListCell   *lc;
 	const char *sep;
 
 	Assert(node != NULL);
@@ -7167,20 +7166,20 @@ get_rule_pattern_node(RPRPatternNode *node, deparse_context *context)
 
 		case RPR_PATTERN_SEQ:
 			sep = "";
-			foreach(lc, node->children)
+			foreach_node(RPRPatternNode, child, node->children)
 			{
 				appendStringInfoString(buf, sep);
-				get_rule_pattern_node((RPRPatternNode *) lfirst(lc), context);
+				get_rule_pattern_node(child, context);
 				sep = " ";
 			}
 			break;
 
 		case RPR_PATTERN_ALT:
 			sep = "";
-			foreach(lc, node->children)
+			foreach_node(RPRPatternNode, child, node->children)
 			{
 				appendStringInfoString(buf, sep);
-				get_rule_pattern_node((RPRPatternNode *) lfirst(lc), context);
+				get_rule_pattern_node(child, context);
 				sep = " | ";
 			}
 			break;
@@ -7188,10 +7187,10 @@ get_rule_pattern_node(RPRPatternNode *node, deparse_context *context)
 		case RPR_PATTERN_GROUP:
 			appendStringInfoChar(buf, '(');
 			sep = "";
-			foreach(lc, node->children)
+			foreach_node(RPRPatternNode, child, node->children)
 			{
 				appendStringInfoString(buf, sep);
-				get_rule_pattern_node((RPRPatternNode *) lfirst(lc), context);
+				get_rule_pattern_node(child, context);
 				sep = " ";
 			}
 			appendStringInfoChar(buf, ')');
@@ -7221,14 +7220,11 @@ get_rule_define(List *defineClause, deparse_context *context)
 {
 	StringInfo	buf = context->buf;
 	const char *sep;
-	ListCell   *lc_def;
 
 	sep = "  ";
 
-	foreach(lc_def, defineClause)
+	foreach_node(TargetEntry, te, defineClause)
 	{
-		TargetEntry *te = (TargetEntry *) lfirst(lc_def);
-
 		appendStringInfo(buf, "%s%s AS ", sep, quote_identifier(te->resname));
 		get_rule_expr((Node *) te->expr, context, false);
 		sep = ",\n  ";
-- 
2.50.1 (Apple Git-155)



Attachments:

  [text/plain] nocfbot-0004-Reclassify-DEFINE-qualifier-check-and-reword-diag.txt (11.9K, 3-nocfbot-0004-Reclassify-DEFINE-qualifier-check-and-reword-diag.txt)
  download | inline diff:
From c215fec2b4a9575973314b8b046552cb5870645e Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Tue, 5 May 2026 11:36:53 +0900
Subject: [PATCH 04/68] Reclassify DEFINE qualifier check and reword diagnostic
 to "expression"

Split the pre-check into three branches: pattern variable ->
FEATURE_NOT_SUPPORTED, range variable (via refnameNamespaceItem) ->
SYNTAX_ERROR, anything else -> fall through to normal resolution.
Reword "qualified column reference" to "qualified expression" since
the quoted token may include an indirection target on composite
types (e.g. (A.items).amount).
---
 src/backend/parser/parse_expr.c        | 31 +++++++---
 src/test/regress/expected/rpr_base.out | 80 +++++++++++++++++++++++---
 src/test/regress/sql/rpr_base.sql      | 61 ++++++++++++++++++--
 3 files changed, 153 insertions(+), 19 deletions(-)

diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 1e9cee36e35..4221643d9c8 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -628,11 +628,24 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 	if (node != NULL)
 		return node;
 
-	/*
-	 * Qualified column references in DEFINE are not supported.  This covers
-	 * both FROM-clause range variables (prohibited by §6.5) and pattern
-	 * variable qualified names (e.g. UP.price), which are valid per §4.16
-	 * but not yet implemented.
+	/*----------
+	 * Qualified references in DEFINE need a tri-classification:
+	 *
+	 *	  pattern variable qualifier (e.g. UP.price): valid per 19075-5 4.16
+	 *	  but not yet implemented -- raise FEATURE_NOT_SUPPORTED.
+	 *
+	 *	  FROM-clause range variable qualifier: prohibited by 19075-5 6.5
+	 *	  -- raise SYNTAX_ERROR.
+	 *
+	 *	  any other qualifier (typo, undefined name): fall through and let
+	 *	  normal column resolution produce a sensible error.
+	 *
+	 * The quoted text reflects only the ColumnRef portion; a trailing field
+	 * selection on a composite type (e.g. ".amount" in "(A.items).amount")
+	 * lives in the surrounding A_Indirection node and is not included here.
+	 * That can be revisited when MEASURES support adds indirection-aware
+	 * traversal.
+	 *----------
 	 */
 	if (pstate->p_expr_kind == EXPR_KIND_RPR_DEFINE &&
 		list_length(cref->fields) != 1)
@@ -653,15 +666,17 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 		if (is_pattern_var)
 			ereport(ERROR,
 					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-					 errmsg("pattern variable qualified column reference \"%s\" is not supported in DEFINE clause",
+					 errmsg("pattern variable qualified expression \"%s\" is not supported in DEFINE clause",
 							NameListToString(cref->fields)),
 					 parser_errposition(pstate, cref->location)));
-		else
+		else if (refnameNamespaceItem(pstate, NULL, qualifier,
+									  cref->location, NULL) != NULL)
 			ereport(ERROR,
 					(errcode(ERRCODE_SYNTAX_ERROR),
-					 errmsg("range variable qualified column reference \"%s\" is not allowed in DEFINE clause",
+					 errmsg("range variable qualified expression \"%s\" is not allowed in DEFINE clause",
 							NameListToString(cref->fields)),
 					 parser_errposition(pstate, cref->location)));
+		/* else: unknown qualifier -- fall through to normal resolution */
 	}
 
 	/*----------
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index a63211ff364..86abb96c177 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -3074,10 +3074,10 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS A.val > 0
 );
-ERROR:  pattern variable qualified column reference "a.val" is not supported in DEFINE clause
+ERROR:  pattern variable qualified expression "a.val" is not supported in DEFINE clause
 LINE 7:     DEFINE A AS A.val > 0
                         ^
--- Expected: ERROR: pattern variable qualified column reference "a.val" is not supported
+-- Expected: ERROR: pattern variable qualified expression "a.val" is not supported
 -- PATTERN-only variable qualified name: not supported even without DEFINE entry
 SELECT COUNT(*) OVER w
 FROM rpr_err
@@ -3087,10 +3087,10 @@ WINDOW w AS (
     PATTERN (A+ B+)
     DEFINE A AS B.val > 0
 );
-ERROR:  pattern variable qualified column reference "b.val" is not supported in DEFINE clause
+ERROR:  pattern variable qualified expression "b.val" is not supported in DEFINE clause
 LINE 7:     DEFINE A AS B.val > 0
                         ^
--- Expected: ERROR: pattern variable qualified column reference "b.val" is not supported
+-- Expected: ERROR: pattern variable qualified expression "b.val" is not supported
 -- DEFINE-only variable qualified name: still a pattern variable, not a range variable
 SELECT COUNT(*) OVER w
 FROM rpr_err
@@ -3103,7 +3103,7 @@ WINDOW w AS (
 ERROR:  DEFINE variable "b" is not used in PATTERN
 LINE 7:     DEFINE A AS val > 0, B AS B.val > 0
                                  ^
--- Expected: ERROR: pattern variable qualified column reference "b.val" is not supported
+-- Expected: ERROR: pattern variable qualified expression "b.val" is not supported
 -- FROM-clause range variable qualified name: not allowed (prohibited by SQL standard 6.5)
 SELECT COUNT(*) OVER w
 FROM rpr_err
@@ -3113,10 +3113,76 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS rpr_err.val > 0
 );
-ERROR:  range variable qualified column reference "rpr_err.val" is not allowed in DEFINE clause
+ERROR:  range variable qualified expression "rpr_err.val" is not allowed in DEFINE clause
 LINE 7:     DEFINE A AS rpr_err.val > 0
                         ^
--- Expected: ERROR: range variable qualified column reference "rpr_err.val" is not allowed
+-- Expected: ERROR: range variable qualified expression "rpr_err.val" is not allowed
+-- Unknown qualifier (neither pattern var nor range var): the DEFINE pre-check
+-- must fall through so that normal column resolution produces a sensible error.
+SELECT COUNT(*) OVER w
+FROM rpr_err
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS nosuch.val > 0
+);
+ERROR:  missing FROM-clause entry for table "nosuch"
+LINE 7:     DEFINE A AS nosuch.val > 0
+                        ^
+-- Expected: ERROR: missing FROM-clause entry for table "nosuch"
+-- Unqualified composite field access in DEFINE works: no qualifier means no
+-- pattern/range-var navigation, so the pre-check skips and normal resolution
+-- handles "(items).amount" via A_Indirection on the current row.
+CREATE TYPE rpr_item AS (name TEXT, amount INT);
+CREATE TEMP TABLE rpr_composite (id int, items rpr_item);
+INSERT INTO rpr_composite VALUES (1, ROW('a',5)), (2, ROW('b',15)), (3, ROW('c',25));
+SELECT id, (items).amount, COUNT(*) OVER w AS cnt
+FROM rpr_composite
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A+)
+    DEFINE A AS (items).amount > 10
+);
+ id | amount | cnt 
+----+--------+-----
+  1 |      5 |   0
+  2 |     15 |   2
+  3 |     25 |   1
+(3 rows)
+
+-- Expected: rows where (items).amount > 10 form matches; counts reflect frame size
+-- Composite type field selection (qualified forms): the ColumnRef portion ("A.items" or
+-- "rpr_composite.items") is what gets quoted; the trailing ".amount" lives in
+-- the surrounding A_Indirection node and is not visible to the pre-check.
+SELECT COUNT(*) OVER w
+FROM rpr_composite
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS (A.items).amount > 10
+);
+ERROR:  pattern variable qualified expression "a.items" is not supported in DEFINE clause
+LINE 7:     DEFINE A AS (A.items).amount > 10
+                         ^
+-- Expected: ERROR: pattern variable qualified expression "a.items" is not supported
+SELECT COUNT(*) OVER w
+FROM rpr_composite
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS (rpr_composite.items).amount > 10
+);
+ERROR:  range variable qualified expression "rpr_composite.items" is not allowed in DEFINE clause
+LINE 7:     DEFINE A AS (rpr_composite.items).amount > 10
+                         ^
+-- Expected: ERROR: range variable qualified expression "rpr_composite.items" is not allowed
+DROP TABLE rpr_composite;
+DROP TYPE rpr_item;
 -- Semantic errors
 -- Undefined column in DEFINE
 SELECT COUNT(*) OVER w
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index 86ed06fec68..e8c72706720 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -2092,7 +2092,7 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS A.val > 0
 );
--- Expected: ERROR: pattern variable qualified column reference "a.val" is not supported
+-- Expected: ERROR: pattern variable qualified expression "a.val" is not supported
 
 -- PATTERN-only variable qualified name: not supported even without DEFINE entry
 SELECT COUNT(*) OVER w
@@ -2103,7 +2103,7 @@ WINDOW w AS (
     PATTERN (A+ B+)
     DEFINE A AS B.val > 0
 );
--- Expected: ERROR: pattern variable qualified column reference "b.val" is not supported
+-- Expected: ERROR: pattern variable qualified expression "b.val" is not supported
 
 -- DEFINE-only variable qualified name: still a pattern variable, not a range variable
 SELECT COUNT(*) OVER w
@@ -2114,7 +2114,7 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > 0, B AS B.val > 0
 );
--- Expected: ERROR: pattern variable qualified column reference "b.val" is not supported
+-- Expected: ERROR: pattern variable qualified expression "b.val" is not supported
 
 -- FROM-clause range variable qualified name: not allowed (prohibited by SQL standard 6.5)
 SELECT COUNT(*) OVER w
@@ -2125,7 +2125,60 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS rpr_err.val > 0
 );
--- Expected: ERROR: range variable qualified column reference "rpr_err.val" is not allowed
+-- Expected: ERROR: range variable qualified expression "rpr_err.val" is not allowed
+
+-- Unknown qualifier (neither pattern var nor range var): the DEFINE pre-check
+-- must fall through so that normal column resolution produces a sensible error.
+SELECT COUNT(*) OVER w
+FROM rpr_err
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS nosuch.val > 0
+);
+-- Expected: ERROR: missing FROM-clause entry for table "nosuch"
+
+-- Unqualified composite field access in DEFINE works: no qualifier means no
+-- pattern/range-var navigation, so the pre-check skips and normal resolution
+-- handles "(items).amount" via A_Indirection on the current row.
+CREATE TYPE rpr_item AS (name TEXT, amount INT);
+CREATE TEMP TABLE rpr_composite (id int, items rpr_item);
+INSERT INTO rpr_composite VALUES (1, ROW('a',5)), (2, ROW('b',15)), (3, ROW('c',25));
+SELECT id, (items).amount, COUNT(*) OVER w AS cnt
+FROM rpr_composite
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A+)
+    DEFINE A AS (items).amount > 10
+);
+-- Expected: rows where (items).amount > 10 form matches; counts reflect frame size
+
+-- Composite type field selection (qualified forms): the ColumnRef portion ("A.items" or
+-- "rpr_composite.items") is what gets quoted; the trailing ".amount" lives in
+-- the surrounding A_Indirection node and is not visible to the pre-check.
+SELECT COUNT(*) OVER w
+FROM rpr_composite
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS (A.items).amount > 10
+);
+-- Expected: ERROR: pattern variable qualified expression "a.items" is not supported
+SELECT COUNT(*) OVER w
+FROM rpr_composite
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS (rpr_composite.items).amount > 10
+);
+-- Expected: ERROR: range variable qualified expression "rpr_composite.items" is not allowed
+DROP TABLE rpr_composite;
+DROP TYPE rpr_item;
 
 -- Semantic errors
 
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0002-Unify-RPR-DEFINE-walkers-and-reject-volatile-call.txt (69.1K, 4-nocfbot-0002-Unify-RPR-DEFINE-walkers-and-reject-volatile-call.txt)
  download

  [text/plain] nocfbot-0005-Sync-stale-comments-on-DEFINE-PATTERN-handling.txt (4.4K, 5-nocfbot-0005-Sync-stale-comments-on-DEFINE-PATTERN-handling.txt)
  download | inline diff:
From d9fc4c58303b6d474168f3168451c2c2e973a848 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Tue, 5 May 2026 11:44:45 +0900
Subject: [PATCH 05/68] Sync stale comments on DEFINE/PATTERN handling

validateRPRPatternVarCount() validates DEFINE names against the
PATTERN list and rejects any not used in PATTERN; the surrounding
comments said it "collected" them.  Align the function header,
inline block comment, and the call-site comment.  Also fix the
matching SELECT doc ("filtered during planning" -> "rejected with
an error") and the XXX note's wording.
---
 doc/src/sgml/ref/select.sgml   |  4 ++--
 src/backend/parser/parse_rpr.c | 28 ++++++++++++++--------------
 2 files changed, 16 insertions(+), 16 deletions(-)

diff --git a/doc/src/sgml/ref/select.sgml b/doc/src/sgml/ref/select.sgml
index 5272d6c0bfa..e4708331439 100644
--- a/doc/src/sgml/ref/select.sgml
+++ b/doc/src/sgml/ref/select.sgml
@@ -1192,8 +1192,8 @@ DEFINE <replaceable class="parameter">definition_variable_name</replaceable> AS
 </synopsis>
 
     Conversely, variables defined in the <literal>DEFINE</literal> clause
-    but not used in the <literal>PATTERN</literal> clause are filtered out
-    during query planning.
+    but not used in the <literal>PATTERN</literal> clause are rejected
+    with an error.
    </para>
 
    <para>
diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index 87411abcbe2..2c6fccebd47 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -183,11 +183,11 @@ transformRPR(ParseState *pstate, WindowClause *wc, WindowDef *windef,
  * Recursively traverses the pattern tree, collecting unique variable names.
  * Throws an error if the number of unique variables exceeds RPR_VARID_MAX.
  *
- * If rpDefs is non-NULL, DEFINE variable names are also collected into
- * varNames so that transformColumnRef can distinguish pattern variable
- * qualifiers from FROM-clause range variables.
- *
- * varNames is both input and output: existing names are preserved, new ones added.
+ * If rpDefs is non-NULL, each DEFINE variable name is also validated against
+ * varNames; any DEFINE name not present in PATTERN is rejected with an error.
+ * varNames itself is not extended by this step -- it carries only PATTERN
+ * variable names, which is what transformColumnRef checks via
+ * p_rpr_pattern_vars to identify pattern variable qualifiers.
  */
 static void
 validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
@@ -244,10 +244,10 @@ validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
 	}
 
 	/*
-	 * After the top-level call, also collect DEFINE variable names that are
-	 * not already in the list.  This is only done once at the outermost
-	 * recursion level, detected by rpDefs being non-NULL (recursive calls
-	 * pass NULL).
+	 * After the top-level call, validate that every DEFINE variable name is
+	 * present in the PATTERN variable list; reject names not used in PATTERN.
+	 * This is only done once at the outermost recursion level, detected by
+	 * rpDefs being non-NULL (recursive calls pass NULL).
 	 */
 	if (rpDefs)
 	{
@@ -293,9 +293,9 @@ validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
  * Note: Variables not in DEFINE are evaluated as TRUE by the executor.
  * Variables in DEFINE but not in PATTERN are rejected as an error.
  *
- * XXX Pattern variable qualified column references in DEFINE (e.g.
- * "A.price") are not yet supported.  Currently rejected by
- * transformColumnRef in parse_expr.c via the p_rpr_pattern_vars check.
+ * XXX Pattern variable qualified expressions in DEFINE (e.g. "A.price")
+ * are not yet supported.  Currently rejected by transformColumnRef in
+ * parse_expr.c via the p_rpr_pattern_vars check.
  */
 static List *
 transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
@@ -317,8 +317,8 @@ transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 	Assert(windef->rpCommonSyntax->rpDefs != NULL);
 
 	/*
-	 * Validate PATTERN variable count and collect all RPR variable names
-	 * (PATTERN + DEFINE) for use in transformColumnRef.
+	 * Validate PATTERN variable count, reject DEFINE variables not used in
+	 * PATTERN, and collect PATTERN variable names for transformColumnRef.
 	 */
 	validateRPRPatternVarCount(pstate, windef->rpCommonSyntax->rpPattern,
 							   windef->rpCommonSyntax->rpDefs,
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0003-Cover-RPR-empty-match-path-with-EXPLAIN-tests-fix.txt (19.0K, 6-nocfbot-0003-Cover-RPR-empty-match-path-with-EXPLAIN-tests-fix.txt)
  download | inline diff:
From 31b21e064270f94a8ec27305ea47740ad5b62382 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Thu, 7 May 2026 14:29:14 +0900
Subject: [PATCH 03/68] Cover RPR empty-match path with EXPLAIN tests; fix
 stale XXX comments

The test_728_* cases in rpr_nfa.sql claimed (via XXX) that the visited
bitmap blocks empty iterations.  In fact the NFA finds the empty
matches; window aggregates just return 0 / NULL over the length-0
frame, indistinguishable from "no match" without MATCH_NUMBER().

Replace the XXX comments with the actual behavior, and add paired
EXPLAIN ANALYZE tests in rpr_explain.sql that lock in
"NFA: 3 matched (len 0/0/0.0)" as observable regression coverage.
---
 src/test/regress/expected/rpr_explain.out | 200 ++++++++++++++++++++++
 src/test/regress/expected/rpr_nfa.out     |  29 ++--
 src/test/regress/sql/rpr_explain.sql      | 116 +++++++++++++
 src/test/regress/sql/rpr_nfa.sql          |  29 ++--
 4 files changed, 340 insertions(+), 34 deletions(-)

diff --git a/src/test/regress/expected/rpr_explain.out b/src/test/regress/expected/rpr_explain.out
index 0a049d1beba..c4516d3c756 100644
--- a/src/test/regress/expected/rpr_explain.out
+++ b/src/test/regress/expected/rpr_explain.out
@@ -2102,6 +2102,206 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=0.00 loops=1)
 (5 rows)
 
+-- Empty matches (length 0): mirror the test_728_* cases in rpr_nfa.sql.
+-- Window aggregates over a length-0 frame return 0 / NULL, so the SELECT
+-- result alone cannot distinguish "no match" from "empty match"; the
+-- "NFA: N matched (len 0/0/0.0)" line in EXPLAIN is the only observable
+-- proof that the empty matches were found.
+-- (A?){0,3}: min=0, A never matches -> 3 length-0 matches
+CREATE VIEW rpr_ev_edge_empty_match_min0 AS
+SELECT count(*) OVER w
+FROM generate_series(1, 3) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A?){0,3})
+    DEFINE A AS FALSE
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_edge_empty_match_min0'), E'\n')) AS line WHERE line ~ 'PATTERN';
+          line          
+------------------------
+   PATTERN ((a?){0,3}) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 3) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A?){0,3})
+    DEFINE A AS FALSE
+);');
+                         rpr_explain_filter                          
+---------------------------------------------------------------------
+ WindowAgg (actual rows=3.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a?){0,3}
+   Nav Mark Lookback: 0
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 6 peak, 20 total, 4 merged
+   NFA Contexts: 2 peak, 4 total, 0 pruned
+   NFA: 3 matched (len 0/0/0.0), 0 mismatched
+   ->  Function Scan on generate_series s (actual rows=3.00 loops=1)
+(9 rows)
+
+-- (A?){1,3}: min=1, one empty iteration satisfies min -> 3 length-0 matches
+CREATE VIEW rpr_ev_edge_empty_match_min1 AS
+SELECT count(*) OVER w
+FROM generate_series(1, 3) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A?){1,3})
+    DEFINE A AS FALSE
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_edge_empty_match_min1'), E'\n')) AS line WHERE line ~ 'PATTERN';
+          line          
+------------------------
+   PATTERN ((a?){1,3}) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 3) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A?){1,3})
+    DEFINE A AS FALSE
+);');
+                         rpr_explain_filter                          
+---------------------------------------------------------------------
+ WindowAgg (actual rows=3.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a?){1,3}
+   Nav Mark Lookback: 0
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 5 peak, 16 total, 4 merged
+   NFA Contexts: 2 peak, 4 total, 0 pruned
+   NFA: 3 matched (len 0/0/0.0), 0 mismatched
+   ->  Function Scan on generate_series s (actual rows=3.00 loops=1)
+(9 rows)
+
+-- (A?){2,3}: min=2 (SQL:2016 STR06 = STRE STRE) -> 3 length-0 matches
+CREATE VIEW rpr_ev_edge_empty_match_min2 AS
+SELECT count(*) OVER w
+FROM generate_series(1, 3) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A?){2,3})
+    DEFINE A AS FALSE
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_edge_empty_match_min2'), E'\n')) AS line WHERE line ~ 'PATTERN';
+          line          
+------------------------
+   PATTERN ((a?){2,3}) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 3) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A?){2,3})
+    DEFINE A AS FALSE
+);');
+                         rpr_explain_filter                          
+---------------------------------------------------------------------
+ WindowAgg (actual rows=3.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a?){2,3}
+   Nav Mark Lookback: 0
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 5 peak, 16 total, 4 merged
+   NFA Contexts: 2 peak, 4 total, 0 pruned
+   NFA: 3 matched (len 0/0/0.0), 0 mismatched
+   ->  Function Scan on generate_series s (actual rows=3.00 loops=1)
+(9 rows)
+
+-- (A?){2,3} mixed: rows 1-2 match A (real), rows 3-4 fall back to empty
+CREATE VIEW rpr_ev_edge_empty_match_mixed AS
+SELECT count(*) OVER w
+FROM generate_series(1, 4) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A?){2,3})
+    DEFINE A AS v <= 2
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_edge_empty_match_mixed'), E'\n')) AS line WHERE line ~ 'PATTERN';
+          line          
+------------------------
+   PATTERN ((a?){2,3}) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 4) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A?){2,3})
+    DEFINE A AS v <= 2
+);');
+                         rpr_explain_filter                          
+---------------------------------------------------------------------
+ WindowAgg (actual rows=4.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a?){2,3}
+   Nav Mark Lookback: 0
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 8 peak, 26 total, 5 merged
+   NFA Contexts: 4 peak, 5 total, 1 pruned
+   NFA: 3 matched (len 0/2/1.0), 0 mismatched
+   ->  Function Scan on generate_series s (actual rows=4.00 loops=1)
+(9 rows)
+
+-- (A? B?){2,3}: pure empty multi-element body -> 3 length-0 matches
+CREATE VIEW rpr_ev_edge_empty_match_multi AS
+SELECT count(*) OVER w
+FROM generate_series(1, 3) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A? B?){2,3})
+    DEFINE A AS FALSE, B AS FALSE
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_edge_empty_match_multi'), E'\n')) AS line WHERE line ~ 'PATTERN';
+           line            
+---------------------------
+   PATTERN ((a? b?){2,3}) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 3) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A? B?){2,3})
+    DEFINE A AS FALSE, B AS FALSE
+);');
+                         rpr_explain_filter                          
+---------------------------------------------------------------------
+ WindowAgg (actual rows=3.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a? b?){2,3}
+   Nav Mark Lookback: 0
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 6 peak, 20 total, 0 merged
+   NFA Contexts: 2 peak, 4 total, 0 pruned
+   NFA: 3 matched (len 0/0/0.0), 0 mismatched
+   ->  Function Scan on generate_series s (actual rows=3.00 loops=1)
+(9 rows)
+
 -- Single row
 CREATE VIEW rpr_ev_edge_single_row AS
 SELECT count(*) OVER w
diff --git a/src/test/regress/expected/rpr_nfa.out b/src/test/regress/expected/rpr_nfa.out
index a19b26c3b94..4cff7cfbbd7 100644
--- a/src/test/regress/expected/rpr_nfa.out
+++ b/src/test/regress/expected/rpr_nfa.out
@@ -4395,9 +4395,9 @@ WINDOW w AS (
 (3 rows)
 
 -- (A?){0,3}: min=0, nullable inner.
--- A never matches. A? matches empty, min=0 satisfied immediately.
--- Per standard: empty match expected for every row.
--- XXX: visited bitmap blocks empty iteration -> no match (same as {2,3})
+-- A never matches but A? matches empty, satisfying min=0 immediately.
+-- NFA reports 3 length-0 matches (one per row); first_value / last_value
+-- are NULL because the window frame for an empty match has no rows.
 WITH test_728_min0 AS (
     SELECT * FROM (VALUES
         (1, ARRAY['B']),
@@ -4425,9 +4425,8 @@ WINDOW w AS (
 (3 rows)
 
 -- (A?){1,3}: min=1, nullable inner.
--- A never matches. Need 1 empty iteration to satisfy min=1.
--- Per standard: empty match expected for every row.
--- XXX: visited bitmap blocks empty iteration -> no match (same as {2,3})
+-- A never matches; one empty iteration satisfies min=1.
+-- NFA reports 3 length-0 matches; first/last_value NULL over empty frame.
 WITH test_728_min1 AS (
     SELECT * FROM (VALUES
         (1, ARRAY['B']),
@@ -4454,11 +4453,9 @@ WINDOW w AS (
   3 | {B}   |             |          
 (3 rows)
 
--- (A?){2,3}: min=2, nullable inner.
--- A never matches. Need 2 empty iterations to satisfy min=2.
--- Per standard: STR06=(STRE STRE) is valid for min=2.
--- Expected: empty match for every row
--- XXX: visited bitmap blocks second empty iteration -> match failure
+-- (A?){2,3}: min=2, nullable inner.  Per SQL:2016 STR06 = (STRE STRE)
+-- is valid: two empty iterations satisfy min=2.
+-- NFA reports 3 length-0 matches; first/last_value NULL over empty frame.
 WITH test_728_min2 AS (
     SELECT * FROM (VALUES
         (1, ARRAY['B']),
@@ -4486,9 +4483,8 @@ WINDOW w AS (
 (3 rows)
 
 -- (A?){2,3} mixed: some rows match A, some don't
--- Rows 1-2: A matches, greedy takes 2 -> min satisfied
--- Row 3: A doesn't match, needs 2 empty iterations for min=2
--- XXX: Row 3 fails due to visited bitmap (same as pure empty {2,3})
+-- Rows 1-2: A matches, greedy takes 2 -> min satisfied (real match)
+-- Row 3: A doesn't match, two empty iterations satisfy min=2 (length-0 match)
 -- Row 4: A matches 1 real iter + 1 ff empty exit -> match 4-4
 WITH test_728_min2_mixed AS (
     SELECT * FROM (VALUES
@@ -4559,9 +4555,8 @@ WINDOW w AS (
   6 | {B}   |           6 |         6
 (6 rows)
 
--- (A? B?){2,3}: pure empty body (nothing matches)
--- XXX: All NULL: same issue as test_728_min2 (empty match at context
--- start yields UNMATCHED via startPos-1 initial advance)
+-- (A? B?){2,3}: pure empty body (nothing matches A or B).
+-- NFA reports 3 length-0 matches; first/last_value NULL over empty frame.
 WITH test_728_multi_empty AS (
     SELECT * FROM (VALUES
         (1, ARRAY['C']),
diff --git a/src/test/regress/sql/rpr_explain.sql b/src/test/regress/sql/rpr_explain.sql
index e123be60aea..d339a80a673 100644
--- a/src/test/regress/sql/rpr_explain.sql
+++ b/src/test/regress/sql/rpr_explain.sql
@@ -1178,6 +1178,122 @@ WINDOW w AS (
     DEFINE A AS v = 1, B AS v = 2
 );');
 
+-- Empty matches (length 0): mirror the test_728_* cases in rpr_nfa.sql.
+-- Window aggregates over a length-0 frame return 0 / NULL, so the SELECT
+-- result alone cannot distinguish "no match" from "empty match"; the
+-- "NFA: N matched (len 0/0/0.0)" line in EXPLAIN is the only observable
+-- proof that the empty matches were found.
+
+-- (A?){0,3}: min=0, A never matches -> 3 length-0 matches
+CREATE VIEW rpr_ev_edge_empty_match_min0 AS
+SELECT count(*) OVER w
+FROM generate_series(1, 3) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A?){0,3})
+    DEFINE A AS FALSE
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_edge_empty_match_min0'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 3) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A?){0,3})
+    DEFINE A AS FALSE
+);');
+
+-- (A?){1,3}: min=1, one empty iteration satisfies min -> 3 length-0 matches
+CREATE VIEW rpr_ev_edge_empty_match_min1 AS
+SELECT count(*) OVER w
+FROM generate_series(1, 3) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A?){1,3})
+    DEFINE A AS FALSE
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_edge_empty_match_min1'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 3) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A?){1,3})
+    DEFINE A AS FALSE
+);');
+
+-- (A?){2,3}: min=2 (SQL:2016 STR06 = STRE STRE) -> 3 length-0 matches
+CREATE VIEW rpr_ev_edge_empty_match_min2 AS
+SELECT count(*) OVER w
+FROM generate_series(1, 3) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A?){2,3})
+    DEFINE A AS FALSE
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_edge_empty_match_min2'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 3) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A?){2,3})
+    DEFINE A AS FALSE
+);');
+
+-- (A?){2,3} mixed: rows 1-2 match A (real), rows 3-4 fall back to empty
+CREATE VIEW rpr_ev_edge_empty_match_mixed AS
+SELECT count(*) OVER w
+FROM generate_series(1, 4) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A?){2,3})
+    DEFINE A AS v <= 2
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_edge_empty_match_mixed'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 4) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A?){2,3})
+    DEFINE A AS v <= 2
+);');
+
+-- (A? B?){2,3}: pure empty multi-element body -> 3 length-0 matches
+CREATE VIEW rpr_ev_edge_empty_match_multi AS
+SELECT count(*) OVER w
+FROM generate_series(1, 3) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A? B?){2,3})
+    DEFINE A AS FALSE, B AS FALSE
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_edge_empty_match_multi'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 3) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN ((A? B?){2,3})
+    DEFINE A AS FALSE, B AS FALSE
+);');
+
 -- Single row
 CREATE VIEW rpr_ev_edge_single_row AS
 SELECT count(*) OVER w
diff --git a/src/test/regress/sql/rpr_nfa.sql b/src/test/regress/sql/rpr_nfa.sql
index 1d27e0dc09e..29ec4a9dacb 100644
--- a/src/test/regress/sql/rpr_nfa.sql
+++ b/src/test/regress/sql/rpr_nfa.sql
@@ -3234,9 +3234,9 @@ WINDOW w AS (
 );
 
 -- (A?){0,3}: min=0, nullable inner.
--- A never matches. A? matches empty, min=0 satisfied immediately.
--- Per standard: empty match expected for every row.
--- XXX: visited bitmap blocks empty iteration -> no match (same as {2,3})
+-- A never matches but A? matches empty, satisfying min=0 immediately.
+-- NFA reports 3 length-0 matches (one per row); first_value / last_value
+-- are NULL because the window frame for an empty match has no rows.
 WITH test_728_min0 AS (
     SELECT * FROM (VALUES
         (1, ARRAY['B']),
@@ -3258,9 +3258,8 @@ WINDOW w AS (
 );
 
 -- (A?){1,3}: min=1, nullable inner.
--- A never matches. Need 1 empty iteration to satisfy min=1.
--- Per standard: empty match expected for every row.
--- XXX: visited bitmap blocks empty iteration -> no match (same as {2,3})
+-- A never matches; one empty iteration satisfies min=1.
+-- NFA reports 3 length-0 matches; first/last_value NULL over empty frame.
 WITH test_728_min1 AS (
     SELECT * FROM (VALUES
         (1, ARRAY['B']),
@@ -3281,11 +3280,9 @@ WINDOW w AS (
         A AS 'A' = ANY(flags)
 );
 
--- (A?){2,3}: min=2, nullable inner.
--- A never matches. Need 2 empty iterations to satisfy min=2.
--- Per standard: STR06=(STRE STRE) is valid for min=2.
--- Expected: empty match for every row
--- XXX: visited bitmap blocks second empty iteration -> match failure
+-- (A?){2,3}: min=2, nullable inner.  Per SQL:2016 STR06 = (STRE STRE)
+-- is valid: two empty iterations satisfy min=2.
+-- NFA reports 3 length-0 matches; first/last_value NULL over empty frame.
 WITH test_728_min2 AS (
     SELECT * FROM (VALUES
         (1, ARRAY['B']),
@@ -3307,9 +3304,8 @@ WINDOW w AS (
 );
 
 -- (A?){2,3} mixed: some rows match A, some don't
--- Rows 1-2: A matches, greedy takes 2 -> min satisfied
--- Row 3: A doesn't match, needs 2 empty iterations for min=2
--- XXX: Row 3 fails due to visited bitmap (same as pure empty {2,3})
+-- Rows 1-2: A matches, greedy takes 2 -> min satisfied (real match)
+-- Row 3: A doesn't match, two empty iterations satisfy min=2 (length-0 match)
 -- Row 4: A matches 1 real iter + 1 ff empty exit -> match 4-4
 WITH test_728_min2_mixed AS (
     SELECT * FROM (VALUES
@@ -3364,9 +3360,8 @@ WINDOW w AS (
         B AS 'B' = ANY(flags)
 );
 
--- (A? B?){2,3}: pure empty body (nothing matches)
--- XXX: All NULL: same issue as test_728_min2 (empty match at context
--- start yields UNMATCHED via startPos-1 initial advance)
+-- (A? B?){2,3}: pure empty body (nothing matches A or B).
+-- NFA reports 3 length-0 matches; first/last_value NULL over empty frame.
 WITH test_728_multi_empty AS (
     SELECT * FROM (VALUES
         (1, ARRAY['C']),
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0001-Add-DEFINE-non-volatile-baseline-to-rpr_integrati.txt (3.2K, 7-nocfbot-0001-Add-DEFINE-non-volatile-baseline-to-rpr_integrati.txt)
  download | inline diff:
From 75ca808a06e22c08442b2fde2bf5ae18987c1ce9 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Mon, 4 May 2026 19:09:34 +0900
Subject: [PATCH 01/68] Add DEFINE non-volatile baseline to rpr_integration B9

B9 in src/test/regress/sql/rpr_integration.sql today only exercises a
volatile callee (random()) inside DEFINE.  Add a baseline query in the
same section that uses STABLE (to_char) and IMMUTABLE (length) callees,
which must remain accepted when the upcoming volatile-only prohibition
lands.  This guards against the prohibition being broadened by accident
(e.g. contain_volatile_functions -> contain_mutable_functions); the
volatile case alone would not catch over-rejection.

Ordered baseline-first then volatile, matching other B sections.
---
 src/test/regress/expected/rpr_integration.out | 26 +++++++++++++++++++
 src/test/regress/sql/rpr_integration.sql      | 13 ++++++++++
 2 files changed, 39 insertions(+)

diff --git a/src/test/regress/expected/rpr_integration.out b/src/test/regress/expected/rpr_integration.out
index 0cc79b75601..ef6a157f45d 100644
--- a/src/test/regress/expected/rpr_integration.out
+++ b/src/test/regress/expected/rpr_integration.out
@@ -1421,6 +1421,32 @@ DROP INDEX rpr_integ_id_idx;
 -- pattern matching non-deterministic.  When the prohibition lands,
 -- this test must be replaced with an error-case test that expects
 -- random() in DEFINE to be rejected.
+-- Baseline: STABLE (to_char) and IMMUTABLE (length) callees are accepted.
+-- This locks the boundary of the volatile-only prohibition.
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_integ
+WINDOW w AS (ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE B AS val > PREV(val)
+                AND length('x') = 1
+                AND to_char(date '2026-01-01', 'YYYY') = '2026')
+ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   2
+  2 |  20 |   0
+  3 |  15 |   2
+  4 |  25 |   0
+  5 |   5 |   3
+  6 |  30 |   0
+  7 |  35 |   0
+  8 |  20 |   3
+  9 |  40 |   0
+ 10 |  45 |   0
+(10 rows)
+
+-- Volatile (random) is the prohibition target; today still accepted.
 SELECT id, val, count(*) OVER w AS cnt
 FROM rpr_integ
 WINDOW w AS (ORDER BY id
diff --git a/src/test/regress/sql/rpr_integration.sql b/src/test/regress/sql/rpr_integration.sql
index 6d47728e911..d9748979d54 100644
--- a/src/test/regress/sql/rpr_integration.sql
+++ b/src/test/regress/sql/rpr_integration.sql
@@ -884,6 +884,19 @@ DROP INDEX rpr_integ_id_idx;
 -- this test must be replaced with an error-case test that expects
 -- random() in DEFINE to be rejected.
 
+-- Baseline: STABLE (to_char) and IMMUTABLE (length) callees are accepted.
+-- This locks the boundary of the volatile-only prohibition.
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_integ
+WINDOW w AS (ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE B AS val > PREV(val)
+                AND length('x') = 1
+                AND to_char(date '2026-01-01', 'YYYY') = '2026')
+ORDER BY id;
+
+-- Volatile (random) is the prohibition target; today still accepted.
 SELECT id, val, count(*) OVER w AS cnt
 FROM rpr_integ
 WINDOW w AS (ORDER BY id
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0007-Remove-optional-outer-parentheses-from-ereport-ca.txt (18.8K, 8-nocfbot-0007-Remove-optional-outer-parentheses-from-ereport-ca.txt)
  download | inline diff:
From 0532539a2509adba38989fc3bf5b1a1281f251ab Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Tue, 5 May 2026 17:27:51 +0900
Subject: [PATCH 07/68] Remove optional outer parentheses from ereport() calls
 in RPR files

Per project coding style, the outer parentheses wrapping errcode/
errmsg/etc. arguments in ereport() are optional and should be omitted.
Applies to parse_rpr.c, optimizer/plan/rpr.c, and gram.y.
---
 src/backend/optimizer/plan/rpr.c |  16 ++--
 src/backend/parser/gram.y        |  72 ++++++++--------
 src/backend/parser/parse_rpr.c   | 140 +++++++++++++++----------------
 3 files changed, 114 insertions(+), 114 deletions(-)

diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index a817eb4a63f..ed8b6c3414c 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -1027,10 +1027,10 @@ scanRPRPatternRecursive(RPRPatternNode *node, char **varNames, int *numVars,
 	/* Check recursion depth limit before overflow occurs */
 	if (depth >= RPR_DEPTH_MAX)
 		ereport(ERROR,
-				(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
-				 errmsg("pattern nesting too deep"),
-				 errdetail("Pattern nesting depth %d exceeds maximum %d.",
-						   depth, RPR_DEPTH_MAX - 1)));
+				errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+				errmsg("pattern nesting too deep"),
+				errdetail("Pattern nesting depth %d exceeds maximum %d.",
+						  depth, RPR_DEPTH_MAX - 1));
 
 	/* Track maximum depth */
 	if (depth > *maxDepth)
@@ -1120,10 +1120,10 @@ scanRPRPattern(RPRPatternNode *node, char **varNames, int *numVars,
 
 	if (*numElements > RPR_ELEMIDX_MAX)
 		ereport(ERROR,
-				(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
-				 errmsg("pattern too complex"),
-				 errdetail("Pattern has %d elements, maximum is %d.",
-						   *numElements, RPR_ELEMIDX_MAX)));
+				errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+				errmsg("pattern too complex"),
+				errdetail("Pattern has %d elements, maximum is %d.",
+						  *numElements, RPR_ELEMIDX_MAX));
 }
 
 /*
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index f3cedfbbb18..aa587e6aced 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -17610,10 +17610,10 @@ opt_row_pattern_initial_or_seek:
 			| SEEK
 				{
 					ereport(ERROR,
-							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-							 errmsg("SEEK is not supported"),
-							 errhint("Use INITIAL instead."),
-							 parser_errposition(@1)));
+							errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+							errmsg("SEEK is not supported"),
+							errhint("Use INITIAL instead."),
+							parser_errposition(@1));
 				}
 			| /*EMPTY*/		{ $$ = true; }
 		;
@@ -17740,40 +17740,40 @@ row_pattern_quantifier_opt:
 						$$ = (Node *) makeRPRQuantifier(0, 1, @1 + 1, @1, yyscanner);
 					else
 						ereport(ERROR,
-								(errcode(ERRCODE_SYNTAX_ERROR),
-								 errmsg("unsupported quantifier \"%s\"", $1),
-								 errhint("Valid quantifiers are: *, +, ?, *?, +?, ??, {n}, {n,}, {,m}, {n,m} and their reluctant versions."),
-								 parser_errposition(@1)));
+								errcode(ERRCODE_SYNTAX_ERROR),
+								errmsg("unsupported quantifier \"%s\"", $1),
+								errhint("Valid quantifiers are: *, +, ?, *?, +?, ??, {n}, {n,}, {,m}, {n,m} and their reluctant versions."),
+								parser_errposition(@1));
 				}
 			/* RELUCTANT quantifiers (when lexer separates tokens) */
 			| '*' Op
 				{
 					if (strcmp($2, "?") != 0)
 						ereport(ERROR,
-								(errcode(ERRCODE_SYNTAX_ERROR),
-								 errmsg("invalid token after \"*\" quantifier"),
-								 errhint("Did you mean \"*?\" for reluctant quantifier?"),
-								 parser_errposition(@2)));
+								errcode(ERRCODE_SYNTAX_ERROR),
+								errmsg("invalid token after \"*\" quantifier"),
+								errhint("Did you mean \"*?\" for reluctant quantifier?"),
+								parser_errposition(@2));
 					$$ = (Node *) makeRPRQuantifier(0, INT_MAX, @2, @1, yyscanner);
 				}
 			| '+' Op
 				{
 					if (strcmp($2, "?") != 0)
 						ereport(ERROR,
-								(errcode(ERRCODE_SYNTAX_ERROR),
-								 errmsg("invalid token after \"+\" quantifier"),
-								 errhint("Did you mean \"+?\" for reluctant quantifier?"),
-								 parser_errposition(@2)));
+								errcode(ERRCODE_SYNTAX_ERROR),
+								errmsg("invalid token after \"+\" quantifier"),
+								errhint("Did you mean \"+?\" for reluctant quantifier?"),
+								parser_errposition(@2));
 					$$ = (Node *) makeRPRQuantifier(1, INT_MAX, @2, @1, yyscanner);
 				}
 			| Op Op
 				{
 					if (strcmp($1, "?") != 0 || strcmp($2, "?") != 0)
 						ereport(ERROR,
-								(errcode(ERRCODE_SYNTAX_ERROR),
-								 errmsg("invalid quantifier combination"),
-								 errhint("Did you mean \"??\" for reluctant quantifier?"),
-								 parser_errposition(@1)));
+								errcode(ERRCODE_SYNTAX_ERROR),
+								errmsg("invalid quantifier combination"),
+								errhint("Did you mean \"??\" for reluctant quantifier?"),
+								parser_errposition(@1));
 					$$ = (Node *) makeRPRQuantifier(0, 1, @2, @1, yyscanner);
 				}
 			/* {n}, {n,}, {,m}, {n,m} quantifiers */
@@ -17823,10 +17823,10 @@ row_pattern_quantifier_opt:
 				{
 					if (strcmp($4, "?") != 0)
 						ereport(ERROR,
-								(errcode(ERRCODE_SYNTAX_ERROR),
-								 errmsg("invalid token after range quantifier"),
-								 errhint("Only \"?\" is allowed after {n} to make it reluctant."),
-								 parser_errposition(@4)));
+								errcode(ERRCODE_SYNTAX_ERROR),
+								errmsg("invalid token after range quantifier"),
+								errhint("Only \"?\" is allowed after {n} to make it reluctant."),
+								parser_errposition(@4));
 					if ($2 <= 0 || $2 >= INT_MAX)
 						ereport(ERROR,
 								errcode(ERRCODE_SYNTAX_ERROR),
@@ -17838,10 +17838,10 @@ row_pattern_quantifier_opt:
 				{
 					if (strcmp($5, "?") != 0)
 						ereport(ERROR,
-								(errcode(ERRCODE_SYNTAX_ERROR),
-								 errmsg("invalid token after range quantifier"),
-								 errhint("Only \"?\" is allowed after {n,} or {,m} to make it reluctant."),
-								 parser_errposition(@5)));
+								errcode(ERRCODE_SYNTAX_ERROR),
+								errmsg("invalid token after range quantifier"),
+								errhint("Only \"?\" is allowed after {n,} or {,m} to make it reluctant."),
+								parser_errposition(@5));
 					if ($2 < 0 || $2 >= INT_MAX)
 						ereport(ERROR,
 								errcode(ERRCODE_SYNTAX_ERROR),
@@ -17853,10 +17853,10 @@ row_pattern_quantifier_opt:
 				{
 					if (strcmp($5, "?") != 0)
 						ereport(ERROR,
-								(errcode(ERRCODE_SYNTAX_ERROR),
-								 errmsg("invalid token after range quantifier"),
-								 errhint("Only \"?\" is allowed after {n,} or {,m} to make it reluctant."),
-								 parser_errposition(@5)));
+								errcode(ERRCODE_SYNTAX_ERROR),
+								errmsg("invalid token after range quantifier"),
+								errhint("Only \"?\" is allowed after {n,} or {,m} to make it reluctant."),
+								parser_errposition(@5));
 					if ($3 <= 0 || $3 >= INT_MAX)
 						ereport(ERROR,
 								errcode(ERRCODE_SYNTAX_ERROR),
@@ -17868,10 +17868,10 @@ row_pattern_quantifier_opt:
 				{
 					if (strcmp($6, "?") != 0)
 						ereport(ERROR,
-								(errcode(ERRCODE_SYNTAX_ERROR),
-								 errmsg("invalid token after range quantifier"),
-								 errhint("Only \"?\" is allowed after {n,m} to make it reluctant."),
-								 parser_errposition(@6)));
+								errcode(ERRCODE_SYNTAX_ERROR),
+								errmsg("invalid token after range quantifier"),
+								errhint("Only \"?\" is allowed after {n,m} to make it reluctant."),
+								parser_errposition(@6));
 					if ($2 < 0 || $4 <= 0 || $2 >= INT_MAX || $4 >= INT_MAX)
 						ereport(ERROR,
 								errcode(ERRCODE_SYNTAX_ERROR),
diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index 2c6fccebd47..bba887f17ce 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -95,20 +95,20 @@ transformRPR(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 	/* Frame type must be "ROW" */
 	if (wc->frameOptions & FRAMEOPTION_GROUPS)
 		ereport(ERROR,
-				(errcode(ERRCODE_WINDOWING_ERROR),
-				 errmsg("cannot use FRAME option GROUPS with row pattern recognition"),
-				 errhint("Use ROWS instead."),
-				 parser_errposition(pstate,
-									windef->frameLocation >= 0 ?
-									windef->frameLocation : windef->location)));
+				errcode(ERRCODE_WINDOWING_ERROR),
+				errmsg("cannot use FRAME option GROUPS with row pattern recognition"),
+				errhint("Use ROWS instead."),
+				parser_errposition(pstate,
+								   windef->frameLocation >= 0 ?
+								   windef->frameLocation : windef->location));
 	if (wc->frameOptions & FRAMEOPTION_RANGE)
 		ereport(ERROR,
-				(errcode(ERRCODE_WINDOWING_ERROR),
-				 errmsg("cannot use FRAME option RANGE with row pattern recognition"),
-				 errhint("Use ROWS instead."),
-				 parser_errposition(pstate,
-									windef->frameLocation >= 0 ?
-									windef->frameLocation : windef->location)));
+				errcode(ERRCODE_WINDOWING_ERROR),
+				errmsg("cannot use FRAME option RANGE with row pattern recognition"),
+				errhint("Use ROWS instead."),
+				parser_errposition(pstate,
+								   windef->frameLocation >= 0 ?
+								   windef->frameLocation : windef->location));
 
 	/* Frame must start at current row */
 	if ((wc->frameOptions & FRAMEOPTION_START_CURRENT_ROW) == 0)
@@ -130,11 +130,11 @@ transformRPR(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 			   (wc->frameOptions & FRAMEOPTION_START_OFFSET_FOLLOWING));
 
 		ereport(ERROR,
-				(errcode(ERRCODE_WINDOWING_ERROR),
-				 errmsg("FRAME must start at CURRENT ROW when using row pattern recognition"),
-				 errdetail("Current frame starts with %s.", startBound),
-				 errhint("Use: %s BETWEEN CURRENT ROW AND ...", frameType),
-				 parser_errposition(pstate, windef->frameLocation >= 0 ? windef->frameLocation : windef->location)));
+				errcode(ERRCODE_WINDOWING_ERROR),
+				errmsg("FRAME must start at CURRENT ROW when using row pattern recognition"),
+				errdetail("Current frame starts with %s.", startBound),
+				errhint("Use: %s BETWEEN CURRENT ROW AND ...", frameType),
+				parser_errposition(pstate, windef->frameLocation >= 0 ? windef->frameLocation : windef->location));
 	}
 
 	/* EXCLUDE options are not permitted */
@@ -156,11 +156,11 @@ transformRPR(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 			   (wc->frameOptions & FRAMEOPTION_EXCLUDE_TIES));
 
 		ereport(ERROR,
-				(errcode(ERRCODE_WINDOWING_ERROR),
-				 errmsg("cannot use EXCLUDE options with row pattern recognition"),
-				 errdetail("Frame definition includes %s.", excludeType),
-				 errhint("Remove the EXCLUDE clause from the window definition."),
-				 parser_errposition(pstate, windef->excludeLocation >= 0 ? windef->excludeLocation : windef->location)));
+				errcode(ERRCODE_WINDOWING_ERROR),
+				errmsg("cannot use EXCLUDE options with row pattern recognition"),
+				errdetail("Frame definition includes %s.", excludeType),
+				errhint("Remove the EXCLUDE clause from the window definition."),
+				parser_errposition(pstate, windef->excludeLocation >= 0 ? windef->excludeLocation : windef->location));
 	}
 
 	/* Transform AFTER MATCH SKIP TO clause */
@@ -220,11 +220,11 @@ validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
 					/* Check against RPR_VARID_MAX before adding */
 					if (list_length(*varNames) >= RPR_VARID_MAX)
 						ereport(ERROR,
-								(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
-								 errmsg("too many pattern variables"),
-								 errdetail("Maximum is %d.", RPR_VARID_MAX),
-								 parser_errposition(pstate,
-													exprLocation((Node *) node))));
+								errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+								errmsg("too many pattern variables"),
+								errdetail("Maximum is %d.", RPR_VARID_MAX),
+								parser_errposition(pstate,
+												   exprLocation((Node *) node)));
 
 					*varNames = lappend(*varNames, makeString(pstrdup(node->varName)));
 				}
@@ -267,10 +267,10 @@ validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
 			}
 			if (!found)
 				ereport(ERROR,
-						(errcode(ERRCODE_SYNTAX_ERROR),
-						 errmsg("DEFINE variable \"%s\" is not used in PATTERN",
-								rt->name),
-						 parser_errposition(pstate, rt->location)));
+						errcode(ERRCODE_SYNTAX_ERROR),
+						errmsg("DEFINE variable \"%s\" is not used in PATTERN",
+							   rt->name),
+						parser_errposition(pstate, rt->location));
 		}
 	}
 }
@@ -347,10 +347,10 @@ transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 
 			if (!strcmp(n, name))
 				ereport(ERROR,
-						(errcode(ERRCODE_SYNTAX_ERROR),
-						 errmsg("DEFINE variable \"%s\" appears more than once",
-								name),
-						 parser_errposition(pstate, exprLocation((Node *) r))));
+						errcode(ERRCODE_SYNTAX_ERROR),
+						errmsg("DEFINE variable \"%s\" appears more than once",
+							   name),
+						parser_errposition(pstate, exprLocation((Node *) r)));
 		}
 
 		restargets = lappend(restargets, restarget);
@@ -529,14 +529,14 @@ define_walker(Node *node, void *context)
 	 */
 	if (check_functions_in_node(node, nav_volatile_func_checker, NULL))
 		ereport(ERROR,
-				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-				 errmsg("volatile functions are not allowed in DEFINE clause"),
-				 parser_errposition(ctx->pstate, exprLocation(node))));
+				errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				errmsg("volatile functions are not allowed in DEFINE clause"),
+				parser_errposition(ctx->pstate, exprLocation(node)));
 	if (IsA(node, NextValueExpr))
 		ereport(ERROR,
-				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-				 errmsg("sequence operations are not allowed in DEFINE clause"),
-				 parser_errposition(ctx->pstate, exprLocation(node))));
+				errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				errmsg("sequence operations are not allowed in DEFINE clause"),
+				parser_errposition(ctx->pstate, exprLocation(node)));
 
 	/* Var sighting feeds the column-ref rule for the enclosing nav scope. */
 	if (IsA(node, Var) &&
@@ -600,17 +600,17 @@ define_walker(Node *node, void *context)
 					/* Reject triple-or-deeper nesting */
 					if (ctx->nav_count > 1)
 						ereport(ERROR,
-								(errcode(ERRCODE_SYNTAX_ERROR),
-								 errmsg("cannot nest row pattern navigation more than two levels deep"),
-								 errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
-								 parser_errposition(ctx->pstate, nav->location)));
+								errcode(ERRCODE_SYNTAX_ERROR),
+								errmsg("cannot nest row pattern navigation more than two levels deep"),
+								errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
+								parser_errposition(ctx->pstate, nav->location));
 
 					if (!IsA(nav->arg, RPRNavExpr))
 						ereport(ERROR,
-								(errcode(ERRCODE_SYNTAX_ERROR),
-								 errmsg("row pattern navigation operation must be a direct argument of the outer navigation"),
-								 errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
-								 parser_errposition(ctx->pstate, nav->location)));
+								errcode(ERRCODE_SYNTAX_ERROR),
+								errmsg("row pattern navigation operation must be a direct argument of the outer navigation"),
+								errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
+								parser_errposition(ctx->pstate, nav->location));
 
 					inner = (RPRNavExpr *) nav->arg;
 
@@ -630,29 +630,29 @@ define_walker(Node *node, void *context)
 				}
 				else if (!outer_phys && inner_phys)
 					ereport(ERROR,
-							(errcode(ERRCODE_SYNTAX_ERROR),
-							 errmsg("FIRST and LAST cannot contain PREV or NEXT"),
-							 errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
-							 parser_errposition(ctx->pstate, nav->location)));
+							errcode(ERRCODE_SYNTAX_ERROR),
+							errmsg("FIRST and LAST cannot contain PREV or NEXT"),
+							errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
+							parser_errposition(ctx->pstate, nav->location));
 				else if (outer_phys && inner_phys)
 					ereport(ERROR,
-							(errcode(ERRCODE_SYNTAX_ERROR),
-							 errmsg("PREV and NEXT cannot contain PREV or NEXT"),
-							 errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
-							 parser_errposition(ctx->pstate, nav->location)));
+							errcode(ERRCODE_SYNTAX_ERROR),
+							errmsg("PREV and NEXT cannot contain PREV or NEXT"),
+							errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
+							parser_errposition(ctx->pstate, nav->location));
 				else
 					ereport(ERROR,
-							(errcode(ERRCODE_SYNTAX_ERROR),
-							 errmsg("FIRST and LAST cannot contain FIRST or LAST"),
-							 errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
-							 parser_errposition(ctx->pstate, nav->location)));
+							errcode(ERRCODE_SYNTAX_ERROR),
+							errmsg("FIRST and LAST cannot contain FIRST or LAST"),
+							errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
+							parser_errposition(ctx->pstate, nav->location));
 			}
 			else if (!ctx->has_column_ref)
 			{
 				ereport(ERROR,
-						(errcode(ERRCODE_SYNTAX_ERROR),
-						 errmsg("argument of row pattern navigation operation must include at least one column reference"),
-						 parser_errposition(ctx->pstate, nav->location)));
+						errcode(ERRCODE_SYNTAX_ERROR),
+						errmsg("argument of row pattern navigation operation must include at least one column reference"),
+						parser_errposition(ctx->pstate, nav->location));
 			}
 
 			/*
@@ -673,9 +673,9 @@ define_walker(Node *node, void *context)
 				(void) define_walker((Node *) nav->offset_arg, ctx);
 				if (ctx->has_column_ref)
 					ereport(ERROR,
-							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-							 errmsg("row pattern navigation offset must be a run-time constant"),
-							 parser_errposition(ctx->pstate, exprLocation((Node *) nav->offset_arg))));
+							errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+							errmsg("row pattern navigation offset must be a run-time constant"),
+							parser_errposition(ctx->pstate, exprLocation((Node *) nav->offset_arg)));
 			}
 			if (flattened && nav->compound_offset_arg != NULL)
 			{
@@ -683,9 +683,9 @@ define_walker(Node *node, void *context)
 				(void) define_walker((Node *) nav->compound_offset_arg, ctx);
 				if (ctx->has_column_ref)
 					ereport(ERROR,
-							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-							 errmsg("row pattern navigation offset must be a run-time constant"),
-							 parser_errposition(ctx->pstate, exprLocation((Node *) nav->compound_offset_arg))));
+							errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+							errmsg("row pattern navigation offset must be a run-time constant"),
+							parser_errposition(ctx->pstate, exprLocation((Node *) nav->compound_offset_arg)));
 			}
 
 			*ctx = saved;
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0006-Add-trailing-commas-to-RPR-enum-definitions.txt (1.8K, 9-nocfbot-0006-Add-trailing-commas-to-RPR-enum-definitions.txt)
  download | inline diff:
From 921232e90b5b9685b87d2b70504077929428563e Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Tue, 5 May 2026 17:21:56 +0900
Subject: [PATCH 06/68] Add trailing commas to RPR enum definitions

RPRNavKind, RPRNavOffsetKind, RPRPatternNodeType lacked a trailing
comma on the last enumerator, contrary to project coding style.
---
 src/include/nodes/parsenodes.h | 4 ++--
 src/include/nodes/primnodes.h  | 2 +-
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 455db2cec61..adefb1d5bad 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -604,7 +604,7 @@ typedef enum RPRNavOffsetKind
 	RPR_NAV_OFFSET_FIXED,		/* resolved constant; use the offset value */
 	RPR_NAV_OFFSET_NEEDS_EVAL,	/* non-constant offset; evaluate at executor
 								 * init */
-	RPR_NAV_OFFSET_RETAIN_ALL	/* cannot determine; retain all rows (no trim) */
+	RPR_NAV_OFFSET_RETAIN_ALL,	/* cannot determine; retain all rows (no trim) */
 } RPRNavOffsetKind;
 
 /*
@@ -615,7 +615,7 @@ typedef enum RPRPatternNodeType
 	RPR_PATTERN_VAR,			/* variable reference */
 	RPR_PATTERN_SEQ,			/* sequence (concatenation) */
 	RPR_PATTERN_ALT,			/* alternation (|) */
-	RPR_PATTERN_GROUP			/* group (parentheses) */
+	RPR_PATTERN_GROUP,			/* group (parentheses) */
 } RPRPatternNodeType;
 
 /*
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index 656c552b0a8..d7138f5141b 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -684,7 +684,7 @@ typedef enum RPRNavKind
 	RPR_NAV_PREV_FIRST,
 	RPR_NAV_PREV_LAST,
 	RPR_NAV_NEXT_FIRST,
-	RPR_NAV_NEXT_LAST
+	RPR_NAV_NEXT_LAST,
 } RPRNavKind;
 
 typedef struct RPRNavExpr
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0008-Add-high-water-mark-tracking-to-NFA-visited-bitma.txt (5.0K, 10-nocfbot-0008-Add-high-water-mark-tracking-to-NFA-visited-bitma.txt)
  download | inline diff:
From 029c2bf753599736f0469a308d8fe050e4e2b91f Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Mon, 4 May 2026 22:31:06 +0900
Subject: [PATCH 08/68] Add high-water mark tracking to NFA visited bitmap
 reset

Track the min/max bitmapword index touched in nfa_mark_visited so the
per-advance reset memsets only the touched range, not the full
nfaVisitedNWords array.

Each visited mark performs two int16 comparisons.  For single-word
bitmaps this overhead is added with no reset saving; for larger NFAs
the reset walks only the slice the DFS actually touched, which is
where the win comes from.  Applied unconditionally for simplicity.
Semantics unchanged.
---
 src/backend/executor/execRPR.c       | 41 +++++++++++++++++++++++-----
 src/backend/executor/nodeWindowAgg.c |  3 ++
 src/include/nodes/execnodes.h        |  4 +++
 3 files changed, 41 insertions(+), 7 deletions(-)

diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 242ae9c6dcf..1e6196d6960 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -38,6 +38,21 @@
 #define WORDNUM(x)	((x) / BITS_PER_BITMAPWORD)
 #define BITNUM(x)	((x) % BITS_PER_BITMAPWORD)
 
+/*
+ * Set the visited bit for elemIdx and update the high-water marks
+ * (nfaVisitedMin/MaxWord) so that the next reset only has to clear
+ * the touched range instead of the full nfaVisitedNWords array.
+ */
+static inline void
+nfa_mark_visited(WindowAggState *winstate, int16 elemIdx)
+{
+	int16		w = WORDNUM(elemIdx);
+
+	winstate->nfaVisitedElems[w] |= ((bitmapword) 1 << BITNUM(elemIdx));
+	winstate->nfaVisitedMinWord = Min(winstate->nfaVisitedMinWord, w);
+	winstate->nfaVisitedMaxWord = Max(winstate->nfaVisitedMaxWord, w);
+}
+
 /* Forward declarations - NFA state management */
 static RPRNFAState *nfa_state_alloc(WindowAggState *winstate);
 static void nfa_state_free(WindowAggState *winstate, RPRNFAState *state);
@@ -320,8 +335,7 @@ nfa_add_state_unique(WindowAggState *winstate, RPRNFAContext *ctx, RPRNFAState *
 	RPRNFAState *tail = NULL;
 
 	/* Mark VAR in visited before duplicate check to prevent DFS loops */
-	winstate->nfaVisitedElems[WORDNUM(state->elemIdx)] |=
-		((bitmapword) 1 << BITNUM(state->elemIdx));
+	nfa_mark_visited(winstate, state->elemIdx);
 
 	/* Check for duplicate and find tail */
 	for (s = ctx->states; s != NULL; s = s->next)
@@ -1341,8 +1355,7 @@ nfa_advance_state(WindowAggState *winstate, RPRNFAContext *ctx,
 	 * the same VAR in a new iteration.
 	 */
 	if (!RPRElemIsVar(elem))
-		winstate->nfaVisitedElems[WORDNUM(state->elemIdx)] |=
-			((bitmapword) 1 << BITNUM(state->elemIdx));
+		nfa_mark_visited(winstate, state->elemIdx);
 
 	switch (elem->varId)
 	{
@@ -1394,9 +1407,23 @@ nfa_advance(WindowAggState *winstate, RPRNFAContext *ctx, int64 currentPos)
 		CHECK_FOR_INTERRUPTS();
 		savedMatchedState = ctx->matchedState;
 
-		/* Clear visited bitmap before each state's DFS expansion */
-		memset(winstate->nfaVisitedElems, 0,
-			   sizeof(bitmapword) * winstate->nfaVisitedNWords);
+		/*
+		 * Clear visited bitmap before each state's DFS expansion.  Only the
+		 * range touched since the previous reset (tracked via the high-water
+		 * marks updated in nfa_mark_visited) needs to be cleared; for small
+		 * NFAs this is the whole array, but for large NFAs whose DFS only
+		 * reaches a few elements per advance it avoids walking the full
+		 * bitmap.
+		 */
+		if (winstate->nfaVisitedMaxWord >= winstate->nfaVisitedMinWord)
+		{
+			memset(&winstate->nfaVisitedElems[winstate->nfaVisitedMinWord], 0,
+				   sizeof(bitmapword) *
+				   (winstate->nfaVisitedMaxWord -
+					winstate->nfaVisitedMinWord + 1));
+			winstate->nfaVisitedMinWord = INT16_MAX;
+			winstate->nfaVisitedMaxWord = -1;
+		}
 
 		state = states;
 		states = states->next;
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 200d8a49001..188445d779c 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -3055,6 +3055,9 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
 			(node->rpPattern->numElements - 1) / BITS_PER_BITMAPWORD + 1;
 		winstate->nfaVisitedElems = palloc0(sizeof(bitmapword) *
 											winstate->nfaVisitedNWords);
+		/* High-water mark sentinels: no bits set yet. */
+		winstate->nfaVisitedMinWord = INT16_MAX;
+		winstate->nfaVisitedMaxWord = -1;
 	}
 
 	/* Set up row pattern recognition DEFINE clause */
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 0cb01baa949..1fba14b892e 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -2673,6 +2673,10 @@ typedef struct WindowAggState
 									 * detection */
 	int			nfaVisitedNWords;	/* number of bitmapwords in
 									 * nfaVisitedElems */
+	int16		nfaVisitedMinWord;	/* lowest bitmapword index touched since
+									 * last reset (INT16_MAX = none) */
+	int16		nfaVisitedMaxWord;	/* highest bitmapword index touched since
+									 * last reset (-1 = none) */
 	int64		nfaLastProcessedRow;	/* last row processed by NFA (-1 =
 										 * none) */
 
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0009-Document-DEFINE-subquery-rejection-as-intentional.txt (2.6K, 11-nocfbot-0009-Document-DEFINE-subquery-rejection-as-intentional.txt)
  download | inline diff:
From 2aaed5f6126ac08b1c11593568bdcbc5f6f58d91 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Thu, 7 May 2026 21:00:48 +0900
Subject: [PATCH 09/68] Document DEFINE subquery rejection as intentional
 over-rejection

The SubLink switch in transformSubLink() rejects every subquery in
EXPR_KIND_RPR_DEFINE with no inline rationale.  Add an XXX block
recording that SQL/RPR (19075-5 4.18.4 / 6.17.4; R010 /
R020) actually permits a nested subquery in DEFINE provided it
does not itself perform row pattern recognition and does not
reference an outer pattern variable, and that the blanket
rejection here subsumes both restrictions by making the subquery
itself unreachable.

Implementing the case distinction would mean walking the analyzed
subquery Query tree for nested RPR and walking it for ColumnRef
qualifiers matching any ancestor's p_rpr_pattern_vars; both are
doable with the existing infrastructure and are left as future
work, not blocked on any other feature.  Comment-only change.
---
 src/backend/parser/parse_expr.c | 22 ++++++++++++++++++++++
 1 file changed, 22 insertions(+)

diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 4221643d9c8..10edccb8afb 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -1944,6 +1944,28 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
 		case EXPR_KIND_FOR_PORTION:
 			err = _("cannot use subquery in FOR PORTION OF expression");
 			break;
+
+			/*----------
+			 * XXX SQL/RPR (19075-5 4.18.4 / 6.17.4; R010 / R020)
+			 * permits a subquery nested in a DEFINE expression provided
+			 * that:
+			 *   (a) the subquery does not itself perform row pattern
+			 *       recognition, and
+			 *   (b) the subquery does not reference a row pattern variable
+			 *       of the outer query.
+			 *
+			 * We reject all subqueries here for now.  Implementing the
+			 * case distinction would mean walking the analyzed subquery
+			 * Query tree for nested RPR window clauses to enforce (a),
+			 * and walking it for ColumnRef qualifiers matching any
+			 * ancestor's p_rpr_pattern_vars to enforce (b).  Both checks
+			 * are doable with the existing infrastructure -- they are
+			 * left as future work, not blocked on any other feature.
+			 * Until then this blanket rejection is intentional
+			 * over-rejection, not a standard fit; it subsumes both (a)
+			 * and (b) by making the subquery itself unreachable.
+			 *----------
+			 */
 		case EXPR_KIND_RPR_DEFINE:
 			err = _("cannot use subquery in DEFINE expression");
 			break;
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0010-Remove-duplicate-include-in-nodeWindowAgg.c.txt (1.1K, 12-nocfbot-0010-Remove-duplicate-include-in-nodeWindowAgg.c.txt)
  download | inline diff:
From a6df6a0af2b523eeff49909cb9e4215b3a435162 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Thu, 7 May 2026 21:37:51 +0900
Subject: [PATCH 10/68] Remove duplicate #include in nodeWindowAgg.c

#include "common/int.h" was included twice; the first occurrence
was also misplaced between two catalog/* headers, breaking the
alphabetical grouping of system header includes.  Drop the
misplaced first occurrence; the second sits in the correct
alphabetical position between catalog/ and executor/ headers.
---
 src/backend/executor/nodeWindowAgg.c | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 188445d779c..99858d22dad 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -36,7 +36,6 @@
 #include "access/htup_details.h"
 #include "catalog/objectaccess.h"
 #include "catalog/pg_aggregate.h"
-#include "common/int.h"
 #include "catalog/pg_proc.h"
 #include "common/int.h"
 #include "executor/executor.h"
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0011-Normalize-SQL-RPR-standard-references.txt (13.6K, 13-nocfbot-0011-Normalize-SQL-RPR-standard-references.txt)
  download | inline diff:
From 5ac8a338e63200e8ab85317cf9cd095a03596fba Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Sat, 9 May 2026 13:47:49 +0900
Subject: [PATCH 11/68] Normalize SQL/RPR standard references

Make every reference cite ISO/IEC 19075-5 explicitly across RPR code
and regress tests.  Prefix bare "19075-5" / "SQL standard" forms,
pin STR06 to its source (7.2.8), and where a clause is mirrored in
both Chapter 4 (FROM) and Chapter 6 (WINDOW), cite the Chapter 6
subclause first because this implementation targets Feature R020.
Document the citation policy in README.rpr.
---
 src/backend/executor/README.rpr               |  8 +++++++-
 src/backend/executor/execRPR.c                |  5 +++--
 src/backend/optimizer/plan/rpr.c              |  8 ++++----
 src/backend/parser/parse_expr.c               | 11 ++++++-----
 src/backend/parser/parse_rpr.c                |  2 +-
 src/test/regress/expected/rpr_base.out        |  6 +++---
 src/test/regress/expected/rpr_explain.out     |  2 +-
 src/test/regress/expected/rpr_integration.out |  4 ++--
 src/test/regress/expected/rpr_nfa.out         |  4 ++--
 src/test/regress/sql/rpr_base.sql             |  6 +++---
 src/test/regress/sql/rpr_explain.sql          |  2 +-
 src/test/regress/sql/rpr_integration.sql      |  4 ++--
 src/test/regress/sql/rpr_nfa.sql              |  4 ++--
 13 files changed, 37 insertions(+), 29 deletions(-)

diff --git a/src/backend/executor/README.rpr b/src/backend/executor/README.rpr
index 52bcd77390c..e64efe0c7fc 100644
--- a/src/backend/executor/README.rpr
+++ b/src/backend/executor/README.rpr
@@ -38,6 +38,12 @@ What is a Flat-Array Stream NFA?
 Chapter I  Row Pattern Recognition Overview
 ============================================================================
 
+Normative reference: ISO/IEC 19075-5 (SQL Technical Report, Part 5: Row
+pattern recognition in SQL).  Subclause numbers cited throughout this code
+base refer to that document.  Where Chapters 4 (FROM clause) and 6 (WINDOW
+clause) describe parallel material, this implementation cites the Chapter 6
+subclause first because it targets Feature R020.
+
 Row Pattern Recognition (hereafter RPR) is a feature introduced in SQL:2016
 that matches regex-based patterns against ordered row sets.
 
@@ -1033,7 +1039,7 @@ match:
 
 X-3. INITIAL vs SEEK
 
-  Standard definition (section 6.12):
+  Standard definition (ISO/IEC 19075-5 6.12):
   INITIAL: "is used to look for a match whose first row is R."
   SEEK:    "is used to permit a search for the first match anywhere
            from R through the end of the full window frame."
diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 1e6196d6960..e1caa7bb528 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -736,8 +736,9 @@ nfa_absorb_contexts(WindowAggState *winstate)
  * once per row by evaluating all DEFINE expressions.  NULL means no DEFINE
  * clauses exist (only possible during early development/testing).
  *
- * Per SQL:2016 R020, pattern variables not listed in DEFINE are implicitly
- * TRUE -- they match every row.  This is checked via varId >= list_length.
+ * Per ISO/IEC 19075-5 Feature R020, pattern variables not listed in DEFINE
+ * are implicitly TRUE -- they match every row.  This is checked via
+ * varId >= list_length.
  */
 static bool
 nfa_eval_var_match(WindowAggState *winstate, RPRPatternElement *elem,
diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index ed8b6c3414c..c65681463b3 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -1050,10 +1050,10 @@ scanRPRPatternRecursive(RPRPatternNode *node, char **varNames, int *numVars,
 			}
 
 			/*
-			 * Variable not in DEFINE clause - this is valid per SQL standard.
-			 * Such variables are implicitly TRUE. Add to varNames so they get
-			 * a varId >= defineVariableList length, which executor treats as
-			 * TRUE.
+			 * Variable not in DEFINE clause - this is valid per ISO/IEC
+			 * 19075-5 Feature R020.  Such variables are implicitly TRUE. Add
+			 * to varNames so they get a varId >= defineVariableList length,
+			 * which executor treats as TRUE.
 			 */
 			Assert(*numVars < RPR_VARID_MAX);
 			varNames[(*numVars)++] = node->varName;
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 10edccb8afb..78abdc88f86 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -631,11 +631,12 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 	/*----------
 	 * Qualified references in DEFINE need a tri-classification:
 	 *
-	 *	  pattern variable qualifier (e.g. UP.price): valid per 19075-5 4.16
-	 *	  but not yet implemented -- raise FEATURE_NOT_SUPPORTED.
+	 *	  pattern variable qualifier (e.g. UP.price): valid per
+	 *	  ISO/IEC 19075-5 6.15 / 4.16 but not yet implemented --
+	 *	  raise FEATURE_NOT_SUPPORTED.
 	 *
-	 *	  FROM-clause range variable qualifier: prohibited by 19075-5 6.5
-	 *	  -- raise SYNTAX_ERROR.
+	 *	  FROM-clause range variable qualifier: prohibited by
+	 *	  ISO/IEC 19075-5 6.5 -- raise SYNTAX_ERROR.
 	 *
 	 *	  any other qualifier (typo, undefined name): fall through and let
 	 *	  normal column resolution produce a sensible error.
@@ -1946,7 +1947,7 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
 			break;
 
 			/*----------
-			 * XXX SQL/RPR (19075-5 4.18.4 / 6.17.4; R010 / R020)
+			 * XXX SQL/RPR (ISO/IEC 19075-5 6.17.4 / 4.18.4; R020 / R010)
 			 * permits a subquery nested in a DEFINE expression provided
 			 * that:
 			 *   (a) the subquery does not itself perform row pattern
diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index bba887f17ce..d2ed6c14811 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -467,7 +467,7 @@ transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
  *     (RPR's NFA may evaluate the same row's predicate multiple times
  *     during backtracking, so a volatile result would make matching
  *     non-deterministic).
- *   - For each outer RPRNavExpr (per SQL 5.6.4 nesting rules):
+ *   - For each outer RPRNavExpr (per ISO/IEC 19075-5 5.6.4 nesting rules):
  *     * arg must contain at least one column reference
  *     * PREV/NEXT wrapping FIRST/LAST flattens to a compound kind
  *     * Other nestings are rejected (FIRST(PREV()), PREV(PREV()), ...)
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index 86abb96c177..cfd2645bbed 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -1,6 +1,6 @@
 -- ============================================================
 -- RPR Base Tests
--- Tests for Row Pattern Recognition (ISO/IEC 19075-5:2016)
+-- Tests for Row Pattern Recognition (ISO/IEC 19075-5)
 -- ============================================================
 --
 -- Parser Layer:
@@ -3065,7 +3065,7 @@ LINE 6:     DEFINE A AS val > 0
             ^
 -- Expected: Syntax error
 -- Qualified column references (NOT SUPPORTED)
--- Pattern variable qualified name: not supported (valid per SQL standard 4.16, not yet implemented)
+-- Pattern variable qualified name: not supported (valid per ISO/IEC 19075-5 6.15 / 4.16, not yet implemented)
 SELECT COUNT(*) OVER w
 FROM rpr_err
 WINDOW w AS (
@@ -3104,7 +3104,7 @@ ERROR:  DEFINE variable "b" is not used in PATTERN
 LINE 7:     DEFINE A AS val > 0, B AS B.val > 0
                                  ^
 -- Expected: ERROR: pattern variable qualified expression "b.val" is not supported
--- FROM-clause range variable qualified name: not allowed (prohibited by SQL standard 6.5)
+-- FROM-clause range variable qualified name: not allowed (prohibited by ISO/IEC 19075-5 6.5)
 SELECT COUNT(*) OVER w
 FROM rpr_err
 WINDOW w AS (
diff --git a/src/test/regress/expected/rpr_explain.out b/src/test/regress/expected/rpr_explain.out
index c4516d3c756..77079d5e8c9 100644
--- a/src/test/regress/expected/rpr_explain.out
+++ b/src/test/regress/expected/rpr_explain.out
@@ -2185,7 +2185,7 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=3.00 loops=1)
 (9 rows)
 
--- (A?){2,3}: min=2 (SQL:2016 STR06 = STRE STRE) -> 3 length-0 matches
+-- (A?){2,3}: min=2 (ISO/IEC 19075-5 7.2.8 STR06 = STRE STRE) -> 3 length-0 matches
 CREATE VIEW rpr_ev_edge_empty_match_min2 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 3) AS s(v)
diff --git a/src/test/regress/expected/rpr_integration.out b/src/test/regress/expected/rpr_integration.out
index 905bd3538de..7cbeed3347e 100644
--- a/src/test/regress/expected/rpr_integration.out
+++ b/src/test/regress/expected/rpr_integration.out
@@ -1278,8 +1278,8 @@ ORDER BY o.id, r.id;
 -- PostgreSQL restriction, so this is the natural place to exercise
 -- "RPR under Recursive Union").
 --
--- XXX: Whether this case falls under the ISO/IEC 9075-2 4.18.5 /
--- 6.17.5 prohibition is not something I can judge.  If this case
+-- XXX: Whether this case falls under the ISO/IEC 19075-5 6.17.5 /
+-- 4.18.5 prohibition is not something I can judge.  If this case
 -- is not prohibited, the open question is whether a query that
 -- does trigger the prohibition can be constructed at all.
 -- Whether to prohibit this case is left to the community.
diff --git a/src/test/regress/expected/rpr_nfa.out b/src/test/regress/expected/rpr_nfa.out
index 4cff7cfbbd7..fe5bb324df0 100644
--- a/src/test/regress/expected/rpr_nfa.out
+++ b/src/test/regress/expected/rpr_nfa.out
@@ -4083,7 +4083,7 @@ WINDOW w AS (
 
 -- ============================================================
 -- Standard Clause 7: Formal Pattern Matching Rules
--- ISO/IEC 19075-5:2021, Clause 7
+-- ISO/IEC 19075-5, Clause 7
 -- ============================================================
 -- ------------------------------------------------------------
 -- 7.2.2 Alternation: first alternative is preferred
@@ -4453,7 +4453,7 @@ WINDOW w AS (
   3 | {B}   |             |          
 (3 rows)
 
--- (A?){2,3}: min=2, nullable inner.  Per SQL:2016 STR06 = (STRE STRE)
+-- (A?){2,3}: min=2, nullable inner.  Per ISO/IEC 19075-5 7.2.8 STR06 = (STRE STRE)
 -- is valid: two empty iterations satisfy min=2.
 -- NFA reports 3 length-0 matches; first/last_value NULL over empty frame.
 WITH test_728_min2 AS (
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index e8c72706720..fd289d7cf67 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -1,6 +1,6 @@
 -- ============================================================
 -- RPR Base Tests
--- Tests for Row Pattern Recognition (ISO/IEC 19075-5:2016)
+-- Tests for Row Pattern Recognition (ISO/IEC 19075-5)
 -- ============================================================
 --
 -- Parser Layer:
@@ -2083,7 +2083,7 @@ WINDOW w AS (
 
 -- Qualified column references (NOT SUPPORTED)
 
--- Pattern variable qualified name: not supported (valid per SQL standard 4.16, not yet implemented)
+-- Pattern variable qualified name: not supported (valid per ISO/IEC 19075-5 6.15 / 4.16, not yet implemented)
 SELECT COUNT(*) OVER w
 FROM rpr_err
 WINDOW w AS (
@@ -2116,7 +2116,7 @@ WINDOW w AS (
 );
 -- Expected: ERROR: pattern variable qualified expression "b.val" is not supported
 
--- FROM-clause range variable qualified name: not allowed (prohibited by SQL standard 6.5)
+-- FROM-clause range variable qualified name: not allowed (prohibited by ISO/IEC 19075-5 6.5)
 SELECT COUNT(*) OVER w
 FROM rpr_err
 WINDOW w AS (
diff --git a/src/test/regress/sql/rpr_explain.sql b/src/test/regress/sql/rpr_explain.sql
index d339a80a673..a527615849a 100644
--- a/src/test/regress/sql/rpr_explain.sql
+++ b/src/test/regress/sql/rpr_explain.sql
@@ -1228,7 +1228,7 @@ WINDOW w AS (
     DEFINE A AS FALSE
 );');
 
--- (A?){2,3}: min=2 (SQL:2016 STR06 = STRE STRE) -> 3 length-0 matches
+-- (A?){2,3}: min=2 (ISO/IEC 19075-5 7.2.8 STR06 = STRE STRE) -> 3 length-0 matches
 CREATE VIEW rpr_ev_edge_empty_match_min2 AS
 SELECT count(*) OVER w
 FROM generate_series(1, 3) AS s(v)
diff --git a/src/test/regress/sql/rpr_integration.sql b/src/test/regress/sql/rpr_integration.sql
index 29b2db2f7bb..f4267c74645 100644
--- a/src/test/regress/sql/rpr_integration.sql
+++ b/src/test/regress/sql/rpr_integration.sql
@@ -792,8 +792,8 @@ ORDER BY o.id, r.id;
 -- PostgreSQL restriction, so this is the natural place to exercise
 -- "RPR under Recursive Union").
 --
--- XXX: Whether this case falls under the ISO/IEC 9075-2 4.18.5 /
--- 6.17.5 prohibition is not something I can judge.  If this case
+-- XXX: Whether this case falls under the ISO/IEC 19075-5 6.17.5 /
+-- 4.18.5 prohibition is not something I can judge.  If this case
 -- is not prohibited, the open question is whether a query that
 -- does trigger the prohibition can be constructed at all.
 -- Whether to prohibit this case is left to the community.
diff --git a/src/test/regress/sql/rpr_nfa.sql b/src/test/regress/sql/rpr_nfa.sql
index 29ec4a9dacb..7a5b5c41b24 100644
--- a/src/test/regress/sql/rpr_nfa.sql
+++ b/src/test/regress/sql/rpr_nfa.sql
@@ -2976,7 +2976,7 @@ WINDOW w AS (
 
 -- ============================================================
 -- Standard Clause 7: Formal Pattern Matching Rules
--- ISO/IEC 19075-5:2021, Clause 7
+-- ISO/IEC 19075-5, Clause 7
 -- ============================================================
 
 -- ------------------------------------------------------------
@@ -3280,7 +3280,7 @@ WINDOW w AS (
         A AS 'A' = ANY(flags)
 );
 
--- (A?){2,3}: min=2, nullable inner.  Per SQL:2016 STR06 = (STRE STRE)
+-- (A?){2,3}: min=2, nullable inner.  Per ISO/IEC 19075-5 7.2.8 STR06 = (STRE STRE)
 -- is valid: two empty iterations satisfy min=2.
 -- NFA reports 3 length-0 matches; first/last_value NULL over empty frame.
 WITH test_728_min2 AS (
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0012-Add-rpr_integration-B7-cases-for-RPR-in-recursive.txt (8.2K, 14-nocfbot-0012-Add-rpr_integration-B7-cases-for-RPR-in-recursive.txt)
  download | inline diff:
From e79f86bb2dddc28457ae0aeb658581457122cf42 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Sun, 10 May 2026 13:51:45 +0900
Subject: [PATCH 12/68] Add rpr_integration B7 cases for RPR in recursive query

Replace the prior B7 test (which asserted that an RPR window works
in the base leg of a recursive CTE) with two cases the recursive-RPR
prohibition needs to cover: WITH RECURSIVE with RPR in the base leg,
and CREATE RECURSIVE VIEW with an RPR window.  Cite ISO/IEC 19075-5
6.17.5 (R020) and 4.18.5 (R010), and the formal rule in ISO/IEC
9075-2:2016 7.17 Syntax Rule 3)e)f), and drop the deferred XXX
comment that left this case open to community input.

Expected output still matches the current (pre-rejection) behavior;
a follow-up patch adds the rejection in parse_cte.c and flips both
queries to ERROR.
---
 src/test/regress/expected/rpr_integration.out | 71 ++++++-------------
 src/test/regress/sql/rpr_integration.sql      | 47 ++++++------
 2 files changed, 43 insertions(+), 75 deletions(-)

diff --git a/src/test/regress/expected/rpr_integration.out b/src/test/regress/expected/rpr_integration.out
index 7cbeed3347e..0b05a826a27 100644
--- a/src/test/regress/expected/rpr_integration.out
+++ b/src/test/regress/expected/rpr_integration.out
@@ -1269,54 +1269,18 @@ ORDER BY o.id, r.id;
 -- ============================================================
 -- B7. RPR + Recursive CTE
 -- ============================================================
--- Verify that an RPR window can appear inside the non-recursive
--- (base) leg of a recursive CTE.  The plan must show the RPR
--- WindowAgg sitting under the Recursive Union as the base-leg
--- child, with the WorkTable Scan feeding the recursive leg above
--- it.  This confirms that RPR output can seed a recursive CTE
--- (window functions cannot appear in the recursive leg itself, a
--- PostgreSQL restriction, so this is the natural place to exercise
--- "RPR under Recursive Union").
---
--- XXX: Whether this case falls under the ISO/IEC 19075-5 6.17.5 /
--- 4.18.5 prohibition is not something I can judge.  If this case
--- is not prohibited, the open question is whether a query that
--- does trigger the prohibition can be constructed at all.
--- Whether to prohibit this case is left to the community.
--- Plan: Recursive Union with the RPR WindowAgg on the base leg and
--- the WorkTable Scan on the recursive leg.
-EXPLAIN (COSTS OFF)
-WITH RECURSIVE seq AS (
-    SELECT id, val, count(*) OVER w AS cnt
-    FROM rpr_integ
-    WINDOW w AS (ORDER BY id
-        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-        PATTERN (A B+)
-        DEFINE B AS val > PREV(val))
-    UNION ALL
-    SELECT id + 100, val, cnt FROM seq WHERE id < 3
-)
-SELECT id, val, cnt FROM seq ORDER BY id;
-                                              QUERY PLAN                                               
--------------------------------------------------------------------------------------------------------
- Sort
-   Sort Key: seq.id
-   CTE seq
-     ->  Recursive Union
-           ->  WindowAgg
-                 Window: w AS (ORDER BY rpr_integ.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-                 Pattern: a b+
-                 Nav Mark Lookback: 1
-                 ->  Sort
-                       Sort Key: rpr_integ.id
-                       ->  Seq Scan on rpr_integ
-           ->  WorkTable Scan on seq seq_1
-                 Filter: (id < 3)
-   ->  CTE Scan on seq
-(14 rows)
-
--- Result: the base leg contributes the RPR match counts; the
--- recursive leg propagates those counts with shifted ids.
+-- Verify that RPR is rejected inside a recursive query.
+-- ISO/IEC 19075-5 6.17.5 (R020) and 4.18.5 (R010) cite CREATE
+-- RECURSIVE VIEW examples and state that "row pattern matching
+-- is prohibited in recursive queries".  The formal rule lives in
+-- ISO/IEC 9075-2:2016 7.17 Syntax Rule 3)f): a potentially
+-- recursive <with list element> shall not contain a <row pattern
+-- measures> or <row pattern common syntax>.  Per 3)e), every
+-- <with list element> under WITH RECURSIVE is "potentially
+-- recursive", so the rejection covers the base (non-recursive)
+-- leg too, not just the self-referencing leg.
+-- WITH RECURSIVE: RPR in the base leg is rejected even though the
+-- base leg never references the recursive CTE name.
 WITH RECURSIVE seq AS (
     SELECT id, val, count(*) OVER w AS cnt
     FROM rpr_integ
@@ -1344,6 +1308,17 @@ SELECT id, val, cnt FROM seq ORDER BY id;
  102 |  20 |   0
 (12 rows)
 
+-- CREATE RECURSIVE VIEW: rewritten by makeRecursiveViewSelect()
+-- into WITH RECURSIVE, so the same rejection applies.  This is
+-- the form ISO/IEC 19075-5 6.17.5 cites verbatim.
+CREATE RECURSIVE VIEW rpr_recv(id, val, cnt) AS
+    SELECT id, val, count(*) OVER w
+    FROM rpr_integ
+    WINDOW w AS (ORDER BY id
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A B+)
+        DEFINE B AS val > PREV(val));
+DROP VIEW rpr_recv;
 -- ============================================================
 -- B8. RPR + Incremental sort
 -- ============================================================
diff --git a/src/test/regress/sql/rpr_integration.sql b/src/test/regress/sql/rpr_integration.sql
index f4267c74645..bc8f4712bcb 100644
--- a/src/test/regress/sql/rpr_integration.sql
+++ b/src/test/regress/sql/rpr_integration.sql
@@ -783,24 +783,19 @@ ORDER BY o.id, r.id;
 -- ============================================================
 -- B7. RPR + Recursive CTE
 -- ============================================================
--- Verify that an RPR window can appear inside the non-recursive
--- (base) leg of a recursive CTE.  The plan must show the RPR
--- WindowAgg sitting under the Recursive Union as the base-leg
--- child, with the WorkTable Scan feeding the recursive leg above
--- it.  This confirms that RPR output can seed a recursive CTE
--- (window functions cannot appear in the recursive leg itself, a
--- PostgreSQL restriction, so this is the natural place to exercise
--- "RPR under Recursive Union").
---
--- XXX: Whether this case falls under the ISO/IEC 19075-5 6.17.5 /
--- 4.18.5 prohibition is not something I can judge.  If this case
--- is not prohibited, the open question is whether a query that
--- does trigger the prohibition can be constructed at all.
--- Whether to prohibit this case is left to the community.
-
--- Plan: Recursive Union with the RPR WindowAgg on the base leg and
--- the WorkTable Scan on the recursive leg.
-EXPLAIN (COSTS OFF)
+-- Verify that RPR is rejected inside a recursive query.
+-- ISO/IEC 19075-5 6.17.5 (R020) and 4.18.5 (R010) cite CREATE
+-- RECURSIVE VIEW examples and state that "row pattern matching
+-- is prohibited in recursive queries".  The formal rule lives in
+-- ISO/IEC 9075-2:2016 7.17 Syntax Rule 3)f): a potentially
+-- recursive <with list element> shall not contain a <row pattern
+-- measures> or <row pattern common syntax>.  Per 3)e), every
+-- <with list element> under WITH RECURSIVE is "potentially
+-- recursive", so the rejection covers the base (non-recursive)
+-- leg too, not just the self-referencing leg.
+
+-- WITH RECURSIVE: RPR in the base leg is rejected even though the
+-- base leg never references the recursive CTE name.
 WITH RECURSIVE seq AS (
     SELECT id, val, count(*) OVER w AS cnt
     FROM rpr_integ
@@ -813,19 +808,17 @@ WITH RECURSIVE seq AS (
 )
 SELECT id, val, cnt FROM seq ORDER BY id;
 
--- Result: the base leg contributes the RPR match counts; the
--- recursive leg propagates those counts with shifted ids.
-WITH RECURSIVE seq AS (
-    SELECT id, val, count(*) OVER w AS cnt
+-- CREATE RECURSIVE VIEW: rewritten by makeRecursiveViewSelect()
+-- into WITH RECURSIVE, so the same rejection applies.  This is
+-- the form ISO/IEC 19075-5 6.17.5 cites verbatim.
+CREATE RECURSIVE VIEW rpr_recv(id, val, cnt) AS
+    SELECT id, val, count(*) OVER w
     FROM rpr_integ
     WINDOW w AS (ORDER BY id
         ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
         PATTERN (A B+)
-        DEFINE B AS val > PREV(val))
-    UNION ALL
-    SELECT id + 100, val, cnt FROM seq WHERE id < 3
-)
-SELECT id, val, cnt FROM seq ORDER BY id;
+        DEFINE B AS val > PREV(val));
+DROP VIEW rpr_recv;
 
 -- ============================================================
 -- B8. RPR + Incremental sort
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0014-Enhance-README.rpr-per-Tatsuo-Ishii-s-review.txt (5.7K, 15-nocfbot-0014-Enhance-README.rpr-per-Tatsuo-Ishii-s-review.txt)
  download | inline diff:
From 0de2f3fbe345717b9f91aad79e9ace2398e75c3c Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Tue, 12 May 2026 15:38:03 +0900
Subject: [PATCH 14/68] Enhance README.rpr per Tatsuo Ishii's review

Apply Tatsuo Ishii's enhancement patch on top of v47:

- Make "target audience" and "scope" more descriptive,
  pointing readers to the SQL standard (and Oracle/Trino
  manuals as alternatives)
- Spell out NFA and AST on first use
- Cross-reference the absorbability sections from the
  RPR_ELEM_ABSORBABLE_BRANCH flag description
- List additional WindowAggState fields in V-3
  (nfaVisitedNWords, defineMatchStartDependent,
  nfaLastProcessedRow)
- State the window framing rules that apply with RPR
- Add a References section (SQL standards)
---
 src/backend/executor/README.rpr | 49 ++++++++++++++++++++++++---------
 1 file changed, 36 insertions(+), 13 deletions(-)

diff --git a/src/backend/executor/README.rpr b/src/backend/executor/README.rpr
index e64efe0c7fc..6c2bddab455 100644
--- a/src/backend/executor/README.rpr
+++ b/src/backend/executor/README.rpr
@@ -2,11 +2,15 @@
   PostgreSQL Row Pattern Recognition: Flat-Array Stream NFA Guide
 ============================================================================
 
-  Target audience: Developers with a basic understanding of the PostgreSQL
-                   executor and planner architecture
+  This README's target audience is developers with a basic
+  understanding of the PostgreSQL executor and planner architecture.
+  Also it would be better for them to understand the specification of
+  the row pattern recognition in the SQL standard [1][2]. If you do
+  not have access to the SQL standard, Oracle's manual or Trino's
+  manual can be alternatives for them.
 
-  Scope: The entire process from PATTERN/DEFINE clause parsing to NFA
-         runtime execution
+  This README's scope is the entire process from PATTERN/DEFINE clause
+  parsing to NFA runtime execution.
 
   Related code:
     - src/backend/parser/parse_rpr.c          (parser phase)
@@ -23,10 +27,11 @@
 
 What is a Flat-Array Stream NFA?
 
-  The NFA in this implementation is not a traditional state-transition graph
-  but a flat array of fixed-size 16-byte elements. At runtime, it processes
-  the row stream in a forward-only manner, expanding epsilon transitions
-  eagerly without backtracking.
+  The NFA (Nondeterministic Finite Automaton) in this implementation
+  is not a traditional state-transition graph but a flat array of
+  fixed-size 16-byte elements. At runtime, it processes the row stream
+  in a forward-only manner, expanding epsilon transitions eagerly
+  without backtracking.
 
   - Flat-Array: Pattern compiled into a flat array,
                 not a graph (Chapter IV)
@@ -132,14 +137,14 @@ following:
 
   (3) DEFINE clause transformation (transformDefineClause)
 
-III-2. PATTERN AST
+III-2. PATTERN AST (Abstract Syntax Tree)
 
 The parser transforms the PATTERN clause into an RPRPatternNode tree.
 Each node has one of the following four types:
 
   RPR_PATTERN_VAR    Variable reference. Name stored in varName field.
   RPR_PATTERN_SEQ    Concatenation. Children node list in children.
-  RPR_PATTERN_ALT    Alternation. Branch node list in children.
+  RPR_PATTERN_ALT    Alternation (or). Branch node list in children.
   RPR_PATTERN_GROUP  Group (parentheses). Body node list in children.
 
 All nodes have min/max fields to express quantifiers:
@@ -270,9 +275,11 @@ Element flags (1 byte, bitmask):
         matches. (IV-4b)
 
   0x04  RPR_ELEM_ABSORBABLE_BRANCH  (VAR, BEGIN, END, ALT)
-        Element lies within an absorbable region.  Used at runtime
-        to track whether the current NFA state is in an absorbable
-        context.
+        Element lies within an absorbable region.  Used at runtime to
+        track whether the current NFA state is in an absorbable
+        context. See "IV-5. Absorbability Analysis" and
+        "VIII-2. Solution: Context Absorption" for more details about
+        absorption.
 
   0x08  RPR_ELEM_ABSORBABLE         (VAR, END)
         Absorption judgment point.  Where to compare consecutive
@@ -514,7 +521,10 @@ V-3. RPR Fields of WindowAggState
   nfaStateFree                  Reuse pool for states
   nfaVarMatched                 Per-row cache: varMatched[varId]
   nfaVisitedElems               Bitmap for cycle detection
+  nfaVisitedNWords              Number of bitmapwords in nfaVisitedElems
   nfaStateSize                  Precomputed size of RPRNFAState
+  defineMatchStartDependent     DEFINE vars needing per-context evaluation (match_start-dependent)
+  nfaLastProcessedRow           Last row processed by NFA (-1 = none)
 
 Memory management:
 
@@ -1053,6 +1063,10 @@ X-3. INITIAL vs SEEK
 
 X-4. Bounded Frame Handling
 
+  With RPR, the frame mode is always ROWS and the frame start must be
+  CURRENT ROW. The frame end can be either UNBOUNDED FOLLOWING or n
+  FOLLOWING.
+
   When the frame is bounded (e.g., ROWS BETWEEN CURRENT ROW AND 5
   FOLLOWING), ExecRPRProcessRow receives hasLimitedFrame=true and
   frameOffset indicating the upper bound.  Before the match phase,
@@ -1579,6 +1593,15 @@ C-7. PATTERN ((A+ B | C*)+ D)  -- Per-branch absorption in ALT
   nullable.
   BEGIN and ALT get ABSORBABLE_BRANCH (on the path to absorbable elements).
 
+
+References:
+
+[1] ISO/IEC 19075-5 Information technology - Guidance for the use of
+    database language SQL - Part 5: Row pattern recognition
+
+[2] ISO/IEC 9075-2 Information technology - Database languages - SQL -
+    Part 2: Foundation (SQL/Foundation)
+
 ============================================================================
   End of document
 ============================================================================
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0015-Round-out-README.rpr-WindowAggState-field-coverag.txt (2.5K, 16-nocfbot-0015-Round-out-README.rpr-WindowAggState-field-coverag.txt)
  download | inline diff:
From e989f94078b49439052bcaeda38afbd74a2fc057 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Tue, 12 May 2026 15:43:49 +0900
Subject: [PATCH 15/68] Round out README.rpr WindowAggState field coverage

Follow-up to the previous commit applying Tatsuo Ishii's
review.  That commit added three WindowAggState fields to
V-3 but left a few related entries out, and Appendix B's
diagram still showed the pre-review field list.

- Add nfaVisitedMinWord and nfaVisitedMaxWord to V-3
- Note that EXPLAIN ANALYZE instrumentation counters are
  omitted from V-3 (see execnodes.h)
- Mirror the V-3 additions in the Appendix B diagram
---
 src/backend/executor/README.rpr | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git a/src/backend/executor/README.rpr b/src/backend/executor/README.rpr
index 6c2bddab455..6ff7f33e62e 100644
--- a/src/backend/executor/README.rpr
+++ b/src/backend/executor/README.rpr
@@ -522,10 +522,15 @@ V-3. RPR Fields of WindowAggState
   nfaVarMatched                 Per-row cache: varMatched[varId]
   nfaVisitedElems               Bitmap for cycle detection
   nfaVisitedNWords              Number of bitmapwords in nfaVisitedElems
+  nfaVisitedMinWord             Lowest bitmapword index touched since last reset
+  nfaVisitedMaxWord             Highest bitmapword index touched since last reset
   nfaStateSize                  Precomputed size of RPRNFAState
   defineMatchStartDependent     DEFINE vars needing per-context evaluation (match_start-dependent)
   nfaLastProcessedRow           Last row processed by NFA (-1 = none)
 
+  EXPLAIN ANALYZE instrumentation counters are omitted here; see
+  execnodes.h for the full list.
+
 Memory management:
 
   States and contexts are managed through their own free lists.
@@ -1480,7 +1485,13 @@ Appendix B. Data Structure Relationship Diagram
     |--- defineVariableList: List<String> (variable names, DEFINE order)
     |--- defineClauseList: List<ExprState>
     |--- nfaVarMatched: bool[] (per-row cache)
+    |--- defineMatchStartDependent: Bitmapset* (match_start-dependent
+    |        DEFINE vars; see VI-4)
     |--- nfaVisitedElems: bitmapword* (cycle detection)
+    |--- nfaVisitedNWords: int (size of nfaVisitedElems)
+    |--- nfaVisitedMinWord / nfaVisitedMaxWord: int16
+    |        (touched-word range for fast reset)
+    |--- nfaLastProcessedRow: int64 (-1 = none)
     |--- nfaStateSize: Size (pre-calculated RPRNFAState allocation size)
     |--- nfaContext <-> nfaContextTail (doubly-linked list)
     |   +--- RPRNFAContext
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0013-Reject-row-pattern-recognition-in-recursive-queri.txt (7.0K, 17-nocfbot-0013-Reject-row-pattern-recognition-in-recursive-queri.txt)
  download | inline diff:
From 476a8f0a9aa2a5fca573ff608d16d3a4d35b360e Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Sun, 10 May 2026 14:42:09 +0900
Subject: [PATCH 13/68] Reject row pattern recognition in recursive queries

Per ISO/IEC 9075-2:2016 7.17 Syntax Rule 3)e)f), every <with list
element> in a WITH RECURSIVE clause is "potentially recursive" and
shall not contain a <row pattern common syntax>.  ISO/IEC 19075-5
6.17.5 (R020) and 4.18.5 (R010) restate the prohibition for CREATE
RECURSIVE VIEW, which makeRecursiveViewSelect() rewrites to WITH
RECURSIVE so the same path catches both forms.

The rejection runs in transformWithClause() against the raw parse
tree, before per-CTE analysis, and reports the PATTERN keyword
position via a new RPCommonSyntax.location field captured in
gram.y.  Flips both rpr_integration B7 cases (added in the
preceding commit) from result rows to the new error.
---
 src/backend/parser/gram.y                     |  1 +
 src/backend/parser/parse_cte.c                | 57 +++++++++++++++++++
 src/include/nodes/parsenodes.h                |  1 +
 src/test/regress/expected/rpr_integration.out | 23 ++------
 src/test/regress/sql/rpr_integration.sql      |  1 -
 src/tools/pgindent/typedefs.list              |  1 +
 6 files changed, 66 insertions(+), 18 deletions(-)

diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index aa587e6aced..a2fafb717cd 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -17585,6 +17585,7 @@ opt_row_pattern_skip_to opt_row_pattern_initial_or_seek
 				n->initial = $2;
 				n->rpPattern = (RPRPatternNode *) $5;
 				n->rpDefs = $8;
+				n->location = @3;
 				$$ = (Node *) n;
 			}
 			| /*EMPTY*/		{ $$ = NULL; }
diff --git a/src/backend/parser/parse_cte.c b/src/backend/parser/parse_cte.c
index ccde199319a..0974b43d028 100644
--- a/src/backend/parser/parse_cte.c
+++ b/src/backend/parser/parse_cte.c
@@ -96,6 +96,14 @@ static void checkWellFormedRecursion(CteState *cstate);
 static bool checkWellFormedRecursionWalker(Node *node, CteState *cstate);
 static void checkWellFormedSelectStmt(SelectStmt *stmt, CteState *cstate);
 
+/* Recursive-WITH RPR rejection */
+typedef struct
+{
+	ParseLoc	location;		/* location of first RPR window, or -1 */
+} ContainRPRContext;
+
+static bool contain_rpr_walker(Node *node, void *context);
+
 
 /*
  * transformWithClause -
@@ -164,6 +172,29 @@ transformWithClause(ParseState *pstate, WithClause *withClause)
 		CteState	cstate;
 		int			i;
 
+		/*
+		 * Per ISO/IEC 9075-2:2016 7.17 Syntax Rule 3)e)f), every <with list
+		 * element> in a WITH RECURSIVE clause is "potentially recursive" and
+		 * shall not contain a <row pattern common syntax>.  (PostgreSQL does
+		 * not implement <row pattern measures>, so only the common syntax
+		 * needs to be checked.)  ISO/IEC 19075-5 6.17.5 (R020) and 4.18.5
+		 * (R010) restate the prohibition for CREATE RECURSIVE VIEW, which is
+		 * rewritten to WITH RECURSIVE by makeRecursiveViewSelect() and so
+		 * flows through here as well.
+		 */
+		foreach(lc, withClause->ctes)
+		{
+			CommonTableExpr *cte = (CommonTableExpr *) lfirst(lc);
+			ContainRPRContext ctx;
+
+			ctx.location = -1;
+			if (contain_rpr_walker(cte->ctequery, &ctx))
+				ereport(ERROR,
+						errcode(ERRCODE_SYNTAX_ERROR),
+						errmsg("cannot use row pattern recognition in a recursive query"),
+						parser_errposition(pstate, ctx.location));
+		}
+
 		cstate.pstate = pstate;
 		cstate.numitems = list_length(withClause->ctes);
 		cstate.items = (CteItem *) palloc0(cstate.numitems * sizeof(CteItem));
@@ -1268,3 +1299,29 @@ checkWellFormedSelectStmt(SelectStmt *stmt, CteState *cstate)
 		}
 	}
 }
+
+
+/*
+ * contain_rpr_walker
+ *	  Returns true if the raw parse tree contains any <row pattern common
+ *	  syntax> -- i.e., any WindowDef with PATTERN/DEFINE attached.  Used
+ *	  by transformWithClause() to enforce ISO/IEC 9075-2:2016 7.17 SR 3)f)
+ *	  on WITH RECURSIVE elements.
+ */
+static bool
+contain_rpr_walker(Node *node, void *context)
+{
+	if (node == NULL)
+		return false;
+	if (IsA(node, WindowDef))
+	{
+		WindowDef  *wd = (WindowDef *) node;
+
+		if (wd->rpCommonSyntax != NULL)
+		{
+			((ContainRPRContext *) context)->location = wd->rpCommonSyntax->location;
+			return true;
+		}
+	}
+	return raw_expression_tree_walker(node, contain_rpr_walker, context);
+}
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index adefb1d5bad..5200182aa46 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -646,6 +646,7 @@ typedef struct RPCommonSyntax
 	RPRPatternNode *rpPattern;	/* PATTERN clause AST */
 	List	   *rpDefs;			/* row pattern definitions clause (list of
 								 * ResTarget) */
+	ParseLoc	location;		/* PATTERN keyword location, or -1 */
 } RPCommonSyntax;
 
 /*
diff --git a/src/test/regress/expected/rpr_integration.out b/src/test/regress/expected/rpr_integration.out
index 0b05a826a27..b598ef95776 100644
--- a/src/test/regress/expected/rpr_integration.out
+++ b/src/test/regress/expected/rpr_integration.out
@@ -1292,22 +1292,9 @@ WITH RECURSIVE seq AS (
     SELECT id + 100, val, cnt FROM seq WHERE id < 3
 )
 SELECT id, val, cnt FROM seq ORDER BY id;
- id  | val | cnt 
------+-----+-----
-   1 |  10 |   2
-   2 |  20 |   0
-   3 |  15 |   2
-   4 |  25 |   0
-   5 |   5 |   3
-   6 |  30 |   0
-   7 |  35 |   0
-   8 |  20 |   3
-   9 |  40 |   0
-  10 |  45 |   0
- 101 |  10 |   2
- 102 |  20 |   0
-(12 rows)
-
+ERROR:  cannot use row pattern recognition in a recursive query
+LINE 6:         PATTERN (A B+)
+                ^
 -- CREATE RECURSIVE VIEW: rewritten by makeRecursiveViewSelect()
 -- into WITH RECURSIVE, so the same rejection applies.  This is
 -- the form ISO/IEC 19075-5 6.17.5 cites verbatim.
@@ -1318,7 +1305,9 @@ CREATE RECURSIVE VIEW rpr_recv(id, val, cnt) AS
         ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
         PATTERN (A B+)
         DEFINE B AS val > PREV(val));
-DROP VIEW rpr_recv;
+ERROR:  cannot use row pattern recognition in a recursive query
+LINE 6:         PATTERN (A B+)
+                ^
 -- ============================================================
 -- B8. RPR + Incremental sort
 -- ============================================================
diff --git a/src/test/regress/sql/rpr_integration.sql b/src/test/regress/sql/rpr_integration.sql
index bc8f4712bcb..5f3853becba 100644
--- a/src/test/regress/sql/rpr_integration.sql
+++ b/src/test/regress/sql/rpr_integration.sql
@@ -818,7 +818,6 @@ CREATE RECURSIVE VIEW rpr_recv(id, val, cnt) AS
         ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
         PATTERN (A B+)
         DEFINE B AS val > PREV(val));
-DROP VIEW rpr_recv;
 
 -- ============================================================
 -- B8. RPR + Incremental sort
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 1970ca5da14..24cf2eb7860 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -532,6 +532,7 @@ Constraint
 ConstraintCategory
 ConstraintInfo
 ConstraintsSetStmt
+ContainRPRContext
 ControlData
 ControlFileData
 ConvInfo
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0016-Add-raw_expression_tree_walker-coverage-for-RPR-r.txt (1.6K, 18-nocfbot-0016-Add-raw_expression_tree_walker-coverage-for-RPR-r.txt)
  download | inline diff:
From ddf9bbf4d9a03b0377ef06556cf0dbba8f1427bb Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Wed, 27 May 2026 07:02:38 +0900
Subject: [PATCH 16/68] Add raw_expression_tree_walker coverage for RPR raw
 nodes

WindowDef.rpCommonSyntax was not walked, and there were no case
arms for T_RPCommonSyntax or T_RPRPatternNode.  RPR core was
unaffected -- contain_rpr_walker() in parse_cte.c intercepts
WindowDef before delegating -- but debug_raw_expression_coverage_test
silently skipped these subtrees, leaving any future raw-node
omission on the RPR side undetected.
---
 src/backend/nodes/nodeFuncs.c | 20 ++++++++++++++++++++
 1 file changed, 20 insertions(+)

diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index 734bb0554fe..101c03b6ae8 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -4641,6 +4641,8 @@ raw_expression_tree_walker_impl(Node *node,
 					return true;
 				if (WALK(wd->endOffset))
 					return true;
+				if (WALK(wd->rpCommonSyntax))
+					return true;
 			}
 			break;
 		case T_RangeSubselect:
@@ -4896,6 +4898,24 @@ raw_expression_tree_walker_impl(Node *node,
 					return true;
 			}
 			break;
+		case T_RPCommonSyntax:
+			{
+				RPCommonSyntax *rc = (RPCommonSyntax *) node;
+
+				if (WALK(rc->rpPattern))
+					return true;
+				if (WALK(rc->rpDefs))
+					return true;
+			}
+			break;
+		case T_RPRPatternNode:
+			{
+				RPRPatternNode *rp = (RPRPatternNode *) node;
+
+				if (WALK(rp->children))
+					return true;
+			}
+			break;
 		default:
 			elog(ERROR, "unrecognized node type: %d",
 				 (int) nodeTag(node));
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0017-Enhance-README.rpr-per-Jian-He-s-review.txt (3.5K, 19-nocfbot-0017-Enhance-README.rpr-per-Jian-He-s-review.txt)
  download | inline diff:
From e0de323a26a95fb235c053c1a1bbb4d8b809e3f7 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Wed, 27 May 2026 13:34:23 +0900
Subject: [PATCH 17/68] Enhance README.rpr per Jian He's review

- Add an intuition summary at the top of Chapter VIII naming
  what context absorption is and the monotonicity principle
  that makes it safe, so the reader meets the core idea before
  the O(N^2) problem framing.
- Add a worked PATTERN (A+) trace at the end of VIII-2 to make
  the state/count dominance comparison concrete.
- Expand "DFS" to "Depth-First Search (DFS)" at first occurrence.
- Replace the stale "nfa_advance(initialAdvance=true)" reference
  in VI-2 with the current signature.
---
 src/backend/executor/README.rpr | 34 +++++++++++++++++++++++++++++++--
 1 file changed, 32 insertions(+), 2 deletions(-)

diff --git a/src/backend/executor/README.rpr b/src/backend/executor/README.rpr
index 6ff7f33e62e..6d40bd70faa 100644
--- a/src/backend/executor/README.rpr
+++ b/src/backend/executor/README.rpr
@@ -354,7 +354,8 @@ The flag is set on all elements that carry the quantifier:
   Simple VAR (A+?):     RPR_ELEM_RELUCTANT on the VAR element
   Group ((...)+?):      RPR_ELEM_RELUCTANT on BEGIN and END elements
 
-At runtime (nfa_advance), the flag controls DFS exploration order:
+At runtime (nfa_advance), the flag controls Depth-First Search
+(DFS) exploration order:
 
   VAR with quantifier:
     Greedy:    primary path = next (continue), clone = jump (skip)
@@ -582,7 +583,8 @@ Creates a new context and performs the initial advance.
   (2) Set matchStartRow = pos
   (3) Create initial state: elemIdx=0 (first pattern element),
       counts=all zero
-  (4) Call nfa_advance(initialAdvance=true)
+  (4) Call nfa_advance() with currentPos = pos - 1 (no row consumed
+      yet)
 
 The initial advance expands epsilon transitions at the beginning of
 the pattern. For example, the initial advance for PATTERN ((A | B) C):
@@ -737,6 +739,15 @@ Immediate advance for simple VARs:
 Chapter VIII  Phase 2: Absorb (Context Absorption)
 ============================================================================
 
+Absorption is the runtime optimization that collapses contexts which
+have converged on identical future behavior.  Two contexts are
+treated as equivalent when one's bookkeeping (elemIdx and per-depth
+iteration counts) is dominated by another's; the younger one is then
+discarded.  The optimization is safe because pattern matching is
+monotonic -- an earlier context's reachable matches always contain a
+later context's.  This is what reduces the naive O(N^2) state count
+to O(N).
+
 VIII-1. Problem
 
 In the current implementation, a new context is started for each row
@@ -762,6 +773,25 @@ is already contained within Context 1.
 
 Therefore Context 2 can be "absorbed" into Context 1.
 
+Worked example for PATTERN (A+) over 3 rows (each matches A):
+
+  After row 1:
+    Ctx_1 (started row 1): state at A with counts[0] = 1
+
+  After row 2:
+    Ctx_1: state at A with counts[0] = 2
+    Ctx_2 (started row 2): state at A with counts[0] = 1
+    -> Same elemIdx; Ctx_1.count (2) dominates Ctx_2.count (1).
+    -> Ctx_2 absorbed.
+
+  After row 3:
+    Ctx_1: state at A with counts[0] = 3
+    Ctx_3 (started row 3): state at A with counts[0] = 1
+    -> Ctx_1.count (3) dominates Ctx_3.count (1).
+    -> Ctx_3 absorbed.
+
+Total active contexts stays at O(1) instead of growing with N.
+
 VIII-3. Absorption Conditions
 
 Planner-time prerequisites (all must hold for absorption to be enabled):
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0019-Change-nfa_add_state_unique-signature-from-bool-t.txt (2.4K, 20-nocfbot-0019-Change-nfa_add_state_unique-signature-from-bool-t.txt)
  download | inline diff:
From fe90e09297dbb9e16123ffbdd200ca7c4b6b95d4 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Wed, 27 May 2026 14:02:19 +0900
Subject: [PATCH 19/68] Change nfa_add_state_unique signature from bool to void

The return value is leftover from an earlier design.  All four
callers ignore it, and the duplicate-found case is fully handled
inside the function (the new state is freed and nfaStatesMerged is
incremented).  Drop the return value and update the doc comment.
---
 src/backend/executor/execRPR.c | 12 +++++-------
 1 file changed, 5 insertions(+), 7 deletions(-)

diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 261e1209744..88c59cf3276 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -61,7 +61,7 @@ static RPRNFAState *nfa_state_create(WindowAggState *winstate, int16 elemIdx,
 									 int32 *counts, bool sourceAbsorbable);
 static bool nfa_states_equal(WindowAggState *winstate, RPRNFAState *s1,
 							 RPRNFAState *s2);
-static bool nfa_add_state_unique(WindowAggState *winstate, RPRNFAContext *ctx,
+static void nfa_add_state_unique(WindowAggState *winstate, RPRNFAContext *ctx,
 								 RPRNFAState *state);
 static void nfa_add_matched_state(WindowAggState *winstate, RPRNFAContext *ctx,
 								  RPRNFAState *state, int64 matchEndRow);
@@ -335,10 +335,10 @@ nfa_states_equal(WindowAggState *winstate, RPRNFAState *s1, RPRNFAState *s2)
  * nfa_add_state_unique
  *
  * Add a state to ctx->states at the END, only if no duplicate exists.
- * Returns true if state was added, false if duplicate found (state is freed).
- * Earlier states have better lexical order (DFS traversal order), so existing wins.
+ * Earlier states have better lexical order (DFS traversal order), so existing
+ * wins; the new state is freed when a duplicate is found.
  */
-static bool
+static void
 nfa_add_state_unique(WindowAggState *winstate, RPRNFAContext *ctx, RPRNFAState *state)
 {
 	RPRNFAState *s;
@@ -365,7 +365,7 @@ nfa_add_state_unique(WindowAggState *winstate, RPRNFAContext *ctx, RPRNFAState *
 			 */
 			nfa_state_free(winstate, state);
 			winstate->nfaStatesMerged++;
-			return false;
+			return;
 		}
 		tail = s;
 	}
@@ -376,8 +376,6 @@ nfa_add_state_unique(WindowAggState *winstate, RPRNFAContext *ctx, RPRNFAState *
 		ctx->states = state;
 	else
 		tail->next = state;
-
-	return true;
 }
 
 /*
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0020-Add-reluctant-bounded-mid-band-test-to-rpr_nfa.txt (3.9K, 21-nocfbot-0020-Add-reluctant-bounded-mid-band-test-to-rpr_nfa.txt)
  download | inline diff:
From a0a9d1581e728a564bbd481f2854a4a28209a874 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Wed, 27 May 2026 14:15:09 +0900
Subject: [PATCH 20/68] Add reluctant bounded mid-band test to rpr_nfa

PATTERN (A{3,5}? B) drives the VAR-level count in nfa_advance_var
through 3, 4, 5 within a single match attempt -- a band the
existing A{1,3}? B test does not exercise, because A and B match
the same row there and advance's early-termination path frees the
loop state before nfa_advance_var sees count > 2.

This explicitly exercises the count > 2 && reluctant &&
!isAbsorbable path that absorbability analysis structurally
constrains (reluctant quantifiers are excluded, so isAbsorbable
stays false).
---
 src/test/regress/expected/rpr_nfa.out | 38 +++++++++++++++++++++++++++
 src/test/regress/sql/rpr_nfa.sql      | 29 ++++++++++++++++++++
 2 files changed, 67 insertions(+)

diff --git a/src/test/regress/expected/rpr_nfa.out b/src/test/regress/expected/rpr_nfa.out
index fe5bb324df0..1f494d2db34 100644
--- a/src/test/regress/expected/rpr_nfa.out
+++ b/src/test/regress/expected/rpr_nfa.out
@@ -2237,6 +2237,44 @@ WINDOW w AS (
   4 | {B,_} |             |          
 (4 rows)
 
+-- A{3,5}? B (reluctant bounded mid-band): the VAR-level count in
+-- nfa_advance_var cycles through 3, 4, 5 within a single match
+-- attempt.  Exercises the count > 2 && reluctant && !isAbsorbable
+-- branch (absorbability analysis excludes reluctant quantifiers, so
+-- isAbsorbable stays false for A).
+WITH test_reluctant_mid_band AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),
+        (4, ARRAY['A']),
+        (5, ARRAY['A']),
+        (6, ARRAY['B'])
+    ) AS t(id, flags)
+)
+SELECT id, flags,
+       first_value(id) OVER w AS match_start,
+       last_value(id) OVER w AS match_end
+FROM test_reluctant_mid_band
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A{3,5}? B)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {A}   |           1 |         6
+  2 | {A}   |             |          
+  3 | {A}   |             |          
+  4 | {A}   |             |          
+  5 | {A}   |             |          
+  6 | {B}   |             |          
+(6 rows)
+
 -- ============================================================
 -- Pathological Pattern Runtime Protection
 -- ============================================================
diff --git a/src/test/regress/sql/rpr_nfa.sql b/src/test/regress/sql/rpr_nfa.sql
index 7a5b5c41b24..76dfc4d88bc 100644
--- a/src/test/regress/sql/rpr_nfa.sql
+++ b/src/test/regress/sql/rpr_nfa.sql
@@ -1554,6 +1554,35 @@ WINDOW w AS (
         B AS 'B' = ANY(flags)
 );
 
+-- A{3,5}? B (reluctant bounded mid-band): the VAR-level count in
+-- nfa_advance_var cycles through 3, 4, 5 within a single match
+-- attempt.  Exercises the count > 2 && reluctant && !isAbsorbable
+-- branch (absorbability analysis excludes reluctant quantifiers, so
+-- isAbsorbable stays false for A).
+WITH test_reluctant_mid_band AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['A']),
+        (4, ARRAY['A']),
+        (5, ARRAY['A']),
+        (6, ARRAY['B'])
+    ) AS t(id, flags)
+)
+SELECT id, flags,
+       first_value(id) OVER w AS match_start,
+       last_value(id) OVER w AS match_end
+FROM test_reluctant_mid_band
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (A{3,5}? B)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags)
+);
+
 -- ============================================================
 -- Pathological Pattern Runtime Protection
 -- ============================================================
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0018-Clarify-execRPR.c-comments-and-tighten-an-Assert-.txt (7.0K, 22-nocfbot-0018-Clarify-execRPR.c-comments-and-tighten-an-Assert-.txt)
  download | inline diff:
From 1a5893d5d23d3787065192fb63bdeec1719169ba Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Wed, 27 May 2026 13:44:55 +0900
Subject: [PATCH 18/68] Clarify execRPR.c comments and tighten an Assert per
 Jian He's review

- nfa_advance_var: add Assert that elem->next is within bounds, as
  any reachable VAR's next pointer must be a valid index.
- nfa_advance: document the state->next reset point as the boundary
  contract for the epsilon-expansion DFS, naming the other linking
  site (nfa_add_state_unique) as the pair.
- nfa_add_state_unique: back-reference the asymmetric visited-marking
  scheme, whose primary explanation sits in nfa_advance_state.
- nfa_states_equal: rewrite the compareDepth comment to explain both
  the +1 slot arithmetic and why deeper slots are excluded.
- nfa_advance_begin: replace the misleading "Greedy: enter first,
  skip second" label with one that covers both greedy-optional and
  non-nullable cases.
- nfa_advance_alt: explain when the depth break actually fires
  (quantified-group last branch) and why <= is the correct relation.
- ExecRPRFinalizeAllContexts: reframe as the partition-end
  classification policy holder, enumerating the three context shapes
  that survive into Finalize and how each is handled.
---
 src/backend/executor/execRPR.c | 75 ++++++++++++++++++++++++++++++----
 1 file changed, 68 insertions(+), 7 deletions(-)

diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index e1caa7bb528..261e1209744 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -311,9 +311,19 @@ nfa_states_equal(WindowAggState *winstate, RPRNFAState *s1, RPRNFAState *s2)
 	if (s1->elemIdx != s2->elemIdx)
 		return false;
 
-	/* Compare counts up to current element's depth */
+	/*
+	 * Compare counts up to current element's depth.  Two states sharing
+	 * elemIdx are equivalent iff every enclosing-or-current depth count
+	 * matches.
+	 *
+	 * The +1 is the slot arithmetic: comparing through depth N requires
+	 * counts[0..N], i.e., N+1 entries.  Deeper slots (counts[d] with d >
+	 * elem->depth) are excluded because they hold scratch state from inner
+	 * groups that gets zeroed on re-entry (see END loop-back in
+	 * nfa_advance_end), and so must not participate in equivalence judgment.
+	 */
 	elem = &pattern->elements[s1->elemIdx];
-	compareDepth = elem->depth + 1; /* depth 0 needs 1 count, etc. */
+	compareDepth = elem->depth + 1;
 
 	if (memcmp(s1->counts, s2->counts, sizeof(int32) * compareDepth) != 0)
 		return false;
@@ -334,7 +344,12 @@ nfa_add_state_unique(WindowAggState *winstate, RPRNFAContext *ctx, RPRNFAState *
 	RPRNFAState *s;
 	RPRNFAState *tail = NULL;
 
-	/* Mark VAR in visited before duplicate check to prevent DFS loops */
+	/*
+	 * Mark VAR in visited before duplicate check to prevent DFS loops. This
+	 * is the deferred half of the asymmetric visited-marking scheme; see
+	 * nfa_advance_state for the non-VAR (END/ALT/BEGIN/FIN) half and the
+	 * rationale for the asymmetry.
+	 */
 	nfa_mark_visited(winstate, state->elemIdx);
 
 	/* Check for duplicate and find tail */
@@ -950,7 +965,16 @@ nfa_advance_alt(WindowAggState *winstate, RPRNFAContext *ctx,
 		RPRPatternElement *altElem = &elements[altIdx];
 		RPRNFAState *newState;
 
-		/* Stop if element is outside ALT scope (not a branch) */
+		/*
+		 * Stop if element is outside ALT scope (not a branch).  The check
+		 * fires when the last branch is a quantified group whose BEGIN.jump
+		 * (set by fillRPRPatternGroup) is preserved -- not overridden by
+		 * fillRPRPatternAlt, which only links non-last branch heads -- and
+		 * leads to a post-ALT element.  Other branch shapes terminate the
+		 * walk earlier via altIdx = RPR_ELEMIDX_INVALID.  Use <=, not <: the
+		 * post-ALT element may sit at the same depth as the ALT when the ALT
+		 * has a sibling at that level.
+		 */
 		if (altElem->depth <= elem->depth)
 			break;
 
@@ -1016,7 +1040,12 @@ nfa_advance_begin(WindowAggState *winstate, RPRNFAContext *ctx,
 	}
 	else
 	{
-		/* Greedy: enter first, skip second */
+		/*
+		 * Greedy-or-non-nullable: route to the first child.  For optional
+		 * groups (skipState != NULL, greedy min=0) additionally create the
+		 * skip path; for non-nullable groups (skipState == NULL, min>0) the
+		 * skip-path action is suppressed by the guard below.
+		 */
 		state->elemIdx = elem->next;
 		nfa_route_to_elem(winstate, ctx, state,
 						  &elements[state->elemIdx], currentPos);
@@ -1205,6 +1234,9 @@ nfa_advance_var(WindowAggState *winstate, RPRNFAContext *ctx,
 	/* After a successful match, count >= 1, so at least one must be true */
 	Assert(canLoop || canExit);
 
+	/* elem->next must be a valid index for any reachable VAR */
+	Assert(elem->next >= 0 && elem->next < pattern->numElements);
+
 	if (canLoop && canExit)
 	{
 		/*
@@ -1428,6 +1460,15 @@ nfa_advance(WindowAggState *winstate, RPRNFAContext *ctx, int64 currentPos)
 
 		state = states;
 		states = states->next;
+
+		/*
+		 * Boundary contract: state->next is reset to NULL here, before
+		 * crossing into nfa_advance_state's epsilon-expansion DFS.  The inner
+		 * branches (nfa_advance_var, nfa_advance_begin/end/alt) treat
+		 * state->next as already-NULL and don't reset it themselves; the
+		 * other linking site is nfa_add_state_unique, which sets it when
+		 * appending to ctx->states.
+		 */
 		state->next = NULL;
 
 		nfa_advance_state(winstate, ctx, state, currentPos);
@@ -1779,8 +1820,28 @@ ExecRPRCleanupDeadContexts(WindowAggState *winstate, RPRNFAContext *excludeCtx)
 /*
  * ExecRPRFinalizeAllContexts
  *
- * Finalize all active contexts when partition ends.
- * Match with NULL to force mismatch, then advance to process epsilon transitions.
+ * Partition-end classification policy: kill any VAR states still pursuing
+ * when rows run out, so cleanup sees a uniform ctx->states == NULL across
+ * every context.  By the time this runs, all genuine FIN reaches have
+ * already been recorded in-flight; three shapes survive here:
+ *
+ *   - Pure pursuit (matchedState == NULL): VAR states waiting for input
+ *     that never arrives (e.g., A+ B mid-pattern at partition end).
+ *   - Empty-match candidate + pursuit (matchedState != NULL,
+ *     matchEndRow < matchStartRow): initial-advance FIN-via-skip recorded
+ *     an empty match while VAR states are still chasing a longer one
+ *     (e.g., greedy A*).
+ *   - Real match + pursuit (matchedState != NULL,
+ *     matchEndRow >= matchStartRow): a match has been recorded and VAR
+ *     states are still looping for a longer one.
+ *
+ * Killing the VAR reclassifies the first two as failures in cleanup
+ * (otherwise they linger without contributing to stats).  The third is
+ * stat-neutral -- cleanup skips it either way -- but goes through the
+ * same uniform path so partition-end classification stays centralized.
+ *
+ * Implementation: nfa_match with NULL forces VAR mismatch; nfa_advance
+ * then drains any remaining epsilon transitions.
  */
 void
 ExecRPRFinalizeAllContexts(WindowAggState *winstate, int64 lastPos)
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0021-Define-RPR-absorption-terminology-in-README.rpr-p.txt (5.3K, 23-nocfbot-0021-Define-RPR-absorption-terminology-in-README.rpr-p.txt)
  download | inline diff:
From d74ee1d2a2ad85dcb3198f13352d4dcf231e3c35 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Fri, 29 May 2026 17:45:13 +0900
Subject: [PATCH 21/68] Define RPR absorption terminology in README.rpr per
 Jian He's review

- Define the "match_start dep." column values (none, direct,
  boundary check) in VIII-3; the table listed them without saying
  what "direct" versus "boundary check" actually mean.
- Fix the "boundary chk" typo in that table to "boundary check".
- Name the cover condition "count-dominance" and spell out the
  comparison, linking back to the count-dominance reference in
  VIII-3(c).
- Rename the prose term "match_start-dependent" to
  "match_start_dependent" to match the defineMatchStartDependent
  identifier.
---
 src/backend/executor/README.rpr | 37 ++++++++++++++++++++++++++-------
 1 file changed, 30 insertions(+), 7 deletions(-)

diff --git a/src/backend/executor/README.rpr b/src/backend/executor/README.rpr
index 6d40bd70faa..1d211245a5b 100644
--- a/src/backend/executor/README.rpr
+++ b/src/backend/executor/README.rpr
@@ -526,7 +526,7 @@ V-3. RPR Fields of WindowAggState
   nfaVisitedMinWord             Lowest bitmapword index touched since last reset
   nfaVisitedMaxWord             Highest bitmapword index touched since last reset
   nfaStateSize                  Precomputed size of RPRNFAState
-  defineMatchStartDependent     DEFINE vars needing per-context evaluation (match_start-dependent)
+  defineMatchStartDependent     DEFINE vars needing per-context evaluation (match_start_dependent)
   nfaLastProcessedRow           Last row processed by NFA (-1 = none)
 
   EXPLAIN ANALYZE instrumentation counters are omitted here; see
@@ -631,7 +631,7 @@ the same row.
 
 The varMatched array is referenced later in Phase 1 (Match).
 
-VI-4. Per-Context Re-evaluation (match_start-dependent variables)
+VI-4. Per-Context Re-evaluation (match_start_dependent variables)
 
 DEFINE variables that depend on match_start (those containing FIRST,
 LAST-with-offset, or compound PREV_FIRST/NEXT_FIRST/PREV_LAST/NEXT_LAST)
@@ -801,7 +801,7 @@ Planner-time prerequisites (all must hold for absorption to be enabled):
   (b) Unbounded frame (ROWS BETWEEN CURRENT ROW AND UNBOUNDED
       FOLLOWING).  Limited frames apply differently to each context,
       breaking the monotonicity principle.
-  (c) No match_start-dependent navigation in DEFINE.
+  (c) No match_start_dependent navigation in DEFINE.
 
       Mechanism: each context has a different matchStartRow, so FIRST
       resolves to a different row for each context at the same
@@ -820,7 +820,27 @@ Planner-time prerequisites (all must hold for absorption to be enabled):
       FIRST (any)                     direct            unsafe
       Compound (inner FIRST)          direct            unsafe
       Compound (inner LAST, no off.)  none              safe
-      Compound (inner LAST, w/off.)   boundary chk      unsafe
+      Compound (inner LAST, w/off.)   boundary check    unsafe
+
+  The "match_start dep." column classifies how the navigation ties a
+  DEFINE result to the context's matchStartRow:
+
+    none            Independent of matchStartRow.  The result depends
+                    only on currentpos (or a fixed offset from it), so
+                    every context evaluates it identically.
+    direct          Computed from matchStartRow itself -- FIRST counts
+                    forward from match start -- so the resolved row,
+                    and thus the result, differs per context.
+    boundary check  The resolved row is currentpos-relative (LAST with
+                    a backward offset, or a compound whose inner LAST
+                    carries an offset), but its in-range test is taken
+                    against the match range [matchStartRow, currentpos].
+                    The range bound differs per context, so the result
+                    can too.
+
+  Only "none" is safe for absorption; "direct" and "boundary check"
+  both make an earlier context's result stop subsuming a later one's
+  (see (c) above).
 
 Runtime conditions (evaluated per context pair):
 
@@ -828,10 +848,13 @@ Runtime conditions (evaluated per context pair):
   (2) allStatesAbsorbable of the target context is true
   (3) An earlier context "covers" all states of the target
 
-Cover condition (nfa_states_covered):
+Cover condition (nfa_states_covered) -- "count-dominance":
 
   A state with the same elemIdx exists in the earlier context,
-  and the count at that depth is greater than or equal -- then it is covered.
+  and the count at that depth is greater than or equal -- then it is
+  covered.  The earlier context's per-depth iteration count thus
+  dominates the later one's; this is the count-dominance comparison
+  referenced in VIII-3(c).
 
 VIII-4. Dual-Flag Design
 
@@ -1515,7 +1538,7 @@ Appendix B. Data Structure Relationship Diagram
     |--- defineVariableList: List<String> (variable names, DEFINE order)
     |--- defineClauseList: List<ExprState>
     |--- nfaVarMatched: bool[] (per-row cache)
-    |--- defineMatchStartDependent: Bitmapset* (match_start-dependent
+    |--- defineMatchStartDependent: Bitmapset* (match_start_dependent
     |        DEFINE vars; see VI-4)
     |--- nfaVisitedElems: bitmapword* (cycle detection)
     |--- nfaVisitedNWords: int (size of nfaVisitedElems)
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0022-Document-the-get_reduced_frame_status-cascade-inv.txt (3.4K, 24-nocfbot-0022-Document-the-get_reduced_frame_status-cascade-inv.txt)
  download | inline diff:
From 75355c0e115a1874f8833520ae89bcc165867263 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Fri, 29 May 2026 17:51:37 +0900
Subject: [PATCH 22/68] Document the get_reduced_frame_status cascade invariant
 per Jian He's review

The RF_* classifier is an early-return cascade whose branches are not
mutually exclusive, so reordering them changes the result.  This is the
standard cascade idiom -- each branch is a minimal test premised on the
negations the preceding returns have established -- not a logic flaw, but
the structure left the contract implicit.

update_reduced_frame() records the match as exactly one of three
(rpr_match_matched, rpr_match_length) shapes: (false, 1), (true, 0), or
(true, >= 1).  Spell that out in the header, add the missing RF_EMPTY_MATCH
return value, and annotate each branch with a "by here" note stating the
running invariant -- notably why the empty match must be classified before
the range test.  No behavior change.
---
 src/backend/executor/nodeWindowAgg.c | 28 ++++++++++++++++++++++++++--
 1 file changed, 26 insertions(+), 2 deletions(-)

diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 99858d22dad..4cf1a9ac67b 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -4306,6 +4306,16 @@ clear_reduced_frame(WindowAggState *winstate)
  *   RF_FRAME_HEAD      pos is the start of the current match
  *   RF_SKIPPED         pos is inside the current match but not the start
  *   RF_UNMATCHED       pos is processed but not part of any match
+ *   RF_EMPTY_MATCH     pos is the start of an empty (zero-length) match
+ *
+ * update_reduced_frame() records the current match as exactly one of three
+ * (rpr_match_matched, rpr_match_length) shapes: (false, 1) for unmatched,
+ * (true, 0) for an empty match, and (true, >= 1) for a real match.  The
+ * tests below form a cascade with early returns: each is a minimal check
+ * that relies on the negations the preceding returns have already
+ * established, so their order is significant.  The "by here" notes spell
+ * out the running invariant; reordering a test would misclassify one of
+ * the three shapes.
  */
 static int
 get_reduced_frame_status(WindowAggState *winstate, int64 pos)
@@ -4316,17 +4326,31 @@ get_reduced_frame_status(WindowAggState *winstate, int64 pos)
 	if (!winstate->rpr_match_valid)
 		return RF_NOT_DETERMINED;
 
-	/* Empty match: covers only the start position */
+	/*
+	 * By here the record is valid and holds one of the three shapes above.
+	 *
+	 * The empty match (true, 0) must be classified first: it has length 0, so
+	 * the range test below would compute start + length == start and reject
+	 * its own start position as out of range.
+	 */
 	if (pos == start && winstate->rpr_match_matched && length == 0)
 		return RF_EMPTY_MATCH;
 
-	/* Outside the result range */
+	/*
+	 * By here length >= 1 -- the only zero-length record, the empty match,
+	 * has been handled -- so [start, start + length) is a well-formed range.
+	 */
 	if (pos < start || pos >= start + length)
 		return RF_NOT_DETERMINED;
 
+	/*
+	 * By here pos lies within [start, start + length).  An unmatched record
+	 * is (false, 1), so this returns for its single in-range position.
+	 */
 	if (!winstate->rpr_match_matched)
 		return RF_UNMATCHED;
 
+	/* By here the match is real (true, >= 1) and pos is one of its rows. */
 	if (pos == start)
 		return RF_FRAME_HEAD;
 
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0023-Explain-the-completed-head-context-branch-in-upda.txt (1.4K, 25-nocfbot-0023-Explain-the-completed-head-context-branch-in-upda.txt)
  download | inline diff:
From 6bf5f93a5c5ce41e15e920c09d702713dd15f7f7 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Fri, 29 May 2026 17:56:02 +0900
Subject: [PATCH 23/68] Explain the completed-head-context branch in
 update_reduced_frame per Jian He's review

ExecRPRGetHeadContext() can return a context whose state list is already
drained, and the branch handling it looked unreachable.  It fires under
SKIP TO NEXT ROW: overlapping contexts let one reach FIN, and record its
result, during an earlier call -- before the call asking about its own
start row arrives.  Comment only; no behavior change.
---
 src/backend/executor/nodeWindowAgg.c | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 4cf1a9ac67b..f16d01e9743 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -4434,7 +4434,12 @@ update_reduced_frame(WindowObject winobj, int64 pos)
 	}
 	else if (targetCtx->states == NULL)
 	{
-		/* Context already completed - skip to result registration */
+		/*
+		 * The head context already completed in an earlier call.  Reachable
+		 * under SKIP TO NEXT ROW, where overlapping contexts let one reach
+		 * FIN -- recording its result -- before the call for its own start
+		 * row arrives.  Register that result.
+		 */
 		goto register_result;
 	}
 
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0024-Tighten-the-RPR-frame-boundary-check-from-to-per-.txt (2.1K, 26-nocfbot-0024-Tighten-the-RPR-frame-boundary-check-from-to-per-.txt)
  download | inline diff:
From a0dc841399f9eb79ae4d77374382dc21f4183f31 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Fri, 29 May 2026 19:07:40 +0900
Subject: [PATCH 24/68] Tighten the RPR frame-boundary check from >= to == per
 Jian He's review

currentPos advances by exactly one row per call, and a finalized context
is skipped by the states == NULL guard, so it can only ever reach
ctxFrameEnd, never overshoot it; >= and == behave identically here, and
== states the intent.  The >= was a defensive guard against an overshoot
that cannot happen -- move that defense into Assert(currentPos <=
ctxFrameEnd) so a future change that breaks the invariant fails
immediately instead of silently slipping past the boundary, and change
the comment from "exceeded" to "reached".  No behavior change.
---
 src/backend/executor/execRPR.c | 15 ++++++++++++---
 1 file changed, 12 insertions(+), 3 deletions(-)

diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 88c59cf3276..4463cfe0a5c 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -1706,7 +1706,7 @@ ExecRPRProcessRow(WindowAggState *winstate, int64 currentPos,
 		if (ctx->states == NULL)
 			continue;
 
-		/* Check frame boundary - finalize if exceeded */
+		/* Check frame boundary - finalize the context when it is reached */
 		if (hasLimitedFrame)
 		{
 			int64		ctxFrameEnd;
@@ -1716,9 +1716,18 @@ ExecRPRProcessRow(WindowAggState *winstate, int64 currentPos,
 									&ctxFrameEnd))
 				ctxFrameEnd = PG_INT64_MAX;
 
-			if (currentPos >= ctxFrameEnd)
+			/*
+			 * currentPos advances by exactly one per call, and a finalized
+			 * context is skipped by the states == NULL guard above, so it can
+			 * only ever reach ctxFrameEnd, never overshoot it.  The Assert
+			 * turns a future change that broke that invariant into an
+			 * immediate failure rather than a silent slip past the boundary.
+			 */
+			Assert(currentPos <= ctxFrameEnd);
+
+			if (currentPos == ctxFrameEnd)
 			{
-				/* Frame boundary exceeded: force mismatch */
+				/* Frame boundary reached: force mismatch */
 				nfa_match(winstate, ctx, NULL);
 				continue;
 			}
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0025-Reject-single-row-window-frame-in-row-pattern-rec.txt (8.1K, 27-nocfbot-0025-Reject-single-row-window-frame-in-row-pattern-rec.txt)
  download | inline diff:
From 36da77822da7f6ead31f0a434a245096cac2aa94 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Sat, 30 May 2026 18:57:56 +0900
Subject: [PATCH 25/68] Reject single-row window frame in row pattern
 recognition

The standard allows only UNBOUNDED FOLLOWING or a positive offset
FOLLOWING as the frame end for row pattern recognition.  A CURRENT ROW
end, or a zero offset, reduces the frame to the single current row, which
is not a valid search space for pattern matching.

Reject the CURRENT ROW spelling in transformRPR() at parse time, and a
zero offset in calculate_frame_offsets() at run time, since the offset
need not be a constant -- it may be a parameter, expression, or subquery.
---
 src/backend/executor/README.rpr        |  5 ++--
 src/backend/executor/nodeWindowAgg.c   | 10 +++++++
 src/backend/parser/parse_rpr.c         | 14 +++++++++
 src/test/regress/expected/rpr_base.out | 41 ++++++++++++++++++--------
 src/test/regress/sql/rpr_base.sql      | 26 ++++++++++++++--
 5 files changed, 80 insertions(+), 16 deletions(-)

diff --git a/src/backend/executor/README.rpr b/src/backend/executor/README.rpr
index 1d211245a5b..467cc03ecff 100644
--- a/src/backend/executor/README.rpr
+++ b/src/backend/executor/README.rpr
@@ -1122,8 +1122,9 @@ X-3. INITIAL vs SEEK
 X-4. Bounded Frame Handling
 
   With RPR, the frame mode is always ROWS and the frame start must be
-  CURRENT ROW. The frame end can be either UNBOUNDED FOLLOWING or n
-  FOLLOWING.
+  CURRENT ROW. The frame end must be UNBOUNDED FOLLOWING or a positive
+  offset (n >= 1) FOLLOWING; a CURRENT ROW end or a zero offset is
+  rejected, since it would reduce the frame to the single current row.
 
   When the frame is bounded (e.g., ROWS BETWEEN CURRENT ROW AND 5
   FOLLOWING), ExecRPRProcessRow receives hasLimitedFrame=true and
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index f16d01e9743..0dab18254e0 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -2369,6 +2369,16 @@ calculate_frame_offsets(PlanState *pstate)
 				ereport(ERROR,
 						(errcode(ERRCODE_INVALID_PRECEDING_OR_FOLLOWING_SIZE),
 						 errmsg("frame ending offset must not be negative")));
+
+			/*
+			 * Row pattern recognition forbids a zero-length frame end;
+			 * checked here so a non-constant offset (e.g. a bind parameter)
+			 * is caught, not just a literal 0.
+			 */
+			if (winstate->rpPattern != NULL && offset == 0)
+				ereport(ERROR,
+						errcode(ERRCODE_WINDOWING_ERROR),
+						errmsg("frame ending offset must be positive with row pattern recognition"));
 		}
 	}
 	winstate->all_first = false;
diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index d2ed6c14811..fa8c375f48b 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -163,6 +163,20 @@ transformRPR(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 				parser_errposition(pstate, windef->excludeLocation >= 0 ? windef->excludeLocation : windef->location));
 	}
 
+	/*
+	 * The standard allows only UNBOUNDED FOLLOWING or a positive offset
+	 * FOLLOWING as the frame end.  The equivalent 0 FOLLOWING spelling is
+	 * caught at runtime in calculate_frame_offsets().
+	 */
+	if (wc->frameOptions & FRAMEOPTION_END_CURRENT_ROW)
+		ereport(ERROR,
+				errcode(ERRCODE_WINDOWING_ERROR),
+				errmsg("cannot use CURRENT ROW as frame end with row pattern recognition"),
+				errhint("Use UNBOUNDED FOLLOWING or a positive offset FOLLOWING."),
+				parser_errposition(pstate,
+								   windef->frameLocation >= 0 ?
+								   windef->frameLocation : windef->location));
+
 	/* Transform AFTER MATCH SKIP TO clause */
 	wc->rpSkipTo = windef->rpCommonSyntax->rpSkipTo;
 
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index cfd2645bbed..d8f805c89aa 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -542,7 +542,8 @@ ERROR:  frame end cannot be UNBOUNDED PRECEDING
 LINE 5:     ROWS BETWEEN CURRENT ROW AND UNBOUNDED PRECEDING
                                          ^
 -- Expected: ERROR: frame end cannot be UNBOUNDED PRECEDING
--- Single row frame: CURRENT ROW AND CURRENT ROW
+-- Single row frame: CURRENT ROW AND CURRENT ROW is rejected (the standard
+-- allows only UNBOUNDED FOLLOWING or a positive offset FOLLOWING).
 SELECT id, val, COUNT(*) OVER w as cnt
 FROM rpr_frame
 WINDOW w AS (
@@ -553,17 +554,13 @@ WINDOW w AS (
     DEFINE A AS val > 0
 )
 ORDER BY id;
- id | val | cnt 
-----+-----+-----
-  1 |  10 |   1
-  2 |  10 |   1
-  3 |  10 |   1
-  4 |  20 |   1
-  5 |  20 |   1
-  6 |  30 |   1
-(6 rows)
-
--- Zero offset: CURRENT ROW AND 0 FOLLOWING (equivalent to CURRENT ROW)
+ERROR:  cannot use CURRENT ROW as frame end with row pattern recognition
+LINE 5:     ROWS BETWEEN CURRENT ROW AND CURRENT ROW
+            ^
+HINT:  Use UNBOUNDED FOLLOWING or a positive offset FOLLOWING.
+-- Expected: ERROR: cannot use CURRENT ROW as frame end with row pattern recognition
+-- Zero offset: CURRENT ROW AND 0 FOLLOWING denotes the same one-row frame
+-- and is likewise rejected (caught at execution time).
 SELECT id, val, COUNT(*) OVER w as cnt
 FROM rpr_frame
 WINDOW w AS (
@@ -574,6 +571,22 @@ WINDOW w AS (
     DEFINE A AS val > 0
 )
 ORDER BY id;
+ERROR:  frame ending offset must be positive with row pattern recognition
+-- Expected: ERROR: frame ending offset must be positive with row pattern recognition
+-- A non-constant frame end offset is allowed; a zero value is still rejected,
+-- this time at execution time (a literal cannot exercise that path).
+PREPARE rpr_end_offset(int8) AS
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_frame
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND $1 FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A)
+    DEFINE A AS val > 0
+)
+ORDER BY id;
+EXECUTE rpr_end_offset(2);
  id | val | cnt 
 ----+-----+-----
   1 |  10 |   1
@@ -584,6 +597,10 @@ ORDER BY id;
   6 |  30 |   1
 (6 rows)
 
+EXECUTE rpr_end_offset(0);
+ERROR:  frame ending offset must be positive with row pattern recognition
+-- Expected: ERROR: frame ending offset must be positive with row pattern recognition
+DEALLOCATE rpr_end_offset;
 -- Large offset: CURRENT ROW AND 1000 FOLLOWING
 SELECT id, val, COUNT(*) OVER w as cnt
 FROM rpr_frame
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index fd289d7cf67..6c2365a2d20 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -445,7 +445,8 @@ WINDOW w AS (
 );
 -- Expected: ERROR: frame end cannot be UNBOUNDED PRECEDING
 
--- Single row frame: CURRENT ROW AND CURRENT ROW
+-- Single row frame: CURRENT ROW AND CURRENT ROW is rejected (the standard
+-- allows only UNBOUNDED FOLLOWING or a positive offset FOLLOWING).
 SELECT id, val, COUNT(*) OVER w as cnt
 FROM rpr_frame
 WINDOW w AS (
@@ -456,8 +457,10 @@ WINDOW w AS (
     DEFINE A AS val > 0
 )
 ORDER BY id;
+-- Expected: ERROR: cannot use CURRENT ROW as frame end with row pattern recognition
 
--- Zero offset: CURRENT ROW AND 0 FOLLOWING (equivalent to CURRENT ROW)
+-- Zero offset: CURRENT ROW AND 0 FOLLOWING denotes the same one-row frame
+-- and is likewise rejected (caught at execution time).
 SELECT id, val, COUNT(*) OVER w as cnt
 FROM rpr_frame
 WINDOW w AS (
@@ -468,6 +471,25 @@ WINDOW w AS (
     DEFINE A AS val > 0
 )
 ORDER BY id;
+-- Expected: ERROR: frame ending offset must be positive with row pattern recognition
+
+-- A non-constant frame end offset is allowed; a zero value is still rejected,
+-- this time at execution time (a literal cannot exercise that path).
+PREPARE rpr_end_offset(int8) AS
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_frame
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND $1 FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A)
+    DEFINE A AS val > 0
+)
+ORDER BY id;
+EXECUTE rpr_end_offset(2);
+EXECUTE rpr_end_offset(0);
+-- Expected: ERROR: frame ending offset must be positive with row pattern recognition
+DEALLOCATE rpr_end_offset;
 
 -- Large offset: CURRENT ROW AND 1000 FOLLOWING
 SELECT id, val, COUNT(*) OVER w as cnt
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0026-Remove-the-redundant-zero-check-on-the-RPR-frame-.txt (1.2K, 28-nocfbot-0026-Remove-the-redundant-zero-check-on-the-RPR-frame-.txt)
  download | inline diff:
From 4df572a905bbb1ce97e425db45e9d9152d53deac Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Sat, 30 May 2026 19:37:11 +0900
Subject: [PATCH 26/68] Remove the redundant zero check on the RPR frame ending
 offset

update_reduced_frame() guarded the frame offset with "endOffsetValue
!= 0", comparing a Datum directly to zero.  Now that a single-row frame
is rejected, a limited frame always carries a real offset, so the guard
is unnecessary; dropping it leaves a plain DatumGetInt64() and removes
the type-confused Datum-vs-zero comparison.

Per Jian He's review.
---
 src/backend/executor/nodeWindowAgg.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 0dab18254e0..bcb3614e96e 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -4399,7 +4399,7 @@ update_reduced_frame(WindowObject winobj, int64 pos)
 	 */
 	hasLimitedFrame = (frameOptions & FRAMEOPTION_ROWS) &&
 		!(frameOptions & FRAMEOPTION_END_UNBOUNDED_FOLLOWING);
-	if (hasLimitedFrame && winstate->endOffsetValue != 0)
+	if (hasLimitedFrame)
 		frameOffset = DatumGetInt64(winstate->endOffsetValue);
 
 	/*
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0027-Restore-findTargetlistEntrySQL99-to-static-per-Ta.txt (2.4K, 29-nocfbot-0027-Restore-findTargetlistEntrySQL99-to-static-per-Ta.txt)
  download | inline diff:
From d070b97113883c1f6efbe2aaaf05dd5640cf3b1a Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Mon, 1 Jun 2026 10:14:33 +0900
Subject: [PATCH 27/68] Restore findTargetlistEntrySQL99 to static per Tatsuo
 Ishii's review

The RPR DEFINE handling once called findTargetlistEntrySQL99 from
parse_rpr.c, which required exposing it as extern.  The Var-only targetlist
fix removed that cross-file call, so revert the function to its original
static linkage and drop the prototype from parse_clause.h.
---
 src/backend/parser/parse_clause.c | 4 +++-
 src/include/parser/parse_clause.h | 3 ---
 2 files changed, 3 insertions(+), 4 deletions(-)

diff --git a/src/backend/parser/parse_clause.c b/src/backend/parser/parse_clause.c
index 3201a22d278..550ea4eb9c0 100644
--- a/src/backend/parser/parse_clause.c
+++ b/src/backend/parser/parse_clause.c
@@ -89,6 +89,8 @@ static void checkExprIsVarFree(ParseState *pstate, Node *n,
 							   const char *constructName);
 static TargetEntry *findTargetlistEntrySQL92(ParseState *pstate, Node *node,
 											 List **tlist, ParseExprKind exprKind);
+static TargetEntry *findTargetlistEntrySQL99(ParseState *pstate, Node *node,
+											 List **tlist, ParseExprKind exprKind);
 static int	get_matching_location(int sortgroupref,
 								  List *sortgrouprefs, List *exprs);
 static List *resolve_unique_index_expr(ParseState *pstate, InferClause *infer,
@@ -2310,7 +2312,7 @@ findTargetlistEntrySQL92(ParseState *pstate, Node *node, List **tlist,
  * tlist	the target list (passed by reference so we can append to it)
  * exprKind identifies clause type being processed
  */
-TargetEntry *
+static TargetEntry *
 findTargetlistEntrySQL99(ParseState *pstate, Node *node, List **tlist,
 						 ParseExprKind exprKind)
 {
diff --git a/src/include/parser/parse_clause.h b/src/include/parser/parse_clause.h
index 8aaac881f2b..fe234611007 100644
--- a/src/include/parser/parse_clause.h
+++ b/src/include/parser/parse_clause.h
@@ -52,9 +52,6 @@ extern List *addTargetToSortList(ParseState *pstate, TargetEntry *tle,
 extern Index assignSortGroupRef(TargetEntry *tle, List *tlist);
 extern bool targetIsInSortList(TargetEntry *tle, Oid sortop, List *sortList);
 
-extern TargetEntry *findTargetlistEntrySQL99(ParseState *pstate, Node *node,
-											 List **tlist, ParseExprKind exprKind);
-
 /* functions in parse_jsontable.c */
 extern ParseNamespaceItem *transformJsonTable(ParseState *pstate, JsonTable *jt);
 
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0029-Rename-RPR-NFA-constructors-to-make-clone-per-Jia.txt (9.3K, 30-nocfbot-0029-Rename-RPR-NFA-constructors-to-make-clone-per-Jia.txt)
  download | inline diff:
From 8a00c552f5f3ecd6c889b53b8062304d43b06252 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Mon, 1 Jun 2026 11:16:49 +0900
Subject: [PATCH 29/68] Rename RPR NFA constructors to make/clone per Jian He's
 round-3 review

Rename nfa_context_alloc and nfa_state_alloc to _make (a blank
allocation), and nfa_state_create to nfa_state_clone (build a state from
an existing one's counts).  This follows the makeNode precedent and the
regex engine's clonesuccessorstates/cloneouts.  Also reword the
nfa_state_clone header comment from "Create" to "Clone".
---
 src/backend/executor/README.rpr       |  2 +-
 src/backend/executor/execRPR.c        | 58 +++++++++++++--------------
 src/test/regress/expected/rpr_nfa.out |  2 +-
 src/test/regress/sql/rpr_nfa.sql      |  2 +-
 4 files changed, 32 insertions(+), 32 deletions(-)

diff --git a/src/backend/executor/README.rpr b/src/backend/executor/README.rpr
index 467cc03ecff..9396a569fbd 100644
--- a/src/backend/executor/README.rpr
+++ b/src/backend/executor/README.rpr
@@ -579,7 +579,7 @@ VI-2. Context Creation: ExecRPRStartContext()
 
 Creates a new context and performs the initial advance.
 
-  (1) Allocate context via nfa_context_alloc()
+  (1) Allocate context via nfa_context_make()
   (2) Set matchStartRow = pos
   (3) Create initial state: elemIdx=0 (first pattern element),
       counts=all zero
diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 9bad36239f8..4022ca66e84 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -54,11 +54,11 @@ nfa_mark_visited(WindowAggState *winstate, int16 elemIdx)
 }
 
 /* Forward declarations - NFA state management */
-static RPRNFAState *nfa_state_alloc(WindowAggState *winstate);
+static RPRNFAState *nfa_state_make(WindowAggState *winstate);
 static void nfa_state_free(WindowAggState *winstate, RPRNFAState *state);
 static void nfa_state_free_list(WindowAggState *winstate, RPRNFAState *list);
-static RPRNFAState *nfa_state_create(WindowAggState *winstate, int16 elemIdx,
-									 int32 *counts, bool sourceAbsorbable);
+static RPRNFAState *nfa_state_clone(WindowAggState *winstate, int16 elemIdx,
+									int32 *counts, bool sourceAbsorbable);
 static bool nfa_states_equal(WindowAggState *winstate, RPRNFAState *s1,
 							 RPRNFAState *s2);
 static void nfa_add_state_unique(WindowAggState *winstate, RPRNFAContext *ctx,
@@ -67,7 +67,7 @@ static void nfa_add_matched_state(WindowAggState *winstate, RPRNFAContext *ctx,
 								  RPRNFAState *state, int64 matchEndRow);
 
 /* Forward declarations - NFA context management (internal) */
-static RPRNFAContext *nfa_context_alloc(WindowAggState *winstate);
+static RPRNFAContext *nfa_context_make(WindowAggState *winstate);
 static void nfa_unlink_context(WindowAggState *winstate, RPRNFAContext *ctx);
 
 /* Forward declarations - NFA statistics */
@@ -199,14 +199,14 @@ static void nfa_advance(WindowAggState *winstate, RPRNFAContext *ctx,
  */
 
 /*
- * nfa_state_alloc
+ * nfa_state_make
  *
  * Allocate an NFA state, reusing from freeList if available.
  * freeList is stored in WindowAggState for reuse across match attempts.
  * Uses flexible array member for counts[].
  */
 static RPRNFAState *
-nfa_state_alloc(WindowAggState *winstate)
+nfa_state_make(WindowAggState *winstate)
 {
 	RPRNFAState *state;
 
@@ -265,21 +265,21 @@ nfa_state_free_list(WindowAggState *winstate, RPRNFAState *list)
 }
 
 /*
- * nfa_state_create
+ * nfa_state_clone
  *
- * Create a new state with given elemIdx and counts.
+ * Clone a state from the given elemIdx and counts.
  * isAbsorbable is computed immediately: inherited AND new element's flag.
  * Monotonic property: once false, stays false through all transitions.
  *
  * Caller is responsible for linking the returned state.
  */
 static RPRNFAState *
-nfa_state_create(WindowAggState *winstate, int16 elemIdx,
-				 int32 *counts, bool sourceAbsorbable)
+nfa_state_clone(WindowAggState *winstate, int16 elemIdx,
+				int32 *counts, bool sourceAbsorbable)
 {
 	RPRPattern *pattern = winstate->rpPattern;
 	int			maxDepth = pattern->maxDepth;
-	RPRNFAState *state = nfa_state_alloc(winstate);
+	RPRNFAState *state = nfa_state_make(winstate);
 	RPRPatternElement *elem = &pattern->elements[elemIdx];
 
 	state->elemIdx = elemIdx;
@@ -418,12 +418,12 @@ nfa_add_matched_state(WindowAggState *winstate, RPRNFAContext *ctx,
 }
 
 /*
- * nfa_context_alloc
+ * nfa_context_make
  *
  * Allocate an NFA context, reusing from free list if available.
  */
 static RPRNFAContext *
-nfa_context_alloc(WindowAggState *winstate)
+nfa_context_make(WindowAggState *winstate)
 {
 	RPRNFAContext *ctx;
 
@@ -931,8 +931,8 @@ nfa_route_to_elem(WindowAggState *winstate, RPRNFAContext *ctx,
 
 		/* Create skip state before add_unique, which may free state */
 		if (RPRElemCanSkip(nextElem))
-			skipState = nfa_state_create(winstate, nextElem->next,
-										 state->counts, state->isAbsorbable);
+			skipState = nfa_state_clone(winstate, nextElem->next,
+										state->counts, state->isAbsorbable);
 
 		nfa_add_state_unique(winstate, ctx, state);
 
@@ -978,8 +978,8 @@ nfa_advance_alt(WindowAggState *winstate, RPRNFAContext *ctx,
 			break;
 
 		/* Create independent state for each branch */
-		newState = nfa_state_create(winstate, altIdx,
-									state->counts, state->isAbsorbable);
+		newState = nfa_state_clone(winstate, altIdx,
+								   state->counts, state->isAbsorbable);
 
 		/* Recursively process this branch before next */
 		nfa_advance_state(winstate, ctx, newState, currentPos);
@@ -1011,8 +1011,8 @@ nfa_advance_begin(WindowAggState *winstate, RPRNFAContext *ctx,
 	/* Optional group: create skip path (but don't route yet) */
 	if (elem->min == 0)
 	{
-		skipState = nfa_state_create(winstate, elem->jump,
-									 state->counts, state->isAbsorbable);
+		skipState = nfa_state_clone(winstate, elem->jump,
+									state->counts, state->isAbsorbable);
 	}
 
 	if (skipState != NULL && RPRElemIsReluctant(elem))
@@ -1094,8 +1094,8 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
 		 *----------
 		 */
 		if (RPRElemCanEmptyLoop(elem))
-			ffState = nfa_state_create(winstate, state->elemIdx,
-									   state->counts, state->isAbsorbable);
+			ffState = nfa_state_clone(winstate, state->elemIdx,
+									  state->counts, state->isAbsorbable);
 
 		/* Primary path: loop back for real matches */
 		for (int d = depth + 1; d < pattern->maxDepth; d++)
@@ -1163,8 +1163,8 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
 		 * Create exit state first (need original counts before modifying
 		 * state)
 		 */
-		exitState = nfa_state_create(winstate, elem->next,
-									 state->counts, state->isAbsorbable);
+		exitState = nfa_state_clone(winstate, elem->next,
+									state->counts, state->isAbsorbable);
 		exitState->counts[depth] = 0;
 		nextElem = &elements[exitState->elemIdx];
 
@@ -1255,8 +1255,8 @@ nfa_advance_var(WindowAggState *winstate, RPRNFAContext *ctx,
 			RPRNFAState *savedMatch = ctx->matchedState;
 
 			/* Clone for exit, original stays for loop */
-			cloneState = nfa_state_create(winstate, elem->next,
-										  state->counts, state->isAbsorbable);
+			cloneState = nfa_state_clone(winstate, elem->next,
+										 state->counts, state->isAbsorbable);
 			cloneState->counts[depth] = 0;
 			nextElem = &elements[cloneState->elemIdx];
 
@@ -1287,8 +1287,8 @@ nfa_advance_var(WindowAggState *winstate, RPRNFAContext *ctx,
 		else
 		{
 			/* Clone for loop, original used for exit */
-			cloneState = nfa_state_create(winstate, state->elemIdx,
-										  state->counts, state->isAbsorbable);
+			cloneState = nfa_state_clone(winstate, state->elemIdx,
+										 state->counts, state->isAbsorbable);
 
 			/* Loop first (preferred for greedy) */
 			nfa_add_state_unique(winstate, ctx, cloneState);
@@ -1505,9 +1505,9 @@ ExecRPRStartContext(WindowAggState *winstate, int64 startPos)
 	RPRPattern *pattern = winstate->rpPattern;
 	RPRPatternElement *elem;
 
-	ctx = nfa_context_alloc(winstate);
+	ctx = nfa_context_make(winstate);
 	ctx->matchStartRow = startPos;
-	ctx->states = nfa_state_alloc(winstate);	/* initial state at elem 0 */
+	ctx->states = nfa_state_make(winstate); /* initial state at elem 0 */
 
 	elem = &pattern->elements[0];
 
diff --git a/src/test/regress/expected/rpr_nfa.out b/src/test/regress/expected/rpr_nfa.out
index 1f494d2db34..72dbf080a37 100644
--- a/src/test/regress/expected/rpr_nfa.out
+++ b/src/test/regress/expected/rpr_nfa.out
@@ -3844,7 +3844,7 @@ WINDOW w AS (
 
 -- Non-absorbable context during absorption
 -- Pattern (A B)+ C: A,B in absorbable group, C is not.
--- When END exits to C via nfa_state_create, isAbsorbable becomes false.
+-- When END exits to C via nfa_state_clone, isAbsorbable becomes false.
 WITH test_non_absorbable AS (
     SELECT * FROM (VALUES
         (1, ARRAY['A']),
diff --git a/src/test/regress/sql/rpr_nfa.sql b/src/test/regress/sql/rpr_nfa.sql
index 76dfc4d88bc..128476aa1d1 100644
--- a/src/test/regress/sql/rpr_nfa.sql
+++ b/src/test/regress/sql/rpr_nfa.sql
@@ -2790,7 +2790,7 @@ WINDOW w AS (
 
 -- Non-absorbable context during absorption
 -- Pattern (A B)+ C: A,B in absorbable group, C is not.
--- When END exits to C via nfa_state_create, isAbsorbable becomes false.
+-- When END exits to C via nfa_state_clone, isAbsorbable becomes false.
 WITH test_non_absorbable AS (
     SELECT * FROM (VALUES
         (1, ARRAY['A']),
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0028-Clarify-RPR-comments-per-Jian-He-s-round-3-review.txt (5.7K, 31-nocfbot-0028-Clarify-RPR-comments-per-Jian-He-s-round-3-review.txt)
  download | inline diff:
From 686c593a23d5604e48a5f08a84cf525cbda8a072 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Mon, 1 Jun 2026 11:03:57 +0900
Subject: [PATCH 28/68] Clarify RPR comments per Jian He's round-3 review

- Replace fragile "Tests line NNN" comments with the branch and condition
  they exercise, so they survive later code shuffles
- Reword nfa_add_state_unique to describe appending to the list tail
- Disambiguate "END chain" as the chain of END elements
- Restate the nfa_advance_var Assert comment as a structural invariant
  (min <= max), not a post-match claim
---
 src/backend/executor/execRPR.c         | 11 ++++++-----
 src/test/regress/expected/rpr_base.out |  8 ++++----
 src/test/regress/sql/rpr_base.sql      |  8 ++++----
 3 files changed, 14 insertions(+), 13 deletions(-)

diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 4463cfe0a5c..9bad36239f8 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -334,7 +334,8 @@ nfa_states_equal(WindowAggState *winstate, RPRNFAState *s1, RPRNFAState *s2)
 /*
  * nfa_add_state_unique
  *
- * Add a state to ctx->states at the END, only if no duplicate exists.
+ * Add the state to the end of the ctx->states linked list, but only if a
+ * duplicate state is not already present.
  * Earlier states have better lexical order (DFS traversal order), so existing
  * wins; the new state is freed when a duplicate is found.
  */
@@ -779,7 +780,7 @@ nfa_eval_var_match(WindowAggState *winstate, RPRPatternElement *elem,
  *     previous advance when count >= min was satisfied)
  *
  * For VARs that reached max count followed by END:
- *   - Advance through END chain to reach absorption judgment point
+ *   - Advance through the END-element chain to the absorption judgment point
  *   - Only deterministic exits (count >= max, max != INF) are handled
  *   - Chains through END elements while count >= max (must-exit path)
  *
@@ -796,8 +797,8 @@ nfa_match(WindowAggState *winstate, RPRNFAContext *ctx, bool *varMatched)
 
 	/*
 	 * Evaluate VAR elements against current row. For VARs that reach max
-	 * count with END next, advance through END chain inline so absorb phase
-	 * can compare states at judgment points.
+	 * count with END next, advance through the chain of END elements inline
+	 * so absorb phase can compare states at judgment points.
 	 */
 	for (state = ctx->states; state != NULL; state = nextState)
 	{
@@ -1229,7 +1230,7 @@ nfa_advance_var(WindowAggState *winstate, RPRNFAContext *ctx,
 	bool		canLoop = (elem->max == RPR_QUANTITY_INF || count < elem->max);
 	bool		canExit = (count >= elem->min);
 
-	/* After a successful match, count >= 1, so at least one must be true */
+	/* min <= max, so !canExit (count < min) implies canLoop (count < max) */
 	Assert(canLoop || canExit);
 
 	/* elem->next must be a valid index for any reachable VAR */
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index d8f805c89aa..c96a1216cc3 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -3442,8 +3442,8 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
 (7 rows)
 
 -- Consecutive VAR merge: A A+ -> a{2,}
--- Tests line 251: child->max == RPR_QUANTITY_INF branch in mergeConsecutiveVars
--- prev: A{1,1} (finite), child: A+ (infinite) triggers line 251 evaluation
+-- Exercises the child->max == RPR_QUANTITY_INF branch in mergeConsecutiveVars,
+-- where a finite prev (A{1,1}) meets an infinite child (A+).
 EXPLAIN (COSTS OFF)
 SELECT COUNT(*) OVER w FROM rpr_plan
 WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
@@ -3492,8 +3492,8 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
 (7 rows)
 
 -- Consecutive GROUP merge: (A B){2} (A B)+ -> (a b){3,}
--- Tests line 325: child->max == RPR_QUANTITY_INF branch in mergeConsecutiveGroups
--- prev: (A B){2,2} (finite), child: (A B)+ (infinite) triggers line 325 evaluation
+-- Exercises the child->max == RPR_QUANTITY_INF branch in mergeConsecutiveGroups,
+-- where a finite prev ((A B){2,2}) meets an infinite child ((A B)+).
 EXPLAIN (COSTS OFF)
 SELECT COUNT(*) OVER w FROM rpr_plan
 WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index 6c2365a2d20..d7b63cfc690 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -2379,8 +2379,8 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
              PATTERN (A+ A*) DEFINE A AS val > 0);
 
 -- Consecutive VAR merge: A A+ -> a{2,}
--- Tests line 251: child->max == RPR_QUANTITY_INF branch in mergeConsecutiveVars
--- prev: A{1,1} (finite), child: A+ (infinite) triggers line 251 evaluation
+-- Exercises the child->max == RPR_QUANTITY_INF branch in mergeConsecutiveVars,
+-- where a finite prev (A{1,1}) meets an infinite child (A+).
 EXPLAIN (COSTS OFF)
 SELECT COUNT(*) OVER w FROM rpr_plan
 WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
@@ -2399,8 +2399,8 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
              PATTERN ((A B)+ (A B)+) DEFINE A AS val <= 50, B AS val > 50);
 
 -- Consecutive GROUP merge: (A B){2} (A B)+ -> (a b){3,}
--- Tests line 325: child->max == RPR_QUANTITY_INF branch in mergeConsecutiveGroups
--- prev: (A B){2,2} (finite), child: (A B)+ (infinite) triggers line 325 evaluation
+-- Exercises the child->max == RPR_QUANTITY_INF branch in mergeConsecutiveGroups,
+-- where a finite prev ((A B){2,2}) meets an infinite child ((A B)+).
 EXPLAIN (COSTS OFF)
 SELECT COUNT(*) OVER w FROM rpr_plan
 WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0030-Demote-RPR-nfaVisitedNWords-to-a-local-per-Jian-H.txt (4.0K, 32-nocfbot-0030-Demote-RPR-nfaVisitedNWords-to-a-local-per-Jian-H.txt)
  download | inline diff:
From 66a754b84526730b31edf03462163ae42aec5f9e Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Mon, 1 Jun 2026 11:38:09 +0900
Subject: [PATCH 30/68] Demote RPR nfaVisitedNWords to a local per Jian He's
 round-3 review

nfaVisitedNWords is read only once, at init, to size the visited bitmap
(the per-row reset clears just the high-water range), so it need not live
in WindowAggState.  nfaStateSize is kept: it is recomputed per state
allocation, the engine's hottest path.
---
 src/backend/executor/README.rpr      | 2 --
 src/backend/executor/execRPR.c       | 2 +-
 src/backend/executor/nodeWindowAgg.c | 6 ++++--
 src/include/nodes/execnodes.h        | 2 --
 4 files changed, 5 insertions(+), 7 deletions(-)

diff --git a/src/backend/executor/README.rpr b/src/backend/executor/README.rpr
index 9396a569fbd..26c1a1ea236 100644
--- a/src/backend/executor/README.rpr
+++ b/src/backend/executor/README.rpr
@@ -522,7 +522,6 @@ V-3. RPR Fields of WindowAggState
   nfaStateFree                  Reuse pool for states
   nfaVarMatched                 Per-row cache: varMatched[varId]
   nfaVisitedElems               Bitmap for cycle detection
-  nfaVisitedNWords              Number of bitmapwords in nfaVisitedElems
   nfaVisitedMinWord             Lowest bitmapword index touched since last reset
   nfaVisitedMaxWord             Highest bitmapword index touched since last reset
   nfaStateSize                  Precomputed size of RPRNFAState
@@ -1542,7 +1541,6 @@ Appendix B. Data Structure Relationship Diagram
     |--- defineMatchStartDependent: Bitmapset* (match_start_dependent
     |        DEFINE vars; see VI-4)
     |--- nfaVisitedElems: bitmapword* (cycle detection)
-    |--- nfaVisitedNWords: int (size of nfaVisitedElems)
     |--- nfaVisitedMinWord / nfaVisitedMaxWord: int16
     |        (touched-word range for fast reset)
     |--- nfaLastProcessedRow: int64 (-1 = none)
diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 4022ca66e84..dca1e45be57 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -41,7 +41,7 @@
 /*
  * Set the visited bit for elemIdx and update the high-water marks
  * (nfaVisitedMin/MaxWord) so that the next reset only has to clear
- * the touched range instead of the full nfaVisitedNWords array.
+ * the touched range instead of the full nfaVisitedElems bitmap.
  */
 static inline void
 nfa_mark_visited(WindowAggState *winstate, int16 elemIdx)
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index bcb3614e96e..0b0196e7e40 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -3058,12 +3058,14 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
 	/* Calculate NFA state size and allocate cycle detection bitmap */
 	if (node->rpPattern != NULL)
 	{
+		int			nfaVisitedNWords;
+
 		winstate->nfaStateSize = offsetof(RPRNFAState, counts) +
 			sizeof(int32) * node->rpPattern->maxDepth;
-		winstate->nfaVisitedNWords =
+		nfaVisitedNWords =
 			(node->rpPattern->numElements - 1) / BITS_PER_BITMAPWORD + 1;
 		winstate->nfaVisitedElems = palloc0(sizeof(bitmapword) *
-											winstate->nfaVisitedNWords);
+											nfaVisitedNWords);
 		/* High-water mark sentinels: no bits set yet. */
 		winstate->nfaVisitedMinWord = INT16_MAX;
 		winstate->nfaVisitedMaxWord = -1;
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 1fba14b892e..4641ed36cee 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -2671,8 +2671,6 @@ typedef struct WindowAggState
 											 * (match_start-dependent) */
 	bitmapword *nfaVisitedElems;	/* elemIdx visited bitmap for cycle
 									 * detection */
-	int			nfaVisitedNWords;	/* number of bitmapwords in
-									 * nfaVisitedElems */
 	int16		nfaVisitedMinWord;	/* lowest bitmapword index touched since
 									 * last reset (INT16_MAX = none) */
 	int16		nfaVisitedMaxWord;	/* highest bitmapword index touched since
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0031-Rename-the-AST-level-prefix-suffix-rewrite-from-a.txt (3.8K, 33-nocfbot-0031-Rename-the-AST-level-prefix-suffix-rewrite-from-a.txt)
  download | inline diff:
From 4817be41a812df933f0a461609c6d38f35179aa4 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Mon, 1 Jun 2026 12:08:50 +0900
Subject: [PATCH 31/68] Rename the AST-level prefix/suffix rewrite from
 "absorption" to "merging"

"absorption" now refers only to the runtime context-equivalence collapse.
The Phase-1 AST rewrite in mergeGroupPrefixSuffix is renamed "prefix/suffix
merging" to match the sibling "consecutive variable / group / ALT merging"
rewrites.  Per the naming discussion with Tatsuo Ishii.
---
 src/backend/executor/README.rpr  |  2 +-
 src/backend/optimizer/plan/rpr.c | 17 ++++++++---------
 2 files changed, 9 insertions(+), 10 deletions(-)

diff --git a/src/backend/executor/README.rpr b/src/backend/executor/README.rpr
index 26c1a1ea236..449bf051153 100644
--- a/src/backend/executor/README.rpr
+++ b/src/backend/executor/README.rpr
@@ -221,7 +221,7 @@ applied:
   (d) Consecutive ALT merging: Merge repeated identical ALT nodes
       (A | B) (A | B) (A | B) -> (A | B){3}
 
-  (e) Prefix/suffix absorption: Absorb identical sequences before/after
+  (e) Prefix/suffix merging: Merge identical sequences before/after
       a group
       A B (A B)+ -> (A B){2,INF}
 
diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index c65681463b3..2a1d665c7ee 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -480,7 +480,7 @@ mergeConsecutiveAlts(List *children)
  *		Merge sequence prefix/suffix into GROUP with matching children.
  *
  * When a GROUP's children appear as a prefix before and/or suffix after
- * the GROUP in a SEQ, absorb them by incrementing the GROUP's quantifier.
+ * the GROUP in a SEQ, merge them by incrementing the GROUP's quantifier.
  * This runs iteratively: A B A B (A B)+ A B -> (A B){5,}.
  *
  * Algorithm:
@@ -503,15 +503,15 @@ mergeGroupPrefixSuffix(List *children)
 	List	   *result = NIL;
 	int			numChildren = list_length(children);
 	int			i;
-	int			skipUntil = -1; /* skip suffix elements already absorbed */
+	int			skipUntil = -1; /* skip suffix elements already merged */
 
 	for (i = 0; i < numChildren; i++)
 	{
 		RPRPatternNode *child = (RPRPatternNode *) list_nth(children, i);
 
 		/*
-		 * The suffix absorption logic below adjusts i to skip absorbed
-		 * elements, ensuring we never revisit them. Verify this invariant.
+		 * The suffix merge logic below adjusts i to skip merged elements,
+		 * ensuring we never revisit them. Verify this invariant.
 		 */
 		Assert(i >= skipUntil);
 
@@ -543,7 +543,7 @@ mergeGroupPrefixSuffix(List *children)
 			groupChildCount = list_length(groupContent);
 
 			/*
-			 * PREFIX MERGE: Check if preceding elements match. Keep absorbing
+			 * PREFIX MERGE: Check if preceding elements match. Keep merging
 			 * as long as we have matching prefixes.
 			 */
 			while (prefixLen >= groupChildCount && groupChildCount > 0)
@@ -592,7 +592,7 @@ mergeGroupPrefixSuffix(List *children)
 			}
 
 			/*
-			 * SUFFIX MERGE: Check if following elements match. Keep absorbing
+			 * SUFFIX MERGE: Check if following elements match. Keep merging
 			 * as long as we have matching suffixes.
 			 */
 			while (i + groupChildCount < numChildren && groupChildCount > 0)
@@ -623,7 +623,7 @@ mergeGroupPrefixSuffix(List *children)
 					 child->max < RPR_QUANTITY_INF - 1))
 				{
 					/*
-					 * Match! Absorb suffix by incrementing quantifier and
+					 * Match! Merge suffix by incrementing quantifier and
 					 * skipping.
 					 */
 					child->min += 1;
@@ -632,8 +632,7 @@ mergeGroupPrefixSuffix(List *children)
 					skipUntil = suffixStart + groupChildCount;
 
 					/*
-					 * Update i to continue suffix check after absorbed
-					 * elements
+					 * Update i to continue suffix check after merged elements
 					 */
 					i = skipUntil - 1;
 				}
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0032-Drop-non-standard-per-group-banner-labels-from-RP.txt (2.5K, 34-nocfbot-0032-Drop-non-standard-per-group-banner-labels-from-RP.txt)
  download | inline diff:
From b5a8065aa1d4e5f4679508c8dbafd6465ef2d6ac Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Mon, 1 Jun 2026 12:49:16 +0900
Subject: [PATCH 32/68] Drop non-standard per-group banner labels from RPR
 forward declarations

The per-group "Forward declarations - ..." labels in execRPR.c are local
to the RPR files; the tree convention is a single banner over the block,
or none.  Consolidate to one "/* Forward declarations */" banner.  Per
Jian He's off-list review.
---
 src/backend/executor/execRPR.c | 6 +-----
 1 file changed, 1 insertion(+), 5 deletions(-)

diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index dca1e45be57..34638409f66 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -53,7 +53,7 @@ nfa_mark_visited(WindowAggState *winstate, int16 elemIdx)
 	winstate->nfaVisitedMaxWord = Max(winstate->nfaVisitedMaxWord, w);
 }
 
-/* Forward declarations - NFA state management */
+/* Forward declarations */
 static RPRNFAState *nfa_state_make(WindowAggState *winstate);
 static void nfa_state_free(WindowAggState *winstate, RPRNFAState *state);
 static void nfa_state_free_list(WindowAggState *winstate, RPRNFAState *list);
@@ -66,23 +66,19 @@ static void nfa_add_state_unique(WindowAggState *winstate, RPRNFAContext *ctx,
 static void nfa_add_matched_state(WindowAggState *winstate, RPRNFAContext *ctx,
 								  RPRNFAState *state, int64 matchEndRow);
 
-/* Forward declarations - NFA context management (internal) */
 static RPRNFAContext *nfa_context_make(WindowAggState *winstate);
 static void nfa_unlink_context(WindowAggState *winstate, RPRNFAContext *ctx);
 
-/* Forward declarations - NFA statistics */
 static void nfa_update_length_stats(int64 count, NFALengthStats *stats, int64 newLen);
 static void nfa_record_context_skipped(WindowAggState *winstate, int64 skippedLen);
 static void nfa_record_context_absorbed(WindowAggState *winstate, int64 absorbedLen);
 
-/* Forward declarations - NFA absorption */
 static void nfa_update_absorption_flags(RPRNFAContext *ctx);
 static bool nfa_states_covered(RPRPattern *pattern, RPRNFAContext *older,
 							   RPRNFAContext *newer);
 static void nfa_try_absorb_context(WindowAggState *winstate, RPRNFAContext *ctx);
 static void nfa_absorb_contexts(WindowAggState *winstate);
 
-/* Forward declarations - NFA match and advance */
 static bool nfa_eval_var_match(WindowAggState *winstate,
 							   RPRPatternElement *elem, bool *varMatched);
 static void nfa_match(WindowAggState *winstate, RPRNFAContext *ctx,
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0033-Clarify-that-ExecRPRCleanupDeadContexts-always-fr.txt (1.6K, 35-nocfbot-0033-Clarify-that-ExecRPRCleanupDeadContexts-always-fr.txt)
  download | inline diff:
From 86949cfa045558fc9754f61211f93483a144872e Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Mon, 1 Jun 2026 12:52:45 +0900
Subject: [PATCH 33/68] Clarify that ExecRPRCleanupDeadContexts always frees
 the failed context

The failure statistic is recorded conditionally, but the context is freed
unconditionally.  A stray "else:" comment before the free made it read as
the conditional branch; reword the comment and drop the "else:".  No code
change.  Per Jian He's off-list review.
---
 src/backend/executor/execRPR.c | 7 +++----
 1 file changed, 3 insertions(+), 4 deletions(-)

diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 34638409f66..16a0f4ae375 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -1805,9 +1805,9 @@ ExecRPRCleanupDeadContexts(WindowAggState *winstate, RPRNFAContext *excludeCtx)
 			continue;
 
 		/*
-		 * This is a failed context - count and remove it. Only count if it
-		 * actually processed its start row. Contexts created for
-		 * beyond-partition rows are silently removed.
+		 * Failed context: always removed below.  Only record the failure
+		 * statistic if it actually processed its start row; contexts created
+		 * for beyond-partition rows are removed without being counted.
 		 */
 		if (ctx->lastProcessedRow >= ctx->matchStartRow)
 		{
@@ -1815,7 +1815,6 @@ ExecRPRCleanupDeadContexts(WindowAggState *winstate, RPRNFAContext *excludeCtx)
 
 			ExecRPRRecordContextFailure(winstate, failedLen);
 		}
-		/* else: context was never processed (beyond-partition), just remove */
 
 		ExecRPRFreeContext(winstate, ctx);
 	}
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0034-Correct-stale-RPR-comments-and-document-a-defensi.txt (5.9K, 36-nocfbot-0034-Correct-stale-RPR-comments-and-document-a-defensi.txt)
  download | inline diff:
From 57989dcb91d0c0e37d07c4d4b79004c45b2815aa Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Mon, 1 Jun 2026 13:45:48 +0900
Subject: [PATCH 34/68] Correct stale RPR comments and document a defensive
 window check

Static analysis surfaced several comments that no longer match the code:

- plannodes.h: rewrite the RPRPattern.isAbsorbable comment to match the
  implemented cases (isUnboundedStart / computeAbsorbability) and the
  normalization that enables absorption; the old text wrongly called the
  prefix/suffix merge unimplemented
- README.rpr: an unmatched row's reduced frame is empty, not "the row
  itself"; the varId range is 0-250, not 0-251
- advanced.sgml: unmatched rows show NULL window functions and
  initial-value aggregates (count() = 0), not all-NULL
- planner.c: note that the RPR fields in optimize_window_clauses' duplicate
  check are reached but defensive -- RPR clauses are separated by their
  frame options first -- kept for parity with transformWindowFuncCall
---
 doc/src/sgml/advanced.sgml           |  5 +++--
 src/backend/executor/README.rpr      |  6 +++---
 src/backend/optimizer/plan/planner.c |  5 ++++-
 src/include/nodes/plannodes.h        | 21 ++++++++++++++-------
 4 files changed, 24 insertions(+), 13 deletions(-)

diff --git a/doc/src/sgml/advanced.sgml b/doc/src/sgml/advanced.sgml
index 11c2416df51..1410a443609 100644
--- a/doc/src/sgml/advanced.sgml
+++ b/doc/src/sgml/advanced.sgml
@@ -600,8 +600,9 @@ DEFINE
     subsequent rows all window functions are shown as NULL. Aggregates on
     non-starting rows return their initial value: for example,
     <function>count()</function> returns 0 and <function>sum()</function>
-    returns NULL. For rows that do not match the PATTERN, columns are shown
-    as NULL too. Example of a <literal>SELECT</literal> using
+    returns NULL. For rows that do not match the PATTERN, window functions
+    are likewise shown as NULL and aggregates return their initial value.
+    Example of a <literal>SELECT</literal> using
     the <literal>DEFINE</literal> and <literal>PATTERN</literal> clause is as
     follows.
 
diff --git a/src/backend/executor/README.rpr b/src/backend/executor/README.rpr
index 449bf051153..08418588114 100644
--- a/src/backend/executor/README.rpr
+++ b/src/backend/executor/README.rpr
@@ -245,7 +245,7 @@ RPRPatternElement struct (16 bytes):
 
   Field      Size     Description
   ---------------------------------------------------------
-  varId      1B      Variable ID (0-251) or control code (252-255)
+  varId      1B      Variable ID (0-250) or control code (252-255)
   depth      1B      Group nesting depth
   flags      1B      Bit flags (see below)
   reserved   1B      Padding
@@ -1303,11 +1303,11 @@ XI-4. Execution Trace
 
 XI-5. Final Result
 
-  Row 0: unmatched     -> frame = the row itself
+  Row 0: unmatched     -> reduced frame empty (window funcs NULL, count() 0)
   Row 1: match head    -> frame = rows 1 through 3
   Row 2: inside match  -> skipped
   Row 3: inside match  -> skipped
-  Row 4: unmatched     -> frame = the row itself
+  Row 4: unmatched     -> reduced frame empty (window funcs NULL, count() 0)
 
 Chapter XII  Summary of Key Design Decisions
 ============================================================================
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index c6fc868cdca..f43cc0edb37 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -6254,7 +6254,10 @@ optimize_window_clauses(PlannerInfo *root, WindowFuncLists *wflists)
 
 				/*
 				 * Perform the same duplicate check that is done in
-				 * transformWindowFuncCall.
+				 * transformWindowFuncCall. wc is never an RPR clause here
+				 * (those are skipped above), and an RPR existing_wc differs
+				 * in its frame options anyway, so the RPR-related comparisons
+				 * are a defensive backstop for parity.
 				 */
 				if (equal(wc->partitionClause, existing_wc->partitionClause) &&
 					equal(wc->orderClause, existing_wc->orderClause) &&
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index fd4bdf2cb31..d45d93d79a2 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -1296,15 +1296,22 @@ typedef struct RPRPattern
 	 * Context absorption optimization.
 	 *
 	 * Absorption is only safe when later matches are guaranteed to be
-	 * suffixes of earlier matches. This requires simple pattern structure:
+	 * suffixes of earlier matches, which requires the pattern to start with
+	 * an unbounded greedy element.  Phase-1 normalization (consecutive
+	 * variable / group / ALT merging and prefix/suffix merging) rewrites the
+	 * pattern toward that form first -- so e.g. A B (A B)+ is merged to
+	 * (A B){2,} and then judged absorbable.
 	 *
-	 * Case 1: No ALT, single unbounded element (A+, (A B)+)
-	 * Case 2: Top-level ALT with each branch being single unbounded (A+ | B+)
+	 * computeAbsorbability() marks the absorbable cases (see isUnboundedStart):
+	 *   - simple unbounded VAR at the start:                    A+ B C
+	 *   - unbounded GROUP with fixed-length children:           (A B)+, (A B{2})+
+	 *   - top-level ALT with independently absorbable branches: A+ | B+
+	 *     (handled in computeAbsorbabilityRecursive)
 	 *
-	 * Complex patterns like A B (A B)+ could theoretically be transformed to
-	 * (A B){2,} for absorption, but this changes lexical order and is not
-	 * implemented. Similarly, (A|B)+ cannot be absorbed because different
-	 * start positions produce different match contents (not suffix relation).
+	 * Not absorbable: an unbounded element not at the start (A B+), a
+	 * reluctant quantifier (A+?), or an ALT inside a group ((A|B)+) -- there
+	 * different start positions yield different match contents, so later
+	 * matches are not suffixes of earlier ones.
 	 */
 	bool		isAbsorbable;	/* true if pattern supports context absorption */
 } RPRPattern;
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0035-Fix-unsafe-A-n-quantifier-flattening-in-row-patte.txt (12.7K, 37-nocfbot-0035-Fix-unsafe-A-n-quantifier-flattening-in-row-patte.txt)
  download | inline diff:
From 6bfd39fc7cf8b758a3dd910d89c0893d57425407 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Mon, 1 Jun 2026 16:35:45 +0900
Subject: [PATCH 35/68] Fix unsafe (A{n,})* quantifier flattening in row
 pattern recognition

The Phase-1 pattern optimizer's tryMultiplyQuantifiers() collapsed a
nested quantifier (A{k,})* to A* whenever both the inner and outer
bounds were unbounded.  That is only correct when the inner minimum is
0 or 1.  For k >= 2 the reachable repetition counts are {0} together
with [k, INF), so the counts 1..k-1 are unreachable; A* admits them.
As a result an isolated single A matched as length 1 where the pattern
must instead produce an empty match (length 0), widening the matching
language.

Skip the multiplication when the outer quantifier is skippable
(minimum 0) and the inner minimum is at least 2.  The existing safe
cases are unaffected: an inner minimum of 0 or 1 stays contiguous, and
a non-skippable outer such as (A{2,})+ still folds to A{2,}.

Per a report from a static analysis tool.
---
 src/backend/executor/README.rpr           |  1 +
 src/backend/optimizer/plan/rpr.c          | 18 +++++-
 src/test/regress/expected/rpr_explain.out | 41 +++++++++++++
 src/test/regress/expected/rpr_nfa.out     | 72 +++++++++++++++++++++++
 src/test/regress/sql/rpr_explain.sql      | 23 ++++++++
 src/test/regress/sql/rpr_nfa.sql          | 53 +++++++++++++++++
 6 files changed, 207 insertions(+), 1 deletion(-)

diff --git a/src/backend/executor/README.rpr b/src/backend/executor/README.rpr
index 08418588114..3a215f2566b 100644
--- a/src/backend/executor/README.rpr
+++ b/src/backend/executor/README.rpr
@@ -232,6 +232,7 @@ applied:
   (g) Quantifier multiplication: Collapse nested quantifiers when safe
       (A+)+ -> A+
       (A{2,3}){5} -> A{10,15}
+      (A{2,})* stays as-is (count 1 unreachable; A* would be wrong)
 
   (h) Single-child unwrap
       SEQ(A) -> A,  (A){1,1} -> A
diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index 2a1d665c7ee..2a98977b288 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -781,7 +781,8 @@ optimizeAltPattern(RPRPatternNode *pattern)
  *		Try to multiply quantifiers.
  *
  * Multiplication is SAFE when:
- *   1. Both unbounded: (A*)* -> A*, (A+)+ -> A+
+ *   1. Both unbounded, with skipless outer or child->min <= 1:
+ *      (A*)* -> A*, (A+)+ -> A+, (A+)* -> A*, (A{2,})+ -> A{2,}
  *   2. Outer exact: (A{m,n}){k} -> A{m*k, n*k}
  *   3. Outer range + child {1,1}: (A){2,} -> A{2,}
  *
@@ -789,6 +790,9 @@ optimizeAltPattern(RPRPatternNode *pattern)
  *   - Only child unbounded: (A+){3} has different semantics
  *   - Outer range + child not {1,1}: gaps possible
  *     e.g., (A{2}){2,3} yields 4,6 only (not 4,5,6)
+ *   - Skippable outer (min 0) + child->min >= 2: (A{2,})* reaches
+ *     {0} UNION [child->min, INF), so 1..child->min-1 are unreachable
+ *     and A* would wrongly admit them
  *
  * Returns the child node with multiplied quantifiers if successful,
  * otherwise returns the original pattern unchanged.
@@ -816,6 +820,18 @@ tryMultiplyQuantifiers(RPRPatternNode *pattern)
 	/* Case 1: Both unbounded - (A*)* -> A*, (A+)+ -> A+ */
 	if (child->max == RPR_QUANTITY_INF && pattern->max == RPR_QUANTITY_INF)
 	{
+		/*
+		 * A skippable outer (min 0) over a child with min >= 2 reaches
+		 * repetition counts {0} UNION [child->min, INF): the counts
+		 * 1..child->min-1 are unreachable, and no single quantifier can
+		 * express that gap.  Flattening to A{0,INF} = A* would wrongly admit
+		 * them, e.g. (A{2,})* would match a single A.  Multiplication is safe
+		 * here only when child->min <= 1 (the reachable set is then
+		 * contiguous from 0); otherwise leave the pattern unflattened.
+		 */
+		if (pattern->min == 0 && child->min >= 2)
+			return pattern;
+
 		new_min_64 = (int64) child->min * pattern->min;
 		if (new_min_64 >= RPR_QUANTITY_INF)
 			return pattern;		/* overflow, skip optimization */
diff --git a/src/test/regress/expected/rpr_explain.out b/src/test/regress/expected/rpr_explain.out
index 77079d5e8c9..9ba302b11ae 100644
--- a/src/test/regress/expected/rpr_explain.out
+++ b/src/test/regress/expected/rpr_explain.out
@@ -782,6 +782,47 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=1000.00 loops=1)
 (10 rows)
 
+-- (A{2,})* must NOT flatten to a* (H-1): counts {0} UNION [2, INF) leave 1
+-- unreachable.  The planner keeps it as (a{2,})*, not a*.
+CREATE VIEW rpr_ev_nested_quant_no_flatten AS
+SELECT count(*) OVER w
+FROM generate_series(1, 6) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A{2,})*)
+    DEFINE A AS v % 3 <> 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_nested_quant_no_flatten'), E'\n')) AS line WHERE line ~ 'PATTERN';
+         line          
+-----------------------
+   PATTERN ((a{2,})*) 
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 6) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A{2,})*)
+    DEFINE A AS v % 3 <> 0
+);');
+                         rpr_explain_filter                          
+---------------------------------------------------------------------
+ WindowAgg (actual rows=6.00 loops=1)
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a{2,}")*
+   Nav Mark Lookback: 0
+   Storage: Memory  Maximum Storage: NkB
+   NFA States: 5 peak, 18 total, 0 merged
+   NFA Contexts: 3 peak, 7 total, 2 pruned
+   NFA: 2 matched (len 2/2/2.0), 0 mismatched
+   NFA: 2 absorbed (len 1/1/1.0), 0 skipped
+   ->  Function Scan on generate_series s (actual rows=6.00 loops=1)
+(10 rows)
+
 -- ============================================================
 -- Context Statistics Tests (peak, total, pruned + absorbed/skipped)
 -- ============================================================
diff --git a/src/test/regress/expected/rpr_nfa.out b/src/test/regress/expected/rpr_nfa.out
index 72dbf080a37..59b91ff9aa4 100644
--- a/src/test/regress/expected/rpr_nfa.out
+++ b/src/test/regress/expected/rpr_nfa.out
@@ -2275,6 +2275,78 @@ WINDOW w AS (
   6 | {B}   |             |          
 (6 rows)
 
+-- Nested quantifier flattening must not widen the matching language (H-1).
+-- (A{k,})* with k >= 2 reaches repetition counts {0} UNION [k, INF); the gap
+-- 1..k-1 is unreachable, so it must NOT collapse to A*.  An isolated single A
+-- must yield an EMPTY match (count 0), not a length-1 match.
+WITH test_nested_quant_var AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),  -- isolated A: (A{2,})* matches empty here, not 1
+        (2, ARRAY['_']),
+        (3, ARRAY['A']),
+        (4, ARRAY['A']),  -- run of 2: matched
+        (5, ARRAY['_'])
+    ) AS t(id, flags)
+)
+SELECT id, flags,
+       first_value(id) OVER w AS match_start,
+       last_value(id) OVER w AS match_end,
+       count(*) OVER w AS match_count
+FROM test_nested_quant_var
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A{2,})*)
+    DEFINE A AS 'A' = ANY(flags)
+);
+ id | flags | match_start | match_end | match_count 
+----+-------+-------------+-----------+-------------
+  1 | {A}   |             |           |           0
+  2 | {_}   |             |           |           0
+  3 | {A}   |           3 |         4 |           2
+  4 | {A}   |             |           |           0
+  5 | {_}   |             |           |           0
+(5 rows)
+
+-- Same for a GROUP child: ((A B){2,})* must not collapse to (A B)*.
+-- An isolated single (A B) pair must yield an EMPTY match (count 0).
+WITH test_nested_quant_group AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),  -- isolated (A B) pair: matches empty here
+        (2, ARRAY['B']),
+        (3, ARRAY['_']),
+        (4, ARRAY['A']),
+        (5, ARRAY['B']),
+        (6, ARRAY['A']),
+        (7, ARRAY['B']),  -- run of 2 pairs: matched
+        (8, ARRAY['_'])
+    ) AS t(id, flags)
+)
+SELECT id, flags,
+       first_value(id) OVER w AS match_start,
+       last_value(id) OVER w AS match_end,
+       count(*) OVER w AS match_count
+FROM test_nested_quant_group
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (((A B){2,})*)
+    DEFINE A AS 'A' = ANY(flags), B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end | match_count 
+----+-------+-------------+-----------+-------------
+  1 | {A}   |             |           |           0
+  2 | {B}   |             |           |           0
+  3 | {_}   |             |           |           0
+  4 | {A}   |           4 |         7 |           4
+  5 | {B}   |             |           |           0
+  6 | {A}   |             |           |           0
+  7 | {B}   |             |           |           0
+  8 | {_}   |             |           |           0
+(8 rows)
+
 -- ============================================================
 -- Pathological Pattern Runtime Protection
 -- ============================================================
diff --git a/src/test/regress/sql/rpr_explain.sql b/src/test/regress/sql/rpr_explain.sql
index a527615849a..c8b159e30e6 100644
--- a/src/test/regress/sql/rpr_explain.sql
+++ b/src/test/regress/sql/rpr_explain.sql
@@ -499,6 +499,29 @@ WINDOW w AS (
     DEFINE A AS v % 3 = 1, B AS v % 3 = 2
 );');
 
+-- (A{2,})* must NOT flatten to a* (H-1): counts {0} UNION [2, INF) leave 1
+-- unreachable.  The planner keeps it as (a{2,})*, not a*.
+CREATE VIEW rpr_ev_nested_quant_no_flatten AS
+SELECT count(*) OVER w
+FROM generate_series(1, 6) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A{2,})*)
+    DEFINE A AS v % 3 <> 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_nested_quant_no_flatten'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 6) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A{2,})*)
+    DEFINE A AS v % 3 <> 0
+);');
+
 -- ============================================================
 -- Context Statistics Tests (peak, total, pruned + absorbed/skipped)
 -- ============================================================
diff --git a/src/test/regress/sql/rpr_nfa.sql b/src/test/regress/sql/rpr_nfa.sql
index 128476aa1d1..febf834565d 100644
--- a/src/test/regress/sql/rpr_nfa.sql
+++ b/src/test/regress/sql/rpr_nfa.sql
@@ -1583,6 +1583,59 @@ WINDOW w AS (
         B AS 'B' = ANY(flags)
 );
 
+-- Nested quantifier flattening must not widen the matching language (H-1).
+-- (A{k,})* with k >= 2 reaches repetition counts {0} UNION [k, INF); the gap
+-- 1..k-1 is unreachable, so it must NOT collapse to A*.  An isolated single A
+-- must yield an EMPTY match (count 0), not a length-1 match.
+WITH test_nested_quant_var AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),  -- isolated A: (A{2,})* matches empty here, not 1
+        (2, ARRAY['_']),
+        (3, ARRAY['A']),
+        (4, ARRAY['A']),  -- run of 2: matched
+        (5, ARRAY['_'])
+    ) AS t(id, flags)
+)
+SELECT id, flags,
+       first_value(id) OVER w AS match_start,
+       last_value(id) OVER w AS match_end,
+       count(*) OVER w AS match_count
+FROM test_nested_quant_var
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A{2,})*)
+    DEFINE A AS 'A' = ANY(flags)
+);
+
+-- Same for a GROUP child: ((A B){2,})* must not collapse to (A B)*.
+-- An isolated single (A B) pair must yield an EMPTY match (count 0).
+WITH test_nested_quant_group AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),  -- isolated (A B) pair: matches empty here
+        (2, ARRAY['B']),
+        (3, ARRAY['_']),
+        (4, ARRAY['A']),
+        (5, ARRAY['B']),
+        (6, ARRAY['A']),
+        (7, ARRAY['B']),  -- run of 2 pairs: matched
+        (8, ARRAY['_'])
+    ) AS t(id, flags)
+)
+SELECT id, flags,
+       first_value(id) OVER w AS match_start,
+       last_value(id) OVER w AS match_end,
+       count(*) OVER w AS match_count
+FROM test_nested_quant_group
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (((A B){2,})*)
+    DEFINE A AS 'A' = ANY(flags), B AS 'B' = ANY(flags)
+);
+
 -- ============================================================
 -- Pathological Pattern Runtime Protection
 -- ============================================================
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0036-Avoid-INF-valued-quantifier-bound-in-RPR-consecut.txt (10.8K, 38-nocfbot-0036-Avoid-INF-valued-quantifier-bound-in-RPR-consecut.txt)
  download | inline diff:
From dcfeb7ba74a9acf49d41c4e395fd44952eed8837 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Mon, 1 Jun 2026 17:03:04 +0900
Subject: [PATCH 36/68] Avoid INF-valued quantifier bound in RPR
 consecutive-merge optimization

mergeConsecutiveVars() and mergeConsecutiveGroups() accumulate the min and
max bounds of two adjacent pattern elements, guarding against overflow with
prev->min <= RPR_QUANTITY_INF - child->min (and likewise for max).  The <=
let a sum land exactly on RPR_QUANTITY_INF (INT32_MAX), the value reserved as
the sentinel for an unbounded max.  The merged element then carried
min == INF, which is not a valid finite lower bound: it tripped the Assert in
fillRPRPattern* at plan time, and in a non-assert build silently produced a
pattern that can never match.

Tighten both guards to a strict <, so a sum reaching INF falls back and
leaves the elements unmerged.  This matches the >= INF check already used by
the quantifier-multiply path and the strict bound in mergeGroupPrefixSuffix.

Per a report from a static analysis tool.
---
 src/backend/optimizer/plan/rpr.c       | 22 ++++---
 src/test/regress/expected/rpr_base.out | 83 ++++++++++++++++++++++++++
 src/test/regress/sql/rpr_base.sql      | 43 +++++++++++++
 3 files changed, 140 insertions(+), 8 deletions(-)

diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index 2a98977b288..b989fcc5162 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -245,13 +245,16 @@ mergeConsecutiveVars(List *children)
 			/* ----------------------
 			 * Can merge consecutive VAR nodes if:
 			 * 1. Same variable name
-			 * 2. No min overflow: prev->min + child->min <= INF
-			 * 3. No max overflow: prev->max + child->max <= INF (or either is INF)
+			 * 2. No min overflow: prev->min + child->min < INF
+			 * 3. No max overflow: prev->max + child->max < INF (or either is INF)
+			 *
+			 * Strict <: a sum equal to INF would alias the unbounded sentinel
+			 * (min must stay finite; a finite max must not become INF).
 			 */
 			if (prev != NULL &&
 				strcmp(prev->varName, child->varName) == 0 &&
-				prev->min <= RPR_QUANTITY_INF - child->min &&
-				(prev->max <= RPR_QUANTITY_INF - child->max ||
+				prev->min < RPR_QUANTITY_INF - child->min &&
+				(prev->max < RPR_QUANTITY_INF - child->max ||
 				 prev->max == RPR_QUANTITY_INF ||
 				 child->max == RPR_QUANTITY_INF))
 			{
@@ -319,13 +322,16 @@ mergeConsecutiveGroups(List *children)
 			/* ----------------------
 			 * Can merge consecutive GROUP nodes if:
 			 * 1. Identical children
-			 * 2. No min overflow: prev->min + child->min <= INF
-			 * 3. No max overflow: prev->max + child->max <= INF (or either is INF)
+			 * 2. No min overflow: prev->min + child->min < INF
+			 * 3. No max overflow: prev->max + child->max < INF (or either is INF)
+			 *
+			 * Strict <: a sum equal to INF would alias the unbounded sentinel
+			 * (min must stay finite; a finite max must not become INF).
 			 */
 			if (prev != NULL &&
 				rprPatternChildrenEqual(prev->children, child->children) &&
-				prev->min <= RPR_QUANTITY_INF - child->min &&
-				(prev->max <= RPR_QUANTITY_INF - child->max ||
+				prev->min < RPR_QUANTITY_INF - child->min &&
+				(prev->max < RPR_QUANTITY_INF - child->max ||
 				 prev->max == RPR_QUANTITY_INF ||
 				 child->max == RPR_QUANTITY_INF))
 			{
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index c96a1216cc3..ebc1088018a 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -3459,6 +3459,25 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
          ->  Seq Scan on rpr_plan
 (7 rows)
 
+-- Consecutive VAR merge at the boundary: A{1073741823,} A{1073741823,} ->
+-- a{2147483646,}.  The min sum 2147483646 = INT32_MAX - 1 is the largest
+-- still-finite bound, so the merge proceeds; a sum of exactly INF instead
+-- falls back (see the Optimization Fallback Tests).
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN (A{1073741823,} A{1073741823,}) DEFINE A AS val > 0);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a{2147483646,}"
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 rows)
+
 -- Consecutive GROUP merge with finite quantifiers: ((A B){5}) ((A B){10}) -> merged
 EXPLAIN (COSTS OFF)
 SELECT COUNT(*) OVER w FROM rpr_plan
@@ -3509,6 +3528,25 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
          ->  Seq Scan on rpr_plan
 (7 rows)
 
+-- Consecutive GROUP merge at the boundary: (A B){1073741823,} (A B){1073741823,}
+-- -> (a b){2147483646,}.  The min sum INT32_MAX - 1 is still finite, so the
+-- merge proceeds; a sum of exactly INF instead falls back (see the
+-- Optimization Fallback Tests).
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN ((A B){1073741823,} (A B){1073741823,}) DEFINE A AS val <= 50, B AS val > 50);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a' b'){2147483646,}"
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 rows)
+
 -- PREFIX merge: A B (A B)+ -> (a b){2,}
 EXPLAIN (COSTS OFF)
 SELECT COUNT(*) OVER w FROM rpr_plan
@@ -5269,6 +5307,51 @@ WINDOW w AS (
 (7 rows)
 
 -- Expected: Fallback - prefix elements don't match GROUP content
+-- Test: consecutive VAR merge whose min sum is exactly INF causes fallback.
+-- 1073741824 + 1073741823 = 2147483647 = INT32_MAX = RPR_QUANTITY_INF.
+-- Merging would yield a VAR with min == INF, so the merge must fall back and
+-- leave the two VARs unmerged (mirrors the multiply path's >= INF guard).
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_fallback
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{1073741824,} A{1073741823,})
+    DEFINE A AS val > 0
+);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a{1073741824,}" a{1073741823,}
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_fallback
+(7 rows)
+
+-- Expected: Fallback - VARs not merged (min sum 2147483647 == INF)
+-- Test: consecutive GROUP merge whose min sum is exactly INF causes fallback.
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_fallback
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A B){1073741824,} (A B){1073741823,})
+    DEFINE A AS val > 0, B AS val > 5
+);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a' b'){1073741824,}" (a b){1073741823,}
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_fallback
+(7 rows)
+
+-- Expected: Fallback - GROUPs not merged (min sum 2147483647 == INF)
 DROP TABLE rpr_fallback;
 -- ============================================================
 -- Planner Integration Tests
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index d7b63cfc690..63173615273 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -2386,6 +2386,15 @@ SELECT COUNT(*) OVER w FROM rpr_plan
 WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
              PATTERN (A A+) DEFINE A AS val > 0);
 
+-- Consecutive VAR merge at the boundary: A{1073741823,} A{1073741823,} ->
+-- a{2147483646,}.  The min sum 2147483646 = INT32_MAX - 1 is the largest
+-- still-finite bound, so the merge proceeds; a sum of exactly INF instead
+-- falls back (see the Optimization Fallback Tests).
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN (A{1073741823,} A{1073741823,}) DEFINE A AS val > 0);
+
 -- Consecutive GROUP merge with finite quantifiers: ((A B){5}) ((A B){10}) -> merged
 EXPLAIN (COSTS OFF)
 SELECT COUNT(*) OVER w FROM rpr_plan
@@ -2406,6 +2415,15 @@ SELECT COUNT(*) OVER w FROM rpr_plan
 WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
              PATTERN ((A B){2} (A B)+) DEFINE A AS val <= 50, B AS val > 50);
 
+-- Consecutive GROUP merge at the boundary: (A B){1073741823,} (A B){1073741823,}
+-- -> (a b){2147483646,}.  The min sum INT32_MAX - 1 is still finite, so the
+-- merge proceeds; a sum of exactly INF instead falls back (see the
+-- Optimization Fallback Tests).
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN ((A B){1073741823,} (A B){1073741823,}) DEFINE A AS val <= 50, B AS val > 50);
+
 -- PREFIX merge: A B (A B)+ -> (a b){2,}
 EXPLAIN (COSTS OFF)
 SELECT COUNT(*) OVER w FROM rpr_plan
@@ -3213,6 +3231,31 @@ WINDOW w AS (
 );
 -- Expected: Fallback - prefix elements don't match GROUP content
 
+-- Test: consecutive VAR merge whose min sum is exactly INF causes fallback.
+-- 1073741824 + 1073741823 = 2147483647 = INT32_MAX = RPR_QUANTITY_INF.
+-- Merging would yield a VAR with min == INF, so the merge must fall back and
+-- leave the two VARs unmerged (mirrors the multiply path's >= INF guard).
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_fallback
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A{1073741824,} A{1073741823,})
+    DEFINE A AS val > 0
+);
+-- Expected: Fallback - VARs not merged (min sum 2147483647 == INF)
+
+-- Test: consecutive GROUP merge whose min sum is exactly INF causes fallback.
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_fallback
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ((A B){1073741824,} (A B){1073741823,})
+    DEFINE A AS val > 0, B AS val > 5
+);
+-- Expected: Fallback - GROUPs not merged (min sum 2147483647 == INF)
+
 DROP TABLE rpr_fallback;
 
 -- ============================================================
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0037-Fix-count-slot-leak-in-row-pattern-recognition-ab.txt (11.3K, 39-nocfbot-0037-Fix-count-slot-leak-in-row-pattern-recognition-ab.txt)
  download | inline diff:
From 176f477a820f40ab31cb34fcf4493de60b004579 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Mon, 1 Jun 2026 21:36:38 +0900
Subject: [PATCH 37/68] Fix count slot leak in row pattern recognition
 absorption

When a leaf variable in an absorbable group reaches its maximum count,
the match-phase inline advance walks it through the enclosing END
elements without clearing the variable's own counts[] slot, unlike the
regular exit in nfa_advance_var.  Sibling elements at the same depth
share a slot, so the stale count leaked into a following sibling group
and tripped the max-count Assert -- or, in a non-assert build, produced
a wrong match count.  PATTERN ((A B)+ (C D)+) is the smallest reproducer.

Clear the leaf variable's count on the inline exit.  Assert the
resulting contract -- each element zeroes its own slot on exit, so a
variable or group is entered with a zero count -- at the entry points,
and drop the now-redundant loop-back body reset.

Per a report from a static analysis tool.
---
 src/backend/executor/execRPR.c        | 55 +++++++++++++++++++++------
 src/test/regress/expected/rpr_nfa.out | 42 ++++++++++++++++++++
 src/test/regress/sql/rpr_nfa.sql      | 31 +++++++++++++++
 3 files changed, 117 insertions(+), 11 deletions(-)

diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 16a0f4ae375..9e45920de9c 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -315,8 +315,9 @@ nfa_states_equal(WindowAggState *winstate, RPRNFAState *s1, RPRNFAState *s2)
 	 * The +1 is the slot arithmetic: comparing through depth N requires
 	 * counts[0..N], i.e., N+1 entries.  Deeper slots (counts[d] with d >
 	 * elem->depth) are excluded because they hold scratch state from inner
-	 * groups that gets zeroed on re-entry (see END loop-back in
-	 * nfa_advance_end), and so must not participate in equivalence judgment.
+	 * groups.  Per the count-clear policy such a slot is zeroed when its
+	 * owning element exits (see nfa_advance_var and the inline fast path in
+	 * nfa_match), so it must not participate in equivalence judgment.
 	 */
 	elem = &pattern->elements[s1->elemIdx];
 	compareDepth = elem->depth + 1;
@@ -858,6 +859,16 @@ nfa_match(WindowAggState *winstate, RPRNFAContext *ctx, bool *varMatched)
 					state->elemIdx = elem->next;
 					state->counts[endDepth] = endCount;
 
+					/*
+					 * Leaf VAR exited (reached max): clear its own count so
+					 * the next occupant enters with zero, as nfa_advance_var
+					 * does on exit (this inline path replaces that exit).
+					 * depth > endDepth, so this leaves the group count just
+					 * written intact.
+					 */
+					Assert(endDepth < depth);
+					state->counts[depth] = 0;
+
 					/*
 					 * Chain through END elements within the absorbable region
 					 * (ABSORBABLE_BRANCH) until reaching the judgment point
@@ -873,7 +884,13 @@ nfa_match(WindowAggState *winstate, RPRNFAContext *ctx, bool *varMatched)
 						int			outerDepth = outerEnd->depth;
 						int32		outerCount = state->counts[outerDepth];
 
-						/* Reset exited group's count */
+						/*
+						 * Exit this intermediate group: clear its own count
+						 * (count-clear policy).  It sits below the absorbable
+						 * judgment point, so it is excluded from the
+						 * dominance comparison; the judgment point where the
+						 * chain stops keeps its count.
+						 */
 						state->counts[endDepth] = 0;
 
 						/* Increment outer group count */
@@ -925,6 +942,15 @@ nfa_route_to_elem(WindowAggState *winstate, RPRNFAContext *ctx,
 	{
 		RPRNFAState *skipState = NULL;
 
+		/*
+		 * Entry-side check of the count-clear policy: a VAR is always routed
+		 * to with a clean slot.  Each element zeroes its own count on exit,
+		 * so a nonzero count here would be a leak from an earlier element
+		 * (see nfa_advance_var / nfa_advance_end exit handling and the inline
+		 * fast path in nfa_match).
+		 */
+		Assert(state->counts[nextElem->depth] == 0);
+
 		/* Create skip state before add_unique, which may free state */
 		if (RPRElemCanSkip(nextElem))
 			skipState = nfa_state_clone(winstate, nextElem->next,
@@ -989,9 +1015,10 @@ nfa_advance_alt(WindowAggState *winstate, RPRNFAContext *ctx,
  * nfa_advance_begin
  *
  * Handle BEGIN element: group entry logic.
- * BEGIN is only visited at initial group entry (count is always 0).
+ * BEGIN is only visited at initial group entry; loop-back from END goes
+ * directly to first child, bypassing BEGIN.  Per the count-clear policy the
+ * group's own count slot is therefore already zero on entry (asserted below).
  * If min=0, creates a skip path past the group.
- * Loop-back from END goes directly to first child, bypassing BEGIN.
  */
 static void
 nfa_advance_begin(WindowAggState *winstate, RPRNFAContext *ctx,
@@ -1002,7 +1029,12 @@ nfa_advance_begin(WindowAggState *winstate, RPRNFAContext *ctx,
 	RPRPatternElement *elements = pattern->elements;
 	RPRNFAState *skipState = NULL;
 
-	state->counts[elem->depth] = 0;
+	/*
+	 * Entry-side check of the count-clear policy: the group's own count slot
+	 * is already zero here.  BEGIN is only visited at initial group entry,
+	 * and the previous occupant of this depth slot cleared it on exit.
+	 */
+	Assert(state->counts[elem->depth] == 0);
 
 	/* Optional group: create skip path (but don't route yet) */
 	if (elem->min == 0)
@@ -1094,8 +1126,6 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
 									  state->counts, state->isAbsorbable);
 
 		/* Primary path: loop back for real matches */
-		for (int d = depth + 1; d < pattern->maxDepth; d++)
-			state->counts[d] = 0;
 		state->elemIdx = elem->jump;
 		jumpElem = &elements[state->elemIdx];
 		nfa_route_to_elem(winstate, ctx, state, jumpElem,
@@ -1112,6 +1142,7 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
 		{
 			RPRPatternElement *nextElem;
 
+			/* Exit the group: clear its own count (count-clear policy) */
 			ffState->counts[depth] = 0;
 			ffState->elemIdx = elem->next;
 			nextElem = &elements[ffState->elemIdx];
@@ -1130,6 +1161,7 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
 		/* Must exit: reached max iterations. */
 		RPRPatternElement *nextElem;
 
+		/* Exit: clear the group's own count (count-clear policy) */
 		state->counts[depth] = 0;
 		state->elemIdx = elem->next;
 		nextElem = &elements[state->elemIdx];
@@ -1161,6 +1193,7 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
 		 */
 		exitState = nfa_state_clone(winstate, elem->next,
 									state->counts, state->isAbsorbable);
+		/* Exit branch: clear the group's own count (count-clear policy) */
 		exitState->counts[depth] = 0;
 		nextElem = &elements[exitState->elemIdx];
 
@@ -1169,8 +1202,6 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
 			exitState->counts[nextElem->depth]++;
 
 		/* Prepare loop state */
-		for (int d = depth + 1; d < pattern->maxDepth; d++)
-			state->counts[d] = 0;
 		state->elemIdx = elem->jump;
 		jumpElem = &elements[state->elemIdx];
 
@@ -1253,6 +1284,7 @@ nfa_advance_var(WindowAggState *winstate, RPRNFAContext *ctx,
 			/* Clone for exit, original stays for loop */
 			cloneState = nfa_state_clone(winstate, elem->next,
 										 state->counts, state->isAbsorbable);
+			/* Exit: clear the VAR's own count (count-clear policy) */
 			cloneState->counts[depth] = 0;
 			nextElem = &elements[cloneState->elemIdx];
 
@@ -1289,7 +1321,7 @@ nfa_advance_var(WindowAggState *winstate, RPRNFAContext *ctx,
 			/* Loop first (preferred for greedy) */
 			nfa_add_state_unique(winstate, ctx, cloneState);
 
-			/* Exit second */
+			/* Exit second: clear the VAR's own count (count-clear policy) */
 			state->counts[depth] = 0;
 			state->elemIdx = elem->next;
 			nextElem = &elements[state->elemIdx];
@@ -1326,6 +1358,7 @@ nfa_advance_var(WindowAggState *winstate, RPRNFAContext *ctx,
 		/* Exit only: advance to next element */
 		RPRPatternElement *nextElem;
 
+		/* Exit: clear the VAR's own count (count-clear policy) */
 		state->counts[depth] = 0;
 		state->elemIdx = elem->next;
 		nextElem = &elements[state->elemIdx];
diff --git a/src/test/regress/expected/rpr_nfa.out b/src/test/regress/expected/rpr_nfa.out
index 59b91ff9aa4..2a0d4f11e74 100644
--- a/src/test/regress/expected/rpr_nfa.out
+++ b/src/test/regress/expected/rpr_nfa.out
@@ -447,6 +447,48 @@ WINDOW w AS (
   7 | {X}   |             |          
 (7 rows)
 
+-- Two consecutive unbounded groups: (A B)+ (C D)+
+-- The leading group (A B)+ is absorbable (unbounded multi-element); (C D)+ is
+-- a distinct sibling group that does not merge with it.  When the leading group
+-- exits into the sibling, its body leaf-VAR count must be cleared so it does
+-- not leak into the sibling's shared depth slot.
+WITH test_absorb_two_groups AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['B']),
+        (3, ARRAY['C']),
+        (4, ARRAY['D']),
+        (5, ARRAY['A']),
+        (6, ARRAY['B']),
+        (7, ARRAY['C']),
+        (8, ARRAY['D'])
+    ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_absorb_two_groups
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A B)+ (C D)+)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags),
+        C AS 'C' = ANY(flags),
+        D AS 'D' = ANY(flags)
+);
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {A}   |           1 |         4
+  2 | {B}   |             |          
+  3 | {C}   |             |          
+  4 | {D}   |             |          
+  5 | {A}   |           5 |         8
+  6 | {B}   |             |          
+  7 | {C}   |             |          
+  8 | {D}   |             |          
+(8 rows)
+
 -- Fixed-length group absorption: (A B{2})+ C
 -- B{2} has min == max, equivalent to unrolling to (A B B)+ C
 WITH test_absorb_fixedlen AS (
diff --git a/src/test/regress/sql/rpr_nfa.sql b/src/test/regress/sql/rpr_nfa.sql
index febf834565d..6362c69f385 100644
--- a/src/test/regress/sql/rpr_nfa.sql
+++ b/src/test/regress/sql/rpr_nfa.sql
@@ -346,6 +346,37 @@ WINDOW w AS (
         B AS 'B' = ANY(flags)
 );
 
+-- Two consecutive unbounded groups: (A B)+ (C D)+
+-- The leading group (A B)+ is absorbable (unbounded multi-element); (C D)+ is
+-- a distinct sibling group that does not merge with it.  When the leading group
+-- exits into the sibling, its body leaf-VAR count must be cleared so it does
+-- not leak into the sibling's shared depth slot.
+WITH test_absorb_two_groups AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['B']),
+        (3, ARRAY['C']),
+        (4, ARRAY['D']),
+        (5, ARRAY['A']),
+        (6, ARRAY['B']),
+        (7, ARRAY['C']),
+        (8, ARRAY['D'])
+    ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_absorb_two_groups
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A B)+ (C D)+)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags),
+        C AS 'C' = ANY(flags),
+        D AS 'D' = ANY(flags)
+);
+
 -- Fixed-length group absorption: (A B{2})+ C
 -- B{2} has min == max, equivalent to unrolling to (A B B)+ C
 WITH test_absorb_fixedlen AS (
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0038-Demote-dead-runtime-checks-in-the-RPR-executor-to.txt (3.7K, 40-nocfbot-0038-Demote-dead-runtime-checks-in-the-RPR-executor-to.txt)
  download | inline diff:
From cd08e4296f499b9c3db3b7926a625f63455a815e Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Mon, 1 Jun 2026 22:15:04 +0900
Subject: [PATCH 38/68] Demote dead runtime checks in the RPR executor to
 assertions

Three checks in the row pattern recognition NFA executor can never fail,
given how the planner builds the pattern:

- The final arm of nfa_advance_var's loop/exit cascade tested canExit,
  but reaching it implies !canLoop, which with min <= max forces canExit
  true.
- nfa_state_clone guarded the counts memcpy with counts != NULL &&
  maxDepth > 0, but every caller passes a live state's counts and the
  stored maxDepth is always >= 1.
- nfa_advance_alt bounded the branch walk with altIdx < numElements, but
  every jump/next link is -1 or a valid index, and the altIdx >= 0 test
  and the depth break already terminate the walk.

Replace each with an assertion that documents the invariant and drop the
runtime branch.  Also document why the nullable fast-forward path in
nfa_advance_end intentionally omits the isAbsorbable update: it runs only
for EMPTY_LOOP groups, which are never inside an absorbable region.

Per a report from a static analysis tool.
---
 src/backend/executor/execRPR.c | 25 +++++++++++++++++++------
 1 file changed, 19 insertions(+), 6 deletions(-)

diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 9e45920de9c..eac0c04c38d 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -279,8 +279,9 @@ nfa_state_clone(WindowAggState *winstate, int16 elemIdx,
 	RPRPatternElement *elem = &pattern->elements[elemIdx];
 
 	state->elemIdx = elemIdx;
-	if (counts != NULL && maxDepth > 0)
-		memcpy(state->counts, counts, sizeof(int32) * maxDepth);
+	/* Every reachable caller passes a live state's counts; maxDepth >= 1. */
+	Assert(counts != NULL && maxDepth > 0);
+	memcpy(state->counts, counts, sizeof(int32) * maxDepth);
 
 	/*
 	 * Compute isAbsorbable immediately at transition time. isAbsorbable =
@@ -981,11 +982,15 @@ nfa_advance_alt(WindowAggState *winstate, RPRNFAContext *ctx,
 	RPRPatternElement *elements = pattern->elements;
 	RPRElemIdx	altIdx = elem->next;
 
-	while (altIdx >= 0 && altIdx < pattern->numElements)
+	while (altIdx >= 0)
 	{
-		RPRPatternElement *altElem = &elements[altIdx];
+		RPRPatternElement *altElem;
 		RPRNFAState *newState;
 
+		/* Branch jump/next links are always -1 or a valid index */
+		Assert(altIdx < pattern->numElements);
+		altElem = &elements[altIdx];
+
 		/*
 		 * Stop if element is outside ALT scope (not a branch).  The check
 		 * fires when the last branch is a quantified group whose BEGIN.jump
@@ -1147,6 +1152,13 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
 			ffState->elemIdx = elem->next;
 			nextElem = &elements[ffState->elemIdx];
 
+			/*
+			 * Unlike the must-exit path, no isAbsorbable update is needed:
+			 * the fast-forward path runs only for EMPTY_LOOP (nullable)
+			 * groups, which are never inside an absorbable region, so
+			 * isAbsorbable is already false here.
+			 */
+
 			/* END->END: increment outer END's count */
 			if (RPRElemIsEnd(nextElem) &&
 				ffState->counts[nextElem->depth] < RPR_COUNT_MAX)
@@ -1353,11 +1365,12 @@ nfa_advance_var(WindowAggState *winstate, RPRNFAContext *ctx,
 		/* Loop only: keep state as-is */
 		nfa_add_state_unique(winstate, ctx, state);
 	}
-	else if (canExit)
+	else
 	{
-		/* Exit only: advance to next element */
+		/* Exit only: advance to next element (canExit necessarily true) */
 		RPRPatternElement *nextElem;
 
+		Assert(canExit);
 		/* Exit: clear the VAR's own count (count-clear policy) */
 		state->counts[depth] = 0;
 		state->elemIdx = elem->next;
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0040-Honor-reluctant-quantifier-for-non-leading-option.txt (7.7K, 41-nocfbot-0040-Honor-reluctant-quantifier-for-non-leading-option.txt)
  download | inline diff:
From fb200f06d1202c13b85935c40d2a5d80c1da584e Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Mon, 1 Jun 2026 23:49:46 +0900
Subject: [PATCH 40/68] Honor reluctant quantifier for non-leading optional RPR
 variables

A reluctant optional pattern variable that is not the leading element --
for example A?? in PATTERN (B A?? C) -- matched greedily.  nfa_route_to_elem
builds the skip path of a min=0 variable in a fixed order, adding the enter
(match) state before the skip state.  Since the state list order encodes
match preference, the enter path always won, ignoring RPRElemIsReluctant.
Leading variables and optional groups were unaffected: they go through
nfa_advance_var / nfa_advance_begin, which already order the skip path first
when reluctant.

Mirror that handling in nfa_route_to_elem: when the optional variable is
reluctant, route the skip path first and drop the enter state if the skip
path has already reached FIN.

Add regression tests for non-leading reluctant optional variables and groups.

Per a report from a static analysis tool.
---
 src/backend/executor/execRPR.c        | 29 ++++++++++-
 src/test/regress/expected/rpr_nfa.out | 69 +++++++++++++++++++++++++++
 src/test/regress/sql/rpr_nfa.sql      | 56 ++++++++++++++++++++++
 3 files changed, 152 insertions(+), 2 deletions(-)

diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index a2c304da0d1..580b25f398d 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -957,10 +957,35 @@ nfa_route_to_elem(WindowAggState *winstate, RPRNFAContext *ctx,
 			skipState = nfa_state_clone(winstate, nextElem->next,
 										state->counts, state->isAbsorbable);
 
-		nfa_add_state_unique(winstate, ctx, state);
+		if (skipState != NULL && RPRElemIsReluctant(nextElem))
+		{
+			RPRNFAState *savedMatch = ctx->matchedState;
 
-		if (skipState != NULL)
+			/*
+			 * Reluctant optional VAR: prefer skipping.  Explore the skip path
+			 * first so it outranks the enter (match) path; if it reaches FIN
+			 * the shortest match is found and the enter state is dropped.
+			 * This mirrors the reluctant branch of nfa_advance_begin used by
+			 * the leading-position and optional-group paths.
+			 */
 			nfa_advance_state(winstate, ctx, skipState, currentPos);
+
+			if (ctx->matchedState != savedMatch)
+			{
+				nfa_state_free(winstate, state);
+				return;
+			}
+
+			nfa_add_state_unique(winstate, ctx, state);
+		}
+		else
+		{
+			/* Greedy (or non-skippable): enter first, then skip */
+			nfa_add_state_unique(winstate, ctx, state);
+
+			if (skipState != NULL)
+				nfa_advance_state(winstate, ctx, skipState, currentPos);
+		}
 	}
 	else
 	{
diff --git a/src/test/regress/expected/rpr_nfa.out b/src/test/regress/expected/rpr_nfa.out
index 2a0d4f11e74..829e8251aed 100644
--- a/src/test/regress/expected/rpr_nfa.out
+++ b/src/test/regress/expected/rpr_nfa.out
@@ -1222,6 +1222,75 @@ WINDOW w AS (
   3 | {C}   |             |          
 (3 rows)
 
+-- Non-leading reluctant optional VAR: (B A?? C)
+-- Reluctant A?? should prefer to skip, matching B(1) C(2) with A left
+-- unmatched (match_end 2).  The leading/group reluctant cases above go through
+-- the begin path; this exercises the non-leading skip path in
+-- nfa_route_to_elem, which must honor reluctant ordering too.
+WITH test_nonleading_reluctant AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['B']),
+        (2, ARRAY['A', 'C']),
+        (3, ARRAY['C'])
+    ) AS t(id, flags)
+)
+SELECT id, flags,
+       first_value(id) OVER w AS match_start,
+       last_value(id) OVER w AS match_end
+FROM test_nonleading_reluctant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (B A?? C)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags),
+        C AS 'C' = ANY(flags)
+);
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {B}   |           1 |         2
+  2 | {A,C} |             |          
+  3 | {C}   |             |          
+(3 rows)
+
+-- Non-leading reluctant optional GROUP with a follower: (B (A X)?? C)
+-- Like the VAR case above but a multi-element group; it goes through the
+-- begin path (nfa_advance_begin), which already honors reluctant ordering.
+-- Reluctant (A X)?? should skip, matching B(1) C(2), with the group skipped
+-- to the following C (not to FIN).
+WITH test_nonleading_reluctant_group AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['B']),
+        (2, ARRAY['A', 'C']),
+        (3, ARRAY['X']),
+        (4, ARRAY['C'])
+    ) AS t(id, flags)
+)
+SELECT id, flags,
+       first_value(id) OVER w AS match_start,
+       last_value(id) OVER w AS match_end
+FROM test_nonleading_reluctant_group
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (B (A X)?? C)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        X AS 'X' = ANY(flags),
+        B AS 'B' = ANY(flags),
+        C AS 'C' = ANY(flags)
+);
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {B}   |           1 |         2
+  2 | {A,C} |             |          
+  3 | {X}   |             |          
+  4 | {C}   |             |          
+(4 rows)
+
 -- Greedy/reluctant sequence: A+ B+? (greedy A, reluctant B at end)
 -- A consumes greedily, B+? exits to FIN after minimum match
 WITH test_greedy_then_reluctant AS (
diff --git a/src/test/regress/sql/rpr_nfa.sql b/src/test/regress/sql/rpr_nfa.sql
index 6362c69f385..3bbec496279 100644
--- a/src/test/regress/sql/rpr_nfa.sql
+++ b/src/test/regress/sql/rpr_nfa.sql
@@ -860,6 +860,62 @@ WINDOW w AS (
         C AS 'C' = ANY(flags)
 );
 
+-- Non-leading reluctant optional VAR: (B A?? C)
+-- Reluctant A?? should prefer to skip, matching B(1) C(2) with A left
+-- unmatched (match_end 2).  The leading/group reluctant cases above go through
+-- the begin path; this exercises the non-leading skip path in
+-- nfa_route_to_elem, which must honor reluctant ordering too.
+WITH test_nonleading_reluctant AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['B']),
+        (2, ARRAY['A', 'C']),
+        (3, ARRAY['C'])
+    ) AS t(id, flags)
+)
+SELECT id, flags,
+       first_value(id) OVER w AS match_start,
+       last_value(id) OVER w AS match_end
+FROM test_nonleading_reluctant
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (B A?? C)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags),
+        C AS 'C' = ANY(flags)
+);
+
+-- Non-leading reluctant optional GROUP with a follower: (B (A X)?? C)
+-- Like the VAR case above but a multi-element group; it goes through the
+-- begin path (nfa_advance_begin), which already honors reluctant ordering.
+-- Reluctant (A X)?? should skip, matching B(1) C(2), with the group skipped
+-- to the following C (not to FIN).
+WITH test_nonleading_reluctant_group AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['B']),
+        (2, ARRAY['A', 'C']),
+        (3, ARRAY['X']),
+        (4, ARRAY['C'])
+    ) AS t(id, flags)
+)
+SELECT id, flags,
+       first_value(id) OVER w AS match_start,
+       last_value(id) OVER w AS match_end
+FROM test_nonleading_reluctant_group
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN (B (A X)?? C)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        X AS 'X' = ANY(flags),
+        B AS 'B' = ANY(flags),
+        C AS 'C' = ANY(flags)
+);
+
 -- Greedy/reluctant sequence: A+ B+? (greedy A, reluctant B at end)
 -- A consumes greedily, B+? exits to FIN after minimum match
 WITH test_greedy_then_reluctant AS (
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0039-Fix-memory-leak-in-row-pattern-recognition-DEFINE.txt (2.8K, 42-nocfbot-0039-Fix-memory-leak-in-row-pattern-recognition-DEFINE.txt)
  download | inline diff:
From 30e1c3dcea5abe8b0909d047550eb589e8e0a248 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Mon, 1 Jun 2026 22:38:55 +0900
Subject: [PATCH 39/68] Fix memory leak in row pattern recognition DEFINE
 evaluation

The row pattern recognition scan evaluates every DEFINE expression once
per partition row in update_reduced_frame, but did so with the plain
ExecEvalExpr in the long-lived query context and never reset it.  By-ref
scratch -- a NUMERIC DEFINE such as price * 1.1 > 165 is the typical case
-- therefore accumulated for the whole forward scan, growing memory
O(partition size) within a single window.

Evaluate the DEFINE expressions in the per-tuple memory context with
ExecEvalExprSwitchContext, in both nfa_evaluate_row and
nfa_reevaluate_dependent_vars, and reset that context once per processed
row in the scan loop.  The boolean results are consumed immediately, and
the cross-row state -- nfaVarMatched in the node context and the NFA
states in the partition context -- lives elsewhere, so the reset is safe.

Per a report from a static analysis tool.
---
 src/backend/executor/execRPR.c       |  3 ++-
 src/backend/executor/nodeWindowAgg.c | 10 ++++++++--
 2 files changed, 10 insertions(+), 3 deletions(-)

diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index eac0c04c38d..a2c304da0d1 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -1706,7 +1706,8 @@ nfa_reevaluate_dependent_vars(WindowAggState *winstate, RPRNFAContext *ctx,
 			Datum		result;
 			bool		isnull;
 
-			result = ExecEvalExpr(exprState, econtext, &isnull);
+			/* Per-tuple context; scratch freed by the per-row reset */
+			result = ExecEvalExprSwitchContext(exprState, econtext, &isnull);
 			winstate->nfaVarMatched[varIdx] = (!isnull && DatumGetBool(result));
 		}
 
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 0b0196e7e40..408bbc120b7 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -4515,6 +4515,12 @@ update_reduced_frame(WindowObject winobj, int64 pos)
 		 * appropriately as pruned or mismatched.
 		 */
 		ExecRPRCleanupDeadContexts(winstate, targetCtx);
+
+		/*
+		 * Free this row's per-tuple DEFINE-evaluation scratch; cross-row
+		 * state (nfaVarMatched, NFA states) lives in other contexts.
+		 */
+		ResetExprContext(winstate->ss.ps.ps_ExprContext);
 	}
 
 register_result:
@@ -4606,8 +4612,8 @@ nfa_evaluate_row(WindowObject winobj, int64 pos, bool *varMatched)
 		Datum		result;
 		bool		isnull;
 
-		/* Evaluate DEFINE expression */
-		result = ExecEvalExpr(exprState, econtext, &isnull);
+		/* Per-tuple context so by-ref scratch is freed by the per-row reset */
+		result = ExecEvalExprSwitchContext(exprState, econtext, &isnull);
 
 		varMatched[varIdx] = (!isnull && DatumGetBool(result));
 
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0041-Reject-column-less-compound-navigation-in-row-pat.txt (6.6K, 43-nocfbot-0041-Reject-column-less-compound-navigation-in-row-pat.txt)
  download | inline diff:
From 8112fe3f2c1bee4fc330df0b12d737abd4d27903 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Tue, 2 Jun 2026 00:05:05 +0900
Subject: [PATCH 41/68] Reject column-less compound navigation in row pattern
 recognition

A row pattern navigation argument must contain at least one column
reference.  The check was applied only to simple navigation; a compound
form such as PREV(FIRST(1)) took the nesting-validation/flatten branch in
parse_rpr.c's define_walker, which never re-checked has_column_ref.  So
column-less compound navigation -- PREV(FIRST(1)), NEXT(LAST(1 + 2)),
PREV(FIRST(1), 2), and so on -- was silently accepted, while the simple
PREV(1) was correctly rejected.

Apply the same has_column_ref check after flattening a compound
navigation, so both forms are rejected consistently.  A compound form
whose argument references a column, such as PREV(FIRST(price)), is
unaffected.

Add regression tests covering column-less compound navigation with the
offset on the inner nav, the outer nav, both, or neither.

Per a report from a static analysis tool.
---
 src/backend/parser/parse_rpr.c    | 10 +++++
 src/test/regress/expected/rpr.out | 62 +++++++++++++++++++++++++++++++
 src/test/regress/sql/rpr.sql      | 52 ++++++++++++++++++++++++++
 3 files changed, 124 insertions(+)

diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index fa8c375f48b..6f44edfc50b 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -641,6 +641,16 @@ define_walker(Node *node, void *context)
 					nav->offset_arg = inner->offset_arg;
 					nav->arg = inner->arg;
 					flattened = true;
+
+					/*
+					 * The flattened argument must include a column reference,
+					 * just like the simple-nav case below.
+					 */
+					if (!ctx->has_column_ref)
+						ereport(ERROR,
+								errcode(ERRCODE_SYNTAX_ERROR),
+								errmsg("argument of row pattern navigation operation must include at least one column reference"),
+								parser_errposition(ctx->pstate, nav->location));
 				}
 				else if (!outer_phys && inner_phys)
 					ereport(ERROR,
diff --git a/src/test/regress/expected/rpr.out b/src/test/regress/expected/rpr.out
index 8793dda3cc3..550113700a9 100644
--- a/src/test/regress/expected/rpr.out
+++ b/src/test/regress/expected/rpr.out
@@ -1133,6 +1133,68 @@ WINDOW w AS (
 ERROR:  argument of row pattern navigation operation must include at least one column reference
 LINE 7:     DEFINE A AS PREV(1, 1) > 0
                         ^
+-- Compound navigation without a column reference must be rejected too,
+-- consistent with the simple forms above.
+-- PREV(FIRST(1)): compound, constant only, no column reference
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS PREV(FIRST(1)) > 0
+);
+ERROR:  argument of row pattern navigation operation must include at least one column reference
+LINE 7:     DEFINE A AS PREV(FIRST(1)) > 0
+                        ^
+-- NEXT(LAST(1 + 2)): compound, constant expression, no column reference
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS NEXT(LAST(1 + 2)) > 0
+);
+ERROR:  argument of row pattern navigation operation must include at least one column reference
+LINE 7:     DEFINE A AS NEXT(LAST(1 + 2)) > 0
+                        ^
+-- PREV(FIRST(1, 2)): compound, two-arg inner, no column reference
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS PREV(FIRST(1, 2)) > 0
+);
+ERROR:  argument of row pattern navigation operation must include at least one column reference
+LINE 7:     DEFINE A AS PREV(FIRST(1, 2)) > 0
+                        ^
+-- PREV(FIRST(1), 2): compound, outer offset only, no column reference
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS PREV(FIRST(1), 2) > 0
+);
+ERROR:  argument of row pattern navigation operation must include at least one column reference
+LINE 7:     DEFINE A AS PREV(FIRST(1), 2) > 0
+                        ^
+-- PREV(FIRST(1, 2), 3): compound, inner and outer offsets, no column reference
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS PREV(FIRST(1, 2), 3) > 0
+);
+ERROR:  argument of row pattern navigation operation must include at least one column reference
+LINE 7:     DEFINE A AS PREV(FIRST(1, 2), 3) > 0
+                        ^
 -- Non-constant offset: column reference as offset
 SELECT price FROM stock
 WINDOW w AS (
diff --git a/src/test/regress/sql/rpr.sql b/src/test/regress/sql/rpr.sql
index e4790f75b0a..0aa17f01e84 100644
--- a/src/test/regress/sql/rpr.sql
+++ b/src/test/regress/sql/rpr.sql
@@ -531,6 +531,58 @@ WINDOW w AS (
     DEFINE A AS PREV(1, 1) > 0
 );
 
+-- Compound navigation without a column reference must be rejected too,
+-- consistent with the simple forms above.
+-- PREV(FIRST(1)): compound, constant only, no column reference
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS PREV(FIRST(1)) > 0
+);
+
+-- NEXT(LAST(1 + 2)): compound, constant expression, no column reference
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS NEXT(LAST(1 + 2)) > 0
+);
+
+-- PREV(FIRST(1, 2)): compound, two-arg inner, no column reference
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS PREV(FIRST(1, 2)) > 0
+);
+
+-- PREV(FIRST(1), 2): compound, outer offset only, no column reference
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS PREV(FIRST(1), 2) > 0
+);
+
+-- PREV(FIRST(1, 2), 3): compound, inner and outer offsets, no column reference
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS PREV(FIRST(1, 2), 3) > 0
+);
+
 -- Non-constant offset: column reference as offset
 SELECT price FROM stock
 WINDOW w AS (
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0042-Reserve-the-high-varId-nibble-for-row-pattern-rec.txt (23.6K, 44-nocfbot-0042-Reserve-the-high-varId-nibble-for-row-pattern-rec.txt)
  download | inline diff:
From 7f127147ae7fd860dc71d22ac539ea7644a77c6f Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Wed, 3 Jun 2026 20:10:59 +0900
Subject: [PATCH 42/68] Reserve the high varId nibble for row pattern
 recognition control elements

Row pattern recognition packs both pattern variables and pattern control
elements (group begin/end, alternation, finish) into a single uint8 varId.
Reserve the entire high nibble, the 16 values 0xF0-0xFF, for control
elements, so pattern variables use varId 0 to 0xEF and at most 240 distinct
variables are allowed.

Reserving all 16 high-nibble values, rather than only the ones currently
needed, leaves spare encodings for future control elements.  RPR_VARID_MAX
denotes the maximum variable varId (0xEF), and the parser limit check, the
bounds assertions, and the variable-name stack are sized from it.  The
boundary regression test exercises the 240/241 limits.
---
 src/backend/optimizer/plan/rpr.c       |  6 +++---
 src/backend/parser/parse_rpr.c         | 18 ++++++++++++------
 src/include/optimizer/rpr.h            | 20 ++++++++++++--------
 src/test/regress/expected/rpr_base.out | 20 ++++++++------------
 src/test/regress/sql/rpr_base.sql      | 18 +++++++-----------
 5 files changed, 42 insertions(+), 40 deletions(-)

diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index b989fcc5162..4652ca6ebeb 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -1017,7 +1017,7 @@ collectDefineVariables(List *defineVariableList, char **varNames)
 	foreach(lc, defineVariableList)
 	{
 		/* Parser already checked this limit in transformDefineClause */
-		Assert(numVars < RPR_VARID_MAX);
+		Assert(numVars <= RPR_VARID_MAX);
 
 		varNames[numVars++] = strVal(lfirst(lc));
 	}
@@ -1076,7 +1076,7 @@ scanRPRPatternRecursive(RPRPatternNode *node, char **varNames, int *numVars,
 			 * to varNames so they get a varId >= defineVariableList length,
 			 * which executor treats as TRUE.
 			 */
-			Assert(*numVars < RPR_VARID_MAX);
+			Assert(*numVars <= RPR_VARID_MAX);
 			varNames[(*numVars)++] = node->varName;
 			break;
 
@@ -1954,7 +1954,7 @@ buildRPRPattern(RPRPatternNode *pattern, List *defineVariableList,
 {
 	RPRPattern *result;
 	RPRPatternNode *optimized;
-	char	   *varNamesStack[RPR_VARID_MAX];
+	char	   *varNamesStack[RPR_VARID_MAX + 1];
 	int			numVars;
 	int			numElements;
 	RPRDepth	maxDepth;
diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index 6f44edfc50b..3872035110a 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -6,7 +6,7 @@
  * This file transforms RPR-related clauses from raw parse tree to planner
  * structures during query analysis:
  *   - Validates frame options (must start at CURRENT ROW, no EXCLUDE)
- *   - Validates PATTERN variable count (max RPR_VARID_MAX)
+ *   - Validates PATTERN variable count (max RPR_VARID_MAX + 1)
  *   - Transforms DEFINE clause into TargetEntry list
  *   - Stores PATTERN/SKIP TO/INITIAL clauses for planner
  *
@@ -192,10 +192,11 @@ transformRPR(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 
 /*
  * validateRPRPatternVarCount
- *		Validate that PATTERN variables don't exceed RPR_VARID_MAX.
+ *		Validate that PATTERN variable count fits the varId range.
  *
  * Recursively traverses the pattern tree, collecting unique variable names.
- * Throws an error if the number of unique variables exceeds RPR_VARID_MAX.
+ * Throws an error if the number of unique variables would require a varId
+ * greater than RPR_VARID_MAX.
  *
  * If rpDefs is non-NULL, each DEFINE variable name is also validated against
  * varNames; any DEFINE name not present in PATTERN is rejected with an error.
@@ -231,12 +232,17 @@ validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
 				}
 				if (!found)
 				{
-					/* Check against RPR_VARID_MAX before adding */
-					if (list_length(*varNames) >= RPR_VARID_MAX)
+					/*
+					 * Check against RPR_VARID_MAX before adding.  varId
+					 * values run 0 to RPR_VARID_MAX inclusive, so the next
+					 * varId to be assigned (the current list length) must not
+					 * exceed it.
+					 */
+					if (list_length(*varNames) > RPR_VARID_MAX)
 						ereport(ERROR,
 								errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
 								errmsg("too many pattern variables"),
-								errdetail("Maximum is %d.", RPR_VARID_MAX),
+								errdetail("Maximum is %d.", RPR_VARID_MAX + 1),
 								parser_errposition(pstate,
 												   exprLocation((Node *) node)));
 
diff --git a/src/include/optimizer/rpr.h b/src/include/optimizer/rpr.h
index 63c4b09daff..2fa483320ce 100644
--- a/src/include/optimizer/rpr.h
+++ b/src/include/optimizer/rpr.h
@@ -18,10 +18,14 @@
 
 /* Limits and special values */
 /*
- * Maximum number of unique pattern variables (varId 0 to RPR_VARID_MAX - 1).
- * Values from RPR_VARID_BEGIN (252) onward are reserved for control elements.
+ * Maximum pattern variable ID.  Pattern variables occupy varId 0 to
+ * RPR_VARID_MAX inclusive (240 distinct variables); any varId with the high
+ * nibble set (0xF0 to 0xFF) is reserved for control elements.  Reserving the
+ * whole high nibble, rather than just the values currently in use, leaves
+ * room for future control elements; this range can only be narrowed safely
+ * before release.
  */
-#define RPR_VARID_MAX		251
+#define RPR_VARID_MAX		0xEF	/* pattern variables are 0 to 0xEF */
 #define RPR_QUANTITY_INF	INT32_MAX	/* unbounded quantifier */
 #define RPR_COUNT_MAX		INT32_MAX	/* max runtime count (NFA state) */
 #define RPR_ELEMIDX_MAX		INT16_MAX	/* max pattern elements */
@@ -29,11 +33,11 @@
 #define RPR_DEPTH_MAX		(UINT8_MAX - 1) /* max pattern nesting depth: 254 */
 #define RPR_DEPTH_NONE		UINT8_MAX	/* no enclosing group (top-level) */
 
-/* Special varId values for control elements (252-255) */
-#define RPR_VARID_BEGIN		((RPRVarId) 252)	/* group begin */
-#define RPR_VARID_END		((RPRVarId) 253)	/* group end */
-#define RPR_VARID_ALT		((RPRVarId) 254)	/* alternation start */
-#define RPR_VARID_FIN		((RPRVarId) 255)	/* pattern finish */
+/* Reserved control-element varIds (high nibble 0xF; 0xF0-0xFB spare) */
+#define RPR_VARID_BEGIN		((RPRVarId) 0xFC)	/* group begin */
+#define RPR_VARID_END		((RPRVarId) 0xFD)	/* group end */
+#define RPR_VARID_ALT		((RPRVarId) 0xFE)	/* alternation start */
+#define RPR_VARID_FIN		((RPRVarId) 0xFF)	/* pattern finish */
 
 /* Element flags */
 #define RPR_ELEM_RELUCTANT			0x01	/* reluctant (non-greedy)
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index ebc1088018a..e9303af6384 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -6280,12 +6280,12 @@ ERROR:  DEFINE variable "b" is not used in PATTERN
 LINE 7:       B AS TRUE
               ^
 -- Expected: Error - B is not used in PATTERN
--- Test: 251 variables in PATTERN and DEFINE (boundary - should succeed)
+-- Test: 240 variables in PATTERN and DEFINE (boundary - should succeed)
 SELECT COUNT(*) OVER w FROM rpr_errors
 WINDOW w AS (
     ORDER BY id
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    PATTERN (V1 V2 V3 V4 V5 V6 V7 V8 V9 V10 V11 V12 V13 V14 V15 V16 V17 V18 V19 V20 V21 V22 V23 V24 V25 V26 V27 V28 V29 V30 V31 V32 V33 V34 V35 V36 V37 V38 V39 V40 V41 V42 V43 V44 V45 V46 V47 V48 V49 V50 V51 V52 V53 V54 V55 V56 V57 V58 V59 V60 V61 V62 V63 V64 V65 V66 V67 V68 V69 V70 V71 V72 V73 V74 V75 V76 V77 V78 V79 V80 V81 V82 V83 V84 V85 V86 V87 V88 V89 V90 V91 V92 V93 V94 V95 V96 V97 V98 V99 V100 V101 V102 V103 V104 V105 V106 V107 V108 V109 V110 V111 V112 V113 V114 V115 V116 V117 V118 V119 V120 V121 V122 V123 V124 V125 V126 V127 V128 V129 V130 V131 V132 V133 V134 V135 V136 V137 V138 V139 V140 V141 V142 V143 V144 V145 V146 V147 V148 V149 V150 V151 V152 V153 V154 V155 V156 V157 V158 V159 V160 V161 V162 V163 V164 V165 V166 V167 V168 V169 V170 V171 V172 V173 V174 V175 V176 V177 V178 V179 V180 V181 V182 V183 V184 V185 V186 V187 V188 V189 V190 V191 V192 V193 V194 V195 V196 V197 V198 V199 V200 V201 V202 V203 V204 V205 V206 V207 V208 V209 V210 V211 V212 V213 V214 V215 V216 V217 V218 V219 V220 V221 V222 V223 V224 V225 V226 V227 V228 V229 V230 V231 V232 V233 V234 V235 V236 V237 V238 V239 V240 V241 V242 V243 V244 V245 V246 V247 V248 V249 V250 V251)
+    PATTERN (V1 V2 V3 V4 V5 V6 V7 V8 V9 V10 V11 V12 V13 V14 V15 V16 V17 V18 V19 V20 V21 V22 V23 V24 V25 V26 V27 V28 V29 V30 V31 V32 V33 V34 V35 V36 V37 V38 V39 V40 V41 V42 V43 V44 V45 V46 V47 V48 V49 V50 V51 V52 V53 V54 V55 V56 V57 V58 V59 V60 V61 V62 V63 V64 V65 V66 V67 V68 V69 V70 V71 V72 V73 V74 V75 V76 V77 V78 V79 V80 V81 V82 V83 V84 V85 V86 V87 V88 V89 V90 V91 V92 V93 V94 V95 V96 V97 V98 V99 V100 V101 V102 V103 V104 V105 V106 V107 V108 V109 V110 V111 V112 V113 V114 V115 V116 V117 V118 V119 V120 V121 V122 V123 V124 V125 V126 V127 V128 V129 V130 V131 V132 V133 V134 V135 V136 V137 V138 V139 V140 V141 V142 V143 V144 V145 V146 V147 V148 V149 V150 V151 V152 V153 V154 V155 V156 V157 V158 V159 V160 V161 V162 V163 V164 V165 V166 V167 V168 V169 V170 V171 V172 V173 V174 V175 V176 V177 V178 V179 V180 V181 V182 V183 V184 V185 V186 V187 V188 V189 V190 V191 V192 V193 V194 V195 V196 V197 V198 V199 V200 V201 V202 V203 V204 V205 V206 V207 V208 V209 V210 V211 V212 V213 V214 V215 V216 V217 V218 V219 V220 V221 V222 V223 V224 V225 V226 V227 V228 V229 V230 V231 V232 V233 V234 V235 V236 V237 V238 V239 V240)
     DEFINE
     V1 AS val > 0, V2 AS val > 0, V3 AS val > 0, V4 AS val > 0, V5 AS val > 0, V6 AS val > 0, V7 AS val > 0, V8 AS val > 0, V9 AS val > 0, V10 AS val > 0,
     V11 AS val > 0, V12 AS val > 0, V13 AS val > 0, V14 AS val > 0, V15 AS val > 0, V16 AS val > 0, V17 AS val > 0, V18 AS val > 0, V19 AS val > 0, V20 AS val > 0,
@@ -6310,9 +6310,7 @@ WINDOW w AS (
     V201 AS val > 0, V202 AS val > 0, V203 AS val > 0, V204 AS val > 0, V205 AS val > 0, V206 AS val > 0, V207 AS val > 0, V208 AS val > 0, V209 AS val > 0, V210 AS val > 0,
     V211 AS val > 0, V212 AS val > 0, V213 AS val > 0, V214 AS val > 0, V215 AS val > 0, V216 AS val > 0, V217 AS val > 0, V218 AS val > 0, V219 AS val > 0, V220 AS val > 0,
     V221 AS val > 0, V222 AS val > 0, V223 AS val > 0, V224 AS val > 0, V225 AS val > 0, V226 AS val > 0, V227 AS val > 0, V228 AS val > 0, V229 AS val > 0, V230 AS val > 0,
-    V231 AS val > 0, V232 AS val > 0, V233 AS val > 0, V234 AS val > 0, V235 AS val > 0, V236 AS val > 0, V237 AS val > 0, V238 AS val > 0, V239 AS val > 0, V240 AS val > 0,
-    V241 AS val > 0, V242 AS val > 0, V243 AS val > 0, V244 AS val > 0, V245 AS val > 0, V246 AS val > 0, V247 AS val > 0, V248 AS val > 0, V249 AS val > 0, V250 AS val > 0,
-    V251 AS val > 0
+    V231 AS val > 0, V232 AS val > 0, V233 AS val > 0, V234 AS val > 0, V235 AS val > 0, V236 AS val > 0, V237 AS val > 0, V238 AS val > 0, V239 AS val > 0, V240 AS val > 0
 );
  count 
 -------
@@ -6321,12 +6319,12 @@ WINDOW w AS (
 (2 rows)
 
 -- Expected: Success - exactly at RPR_VARID_MAX boundary
--- Test: 252 variables in PATTERN, 251 in DEFINE (exceeds limit with implicit TRUE)
+-- Test: 241 variables in PATTERN, 240 in DEFINE (exceeds limit with implicit TRUE)
 SELECT COUNT(*) OVER w FROM rpr_errors
 WINDOW w AS (
     ORDER BY id
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    PATTERN (V1 V2 V3 V4 V5 V6 V7 V8 V9 V10 V11 V12 V13 V14 V15 V16 V17 V18 V19 V20 V21 V22 V23 V24 V25 V26 V27 V28 V29 V30 V31 V32 V33 V34 V35 V36 V37 V38 V39 V40 V41 V42 V43 V44 V45 V46 V47 V48 V49 V50 V51 V52 V53 V54 V55 V56 V57 V58 V59 V60 V61 V62 V63 V64 V65 V66 V67 V68 V69 V70 V71 V72 V73 V74 V75 V76 V77 V78 V79 V80 V81 V82 V83 V84 V85 V86 V87 V88 V89 V90 V91 V92 V93 V94 V95 V96 V97 V98 V99 V100 V101 V102 V103 V104 V105 V106 V107 V108 V109 V110 V111 V112 V113 V114 V115 V116 V117 V118 V119 V120 V121 V122 V123 V124 V125 V126 V127 V128 V129 V130 V131 V132 V133 V134 V135 V136 V137 V138 V139 V140 V141 V142 V143 V144 V145 V146 V147 V148 V149 V150 V151 V152 V153 V154 V155 V156 V157 V158 V159 V160 V161 V162 V163 V164 V165 V166 V167 V168 V169 V170 V171 V172 V173 V174 V175 V176 V177 V178 V179 V180 V181 V182 V183 V184 V185 V186 V187 V188 V189 V190 V191 V192 V193 V194 V195 V196 V197 V198 V199 V200 V201 V202 V203 V204 V205 V206 V207 V208 V209 V210 V211 V212 V213 V214 V215 V216 V217 V218 V219 V220 V221 V222 V223 V224 V225 V226 V227 V228 V229 V230 V231 V232 V233 V234 V235 V236 V237 V238 V239 V240 V241 V242 V243 V244 V245 V246 V247 V248 V249 V250 V251 V252)
+    PATTERN (V1 V2 V3 V4 V5 V6 V7 V8 V9 V10 V11 V12 V13 V14 V15 V16 V17 V18 V19 V20 V21 V22 V23 V24 V25 V26 V27 V28 V29 V30 V31 V32 V33 V34 V35 V36 V37 V38 V39 V40 V41 V42 V43 V44 V45 V46 V47 V48 V49 V50 V51 V52 V53 V54 V55 V56 V57 V58 V59 V60 V61 V62 V63 V64 V65 V66 V67 V68 V69 V70 V71 V72 V73 V74 V75 V76 V77 V78 V79 V80 V81 V82 V83 V84 V85 V86 V87 V88 V89 V90 V91 V92 V93 V94 V95 V96 V97 V98 V99 V100 V101 V102 V103 V104 V105 V106 V107 V108 V109 V110 V111 V112 V113 V114 V115 V116 V117 V118 V119 V120 V121 V122 V123 V124 V125 V126 V127 V128 V129 V130 V131 V132 V133 V134 V135 V136 V137 V138 V139 V140 V141 V142 V143 V144 V145 V146 V147 V148 V149 V150 V151 V152 V153 V154 V155 V156 V157 V158 V159 V160 V161 V162 V163 V164 V165 V166 V167 V168 V169 V170 V171 V172 V173 V174 V175 V176 V177 V178 V179 V180 V181 V182 V183 V184 V185 V186 V187 V188 V189 V190 V191 V192 V193 V194 V195 V196 V197 V198 V199 V200 V201 V202 V203 V204 V205 V206 V207 V208 V209 V210 V211 V212 V213 V214 V215 V216 V217 V218 V219 V220 V221 V222 V223 V224 V225 V226 V227 V228 V229 V230 V231 V232 V233 V234 V235 V236 V237 V238 V239 V240 V241)
     DEFINE
     V1 AS val > 0, V2 AS val > 0, V3 AS val > 0, V4 AS val > 0, V5 AS val > 0, V6 AS val > 0, V7 AS val > 0, V8 AS val > 0, V9 AS val > 0, V10 AS val > 0,
     V11 AS val > 0, V12 AS val > 0, V13 AS val > 0, V14 AS val > 0, V15 AS val > 0, V16 AS val > 0, V17 AS val > 0, V18 AS val > 0, V19 AS val > 0, V20 AS val > 0,
@@ -6351,13 +6349,11 @@ WINDOW w AS (
     V201 AS val > 0, V202 AS val > 0, V203 AS val > 0, V204 AS val > 0, V205 AS val > 0, V206 AS val > 0, V207 AS val > 0, V208 AS val > 0, V209 AS val > 0, V210 AS val > 0,
     V211 AS val > 0, V212 AS val > 0, V213 AS val > 0, V214 AS val > 0, V215 AS val > 0, V216 AS val > 0, V217 AS val > 0, V218 AS val > 0, V219 AS val > 0, V220 AS val > 0,
     V221 AS val > 0, V222 AS val > 0, V223 AS val > 0, V224 AS val > 0, V225 AS val > 0, V226 AS val > 0, V227 AS val > 0, V228 AS val > 0, V229 AS val > 0, V230 AS val > 0,
-    V231 AS val > 0, V232 AS val > 0, V233 AS val > 0, V234 AS val > 0, V235 AS val > 0, V236 AS val > 0, V237 AS val > 0, V238 AS val > 0, V239 AS val > 0, V240 AS val > 0,
-    V241 AS val > 0, V242 AS val > 0, V243 AS val > 0, V244 AS val > 0, V245 AS val > 0, V246 AS val > 0, V247 AS val > 0, V248 AS val > 0, V249 AS val > 0, V250 AS val > 0,
-    V251 AS val > 0
+    V231 AS val > 0, V232 AS val > 0, V233 AS val > 0, V234 AS val > 0, V235 AS val > 0, V236 AS val > 0, V237 AS val > 0, V238 AS val > 0, V239 AS val > 0, V240 AS val > 0
 );
 ERROR:  too many pattern variables
-DETAIL:  Maximum is 251.
--- Expected: ERROR - too many pattern variables (Maximum is 251)
+DETAIL:  Maximum is 240.
+-- Expected: ERROR - too many pattern variables (Maximum is 240)
 -- Test: Pattern nesting at maximum depth (depth 253)
 -- Note: 253 nested GROUP{3,7} quantifiers produce depth 253 after optimization
 SELECT id, val, COUNT(*) OVER w FROM rpr_errors
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index 63173615273..475541d4550 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -3948,12 +3948,12 @@ WINDOW w AS (
 );
 -- Expected: Error - B is not used in PATTERN
 
--- Test: 251 variables in PATTERN and DEFINE (boundary - should succeed)
+-- Test: 240 variables in PATTERN and DEFINE (boundary - should succeed)
 SELECT COUNT(*) OVER w FROM rpr_errors
 WINDOW w AS (
     ORDER BY id
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    PATTERN (V1 V2 V3 V4 V5 V6 V7 V8 V9 V10 V11 V12 V13 V14 V15 V16 V17 V18 V19 V20 V21 V22 V23 V24 V25 V26 V27 V28 V29 V30 V31 V32 V33 V34 V35 V36 V37 V38 V39 V40 V41 V42 V43 V44 V45 V46 V47 V48 V49 V50 V51 V52 V53 V54 V55 V56 V57 V58 V59 V60 V61 V62 V63 V64 V65 V66 V67 V68 V69 V70 V71 V72 V73 V74 V75 V76 V77 V78 V79 V80 V81 V82 V83 V84 V85 V86 V87 V88 V89 V90 V91 V92 V93 V94 V95 V96 V97 V98 V99 V100 V101 V102 V103 V104 V105 V106 V107 V108 V109 V110 V111 V112 V113 V114 V115 V116 V117 V118 V119 V120 V121 V122 V123 V124 V125 V126 V127 V128 V129 V130 V131 V132 V133 V134 V135 V136 V137 V138 V139 V140 V141 V142 V143 V144 V145 V146 V147 V148 V149 V150 V151 V152 V153 V154 V155 V156 V157 V158 V159 V160 V161 V162 V163 V164 V165 V166 V167 V168 V169 V170 V171 V172 V173 V174 V175 V176 V177 V178 V179 V180 V181 V182 V183 V184 V185 V186 V187 V188 V189 V190 V191 V192 V193 V194 V195 V196 V197 V198 V199 V200 V201 V202 V203 V204 V205 V206 V207 V208 V209 V210 V211 V212 V213 V214 V215 V216 V217 V218 V219 V220 V221 V222 V223 V224 V225 V226 V227 V228 V229 V230 V231 V232 V233 V234 V235 V236 V237 V238 V239 V240 V241 V242 V243 V244 V245 V246 V247 V248 V249 V250 V251)
+    PATTERN (V1 V2 V3 V4 V5 V6 V7 V8 V9 V10 V11 V12 V13 V14 V15 V16 V17 V18 V19 V20 V21 V22 V23 V24 V25 V26 V27 V28 V29 V30 V31 V32 V33 V34 V35 V36 V37 V38 V39 V40 V41 V42 V43 V44 V45 V46 V47 V48 V49 V50 V51 V52 V53 V54 V55 V56 V57 V58 V59 V60 V61 V62 V63 V64 V65 V66 V67 V68 V69 V70 V71 V72 V73 V74 V75 V76 V77 V78 V79 V80 V81 V82 V83 V84 V85 V86 V87 V88 V89 V90 V91 V92 V93 V94 V95 V96 V97 V98 V99 V100 V101 V102 V103 V104 V105 V106 V107 V108 V109 V110 V111 V112 V113 V114 V115 V116 V117 V118 V119 V120 V121 V122 V123 V124 V125 V126 V127 V128 V129 V130 V131 V132 V133 V134 V135 V136 V137 V138 V139 V140 V141 V142 V143 V144 V145 V146 V147 V148 V149 V150 V151 V152 V153 V154 V155 V156 V157 V158 V159 V160 V161 V162 V163 V164 V165 V166 V167 V168 V169 V170 V171 V172 V173 V174 V175 V176 V177 V178 V179 V180 V181 V182 V183 V184 V185 V186 V187 V188 V189 V190 V191 V192 V193 V194 V195 V196 V197 V198 V199 V200 V201 V202 V203 V204 V205 V206 V207 V208 V209 V210 V211 V212 V213 V214 V215 V216 V217 V218 V219 V220 V221 V222 V223 V224 V225 V226 V227 V228 V229 V230 V231 V232 V233 V234 V235 V236 V237 V238 V239 V240)
     DEFINE
     V1 AS val > 0, V2 AS val > 0, V3 AS val > 0, V4 AS val > 0, V5 AS val > 0, V6 AS val > 0, V7 AS val > 0, V8 AS val > 0, V9 AS val > 0, V10 AS val > 0,
     V11 AS val > 0, V12 AS val > 0, V13 AS val > 0, V14 AS val > 0, V15 AS val > 0, V16 AS val > 0, V17 AS val > 0, V18 AS val > 0, V19 AS val > 0, V20 AS val > 0,
@@ -3978,18 +3978,16 @@ WINDOW w AS (
     V201 AS val > 0, V202 AS val > 0, V203 AS val > 0, V204 AS val > 0, V205 AS val > 0, V206 AS val > 0, V207 AS val > 0, V208 AS val > 0, V209 AS val > 0, V210 AS val > 0,
     V211 AS val > 0, V212 AS val > 0, V213 AS val > 0, V214 AS val > 0, V215 AS val > 0, V216 AS val > 0, V217 AS val > 0, V218 AS val > 0, V219 AS val > 0, V220 AS val > 0,
     V221 AS val > 0, V222 AS val > 0, V223 AS val > 0, V224 AS val > 0, V225 AS val > 0, V226 AS val > 0, V227 AS val > 0, V228 AS val > 0, V229 AS val > 0, V230 AS val > 0,
-    V231 AS val > 0, V232 AS val > 0, V233 AS val > 0, V234 AS val > 0, V235 AS val > 0, V236 AS val > 0, V237 AS val > 0, V238 AS val > 0, V239 AS val > 0, V240 AS val > 0,
-    V241 AS val > 0, V242 AS val > 0, V243 AS val > 0, V244 AS val > 0, V245 AS val > 0, V246 AS val > 0, V247 AS val > 0, V248 AS val > 0, V249 AS val > 0, V250 AS val > 0,
-    V251 AS val > 0
+    V231 AS val > 0, V232 AS val > 0, V233 AS val > 0, V234 AS val > 0, V235 AS val > 0, V236 AS val > 0, V237 AS val > 0, V238 AS val > 0, V239 AS val > 0, V240 AS val > 0
 );
 -- Expected: Success - exactly at RPR_VARID_MAX boundary
 
--- Test: 252 variables in PATTERN, 251 in DEFINE (exceeds limit with implicit TRUE)
+-- Test: 241 variables in PATTERN, 240 in DEFINE (exceeds limit with implicit TRUE)
 SELECT COUNT(*) OVER w FROM rpr_errors
 WINDOW w AS (
     ORDER BY id
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    PATTERN (V1 V2 V3 V4 V5 V6 V7 V8 V9 V10 V11 V12 V13 V14 V15 V16 V17 V18 V19 V20 V21 V22 V23 V24 V25 V26 V27 V28 V29 V30 V31 V32 V33 V34 V35 V36 V37 V38 V39 V40 V41 V42 V43 V44 V45 V46 V47 V48 V49 V50 V51 V52 V53 V54 V55 V56 V57 V58 V59 V60 V61 V62 V63 V64 V65 V66 V67 V68 V69 V70 V71 V72 V73 V74 V75 V76 V77 V78 V79 V80 V81 V82 V83 V84 V85 V86 V87 V88 V89 V90 V91 V92 V93 V94 V95 V96 V97 V98 V99 V100 V101 V102 V103 V104 V105 V106 V107 V108 V109 V110 V111 V112 V113 V114 V115 V116 V117 V118 V119 V120 V121 V122 V123 V124 V125 V126 V127 V128 V129 V130 V131 V132 V133 V134 V135 V136 V137 V138 V139 V140 V141 V142 V143 V144 V145 V146 V147 V148 V149 V150 V151 V152 V153 V154 V155 V156 V157 V158 V159 V160 V161 V162 V163 V164 V165 V166 V167 V168 V169 V170 V171 V172 V173 V174 V175 V176 V177 V178 V179 V180 V181 V182 V183 V184 V185 V186 V187 V188 V189 V190 V191 V192 V193 V194 V195 V196 V197 V198 V199 V200 V201 V202 V203 V204 V205 V206 V207 V208 V209 V210 V211 V212 V213 V214 V215 V216 V217 V218 V219 V220 V221 V222 V223 V224 V225 V226 V227 V228 V229 V230 V231 V232 V233 V234 V235 V236 V237 V238 V239 V240 V241 V242 V243 V244 V245 V246 V247 V248 V249 V250 V251 V252)
+    PATTERN (V1 V2 V3 V4 V5 V6 V7 V8 V9 V10 V11 V12 V13 V14 V15 V16 V17 V18 V19 V20 V21 V22 V23 V24 V25 V26 V27 V28 V29 V30 V31 V32 V33 V34 V35 V36 V37 V38 V39 V40 V41 V42 V43 V44 V45 V46 V47 V48 V49 V50 V51 V52 V53 V54 V55 V56 V57 V58 V59 V60 V61 V62 V63 V64 V65 V66 V67 V68 V69 V70 V71 V72 V73 V74 V75 V76 V77 V78 V79 V80 V81 V82 V83 V84 V85 V86 V87 V88 V89 V90 V91 V92 V93 V94 V95 V96 V97 V98 V99 V100 V101 V102 V103 V104 V105 V106 V107 V108 V109 V110 V111 V112 V113 V114 V115 V116 V117 V118 V119 V120 V121 V122 V123 V124 V125 V126 V127 V128 V129 V130 V131 V132 V133 V134 V135 V136 V137 V138 V139 V140 V141 V142 V143 V144 V145 V146 V147 V148 V149 V150 V151 V152 V153 V154 V155 V156 V157 V158 V159 V160 V161 V162 V163 V164 V165 V166 V167 V168 V169 V170 V171 V172 V173 V174 V175 V176 V177 V178 V179 V180 V181 V182 V183 V184 V185 V186 V187 V188 V189 V190 V191 V192 V193 V194 V195 V196 V197 V198 V199 V200 V201 V202 V203 V204 V205 V206 V207 V208 V209 V210 V211 V212 V213 V214 V215 V216 V217 V218 V219 V220 V221 V222 V223 V224 V225 V226 V227 V228 V229 V230 V231 V232 V233 V234 V235 V236 V237 V238 V239 V240 V241)
     DEFINE
     V1 AS val > 0, V2 AS val > 0, V3 AS val > 0, V4 AS val > 0, V5 AS val > 0, V6 AS val > 0, V7 AS val > 0, V8 AS val > 0, V9 AS val > 0, V10 AS val > 0,
     V11 AS val > 0, V12 AS val > 0, V13 AS val > 0, V14 AS val > 0, V15 AS val > 0, V16 AS val > 0, V17 AS val > 0, V18 AS val > 0, V19 AS val > 0, V20 AS val > 0,
@@ -4014,11 +4012,9 @@ WINDOW w AS (
     V201 AS val > 0, V202 AS val > 0, V203 AS val > 0, V204 AS val > 0, V205 AS val > 0, V206 AS val > 0, V207 AS val > 0, V208 AS val > 0, V209 AS val > 0, V210 AS val > 0,
     V211 AS val > 0, V212 AS val > 0, V213 AS val > 0, V214 AS val > 0, V215 AS val > 0, V216 AS val > 0, V217 AS val > 0, V218 AS val > 0, V219 AS val > 0, V220 AS val > 0,
     V221 AS val > 0, V222 AS val > 0, V223 AS val > 0, V224 AS val > 0, V225 AS val > 0, V226 AS val > 0, V227 AS val > 0, V228 AS val > 0, V229 AS val > 0, V230 AS val > 0,
-    V231 AS val > 0, V232 AS val > 0, V233 AS val > 0, V234 AS val > 0, V235 AS val > 0, V236 AS val > 0, V237 AS val > 0, V238 AS val > 0, V239 AS val > 0, V240 AS val > 0,
-    V241 AS val > 0, V242 AS val > 0, V243 AS val > 0, V244 AS val > 0, V245 AS val > 0, V246 AS val > 0, V247 AS val > 0, V248 AS val > 0, V249 AS val > 0, V250 AS val > 0,
-    V251 AS val > 0
+    V231 AS val > 0, V232 AS val > 0, V233 AS val > 0, V234 AS val > 0, V235 AS val > 0, V236 AS val > 0, V237 AS val > 0, V238 AS val > 0, V239 AS val > 0, V240 AS val > 0
 );
--- Expected: ERROR - too many pattern variables (Maximum is 251)
+-- Expected: ERROR - too many pattern variables (Maximum is 240)
 
 -- Test: Pattern nesting at maximum depth (depth 253)
 -- Note: 253 nested GROUP{3,7} quantifiers produce depth 253 after optimization
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0043-Generalize-quantifier-multiplication-in-row-patte.txt (38.0K, 45-nocfbot-0043-Generalize-quantifier-multiplication-in-row-patte.txt)
  download | inline diff:
From c0aa0a1ca48f6c520658a50560b86d708118b9a0 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Wed, 3 Jun 2026 23:10:49 +0900
Subject: [PATCH 43/68] Generalize quantifier multiplication in row pattern
 recognition

The planner flattens a nested pattern quantifier (child{p,q}){m,n} into
child{p*m, q*n}.  This is valid only when the repetition counts the nested
quantifiers can produce form exactly one contiguous interval.  Replace the
previous handful of special cases with that general test: fold when the
outer count is exact (m == n), or the child is skippable (p == 0), or the
per-iteration intervals touch (p <= Max(m,1)*(q-p) + 1) and, for a skippable
outer, the zero case connects to the child range (m >= 1 or p <= 1).

This additionally folds contiguous cases that were previously left
unflattened, such as (A{2,3}){2,3} -> a{4,9}, (A+){3} -> a{3,}, and
(A+){2,4} -> a{2,}, while still leaving gap-producing patterns like
(A{2}){2,3} and (A{2,})* untouched.  The int64 overflow guards on the
multiplied bounds are retained.

Expand the quatifier-multiplication regression cases to exercise each
branch with both folding and gap (no-fold) patterns.  The pattern-nesting
depth tests now use reluctant quantifiers, which are not subject to
multiplication, so they still build the intended nesting depth.
---
 src/backend/optimizer/plan/rpr.c          | 130 +++++++++---------
 src/test/regress/expected/rpr_base.out    | 154 +++++++++++++++++++++-
 src/test/regress/expected/rpr_explain.out |  16 +--
 src/test/regress/sql/rpr_base.sql         |  72 +++++++++-
 4 files changed, 284 insertions(+), 88 deletions(-)

diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index 4652ca6ebeb..617a3869948 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -784,21 +784,25 @@ optimizeAltPattern(RPRPatternNode *pattern)
 
 /*
  * tryMultiplyQuantifiers
- *		Try to multiply quantifiers.
- *
- * Multiplication is SAFE when:
- *   1. Both unbounded, with skipless outer or child->min <= 1:
- *      (A*)* -> A*, (A+)+ -> A+, (A+)* -> A*, (A{2,})+ -> A{2,}
- *   2. Outer exact: (A{m,n}){k} -> A{m*k, n*k}
- *   3. Outer range + child {1,1}: (A){2,} -> A{2,}
- *
- * Multiplication is NOT safe when:
- *   - Only child unbounded: (A+){3} has different semantics
- *   - Outer range + child not {1,1}: gaps possible
- *     e.g., (A{2}){2,3} yields 4,6 only (not 4,5,6)
- *   - Skippable outer (min 0) + child->min >= 2: (A{2,})* reaches
- *     {0} UNION [child->min, INF), so 1..child->min-1 are unreachable
- *     and A* would wrongly admit them
+ *		Try to flatten (child{p,q}){m,n} into child{p*m, q*n}.
+ *
+ * Below, p,q are the child's {min,max} and m,n the outer {min,max}.
+ *
+ * Flattening is valid only when the repetition counts the nested quantifiers
+ * can produce form exactly the contiguous interval [p*m, q*n].  For an outer
+ * iteration count t (m <= t <= n) the child contributes any count in
+ * [t*p, t*q], and t = 0 contributes {0}.  The union of those intervals is
+ * contiguous, hence flattenable, when:
+ *
+ *   - m == n: a single outer count, so the result is just [m*p, m*q]; or
+ *   - p == 0: every interval starts at 0, so they all overlap; or
+ *   - consecutive intervals touch and the zero case (if any) connects:
+ *       p <= Max(m,1)*(q-p) + 1   (touch; trivially true if q is unbounded)
+ *       and (m >= 1 or p <= 1)    (when m == 0, {0} must reach [p,q])
+ *
+ * Otherwise gaps appear and the pattern is left unflattened: (A{2}){2,3}
+ * yields {4,6} (not 4..6), and (A{2,})* yields {0} UNION [2,INF) (not
+ * [0,INF), so A* would wrongly admit a single A).
  *
  * Returns the child node with multiplied quantifiers if successful,
  * otherwise returns the original pattern unchanged.
@@ -807,6 +811,7 @@ static RPRPatternNode *
 tryMultiplyQuantifiers(RPRPatternNode *pattern)
 {
 	RPRPatternNode *child;
+	bool		safe;
 	int64		new_min_64;
 	int64		new_max_64;
 
@@ -823,69 +828,60 @@ tryMultiplyQuantifiers(RPRPatternNode *pattern)
 		child->reluctant)
 		return pattern;
 
-	/* Case 1: Both unbounded - (A*)* -> A*, (A+)+ -> A+ */
-	if (child->max == RPR_QUANTITY_INF && pattern->max == RPR_QUANTITY_INF)
+	/*
+	 * Decide whether the achievable counts form one contiguous interval.  The
+	 * child quantifier is {child->min, child->max} and the outer one is
+	 * {pattern->min, pattern->max}; either max may be RPR_QUANTITY_INF.
+	 */
+	if (pattern->min == pattern->max || child->min == 0)
+		safe = true;
+	else
 	{
+		bool		touch;
+		bool		zero_ok;
+
 		/*
-		 * A skippable outer (min 0) over a child with min >= 2 reaches
-		 * repetition counts {0} UNION [child->min, INF): the counts
-		 * 1..child->min-1 are unreachable, and no single quantifier can
-		 * express that gap.  Flattening to A{0,INF} = A* would wrongly admit
-		 * them, e.g. (A{2,})* would match a single A.  Multiplication is safe
-		 * here only when child->min <= 1 (the reachable set is then
-		 * contiguous from 0); otherwise leave the pattern unflattened.
+		 * Consecutive intervals [t*min, t*max] and [(t+1)*min, (t+1)*max]
+		 * touch when (t+1)*min <= t*max + 1, i.e. min <= t*(max-min) + 1.
+		 * This is tightest at the smallest t in play, Max(pattern->min, 1).
+		 * An unbounded child->max makes every interval reach INF, so they
+		 * always touch.
 		 */
-		if (pattern->min == 0 && child->min >= 2)
-			return pattern;
+		if (child->max == RPR_QUANTITY_INF)
+			touch = true;
+		else
+			touch = ((int64) child->min <=
+					 (int64) Max(pattern->min, 1) * (child->max - child->min) + 1);
 
-		new_min_64 = (int64) child->min * pattern->min;
-		if (new_min_64 >= RPR_QUANTITY_INF)
-			return pattern;		/* overflow, skip optimization */
+		/*
+		 * A skippable outer (min 0) also needs {0} adjacent to the child
+		 * range.
+		 */
+		zero_ok = (pattern->min >= 1 || child->min <= 1);
 
-		child->min = (int) new_min_64;
-		child->max = RPR_QUANTITY_INF;
-		return child;
+		safe = touch && zero_ok;
 	}
 
-	/*----------
-	 * Case 2: Outer exact (min == max): (A{2,3}){4} -> A{8,12}.
-	 *         Safe because every iteration produces the same range.
-	 *
-	 * Case 3: Child {1,1}: (A){2,5} -> A{2,5}.
-	 *         Safe because the child contributes exactly one per
-	 *         iteration, so the outer range maps directly.
-	 *
-	 * Unsafe example: (A{2}){2,3} produces counts 4 or 6 only, not the full
-	 * range 4..6, so we cannot flatten when child has a non-trivial range AND
-	 * outer is also a range.
-	 *----------
-	 */
-	if (child->max != RPR_QUANTITY_INF &&
-		(pattern->min == pattern->max ||
-		 (child->min == 1 && child->max == 1)))
-	{
-		new_min_64 = (int64) pattern->min * child->min;
-		if (new_min_64 >= RPR_QUANTITY_INF)
-			return pattern;
-
-		/* Outer unbounded: result is unbounded regardless of child */
-		if (pattern->max == RPR_QUANTITY_INF)
-			new_max_64 = RPR_QUANTITY_INF;
-		else
-		{
-			new_max_64 = (int64) pattern->max * child->max;
+	if (!safe)
+		return pattern;
 
-			if (new_max_64 >= RPR_QUANTITY_INF)
-				return pattern;
-		}
+	/* Flatten the child quantifier, guarding against overflow. */
+	new_min_64 = (int64) pattern->min * child->min;
+	if (new_min_64 >= RPR_QUANTITY_INF)
+		return pattern;			/* overflow, skip optimization */
 
-		child->min = (int) new_min_64;
-		child->max = (int) new_max_64;
-		return child;
+	if (pattern->max == RPR_QUANTITY_INF || child->max == RPR_QUANTITY_INF)
+		new_max_64 = RPR_QUANTITY_INF;
+	else
+	{
+		new_max_64 = (int64) pattern->max * child->max;
+		if (new_max_64 >= RPR_QUANTITY_INF)
+			return pattern;
 	}
 
-	/* Not safe to multiply */
-	return pattern;
+	child->min = (int) new_min_64;
+	child->max = (int) new_max_64;
+	return child;
 }
 
 /*
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index e9303af6384..1410ba75395 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -3785,8 +3785,8 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
          ->  Seq Scan on rpr_plan
 (7 rows)
 
--- Quantifier NO multiply: (A{2,3}){2,3} stays as (a{2,3}){2,3}
--- outer range, child range - gaps possible (e.g., (A{4,5}){2,3} misses 11)
+-- Quantifier multiply: (A{2,3}){2,3} -> a{4,9}
+-- outer range, child range: counts [4,6] U [6,9] = [4,9] are contiguous, so it folds
 EXPLAIN (COSTS OFF)
 SELECT COUNT(*) OVER w FROM rpr_plan
 WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
@@ -3795,7 +3795,24 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
 -------------------------------------------------------------------------------
  WindowAgg
    Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: (a{2,3}){2,3}
+   Pattern: a{4,9}
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 rows)
+
+-- Quantifier NO multiply: (A{4,5}){2,3} stays as (a{4,5}){2,3}
+-- outer range, child range with a gap: [8,10] U [12,15] misses 11
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN ((A{4,5}){2,3}) DEFINE A AS val > 0);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a{4,5}){2,3}
    Nav Mark Lookback: 0
    ->  Sort
          Sort Key: id
@@ -3850,6 +3867,125 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
          ->  Seq Scan on rpr_plan
 (7 rows)
 
+-- Quantifier multiply with an unbounded child: an exact outer count (m == n)
+-- always folds regardless of the child's max - (A+){3} -> a{3,}
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN ((A+){3}) DEFINE A AS val > 0);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a{3,}"
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 rows)
+
+-- (A{2,}){3} -> a{6,}  (m == n, unbounded child with min 2)
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN ((A{2,}){3}) DEFINE A AS val > 0);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a{6,}"
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 rows)
+
+-- (A+){2,4} -> a{2,}  (outer range, unbounded child: every interval reaches INF,
+-- so they always touch)
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN ((A+){2,4}) DEFINE A AS val > 0);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a{2,}"
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 rows)
+
+-- (A{2,3}){2,4} -> a{4,12}  (outer range x child range, contiguous:
+-- [4,6] U [6,9] U [8,12] = [4,12])
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN ((A{2,3}){2,4}) DEFINE A AS val > 0);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a{4,12}
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 rows)
+
+-- Skippable outer (min 0) folds only when the zero case connects to the child
+-- range: (A{1,3})? -> a{0,3}  (child min <= 1, so {0} U [1,3] = [0,3] is contiguous)
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN ((A{1,3})?) DEFINE A AS val > 0);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a{0,3}
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 rows)
+
+-- Quantifier NO multiply: (A{2,3})? stays as (a{2,3})?
+-- min 0 with child min >= 2: {0} U [2,3] leaves 1 unreachable (intervals touch but
+-- the zero case does not connect)
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN ((A{2,3})?) DEFINE A AS val > 0);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a{2,3})?
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 rows)
+
+-- Quantifier NO multiply: (A{3,4})? stays as (a{3,4})?
+-- min 0 with child min >= 2: {0} U [3,4] leaves 1,2 unreachable
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN ((A{3,4})?) DEFINE A AS val > 0);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a{3,4})?
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 rows)
+
 -- Unwrap GROUP{1,1}: (A) -> a
 EXPLAIN (COSTS OFF)
 SELECT COUNT(*) OVER w FROM rpr_plan
@@ -6355,12 +6491,14 @@ ERROR:  too many pattern variables
 DETAIL:  Maximum is 240.
 -- Expected: ERROR - too many pattern variables (Maximum is 240)
 -- Test: Pattern nesting at maximum depth (depth 253)
--- Note: 253 nested GROUP{3,7} quantifiers produce depth 253 after optimization
+-- Note: 253 nested GROUP{3,7}? quantifiers; reluctant quantifiers are not
+-- subject to quantifier multiplication, so the nesting (and depth 253) is
+-- preserved after optimization.
 SELECT id, val, COUNT(*) OVER w FROM rpr_errors
 WINDOW w AS (
     ORDER BY id
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    PATTERN ((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((A{3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7})
+    PATTERN ((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((A{3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?)
     DEFINE A AS val > 0
 );
  id | val | count 
@@ -6371,12 +6509,14 @@ WINDOW w AS (
 
 -- Expected: Should succeed
 -- Test: Pattern nesting depth exceeds maximum (depth 254)
--- Note: 254 nested GROUP{3,7} quantifiers produce depth 254 after optimization
+-- Note: 254 nested GROUP{3,7}? quantifiers; reluctant quantifiers are not
+-- subject to quantifier multiplication, so the nesting reaches depth 254 and
+-- exceeds the limit.
 SELECT id, val, COUNT(*) OVER w FROM rpr_errors
 WINDOW w AS (
     ORDER BY id
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    PATTERN (((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((A{3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7})
+    PATTERN (((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((A{3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?)
     DEFINE A AS val > 0
 );
 ERROR:  pattern nesting too deep
diff --git a/src/test/regress/expected/rpr_explain.out b/src/test/regress/expected/rpr_explain.out
index 9ba302b11ae..5cddd1a56df 100644
--- a/src/test/regress/expected/rpr_explain.out
+++ b/src/test/regress/expected/rpr_explain.out
@@ -2178,10 +2178,10 @@ WINDOW w AS (
 ---------------------------------------------------------------------
  WindowAgg (actual rows=3.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: (a?){0,3}
+   Pattern: a{0,3}
    Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
-   NFA States: 6 peak, 20 total, 4 merged
+   NFA States: 3 peak, 8 total, 0 merged
    NFA Contexts: 2 peak, 4 total, 0 pruned
    NFA: 3 matched (len 0/0/0.0), 0 mismatched
    ->  Function Scan on generate_series s (actual rows=3.00 loops=1)
@@ -2217,10 +2217,10 @@ WINDOW w AS (
 ---------------------------------------------------------------------
  WindowAgg (actual rows=3.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: (a?){1,3}
+   Pattern: a{0,3}
    Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
-   NFA States: 5 peak, 16 total, 4 merged
+   NFA States: 3 peak, 8 total, 0 merged
    NFA Contexts: 2 peak, 4 total, 0 pruned
    NFA: 3 matched (len 0/0/0.0), 0 mismatched
    ->  Function Scan on generate_series s (actual rows=3.00 loops=1)
@@ -2256,10 +2256,10 @@ WINDOW w AS (
 ---------------------------------------------------------------------
  WindowAgg (actual rows=3.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: (a?){2,3}
+   Pattern: a{0,3}
    Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
-   NFA States: 5 peak, 16 total, 4 merged
+   NFA States: 3 peak, 8 total, 0 merged
    NFA Contexts: 2 peak, 4 total, 0 pruned
    NFA: 3 matched (len 0/0/0.0), 0 mismatched
    ->  Function Scan on generate_series s (actual rows=3.00 loops=1)
@@ -2295,10 +2295,10 @@ WINDOW w AS (
 ---------------------------------------------------------------------
  WindowAgg (actual rows=4.00 loops=1)
    Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
-   Pattern: (a?){2,3}
+   Pattern: a{0,3}
    Nav Mark Lookback: 0
    Storage: Memory  Maximum Storage: NkB
-   NFA States: 8 peak, 26 total, 5 merged
+   NFA States: 6 peak, 13 total, 0 merged
    NFA Contexts: 4 peak, 5 total, 1 pruned
    NFA: 3 matched (len 0/2/1.0), 0 mismatched
    ->  Function Scan on generate_series s (actual rows=4.00 loops=1)
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index 475541d4550..53bf090b903 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -2516,13 +2516,20 @@ SELECT COUNT(*) OVER w FROM rpr_plan
 WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
              PATTERN ((A{2}){3,5}) DEFINE A AS val > 0);
 
--- Quantifier NO multiply: (A{2,3}){2,3} stays as (a{2,3}){2,3}
--- outer range, child range - gaps possible (e.g., (A{4,5}){2,3} misses 11)
+-- Quantifier multiply: (A{2,3}){2,3} -> a{4,9}
+-- outer range, child range: counts [4,6] U [6,9] = [4,9] are contiguous, so it folds
 EXPLAIN (COSTS OFF)
 SELECT COUNT(*) OVER w FROM rpr_plan
 WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
              PATTERN ((A{2,3}){2,3}) DEFINE A AS val > 0);
 
+-- Quantifier NO multiply: (A{4,5}){2,3} stays as (a{4,5}){2,3}
+-- outer range, child range with a gap: [8,10] U [12,15] misses 11
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN ((A{4,5}){2,3}) DEFINE A AS val > 0);
+
 -- Nested unbounded: (A*)* -> a*
 EXPLAIN (COSTS OFF)
 SELECT COUNT(*) OVER w FROM rpr_plan
@@ -2541,6 +2548,55 @@ SELECT COUNT(*) OVER w FROM rpr_plan
 WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
              PATTERN ((A+)+) DEFINE A AS val > 0);
 
+-- Quantifier multiply with an unbounded child: an exact outer count (m == n)
+-- always folds regardless of the child's max - (A+){3} -> a{3,}
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN ((A+){3}) DEFINE A AS val > 0);
+
+-- (A{2,}){3} -> a{6,}  (m == n, unbounded child with min 2)
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN ((A{2,}){3}) DEFINE A AS val > 0);
+
+-- (A+){2,4} -> a{2,}  (outer range, unbounded child: every interval reaches INF,
+-- so they always touch)
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN ((A+){2,4}) DEFINE A AS val > 0);
+
+-- (A{2,3}){2,4} -> a{4,12}  (outer range x child range, contiguous:
+-- [4,6] U [6,9] U [8,12] = [4,12])
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN ((A{2,3}){2,4}) DEFINE A AS val > 0);
+
+-- Skippable outer (min 0) folds only when the zero case connects to the child
+-- range: (A{1,3})? -> a{0,3}  (child min <= 1, so {0} U [1,3] = [0,3] is contiguous)
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN ((A{1,3})?) DEFINE A AS val > 0);
+
+-- Quantifier NO multiply: (A{2,3})? stays as (a{2,3})?
+-- min 0 with child min >= 2: {0} U [2,3] leaves 1 unreachable (intervals touch but
+-- the zero case does not connect)
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN ((A{2,3})?) DEFINE A AS val > 0);
+
+-- Quantifier NO multiply: (A{3,4})? stays as (a{3,4})?
+-- min 0 with child min >= 2: {0} U [3,4] leaves 1,2 unreachable
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN ((A{3,4})?) DEFINE A AS val > 0);
+
 -- Unwrap GROUP{1,1}: (A) -> a
 EXPLAIN (COSTS OFF)
 SELECT COUNT(*) OVER w FROM rpr_plan
@@ -4017,23 +4073,27 @@ WINDOW w AS (
 -- Expected: ERROR - too many pattern variables (Maximum is 240)
 
 -- Test: Pattern nesting at maximum depth (depth 253)
--- Note: 253 nested GROUP{3,7} quantifiers produce depth 253 after optimization
+-- Note: 253 nested GROUP{3,7}? quantifiers; reluctant quantifiers are not
+-- subject to quantifier multiplication, so the nesting (and depth 253) is
+-- preserved after optimization.
 SELECT id, val, COUNT(*) OVER w FROM rpr_errors
 WINDOW w AS (
     ORDER BY id
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    PATTERN ((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((A{3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7})
+    PATTERN ((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((A{3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?)
     DEFINE A AS val > 0
 );
 -- Expected: Should succeed
 
 -- Test: Pattern nesting depth exceeds maximum (depth 254)
--- Note: 254 nested GROUP{3,7} quantifiers produce depth 254 after optimization
+-- Note: 254 nested GROUP{3,7}? quantifiers; reluctant quantifiers are not
+-- subject to quantifier multiplication, so the nesting reaches depth 254 and
+-- exceeds the limit.
 SELECT id, val, COUNT(*) OVER w FROM rpr_errors
 WINDOW w AS (
     ORDER BY id
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    PATTERN (((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((A{3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7}){3,7})
+    PATTERN (((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((A{3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?)
     DEFINE A AS val > 0
 );
 -- Expected: ERROR - pattern nesting too deep
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0044-Tidy-up-row-pattern-recognition-pattern-compilati.txt (3.0K, 46-nocfbot-0044-Tidy-up-row-pattern-recognition-pattern-compilati.txt)
  download | inline diff:
From 1e9d66e961bae1e3884be7c844c7faba252c4bed Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Thu, 4 Jun 2026 20:28:51 +0900
Subject: [PATCH 44/68] Tidy up row pattern recognition pattern compilation

Mark the unreachable tails of the three node-type switches
(rprPatternEqual, optimizeRPRPattern, fillRPRPattern) with
pg_unreachable() instead of a "keep compiler quiet" return.
RPRPatternNodeType has only four members and each switch handles
all of them, so the trailing return can never be reached.

In buildRPRPattern(), the absorption-eligibility test guarded
against a ROWS frame redundantly: RPR is ROWS-only because
transformRPR() rejects RANGE and GROUPS up front, so the
FRAMEOPTION_ROWS term was always true.  Drop the hasLimitedFrame
variable, assert the ROWS-only invariant instead, and test
FRAMEOPTION_END_UNBOUNDED_FOLLOWING directly.

No behavior change.

Patch by Jian He, with minor adjustments.
---
 src/backend/optimizer/plan/rpr.c | 18 ++++++++++--------
 1 file changed, 10 insertions(+), 8 deletions(-)

diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index 617a3869948..3205559c03a 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -132,7 +132,8 @@ rprPatternEqual(RPRPatternNode *a, RPRPatternNode *b)
 			return rprPatternChildrenEqual(a->children, b->children);
 	}
 
-	return false;				/* keep compiler quiet */
+	pg_unreachable();
+	return false;
 }
 
 /*
@@ -993,7 +994,8 @@ optimizeRPRPattern(RPRPatternNode *pattern)
 			return optimizeGroupPattern(pattern);
 	}
 
-	return pattern;				/* keep compiler quiet */
+	pg_unreachable();
+	return pattern;
 }
 
 /*
@@ -1450,7 +1452,8 @@ fillRPRPattern(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth depth)
 			return fillRPRPatternAlt(node, pat, idx, depth);
 	}
 
-	return false;				/* unreachable */
+	pg_unreachable();
+	return false;
 }
 
 /*
@@ -1955,10 +1958,11 @@ buildRPRPattern(RPRPatternNode *pattern, List *defineVariableList,
 	int			numElements;
 	RPRDepth	maxDepth;
 	int			idx;
-	bool		hasLimitedFrame;
 
 	/* Caller must check for NULL pattern before calling */
 	Assert(pattern != NULL);
+	/* RPR is ROWS-only: transformRPR() rejects RANGE/GROUPS up front */
+	Assert(frameOptions & FRAMEOPTION_ROWS);
 
 	/* Optimize the pattern tree */
 	optimized = optimizeRPRPattern(copyObject(pattern));
@@ -1997,10 +2001,8 @@ buildRPRPattern(RPRPatternNode *pattern, List *defineVariableList,
 	 * absorption semantics - older contexts don't necessarily produce longer
 	 * matches when frame limits apply differently to each context.
 	 */
-	hasLimitedFrame = (frameOptions & FRAMEOPTION_ROWS) &&
-		!(frameOptions & FRAMEOPTION_END_UNBOUNDED_FOLLOWING);
-
-	if (rpSkipTo == ST_PAST_LAST_ROW && !hasLimitedFrame &&
+	if (rpSkipTo == ST_PAST_LAST_ROW &&
+		(frameOptions & FRAMEOPTION_END_UNBOUNDED_FOLLOWING) &&
 		!hasMatchStartDependent)
 	{
 		/* Runtime conditions met - check structural absorbability */
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0045-Drop-redundant-parentheses-from-row-pattern-recog.txt (7.7K, 47-nocfbot-0045-Drop-redundant-parentheses-from-row-pattern-recog.txt)
  download | inline diff:
From 427916e331cd97c6ee68e3e09b3ee1ccfcb14cb3 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Fri, 5 Jun 2026 11:29:33 +0900
Subject: [PATCH 45/68] Drop redundant parentheses from row pattern recognition
 ereports

The row pattern recognition patch added a number of ereport() calls
that still wrap their auxiliary functions in the optional outer
parentheses.  The newer style omits them, and most of the patch --
all of parse_rpr.c, for one -- already does.

Convert the remaining calls to the parenthesis-free style so the
feature is internally consistent: the DEFINE-clause checks in
ParseFuncOrColumn() and transformColumnRef(), the navigation offset
checks in execExprInterp.c, and the navigation placeholder window
functions in windowfuncs.c.  Pre-existing core ereport() calls are
left untouched.

No behavior change.
---
 src/backend/executor/execExprInterp.c | 16 +++++++-------
 src/backend/parser/parse_expr.c       | 16 +++++++-------
 src/backend/parser/parse_func.c       |  8 +++----
 src/backend/utils/adt/windowfuncs.c   | 32 +++++++++++++--------------
 4 files changed, 36 insertions(+), 36 deletions(-)

diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c
index 324b9a962a8..805c8583fb2 100644
--- a/src/backend/executor/execExprInterp.c
+++ b/src/backend/executor/execExprInterp.c
@@ -6026,15 +6026,15 @@ rpr_nav_get_compound_offset(ExprEvalStep *op)
 
 	if (op->d.rpr_nav.offset_isnull[1])
 		ereport(ERROR,
-				(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
-				 errmsg("row pattern navigation offset must not be null")));
+				errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
+				errmsg("row pattern navigation offset must not be null"));
 
 	val = DatumGetInt64(op->d.rpr_nav.offset_value[1]);
 
 	if (val < 0)
 		ereport(ERROR,
-				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-				 errmsg("row pattern navigation offset must not be negative")));
+				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				errmsg("row pattern navigation offset must not be negative"));
 
 	return val;
 }
@@ -6071,15 +6071,15 @@ ExecEvalRPRNavSet(ExprState *state, ExprEvalStep *op, ExprContext *econtext)
 	{
 		if (*op->d.rpr_nav.offset_isnull)
 			ereport(ERROR,
-					(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
-					 errmsg("row pattern navigation offset must not be null")));
+					errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
+					errmsg("row pattern navigation offset must not be null"));
 
 		offset = DatumGetInt64(*op->d.rpr_nav.offset_value);
 
 		if (offset < 0)
 			ereport(ERROR,
-					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-					 errmsg("row pattern navigation offset must not be negative")));
+					errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					errmsg("row pattern navigation offset must not be negative"));
 	}
 	else
 	{
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 78abdc88f86..2344aaef9ae 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -666,17 +666,17 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 
 		if (is_pattern_var)
 			ereport(ERROR,
-					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-					 errmsg("pattern variable qualified expression \"%s\" is not supported in DEFINE clause",
-							NameListToString(cref->fields)),
-					 parser_errposition(pstate, cref->location)));
+					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					errmsg("pattern variable qualified expression \"%s\" is not supported in DEFINE clause",
+						   NameListToString(cref->fields)),
+					parser_errposition(pstate, cref->location));
 		else if (refnameNamespaceItem(pstate, NULL, qualifier,
 									  cref->location, NULL) != NULL)
 			ereport(ERROR,
-					(errcode(ERRCODE_SYNTAX_ERROR),
-					 errmsg("range variable qualified expression \"%s\" is not allowed in DEFINE clause",
-							NameListToString(cref->fields)),
-					 parser_errposition(pstate, cref->location)));
+					errcode(ERRCODE_SYNTAX_ERROR),
+					errmsg("range variable qualified expression \"%s\" is not allowed in DEFINE clause",
+						   NameListToString(cref->fields)),
+					parser_errposition(pstate, cref->location));
 		/* else: unknown qualifier -- fall through to normal resolution */
 	}
 
diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c
index 49646b728c6..8cae9ba52b6 100644
--- a/src/backend/parser/parse_func.c
+++ b/src/backend/parser/parse_func.c
@@ -789,10 +789,10 @@ ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs,
 
 		/* Not a column projection -- report error */
 		ereport(ERROR,
-				(errcode(ERRCODE_SYNTAX_ERROR),
-				 errmsg("cannot use %s outside a DEFINE clause",
-						NameListToString(funcname)),
-				 parser_errposition(pstate, location)));
+				errcode(ERRCODE_SYNTAX_ERROR),
+				errmsg("cannot use %s outside a DEFINE clause",
+					   NameListToString(funcname)),
+				parser_errposition(pstate, location));
 	}
 
 	/* build the appropriate output structure */
diff --git a/src/backend/utils/adt/windowfuncs.c b/src/backend/utils/adt/windowfuncs.c
index 46e7a03a666..3869f6c8994 100644
--- a/src/backend/utils/adt/windowfuncs.c
+++ b/src/backend/utils/adt/windowfuncs.c
@@ -739,8 +739,8 @@ Datum
 window_prev(PG_FUNCTION_ARGS)
 {
 	ereport(ERROR,
-			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-			 errmsg("cannot use PREV() outside a DEFINE clause")));
+			errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			errmsg("cannot use PREV() outside a DEFINE clause"));
 	PG_RETURN_NULL();			/* not reached */
 }
 
@@ -753,8 +753,8 @@ Datum
 window_next(PG_FUNCTION_ARGS)
 {
 	ereport(ERROR,
-			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-			 errmsg("cannot use NEXT() outside a DEFINE clause")));
+			errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			errmsg("cannot use NEXT() outside a DEFINE clause"));
 	PG_RETURN_NULL();			/* not reached */
 }
 
@@ -767,8 +767,8 @@ Datum
 window_prev_offset(PG_FUNCTION_ARGS)
 {
 	ereport(ERROR,
-			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-			 errmsg("cannot use PREV() outside a DEFINE clause")));
+			errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			errmsg("cannot use PREV() outside a DEFINE clause"));
 	PG_RETURN_NULL();			/* not reached */
 }
 
@@ -781,8 +781,8 @@ Datum
 window_next_offset(PG_FUNCTION_ARGS)
 {
 	ereport(ERROR,
-			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-			 errmsg("cannot use NEXT() outside a DEFINE clause")));
+			errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			errmsg("cannot use NEXT() outside a DEFINE clause"));
 	PG_RETURN_NULL();			/* not reached */
 }
 
@@ -795,8 +795,8 @@ Datum
 window_first(PG_FUNCTION_ARGS)
 {
 	ereport(ERROR,
-			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-			 errmsg("cannot use FIRST() outside a DEFINE clause")));
+			errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			errmsg("cannot use FIRST() outside a DEFINE clause"));
 	PG_RETURN_NULL();			/* not reached */
 }
 
@@ -809,8 +809,8 @@ Datum
 window_last(PG_FUNCTION_ARGS)
 {
 	ereport(ERROR,
-			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-			 errmsg("cannot use LAST() outside a DEFINE clause")));
+			errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			errmsg("cannot use LAST() outside a DEFINE clause"));
 	PG_RETURN_NULL();			/* not reached */
 }
 
@@ -823,8 +823,8 @@ Datum
 window_first_offset(PG_FUNCTION_ARGS)
 {
 	ereport(ERROR,
-			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-			 errmsg("cannot use FIRST() outside a DEFINE clause")));
+			errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			errmsg("cannot use FIRST() outside a DEFINE clause"));
 	PG_RETURN_NULL();			/* not reached */
 }
 
@@ -837,7 +837,7 @@ Datum
 window_last_offset(PG_FUNCTION_ARGS)
 {
 	ereport(ERROR,
-			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-			 errmsg("cannot use LAST() outside a DEFINE clause")));
+			errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			errmsg("cannot use LAST() outside a DEFINE clause"));
 	PG_RETURN_NULL();			/* not reached */
 }
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0046-Tidy-up-forward-declarations-and-helper-placement.txt (24.2K, 48-nocfbot-0046-Tidy-up-forward-declarations-and-helper-placement.txt)
  download | inline diff:
From 178fe9979b1a5c18e06af67a696e12f0ee9da107 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Fri, 5 Jun 2026 12:32:38 +0900
Subject: [PATCH 46/68] Tidy up forward declarations and helper placement for
 row pattern recognition

Bring the row pattern recognition code in line with the surrounding
conventions for static helpers:

- each static helper has a forward declaration;
- static helpers are defined above the "API exposed" banner, public
  entry points below;
- the forward-declaration block uses a single banner, not per-group
  labels;
- forward declarations follow definition order.

Pure code movement and comments, no behavior change.

Per review comments from Tatsuo Ishii and Jian He.
---
 src/backend/executor/execRPR.c       | 110 +++----
 src/backend/executor/nodeWindowAgg.c | 454 +++++++++++++--------------
 src/backend/optimizer/plan/rpr.c     |  12 +-
 src/backend/parser/parse_rpr.c       |   2 +-
 4 files changed, 289 insertions(+), 289 deletions(-)

diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 580b25f398d..56399c0c7fd 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -83,8 +83,6 @@ static bool nfa_eval_var_match(WindowAggState *winstate,
 							   RPRPatternElement *elem, bool *varMatched);
 static void nfa_match(WindowAggState *winstate, RPRNFAContext *ctx,
 					  bool *varMatched);
-static void nfa_advance_state(WindowAggState *winstate, RPRNFAContext *ctx,
-							  RPRNFAState *state, int64 currentPos);
 static void nfa_route_to_elem(WindowAggState *winstate, RPRNFAContext *ctx,
 							  RPRNFAState *state, RPRPatternElement *nextElem,
 							  int64 currentPos);
@@ -100,9 +98,15 @@ static void nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
 static void nfa_advance_var(WindowAggState *winstate, RPRNFAContext *ctx,
 							RPRNFAState *state, RPRPatternElement *elem,
 							int64 currentPos);
+static void nfa_advance_state(WindowAggState *winstate, RPRNFAContext *ctx,
+							  RPRNFAState *state, int64 currentPos);
 static void nfa_advance(WindowAggState *winstate, RPRNFAContext *ctx,
 						int64 currentPos);
 
+static void nfa_reevaluate_dependent_vars(WindowAggState *winstate,
+										  RPRNFAContext *ctx,
+										  int64 currentPos);
+
 /*
  * NFA-based pattern matching implementation
  *
@@ -1552,6 +1556,57 @@ nfa_advance(WindowAggState *winstate, RPRNFAContext *ctx, int64 currentPos)
 	}
 }
 
+/*
+ * nfa_reevaluate_dependent_vars
+ *		Re-evaluate match_start-dependent DEFINE variables for a specific
+ *		context whose matchStartRow differs from the shared evaluation's
+ *		nav_match_start.
+ *
+ * Only variables in defineMatchStartDependent are re-evaluated.  The
+ * current row's slot (ecxt_outertuple) must already be set up by
+ * nfa_evaluate_row().
+ */
+static void
+nfa_reevaluate_dependent_vars(WindowAggState *winstate, RPRNFAContext *ctx,
+							  int64 currentPos)
+{
+	ExprContext *econtext = winstate->ss.ps.ps_ExprContext;
+	int64		saved_match_start = winstate->nav_match_start;
+	int64		saved_pos = winstate->currentpos;
+	int			varIdx = 0;
+	ListCell   *lc;
+
+	/* Temporarily set nav_match_start and currentpos for FIRST/LAST */
+	winstate->nav_match_start = ctx->matchStartRow;
+	winstate->currentpos = currentPos;
+
+	/* Invalidate nav_slot cache since match_start changed */
+	winstate->nav_slot_pos = -1;
+
+	foreach(lc, winstate->defineClauseList)
+	{
+		if (bms_is_member(varIdx, winstate->defineMatchStartDependent))
+		{
+			ExprState  *exprState = (ExprState *) lfirst(lc);
+			Datum		result;
+			bool		isnull;
+
+			/* Per-tuple context; scratch freed by the per-row reset */
+			result = ExecEvalExprSwitchContext(exprState, econtext, &isnull);
+			winstate->nfaVarMatched[varIdx] = (!isnull && DatumGetBool(result));
+		}
+
+		varIdx++;
+		if (varIdx >= list_length(winstate->defineVariableList))
+			break;
+	}
+
+	/* Restore original match_start, currentpos, and invalidate cache */
+	winstate->nav_match_start = saved_match_start;
+	winstate->currentpos = saved_pos;
+	winstate->nav_slot_pos = -1;
+}
+
 
 /***********************************************************************
  * API exposed to nodeWindowAgg.c
@@ -1696,57 +1751,6 @@ ExecRPRRecordContextFailure(WindowAggState *winstate, int64 failedLen)
 	}
 }
 
-/*
- * nfa_reevaluate_dependent_vars
- *		Re-evaluate match_start-dependent DEFINE variables for a specific
- *		context whose matchStartRow differs from the shared evaluation's
- *		nav_match_start.
- *
- * Only variables in defineMatchStartDependent are re-evaluated.  The
- * current row's slot (ecxt_outertuple) must already be set up by
- * nfa_evaluate_row().
- */
-static void
-nfa_reevaluate_dependent_vars(WindowAggState *winstate, RPRNFAContext *ctx,
-							  int64 currentPos)
-{
-	ExprContext *econtext = winstate->ss.ps.ps_ExprContext;
-	int64		saved_match_start = winstate->nav_match_start;
-	int64		saved_pos = winstate->currentpos;
-	int			varIdx = 0;
-	ListCell   *lc;
-
-	/* Temporarily set nav_match_start and currentpos for FIRST/LAST */
-	winstate->nav_match_start = ctx->matchStartRow;
-	winstate->currentpos = currentPos;
-
-	/* Invalidate nav_slot cache since match_start changed */
-	winstate->nav_slot_pos = -1;
-
-	foreach(lc, winstate->defineClauseList)
-	{
-		if (bms_is_member(varIdx, winstate->defineMatchStartDependent))
-		{
-			ExprState  *exprState = (ExprState *) lfirst(lc);
-			Datum		result;
-			bool		isnull;
-
-			/* Per-tuple context; scratch freed by the per-row reset */
-			result = ExecEvalExprSwitchContext(exprState, econtext, &isnull);
-			winstate->nfaVarMatched[varIdx] = (!isnull && DatumGetBool(result));
-		}
-
-		varIdx++;
-		if (varIdx >= list_length(winstate->defineVariableList))
-			break;
-	}
-
-	/* Restore original match_start, currentpos, and invalidate cache */
-	winstate->nav_match_start = saved_match_start;
-	winstate->currentpos = saved_pos;
-	winstate->nav_slot_pos = -1;
-}
-
 /*
  * ExecRPRProcessRow
  *
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 408bbc120b7..86b39bf7f61 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -4627,6 +4627,233 @@ nfa_evaluate_row(WindowObject winobj, int64 pos, bool *varMatched)
 	return true;				/* Row exists */
 }
 
+/*
+ * WinGetSlotInFrame
+ * slot: TupleTableSlot to store the result
+ * relpos: signed rowcount offset from the seek position
+ * seektype: WINDOW_SEEK_HEAD or WINDOW_SEEK_TAIL
+ * set_mark: If the row is found/in frame and set_mark is true, the mark is
+ *		moved to the row as a side-effect.
+ * isnull: output argument, receives isnull status of result
+ * isout: output argument, set to indicate whether target row position
+ *		is out of frame (can pass NULL if caller doesn't care about this)
+ *
+ * Returns 0 if we successfully got the slot, or nonzero if out of frame.
+ * (isout is also set in the latter case.)
+ */
+static int
+WinGetSlotInFrame(WindowObject winobj, TupleTableSlot *slot,
+				  int relpos, int seektype, bool set_mark,
+				  bool *isnull, bool *isout)
+{
+	WindowAggState *winstate;
+	int64		abs_pos;
+	int64		mark_pos;
+	int64		num_reduced_frame;
+
+	Assert(WindowObjectIsValid(winobj));
+	winstate = winobj->winstate;
+
+	switch (seektype)
+	{
+		case WINDOW_SEEK_CURRENT:
+			elog(ERROR, "WINDOW_SEEK_CURRENT is not supported for WinGetFuncArgInFrame");
+			abs_pos = mark_pos = 0; /* keep compiler quiet */
+			break;
+		case WINDOW_SEEK_HEAD:
+			/* rejecting relpos < 0 is easy and simplifies code below */
+			if (relpos < 0)
+				goto out_of_frame;
+			update_frameheadpos(winstate);
+			abs_pos = winstate->frameheadpos + relpos;
+			mark_pos = abs_pos;
+
+			/*
+			 * Account for exclusion option if one is active, but advance only
+			 * abs_pos not mark_pos.  This prevents changes of the current
+			 * row's peer group from resulting in trying to fetch a row before
+			 * some previous mark position.
+			 *
+			 * Note that in some corner cases such as current row being
+			 * outside frame, these calculations are theoretically too simple,
+			 * but it doesn't matter because we'll end up deciding the row is
+			 * out of frame.  We do not attempt to avoid fetching rows past
+			 * end of frame; that would happen in some cases anyway.
+			 */
+			switch (winstate->frameOptions & FRAMEOPTION_EXCLUSION)
+			{
+				case 0:
+					/* no adjustment needed */
+					break;
+				case FRAMEOPTION_EXCLUDE_CURRENT_ROW:
+					if (abs_pos >= winstate->currentpos &&
+						winstate->currentpos >= winstate->frameheadpos)
+						abs_pos++;
+					break;
+				case FRAMEOPTION_EXCLUDE_GROUP:
+					update_grouptailpos(winstate);
+					if (abs_pos >= winstate->groupheadpos &&
+						winstate->grouptailpos > winstate->frameheadpos)
+					{
+						int64		overlapstart = Max(winstate->groupheadpos,
+													   winstate->frameheadpos);
+
+						abs_pos += winstate->grouptailpos - overlapstart;
+					}
+					break;
+				case FRAMEOPTION_EXCLUDE_TIES:
+					update_grouptailpos(winstate);
+					if (abs_pos >= winstate->groupheadpos &&
+						winstate->grouptailpos > winstate->frameheadpos)
+					{
+						int64		overlapstart = Max(winstate->groupheadpos,
+													   winstate->frameheadpos);
+
+						if (abs_pos == overlapstart)
+							abs_pos = winstate->currentpos;
+						else
+							abs_pos += winstate->grouptailpos - overlapstart - 1;
+					}
+					break;
+				default:
+					elog(ERROR, "unrecognized frame option state: 0x%x",
+						 winstate->frameOptions);
+					break;
+			}
+			num_reduced_frame = row_is_in_reduced_frame(winobj,
+														winstate->frameheadpos);
+			if (num_reduced_frame < 0)
+				goto out_of_frame;
+			else if (num_reduced_frame > 0)
+				if (relpos >= num_reduced_frame)
+					goto out_of_frame;
+			break;
+		case WINDOW_SEEK_TAIL:
+			/* rejecting relpos > 0 is easy and simplifies code below */
+			if (relpos > 0)
+				goto out_of_frame;
+
+			/*
+			 * RPR cares about frame head pos. Need to call
+			 * update_frameheadpos
+			 */
+			update_frameheadpos(winstate);
+
+			update_frametailpos(winstate);
+			abs_pos = winstate->frametailpos - 1 + relpos;
+
+			/*
+			 * Account for exclusion option if one is active.  If there is no
+			 * exclusion, we can safely set the mark at the accessed row.  But
+			 * if there is, we can only mark the frame start, because we can't
+			 * be sure how far back in the frame the exclusion might cause us
+			 * to fetch in future.  Furthermore, we have to actually check
+			 * against frameheadpos here, since it's unsafe to try to fetch a
+			 * row before frame start if the mark might be there already.
+			 */
+			switch (winstate->frameOptions & FRAMEOPTION_EXCLUSION)
+			{
+				case 0:
+					/* no adjustment needed */
+					mark_pos = abs_pos;
+					break;
+				case FRAMEOPTION_EXCLUDE_CURRENT_ROW:
+					if (abs_pos <= winstate->currentpos &&
+						winstate->currentpos < winstate->frametailpos)
+						abs_pos--;
+					update_frameheadpos(winstate);
+					if (abs_pos < winstate->frameheadpos)
+						goto out_of_frame;
+					mark_pos = winstate->frameheadpos;
+					break;
+				case FRAMEOPTION_EXCLUDE_GROUP:
+					update_grouptailpos(winstate);
+					if (abs_pos < winstate->grouptailpos &&
+						winstate->groupheadpos < winstate->frametailpos)
+					{
+						int64		overlapend = Min(winstate->grouptailpos,
+													 winstate->frametailpos);
+
+						abs_pos -= overlapend - winstate->groupheadpos;
+					}
+					update_frameheadpos(winstate);
+					if (abs_pos < winstate->frameheadpos)
+						goto out_of_frame;
+					mark_pos = winstate->frameheadpos;
+					break;
+				case FRAMEOPTION_EXCLUDE_TIES:
+					update_grouptailpos(winstate);
+					if (abs_pos < winstate->grouptailpos &&
+						winstate->groupheadpos < winstate->frametailpos)
+					{
+						int64		overlapend = Min(winstate->grouptailpos,
+													 winstate->frametailpos);
+
+						if (abs_pos == overlapend - 1)
+							abs_pos = winstate->currentpos;
+						else
+							abs_pos -= overlapend - 1 - winstate->groupheadpos;
+					}
+					update_frameheadpos(winstate);
+					if (abs_pos < winstate->frameheadpos)
+						goto out_of_frame;
+					mark_pos = winstate->frameheadpos;
+					break;
+				default:
+					elog(ERROR, "unrecognized frame option state: 0x%x",
+						 winstate->frameOptions);
+					mark_pos = 0;	/* keep compiler quiet */
+					break;
+			}
+
+			num_reduced_frame = row_is_in_reduced_frame(winobj,
+														winstate->frameheadpos);
+			if (num_reduced_frame < 0)
+				goto out_of_frame;
+			else if (num_reduced_frame > 0)
+			{
+				if (-relpos >= num_reduced_frame)
+					goto out_of_frame;
+				abs_pos = winstate->frameheadpos + relpos +
+					num_reduced_frame - 1;
+			}
+			break;
+		default:
+			elog(ERROR, "unrecognized window seek type: %d", seektype);
+			abs_pos = mark_pos = 0; /* keep compiler quiet */
+			break;
+	}
+
+	if (!window_gettupleslot(winobj, abs_pos, slot))
+		goto out_of_frame;
+
+	/* The code above does not detect all out-of-frame cases, so check */
+	if (row_is_in_frame(winobj, abs_pos, slot, false) <= 0)
+		goto out_of_frame;
+
+	if (isout)
+		*isout = false;
+	if (set_mark)
+	{
+		/*
+		 * If RPR is enabled and seek type is WINDOW_SEEK_TAIL, we set the
+		 * mark position unconditionally to frameheadpos. In this case the
+		 * frame always starts at CURRENT_ROW and never goes back, thus
+		 * setting the mark at the position is safe.
+		 */
+		if (winstate->rpPattern != NULL && seektype == WINDOW_SEEK_TAIL)
+			mark_pos = winstate->frameheadpos;
+		WinSetMarkPosition(winobj, mark_pos);
+	}
+	return 0;
+
+out_of_frame:
+	if (isout)
+		*isout = true;
+	*isnull = true;
+	return -1;
+}
+
 
 /***********************************************************************
  * API exposed to window functions
@@ -5019,233 +5246,6 @@ WinGetFuncArgInFrame(WindowObject winobj, int argno,
 	return (Datum) 0;
 }
 
-/*
- * WinGetSlotInFrame
- * slot: TupleTableSlot to store the result
- * relpos: signed rowcount offset from the seek position
- * seektype: WINDOW_SEEK_HEAD or WINDOW_SEEK_TAIL
- * set_mark: If the row is found/in frame and set_mark is true, the mark is
- *		moved to the row as a side-effect.
- * isnull: output argument, receives isnull status of result
- * isout: output argument, set to indicate whether target row position
- *		is out of frame (can pass NULL if caller doesn't care about this)
- *
- * Returns 0 if we successfully got the slot, or nonzero if out of frame.
- * (isout is also set in the latter case.)
- */
-static int
-WinGetSlotInFrame(WindowObject winobj, TupleTableSlot *slot,
-				  int relpos, int seektype, bool set_mark,
-				  bool *isnull, bool *isout)
-{
-	WindowAggState *winstate;
-	int64		abs_pos;
-	int64		mark_pos;
-	int64		num_reduced_frame;
-
-	Assert(WindowObjectIsValid(winobj));
-	winstate = winobj->winstate;
-
-	switch (seektype)
-	{
-		case WINDOW_SEEK_CURRENT:
-			elog(ERROR, "WINDOW_SEEK_CURRENT is not supported for WinGetFuncArgInFrame");
-			abs_pos = mark_pos = 0; /* keep compiler quiet */
-			break;
-		case WINDOW_SEEK_HEAD:
-			/* rejecting relpos < 0 is easy and simplifies code below */
-			if (relpos < 0)
-				goto out_of_frame;
-			update_frameheadpos(winstate);
-			abs_pos = winstate->frameheadpos + relpos;
-			mark_pos = abs_pos;
-
-			/*
-			 * Account for exclusion option if one is active, but advance only
-			 * abs_pos not mark_pos.  This prevents changes of the current
-			 * row's peer group from resulting in trying to fetch a row before
-			 * some previous mark position.
-			 *
-			 * Note that in some corner cases such as current row being
-			 * outside frame, these calculations are theoretically too simple,
-			 * but it doesn't matter because we'll end up deciding the row is
-			 * out of frame.  We do not attempt to avoid fetching rows past
-			 * end of frame; that would happen in some cases anyway.
-			 */
-			switch (winstate->frameOptions & FRAMEOPTION_EXCLUSION)
-			{
-				case 0:
-					/* no adjustment needed */
-					break;
-				case FRAMEOPTION_EXCLUDE_CURRENT_ROW:
-					if (abs_pos >= winstate->currentpos &&
-						winstate->currentpos >= winstate->frameheadpos)
-						abs_pos++;
-					break;
-				case FRAMEOPTION_EXCLUDE_GROUP:
-					update_grouptailpos(winstate);
-					if (abs_pos >= winstate->groupheadpos &&
-						winstate->grouptailpos > winstate->frameheadpos)
-					{
-						int64		overlapstart = Max(winstate->groupheadpos,
-													   winstate->frameheadpos);
-
-						abs_pos += winstate->grouptailpos - overlapstart;
-					}
-					break;
-				case FRAMEOPTION_EXCLUDE_TIES:
-					update_grouptailpos(winstate);
-					if (abs_pos >= winstate->groupheadpos &&
-						winstate->grouptailpos > winstate->frameheadpos)
-					{
-						int64		overlapstart = Max(winstate->groupheadpos,
-													   winstate->frameheadpos);
-
-						if (abs_pos == overlapstart)
-							abs_pos = winstate->currentpos;
-						else
-							abs_pos += winstate->grouptailpos - overlapstart - 1;
-					}
-					break;
-				default:
-					elog(ERROR, "unrecognized frame option state: 0x%x",
-						 winstate->frameOptions);
-					break;
-			}
-			num_reduced_frame = row_is_in_reduced_frame(winobj,
-														winstate->frameheadpos);
-			if (num_reduced_frame < 0)
-				goto out_of_frame;
-			else if (num_reduced_frame > 0)
-				if (relpos >= num_reduced_frame)
-					goto out_of_frame;
-			break;
-		case WINDOW_SEEK_TAIL:
-			/* rejecting relpos > 0 is easy and simplifies code below */
-			if (relpos > 0)
-				goto out_of_frame;
-
-			/*
-			 * RPR cares about frame head pos. Need to call
-			 * update_frameheadpos
-			 */
-			update_frameheadpos(winstate);
-
-			update_frametailpos(winstate);
-			abs_pos = winstate->frametailpos - 1 + relpos;
-
-			/*
-			 * Account for exclusion option if one is active.  If there is no
-			 * exclusion, we can safely set the mark at the accessed row.  But
-			 * if there is, we can only mark the frame start, because we can't
-			 * be sure how far back in the frame the exclusion might cause us
-			 * to fetch in future.  Furthermore, we have to actually check
-			 * against frameheadpos here, since it's unsafe to try to fetch a
-			 * row before frame start if the mark might be there already.
-			 */
-			switch (winstate->frameOptions & FRAMEOPTION_EXCLUSION)
-			{
-				case 0:
-					/* no adjustment needed */
-					mark_pos = abs_pos;
-					break;
-				case FRAMEOPTION_EXCLUDE_CURRENT_ROW:
-					if (abs_pos <= winstate->currentpos &&
-						winstate->currentpos < winstate->frametailpos)
-						abs_pos--;
-					update_frameheadpos(winstate);
-					if (abs_pos < winstate->frameheadpos)
-						goto out_of_frame;
-					mark_pos = winstate->frameheadpos;
-					break;
-				case FRAMEOPTION_EXCLUDE_GROUP:
-					update_grouptailpos(winstate);
-					if (abs_pos < winstate->grouptailpos &&
-						winstate->groupheadpos < winstate->frametailpos)
-					{
-						int64		overlapend = Min(winstate->grouptailpos,
-													 winstate->frametailpos);
-
-						abs_pos -= overlapend - winstate->groupheadpos;
-					}
-					update_frameheadpos(winstate);
-					if (abs_pos < winstate->frameheadpos)
-						goto out_of_frame;
-					mark_pos = winstate->frameheadpos;
-					break;
-				case FRAMEOPTION_EXCLUDE_TIES:
-					update_grouptailpos(winstate);
-					if (abs_pos < winstate->grouptailpos &&
-						winstate->groupheadpos < winstate->frametailpos)
-					{
-						int64		overlapend = Min(winstate->grouptailpos,
-													 winstate->frametailpos);
-
-						if (abs_pos == overlapend - 1)
-							abs_pos = winstate->currentpos;
-						else
-							abs_pos -= overlapend - 1 - winstate->groupheadpos;
-					}
-					update_frameheadpos(winstate);
-					if (abs_pos < winstate->frameheadpos)
-						goto out_of_frame;
-					mark_pos = winstate->frameheadpos;
-					break;
-				default:
-					elog(ERROR, "unrecognized frame option state: 0x%x",
-						 winstate->frameOptions);
-					mark_pos = 0;	/* keep compiler quiet */
-					break;
-			}
-
-			num_reduced_frame = row_is_in_reduced_frame(winobj,
-														winstate->frameheadpos);
-			if (num_reduced_frame < 0)
-				goto out_of_frame;
-			else if (num_reduced_frame > 0)
-			{
-				if (-relpos >= num_reduced_frame)
-					goto out_of_frame;
-				abs_pos = winstate->frameheadpos + relpos +
-					num_reduced_frame - 1;
-			}
-			break;
-		default:
-			elog(ERROR, "unrecognized window seek type: %d", seektype);
-			abs_pos = mark_pos = 0; /* keep compiler quiet */
-			break;
-	}
-
-	if (!window_gettupleslot(winobj, abs_pos, slot))
-		goto out_of_frame;
-
-	/* The code above does not detect all out-of-frame cases, so check */
-	if (row_is_in_frame(winobj, abs_pos, slot, false) <= 0)
-		goto out_of_frame;
-
-	if (isout)
-		*isout = false;
-	if (set_mark)
-	{
-		/*
-		 * If RPR is enabled and seek type is WINDOW_SEEK_TAIL, we set the
-		 * mark position unconditionally to frameheadpos. In this case the
-		 * frame always starts at CURRENT_ROW and never goes back, thus
-		 * setting the mark at the position is safe.
-		 */
-		if (winstate->rpPattern != NULL && seektype == WINDOW_SEEK_TAIL)
-			mark_pos = winstate->frameheadpos;
-		WinSetMarkPosition(winobj, mark_pos);
-	}
-	return 0;
-
-out_of_frame:
-	if (isout)
-		*isout = true;
-	*isnull = true;
-	return -1;
-}
-
 /*
  * WinGetFuncArgCurrent
  *		Evaluate a window function's argument expression on the current row.
diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index 3205559c03a..43114088c3f 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -44,14 +44,12 @@
 #include "nodes/nodeFuncs.h"
 #include "optimizer/rpr.h"
 
-/* Forward declarations - pattern comparison */
+/* Forward declarations */
 static bool rprPatternEqual(RPRPatternNode *a, RPRPatternNode *b);
 static bool rprPatternChildrenEqual(List *a, List *b);
 
-/* Forward declarations - pattern optimization (shared) */
 static RPRPatternNode *tryUnwrapSingleChild(RPRPatternNode *pattern);
 
-/* Forward declarations - SEQ optimization */
 static List *flattenSeqChildren(List *children);
 static List *mergeConsecutiveVars(List *children);
 static List *mergeConsecutiveGroups(List *children);
@@ -59,20 +57,16 @@ static List *mergeConsecutiveAlts(List *children);
 static List *mergeGroupPrefixSuffix(List *children);
 static RPRPatternNode *optimizeSeqPattern(RPRPatternNode *pattern);
 
-/* Forward declarations - ALT optimization */
 static List *flattenAltChildren(List *children);
 static List *removeDuplicateAlternatives(List *children);
 static RPRPatternNode *optimizeAltPattern(RPRPatternNode *pattern);
 
-/* Forward declarations - GROUP optimization */
 static RPRPatternNode *tryMultiplyQuantifiers(RPRPatternNode *pattern);
 static RPRPatternNode *tryUnwrapGroup(RPRPatternNode *pattern);
 static RPRPatternNode *optimizeGroupPattern(RPRPatternNode *pattern);
 
-/* Forward declarations - optimization dispatcher */
 static RPRPatternNode *optimizeRPRPattern(RPRPatternNode *pattern);
 
-/* Forward declarations - pattern compilation */
 static int	collectDefineVariables(List *defineVariableList, char **varNames);
 static void scanRPRPatternRecursive(RPRPatternNode *node, char **varNames,
 									int *numVars, int *numElements,
@@ -92,7 +86,6 @@ static bool fillRPRPattern(RPRPatternNode *node, RPRPattern *pat,
 						   int *idx, RPRDepth depth);
 static void finalizeRPRPattern(RPRPattern *result);
 
-/* Forward declarations - context absorption */
 static bool isFixedLengthChildren(RPRPattern *pattern, RPRElemIdx idx,
 								  RPRDepth scopeDepth);
 static bool isUnboundedStart(RPRPattern *pattern, RPRElemIdx idx);
@@ -101,6 +94,9 @@ static void computeAbsorbabilityRecursive(RPRPattern *pattern,
 										  bool *hasAbsorbable);
 static void computeAbsorbability(RPRPattern *pattern);
 
+static void collectPatternVariablesRecursive(RPRPatternNode *node,
+											 List **varNames);
+
 /*
  * rprPatternEqual
  *		Compare two RPRPatternNode trees for equality.
diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index 3872035110a..c9469b56b7b 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -61,8 +61,8 @@ static void validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
 									   List *rpDefs, List **varNames);
 static List *transformDefineClause(ParseState *pstate, WindowClause *wc,
 								   WindowDef *windef, List **targetlist);
-static bool define_walker(Node *node, void *context);
 static bool nav_volatile_func_checker(Oid funcid, void *context);
+static bool define_walker(Node *node, void *context);
 
 /*
  * transformRPR
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0047-Update-the-varId-documentation-for-row-pattern-re.txt (2.8K, 49-nocfbot-0047-Update-the-varId-documentation-for-row-pattern-re.txt)
  download | inline diff:
From 04f18c586669af22eef26eab7f7cb383f0ccf6d4 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Fri, 5 Jun 2026 12:45:09 +0900
Subject: [PATCH 47/68] Update the varId documentation for row pattern
 recognition

Reserving the whole high varId nibble (0xF0-0xFF) for control elements
left 240 pattern variables (0x00-0xEF), but the documentation still
described the old 0-250 / 252-255 layout.

Update README.rpr's element table and control-code list to the hex
nibble boundaries, note the 0xF0-0xFB spare range, and correct the
user-facing limit in select.sgml from 251 to 240 unique pattern
variables.

Documentation only.
---
 doc/src/sgml/ref/select.sgml    |  2 +-
 src/backend/executor/README.rpr | 15 ++++++++++-----
 2 files changed, 11 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/ref/select.sgml b/doc/src/sgml/ref/select.sgml
index e4708331439..be5ed814853 100644
--- a/doc/src/sgml/ref/select.sgml
+++ b/doc/src/sgml/ref/select.sgml
@@ -1198,7 +1198,7 @@ DEFINE <replaceable class="parameter">definition_variable_name</replaceable> AS
 
    <para>
     Note that the maximum number of unique pattern variables
-    used in the <literal>PATTERN</literal> clause is 251.
+    used in the <literal>PATTERN</literal> clause is 240.
     If this limit is exceeded, an error will be raised.
     Additionally, the maximum nesting depth of pattern groups
     (parentheses) is 253 levels.
diff --git a/src/backend/executor/README.rpr b/src/backend/executor/README.rpr
index 3a215f2566b..55f899f7fef 100644
--- a/src/backend/executor/README.rpr
+++ b/src/backend/executor/README.rpr
@@ -246,7 +246,7 @@ RPRPatternElement struct (16 bytes):
 
   Field      Size     Description
   ---------------------------------------------------------
-  varId      1B      Variable ID (0-250) or control code (252-255)
+  varId      1B      Variable ID (0-0xEF) or control code (0xFC-0xFF)
   depth      1B      Group nesting depth
   flags      1B      Bit flags (see below)
   reserved   1B      Padding
@@ -255,12 +255,17 @@ RPRPatternElement struct (16 bytes):
   next       2B      Next element index (sequential flow)
   jump       2B      Branch target index (for ALT/GROUP)
 
+Pattern variables occupy varId 0 to RPR_VARID_MAX (0xEF) inclusive,
+giving 240 distinct variables.  Any varId with the high nibble set
+(0xF0-0xFF) is reserved for control elements; 0xF0-0xFB are currently
+spare.
+
 Control codes:
 
-  RPR_VARID_BEGIN (252)  Group start marker
-  RPR_VARID_END   (253)  Group end marker
-  RPR_VARID_ALT   (254)  Alternation start marker
-  RPR_VARID_FIN   (255)  Pattern completion marker
+  RPR_VARID_BEGIN (0xFC)  Group start marker
+  RPR_VARID_END   (0xFD)  Group end marker
+  RPR_VARID_ALT   (0xFE)  Alternation start marker
+  RPR_VARID_FIN   (0xFF)  Pattern completion marker
 
 Element flags (1 byte, bitmask):
 
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0048-Reformat-the-design-decisions-chapter-in-the-row-.txt (4.3K, 50-nocfbot-0048-Reformat-the-design-decisions-chapter-in-the-row-.txt)
  download | inline diff:
From c2539388101eaf8c9cc5fa198a3b90d7e9346889 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Fri, 5 Jun 2026 13:07:22 +0900
Subject: [PATCH 48/68] Reformat the design-decisions chapter in the row
 pattern recognition README

Chapter XII recorded each key design decision as a "Choice:" line plus a
bulleted "Rationale:" list.  Rewrite XII-1 through XII-4 as prose instead
-- a short paragraph stating the decision, followed by a paragraph for
its rationale -- to match the rest of the file.  No rationale is dropped.

XII-4 also picks up the detail, noted by Jian He, that the state and
context structures live in a partition-lifespan memory context that is
freed in release_partition.

Documentation only.
---
 src/backend/executor/README.rpr | 54 ++++++++++++++++-----------------
 1 file changed, 26 insertions(+), 28 deletions(-)

diff --git a/src/backend/executor/README.rpr b/src/backend/executor/README.rpr
index 55f899f7fef..df574a0a6f4 100644
--- a/src/backend/executor/README.rpr
+++ b/src/backend/executor/README.rpr
@@ -1320,47 +1320,45 @@ Chapter XII  Summary of Key Design Decisions
 
 XII-1. Flat Array vs Tree-Based NFA
 
-  Choice: Flat array (RPRPatternElement[])
+  The compiled pattern is stored as a flat array of fixed-size 16-byte
+  RPRPatternElement structs rather than as a tree.
 
-  Rationale:
-  - Cache-friendly: 16-byte fixed size, contiguous memory
-  - Index-based references: 2-byte indices instead of pointers
-  - Easy to serialize: can use memcpy when passing to plan nodes
+  The array is contiguous and cache-friendly, elements reference each
+  other by 2-byte index instead of by pointer, and the whole structure
+  can be serialized with memcpy when passed to plan nodes.
 
 XII-2. Forward-only Execution vs Backtracking
 
-  Choice: Forward-only (state set tracking)
+  The NFA is simulated forward-only, tracking a set of live states,
+  rather than by backtracking.
 
-  Rationale:
-  - Backtracking takes exponential time in the worst case
-  - NFA simulation guarantees polynomial time
-  - DFS order naturally guarantees preferment.
-    Greedy/reluctant per quantifier requires only reversing the DFS order
-  - Window functions receive sorted rows sequentially.
-    Forward-only fits directly into this pipeline,
-    whereas backtracking requires re-fetching previous rows
-  - DEFINE conditions are SQL expressions (PREV, RUNNING aggregates, etc.)
-    with high re-evaluation cost. Forward-only requires only one evaluation
-    per row
+  Backtracking would take exponential time in the worst case, whereas
+  forward-only NFA simulation is polynomial.  Forward-only also fits the
+  window pipeline, which delivers sorted rows sequentially: it needs no
+  re-fetching of earlier rows, and each row's DEFINE conditions (SQL
+  expressions such as PREV or running aggregates, with high re-evaluation
+  cost) are evaluated only once.  DFS order yields preferment naturally,
+  with greedy or reluctant behavior per quantifier obtained by reversing
+  that order.
 
 XII-3. Per-Context Management
 
-  Choice: Independent context per start row
+  A separate match context is maintained for each start row.
 
-  Rationale:
-  - Supports overlapping matches under SKIP TO NEXT ROW
-  - Determines the frame for each row independently
-  - Absorption optimization can eliminate redundant contexts in O(n)
+  This supports overlapping matches under SKIP TO NEXT ROW, determines
+  each row's frame independently, and lets the absorption optimization
+  eliminate redundant contexts in O(n).
 
 XII-4. Memory Pool Management
 
-  Choice: Custom free list
+  NFA states are managed through a custom free list, and both RPRNFAState
+  and RPRNFAContext are allocated in a partition-lifespan memory context
+  that is freed in release_partition.
 
-  Rationale:
-  - NFA states are created and destroyed in large numbers per row
-  - Avoids palloc/pfree overhead
-  - State size is variable (counts[] array), but within a single query
-    maxDepth is fixed, so all states have the same size
+  NFA states are created and destroyed in large numbers per row, so the
+  free list avoids palloc/pfree overhead.  Their size varies (the
+  counts[] array), but maxDepth is fixed within a single query, so all
+  states have the same size.
 
 XII-5. Execution Optimization Summary
 
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0049-Point-to-the-absorption-analysis-docs-from-the-RP.txt (1.4K, 51-nocfbot-0049-Point-to-the-absorption-analysis-docs-from-the-RP.txt)
  download | inline diff:
From acc0ebfda053b8206b6c336a68229c7d724aea8d Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Fri, 5 Jun 2026 13:14:01 +0900
Subject: [PATCH 49/68] Point to the absorption-analysis docs from the RPR flag
 definitions

The RPR_ELEM_ABSORBABLE and RPR_ELEM_ABSORBABLE_BRANCH element flags are
described in README.rpr and computed in rpr.c, but the rpr.h definitions
gave no hint of where to look.  Add a short pointer comment to
README.rpr IV-5 ("Absorbability Analysis"), its Appendix C examples, and
computeAbsorbability(), without restating the explanation.

Documentation only.

Per an off-list review comment from Jian He.
---
 src/include/optimizer/rpr.h | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/src/include/optimizer/rpr.h b/src/include/optimizer/rpr.h
index 2fa483320ce..73c827de2b1 100644
--- a/src/include/optimizer/rpr.h
+++ b/src/include/optimizer/rpr.h
@@ -44,6 +44,12 @@
 											 * quantifier */
 #define RPR_ELEM_EMPTY_LOOP			0x02	/* END: group body can produce
 											 * empty match */
+/*
+ * The two absorption flags below are explained in README.rpr IV-5
+ * ("Absorbability Analysis"), with worked examples in Appendix C; the
+ * analysis that sets them is computeAbsorbability() in
+ * optimizer/plan/rpr.c.
+ */
 #define RPR_ELEM_ABSORBABLE_BRANCH	0x04	/* element in absorbable region */
 #define RPR_ELEM_ABSORBABLE			0x08	/* absorption judgment point */
 
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0050-Fix-signed-integer-overflow-in-row-pattern-recogn.txt (5.0K, 52-nocfbot-0050-Fix-signed-integer-overflow-in-row-pattern-recogn.txt)
  download | inline diff:
From 8e806e30fd8a7dcbeb18e13faccd50cdeff0f911 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Fri, 5 Jun 2026 13:31:20 +0900
Subject: [PATCH 50/68] Fix signed-integer overflow in row pattern recognition
 frame-end clamp

ExecRPRProcessRow() computed "frameOffset + 1" before passing it to
pg_add_s64_overflow(), so a FOLLOWING offset near PG_INT64_MAX made
that add overflow on its own -- undefined behavior that crashed
cassert builds and gave wrong results otherwise.

Add the offset and the +1 in two separately checked steps, clamping to
PG_INT64_MAX on overflow, as core WindowAgg already does.  The clamp is
safe because the partition row count is an int64, so no row ever sits
at that position; a huge FOLLOWING offset thus behaves like UNBOUNDED
FOLLOWING.  Also add a regression test.
---
 src/backend/executor/execRPR.c         | 31 +++++++++++++++++++++-----
 src/test/regress/expected/rpr_base.out | 25 +++++++++++++++++++++
 src/test/regress/sql/rpr_base.sql      | 16 +++++++++++++
 3 files changed, 66 insertions(+), 6 deletions(-)

diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 56399c0c7fd..69e3603adef 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -1784,9 +1784,15 @@ ExecRPRProcessRow(WindowAggState *winstate, int64 currentPos,
 		{
 			int64		ctxFrameEnd;
 
-			/* Clamp to INT64_MAX on overflow */
-			if (pg_add_s64_overflow(ctx->matchStartRow, frameOffset + 1,
-									&ctxFrameEnd))
+			/*
+			 * Clamp to INT64_MAX on overflow.  frameOffset can be as large as
+			 * PG_INT64_MAX (e.g. "ROWS <huge> FOLLOWING"), so add the offset
+			 * and the trailing +1 in two separately checked steps to avoid
+			 * signed-integer overflow in the "frameOffset + 1" subexpression.
+			 */
+			if (pg_add_s64_overflow(ctx->matchStartRow, frameOffset,
+									&ctxFrameEnd) ||
+				pg_add_s64_overflow(ctxFrameEnd, 1, &ctxFrameEnd))
 				ctxFrameEnd = PG_INT64_MAX;
 
 			/*
@@ -1844,10 +1850,23 @@ ExecRPRProcessRow(WindowAggState *winstate, int64 currentPos,
 		 * mismatch (nfa_match with NULL), which removes all states (all
 		 * states are at VAR positions after advance). So any surviving
 		 * context here must be within its frame boundary.
+		 *
+		 * Compute the (clamped) frame end the same way as Phase 1, using two
+		 * separately checked adds so that "frameOffset + 1" cannot overflow
+		 * when frameOffset is near PG_INT64_MAX.
 		 */
-		Assert(!hasLimitedFrame ||
-			   ctx->matchStartRow > PG_INT64_MAX - frameOffset - 1 ||
-			   currentPos < ctx->matchStartRow + frameOffset + 1);
+#ifdef USE_ASSERT_CHECKING
+		if (hasLimitedFrame)
+		{
+			int64		ctxFrameEnd;
+
+			if (pg_add_s64_overflow(ctx->matchStartRow, frameOffset,
+									&ctxFrameEnd) ||
+				pg_add_s64_overflow(ctxFrameEnd, 1, &ctxFrameEnd))
+				ctxFrameEnd = PG_INT64_MAX;
+			Assert(currentPos < ctxFrameEnd);
+		}
+#endif
 
 		nfa_advance(winstate, ctx, currentPos);
 	}
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index 1410ba75395..4fe7360114f 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -643,6 +643,31 @@ ORDER BY id;
   6 |  30 |   1
 (6 rows)
 
+-- int64 frame-end overflow: a huge FOLLOWING offset must clamp to the
+-- partition end (matchStartRow + offset + 1 overflows int64; the clamp makes
+-- it behave like UNBOUNDED FOLLOWING).  Guards against signed-integer overflow
+-- in the "frameOffset + 1" subexpression (undefined behavior).  The cnt values
+-- must match the UNBOUNDED FOLLOWING result for the same data.
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_frame
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND 9223372036854775806 FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A+)
+    DEFINE A AS val > 0
+)
+ORDER BY id;
+ id | val | cnt 
+----+-----+-----
+  1 |  10 |   6
+  2 |  10 |   5
+  3 |  10 |   4
+  4 |  20 |   3
+  5 |  20 |   2
+  6 |  30 |   1
+(6 rows)
+
 -- RANGE frame with RPR (not permitted)
 SELECT id, val, COUNT(*) OVER w as cnt
 FROM rpr_frame
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index 53bf090b903..c6fcfa3e9ff 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -515,6 +515,22 @@ WINDOW w AS (
 )
 ORDER BY id;
 
+-- int64 frame-end overflow: a huge FOLLOWING offset must clamp to the
+-- partition end (matchStartRow + offset + 1 overflows int64; the clamp makes
+-- it behave like UNBOUNDED FOLLOWING).  Guards against signed-integer overflow
+-- in the "frameOffset + 1" subexpression (undefined behavior).  The cnt values
+-- must match the UNBOUNDED FOLLOWING result for the same data.
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_frame
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND 9223372036854775806 FOLLOWING
+    AFTER MATCH SKIP TO NEXT ROW
+    PATTERN (A+)
+    DEFINE A AS val > 0
+)
+ORDER BY id;
+
 -- RANGE frame with RPR (not permitted)
 SELECT id, val, COUNT(*) OVER w as cnt
 FROM rpr_frame
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0052-Handle-row-pattern-navigation-nodes-in-exprTypmod.txt (6.0K, 53-nocfbot-0052-Handle-row-pattern-navigation-nodes-in-exprTypmod.txt)
  download | inline diff:
From 8b325c572b63859781879cf645e7eb87521e1118 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Fri, 5 Jun 2026 16:52:38 +0900
Subject: [PATCH 52/68] Handle row pattern navigation nodes in exprTypmod and
 isSimpleNode

RPRNavExpr, the PREV/NEXT/FIRST/LAST navigation node used in a row
pattern DEFINE clause, was missing from two type-helper switches and
fell through to their defaults.

In exprTypmod() the default returns -1, so the argument's typmod was
dropped.  A navigation expression evaluates its argument on another row
without changing its type, so its result has the same type and typmod
as the argument; return the argument's typmod, matching how exprType()
already reports its result type.

In isSimpleNode() the default returns false, which made pretty-printing
wrap a navigation operand in redundant parentheses, for example
val > (PREV(val)).  A navigation expression deparses as a function-like
FUNC(..) call that already delimits itself, so it is simple and needs
no extra parentheses; add it to the function-like group.

This affects display and node completeness only; execution and
re-parsing were already correct, as the extra parentheses were still
valid SQL.  Add a pretty-mode regression using the two-argument
pg_get_viewdef in rpr_base, a path the existing single-argument
navigation tests did not exercise.
---
 src/backend/nodes/nodeFuncs.c          |  3 +++
 src/backend/utils/adt/ruleutils.c      |  1 +
 src/test/regress/expected/rpr_base.out | 27 ++++++++++++++++++++++++++
 src/test/regress/sql/rpr_base.sql      | 13 +++++++++++++
 4 files changed, 44 insertions(+)

diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index 101c03b6ae8..aecf4e4da0a 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -392,6 +392,9 @@ exprTypmod(const Node *expr)
 			return ((const ArrayCoerceExpr *) expr)->resulttypmod;
 		case T_CollateExpr:
 			return exprTypmod((Node *) ((const CollateExpr *) expr)->arg);
+		case T_RPRNavExpr:
+			/* result has the same type/typmod as the argument expression */
+			return exprTypmod((Node *) ((const RPRNavExpr *) expr)->arg);
 		case T_CaseExpr:
 			{
 				/*
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 6237080fb36..2b8439e452e 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -9616,6 +9616,7 @@ isSimpleNode(Node *node, Node *parentNode, int prettyFlags)
 		case T_FuncExpr:
 		case T_JsonConstructorExpr:
 		case T_JsonExpr:
+		case T_RPRNavExpr:
 			/* function-like: name(..) or name[..] */
 			return true;
 
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index 4fe7360114f..fa6d101f874 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -2488,6 +2488,33 @@ SELECT pg_get_viewdef('rpr_serial_nav6'::regclass);
    b AS (NEXT(FIRST(val), (3)::bigint) > 0) );
 (1 row)
 
+-- Pretty deparse: navigation calls are function-like and take no extra parens
+CREATE VIEW rpr_nav_pretty_v AS
+SELECT id, val, count(*) OVER w
+FROM rpr_serial
+WINDOW w AS (ORDER BY id
+             ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN (A B+)
+             DEFINE A AS TRUE,
+                    B AS val > PREV(val) AND PREV(val) IS NOT NULL
+                         AND NEXT(val) > FIRST(val)
+                         AND PREV(FIRST(val)) > 0);
+SELECT pg_get_viewdef('rpr_nav_pretty_v'::regclass, true);
+                                             pg_get_viewdef                                              
+---------------------------------------------------------------------------------------------------------
+  SELECT id,                                                                                            +
+     val,                                                                                               +
+     count(*) OVER w AS count                                                                           +
+    FROM rpr_serial                                                                                     +
+   WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING                            +
+   AFTER MATCH SKIP PAST LAST ROW                                                                       +
+   INITIAL                                                                                              +
+   PATTERN (a b+)                                                                                       +
+   DEFINE                                                                                               +
+   a AS true,                                                                                           +
+   b AS val > PREV(val) AND PREV(val) IS NOT NULL AND NEXT(val) > FIRST(val) AND PREV(FIRST(val)) > 0 );
+(1 row)
+
 -- Reluctant {1}? quantifier deparse through ruleutils
 CREATE VIEW rpr_quant_reluctant_v AS
 SELECT id, val, count(*) OVER w
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index c6fcfa3e9ff..e0af7199629 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -1693,6 +1693,19 @@ WINDOW w AS (ORDER BY id
              DEFINE A AS TRUE, B AS NEXT(FIRST(val), 3) > 0);
 SELECT pg_get_viewdef('rpr_serial_nav6'::regclass);
 
+-- Pretty deparse: navigation calls are function-like and take no extra parens
+CREATE VIEW rpr_nav_pretty_v AS
+SELECT id, val, count(*) OVER w
+FROM rpr_serial
+WINDOW w AS (ORDER BY id
+             ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN (A B+)
+             DEFINE A AS TRUE,
+                    B AS val > PREV(val) AND PREV(val) IS NOT NULL
+                         AND NEXT(val) > FIRST(val)
+                         AND PREV(FIRST(val)) > 0);
+SELECT pg_get_viewdef('rpr_nav_pretty_v'::regclass, true);
+
 -- Reluctant {1}? quantifier deparse through ruleutils
 CREATE VIEW rpr_quant_reluctant_v AS
 SELECT id, val, count(*) OVER w
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0054-Test-deparse-of-an-inline-row-pattern-window.txt (3.4K, 54-nocfbot-0054-Test-deparse-of-an-inline-row-pattern-window.txt)
  download | inline diff:
From c33935aecc31bc25961122e69cd96fd65710ea2a Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Sat, 6 Jun 2026 06:58:01 +0900
Subject: [PATCH 54/68] Test deparse of an inline row pattern window

A window spec written inline (OVER (...) with no WINDOW alias) deparses
through a different get_window_function_node branch than a named
window, and no view exercised it.  Add an inline-OVER view to rpr_base
and capture its viewdef, covering that path.  No code change.
---
 src/test/regress/expected/rpr_base.out | 23 +++++++++++++++++++++++
 src/test/regress/sql/rpr_base.sql      | 10 ++++++++++
 2 files changed, 33 insertions(+)

diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index f086ecc9a5d..c50c7a5f6a8 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -2564,6 +2564,29 @@ SELECT pg_get_viewdef('rpr_serial_quoted'::regclass);
    "Up" AS (val > PREV(val)) );
 (1 row)
 
+-- Inline OVER round-trip: inline window spec (no WINDOW alias) deparses inside OVER (...)
+CREATE VIEW rpr_serial_inline_over AS
+SELECT id, val,
+       count(*) OVER (ORDER BY id
+                      ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+                      PATTERN (A B+)
+                      DEFINE A AS val > 10, B AS val > PREV(val)) AS cnt
+FROM rpr_serial;
+SELECT pg_get_viewdef('rpr_serial_inline_over'::regclass);
+                                  pg_get_viewdef                                  
+----------------------------------------------------------------------------------
+  SELECT id,                                                                     +
+     val,                                                                        +
+     count(*) OVER (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
+   AFTER MATCH SKIP PAST LAST ROW                                                +
+   INITIAL                                                                       +
+   PATTERN (a b+)                                                                +
+   DEFINE                                                                        +
+   a AS (val > 10),                                                              +
+   b AS (val > PREV(val)) ) AS cnt                                               +
+    FROM rpr_serial;
+(1 row)
+
 -- Materialized view (if supported)
 CREATE TABLE rpr_mview (id INT, val INT);
 INSERT INTO rpr_mview VALUES (1, 10), (2, 20), (3, 30);
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index e0af7199629..7dfb72f6bfd 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -1727,6 +1727,16 @@ WINDOW w AS (ORDER BY id
              DEFINE "Start" AS TRUE, "Up" AS val > PREV(val));
 SELECT pg_get_viewdef('rpr_serial_quoted'::regclass);
 
+-- Inline OVER round-trip: inline window spec (no WINDOW alias) deparses inside OVER (...)
+CREATE VIEW rpr_serial_inline_over AS
+SELECT id, val,
+       count(*) OVER (ORDER BY id
+                      ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+                      PATTERN (A B+)
+                      DEFINE A AS val > 10, B AS val > PREV(val)) AS cnt
+FROM rpr_serial;
+SELECT pg_get_viewdef('rpr_serial_inline_over'::regclass);
+
 -- Materialized view (if supported)
 
 CREATE TABLE rpr_mview (id INT, val INT);
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0055-Fix-a-mislabeled-INITIAL-test-in-row-pattern-reco.txt (4.9K, 55-nocfbot-0055-Fix-a-mislabeled-INITIAL-test-in-row-pattern-reco.txt)
  download | inline diff:
From a17415a855ba345bbd189869fb0b50334752c404 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Sat, 6 Jun 2026 07:18:12 +0900
Subject: [PATCH 55/68] Fix a mislabeled INITIAL test in row pattern
 recognition

The "INITIAL Mode" block in rpr_nfa claimed INITIAL was unimplemented
and produced a syntax error.  INITIAL is the default match mode and is
fully supported; the error was only a clause-order violation -- INITIAL
was written before AFTER MATCH SKIP, which the grammar forbids.

Reorder INITIAL after AFTER MATCH SKIP and correct the comments.  The
queries now run and confirm that explicit INITIAL matches the default
mode.  Test-only change.
---
 src/test/regress/expected/rpr_nfa.out | 31 ++++++++++++++++-----------
 src/test/regress/sql/rpr_nfa.sql      | 10 ++++-----
 2 files changed, 23 insertions(+), 18 deletions(-)

diff --git a/src/test/regress/expected/rpr_nfa.out b/src/test/regress/expected/rpr_nfa.out
index 829e8251aed..02a5e517b0e 100644
--- a/src/test/regress/expected/rpr_nfa.out
+++ b/src/test/regress/expected/rpr_nfa.out
@@ -3524,10 +3524,8 @@ ORDER BY mode, id;
 
 -- ============================================================
 -- INITIAL Mode (Runtime)
--- Placeholder: INITIAL is not yet implemented (syntax error).
--- Kept here so tests convert to runtime tests when implemented.
 -- ============================================================
--- INITIAL mode (not yet supported - produces syntax error)
+-- Explicit INITIAL (after AFTER MATCH SKIP, per the grammar); same as the default
 WITH test_initial_mode AS (
     SELECT * FROM (VALUES
         (1, ARRAY['_']),  -- Unmatched
@@ -3544,15 +3542,21 @@ FROM test_initial_mode
 WINDOW w AS (
     ORDER BY id
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    INITIAL
     AFTER MATCH SKIP TO NEXT ROW
+    INITIAL
     PATTERN (A+)
     DEFINE
         A AS 'A' = ANY(flags)
 );
-ERROR:  syntax error at or near "AFTER"
-LINE 18:     AFTER MATCH SKIP TO NEXT ROW
-             ^
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {_}   |             |          
+  2 | {A}   |           2 |         3
+  3 | {A}   |           3 |         3
+  4 | {_}   |             |          
+  5 | {A}   |           5 |         5
+(5 rows)
+
 -- Default mode (include all rows)
 WITH test_default_mode AS (
     SELECT * FROM (VALUES
@@ -3584,7 +3588,7 @@ WINDOW w AS (
   5 | {A}   |           5 |         5
 (5 rows)
 
--- Mode difference verification (INITIAL not yet supported - produces syntax error)
+-- Mode equivalence verification: explicit INITIAL equals the default mode
 WITH test_mode_diff AS (
     SELECT * FROM (VALUES
         (1, ARRAY['_']),
@@ -3598,8 +3602,8 @@ FROM (
     WINDOW w AS (
         ORDER BY id
         ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-        INITIAL
         AFTER MATCH SKIP TO NEXT ROW
+        INITIAL
         PATTERN (A)
         DEFINE A AS 'A' = ANY(flags)
     )
@@ -3617,9 +3621,12 @@ FROM (
     )
 ) sub
 ORDER BY mode;
-ERROR:  syntax error at or near "AFTER"
-LINE 15:         AFTER MATCH SKIP TO NEXT ROW
-                 ^
+  mode   | row_count 
+---------+-----------
+ DEFAULT |         3
+ INITIAL |         3
+(2 rows)
+
 -- ============================================================
 -- Frame Boundary Variations
 -- ============================================================
diff --git a/src/test/regress/sql/rpr_nfa.sql b/src/test/regress/sql/rpr_nfa.sql
index 3bbec496279..213385f143b 100644
--- a/src/test/regress/sql/rpr_nfa.sql
+++ b/src/test/regress/sql/rpr_nfa.sql
@@ -2530,11 +2530,9 @@ ORDER BY mode, id;
 
 -- ============================================================
 -- INITIAL Mode (Runtime)
--- Placeholder: INITIAL is not yet implemented (syntax error).
--- Kept here so tests convert to runtime tests when implemented.
 -- ============================================================
 
--- INITIAL mode (not yet supported - produces syntax error)
+-- Explicit INITIAL (after AFTER MATCH SKIP, per the grammar); same as the default
 WITH test_initial_mode AS (
     SELECT * FROM (VALUES
         (1, ARRAY['_']),  -- Unmatched
@@ -2551,8 +2549,8 @@ FROM test_initial_mode
 WINDOW w AS (
     ORDER BY id
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-    INITIAL
     AFTER MATCH SKIP TO NEXT ROW
+    INITIAL
     PATTERN (A+)
     DEFINE
         A AS 'A' = ANY(flags)
@@ -2581,7 +2579,7 @@ WINDOW w AS (
         A AS 'A' = ANY(flags)
 );
 
--- Mode difference verification (INITIAL not yet supported - produces syntax error)
+-- Mode equivalence verification: explicit INITIAL equals the default mode
 WITH test_mode_diff AS (
     SELECT * FROM (VALUES
         (1, ARRAY['_']),
@@ -2595,8 +2593,8 @@ FROM (
     WINDOW w AS (
         ORDER BY id
         ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-        INITIAL
         AFTER MATCH SKIP TO NEXT ROW
+        INITIAL
         PATTERN (A)
         DEFINE A AS 'A' = ANY(flags)
     )
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0053-Restore-the-error-cursor-for-too-many-row-pattern.txt (2.1K, 56-nocfbot-0053-Restore-the-error-cursor-for-too-many-row-pattern.txt)
  download | inline diff:
From 9a191e6f099df41bc976728d83ba5f8b3190a5a9 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Fri, 5 Jun 2026 22:30:14 +0900
Subject: [PATCH 53/68] Restore the error cursor for too many row pattern
 variables

The "too many pattern variables" error came out without a LINE/caret
cursor: the check locates the cursor via exprLocation(), which had no
T_RPRPatternNode case and fell through to the default returning -1.

Add the case alongside T_RPRNavExpr to return the node's token
location.  Caller and message text are unchanged; only the cursor is
restored.  Update the existing 241-variable rpr_base regression.
---
 src/backend/nodes/nodeFuncs.c          | 3 +++
 src/test/regress/expected/rpr_base.out | 2 ++
 2 files changed, 5 insertions(+)

diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index aecf4e4da0a..6ab227b2b60 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -1452,6 +1452,9 @@ exprLocation(const Node *expr)
 		case T_RPRNavExpr:
 			loc = ((const RPRNavExpr *) expr)->location;
 			break;
+		case T_RPRPatternNode:
+			loc = ((const RPRPatternNode *) expr)->location;
+			break;
 		case T_SubscriptingRef:
 			/* just use container argument's location */
 			loc = exprLocation((Node *) ((const SubscriptingRef *) expr)->refexpr);
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index fa6d101f874..f086ecc9a5d 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -6540,6 +6540,8 @@ WINDOW w AS (
     V231 AS val > 0, V232 AS val > 0, V233 AS val > 0, V234 AS val > 0, V235 AS val > 0, V236 AS val > 0, V237 AS val > 0, V238 AS val > 0, V239 AS val > 0, V240 AS val > 0
 );
 ERROR:  too many pattern variables
+LINE 5: ...V230 V231 V232 V233 V234 V235 V236 V237 V238 V239 V240 V241)
+                                                                  ^
 DETAIL:  Maximum is 240.
 -- Expected: ERROR - too many pattern variables (Maximum is 240)
 -- Test: Pattern nesting at maximum depth (depth 253)
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0051-Rework-row-pattern-EXPLAIN-deparser-to-fix-groupe.txt (77.2K, 57-nocfbot-0051-Rework-row-pattern-EXPLAIN-deparser-to-fix-groupe.txt)
  download

  [text/plain] nocfbot-0056-Reject-invalid-column-references-in-row-pattern-D.txt (7.6K, 58-nocfbot-0056-Reject-invalid-column-references-in-row-pattern-D.txt)
  download | inline diff:
From 37ae9552adb03842984f4305b9b72bb26af3591d Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Sun, 7 Jun 2026 18:27:16 +0900
Subject: [PATCH 56/68] Reject invalid column references in row pattern DEFINE
 clauses

A DEFINE expression could reference an outer query's column (a correlated
reference), which surfaced as the internal "Upper-level Var found where not
expected" error (XX000) raised by pull_var_clause.  Separately, a schema- or
catalog-qualified column reference (three or more name parts) slipped past
the existing two-part classifier and was wrongly accepted.

Reject both in transformColumnRef, right after the reference is resolved: an
outer-level Var (varlevelsup > 0) with ERRCODE_FEATURE_NOT_SUPPORTED, and a
reference with three or more name parts with ERRCODE_SYNTAX_ERROR.  The latter
uses "qualified expression" wording because a composite-typed qualifier
(e.g. "a.items" in (a.items).amount) is not a column name.
---
 src/backend/parser/parse_expr.c   | 24 ++++++++++
 src/test/regress/expected/rpr.out | 74 +++++++++++++++++++++++++++++++
 src/test/regress/sql/rpr.sql      | 61 +++++++++++++++++++++++++
 3 files changed, 159 insertions(+)

diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 2344aaef9ae..f65a270d20e 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -947,6 +947,30 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 		}
 	}
 
+	/*
+	 * Restrict column references in a row pattern DEFINE clause.  node is now
+	 * a successfully resolved reference, so reject the two forms RPR does not
+	 * allow: a correlated reference to an outer query's column, and a
+	 * schema/catalog-qualified reference (three or more name parts).  Simple
+	 * two-part qualifiers (pattern or range variable) are handled earlier,
+	 * before resolution.
+	 */
+	if (pstate->p_expr_kind == EXPR_KIND_RPR_DEFINE)
+	{
+		if (IsA(node, Var) && ((Var *) node)->varlevelsup > 0)
+			ereport(ERROR,
+					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					errmsg("cannot use outer query column in DEFINE clause"),
+					parser_errposition(pstate, cref->location));
+
+		if (list_length(cref->fields) >= 3)
+			ereport(ERROR,
+					errcode(ERRCODE_SYNTAX_ERROR),
+					errmsg("qualified expression \"%s\" is not allowed in DEFINE clause",
+						   NameListToString(cref->fields)),
+					parser_errposition(pstate, cref->location));
+	}
+
 	return node;
 }
 
diff --git a/src/test/regress/expected/rpr.out b/src/test/regress/expected/rpr.out
index 550113700a9..1b409b923dd 100644
--- a/src/test/regress/expected/rpr.out
+++ b/src/test/regress/expected/rpr.out
@@ -1295,6 +1295,80 @@ ERROR:  volatile functions are not allowed in DEFINE clause
 LINE 7:     DEFINE A AS price > nextval('rpr_seq')
                                 ^
 DROP SEQUENCE rpr_seq;
+-- DEFINE cannot reference an outer query's column.  A correlated outer
+-- reference must produce a clean error, not the internal "Upper-level Var"
+-- elog that pull_var_clause would otherwise raise.
+-- Qualified outer reference (o.threshold):
+SELECT * FROM (VALUES (95)) AS o(threshold),
+LATERAL (
+    SELECT price FROM stock
+    WINDOW w AS (
+        PARTITION BY company
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        INITIAL
+        PATTERN (A)
+        DEFINE A AS price > o.threshold
+    )
+) s;
+ERROR:  cannot use outer query column in DEFINE clause
+LINE 9:         DEFINE A AS price > o.threshold
+                                    ^
+-- Unqualified name resolving to the outer column (threshold):
+SELECT * FROM (VALUES (95)) AS o(threshold),
+LATERAL (
+    SELECT price FROM stock
+    WINDOW w AS (
+        PARTITION BY company
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        INITIAL
+        PATTERN (A)
+        DEFINE A AS price > threshold
+    )
+) s;
+ERROR:  cannot use outer query column in DEFINE clause
+LINE 9:         DEFINE A AS price > threshold
+                                    ^
+-- Outer reference inside a navigation argument is rejected too:
+SELECT * FROM (VALUES (95)) AS o(threshold),
+LATERAL (
+    SELECT price FROM stock
+    WINDOW w AS (
+        PARTITION BY company ORDER BY tdate
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A+)
+        DEFINE A AS PREV(o.threshold, 1) > 0
+    )
+) s;
+ERROR:  cannot use outer query column in DEFINE clause
+LINE 8:         DEFINE A AS PREV(o.threshold, 1) > 0
+                                 ^
+-- DEFINE rejects a schema-qualified column reference (three or more name
+-- parts) once it resolves; the qualified form itself is not allowed.  (stock
+-- is a temp table, so it is qualified with pg_temp here.)
+-- 3-part (schema.table.column):
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS pg_temp.stock.price > 0
+);
+ERROR:  qualified expression "pg_temp.stock.price" is not allowed in DEFINE clause
+LINE 7:     DEFINE A AS pg_temp.stock.price > 0
+                        ^
+-- whole-row variant (schema.table.*):
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS (pg_temp.stock.*) IS NOT NULL
+);
+ERROR:  qualified expression "pg_temp.stock.*" is not allowed in DEFINE clause
+LINE 7:     DEFINE A AS (pg_temp.stock.*) IS NOT NULL
+                         ^
 --
 -- 2-arg PREV/NEXT: functional tests
 --
diff --git a/src/test/regress/sql/rpr.sql b/src/test/regress/sql/rpr.sql
index 0aa17f01e84..56dff9b6725 100644
--- a/src/test/regress/sql/rpr.sql
+++ b/src/test/regress/sql/rpr.sql
@@ -667,6 +667,67 @@ WINDOW w AS (
 );
 DROP SEQUENCE rpr_seq;
 
+-- DEFINE cannot reference an outer query's column.  A correlated outer
+-- reference must produce a clean error, not the internal "Upper-level Var"
+-- elog that pull_var_clause would otherwise raise.
+-- Qualified outer reference (o.threshold):
+SELECT * FROM (VALUES (95)) AS o(threshold),
+LATERAL (
+    SELECT price FROM stock
+    WINDOW w AS (
+        PARTITION BY company
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        INITIAL
+        PATTERN (A)
+        DEFINE A AS price > o.threshold
+    )
+) s;
+-- Unqualified name resolving to the outer column (threshold):
+SELECT * FROM (VALUES (95)) AS o(threshold),
+LATERAL (
+    SELECT price FROM stock
+    WINDOW w AS (
+        PARTITION BY company
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        INITIAL
+        PATTERN (A)
+        DEFINE A AS price > threshold
+    )
+) s;
+-- Outer reference inside a navigation argument is rejected too:
+SELECT * FROM (VALUES (95)) AS o(threshold),
+LATERAL (
+    SELECT price FROM stock
+    WINDOW w AS (
+        PARTITION BY company ORDER BY tdate
+        ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+        PATTERN (A+)
+        DEFINE A AS PREV(o.threshold, 1) > 0
+    )
+) s;
+
+-- DEFINE rejects a schema-qualified column reference (three or more name
+-- parts) once it resolves; the qualified form itself is not allowed.  (stock
+-- is a temp table, so it is qualified with pg_temp here.)
+-- 3-part (schema.table.column):
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS pg_temp.stock.price > 0
+);
+-- whole-row variant (schema.table.*):
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS (pg_temp.stock.*) IS NOT NULL
+);
+
 --
 -- 2-arg PREV/NEXT: functional tests
 --
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0058-Compare-varno-when-preserving-DEFINE-referenced-c.txt (4.8K, 59-nocfbot-0058-Compare-varno-when-preserving-DEFINE-referenced-c.txt)
  download | inline diff:
From fcb8bccafe66cf7f1ded30e5237bb157a94c82e5 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Sun, 7 Jun 2026 20:38:00 +0900
Subject: [PATCH 58/68] Compare varno when preserving DEFINE-referenced columns
 in row pattern recognition

remove_unused_subquery_outputs keeps subquery output columns that an RPR
DEFINE clause references.  The check compared only varattno, so an unrelated
output column of a different relation that happened to share an attribute
number was over-retained and propagated through the plan.

Also compare varno (and, as paranoia, varlevelsup), matching the pattern
already used in set_function_pathlist.  The result was already correct; this
just stops retaining the needless column.
---
 src/backend/optimizer/path/allpaths.c         | 13 ++++++-
 src/test/regress/expected/rpr_integration.out | 35 +++++++++++++++++++
 src/test/regress/sql/rpr_integration.sql      | 16 +++++++++
 3 files changed, 63 insertions(+), 1 deletion(-)

diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c
index fb0506933cd..22339f7491f 100644
--- a/src/backend/optimizer/path/allpaths.c
+++ b/src/backend/optimizer/path/allpaths.c
@@ -4776,7 +4776,18 @@ remove_unused_subquery_outputs(Query *subquery, RelOptInfo *rel,
 					{
 						Var		   *dvar = (Var *) lfirst(vlc);
 
-						if (dvar->varattno == var->varattno)
+						/*
+						 * Match varno as well as varattno: a Var pulled from
+						 * a DEFINE clause can share an attribute number with
+						 * an unrelated output column of a different relation,
+						 * which would otherwise be over-retained.  Checking
+						 * varlevelsup is just paranoia, since outer
+						 * references in DEFINE are rejected during parse
+						 * analysis.
+						 */
+						if (dvar->varno == var->varno &&
+							dvar->varattno == var->varattno &&
+							dvar->varlevelsup == var->varlevelsup)
 						{
 							needed_by_define = true;
 							break;
diff --git a/src/test/regress/expected/rpr_integration.out b/src/test/regress/expected/rpr_integration.out
index b598ef95776..2133e2dfe13 100644
--- a/src/test/regress/expected/rpr_integration.out
+++ b/src/test/regress/expected/rpr_integration.out
@@ -1481,6 +1481,41 @@ ORDER BY o.id;
  10 |  45 |         2
 (10 rows)
 
+-- A column referenced only by DEFINE must not keep an unrelated column that
+-- merely shares its attribute number.  DEFINE references a (rpr_over1); c
+-- (rpr_over2) has the same attno but is unused, so it must be dropped.
+CREATE TABLE rpr_over1 (a int);
+CREATE TABLE rpr_over2 (c int);
+INSERT INTO rpr_over1 VALUES (1),(2),(3);
+INSERT INTO rpr_over2 VALUES (1),(2),(3);
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT cnt FROM (
+  SELECT a AS oa, c AS oc, count(*) OVER w AS cnt
+  FROM rpr_over1 CROSS JOIN rpr_over2
+  WINDOW w AS (ORDER BY a ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+               PATTERN (X+) DEFINE X AS a > 0)
+) s;
+                                          QUERY PLAN                                          
+----------------------------------------------------------------------------------------------
+ Subquery Scan on s
+   Output: s.cnt
+   ->  WindowAgg
+         Output: rpr_over1.a, NULL::integer, count(*) OVER w
+         Window: w AS (ORDER BY rpr_over1.a ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+         Pattern: x+"
+         Nav Mark Lookback: 0
+         ->  Sort
+               Output: rpr_over1.a
+               Sort Key: rpr_over1.a
+               ->  Nested Loop
+                     Output: rpr_over1.a
+                     ->  Seq Scan on public.rpr_over1
+                           Output: rpr_over1.a
+                     ->  Materialize
+                           ->  Seq Scan on public.rpr_over2
+(16 rows)
+
+DROP TABLE rpr_over1, rpr_over2;
 -- Cleanup
 DROP TABLE rpr_integ;
 DROP TABLE rpr_integ2;
diff --git a/src/test/regress/sql/rpr_integration.sql b/src/test/regress/sql/rpr_integration.sql
index 5f3853becba..24b0b1811b9 100644
--- a/src/test/regress/sql/rpr_integration.sql
+++ b/src/test/regress/sql/rpr_integration.sql
@@ -929,6 +929,22 @@ SELECT o.id, o.val,
 FROM rpr_integ o
 ORDER BY o.id;
 
+-- A column referenced only by DEFINE must not keep an unrelated column that
+-- merely shares its attribute number.  DEFINE references a (rpr_over1); c
+-- (rpr_over2) has the same attno but is unused, so it must be dropped.
+CREATE TABLE rpr_over1 (a int);
+CREATE TABLE rpr_over2 (c int);
+INSERT INTO rpr_over1 VALUES (1),(2),(3);
+INSERT INTO rpr_over2 VALUES (1),(2),(3);
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT cnt FROM (
+  SELECT a AS oa, c AS oc, count(*) OVER w AS cnt
+  FROM rpr_over1 CROSS JOIN rpr_over2
+  WINDOW w AS (ORDER BY a ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+               PATTERN (X+) DEFINE X AS a > 0)
+) s;
+DROP TABLE rpr_over1, rpr_over2;
+
 -- Cleanup
 DROP TABLE rpr_integ;
 DROP TABLE rpr_integ2;
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0057-Fix-shortest-match-for-reluctant-nullable-quantif.txt (10.2K, 60-nocfbot-0057-Fix-shortest-match-for-reluctant-nullable-quantif.txt)
  download | inline diff:
From 426451992a4a865203f4fed65a5d38839bc1402b Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Sun, 7 Jun 2026 19:38:16 +0900
Subject: [PATCH 57/68] Fix shortest match for reluctant nullable quantifiers
 in row pattern recognition

When a reluctant outer quantifier wrapped a nullable reluctant body, such as
(A??)+?, the match consumed rows instead of producing the required shortest
(empty) match.

nfa_advance_end decides a group's repeat-or-exit by comparing count with the
quantifier's min and max.  The count < min branch always routed the loop-back
(real match) before the fast-forward exit and never suppressed it, so a longer
match could replace the shortest one.  The sibling min <= count < max branch
already handles this correctly for reluctant groups, leaving the two branches
asymmetric.

Split the count < min branch into reluctant and greedy cases, mirroring the
sibling: a reluctant group takes the fast-forward exit first and, if it reaches
FIN, frees the loop-back state so a longer match cannot replace the shortest
one.  Greedy and non-nullable groups keep the existing loop-first behaviour.
---
 src/backend/executor/execRPR.c        | 72 +++++++++++++++++----------
 src/test/regress/expected/rpr_nfa.out | 33 ++++++++++++
 src/test/regress/sql/rpr_nfa.sql      | 26 ++++++++++
 3 files changed, 106 insertions(+), 25 deletions(-)

diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 69e3603adef..b7e3c4a8274 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -1139,43 +1139,32 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
 	{
 		RPRPatternElement *jumpElem;
 		RPRNFAState *ffState = NULL;
+		RPRPatternElement *nextElem = NULL;
 
 		/*----------
-		 * Two paths are explored in parallel when the group body is nullable
+		 * Two paths are explored when the group body is nullable
 		 * (RPR_ELEM_EMPTY_LOOP):
 		 *
-		 * 1. Primary path: loop back and attempt real matches in the
-		 *    next iteration (state, modified below).
+		 * 1. Loop-back path: attempt real matches in the next iteration
+		 *    (state, modified below).
 		 *
-		 * 2. Fast-forward path: skip directly to after the group,
-		 *    treating all remaining required iterations as empty
-		 *    matches (ffState, handled after the primary path).
+		 * 2. Fast-forward path: skip directly to after the group, treating
+		 *    all remaining required iterations as empty matches (ffState).
+		 *    Route to elem->next (not nfa_advance_end) to avoid creating
+		 *    competing greedy/reluctant loop states.
 		 *
-		 * The snapshot must be taken BEFORE modifying state for the loop-back,
-		 * since both paths diverge from the same point.
+		 * Greedy prefers the loop-back first (more iterations); reluctant
+		 * prefers the fast-forward (exit) first and, if it reaches FIN, drops
+		 * the loop-back so a longer match cannot replace the shortest one --
+		 * mirroring the min<=count<max branch below.  The ffState snapshot is
+		 * taken BEFORE modifying state, since both paths diverge from here.
 		 *----------
 		 */
 		if (RPRElemCanEmptyLoop(elem))
+		{
 			ffState = nfa_state_clone(winstate, state->elemIdx,
 									  state->counts, state->isAbsorbable);
 
-		/* Primary path: loop back for real matches */
-		state->elemIdx = elem->jump;
-		jumpElem = &elements[state->elemIdx];
-		nfa_route_to_elem(winstate, ctx, state, jumpElem,
-						  currentPos);
-
-		/*
-		 * Fast-forward path for nullable bodies.  E.g. (A?){2,3} when A
-		 * doesn't match: the primary loop-back produces empty iterations that
-		 * cycle detection would kill.  Instead, exit directly with count
-		 * satisfied.  Route to elem->next (not nfa_advance_end) to avoid
-		 * creating competing greedy/reluctant loop states.
-		 */
-		if (ffState != NULL)
-		{
-			RPRPatternElement *nextElem;
-
 			/* Exit the group: clear its own count (count-clear policy) */
 			ffState->counts[depth] = 0;
 			ffState->elemIdx = elem->next;
@@ -1192,9 +1181,42 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
 			if (RPRElemIsEnd(nextElem) &&
 				ffState->counts[nextElem->depth] < RPR_COUNT_MAX)
 				ffState->counts[nextElem->depth]++;
+		}
+
+		/* Prepare the loop-back state */
+		state->elemIdx = elem->jump;
+		jumpElem = &elements[state->elemIdx];
 
+		if (ffState != NULL && RPRElemIsReluctant(elem))
+		{
+			RPRNFAState *savedMatch = ctx->matchedState;
+
+			/* Reluctant: take the fast-forward (exit) first */
 			nfa_route_to_elem(winstate, ctx, ffState, nextElem,
 							  currentPos);
+
+			/*
+			 * If the exit reached FIN, the shortest match is found.  Skip the
+			 * loop-back to prevent longer matches from replacing it.
+			 */
+			if (ctx->matchedState != savedMatch)
+			{
+				nfa_state_free(winstate, state);
+				return;
+			}
+
+			/* Loop-back second */
+			nfa_route_to_elem(winstate, ctx, state, jumpElem,
+							  currentPos);
+		}
+		else
+		{
+			/* Greedy (or non-nullable): loop-back first, fast-forward second */
+			nfa_route_to_elem(winstate, ctx, state, jumpElem,
+							  currentPos);
+			if (ffState != NULL)
+				nfa_route_to_elem(winstate, ctx, ffState, nextElem,
+								  currentPos);
 		}
 	}
 	else if (elem->max != RPR_QUANTITY_INF && count >= elem->max)
diff --git a/src/test/regress/expected/rpr_nfa.out b/src/test/regress/expected/rpr_nfa.out
index 02a5e517b0e..3b9975a83df 100644
--- a/src/test/regress/expected/rpr_nfa.out
+++ b/src/test/regress/expected/rpr_nfa.out
@@ -1255,6 +1255,39 @@ WINDOW w AS (
   3 | {C}   |             |          
 (3 rows)
 
+-- Reluctant outer quantifier over a nullable reluctant body: SQL/RPR
+-- semantics call for the shortest (empty) match.  The count<min branch of
+-- nfa_advance_end must prefer the fast-forward (exit) path for reluctant
+-- groups and suppress longer matches once exit reaches FIN, mirroring the
+-- sibling min<=count<max branch.  The 2-level greedy/reluctant matrix plus a
+-- min>=2 boundary and single-quantifier controls localize the behaviour: only
+-- the all-reluctant case (rr) should differ.
+WITH t(id, isa) AS (VALUES (1, true), (2, true), (3, true), (4, false))
+SELECT id,
+       count(*) OVER gg  AS gg,     -- (A?)+      greedy / greedy
+       count(*) OVER gr  AS gr,     -- (A??)+     greedy / reluctant
+       count(*) OVER rg  AS rg,     -- (A?)+?     reluctant / greedy
+       count(*) OVER rr  AS rr,     -- (A??)+?    reluctant / reluctant
+       count(*) OVER rr2 AS rr2,    -- (A??){2,}? reluctant, min>=2 boundary
+       count(*) OVER ca  AS ca,     -- A??        single reluctant control
+       count(*) OVER cs  AS cs      -- A*?        single reluctant control
+FROM t
+WINDOW gg  AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A?)+)      DEFINE A AS isa),
+       gr  AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A??)+)     DEFINE A AS isa),
+       rg  AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A?)+?)     DEFINE A AS isa),
+       rr  AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A??)+?)    DEFINE A AS isa),
+       rr2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A??){2,}?) DEFINE A AS isa),
+       ca  AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A??)        DEFINE A AS isa),
+       cs  AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*?)        DEFINE A AS isa)
+ORDER BY id;
+ id | gg | gr | rg | rr | rr2 | ca | cs 
+----+----+----+----+----+-----+----+----
+  1 |  3 |  3 |  1 |  0 |   0 |  0 |  0
+  2 |  0 |  0 |  1 |  0 |   0 |  0 |  0
+  3 |  0 |  0 |  1 |  0 |   0 |  0 |  0
+  4 |  0 |  0 |  0 |  0 |   0 |  0 |  0
+(4 rows)
+
 -- Non-leading reluctant optional GROUP with a follower: (B (A X)?? C)
 -- Like the VAR case above but a multi-element group; it goes through the
 -- begin path (nfa_advance_begin), which already honors reluctant ordering.
diff --git a/src/test/regress/sql/rpr_nfa.sql b/src/test/regress/sql/rpr_nfa.sql
index 213385f143b..61072d1d6f1 100644
--- a/src/test/regress/sql/rpr_nfa.sql
+++ b/src/test/regress/sql/rpr_nfa.sql
@@ -887,6 +887,32 @@ WINDOW w AS (
         C AS 'C' = ANY(flags)
 );
 
+-- Reluctant outer quantifier over a nullable reluctant body: SQL/RPR
+-- semantics call for the shortest (empty) match.  The count<min branch of
+-- nfa_advance_end must prefer the fast-forward (exit) path for reluctant
+-- groups and suppress longer matches once exit reaches FIN, mirroring the
+-- sibling min<=count<max branch.  The 2-level greedy/reluctant matrix plus a
+-- min>=2 boundary and single-quantifier controls localize the behaviour: only
+-- the all-reluctant case (rr) should differ.
+WITH t(id, isa) AS (VALUES (1, true), (2, true), (3, true), (4, false))
+SELECT id,
+       count(*) OVER gg  AS gg,     -- (A?)+      greedy / greedy
+       count(*) OVER gr  AS gr,     -- (A??)+     greedy / reluctant
+       count(*) OVER rg  AS rg,     -- (A?)+?     reluctant / greedy
+       count(*) OVER rr  AS rr,     -- (A??)+?    reluctant / reluctant
+       count(*) OVER rr2 AS rr2,    -- (A??){2,}? reluctant, min>=2 boundary
+       count(*) OVER ca  AS ca,     -- A??        single reluctant control
+       count(*) OVER cs  AS cs      -- A*?        single reluctant control
+FROM t
+WINDOW gg  AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A?)+)      DEFINE A AS isa),
+       gr  AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A??)+)     DEFINE A AS isa),
+       rg  AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A?)+?)     DEFINE A AS isa),
+       rr  AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A??)+?)    DEFINE A AS isa),
+       rr2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A??){2,}?) DEFINE A AS isa),
+       ca  AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A??)        DEFINE A AS isa),
+       cs  AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*?)        DEFINE A AS isa)
+ORDER BY id;
+
 -- Non-leading reluctant optional GROUP with a follower: (B (A X)?? C)
 -- Like the VAR case above but a multi-element group; it goes through the
 -- begin path (nfa_advance_begin), which already honors reluctant ordering.
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0059-Allow-a-row-pattern-quantifier-with-no-space-befo.txt (40.8K, 61-nocfbot-0059-Allow-a-row-pattern-quantifier-with-no-space-befo.txt)
  download | inline diff:
From fcd882526042fa7bec72c0658f02b8dddd2109af Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Mon, 8 Jun 2026 11:23:03 +0900
Subject: [PATCH 59/68] Allow a row pattern quantifier with no space before the
 alternation operator

The scanner lexes an operator such as "*|" as a single token, so a PATTERN
like (A*|B) previously failed with "unsupported quantifier".  Recognize these
glued forms in the quantifier rules and, once the surrounding sequence is
built, re-split it at the affected term so "|" keeps its lowest precedence;
(A*|B) now parses identically to the spaced (A* | B).  This covers the op-char
quantifiers (*| +| ?| *?| +?| ??|), the reluctant range forms ({n}?| and the
like), and mixed spacing such as "A* ?|B".  A dangling "|" with no right-hand
pattern is still rejected.
---
 src/backend/parser/gram.y              | 195 +++++++++++++++++++--
 src/backend/parser/parse_rpr.c         |   6 +
 src/include/nodes/parsenodes.h         |   8 +
 src/test/regress/expected/rpr_base.out | 228 +++++++++++++++++++++++++
 src/test/regress/sql/rpr_base.sql      | 112 ++++++++++++
 5 files changed, 535 insertions(+), 14 deletions(-)

diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index a2fafb717cd..147b5f37293 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -210,6 +210,8 @@ static void preprocess_pub_all_objtype_list(List *all_objects_list,
 static void preprocess_pubobj_list(List *pubobjspec_list,
 								   core_yyscan_t yyscanner);
 static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
+static RPRPatternNode *makeRPRSeqOrSingle(List *children, int location);
+static RPRPatternNode *splitRPRTrailingAlt(RPRPatternNode *node, core_yyscan_t yyscanner);
 static RPRPatternNode *makeRPRQuantifier(int min, int max, ParseLoc reluctant, int location,
 									   core_yyscan_t yyscanner);
 
@@ -17624,23 +17626,30 @@ row_pattern:
 		;
 
 row_pattern_alt:
-			row_pattern_seq						{ $$ = $1; }
+			row_pattern_seq
+				{
+					$$ = (Node *) splitRPRTrailingAlt((RPRPatternNode *) $1,
+													 yyscanner);
+				}
 			| row_pattern_alt '|' row_pattern_seq
 				{
 					RPRPatternNode *n;
+					RPRPatternNode *rhs = splitRPRTrailingAlt((RPRPatternNode *) $3,
+															 yyscanner);
+
 					/* If left side is already ALT, append to it */
 					if (IsA($1, RPRPatternNode) &&
 						((RPRPatternNode *) $1)->nodeType == RPR_PATTERN_ALT)
 					{
 						n = (RPRPatternNode *) $1;
-						n->children = lappend(n->children, $3);
+						n->children = lappend(n->children, rhs);
 						$$ = (Node *) n;
 					}
 					else
 					{
 						n = makeNode(RPRPatternNode);
 						n->nodeType = RPR_PATTERN_ALT;
-						n->children = list_make2($1, $3);
+						n->children = list_make2($1, rhs);
 						n->min = 1;
 						n->max = 1;
 						n->reluctant = false;
@@ -17656,7 +17665,12 @@ row_pattern_seq:
 			| row_pattern_seq row_pattern_term
 				{
 					RPRPatternNode *n;
-					/* If left side is already SEQ, append to it */
+
+					/*
+					 * If left side is already SEQ, append to it.  A glued
+					 * quantifier's trailing_alt stays on the child term;
+					 * row_pattern_alt splits on it once the seq is complete.
+					 */
 					if (IsA($1, RPRPatternNode) &&
 						((RPRPatternNode *) $1)->nodeType == RPR_PATTERN_SEQ)
 					{
@@ -17689,6 +17703,7 @@ row_pattern_term:
 					n->max = q->max;
 					n->reluctant = q->reluctant;
 					n->reluctant_location = q->reluctant_location;
+					n->trailing_alt = q->trailing_alt;
 					$$ = (Node *) n;
 				}
 		;
@@ -17739,6 +17754,36 @@ row_pattern_quantifier_opt:
 						$$ = (Node *) makeRPRQuantifier(1, INT_MAX, @1 + 1, @1, yyscanner);
 					else if (strcmp($1, "??") == 0)
 						$$ = (Node *) makeRPRQuantifier(0, 1, @1 + 1, @1, yyscanner);
+					else if (strcmp($1, "*|") == 0)
+					{
+						$$ = (Node *) makeRPRQuantifier(0, INT_MAX, -1, @1, yyscanner);
+						((RPRPatternNode *) $$)->trailing_alt = true;
+					}
+					else if (strcmp($1, "+|") == 0)
+					{
+						$$ = (Node *) makeRPRQuantifier(1, INT_MAX, -1, @1, yyscanner);
+						((RPRPatternNode *) $$)->trailing_alt = true;
+					}
+					else if (strcmp($1, "?|") == 0)
+					{
+						$$ = (Node *) makeRPRQuantifier(0, 1, -1, @1, yyscanner);
+						((RPRPatternNode *) $$)->trailing_alt = true;
+					}
+					else if (strcmp($1, "*?|") == 0)
+					{
+						$$ = (Node *) makeRPRQuantifier(0, INT_MAX, @1 + 1, @1, yyscanner);
+						((RPRPatternNode *) $$)->trailing_alt = true;
+					}
+					else if (strcmp($1, "+?|") == 0)
+					{
+						$$ = (Node *) makeRPRQuantifier(1, INT_MAX, @1 + 1, @1, yyscanner);
+						((RPRPatternNode *) $$)->trailing_alt = true;
+					}
+					else if (strcmp($1, "??|") == 0)
+					{
+						$$ = (Node *) makeRPRQuantifier(0, 1, @1 + 1, @1, yyscanner);
+						((RPRPatternNode *) $$)->trailing_alt = true;
+					}
 					else
 						ereport(ERROR,
 								errcode(ERRCODE_SYNTAX_ERROR),
@@ -17749,33 +17794,60 @@ row_pattern_quantifier_opt:
 			/* RELUCTANT quantifiers (when lexer separates tokens) */
 			| '*' Op
 				{
-					if (strcmp($2, "?") != 0)
+					if (strcmp($2, "?") == 0)
+						$$ = (Node *) makeRPRQuantifier(0, INT_MAX, @2, @1, yyscanner);
+					else if (strcmp($2, "?|") == 0)
+					{
+						/* "A* ?|B" = reluctant "A*?" plus alternation */
+						$$ = (Node *) makeRPRQuantifier(0, INT_MAX, @2, @1, yyscanner);
+						((RPRPatternNode *) $$)->trailing_alt = true;
+					}
+					else
 						ereport(ERROR,
 								errcode(ERRCODE_SYNTAX_ERROR),
 								errmsg("invalid token after \"*\" quantifier"),
 								errhint("Did you mean \"*?\" for reluctant quantifier?"),
 								parser_errposition(@2));
-					$$ = (Node *) makeRPRQuantifier(0, INT_MAX, @2, @1, yyscanner);
 				}
 			| '+' Op
 				{
-					if (strcmp($2, "?") != 0)
+					if (strcmp($2, "?") == 0)
+						$$ = (Node *) makeRPRQuantifier(1, INT_MAX, @2, @1, yyscanner);
+					else if (strcmp($2, "?|") == 0)
+					{
+						/* "A+ ?|B" = reluctant "A+?" plus alternation */
+						$$ = (Node *) makeRPRQuantifier(1, INT_MAX, @2, @1, yyscanner);
+						((RPRPatternNode *) $$)->trailing_alt = true;
+					}
+					else
 						ereport(ERROR,
 								errcode(ERRCODE_SYNTAX_ERROR),
 								errmsg("invalid token after \"+\" quantifier"),
 								errhint("Did you mean \"+?\" for reluctant quantifier?"),
 								parser_errposition(@2));
-					$$ = (Node *) makeRPRQuantifier(1, INT_MAX, @2, @1, yyscanner);
 				}
 			| Op Op
 				{
-					if (strcmp($1, "?") != 0 || strcmp($2, "?") != 0)
+					if (strcmp($1, "?") != 0)
+						ereport(ERROR,
+								errcode(ERRCODE_SYNTAX_ERROR),
+								errmsg("invalid quantifier combination"),
+								errhint("Did you mean \"??\" for reluctant quantifier?"),
+								parser_errposition(@1));
+					if (strcmp($2, "?") == 0)
+						$$ = (Node *) makeRPRQuantifier(0, 1, @2, @1, yyscanner);
+					else if (strcmp($2, "?|") == 0)
+					{
+						/* "A? ?|B" = reluctant "A??" plus alternation */
+						$$ = (Node *) makeRPRQuantifier(0, 1, @2, @1, yyscanner);
+						((RPRPatternNode *) $$)->trailing_alt = true;
+					}
+					else
 						ereport(ERROR,
 								errcode(ERRCODE_SYNTAX_ERROR),
 								errmsg("invalid quantifier combination"),
 								errhint("Did you mean \"??\" for reluctant quantifier?"),
 								parser_errposition(@1));
-					$$ = (Node *) makeRPRQuantifier(0, 1, @2, @1, yyscanner);
 				}
 			/* {n}, {n,}, {,m}, {n,m} quantifiers */
 			| '{' Iconst '}'
@@ -17822,7 +17894,7 @@ row_pattern_quantifier_opt:
 			/* Reluctant versions: {n}?, {n,}?, {,m}?, {n,m}? */
 			| '{' Iconst '}' Op
 				{
-					if (strcmp($4, "?") != 0)
+					if (strcmp($4, "?") != 0 && strcmp($4, "?|") != 0)
 						ereport(ERROR,
 								errcode(ERRCODE_SYNTAX_ERROR),
 								errmsg("invalid token after range quantifier"),
@@ -17834,10 +17906,12 @@ row_pattern_quantifier_opt:
 								errmsg("quantifier bound must be between 1 and %d", INT_MAX - 1),
 								parser_errposition(@2));
 					$$ = (Node *) makeRPRQuantifier($2, $2, @4, @1, yyscanner);
+					if (strcmp($4, "?|") == 0)
+						((RPRPatternNode *) $$)->trailing_alt = true;
 				}
 			| '{' Iconst ',' '}' Op
 				{
-					if (strcmp($5, "?") != 0)
+					if (strcmp($5, "?") != 0 && strcmp($5, "?|") != 0)
 						ereport(ERROR,
 								errcode(ERRCODE_SYNTAX_ERROR),
 								errmsg("invalid token after range quantifier"),
@@ -17849,10 +17923,12 @@ row_pattern_quantifier_opt:
 								errmsg("quantifier bound must be between 0 and %d", INT_MAX - 1),
 								parser_errposition(@2));
 					$$ = (Node *) makeRPRQuantifier($2, INT_MAX, @5, @1, yyscanner);
+					if (strcmp($5, "?|") == 0)
+						((RPRPatternNode *) $$)->trailing_alt = true;
 				}
 			| '{' ',' Iconst '}' Op
 				{
-					if (strcmp($5, "?") != 0)
+					if (strcmp($5, "?") != 0 && strcmp($5, "?|") != 0)
 						ereport(ERROR,
 								errcode(ERRCODE_SYNTAX_ERROR),
 								errmsg("invalid token after range quantifier"),
@@ -17864,10 +17940,12 @@ row_pattern_quantifier_opt:
 								errmsg("quantifier bound must be between 1 and %d", INT_MAX - 1),
 								parser_errposition(@3));
 					$$ = (Node *) makeRPRQuantifier(0, $3, @5, @1, yyscanner);
+					if (strcmp($5, "?|") == 0)
+						((RPRPatternNode *) $$)->trailing_alt = true;
 				}
 			| '{' Iconst ',' Iconst '}' Op
 				{
-					if (strcmp($6, "?") != 0)
+					if (strcmp($6, "?") != 0 && strcmp($6, "?|") != 0)
 						ereport(ERROR,
 								errcode(ERRCODE_SYNTAX_ERROR),
 								errmsg("invalid token after range quantifier"),
@@ -17884,6 +17962,8 @@ row_pattern_quantifier_opt:
 								errmsg("quantifier minimum bound must not exceed maximum"),
 								parser_errposition(@2));
 					$$ = (Node *) makeRPRQuantifier($2, $4, @6, @1, yyscanner);
+					if (strcmp($6, "?|") == 0)
+						((RPRPatternNode *) $$)->trailing_alt = true;
 				}
 		;
 
@@ -21325,6 +21405,93 @@ makeRPRQuantifier(int min, int max, ParseLoc reluctant_location, int location,
 	return n;
 }
 
+/*
+ * Build a SEQ node from children, or return the lone child unchanged.
+ */
+static RPRPatternNode *
+makeRPRSeqOrSingle(List *children, int location)
+{
+	RPRPatternNode *n;
+
+	if (list_length(children) == 1)
+		return (RPRPatternNode *) linitial(children);
+
+	n = makeNode(RPRPatternNode);
+	n->nodeType = RPR_PATTERN_SEQ;
+	n->children = children;
+	n->min = 1;
+	n->max = 1;
+	n->reluctant = false;
+	n->reluctant_location = -1;
+	n->location = location;
+	return n;
+}
+
+/*
+ * A glued quantifier such as "A*|" leaves trailing_alt set on its term while
+ * the enclosing sequence is built.  Once the sequence is complete, split it at
+ * the flagged term into alt(left, right), where the right operand is the whole
+ * remaining sequence -- this keeps "|" as the lowest-precedence operator, so
+ * "A*|B C" parses as "A* | (B C)", identical to the spaced form.  A flag with
+ * nothing to its right is a dangling "|" and is rejected.
+ */
+static RPRPatternNode *
+splitRPRTrailingAlt(RPRPatternNode *node, core_yyscan_t yyscanner)
+{
+	ListCell   *lc;
+	int			i = 0;
+
+	if (node->nodeType != RPR_PATTERN_SEQ)
+	{
+		if (node->trailing_alt)
+		{
+			node->trailing_alt = false;
+			ereport(ERROR,
+					errcode(ERRCODE_SYNTAX_ERROR),
+					errmsg("alternation operator \"|\" requires a pattern on both sides"),
+					parser_errposition(node->location));
+		}
+		return node;
+	}
+
+	foreach(lc, node->children)
+	{
+		RPRPatternNode *child = (RPRPatternNode *) lfirst(lc);
+
+		if (child->trailing_alt)
+		{
+			List	   *lefthalf = list_copy_head(node->children, i + 1);
+			List	   *righthalf = list_copy_tail(node->children, i + 1);
+			RPRPatternNode *altn;
+			RPRPatternNode *rightnode;
+
+			child->trailing_alt = false;
+			if (righthalf == NIL)
+				ereport(ERROR,
+						errcode(ERRCODE_SYNTAX_ERROR),
+						errmsg("alternation operator \"|\" requires a pattern on both sides"),
+						parser_errposition(node->location));
+
+			/* the right branch starts at its own first element, not the seq start */
+			rightnode = splitRPRTrailingAlt(makeRPRSeqOrSingle(righthalf,
+															   ((RPRPatternNode *) linitial(righthalf))->location),
+											yyscanner);
+			altn = makeNode(RPRPatternNode);
+			altn->nodeType = RPR_PATTERN_ALT;
+			altn->children = list_make2(makeRPRSeqOrSingle(lefthalf, node->location),
+										rightnode);
+			altn->min = 1;
+			altn->max = 1;
+			altn->reluctant = false;
+			altn->reluctant_location = -1;
+			altn->location = node->location;
+			return altn;
+		}
+		i++;
+	}
+	return node;
+}
+
 /* parser_init()
  * Initialize to parse one query string
  */
diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index c9469b56b7b..4e1d2650cf6 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -213,6 +213,12 @@ validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
 	/* Pattern node must exist - parser always provides non-NULL root */
 	Assert(node != NULL);
 
+	/*
+	 * trailing_alt is a transient grammar flag; splitRPRTrailingAlt must have
+	 * cleared it on every node before the pattern reaches parse analysis.
+	 */
+	Assert(!node->trailing_alt);
+
 	check_stack_depth();
 
 	switch (node->nodeType)
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 5200182aa46..e371f04a403 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -632,6 +632,14 @@ typedef struct RPRPatternNode
 	ParseLoc	location;		/* token location, or -1 */
 	char	   *varName;		/* VAR: variable name */
 	List	   *children;		/* SEQ, ALT, GROUP: child nodes */
+
+	/*
+	 * Transient parse flag, cleared by splitRPRTrailingAlt before the tree is
+	 * finalized: a glued quantifier such as "*|" is immediately followed by
+	 * the alternation operator '|'.  It is always false in a finalized tree,
+	 * so query_jumble_ignore keeps it off the pg_stat_statements queryid.
+	 */
+	bool		trailing_alt pg_node_attr(query_jumble_ignore);
 } RPRPatternNode;
 
 /*
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index c50c7a5f6a8..1fcb2ce22f0 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -13,6 +13,7 @@
 --   Navigation Functions Tests
 --   SKIP TO / INITIAL Tests
 --   Serialization/Deserialization Tests (objects kept for pg_upgrade/pg_dump)
+--   Glued Quantifier / Alternation Tests
 --   Error Cases Tests
 --   Window Deduplication Tests
 --
@@ -3083,6 +3084,233 @@ SELECT pg_get_viewdef('rpr_quant_n_plus_v'::regclass);
    a AS (val > 0) );
 (1 row)
 
+-- ============================================================
+-- Glued Quantifier / Alternation Tests
+-- ============================================================
+CREATE TABLE rpr_glue (id INT, val INT);
+INSERT INTO rpr_glue VALUES (1, 5), (2, 8), (3, 9), (4, -1), (5, 6), (6, -2);
+-- Quantifier glued to the alternation operator '|' without a space (0059).
+-- The lexer glues the trailing '|' into one Op token; the grammar reattaches it
+-- as the lowest-precedence alternation once the surrounding sequence is built.
+-- Deparse is canonical, so the glued, spaced, and mixed-spacing forms all
+-- reduce to the same PATTERN -- one deparse per shape proves the parse tree.
+-- Op-char quantifiers (*, +, ?, *?, +?, ??) glued to '|'.
+CREATE VIEW rpr_dp_op AS SELECT
+    count(*) OVER w1 AS w1, count(*) OVER w2 AS w2, count(*) OVER w3 AS w3,
+    count(*) OVER w4 AS w4, count(*) OVER w5 AS w5, count(*) OVER w6 AS w6
+FROM rpr_glue
+WINDOW w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|B) DEFINE A AS val > 0, B AS val <= 0),
+       w2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+|B) DEFINE A AS val > 0, B AS val <= 0),
+       w3 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A?|B) DEFINE A AS val > 0, B AS val <= 0),
+       w4 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*?|B) DEFINE A AS val > 0, B AS val <= 0),
+       w5 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+?|B) DEFINE A AS val > 0, B AS val <= 0),
+       w6 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A??|B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_dp_op'), E'\n')) AS line WHERE line ~ 'PATTERN';
+         line         
+----------------------
+   PATTERN (a* | b) 
+   PATTERN (a+ | b) 
+   PATTERN (a? | b) 
+   PATTERN (a*? | b) 
+   PATTERN (a+? | b) 
+   PATTERN (a?? | b) 
+(6 rows)
+
+DROP VIEW rpr_dp_op;
+-- Spaced reference: the fully-spaced canonical forms.  Identical deparse to the
+-- glued rpr_dp_op w1/w4 above completes the glued = spaced = mixed equivalence.
+CREATE VIEW rpr_dp_spc AS SELECT count(*) OVER w1 AS w1, count(*) OVER w2 AS w2
+FROM rpr_glue
+WINDOW w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A* | B) DEFINE A AS val > 0, B AS val <= 0),
+       w2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*? | B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_dp_spc'), E'\n')) AS line WHERE line ~ 'PATTERN';
+         line         
+----------------------
+   PATTERN (a* | b) 
+   PATTERN (a*? | b) 
+(2 rows)
+
+DROP VIEW rpr_dp_spc;
+-- Range quantifiers glued to '|': non-reluctant {n}| (} + char '|') and
+-- reluctant {n}?| (} + Op "?|").
+CREATE VIEW rpr_dp_rng AS SELECT
+    count(*) OVER w1 AS w1, count(*) OVER w2 AS w2, count(*) OVER w3 AS w3, count(*) OVER w4 AS w4,
+    count(*) OVER w5 AS w5, count(*) OVER w6 AS w6, count(*) OVER w7 AS w7, count(*) OVER w8 AS w8
+FROM rpr_glue
+WINDOW w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2}|B) DEFINE A AS val > 0, B AS val <= 0),
+       w2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2,}|B) DEFINE A AS val > 0, B AS val <= 0),
+       w3 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{,3}|B) DEFINE A AS val > 0, B AS val <= 0),
+       w4 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2,3}|B) DEFINE A AS val > 0, B AS val <= 0),
+       w5 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2}?|B) DEFINE A AS val > 0, B AS val <= 0),
+       w6 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2,}?|B) DEFINE A AS val > 0, B AS val <= 0),
+       w7 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{,3}?|B) DEFINE A AS val > 0, B AS val <= 0),
+       w8 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2,3}?|B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_dp_rng'), E'\n')) AS line WHERE line ~ 'PATTERN';
+           line           
+--------------------------
+   PATTERN (a{2} | b) 
+   PATTERN (a{2,} | b) 
+   PATTERN (a{0,3} | b) 
+   PATTERN (a{2,3} | b) 
+   PATTERN (a{2}? | b) 
+   PATTERN (a{2,}? | b) 
+   PATTERN (a{0,3}? | b) 
+   PATTERN (a{2,3}? | b) 
+(8 rows)
+
+DROP VIEW rpr_dp_rng;
+-- Mixed spacing: a space inside the quantifier with '|' still glued.
+-- "A* ?|B" = '*' + Op"?|" = reluctant "A*?" plus alternation.
+CREATE VIEW rpr_dp_mix AS SELECT count(*) OVER w1 AS w1, count(*) OVER w2 AS w2, count(*) OVER w3 AS w3
+FROM rpr_glue
+WINDOW w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A* ?|B) DEFINE A AS val > 0, B AS val <= 0),
+       w2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+ ?|B) DEFINE A AS val > 0, B AS val <= 0),
+       w3 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A? ?|B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_dp_mix'), E'\n')) AS line WHERE line ~ 'PATTERN';
+         line         
+----------------------
+   PATTERN (a*? | b) 
+   PATTERN (a+? | b) 
+   PATTERN (a?? | b) 
+(3 rows)
+
+DROP VIEW rpr_dp_mix;
+-- Structure: precedence (| is lowest, so its right operand is the whole
+-- following sequence), chaining, concatenation, and grouping.
+CREATE VIEW rpr_dp_struct AS SELECT
+    count(*) OVER w1 AS w1, count(*) OVER w2 AS w2, count(*) OVER w3 AS w3,
+    count(*) OVER w4 AS w4, count(*) OVER w5 AS w5, count(*) OVER w6 AS w6
+FROM rpr_glue
+WINDOW w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|B C) DEFINE A AS val > 0, B AS val <= 0, C AS val < 100),
+       w2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|B*|C) DEFINE A AS val > 0, B AS val <= 0, C AS val < 100),
+       w3 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A B*|C D) DEFINE A AS val > 0, B AS val <= 0, C AS val < 100, D AS val > 5),
+       w4 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A*|B)) DEFINE A AS val > 0, B AS val <= 0),
+       w5 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|(B|C)) DEFINE A AS val > 0, B AS val <= 0, C AS val < 100),
+       w6 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A*|B)+) DEFINE A AS val > 0, B AS val <= 0);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_dp_struct'), E'\n')) AS line WHERE line ~ 'PATTERN';
+           line            
+---------------------------
+   PATTERN (a* | b c) 
+   PATTERN (a* | b* | c) 
+   PATTERN (a b* | c d) 
+   PATTERN ((a* | b)) 
+   PATTERN (a* | (b | c)) 
+   PATTERN ((a* | b)+) 
+(6 rows)
+
+DROP VIEW rpr_dp_struct;
+-- Execution semantics (deparse cannot show reluctant shortest-match).  The
+-- rpr_glue rows -- an A-run followed by B rows -- make the '|B' alternative
+-- reachable: with "*" the greedy form matches the whole run while the
+-- reluctant form matches empty; with "+" the greedy form matches the run and
+-- the reluctant form matches one row, and on a B row (where "A+" fails) the B
+-- alternative fires.
+SELECT id, val,
+       count(*) OVER gs AS gstar, count(*) OVER rs AS rstar,
+       count(*) OVER gp AS gplus, count(*) OVER rp AS rplus
+FROM rpr_glue
+WINDOW gs AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|B) DEFINE A AS val > 0, B AS val <= 0),
+       rs AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*?|B) DEFINE A AS val > 0, B AS val <= 0),
+       gp AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+|B) DEFINE A AS val > 0, B AS val <= 0),
+       rp AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+?|B) DEFINE A AS val > 0, B AS val <= 0)
+ORDER BY id;
+ id | val | gstar | rstar | gplus | rplus 
+----+-----+-------+-------+-------+-------
+  1 |   5 |     3 |     0 |     3 |     1
+  2 |   8 |     0 |     0 |     0 |     1
+  3 |   9 |     0 |     0 |     0 |     1
+  4 |  -1 |     1 |     1 |     1 |     1
+  5 |   6 |     1 |     0 |     1 |     1
+  6 |  -2 |     1 |     1 |     1 |     1
+(6 rows)
+
+-- Patterns that must stay rejected.  "&" is an invalid op; a '|' with an empty
+-- side (leading, trailing, doubled, or alone in a group) has no operand; "||"
+-- and "*||" are doubled pipes; "A* *|B"/"A* *?|B"/"A{2}*?|B" are doubled
+-- quantifiers.
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A&B) DEFINE A AS val > 0);
+ERROR:  unsupported quantifier "&"
+LINE 1: ...EEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A&B) DEFINE...
+                                                             ^
+HINT:  Valid quantifiers are: *, +, ?, *?, +?, ??, {n}, {n,}, {,m}, {n,m} and their reluctant versions.
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|) DEFINE A AS val > 0);
+ERROR:  alternation operator "|" requires a pattern on both sides
+LINE 1: ...WEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|) DEFIN...
+                                                             ^
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*| |B) DEFINE A AS val > 0, B AS val <= 0);
+ERROR:  alternation operator "|" requires a pattern on both sides
+LINE 1: ...WEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*| |B) DE...
+                                                             ^
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*||B) DEFINE A AS val > 0, B AS val <= 0);
+ERROR:  unsupported quantifier "*||"
+LINE 1: ...EEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*||B) DEFI...
+                                                             ^
+HINT:  Valid quantifiers are: *, +, ?, *?, +?, ??, {n}, {n,}, {,m}, {n,m} and their reluctant versions.
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A||B) DEFINE A AS val > 0, B AS val <= 0);
+ERROR:  unsupported quantifier "||"
+LINE 1: ...EEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A||B) DEFIN...
+                                                             ^
+HINT:  Valid quantifiers are: *, +, ?, *?, +?, ??, {n}, {n,}, {,m}, {n,m} and their reluctant versions.
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|B|) DEFINE A AS val > 0, B AS val <= 0);
+ERROR:  syntax error at or near ")"
+LINE 1: ...CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|B|) DEFINE A...
+                                                             ^
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (|A) DEFINE A AS val > 0);
+ERROR:  syntax error at or near "|"
+LINE 1: ...WEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (|A) DEFINE...
+                                                             ^
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A*|)) DEFINE A AS val > 0);
+ERROR:  alternation operator "|" requires a pattern on both sides
+LINE 1: ...EEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A*|)) DEFI...
+                                                             ^
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A* *|B) DEFINE A AS val > 0, B AS val <= 0);
+ERROR:  invalid token after "*" quantifier
+LINE 1: ...N CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A* *|B) DEFIN...
+                                                             ^
+HINT:  Did you mean "*?" for reluctant quantifier?
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A* *?|B) DEFINE A AS val > 0, B AS val <= 0);
+ERROR:  invalid token after "*" quantifier
+LINE 1: ...N CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A* *?|B) DEFI...
+                                                             ^
+HINT:  Did you mean "*?" for reluctant quantifier?
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A? *?|B) DEFINE A AS val > 0, B AS val <= 0);
+ERROR:  invalid quantifier combination
+LINE 1: ...EEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A? *?|B) DE...
+                                                             ^
+HINT:  Did you mean "??" for reluctant quantifier?
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2}*?|B) DEFINE A AS val > 0, B AS val <= 0);
+ERROR:  invalid token after range quantifier
+LINE 1: ... CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2}*?|B) DEFI...
+                                                             ^
+HINT:  Only "?" is allowed after {n} to make it reluctant.
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2} *?|B) DEFINE A AS val > 0, B AS val <= 0);
+ERROR:  invalid token after range quantifier
+LINE 1: ...CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2} *?|B) DEFI...
+                                                             ^
+HINT:  Only "?" is allowed after {n} to make it reluctant.
+-- Doubled op-char quantifiers lex as one Op token and are unsupported, whether
+-- glued to '|' ("**|", "*+|", "???|") or on their own ("**").
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A**|B) DEFINE A AS val > 0, B AS val <= 0);
+ERROR:  unsupported quantifier "**|"
+LINE 1: ...EEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A**|B) DEFI...
+                                                             ^
+HINT:  Valid quantifiers are: *, +, ?, *?, +?, ??, {n}, {n,}, {,m}, {n,m} and their reluctant versions.
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*+|B) DEFINE A AS val > 0, B AS val <= 0);
+ERROR:  unsupported quantifier "*+|"
+LINE 1: ...EEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*+|B) DEFI...
+                                                             ^
+HINT:  Valid quantifiers are: *, +, ?, *?, +?, ??, {n}, {n,}, {,m}, {n,m} and their reluctant versions.
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A???|B) DEFINE A AS val > 0, B AS val <= 0);
+ERROR:  unsupported quantifier "???|"
+LINE 1: ...EEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A???|B) DEF...
+                                                             ^
+HINT:  Valid quantifiers are: *, +, ?, *?, +?, ??, {n}, {n,}, {,m}, {n,m} and their reluctant versions.
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A**B) DEFINE A AS val > 0);
+ERROR:  unsupported quantifier "**"
+LINE 1: ...EEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A**B) DEFIN...
+                                                             ^
+HINT:  Valid quantifiers are: *, +, ?, *?, +?, ??, {n}, {n,}, {,m}, {n,m} and their reluctant versions.
+DROP TABLE rpr_glue;
 -- ============================================================
 -- Error Cases Tests
 -- ============================================================
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index 7dfb72f6bfd..cc79843aeb7 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -13,6 +13,7 @@
 --   Navigation Functions Tests
 --   SKIP TO / INITIAL Tests
 --   Serialization/Deserialization Tests (objects kept for pg_upgrade/pg_dump)
+--   Glued Quantifier / Alternation Tests
 --   Error Cases Tests
 --   Window Deduplication Tests
 --
@@ -2074,6 +2075,117 @@ WINDOW w AS (ORDER BY id
              DEFINE A AS val > 0);
 SELECT pg_get_viewdef('rpr_quant_n_plus_v'::regclass);
 
+-- ============================================================
+-- Glued Quantifier / Alternation Tests
+-- ============================================================
+CREATE TABLE rpr_glue (id INT, val INT);
+INSERT INTO rpr_glue VALUES (1, 5), (2, 8), (3, 9), (4, -1), (5, 6), (6, -2);
+-- Quantifier glued to the alternation operator '|' without a space (0059).
+-- The lexer glues the trailing '|' into one Op token; the grammar reattaches it
+-- as the lowest-precedence alternation once the surrounding sequence is built.
+-- Deparse is canonical, so the glued, spaced, and mixed-spacing forms all
+-- reduce to the same PATTERN -- one deparse per shape proves the parse tree.
+
+-- Op-char quantifiers (*, +, ?, *?, +?, ??) glued to '|'.
+CREATE VIEW rpr_dp_op AS SELECT
+    count(*) OVER w1 AS w1, count(*) OVER w2 AS w2, count(*) OVER w3 AS w3,
+    count(*) OVER w4 AS w4, count(*) OVER w5 AS w5, count(*) OVER w6 AS w6
+FROM rpr_glue
+WINDOW w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|B) DEFINE A AS val > 0, B AS val <= 0),
+       w2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+|B) DEFINE A AS val > 0, B AS val <= 0),
+       w3 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A?|B) DEFINE A AS val > 0, B AS val <= 0),
+       w4 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*?|B) DEFINE A AS val > 0, B AS val <= 0),
+       w5 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+?|B) DEFINE A AS val > 0, B AS val <= 0),
+       w6 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A??|B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_dp_op'), E'\n')) AS line WHERE line ~ 'PATTERN';
+DROP VIEW rpr_dp_op;
+-- Spaced reference: the fully-spaced canonical forms.  Identical deparse to the
+-- glued rpr_dp_op w1/w4 above completes the glued = spaced = mixed equivalence.
+CREATE VIEW rpr_dp_spc AS SELECT count(*) OVER w1 AS w1, count(*) OVER w2 AS w2
+FROM rpr_glue
+WINDOW w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A* | B) DEFINE A AS val > 0, B AS val <= 0),
+       w2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*? | B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_dp_spc'), E'\n')) AS line WHERE line ~ 'PATTERN';
+DROP VIEW rpr_dp_spc;
+-- Range quantifiers glued to '|': non-reluctant {n}| (} + char '|') and
+-- reluctant {n}?| (} + Op "?|").
+CREATE VIEW rpr_dp_rng AS SELECT
+    count(*) OVER w1 AS w1, count(*) OVER w2 AS w2, count(*) OVER w3 AS w3, count(*) OVER w4 AS w4,
+    count(*) OVER w5 AS w5, count(*) OVER w6 AS w6, count(*) OVER w7 AS w7, count(*) OVER w8 AS w8
+FROM rpr_glue
+WINDOW w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2}|B) DEFINE A AS val > 0, B AS val <= 0),
+       w2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2,}|B) DEFINE A AS val > 0, B AS val <= 0),
+       w3 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{,3}|B) DEFINE A AS val > 0, B AS val <= 0),
+       w4 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2,3}|B) DEFINE A AS val > 0, B AS val <= 0),
+       w5 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2}?|B) DEFINE A AS val > 0, B AS val <= 0),
+       w6 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2,}?|B) DEFINE A AS val > 0, B AS val <= 0),
+       w7 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{,3}?|B) DEFINE A AS val > 0, B AS val <= 0),
+       w8 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2,3}?|B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_dp_rng'), E'\n')) AS line WHERE line ~ 'PATTERN';
+DROP VIEW rpr_dp_rng;
+-- Mixed spacing: a space inside the quantifier with '|' still glued.
+-- "A* ?|B" = '*' + Op"?|" = reluctant "A*?" plus alternation.
+CREATE VIEW rpr_dp_mix AS SELECT count(*) OVER w1 AS w1, count(*) OVER w2 AS w2, count(*) OVER w3 AS w3
+FROM rpr_glue
+WINDOW w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A* ?|B) DEFINE A AS val > 0, B AS val <= 0),
+       w2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+ ?|B) DEFINE A AS val > 0, B AS val <= 0),
+       w3 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A? ?|B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_dp_mix'), E'\n')) AS line WHERE line ~ 'PATTERN';
+DROP VIEW rpr_dp_mix;
+-- Structure: precedence (| is lowest, so its right operand is the whole
+-- following sequence), chaining, concatenation, and grouping.
+CREATE VIEW rpr_dp_struct AS SELECT
+    count(*) OVER w1 AS w1, count(*) OVER w2 AS w2, count(*) OVER w3 AS w3,
+    count(*) OVER w4 AS w4, count(*) OVER w5 AS w5, count(*) OVER w6 AS w6
+FROM rpr_glue
+WINDOW w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|B C) DEFINE A AS val > 0, B AS val <= 0, C AS val < 100),
+       w2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|B*|C) DEFINE A AS val > 0, B AS val <= 0, C AS val < 100),
+       w3 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A B*|C D) DEFINE A AS val > 0, B AS val <= 0, C AS val < 100, D AS val > 5),
+       w4 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A*|B)) DEFINE A AS val > 0, B AS val <= 0),
+       w5 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|(B|C)) DEFINE A AS val > 0, B AS val <= 0, C AS val < 100),
+       w6 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A*|B)+) DEFINE A AS val > 0, B AS val <= 0);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_dp_struct'), E'\n')) AS line WHERE line ~ 'PATTERN';
+DROP VIEW rpr_dp_struct;
+-- Execution semantics (deparse cannot show reluctant shortest-match).  The
+-- rpr_glue rows -- an A-run followed by B rows -- make the '|B' alternative
+-- reachable: with "*" the greedy form matches the whole run while the
+-- reluctant form matches empty; with "+" the greedy form matches the run and
+-- the reluctant form matches one row, and on a B row (where "A+" fails) the B
+-- alternative fires.
+SELECT id, val,
+       count(*) OVER gs AS gstar, count(*) OVER rs AS rstar,
+       count(*) OVER gp AS gplus, count(*) OVER rp AS rplus
+FROM rpr_glue
+WINDOW gs AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|B) DEFINE A AS val > 0, B AS val <= 0),
+       rs AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*?|B) DEFINE A AS val > 0, B AS val <= 0),
+       gp AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+|B) DEFINE A AS val > 0, B AS val <= 0),
+       rp AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+?|B) DEFINE A AS val > 0, B AS val <= 0)
+ORDER BY id;
+-- Patterns that must stay rejected.  "&" is an invalid op; a '|' with an empty
+-- side (leading, trailing, doubled, or alone in a group) has no operand; "||"
+-- and "*||" are doubled pipes; "A* *|B"/"A* *?|B"/"A{2}*?|B" are doubled
+-- quantifiers.
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A&B) DEFINE A AS val > 0);
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|) DEFINE A AS val > 0);
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*| |B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*||B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A||B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|B|) DEFINE A AS val > 0, B AS val <= 0);
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (|A) DEFINE A AS val > 0);
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A*|)) DEFINE A AS val > 0);
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A* *|B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A* *?|B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A? *?|B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2}*?|B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2} *?|B) DEFINE A AS val > 0, B AS val <= 0);
+-- Doubled op-char quantifiers lex as one Op token and are unsupported, whether
+-- glued to '|' ("**|", "*+|", "???|") or on their own ("**").
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A**|B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*+|B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A???|B) DEFINE A AS val > 0, B AS val <= 0);
+SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A**B) DEFINE A AS val > 0);
+DROP TABLE rpr_glue;
+
 -- ============================================================
 -- Error Cases Tests
 -- ============================================================
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0060-Fix-outdated-function-and-file-references-in-row-.txt (4.4K, 62-nocfbot-0060-Fix-outdated-function-and-file-references-in-row-.txt)
  download | inline diff:
From 071126a8559aceea9299a2801a78f241777071dd Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Mon, 8 Jun 2026 11:46:56 +0900
Subject: [PATCH 60/68] Fix outdated function and file references in row
 pattern recognition docs

Two comments in rpr_nfa.sql attributed nfa_advance_begin/end to
nodeWindowAgg.c, but both are defined in execRPR.c.  README.rpr referred to a
nonexistent compute_nav_offsets; the planner routine that computes the trim
offsets is compute_define_metadata in createplan.c.  These are comment and
documentation fixes only, with no behavior change; the rpr_nfa expected output
is updated to match the corrected comment.
---
 src/backend/executor/README.rpr       | 4 ++--
 src/test/regress/expected/rpr_nfa.out | 4 ++--
 src/test/regress/sql/rpr_nfa.sql      | 4 ++--
 3 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/src/backend/executor/README.rpr b/src/backend/executor/README.rpr
index df574a0a6f4..c1822d8240b 100644
--- a/src/backend/executor/README.rpr
+++ b/src/backend/executor/README.rpr
@@ -667,7 +667,7 @@ VI-5. Tuplestore Mark and Trim (nodeWindowAgg.c)
 
 Navigation functions require access to past rows via the tuplestore.
 To allow tuplestore_trim() to free rows that are no longer reachable,
-the planner computes two offsets (see compute_nav_offsets):
+the planner computes two offsets (see compute_define_metadata):
 
   navMaxOffset (Nav Mark Lookback):
     Maximum backward reach from currentpos.  Contributed by PREV,
@@ -1499,7 +1499,7 @@ Appendix A. Key Function Index
   ExecRPRFinalizeAllContexts    execRPR.c             Partition-end finalize
   ExecRPRRecordContextSuccess   execRPR.c             Stats: match success
   ExecRPRRecordContextFailure   execRPR.c             Stats: match failure
-  compute_nav_offsets           createplan.c          Trim offset computation
+  compute_define_metadata       createplan.c          Trim offset computation
 
 Appendix B. Data Structure Relationship Diagram
 ============================================================================
diff --git a/src/test/regress/expected/rpr_nfa.out b/src/test/regress/expected/rpr_nfa.out
index 3b9975a83df..d7146168f2b 100644
--- a/src/test/regress/expected/rpr_nfa.out
+++ b/src/test/regress/expected/rpr_nfa.out
@@ -1357,7 +1357,7 @@ WINDOW w AS (
 
 -- Reluctant optional group skip-to-FIN
 -- When a reluctant optional group's skip path reaches FIN, the group
--- entry path is abandoned (nodeWindowAgg.c nfa_advance_begin).
+-- entry path is abandoned (execRPR.c nfa_advance_begin).
 -- Pattern: C (A B)?? -- after C matches, the reluctant group (A B)??
 -- prefers to skip.  Skip goes to FIN (group is last element), so
 -- the match completes with just C.
@@ -3364,7 +3364,7 @@ WINDOW w AS (
 -- Nested END->END fast-forward
 -- When an inner group has a nullable body and count < min, the
 -- fast-forward path exits through the outer END, incrementing
--- the outer group's count (nodeWindowAgg.c nfa_advance_end).
+-- the outer group's count (execRPR.c nfa_advance_end).
 -- Pattern: ((A?){2,3}){2,3} -- nested groups, neither collapses
 -- because the optimizer cannot safely multiply non-exact quantifiers.
 -- Data has no A rows, forcing all-empty iterations via fast-forward.
diff --git a/src/test/regress/sql/rpr_nfa.sql b/src/test/regress/sql/rpr_nfa.sql
index 61072d1d6f1..8daa0a73725 100644
--- a/src/test/regress/sql/rpr_nfa.sql
+++ b/src/test/regress/sql/rpr_nfa.sql
@@ -968,7 +968,7 @@ WINDOW w AS (
 
 -- Reluctant optional group skip-to-FIN
 -- When a reluctant optional group's skip path reaches FIN, the group
--- entry path is abandoned (nodeWindowAgg.c nfa_advance_begin).
+-- entry path is abandoned (execRPR.c nfa_advance_begin).
 -- Pattern: C (A B)?? -- after C matches, the reluctant group (A B)??
 -- prefers to skip.  Skip goes to FIN (group is last element), so
 -- the match completes with just C.
@@ -2406,7 +2406,7 @@ WINDOW w AS (
 -- Nested END->END fast-forward
 -- When an inner group has a nullable body and count < min, the
 -- fast-forward path exits through the outer END, incrementing
--- the outer group's count (nodeWindowAgg.c nfa_advance_end).
+-- the outer group's count (execRPR.c nfa_advance_end).
 -- Pattern: ((A?){2,3}){2,3} -- nested groups, neither collapses
 -- because the optimizer cannot safely multiply non-exact quantifiers.
 -- Data has no A rows, forcing all-empty iterations via fast-forward.
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0061-Assert-that-row-pattern-nesting-depth-never-alias.txt (1.9K, 63-nocfbot-0061-Assert-that-row-pattern-nesting-depth-never-alias.txt)
  download | inline diff:
From 8d43d10030a6fad989e2ed8bbde1d6b036d348ce Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Mon, 8 Jun 2026 12:22:21 +0900
Subject: [PATCH 61/68] Assert that row pattern nesting depth never aliases the
 RPR_DEPTH_NONE sentinel

allocateRPRPattern stores maxDepth + 1, and scanRPRPatternRecursive rejects
nesting at or above RPR_DEPTH_MAX, so the stored value stays <= RPR_DEPTH_MAX
and never reaches the RPR_DEPTH_NONE sentinel (UINT8_MAX).  Assert that bound
so raising RPR_DEPTH_MAX or the depth limit trips immediately.  Also note in
compute_define_metadata that its default offset (1) serves only PREV; the
guarded LAST sub-case never uses it.
---
 src/backend/optimizer/plan/createplan.c | 3 +++
 src/backend/optimizer/plan/rpr.c        | 3 +++
 2 files changed, 6 insertions(+)

diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index c8ecaeea7cf..c5b0857ff20 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -2581,6 +2581,9 @@ visit_nav_plan(NavTraversal *t, RPRNavExpr *nav)
 		{
 			int64		offset;
 
+			/*
+			 * default 1 is for PREV; the guarded LAST sub-case never uses it.
+			 */
 			if (extract_const_offset(nav->offset_arg, 1, &offset))
 				context->maxOffset = Max(context->maxOffset, offset);
 			else
diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index 43114088c3f..e7276c42ca6 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -1157,6 +1157,9 @@ allocateRPRPattern(int numVars, int numElements, RPRDepth maxDepth,
 
 	result = makeNode(RPRPattern);
 	result->numVars = numVars;
+
+	/* depth < RPR_DEPTH_MAX, so maxDepth+1 never aliases RPR_DEPTH_NONE. */
+	Assert(maxDepth < RPR_DEPTH_MAX);
 	result->maxDepth = maxDepth + 1;	/* +1: depth is 0-based */
 	result->numElements = numElements;
 
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0062-Invalidate-the-row-pattern-nav-slot-cache-when-a-.txt (2.2K, 64-nocfbot-0062-Invalidate-the-row-pattern-nav-slot-cache-when-a-.txt)
  download | inline diff:
From f55bf199846bb94e42cb3238a2dc92ccff1c50fe Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Mon, 8 Jun 2026 12:44:45 +0900
Subject: [PATCH 62/68] Invalidate the row pattern nav slot cache when a window
 partition changes

release_partition clears the per-partition tuplestore, leaving the cached
nav slot position (which is partition-relative) stale.  This is not a live
bug: nfa_evaluate_row resets nav_slot_pos to -1 before each row's DEFINE
evaluation, the only path that fetches nav slots, so a stale position never
produces a cache hit.  Reset it here too for consistency with the rest of the
per-partition cleanup.  Also note in visit_nav_plan that a constant
LAST(x, 0) is conservatively flagged match-start-dependent, causing a
harmless redundant re-evaluation.
---
 src/backend/executor/nodeWindowAgg.c    | 3 +++
 src/backend/optimizer/plan/createplan.c | 7 ++++++-
 2 files changed, 9 insertions(+), 1 deletion(-)

diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 86b39bf7f61..95f4f06501e 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -1574,6 +1574,9 @@ release_partition(WindowAggState *winstate)
 	winstate->nfaLastProcessedRow = -1;
 	winstate->nfaStatesActive = 0;
 	winstate->nfaContextsActive = 0;
+
+	/* Invalidate the nav slot position cache for the new partition. */
+	winstate->nav_slot_pos = -1;
 }
 
 /*
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index c5b0857ff20..5608d71cbe2 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -2693,7 +2693,12 @@ visit_nav_plan(NavTraversal *t, RPRNavExpr *nav)
 		}
 	}
 
-	/* Match-start dependency: classify the outer nav kind. */
+	/*
+	 * Match-start dependency: classify the outer nav kind.  A constant
+	 * LAST(x, 0) is conservatively included (offset_arg is a non-NULL Const),
+	 * causing a harmless extra re-evaluation; since LAST(x, 0) is the current
+	 * row, its result is independent of the match start.
+	 */
 	if (nav->kind == RPR_NAV_FIRST ||
 		(nav->kind == RPR_NAV_LAST && nav->offset_arg != NULL) ||
 		nav->kind == RPR_NAV_PREV_FIRST ||
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0063-Tidy-up-formatting-in-row-pattern-recognition-cod.txt (4.3K, 65-nocfbot-0063-Tidy-up-formatting-in-row-pattern-recognition-cod.txt)
  download | inline diff:
From a3ef40ae024cd1db787a1c19dc967dbe03bb28f2 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Mon, 8 Jun 2026 13:48:10 +0900
Subject: [PATCH 63/68] Tidy up formatting in row pattern recognition code

Reformat the first three row_pattern_quantifier_opt alternatives as
multiline blocks to match the rest of the rule, change the RF_* reduced
frame macros from a tab to a space after #define, add a trailing comma to
the last RPSkipTo enumerator, and move the RPRNav* entries to their correct
C-collation position in typedefs.list.  These are formatting and alignment
changes only, with no effect on behavior.
---
 src/backend/optimizer/plan/rpr.c |  2 +-
 src/backend/parser/gram.y        | 14 +++++++++++---
 src/include/nodes/execnodes.h    | 10 +++++-----
 src/include/nodes/parsenodes.h   |  2 +-
 src/tools/pgindent/typedefs.list |  6 +++---
 5 files changed, 21 insertions(+), 13 deletions(-)

diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index e7276c42ca6..143ea034fb3 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -484,7 +484,7 @@ mergeConsecutiveAlts(List *children)
  *
  * When a GROUP's children appear as a prefix before and/or suffix after
  * the GROUP in a SEQ, merge them by incrementing the GROUP's quantifier.
- * This runs iteratively: A B A B (A B)+ A B -> (A B){5,}.
+ * This runs iteratively: A B A B (A B)+ A B -> (A B){4,}.
  *
  * Algorithm:
  *   For each GROUP encountered in the sequence:
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 147b5f37293..e0799f35638 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -17740,9 +17740,17 @@ row_pattern_primary:
 row_pattern_quantifier_opt:
 			/* EMPTY - no quantifier means exactly once; @$ is unused since
 			 * min=max=1 never produces an error */
-			{ $$ = (Node *) makeRPRQuantifier(1, 1, -1, @$, yyscanner); }
-			| '*'					{ $$ = (Node *) makeRPRQuantifier(0, INT_MAX, -1, @1, yyscanner); }
-			| '+'					{ $$ = (Node *) makeRPRQuantifier(1, INT_MAX, -1, @1, yyscanner); }
+				{
+					$$ = (Node *) makeRPRQuantifier(1, 1, -1, @$, yyscanner);
+				}
+			| '*'
+				{
+					$$ = (Node *) makeRPRQuantifier(0, INT_MAX, -1, @1, yyscanner);
+				}
+			| '+'
+				{
+					$$ = (Node *) makeRPRQuantifier(1, INT_MAX, -1, @1, yyscanner);
+				}
 			| Op
 				{
 					/* Handle single Op: ? or reluctant quantifiers *?, +?, ?? */
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 4641ed36cee..77d75aca91f 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -2528,11 +2528,11 @@ typedef enum WindowAggStatus
 } WindowAggStatus;
 
 /* RPR reduced frame states returned by get_reduced_frame_status() */
-#define	RF_NOT_DETERMINED	0	/* not yet processed */
-#define	RF_FRAME_HEAD		1	/* start row of a match */
-#define	RF_SKIPPED			2	/* interior row of a match */
-#define	RF_UNMATCHED		3	/* no match at this row */
-#define	RF_EMPTY_MATCH		4	/* empty match (0 rows); treated as unmatched */
+#define RF_NOT_DETERMINED	0	/* not yet processed */
+#define RF_FRAME_HEAD		1	/* start row of a match */
+#define RF_SKIPPED			2	/* interior row of a match */
+#define RF_UNMATCHED		3	/* no match at this row */
+#define RF_EMPTY_MATCH		4	/* empty match (0 rows); treated as unmatched */
 
 /*
  * RPRNFAState - single NFA state for pattern matching
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index e371f04a403..9636a8efca1 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -589,7 +589,7 @@ typedef enum RPSkipTo
 	ST_NONE,					/* no AFTER MATCH clause; default for non-RPR
 								 * windows */
 	ST_NEXT_ROW,				/* SKIP TO NEXT ROW */
-	ST_PAST_LAST_ROW			/* SKIP TO PAST LAST ROW */
+	ST_PAST_LAST_ROW,			/* SKIP TO PAST LAST ROW */
 } RPSkipTo;
 
 /*
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 24cf2eb7860..06cd541be5b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2519,9 +2519,6 @@ QuerySource
 QueueBackendStatus
 QueuePosition
 QuitSignalReason
-RPRNavExpr
-RPRNavKind
-RPRNavOffsetKind
 RBTNode
 RBTOrderControl
 RBTree
@@ -2540,6 +2537,9 @@ RPRElemFlags
 RPRElemIdx
 RPRNFAContext
 RPRNFAState
+RPRNavExpr
+RPRNavKind
+RPRNavOffsetKind
 RPRPattern
 RPRPatternElement
 RPRPatternNode
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0065-Use-width-explicit-integer-limit-macros-in-row-pa.txt (21.3K, 66-nocfbot-0065-Use-width-explicit-integer-limit-macros-in-row-pa.txt)
  download | inline diff:
From 4f04a1aefc4a5a041e1070f0cbae2f8fa9ff5e40 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Mon, 8 Jun 2026 14:59:00 +0900
Subject: [PATCH 65/68] Use width-explicit integer limit macros in row pattern
 recognition

Replace INT_MAX/INT16_MAX/INT32_MAX/INT64_MAX/UINT8_MAX with the
PG_INT*_MAX/PG_UINT8_MAX equivalents, and type RPRPatternNode.min/max
as int32 to match the plan representation.  Value- and output-preserving.
---
 src/backend/commands/explain.c          |  2 +-
 src/backend/executor/execRPR.c          | 11 +++--
 src/backend/executor/nodeWindowAgg.c    | 16 +++----
 src/backend/optimizer/plan/createplan.c | 14 +++---
 src/backend/optimizer/plan/rpr.c        |  6 +--
 src/backend/parser/gram.y               | 64 ++++++++++++-------------
 src/backend/utils/adt/ruleutils.c       |  6 +--
 src/include/nodes/execnodes.h           |  2 +-
 src/include/nodes/parsenodes.h          |  4 +-
 src/include/optimizer/rpr.h             | 11 +++--
 10 files changed, 69 insertions(+), 67 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 7992829d5c4..70fd7f386a0 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -3270,7 +3270,7 @@ show_window_def(WindowAggState *planstate, List *ancestors, ExplainState *es)
 											es);
 						break;
 					case RPR_NAV_OFFSET_FIXED:
-						if (firstOffset == INT64_MAX)
+						if (firstOffset == PG_INT64_MAX)
 							ExplainPropertyText("Nav Mark Lookahead", "infinite",
 												es);
 						else
diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index b7e3c4a8274..1c89875a306 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -1546,7 +1546,7 @@ nfa_advance(WindowAggState *winstate, RPRNFAContext *ctx, int64 currentPos)
 				   sizeof(bitmapword) *
 				   (winstate->nfaVisitedMaxWord -
 					winstate->nfaVisitedMinWord + 1));
-			winstate->nfaVisitedMinWord = INT16_MAX;
+			winstate->nfaVisitedMinWord = PG_INT16_MAX;
 			winstate->nfaVisitedMaxWord = -1;
 		}
 
@@ -1807,10 +1807,11 @@ ExecRPRProcessRow(WindowAggState *winstate, int64 currentPos,
 			int64		ctxFrameEnd;
 
 			/*
-			 * Clamp to INT64_MAX on overflow.  frameOffset can be as large as
-			 * PG_INT64_MAX (e.g. "ROWS <huge> FOLLOWING"), so add the offset
-			 * and the trailing +1 in two separately checked steps to avoid
-			 * signed-integer overflow in the "frameOffset + 1" subexpression.
+			 * Clamp to PG_INT64_MAX on overflow.  frameOffset can be as large
+			 * as PG_INT64_MAX (e.g. "ROWS <huge> FOLLOWING"), so add the
+			 * offset and the trailing +1 in two separately checked steps to
+			 * avoid signed-integer overflow in the "frameOffset + 1"
+			 * subexpression.
 			 */
 			if (pg_add_s64_overflow(ctx->matchStartRow, frameOffset,
 									&ctxFrameEnd) ||
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 95f4f06501e..13661181986 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -3070,7 +3070,7 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
 		winstate->nfaVisitedElems = palloc0(sizeof(bitmapword) *
 											nfaVisitedNWords);
 		/* High-water mark sentinels: no bits set yet. */
-		winstate->nfaVisitedMinWord = INT16_MAX;
+		winstate->nfaVisitedMinWord = PG_INT16_MAX;
 		winstate->nfaVisitedMaxWord = -1;
 	}
 
@@ -4048,7 +4048,7 @@ typedef struct
  *			NEXT_LAST (= max(inner - outer, 0))
  *		  - minFirstOffset (forward reach from match_start): FIRST,
  *			compound PREV_FIRST (= inner - outer, may be negative),
- *			compound NEXT_FIRST (= inner + outer, clamped to INT64_MAX on
+ *			compound NEXT_FIRST (= inner + outer, clamped to PG_INT64_MAX on
  *			overflow; always >= 0 so never updates minFirstOffset in practice)
  *
  * Counterpart of visit_nav_plan but using runtime evaluation instead of
@@ -4137,7 +4137,7 @@ visit_nav_exec(NavTraversal *t, RPRNavExpr *nav)
 		{
 			/*
 			 * reach = inner - outer.  Both are non-negative, so the result >=
-			 * -INT64_MAX, which cannot underflow int64.
+			 * -PG_INT64_MAX, which cannot underflow int64.
 			 */
 			reach = inner - outer;
 		}
@@ -4146,10 +4146,10 @@ visit_nav_exec(NavTraversal *t, RPRNavExpr *nav)
 			/*
 			 * NEXT_FIRST: reach = inner + outer.  This can overflow, but the
 			 * result is always >= 0, so it never updates minFirstOffset
-			 * (which tracks the minimum).  Clamp to INT64_MAX on overflow.
+			 * (which tracks the minimum).  Clamp to PG_INT64_MAX on overflow.
 			 */
 			if (pg_add_s64_overflow(inner, outer, &reach))
-				reach = INT64_MAX;
+				reach = PG_INT64_MAX;
 		}
 		context->minFirstOffset = Min(context->minFirstOffset, reach);
 	}
@@ -4184,7 +4184,7 @@ eval_define_offsets(WindowAggState *winstate, List *defineClause)
 	ctx.winstate = winstate;
 	ctx.maxOffset = 0;
 	ctx.maxOverflow = false;
-	ctx.minFirstOffset = INT64_MAX;
+	ctx.minFirstOffset = PG_INT64_MAX;
 
 	trav.visit = visit_nav_exec;
 	trav.data = &ctx;
@@ -4213,10 +4213,10 @@ eval_define_offsets(WindowAggState *winstate, List *defineClause)
 	if (needsFirst)
 	{
 		winstate->navFirstOffsetKind = RPR_NAV_OFFSET_FIXED;
-		if (ctx.minFirstOffset < INT64_MAX)
+		if (ctx.minFirstOffset < PG_INT64_MAX)
 			winstate->navFirstOffset = ctx.minFirstOffset;
 		else
-			winstate->navFirstOffset = INT64_MAX;
+			winstate->navFirstOffset = PG_INT64_MAX;
 	}
 }
 
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index 5608d71cbe2..d2e19d61d58 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -2669,8 +2669,8 @@ visit_nav_plan(NavTraversal *t, RPRNavExpr *nav)
 				{
 					/*
 					 * reach = inner - outer.  Both are non-negative, so the
-					 * result >= -INT64_MAX, which cannot underflow int64. No
-					 * overflow check needed.
+					 * result >= -PG_INT64_MAX, which cannot underflow int64.
+					 * No overflow check needed.
 					 */
 					reach = inner - outer;
 				}
@@ -2680,10 +2680,10 @@ visit_nav_plan(NavTraversal *t, RPRNavExpr *nav)
 					 * NEXT_FIRST: reach = inner + outer.  This can overflow,
 					 * but the result is always >= 0, so it never updates
 					 * firstOffset (which tracks the minimum).  Clamp to
-					 * INT64_MAX on overflow.
+					 * PG_INT64_MAX on overflow.
 					 */
 					if (pg_add_s64_overflow(inner, outer, &reach))
-						reach = INT64_MAX;
+						reach = PG_INT64_MAX;
 				}
 
 				context->firstOffset = Min(context->firstOffset, reach);
@@ -2741,7 +2741,7 @@ compute_define_metadata(List *defineClause,
 	ctx.maxOffset = 0;
 	ctx.maxNeedsEval = false;
 	ctx.maxOverflow = false;
-	ctx.firstOffset = INT64_MAX;	/* sentinel: no FIRST found yet */
+	ctx.firstOffset = PG_INT64_MAX; /* sentinel: no FIRST found yet */
 	ctx.hasFirst = false;
 	ctx.firstNeedsEval = false;
 	ctx.curVarIdx = 0;
@@ -2789,8 +2789,8 @@ compute_define_metadata(List *defineClause,
 		else
 		{
 			*firstKind = RPR_NAV_OFFSET_FIXED;
-			*firstResult = ctx.firstOffset; /* may be negative; INT64_MAX if
-											 * overflowed */
+			*firstResult = ctx.firstOffset; /* may be negative; PG_INT64_MAX
+											 * if overflowed */
 		}
 	}
 	else
diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index 9364136b071..d768422deeb 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -1210,7 +1210,7 @@ fillRPRPatternVar(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth dept
 	elem->varId = getVarIdFromPattern(pat, node->varName);
 	elem->depth = depth;
 	elem->min = node->min;
-	elem->max = (node->max == INT_MAX) ? RPR_QUANTITY_INF : node->max;
+	elem->max = (node->max == PG_INT32_MAX) ? RPR_QUANTITY_INF : node->max;
 	Assert(elem->min >= 0 && elem->min < RPR_QUANTITY_INF &&
 		   elem->max >= 1 &&
 		   (elem->max == RPR_QUANTITY_INF || elem->min <= elem->max));
@@ -1263,7 +1263,7 @@ fillRPRPatternGroup(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth de
 		elem->varId = RPR_VARID_BEGIN;
 		elem->depth = depth;
 		elem->min = node->min;
-		elem->max = (node->max == INT_MAX) ? RPR_QUANTITY_INF : node->max;
+		elem->max = (node->max == PG_INT32_MAX) ? RPR_QUANTITY_INF : node->max;
 		Assert(elem->min >= 0 && elem->min < RPR_QUANTITY_INF &&
 			   elem->max >= 1 &&
 			   (elem->max == RPR_QUANTITY_INF || elem->min <= elem->max));
@@ -1291,7 +1291,7 @@ fillRPRPatternGroup(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth de
 		endElem->varId = RPR_VARID_END;
 		endElem->depth = depth;
 		endElem->min = node->min;
-		endElem->max = (node->max == INT_MAX) ? RPR_QUANTITY_INF : node->max;
+		endElem->max = (node->max == PG_INT32_MAX) ? RPR_QUANTITY_INF : node->max;
 		Assert(endElem->min >= 0 && endElem->min < RPR_QUANTITY_INF &&
 			   endElem->max >= 1 &&
 			   (endElem->max == RPR_QUANTITY_INF || endElem->min <= endElem->max));
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index e0799f35638..ac2e5d7914a 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -212,7 +212,7 @@ static void preprocess_pubobj_list(List *pubobjspec_list,
 static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 static RPRPatternNode *makeRPRSeqOrSingle(List *children, int location);
 static RPRPatternNode *splitRPRTrailingAlt(RPRPatternNode *node, core_yyscan_t yyscanner);
-static RPRPatternNode *makeRPRQuantifier(int min, int max, ParseLoc reluctant, int location,
+static RPRPatternNode *makeRPRQuantifier(int32 min, int32 max, ParseLoc reluctant, int location,
 									   core_yyscan_t yyscanner);
 
 %}
@@ -17745,11 +17745,11 @@ row_pattern_quantifier_opt:
 				}
 			| '*'
 				{
-					$$ = (Node *) makeRPRQuantifier(0, INT_MAX, -1, @1, yyscanner);
+					$$ = (Node *) makeRPRQuantifier(0, PG_INT32_MAX, -1, @1, yyscanner);
 				}
 			| '+'
 				{
-					$$ = (Node *) makeRPRQuantifier(1, INT_MAX, -1, @1, yyscanner);
+					$$ = (Node *) makeRPRQuantifier(1, PG_INT32_MAX, -1, @1, yyscanner);
 				}
 			| Op
 				{
@@ -17757,19 +17757,19 @@ row_pattern_quantifier_opt:
 					if (strcmp($1, "?") == 0)
 						$$ = (Node *) makeRPRQuantifier(0, 1, -1, @1, yyscanner);
 					else if (strcmp($1, "*?") == 0)
-						$$ = (Node *) makeRPRQuantifier(0, INT_MAX, @1 + 1, @1, yyscanner);
+						$$ = (Node *) makeRPRQuantifier(0, PG_INT32_MAX, @1 + 1, @1, yyscanner);
 					else if (strcmp($1, "+?") == 0)
-						$$ = (Node *) makeRPRQuantifier(1, INT_MAX, @1 + 1, @1, yyscanner);
+						$$ = (Node *) makeRPRQuantifier(1, PG_INT32_MAX, @1 + 1, @1, yyscanner);
 					else if (strcmp($1, "??") == 0)
 						$$ = (Node *) makeRPRQuantifier(0, 1, @1 + 1, @1, yyscanner);
 					else if (strcmp($1, "*|") == 0)
 					{
-						$$ = (Node *) makeRPRQuantifier(0, INT_MAX, -1, @1, yyscanner);
+						$$ = (Node *) makeRPRQuantifier(0, PG_INT32_MAX, -1, @1, yyscanner);
 						((RPRPatternNode *) $$)->trailing_alt = true;
 					}
 					else if (strcmp($1, "+|") == 0)
 					{
-						$$ = (Node *) makeRPRQuantifier(1, INT_MAX, -1, @1, yyscanner);
+						$$ = (Node *) makeRPRQuantifier(1, PG_INT32_MAX, -1, @1, yyscanner);
 						((RPRPatternNode *) $$)->trailing_alt = true;
 					}
 					else if (strcmp($1, "?|") == 0)
@@ -17779,12 +17779,12 @@ row_pattern_quantifier_opt:
 					}
 					else if (strcmp($1, "*?|") == 0)
 					{
-						$$ = (Node *) makeRPRQuantifier(0, INT_MAX, @1 + 1, @1, yyscanner);
+						$$ = (Node *) makeRPRQuantifier(0, PG_INT32_MAX, @1 + 1, @1, yyscanner);
 						((RPRPatternNode *) $$)->trailing_alt = true;
 					}
 					else if (strcmp($1, "+?|") == 0)
 					{
-						$$ = (Node *) makeRPRQuantifier(1, INT_MAX, @1 + 1, @1, yyscanner);
+						$$ = (Node *) makeRPRQuantifier(1, PG_INT32_MAX, @1 + 1, @1, yyscanner);
 						((RPRPatternNode *) $$)->trailing_alt = true;
 					}
 					else if (strcmp($1, "??|") == 0)
@@ -17803,11 +17803,11 @@ row_pattern_quantifier_opt:
 			| '*' Op
 				{
 					if (strcmp($2, "?") == 0)
-						$$ = (Node *) makeRPRQuantifier(0, INT_MAX, @2, @1, yyscanner);
+						$$ = (Node *) makeRPRQuantifier(0, PG_INT32_MAX, @2, @1, yyscanner);
 					else if (strcmp($2, "?|") == 0)
 					{
 						/* "A* ?|B" = reluctant "A*?" plus alternation */
-						$$ = (Node *) makeRPRQuantifier(0, INT_MAX, @2, @1, yyscanner);
+						$$ = (Node *) makeRPRQuantifier(0, PG_INT32_MAX, @2, @1, yyscanner);
 						((RPRPatternNode *) $$)->trailing_alt = true;
 					}
 					else
@@ -17820,11 +17820,11 @@ row_pattern_quantifier_opt:
 			| '+' Op
 				{
 					if (strcmp($2, "?") == 0)
-						$$ = (Node *) makeRPRQuantifier(1, INT_MAX, @2, @1, yyscanner);
+						$$ = (Node *) makeRPRQuantifier(1, PG_INT32_MAX, @2, @1, yyscanner);
 					else if (strcmp($2, "?|") == 0)
 					{
 						/* "A+ ?|B" = reluctant "A+?" plus alternation */
-						$$ = (Node *) makeRPRQuantifier(1, INT_MAX, @2, @1, yyscanner);
+						$$ = (Node *) makeRPRQuantifier(1, PG_INT32_MAX, @2, @1, yyscanner);
 						((RPRPatternNode *) $$)->trailing_alt = true;
 					}
 					else
@@ -17860,37 +17860,37 @@ row_pattern_quantifier_opt:
 			/* {n}, {n,}, {,m}, {n,m} quantifiers */
 			| '{' Iconst '}'
 				{
-					if ($2 <= 0 || $2 >= INT_MAX)
+					if ($2 <= 0 || $2 >= PG_INT32_MAX)
 						ereport(ERROR,
 								errcode(ERRCODE_SYNTAX_ERROR),
-								errmsg("quantifier bound must be between 1 and %d", INT_MAX - 1),
+								errmsg("quantifier bound must be between 1 and %d", PG_INT32_MAX - 1),
 								parser_errposition(@2));
 					$$ = (Node *) makeRPRQuantifier($2, $2, -1, @1, yyscanner);
 				}
 			| '{' Iconst ',' '}'
 				{
-					if ($2 < 0 || $2 >= INT_MAX)
+					if ($2 < 0 || $2 >= PG_INT32_MAX)
 						ereport(ERROR,
 								errcode(ERRCODE_SYNTAX_ERROR),
-								errmsg("quantifier bound must be between 0 and %d", INT_MAX - 1),
+								errmsg("quantifier bound must be between 0 and %d", PG_INT32_MAX - 1),
 								parser_errposition(@2));
-					$$ = (Node *) makeRPRQuantifier($2, INT_MAX, -1, @1, yyscanner);
+					$$ = (Node *) makeRPRQuantifier($2, PG_INT32_MAX, -1, @1, yyscanner);
 				}
 			| '{' ',' Iconst '}'
 				{
-					if ($3 <= 0 || $3 >= INT_MAX)
+					if ($3 <= 0 || $3 >= PG_INT32_MAX)
 						ereport(ERROR,
 								errcode(ERRCODE_SYNTAX_ERROR),
-								errmsg("quantifier bound must be between 1 and %d", INT_MAX - 1),
+								errmsg("quantifier bound must be between 1 and %d", PG_INT32_MAX - 1),
 								parser_errposition(@3));
 					$$ = (Node *) makeRPRQuantifier(0, $3, -1, @1, yyscanner);
 				}
 			| '{' Iconst ',' Iconst '}'
 				{
-					if ($2 < 0 || $4 <= 0 || $2 >= INT_MAX || $4 >= INT_MAX)
+					if ($2 < 0 || $4 <= 0 || $2 >= PG_INT32_MAX || $4 >= PG_INT32_MAX)
 						ereport(ERROR,
 								errcode(ERRCODE_SYNTAX_ERROR),
-								errmsg("quantifier bounds must be between 0 and %d with max >= 1", INT_MAX - 1),
+								errmsg("quantifier bounds must be between 0 and %d with max >= 1", PG_INT32_MAX - 1),
 								parser_errposition(@2));
 					if ($2 > $4)
 						ereport(ERROR,
@@ -17908,10 +17908,10 @@ row_pattern_quantifier_opt:
 								errmsg("invalid token after range quantifier"),
 								errhint("Only \"?\" is allowed after {n} to make it reluctant."),
 								parser_errposition(@4));
-					if ($2 <= 0 || $2 >= INT_MAX)
+					if ($2 <= 0 || $2 >= PG_INT32_MAX)
 						ereport(ERROR,
 								errcode(ERRCODE_SYNTAX_ERROR),
-								errmsg("quantifier bound must be between 1 and %d", INT_MAX - 1),
+								errmsg("quantifier bound must be between 1 and %d", PG_INT32_MAX - 1),
 								parser_errposition(@2));
 					$$ = (Node *) makeRPRQuantifier($2, $2, @4, @1, yyscanner);
 					if (strcmp($4, "?|") == 0)
@@ -17925,12 +17925,12 @@ row_pattern_quantifier_opt:
 								errmsg("invalid token after range quantifier"),
 								errhint("Only \"?\" is allowed after {n,} or {,m} to make it reluctant."),
 								parser_errposition(@5));
-					if ($2 < 0 || $2 >= INT_MAX)
+					if ($2 < 0 || $2 >= PG_INT32_MAX)
 						ereport(ERROR,
 								errcode(ERRCODE_SYNTAX_ERROR),
-								errmsg("quantifier bound must be between 0 and %d", INT_MAX - 1),
+								errmsg("quantifier bound must be between 0 and %d", PG_INT32_MAX - 1),
 								parser_errposition(@2));
-					$$ = (Node *) makeRPRQuantifier($2, INT_MAX, @5, @1, yyscanner);
+					$$ = (Node *) makeRPRQuantifier($2, PG_INT32_MAX, @5, @1, yyscanner);
 					if (strcmp($5, "?|") == 0)
 						((RPRPatternNode *) $$)->trailing_alt = true;
 				}
@@ -17942,10 +17942,10 @@ row_pattern_quantifier_opt:
 								errmsg("invalid token after range quantifier"),
 								errhint("Only \"?\" is allowed after {n,} or {,m} to make it reluctant."),
 								parser_errposition(@5));
-					if ($3 <= 0 || $3 >= INT_MAX)
+					if ($3 <= 0 || $3 >= PG_INT32_MAX)
 						ereport(ERROR,
 								errcode(ERRCODE_SYNTAX_ERROR),
-								errmsg("quantifier bound must be between 1 and %d", INT_MAX - 1),
+								errmsg("quantifier bound must be between 1 and %d", PG_INT32_MAX - 1),
 								parser_errposition(@3));
 					$$ = (Node *) makeRPRQuantifier(0, $3, @5, @1, yyscanner);
 					if (strcmp($5, "?|") == 0)
@@ -17959,10 +17959,10 @@ row_pattern_quantifier_opt:
 								errmsg("invalid token after range quantifier"),
 								errhint("Only \"?\" is allowed after {n,m} to make it reluctant."),
 								parser_errposition(@6));
-					if ($2 < 0 || $4 <= 0 || $2 >= INT_MAX || $4 >= INT_MAX)
+					if ($2 < 0 || $4 <= 0 || $2 >= PG_INT32_MAX || $4 >= PG_INT32_MAX)
 						ereport(ERROR,
 								errcode(ERRCODE_SYNTAX_ERROR),
-								errmsg("quantifier bounds must be between 0 and %d with max >= 1", INT_MAX - 1),
+								errmsg("quantifier bounds must be between 0 and %d with max >= 1", PG_INT32_MAX - 1),
 								parser_errposition(@2));
 					if ($2 > $4)
 						ereport(ERROR,
@@ -21400,7 +21400,7 @@ makeRecursiveViewSelect(char *relname, List *aliases, Node *query)
  *		Create an RPRPatternNode with specified quantifier bounds.
  */
 static RPRPatternNode *
-makeRPRQuantifier(int min, int max, ParseLoc reluctant_location, int location,
+makeRPRQuantifier(int32 min, int32 max, ParseLoc reluctant_location, int location,
 				  core_yyscan_t yyscanner)
 {
 	RPRPatternNode *n = makeNode(RPRPatternNode);
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index e8087d4316a..d588cd8263d 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -7124,13 +7124,13 @@ append_pattern_quantifier(StringInfo buf, RPRPatternNode *node)
 		/* {1,1} = no quantifier */
 		has_quantifier = false;
 	}
-	else if (node->min == 0 && node->max == INT_MAX)
+	else if (node->min == 0 && node->max == PG_INT32_MAX)
 		appendStringInfoChar(buf, '*');
-	else if (node->min == 1 && node->max == INT_MAX)
+	else if (node->min == 1 && node->max == PG_INT32_MAX)
 		appendStringInfoChar(buf, '+');
 	else if (node->min == 0 && node->max == 1)
 		appendStringInfoChar(buf, '?');
-	else if (node->max == INT_MAX)
+	else if (node->max == PG_INT32_MAX)
 		appendStringInfo(buf, "{%d,}", node->min);
 	else if (node->min == node->max)
 		appendStringInfo(buf, "{%d}", node->min);
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 77d75aca91f..dca0bbc3e30 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -2672,7 +2672,7 @@ typedef struct WindowAggState
 	bitmapword *nfaVisitedElems;	/* elemIdx visited bitmap for cycle
 									 * detection */
 	int16		nfaVisitedMinWord;	/* lowest bitmapword index touched since
-									 * last reset (INT16_MAX = none) */
+									 * last reset (PG_INT16_MAX = none) */
 	int16		nfaVisitedMaxWord;	/* highest bitmapword index touched since
 									 * last reset (-1 = none) */
 	int64		nfaLastProcessedRow;	/* last row processed by NFA (-1 =
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 9636a8efca1..16f31dbe7ff 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -625,8 +625,8 @@ typedef struct RPRPatternNode
 {
 	NodeTag		type;			/* T_RPRPatternNode */
 	RPRPatternNodeType nodeType;	/* VAR, SEQ, ALT, GROUP */
-	int			min;			/* minimum repetitions (0 for *, ?) */
-	int			max;			/* maximum repetitions (INT_MAX for *, +) */
+	int32		min;			/* minimum repetitions (0 for *, ?) */
+	int32		max;			/* maximum repetitions (PG_INT32_MAX for *, +) */
 	bool		reluctant;		/* true for reluctant (non-greedy) */
 	ParseLoc	reluctant_location; /* location of '?' token, or -1 */
 	ParseLoc	location;		/* token location, or -1 */
diff --git a/src/include/optimizer/rpr.h b/src/include/optimizer/rpr.h
index 73c827de2b1..8e0bc7efc53 100644
--- a/src/include/optimizer/rpr.h
+++ b/src/include/optimizer/rpr.h
@@ -26,12 +26,13 @@
  * before release.
  */
 #define RPR_VARID_MAX		0xEF	/* pattern variables are 0 to 0xEF */
-#define RPR_QUANTITY_INF	INT32_MAX	/* unbounded quantifier */
-#define RPR_COUNT_MAX		INT32_MAX	/* max runtime count (NFA state) */
-#define RPR_ELEMIDX_MAX		INT16_MAX	/* max pattern elements */
+#define RPR_QUANTITY_INF	PG_INT32_MAX	/* unbounded quantifier */
+#define RPR_COUNT_MAX		PG_INT32_MAX	/* max runtime count (NFA state) */
+#define RPR_ELEMIDX_MAX		PG_INT16_MAX	/* max pattern elements */
 #define RPR_ELEMIDX_INVALID	((RPRElemIdx) -1)	/* invalid index */
-#define RPR_DEPTH_MAX		(UINT8_MAX - 1) /* max pattern nesting depth: 254 */
-#define RPR_DEPTH_NONE		UINT8_MAX	/* no enclosing group (top-level) */
+#define RPR_DEPTH_MAX		(PG_UINT8_MAX - 1)	/* max pattern nesting depth:
+												 * 254 */
+#define RPR_DEPTH_NONE		PG_UINT8_MAX	/* no enclosing group (top-level) */
 
 /* Reserved control-element varIds (high nibble 0xF; 0xF0-0xFB spare) */
 #define RPR_VARID_BEGIN		((RPRVarId) 0xFC)	/* group begin */
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0064-Modernize-idioms-in-row-pattern-recognition-code.txt (6.2K, 67-nocfbot-0064-Modernize-idioms-in-row-pattern-recognition-code.txt)
  download | inline diff:
From ad3e63eea24c4f80f8d18ef7b5cc40fff14e4828 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Mon, 8 Jun 2026 13:59:43 +0900
Subject: [PATCH 64/68] Modernize idioms in row pattern recognition code

Replace appendStringInfo() with appendStringInfoString() for the eight
constant string literals that contain no format specifiers, avoiding an
unnecessary trip through vsnprintf().

Replace the four palloc(n * sizeof(type)) calls in _copyRPRPattern() and
_readRPRPattern() with palloc_array()/palloc0_array(), which make the
element type and count explicit and guard against size multiplication
overflow.

Replace an open-coded maximum in scanRPRPatternRecursive() with Max().

These are mechanical idiom changes only; behavior, output, and ABI are
unchanged, and the row pattern regression tests pass with no .out diffs.
---
 src/backend/commands/explain.c    | 15 ++++++++-------
 src/backend/nodes/copyfuncs.c     |  4 ++--
 src/backend/nodes/readfuncs.c     |  4 ++--
 src/backend/optimizer/plan/rpr.c  |  3 +--
 src/backend/utils/adt/ruleutils.c |  3 ++-
 5 files changed, 15 insertions(+), 14 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 7ba0b6df849..7992829d5c4 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -2932,7 +2932,8 @@ append_rpr_quantifier(StringInfo buf, RPRPatternElement *elem)
 	if (RPRElemIsReluctant(elem))
 	{
 		if (elem->min == 1 && elem->max == 1)
-			appendStringInfo(buf, "{1}");	/* make reluctant ? unambiguous */
+			appendStringInfoString(buf, "{1}"); /* make reluctant ?
+												 * unambiguous */
 		appendStringInfoChar(buf, '?');
 	}
 
@@ -3929,7 +3930,7 @@ show_rpr_nfa_stats(WindowAggState *winstate, ExplainState *es)
 
 		/* Match/mismatch counts with length min/max/avg */
 		ExplainIndentText(es);
-		appendStringInfo(es->str, "NFA: ");
+		appendStringInfoString(es->str, "NFA: ");
 		if (winstate->nfaMatchesSucceeded > 0)
 		{
 			double		avgLen = (double) winstate->nfaMatchLen.total / winstate->nfaMatchesSucceeded;
@@ -3943,7 +3944,7 @@ show_rpr_nfa_stats(WindowAggState *winstate, ExplainState *es)
 		}
 		else
 		{
-			appendStringInfo(es->str, "0 matched");
+			appendStringInfoString(es->str, "0 matched");
 		}
 		if (winstate->nfaMatchesFailed > 0)
 		{
@@ -3958,7 +3959,7 @@ show_rpr_nfa_stats(WindowAggState *winstate, ExplainState *es)
 		}
 		else
 		{
-			appendStringInfo(es->str, ", 0 mismatched");
+			appendStringInfoString(es->str, ", 0 mismatched");
 		}
 		appendStringInfoChar(es->str, '\n');
 
@@ -3966,7 +3967,7 @@ show_rpr_nfa_stats(WindowAggState *winstate, ExplainState *es)
 		if (winstate->nfaContextsAbsorbed > 0 || winstate->nfaContextsSkipped > 0)
 		{
 			ExplainIndentText(es);
-			appendStringInfo(es->str, "NFA: ");
+			appendStringInfoString(es->str, "NFA: ");
 
 			if (winstate->nfaContextsAbsorbed > 0)
 			{
@@ -3981,7 +3982,7 @@ show_rpr_nfa_stats(WindowAggState *winstate, ExplainState *es)
 			}
 			else
 			{
-				appendStringInfo(es->str, "0 absorbed");
+				appendStringInfoString(es->str, "0 absorbed");
 			}
 
 			if (winstate->nfaContextsSkipped > 0)
@@ -3997,7 +3998,7 @@ show_rpr_nfa_stats(WindowAggState *winstate, ExplainState *es)
 			}
 			else
 			{
-				appendStringInfo(es->str, ", 0 skipped");
+				appendStringInfoString(es->str, ", 0 skipped");
 			}
 
 			appendStringInfoChar(es->str, '\n');
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index e67ad39bdb8..17d45930d7b 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -178,13 +178,13 @@ _copyRPRPattern(const RPRPattern *from)
 
 	/* Deep copy the varNames array (DEFINE clause is required) */
 	Assert(from->numVars > 0);
-	newnode->varNames = palloc0(from->numVars * sizeof(char *));
+	newnode->varNames = palloc0_array(char *, from->numVars);
 	for (int i = 0; i < from->numVars; i++)
 		newnode->varNames[i] = pstrdup(from->varNames[i]);
 
 	/* Deep copy the elements array (always has at least one element + FIN) */
 	Assert(from->numElements >= 2);
-	newnode->elements = palloc(from->numElements * sizeof(RPRPatternElement));
+	newnode->elements = palloc_array(RPRPatternElement, from->numElements);
 	memcpy(newnode->elements, from->elements,
 		   from->numElements * sizeof(RPRPatternElement));
 
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
index 5bbde5bcad2..6c39c6fe06d 100644
--- a/src/backend/nodes/readfuncs.c
+++ b/src/backend/nodes/readfuncs.c
@@ -582,7 +582,7 @@ _readRPRPattern(void)
 	token = pg_strtok(&length); /* get '(' or '<>' */
 	if (local_node->numVars > 0 && token[0] == '(')
 	{
-		local_node->varNames = palloc(local_node->numVars * sizeof(char *));
+		local_node->varNames = palloc_array(char *, local_node->numVars);
 		for (int i = 0; i < local_node->numVars; i++)
 		{
 			token = pg_strtok(&length);
@@ -600,7 +600,7 @@ _readRPRPattern(void)
 	token = pg_strtok(&length); /* get '(' or '<>' */
 	if (local_node->numElements > 0 && token[0] == '(')
 	{
-		local_node->elements = palloc0(local_node->numElements * sizeof(RPRPatternElement));
+		local_node->elements = palloc0_array(RPRPatternElement, local_node->numElements);
 		for (int i = 0; i < local_node->numElements; i++)
 		{
 			RPRPatternElement *elem = &local_node->elements[i];
diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index 143ea034fb3..9364136b071 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -1048,8 +1048,7 @@ scanRPRPatternRecursive(RPRPatternNode *node, char **varNames, int *numVars,
 						  depth, RPR_DEPTH_MAX - 1));
 
 	/* Track maximum depth */
-	if (depth > *maxDepth)
-		*maxDepth = depth;
+	*maxDepth = Max(*maxDepth, depth);
 
 	switch (node->nodeType)
 	{
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 2b8439e452e..e8087d4316a 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -7140,7 +7140,8 @@ append_pattern_quantifier(StringInfo buf, RPRPatternNode *node)
 	if (node->reluctant)
 	{
 		if (!has_quantifier)
-			appendStringInfo(buf, "{1}");	/* make reluctant ? unambiguous */
+			appendStringInfoString(buf, "{1}"); /* make reluctant ?
+												 * unambiguous */
 		appendStringInfoChar(buf, '?');
 	}
 }
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0067-Add-row-pattern-recognition-negative-and-coverage.txt (10.9K, 68-nocfbot-0067-Add-row-pattern-recognition-negative-and-coverage.txt)
  download | inline diff:
From 033b6d0bd42a41b2501d34fb413cb889caffa366 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Mon, 8 Jun 2026 16:34:09 +0900
Subject: [PATCH 67/68] Add row pattern recognition negative and coverage tests

Cover the split-token error paths after a quantifier (* / + / Op and
the {n}/{n,}/{,m}/{n,m} range forms), rejection of set-returning and
window functions in a DEFINE clause, and the reluctant-GROUP case where
quantifier multiplication is correctly suppressed.  Tests only.
---
 src/test/regress/expected/rpr_base.out | 91 ++++++++++++++++++++++++--
 src/test/regress/sql/rpr_base.sql      | 37 +++++++++--
 2 files changed, 120 insertions(+), 8 deletions(-)

diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index f1767305d06..2407c455164 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -3283,7 +3283,48 @@ ERROR:  unsupported quantifier "+!"
 LINE 6:     PATTERN (A+!)
                       ^
 HINT:  Valid quantifiers are: *, +, ?, *?, +?, ??, {n}, {n,}, {,m}, {n,m} and their reluctant versions.
--- Expected: Syntax error
+-- none of the following 4 queries should be accepted
+SELECT FROM rpr_err WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A+ !) DEFINE A AS TRUE);
+ERROR:  invalid token after "+" quantifier
+LINE 1: ...S BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A+ !) DEFINE ...
+                                                             ^
+HINT:  Did you mean "+?" for reluctant quantifier?
+SELECT FROM rpr_err WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A+ ?+) DEFINE A AS TRUE);
+ERROR:  invalid token after "+" quantifier
+LINE 1: ...S BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A+ ?+) DEFINE...
+                                                             ^
+HINT:  Did you mean "+?" for reluctant quantifier?
+SELECT FROM rpr_err WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A* ?+) DEFINE A AS TRUE);
+ERROR:  invalid token after "*" quantifier
+LINE 1: ...S BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A* ?+) DEFINE...
+                                                             ^
+HINT:  Did you mean "*?" for reluctant quantifier?
+SELECT FROM rpr_err WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A? ??) DEFINE A AS TRUE);
+ERROR:  invalid quantifier combination
+LINE 1: ...OWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A? ??) DEFI...
+                                                             ^
+HINT:  Did you mean "??" for reluctant quantifier?
+-- none of the following 4 range-quantifier queries should be accepted
+SELECT FROM rpr_err WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A{2} !) DEFINE A AS TRUE);
+ERROR:  invalid token after range quantifier
+LINE 1: ...BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A{2} !) DEFINE ...
+                                                             ^
+HINT:  Only "?" is allowed after {n} to make it reluctant.
+SELECT FROM rpr_err WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A{2,} !) DEFINE A AS TRUE);
+ERROR:  invalid token after range quantifier
+LINE 1: ...ETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A{2,} !) DEFINE ...
+                                                             ^
+HINT:  Only "?" is allowed after {n,} or {,m} to make it reluctant.
+SELECT FROM rpr_err WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A{,3} !) DEFINE A AS TRUE);
+ERROR:  invalid token after range quantifier
+LINE 1: ...ETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A{,3} !) DEFINE ...
+                                                             ^
+HINT:  Only "?" is allowed after {n,} or {,m} to make it reluctant.
+SELECT FROM rpr_err WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A{2,3} !) DEFINE A AS TRUE);
+ERROR:  invalid token after range quantifier
+LINE 1: ...TWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A{2,3} !) DEFINE ...
+                                                             ^
+HINT:  Only "?" is allowed after {n,m} to make it reluctant.
 -- Unmatched parentheses
 SET client_min_messages = NOTICE;
 DO $$
@@ -3310,7 +3351,6 @@ WINDOW w AS (
 ERROR:  syntax error at or near ")"
 LINE 8: );
         ^
--- Expected: Syntax error
 -- ERROR: empty PATTERN not allowed
 SELECT COUNT(*) OVER w
 FROM rpr_err
@@ -3323,7 +3363,6 @@ WINDOW w AS (
 ERROR:  syntax error at or near ")"
 LINE 6:     PATTERN ()
                      ^
--- Expected: Syntax error
 -- ERROR: DEFINE without PATTERN (PATTERN and DEFINE must be used together)
 SELECT COUNT(*) OVER w
 FROM rpr_err
@@ -3335,7 +3374,6 @@ WINDOW w AS (
 ERROR:  syntax error at or near "DEFINE"
 LINE 6:     DEFINE A AS val > 0
             ^
--- Expected: Syntax error
 -- Qualified column references (NOT SUPPORTED)
 -- Pattern variable qualified name: not supported (valid per ISO/IEC 19075-5 6.15 / 4.16, not yet implemented)
 SELECT COUNT(*) OVER w
@@ -3484,6 +3522,18 @@ WINDOW w AS (
 ERROR:  aggregate functions are not allowed in DEFINE
 LINE 7:     DEFINE A AS COUNT(*) > 0
                         ^
+-- ERROR: set-returning function in DEFINE is not supported
+SELECT FROM rpr_err
+WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A+) DEFINE A AS 1 > generate_series(1 ,2));
+ERROR:  set-returning functions are not allowed in DEFINE
+LINE 2: ... ROW AND 1 FOLLOWING PATTERN (A+) DEFINE A AS 1 > generate_s...
+                                                             ^
+-- ERROR: window function in DEFINE is not supported
+SELECT FROM rpr_err
+WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A+) DEFINE A AS 1 > row_number() OVER ());
+ERROR:  window functions are not allowed in DEFINE
+LINE 2: ... ROW AND 1 FOLLOWING PATTERN (A+) DEFINE A AS 1 > row_number...
+                                                             ^
 -- Subquery in DEFINE is not supported
 SELECT COUNT(*) OVER w
 FROM rpr_err
@@ -3925,6 +3975,39 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
          ->  Seq Scan on rpr_plan
 (7 rows)
 
+-- Quantifier NO multiply: reluctant GROUP child (((A B){2}?){3}) stays nested
+-- a reluctant quantifier on a GROUP is not subject to multiplication
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN (((A B){2}?){3}) DEFINE A AS val > 0, B AS val > 0);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: ((a b){2}?){3}
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 rows)
+
+-- Quantifier multiply control: greedy GROUP (((A B){2}){3}) -> (a b){6}
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN (((A B){2}){3}) DEFINE A AS val > 0, B AS val > 0);
+                                  QUERY PLAN                                   
+-------------------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: (a b){6}
+   Nav Mark Lookback: 0
+   ->  Sort
+         Sort Key: id
+         ->  Seq Scan on rpr_plan
+(7 rows)
+
 -- Quantifier multiply with child range: (A{2,3}){3} -> a{6,9}
 -- outer exact, child range - optimization applies
 EXPLAIN (COSTS OFF)
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index 7dc13232d89..cec7ea1e8db 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -2145,7 +2145,18 @@ WINDOW w AS (
     PATTERN (A+!)
     DEFINE A AS val > 0
 );
--- Expected: Syntax error
+
+-- none of the following 4 queries should be accepted
+SELECT FROM rpr_err WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A+ !) DEFINE A AS TRUE);
+SELECT FROM rpr_err WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A+ ?+) DEFINE A AS TRUE);
+SELECT FROM rpr_err WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A* ?+) DEFINE A AS TRUE);
+SELECT FROM rpr_err WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A? ??) DEFINE A AS TRUE);
+
+-- none of the following 4 range-quantifier queries should be accepted
+SELECT FROM rpr_err WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A{2} !) DEFINE A AS TRUE);
+SELECT FROM rpr_err WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A{2,} !) DEFINE A AS TRUE);
+SELECT FROM rpr_err WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A{,3} !) DEFINE A AS TRUE);
+SELECT FROM rpr_err WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A{2,3} !) DEFINE A AS TRUE);
 
 -- Unmatched parentheses
 SET client_min_messages = NOTICE;
@@ -2170,7 +2181,6 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE
 );
--- Expected: Syntax error
 
 -- ERROR: empty PATTERN not allowed
 SELECT COUNT(*) OVER w
@@ -2181,7 +2191,6 @@ WINDOW w AS (
     PATTERN ()
     DEFINE A AS val > 0
 );
--- Expected: Syntax error
 
 -- ERROR: DEFINE without PATTERN (PATTERN and DEFINE must be used together)
 SELECT COUNT(*) OVER w
@@ -2191,7 +2200,6 @@ WINDOW w AS (
     ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
     DEFINE A AS val > 0
 );
--- Expected: Syntax error
 
 -- Qualified column references (NOT SUPPORTED)
 
@@ -2315,6 +2323,14 @@ WINDOW w AS (
     DEFINE A AS COUNT(*) > 0
 );
 
+-- ERROR: set-returning function in DEFINE is not supported
+SELECT FROM rpr_err
+WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A+) DEFINE A AS 1 > generate_series(1 ,2));
+
+-- ERROR: window function in DEFINE is not supported
+SELECT FROM rpr_err
+WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A+) DEFINE A AS 1 > row_number() OVER ());
+
 -- Subquery in DEFINE is not supported
 SELECT COUNT(*) OVER w
 FROM rpr_err
@@ -2549,6 +2565,19 @@ SELECT COUNT(*) OVER w FROM rpr_plan
 WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
              PATTERN ((A{2}){3}) DEFINE A AS val > 0);
 
+-- Quantifier NO multiply: reluctant GROUP child (((A B){2}?){3}) stays nested
+-- a reluctant quantifier on a GROUP is not subject to multiplication
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN (((A B){2}?){3}) DEFINE A AS val > 0, B AS val > 0);
+
+-- Quantifier multiply control: greedy GROUP (((A B){2}){3}) -> (a b){6}
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN (((A B){2}){3}) DEFINE A AS val > 0, B AS val > 0);
+
 -- Quantifier multiply with child range: (A{2,3}){3} -> a{6,9}
 -- outer exact, child range - optimization applies
 EXPLAIN (COSTS OFF)
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0066-Tidy-up-row-pattern-recognition-regression-test-c.txt (58.3K, 69-nocfbot-0066-Tidy-up-row-pattern-recognition-regression-test-c.txt)
  download | inline diff:
From 5221784ac469bb213fae2853beab7f5b2b4562cd Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Mon, 8 Jun 2026 16:22:16 +0900
Subject: [PATCH 66/68] Tidy up row pattern recognition regression test
 comments

Drop the "-- Expected: ERROR: ..." comments that repeat exact error text
(they go stale; the .out already has the real error) and reword comments
that embedded literal limits.  Rename the "Jacob's Patterns" section and
clarify the serialization section's deparse/re-parse round-trip intent.

Test comments only; rpr_base.out regenerated, no output changes.
---
 src/test/regress/expected/rpr_base.out | 169 +++++++----------------
 src/test/regress/sql/rpr_base.sql      | 184 +++++++------------------
 2 files changed, 102 insertions(+), 251 deletions(-)

diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index 1fcb2ce22f0..f1767305d06 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -12,7 +12,7 @@
 --   Quantifiers Tests
 --   Navigation Functions Tests
 --   SKIP TO / INITIAL Tests
---   Serialization/Deserialization Tests (objects kept for pg_upgrade/pg_dump)
+--   Serialization/Deserialization Tests
 --   Glued Quantifier / Alternation Tests
 --   Error Cases Tests
 --   Window Deduplication Tests
@@ -34,7 +34,7 @@
 --   Error Limit Tests
 --
 -- Contributed Tests:
---   Jacob's Patterns
+--   Basic Pattern Matching
 --   Pathological Patterns
 -- ============================================================
 SET client_min_messages = WARNING;
@@ -50,6 +50,7 @@ CREATE TABLE rpr_keywords (
     past INT,        -- PAST keyword
     pattern INT,     -- PATTERN keyword
     seek INT,        -- SEEK keyword
+-- ERROR: SEEK is not supported
     skip INT         -- SKIP keyword (pre-existing)
 );
 INSERT INTO rpr_keywords VALUES (1, 10, 20, 30, 40, 50, 60);
@@ -224,7 +225,7 @@ DROP TABLE rpr_auto;
 -- Duplicate variable names
 CREATE TABLE rpr_dup (id INT);
 INSERT INTO rpr_dup VALUES (1), (2);
--- Duplicate DEFINE entries
+-- Duplicate DEFINE variable name is not allowed
 SELECT COUNT(*) OVER w
 FROM rpr_dup
 WINDOW w AS (
@@ -236,12 +237,11 @@ WINDOW w AS (
 ERROR:  DEFINE variable "a" appears more than once
 LINE 7:     DEFINE A AS id > 0, A AS id < 10
                    ^
--- Expected: ERROR: row pattern definition variable name "a" appears more than once in DEFINE clause
 DROP TABLE rpr_dup;
 -- Boolean coercion
 CREATE TABLE rpr_bool (id INT, flag BOOLEAN);
 INSERT INTO rpr_bool VALUES (1, true), (2, false);
--- Non-boolean expression
+-- DEFINE clause must be a boolean expression
 SELECT COUNT(*) OVER w
 FROM rpr_bool
 WINDOW w AS (
@@ -253,7 +253,6 @@ WINDOW w AS (
 ERROR:  argument of DEFINE must be type boolean, not type integer
 LINE 7:     DEFINE A AS id
                         ^
--- Expected: ERROR: argument of DEFINE must be type boolean
 -- Boolean column reference
 SELECT id, flag, COUNT(*) OVER w as cnt
 FROM rpr_bool
@@ -393,8 +392,7 @@ ORDER BY id;
   6 |  30 |   1
 (6 rows)
 
--- Invalid frame start positions
--- Not starting at CURRENT ROW
+-- ERROR: frame must start at current row when row pattern recognition is used
 SELECT COUNT(*) OVER w
 FROM rpr_frame
 WINDOW w AS (
@@ -408,7 +406,6 @@ LINE 5:     ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
             ^
 DETAIL:  Current frame starts with UNBOUNDED PRECEDING.
 HINT:  Use: ROWS BETWEEN CURRENT ROW AND ...
--- Expected: ERROR: FRAME must start at current row when row pattern recognition is used
 -- EXCLUDE options
 -- EXCLUDE not permitted
 SELECT COUNT(*) OVER w
@@ -425,7 +422,6 @@ LINE 6:     EXCLUDE CURRENT ROW
             ^
 DETAIL:  Frame definition includes EXCLUDE CURRENT ROW.
 HINT:  Remove the EXCLUDE clause from the window definition.
--- Expected: ERROR: cannot use EXCLUDE options with row pattern recognition
 -- EXCLUDE GROUP not permitted
 SELECT COUNT(*) OVER w
 FROM rpr_frame
@@ -441,7 +437,6 @@ LINE 6:     EXCLUDE GROUP
             ^
 DETAIL:  Frame definition includes EXCLUDE GROUP.
 HINT:  Remove the EXCLUDE clause from the window definition.
--- Expected: ERROR: cannot use EXCLUDE options with row pattern recognition
 -- EXCLUDE TIES not permitted
 SELECT COUNT(*) OVER w
 FROM rpr_frame
@@ -457,8 +452,7 @@ LINE 6:     EXCLUDE TIES
             ^
 DETAIL:  Frame definition includes EXCLUDE TIES.
 HINT:  Remove the EXCLUDE clause from the window definition.
--- Expected: ERROR: cannot use EXCLUDE options with row pattern recognition
--- RANGE frame not starting at CURRENT ROW
+-- range frame is not allowed with RPR
 SELECT COUNT(*) OVER w
 FROM rpr_frame
 WINDOW w AS (
@@ -471,8 +465,7 @@ ERROR:  cannot use FRAME option RANGE with row pattern recognition
 LINE 5:     RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWIN...
             ^
 HINT:  Use ROWS instead.
--- Expected: ERROR: cannot use FRAME option RANGE with row pattern recognition
--- GROUPS frame not starting at CURRENT ROW
+-- GROUPS frame is not allowed with RPR
 SELECT COUNT(*) OVER w
 FROM rpr_frame
 WINDOW w AS (
@@ -485,8 +478,7 @@ ERROR:  cannot use FRAME option GROUPS with row pattern recognition
 LINE 5:     GROUPS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWI...
             ^
 HINT:  Use ROWS instead.
--- Expected: ERROR: cannot use FRAME option GROUPS with row pattern recognition
--- Starting with N PRECEDING
+-- ERROR: frame must start at current row when row pattern recognition is used
 SELECT COUNT(*) OVER w
 FROM rpr_frame
 WINDOW w AS (
@@ -500,8 +492,7 @@ LINE 5:     ROWS BETWEEN 1 PRECEDING AND UNBOUNDED FOLLOWING
             ^
 DETAIL:  Current frame starts with offset PRECEDING.
 HINT:  Use: ROWS BETWEEN CURRENT ROW AND ...
--- Expected: ERROR: FRAME must start at current row when row pattern recognition is used
--- Starting with N FOLLOWING
+-- ERROR: frame must start at current row with RPR
 SELECT COUNT(*) OVER w
 FROM rpr_frame
 WINDOW w AS (
@@ -515,9 +506,7 @@ LINE 5:     ROWS BETWEEN 1 FOLLOWING AND UNBOUNDED FOLLOWING
             ^
 DETAIL:  Current frame starts with offset FOLLOWING.
 HINT:  Use: ROWS BETWEEN CURRENT ROW AND ...
--- Expected: ERROR: FRAME must start at current row when row pattern recognition is used
--- Frame end bound edge cases
--- End before start: CURRENT ROW AND 1 PRECEDING
+-- ERROR: end before start: CURRENT ROW AND 1 PRECEDING
 SELECT COUNT(*) OVER w
 FROM rpr_frame
 WINDOW w AS (
@@ -529,8 +518,7 @@ WINDOW w AS (
 ERROR:  frame starting from current row cannot have preceding rows
 LINE 5:     ROWS BETWEEN CURRENT ROW AND 1 PRECEDING
                                          ^
--- Expected: ERROR: frame starting from current row cannot have preceding rows
--- End before start: CURRENT ROW AND UNBOUNDED PRECEDING
+-- ERROR: end before start: CURRENT ROW AND UNBOUNDED PRECEDING
 SELECT COUNT(*) OVER w
 FROM rpr_frame
 WINDOW w AS (
@@ -542,7 +530,6 @@ WINDOW w AS (
 ERROR:  frame end cannot be UNBOUNDED PRECEDING
 LINE 5:     ROWS BETWEEN CURRENT ROW AND UNBOUNDED PRECEDING
                                          ^
--- Expected: ERROR: frame end cannot be UNBOUNDED PRECEDING
 -- Single row frame: CURRENT ROW AND CURRENT ROW is rejected (the standard
 -- allows only UNBOUNDED FOLLOWING or a positive offset FOLLOWING).
 SELECT id, val, COUNT(*) OVER w as cnt
@@ -559,7 +546,6 @@ ERROR:  cannot use CURRENT ROW as frame end with row pattern recognition
 LINE 5:     ROWS BETWEEN CURRENT ROW AND CURRENT ROW
             ^
 HINT:  Use UNBOUNDED FOLLOWING or a positive offset FOLLOWING.
--- Expected: ERROR: cannot use CURRENT ROW as frame end with row pattern recognition
 -- Zero offset: CURRENT ROW AND 0 FOLLOWING denotes the same one-row frame
 -- and is likewise rejected (caught at execution time).
 SELECT id, val, COUNT(*) OVER w as cnt
@@ -573,7 +559,6 @@ WINDOW w AS (
 )
 ORDER BY id;
 ERROR:  frame ending offset must be positive with row pattern recognition
--- Expected: ERROR: frame ending offset must be positive with row pattern recognition
 -- A non-constant frame end offset is allowed; a zero value is still rejected,
 -- this time at execution time (a literal cannot exercise that path).
 PREPARE rpr_end_offset(int8) AS
@@ -600,7 +585,6 @@ EXECUTE rpr_end_offset(2);
 
 EXECUTE rpr_end_offset(0);
 ERROR:  frame ending offset must be positive with row pattern recognition
--- Expected: ERROR: frame ending offset must be positive with row pattern recognition
 DEALLOCATE rpr_end_offset;
 -- Large offset: CURRENT ROW AND 1000 FOLLOWING
 SELECT id, val, COUNT(*) OVER w as cnt
@@ -669,7 +653,7 @@ ORDER BY id;
   6 |  30 |   1
 (6 rows)
 
--- RANGE frame with RPR (not permitted)
+-- range frame is not allowed with RPR
 SELECT id, val, COUNT(*) OVER w as cnt
 FROM rpr_frame
 WINDOW w AS (
@@ -684,7 +668,6 @@ ERROR:  cannot use FRAME option RANGE with row pattern recognition
 LINE 5:     RANGE BETWEEN CURRENT ROW AND 10 FOLLOWING
             ^
 HINT:  Use ROWS instead.
--- Expected: ERROR: cannot use FRAME option RANGE with row pattern recognition
 -- GROUPS frame with RPR (not permitted)
 SELECT id, val, COUNT(*) OVER w as cnt
 FROM rpr_frame
@@ -700,7 +683,6 @@ ERROR:  cannot use FRAME option GROUPS with row pattern recognition
 LINE 5:     GROUPS BETWEEN CURRENT ROW AND 1 FOLLOWING
             ^
 HINT:  Use ROWS instead.
--- Expected: ERROR: cannot use FRAME option GROUPS with row pattern recognition
 DROP TABLE rpr_frame;
 -- ============================================================
 -- PARTITION BY + FRAME Tests
@@ -749,7 +731,6 @@ ERROR:  cannot use FRAME option RANGE with row pattern recognition
 LINE 6:     RANGE BETWEEN CURRENT ROW AND 10 FOLLOWING
             ^
 HINT:  Use ROWS instead.
--- Expected: ERROR: cannot use FRAME option RANGE with row pattern recognition
 DROP TABLE rpr_partition;
 -- ============================================================
 -- PATTERN Syntax Tests
@@ -1011,7 +992,6 @@ ORDER BY id;
 ERROR:  quantifier bound must be between 1 and 2147483646
 LINE 6:     PATTERN (A{0} B)
                        ^
--- Expected: ERROR: quantifier bound must be between 1 and 2147483646
 -- {0,0} is not allowed (max must be >= 1)
 SELECT id, val, COUNT(*) OVER w as cnt
 FROM rpr_quant
@@ -1025,7 +1005,6 @@ ORDER BY id;
 ERROR:  quantifier bounds must be between 0 and 2147483646 with max >= 1
 LINE 6:     PATTERN (A{0,0} B)
                        ^
--- Expected: ERROR: quantifier bounds must be between 0 and 2147483646 with max >= 1
 -- {0,1} (equivalent to ?)
 SELECT id, val, COUNT(*) OVER w as cnt
 FROM rpr_quant
@@ -1155,6 +1134,7 @@ DROP TABLE rpr_quant;
 CREATE TABLE rpr_reluctant (id INT, val INT);
 INSERT INTO rpr_reluctant VALUES (1, 10), (2, 20), (3, 30);
 -- *? (zero or more, reluctant)
+-- Reluctant quantifier: prefer shortest match
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1170,8 +1150,8 @@ WINDOW w AS (
      0
 (3 rows)
 
--- Reluctant quantifier: prefer shortest match
 -- +? (one or more, reluctant)
+-- Reluctant quantifier: prefer shortest match
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1187,8 +1167,8 @@ WINDOW w AS (
      1
 (3 rows)
 
--- Reluctant quantifier: prefer shortest match
 -- ?? (zero or one, reluctant)
+-- Reluctant quantifier: prefer shortest match
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1204,8 +1184,8 @@ WINDOW w AS (
      0
 (3 rows)
 
--- Reluctant quantifier: prefer shortest match
 -- {n,}? (n or more, reluctant)
+-- Reluctant quantifier: prefer shortest match
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1221,8 +1201,8 @@ WINDOW w AS (
      0
 (3 rows)
 
--- Reluctant quantifier: prefer shortest match
 -- {n,m}? (n to m, reluctant)
+-- Reluctant quantifier: prefer shortest match
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1238,8 +1218,8 @@ WINDOW w AS (
      1
 (3 rows)
 
--- Reluctant quantifier: prefer shortest match
 -- {n}? (exactly n, reluctant)
+-- Reluctant quantifier: prefer shortest match
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1255,8 +1235,8 @@ WINDOW w AS (
      0
 (3 rows)
 
--- Reluctant quantifier: prefer shortest match
 -- {,m}? (up to m, reluctant) - COMPLETELY UNTESTED RULE!
+-- Reluctant quantifier: prefer shortest match
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1272,8 +1252,6 @@ WINDOW w AS (
      0
 (3 rows)
 
--- Reluctant quantifier: prefer shortest match
--- Invalid reluctant patterns (wrong token after quantifier)
 -- {2}+ (should be {2}? not {2}+)
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
@@ -1286,7 +1264,6 @@ WINDOW w AS (
 ERROR:  syntax error at or near "+"
 LINE 6:     PATTERN (A{2}+)
                          ^
--- Expected: ERROR: syntax error at or near "+"
 -- {2,}* (should be {2,}? not {2,}*)
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
@@ -1299,7 +1276,6 @@ WINDOW w AS (
 ERROR:  syntax error at or near "*"
 LINE 6:     PATTERN (A{2,}*)
                           ^
--- Expected: ERROR: syntax error at or near "*"
 -- {,3}* (should be {,3}? not {,3}*)
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
@@ -1312,7 +1288,6 @@ WINDOW w AS (
 ERROR:  syntax error at or near "*"
 LINE 6:     PATTERN (A{,3}*)
                           ^
--- Expected: ERROR: syntax error at or near "*"
 -- {1,3}+ (should be {1,3}? not {1,3}+)
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
@@ -1325,9 +1300,8 @@ WINDOW w AS (
 ERROR:  syntax error at or near "+"
 LINE 6:     PATTERN (A{1,3}+)
                            ^
--- Expected: ERROR: syntax error at or near "+"
 -- Boundary errors in reluctant quantifiers
--- {-1}? (negative bound)
+-- negative bound is not allowed
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1339,8 +1313,7 @@ WINDOW w AS (
 ERROR:  syntax error at or near "-"
 LINE 6:     PATTERN (A{-1}?)
                        ^
--- Expected: ERROR: syntax error at or near "-"
--- {2147483647}? (INT_MAX)
+-- ERROR: quantifier bound exceeds limits
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1352,8 +1325,7 @@ WINDOW w AS (
 ERROR:  quantifier bound must be between 1 and 2147483646
 LINE 6:     PATTERN (A{2147483647}?)
                        ^
--- Expected: ERROR: quantifier bound must be between 1 and 2147483646
--- {-1,}? (negative lower bound)
+-- negative lower bound is not allowed
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1365,8 +1337,7 @@ WINDOW w AS (
 ERROR:  syntax error at or near "-"
 LINE 6:     PATTERN (A{-1,}?)
                        ^
--- Expected: ERROR: syntax error at or near "-"
--- {2147483647,}? (INT_MAX lower bound)
+-- ERROR: quantifier lower bound exceeds limits
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1378,8 +1349,7 @@ WINDOW w AS (
 ERROR:  quantifier bound must be between 0 and 2147483646
 LINE 6:     PATTERN (A{2147483647,}?)
                        ^
--- Expected: ERROR: quantifier bound must be between 0 and 2147483646
--- {,0}? (zero upper bound)
+-- zero upper bound is not allowed
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1391,8 +1361,7 @@ WINDOW w AS (
 ERROR:  quantifier bound must be between 1 and 2147483646
 LINE 6:     PATTERN (A{,0}?)
                         ^
--- Expected: ERROR: quantifier bound must be between 1 and 2147483646
--- {,2147483647}? (INT_MAX upper bound)
+-- ERROR: {,2147483647}? (upper bound in range exceeds limits)
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1404,8 +1373,7 @@ WINDOW w AS (
 ERROR:  quantifier bound must be between 1 and 2147483646
 LINE 6:     PATTERN (A{,2147483647}?)
                         ^
--- Expected: ERROR: quantifier bound must be between 1 and 2147483646
--- {-1,3}? (negative lower in range)
+-- ERROR: {-1,3}? (negative lower bound in range is not allowed)
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1417,8 +1385,7 @@ WINDOW w AS (
 ERROR:  syntax error at or near "-"
 LINE 6:     PATTERN (A{-1,3}?)
                        ^
--- Expected: ERROR: syntax error at or near "-"
--- {1,2147483647}? (INT_MAX upper in range)
+-- ERROR: {1,2147483647}? (upper bound in range exceeds limits)
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1430,8 +1397,7 @@ WINDOW w AS (
 ERROR:  quantifier bounds must be between 0 and 2147483646 with max >= 1
 LINE 6:     PATTERN (A{1,2147483647}?)
                        ^
--- Expected: ERROR: quantifier bounds must be between 0 and 2147483646 with max >= 1
--- {5,3}? (min > max)
+-- ERROR: {5,3}? (min > max is not allowed)
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1443,10 +1409,10 @@ WINDOW w AS (
 ERROR:  quantifier minimum bound must not exceed maximum
 LINE 6:     PATTERN (A{5,3}?)
                        ^
--- Expected: ERROR: quantifier minimum bound must not exceed maximum
 -- Token-separated reluctant quantifiers (space between quantifier and ?)
 -- These may be tokenized differently by the lexer
 -- * ? (token separated)
+-- Reluctant quantifier: prefer shortest match
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1462,8 +1428,8 @@ WINDOW w AS (
      0
 (3 rows)
 
--- Reluctant quantifier: prefer shortest match
 -- + ? (token separated)
+-- Reluctant quantifier: prefer shortest match
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1479,8 +1445,8 @@ WINDOW w AS (
      1
 (3 rows)
 
--- Reluctant quantifier: prefer shortest match
 -- {2,} ? (token separated)
+-- Reluctant quantifier: prefer shortest match
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1496,8 +1462,6 @@ WINDOW w AS (
      0
 (3 rows)
 
--- Reluctant quantifier: prefer shortest match
--- Invalid token combinations
 -- * + (invalid combination)
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
@@ -1510,7 +1474,6 @@ WINDOW w AS (
 ERROR:  syntax error at or near "+"
 LINE 6:     PATTERN (A* +)
                         ^
--- Expected: ERROR: syntax error at or near "+"
 -- + * (invalid combination)
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
@@ -1523,8 +1486,8 @@ WINDOW w AS (
 ERROR:  syntax error at or near "*"
 LINE 6:     PATTERN (A+ *)
                         ^
--- Expected: ERROR: syntax error at or near "*"
 -- ? ? (parsed as ?? reluctant quantifier)
+-- Reluctant quantifier: prefer shortest match
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1540,12 +1503,11 @@ WINDOW w AS (
      0
 (3 rows)
 
--- Reluctant quantifier: prefer shortest match
 DROP TABLE rpr_reluctant;
 -- Quantifier boundary conditions
 CREATE TABLE rpr_bounds (id INT);
 INSERT INTO rpr_bounds VALUES (1), (2);
--- min > max
+-- ERROR: quantifier lower bound must not exceed upper bound
 SELECT COUNT(*) OVER w
 FROM rpr_bounds
 WINDOW w AS (
@@ -1557,7 +1519,6 @@ WINDOW w AS (
 ERROR:  quantifier minimum bound must not exceed maximum
 LINE 6:     PATTERN (A{5,3})
                        ^
--- Expected: ERROR: quantifier minimum bound must not exceed maximum
 -- Large bounds
 SELECT COUNT(*) OVER w
 FROM rpr_bounds
@@ -1603,7 +1564,7 @@ WINDOW w AS (
      0
 (2 rows)
 
--- INT_MAX = 2147483647 (over limit)
+-- ERROR: quantifier bound exceeds limits
 SELECT COUNT(*) OVER w
 FROM rpr_bounds
 WINDOW w AS (
@@ -1615,9 +1576,8 @@ WINDOW w AS (
 ERROR:  quantifier bound must be between 1 and 2147483646
 LINE 6:     PATTERN (A{2147483647})
                        ^
--- Expected: ERROR: quantifier bound must be between 1 and 2147483646
 -- {n,} boundary errors
--- Negative lower bound in {n,}
+-- ERROR: negative lower bound in {n,} is not allowed
 SELECT COUNT(*) OVER w
 FROM rpr_bounds
 WINDOW w AS (
@@ -1629,8 +1589,7 @@ WINDOW w AS (
 ERROR:  syntax error at or near "-"
 LINE 6:     PATTERN (A{-1,})
                        ^
--- Expected: ERROR: syntax error at or near "-"
--- INT_MAX in {n,}
+-- ERROR: quantifier lower bound exceeds limits
 SELECT COUNT(*) OVER w
 FROM rpr_bounds
 WINDOW w AS (
@@ -1642,7 +1601,6 @@ WINDOW w AS (
 ERROR:  quantifier bound must be between 0 and 2147483646
 LINE 6:     PATTERN (A{2147483647,})
                        ^
--- Expected: ERROR: quantifier bound must be between 0 and 2147483646
 -- {,m} boundary errors
 -- Zero upper bound in {,m}
 SELECT COUNT(*) OVER w
@@ -1656,8 +1614,7 @@ WINDOW w AS (
 ERROR:  quantifier bound must be between 1 and 2147483646
 LINE 6:     PATTERN (A{,0})
                         ^
--- Expected: ERROR: quantifier bound must be between 1 and 2147483646
--- INT_MAX in {,m}
+-- ERROR: quantifier upper bound exceeds limits
 SELECT COUNT(*) OVER w
 FROM rpr_bounds
 WINDOW w AS (
@@ -1669,7 +1626,6 @@ WINDOW w AS (
 ERROR:  quantifier bound must be between 1 and 2147483646
 LINE 6:     PATTERN (A{,2147483647})
                         ^
--- Expected: ERROR: quantifier bound must be between 1 and 2147483646
 DROP TABLE rpr_bounds;
 -- ============================================================
 -- Navigation Functions Tests (PREV / NEXT / FIRST / LAST)
@@ -1756,7 +1712,6 @@ ORDER BY id;
 ERROR:  cannot use prev outside a DEFINE clause
 LINE 1: SELECT PREV(id), id, val, COUNT(*) OVER w as cnt
                ^
--- Expected: ERROR: cannot use prev outside a DEFINE clause
 -- NEXT function cannot be used other than in DEFINE
 SELECT NEXT(id), id, val, COUNT(*) OVER w as cnt
 FROM rpr_nav
@@ -1772,7 +1727,6 @@ ORDER BY id;
 ERROR:  cannot use next outside a DEFINE clause
 LINE 1: SELECT NEXT(id), id, val, COUNT(*) OVER w as cnt
                ^
--- Expected: ERROR: cannot use next outside a DEFINE clause
 -- FIRST function - reference match_start row
 SELECT id, val, COUNT(*) OVER w as cnt
 FROM rpr_nav
@@ -1841,13 +1795,11 @@ SELECT FIRST(id), id, val FROM rpr_nav;
 ERROR:  cannot use first outside a DEFINE clause
 LINE 1: SELECT FIRST(id), id, val FROM rpr_nav;
                ^
--- Expected: ERROR: cannot use first outside a DEFINE clause
 -- LAST function cannot be used other than in DEFINE
 SELECT LAST(id), id, val FROM rpr_nav;
 ERROR:  cannot use last outside a DEFINE clause
 LINE 1: SELECT LAST(id), id, val FROM rpr_nav;
                ^
--- Expected: ERROR: cannot use last outside a DEFINE clause
 DROP TABLE rpr_nav;
 -- ============================================================
 -- SKIP TO / INITIAL Tests
@@ -2033,12 +1985,13 @@ ERROR:  SEEK is not supported
 LINE 6:     SEEK
             ^
 HINT:  Use INITIAL instead.
--- Expected: ERROR: SEEK is not supported
--- HINT: Use INITIAL instead.
 DROP TABLE rpr_seek;
 -- ============================================================
 -- Serialization/Deserialization Tests
 -- ============================================================
+-- RPR-defining views and tables here are intentionally left in place (not
+-- dropped) so that pg_dump/pg_upgrade exercise the deparse-then-re-parse
+-- round-trip of the RPR window clause.
 -- View creation and deparsing
 CREATE TABLE rpr_serial (id INT, val INT);
 INSERT INTO rpr_serial VALUES
@@ -3317,7 +3270,6 @@ DROP TABLE rpr_glue;
 DROP TABLE IF EXISTS rpr_err;
 CREATE TABLE rpr_err (id INT, val INT);
 INSERT INTO rpr_err VALUES (1, 10), (2, 20);
--- Syntax errors
 -- Invalid quantifier syntax
 SELECT COUNT(*) OVER w
 FROM rpr_err
@@ -3346,7 +3298,7 @@ EXCEPTION
 END $$;
 NOTICE:  Unmatched parentheses: EXPECTED ERROR - syntax error at or near "AS"
 SET client_min_messages = WARNING;
--- Empty DEFINE
+-- ERROR: empty DEFINE not allowed
 SELECT COUNT(*) OVER w
 FROM rpr_err
 WINDOW w AS (
@@ -3359,7 +3311,7 @@ ERROR:  syntax error at or near ")"
 LINE 8: );
         ^
 -- Expected: Syntax error
--- Empty PATTERN
+-- ERROR: empty PATTERN not allowed
 SELECT COUNT(*) OVER w
 FROM rpr_err
 WINDOW w AS (
@@ -3372,7 +3324,7 @@ ERROR:  syntax error at or near ")"
 LINE 6:     PATTERN ()
                      ^
 -- Expected: Syntax error
--- DEFINE without PATTERN (PATTERN and DEFINE must be used together)
+-- ERROR: DEFINE without PATTERN (PATTERN and DEFINE must be used together)
 SELECT COUNT(*) OVER w
 FROM rpr_err
 WINDOW w AS (
@@ -3397,7 +3349,6 @@ WINDOW w AS (
 ERROR:  pattern variable qualified expression "a.val" is not supported in DEFINE clause
 LINE 7:     DEFINE A AS A.val > 0
                         ^
--- Expected: ERROR: pattern variable qualified expression "a.val" is not supported
 -- PATTERN-only variable qualified name: not supported even without DEFINE entry
 SELECT COUNT(*) OVER w
 FROM rpr_err
@@ -3410,7 +3361,6 @@ WINDOW w AS (
 ERROR:  pattern variable qualified expression "b.val" is not supported in DEFINE clause
 LINE 7:     DEFINE A AS B.val > 0
                         ^
--- Expected: ERROR: pattern variable qualified expression "b.val" is not supported
 -- DEFINE-only variable qualified name: still a pattern variable, not a range variable
 SELECT COUNT(*) OVER w
 FROM rpr_err
@@ -3423,7 +3373,6 @@ WINDOW w AS (
 ERROR:  DEFINE variable "b" is not used in PATTERN
 LINE 7:     DEFINE A AS val > 0, B AS B.val > 0
                                  ^
--- Expected: ERROR: pattern variable qualified expression "b.val" is not supported
 -- FROM-clause range variable qualified name: not allowed (prohibited by ISO/IEC 19075-5 6.5)
 SELECT COUNT(*) OVER w
 FROM rpr_err
@@ -3436,7 +3385,6 @@ WINDOW w AS (
 ERROR:  range variable qualified expression "rpr_err.val" is not allowed in DEFINE clause
 LINE 7:     DEFINE A AS rpr_err.val > 0
                         ^
--- Expected: ERROR: range variable qualified expression "rpr_err.val" is not allowed
 -- Unknown qualifier (neither pattern var nor range var): the DEFINE pre-check
 -- must fall through so that normal column resolution produces a sensible error.
 SELECT COUNT(*) OVER w
@@ -3450,7 +3398,6 @@ WINDOW w AS (
 ERROR:  missing FROM-clause entry for table "nosuch"
 LINE 7:     DEFINE A AS nosuch.val > 0
                         ^
--- Expected: ERROR: missing FROM-clause entry for table "nosuch"
 -- Unqualified composite field access in DEFINE works: no qualifier means no
 -- pattern/range-var navigation, so the pre-check skips and normal resolution
 -- handles "(items).amount" via A_Indirection on the current row.
@@ -3488,7 +3435,6 @@ WINDOW w AS (
 ERROR:  pattern variable qualified expression "a.items" is not supported in DEFINE clause
 LINE 7:     DEFINE A AS (A.items).amount > 10
                          ^
--- Expected: ERROR: pattern variable qualified expression "a.items" is not supported
 SELECT COUNT(*) OVER w
 FROM rpr_composite
 WINDOW w AS (
@@ -3500,11 +3446,9 @@ WINDOW w AS (
 ERROR:  range variable qualified expression "rpr_composite.items" is not allowed in DEFINE clause
 LINE 7:     DEFINE A AS (rpr_composite.items).amount > 10
                          ^
--- Expected: ERROR: range variable qualified expression "rpr_composite.items" is not allowed
 DROP TABLE rpr_composite;
 DROP TYPE rpr_item;
--- Semantic errors
--- Undefined column in DEFINE
+-- ERROR: undefined column in DEFINE
 SELECT COUNT(*) OVER w
 FROM rpr_err
 WINDOW w AS (
@@ -3516,8 +3460,7 @@ WINDOW w AS (
 ERROR:  column "nonexistent_column" does not exist
 LINE 7:     DEFINE A AS nonexistent_column > 0
                         ^
--- Expected: ERROR: column "nonexistent_column" does not exist
--- Type mismatch
+-- ERROR: type mismatch
 SELECT COUNT(*) OVER w
 FROM rpr_err
 WINDOW w AS (
@@ -3529,8 +3472,7 @@ WINDOW w AS (
 ERROR:  invalid input syntax for type integer: "string"
 LINE 7:     DEFINE A AS val > 'string'
                               ^
--- Expected: ERROR: invalid input syntax for type integer: "string"
--- Aggregate function in DEFINE (if not allowed)
+-- ERROR: aggregate function in DEFINE is not supported
 SELECT COUNT(*) OVER w
 FROM rpr_err
 WINDOW w AS (
@@ -3542,8 +3484,7 @@ WINDOW w AS (
 ERROR:  aggregate functions are not allowed in DEFINE
 LINE 7:     DEFINE A AS COUNT(*) > 0
                         ^
--- Expected: ERROR: aggregate functions are not allowed in DEFINE
--- Subquery in DEFINE (NOT SUPPORTED)
+-- Subquery in DEFINE is not supported
 SELECT COUNT(*) OVER w
 FROM rpr_err
 WINDOW w AS (
@@ -3555,8 +3496,6 @@ WINDOW w AS (
 ERROR:  cannot use subquery in DEFINE expression
 LINE 7:     DEFINE A AS val > (SELECT max(val) FROM rpr_err)
                               ^
--- Expected: ERROR: cannot use subquery in DEFINE expression
--- Edge cases
 -- Pattern variable not used (should work, extra vars ignored)
 SELECT id, val, COUNT(*) OVER w as cnt
 FROM rpr_err
@@ -5703,7 +5642,6 @@ WINDOW w AS (
 ERROR:  quantifier bounds must be between 0 and 2147483646 with max >= 1
 LINE 6:     PATTERN ((A{2000000000,2147483647}){2})
                         ^
--- Expected: ERROR at parse time before optimization
 -- Test: nested unbounded with large min causes overflow fallback
 EXPLAIN (COSTS OFF)
 SELECT COUNT(*) OVER w FROM rpr_fallback
@@ -5947,7 +5885,6 @@ ORDER BY category;
 ERROR:  syntax error at or near "GROUP"
 LINE 12: GROUP BY category
          ^
--- Expected: ERROR: syntax error at or near "GROUP"
 -- (GROUP BY after WINDOW clause is not valid SQL syntax)
 -- ============================================================
 -- Subquery and CTE Tests
@@ -6427,7 +6364,6 @@ INSERT INTO rpr_sort VALUES
     (1, 'A', 30), (2, 'B', 20), (3, 'A', 10),
     (4, 'B', 40), (5, 'A', 50), (6, 'B', 60);
 -- RPR with GROUP BY (aggregate in DEFINE -> ERROR before GROUP BY interaction)
--- Expected: ERROR: aggregate functions are not allowed in DEFINE
 SELECT category,
        COUNT(*) as group_cnt,
        MAX(val) as max_val,
@@ -6445,7 +6381,6 @@ ERROR:  aggregate functions are not allowed in DEFINE
 LINE 11:     DEFINE A AS COUNT(*) > 0
                          ^
 -- RPR with HAVING (same aggregate-in-DEFINE error)
--- Expected: ERROR: aggregate functions are not allowed in DEFINE
 SELECT category,
        COUNT(*) as group_cnt,
        COUNT(*) OVER w as window_cnt
@@ -6758,7 +6693,7 @@ WINDOW w AS (
 (2 rows)
 
 -- Expected: Success - exactly at RPR_VARID_MAX boundary
--- Test: 241 variables in PATTERN, 240 in DEFINE (exceeds limit with implicit TRUE)
+-- ERROR: 241 variables in PATTERN, 240 in DEFINE (exceeds limit with implicit TRUE)
 SELECT COUNT(*) OVER w FROM rpr_errors
 WINDOW w AS (
     ORDER BY id
@@ -6794,7 +6729,6 @@ ERROR:  too many pattern variables
 LINE 5: ...V230 V231 V232 V233 V234 V235 V236 V237 V238 V239 V240 V241)
                                                                   ^
 DETAIL:  Maximum is 240.
--- Expected: ERROR - too many pattern variables (Maximum is 240)
 -- Test: Pattern nesting at maximum depth (depth 253)
 -- Note: 253 nested GROUP{3,7}? quantifiers; reluctant quantifiers are not
 -- subject to quantifier multiplication, so the nesting (and depth 253) is
@@ -6826,12 +6760,10 @@ WINDOW w AS (
 );
 ERROR:  pattern nesting too deep
 DETAIL:  Pattern nesting depth 254 exceeds maximum 253.
--- Expected: ERROR - pattern nesting too deep
 DROP TABLE rpr_errors;
 -- ============================================================
--- Jacob's Patterns
+-- Basic Pattern Matching
 -- ============================================================
--- Basic pattern matching tests from jacob branch
 -- Test: A? (optional, greedy)
 SELECT id, val, count(*) OVER w AS c
 FROM rpr_plan
@@ -7194,3 +7126,4 @@ FROM (SELECT id, val,
 (2 rows)
 
 DROP TABLE rpr_plan;
+RESET client_min_messages;
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index cc79843aeb7..7dc13232d89 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -12,7 +12,7 @@
 --   Quantifiers Tests
 --   Navigation Functions Tests
 --   SKIP TO / INITIAL Tests
---   Serialization/Deserialization Tests (objects kept for pg_upgrade/pg_dump)
+--   Serialization/Deserialization Tests
 --   Glued Quantifier / Alternation Tests
 --   Error Cases Tests
 --   Window Deduplication Tests
@@ -34,7 +34,7 @@
 --   Error Limit Tests
 --
 -- Contributed Tests:
---   Jacob's Patterns
+--   Basic Pattern Matching
 --   Pathological Patterns
 -- ============================================================
 
@@ -54,6 +54,7 @@ CREATE TABLE rpr_keywords (
     past INT,        -- PAST keyword
     pattern INT,     -- PATTERN keyword
     seek INT,        -- SEEK keyword
+-- ERROR: SEEK is not supported
     skip INT         -- SKIP keyword (pre-existing)
 );
 
@@ -69,7 +70,6 @@ DROP TABLE rpr_keywords;
 -- DEFINE Clause Tests
 -- ============================================================
 
-
 -- Simple column references
 CREATE TABLE stock_price (
     dt DATE,
@@ -182,7 +182,7 @@ DROP TABLE rpr_auto;
 CREATE TABLE rpr_dup (id INT);
 INSERT INTO rpr_dup VALUES (1), (2);
 
--- Duplicate DEFINE entries
+-- Duplicate DEFINE variable name is not allowed
 SELECT COUNT(*) OVER w
 FROM rpr_dup
 WINDOW w AS (
@@ -191,7 +191,6 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS id > 0, A AS id < 10
 );
--- Expected: ERROR: row pattern definition variable name "a" appears more than once in DEFINE clause
 
 DROP TABLE rpr_dup;
 
@@ -199,7 +198,7 @@ DROP TABLE rpr_dup;
 CREATE TABLE rpr_bool (id INT, flag BOOLEAN);
 INSERT INTO rpr_bool VALUES (1, true), (2, false);
 
--- Non-boolean expression
+-- DEFINE clause must be a boolean expression
 SELECT COUNT(*) OVER w
 FROM rpr_bool
 WINDOW w AS (
@@ -208,7 +207,6 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS id
 );
--- Expected: ERROR: argument of DEFINE must be type boolean
 
 -- Boolean column reference
 SELECT id, flag, COUNT(*) OVER w as cnt
@@ -301,7 +299,6 @@ DROP TABLE rpr_unused;
 -- FRAME Options Tests
 -- ============================================================
 
-
 CREATE TABLE rpr_frame (id INT, val INT);
 INSERT INTO rpr_frame VALUES
     (1, 10), (2, 10), (3, 10),  -- Same val: 10
@@ -327,9 +324,7 @@ WINDOW w AS (
 )
 ORDER BY id;
 
--- Invalid frame start positions
-
--- Not starting at CURRENT ROW
+-- ERROR: frame must start at current row when row pattern recognition is used
 SELECT COUNT(*) OVER w
 FROM rpr_frame
 WINDOW w AS (
@@ -338,7 +333,6 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: FRAME must start at current row when row pattern recognition is used
 
 -- EXCLUDE options
 
@@ -352,7 +346,6 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: cannot use EXCLUDE options with row pattern recognition
 
 -- EXCLUDE GROUP not permitted
 SELECT COUNT(*) OVER w
@@ -364,7 +357,6 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: cannot use EXCLUDE options with row pattern recognition
 
 -- EXCLUDE TIES not permitted
 SELECT COUNT(*) OVER w
@@ -376,9 +368,8 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: cannot use EXCLUDE options with row pattern recognition
 
--- RANGE frame not starting at CURRENT ROW
+-- range frame is not allowed with RPR
 SELECT COUNT(*) OVER w
 FROM rpr_frame
 WINDOW w AS (
@@ -387,9 +378,8 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: cannot use FRAME option RANGE with row pattern recognition
 
--- GROUPS frame not starting at CURRENT ROW
+-- GROUPS frame is not allowed with RPR
 SELECT COUNT(*) OVER w
 FROM rpr_frame
 WINDOW w AS (
@@ -398,9 +388,8 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: cannot use FRAME option GROUPS with row pattern recognition
 
--- Starting with N PRECEDING
+-- ERROR: frame must start at current row when row pattern recognition is used
 SELECT COUNT(*) OVER w
 FROM rpr_frame
 WINDOW w AS (
@@ -409,9 +398,8 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: FRAME must start at current row when row pattern recognition is used
 
--- Starting with N FOLLOWING
+-- ERROR: frame must start at current row with RPR
 SELECT COUNT(*) OVER w
 FROM rpr_frame
 WINDOW w AS (
@@ -420,11 +408,8 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: FRAME must start at current row when row pattern recognition is used
 
--- Frame end bound edge cases
-
--- End before start: CURRENT ROW AND 1 PRECEDING
+-- ERROR: end before start: CURRENT ROW AND 1 PRECEDING
 SELECT COUNT(*) OVER w
 FROM rpr_frame
 WINDOW w AS (
@@ -433,9 +418,8 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: frame starting from current row cannot have preceding rows
 
--- End before start: CURRENT ROW AND UNBOUNDED PRECEDING
+-- ERROR: end before start: CURRENT ROW AND UNBOUNDED PRECEDING
 SELECT COUNT(*) OVER w
 FROM rpr_frame
 WINDOW w AS (
@@ -444,7 +428,6 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: frame end cannot be UNBOUNDED PRECEDING
 
 -- Single row frame: CURRENT ROW AND CURRENT ROW is rejected (the standard
 -- allows only UNBOUNDED FOLLOWING or a positive offset FOLLOWING).
@@ -458,7 +441,6 @@ WINDOW w AS (
     DEFINE A AS val > 0
 )
 ORDER BY id;
--- Expected: ERROR: cannot use CURRENT ROW as frame end with row pattern recognition
 
 -- Zero offset: CURRENT ROW AND 0 FOLLOWING denotes the same one-row frame
 -- and is likewise rejected (caught at execution time).
@@ -472,7 +454,6 @@ WINDOW w AS (
     DEFINE A AS val > 0
 )
 ORDER BY id;
--- Expected: ERROR: frame ending offset must be positive with row pattern recognition
 
 -- A non-constant frame end offset is allowed; a zero value is still rejected,
 -- this time at execution time (a literal cannot exercise that path).
@@ -489,7 +470,6 @@ WINDOW w AS (
 ORDER BY id;
 EXECUTE rpr_end_offset(2);
 EXECUTE rpr_end_offset(0);
--- Expected: ERROR: frame ending offset must be positive with row pattern recognition
 DEALLOCATE rpr_end_offset;
 
 -- Large offset: CURRENT ROW AND 1000 FOLLOWING
@@ -532,7 +512,7 @@ WINDOW w AS (
 )
 ORDER BY id;
 
--- RANGE frame with RPR (not permitted)
+-- range frame is not allowed with RPR
 SELECT id, val, COUNT(*) OVER w as cnt
 FROM rpr_frame
 WINDOW w AS (
@@ -543,7 +523,6 @@ WINDOW w AS (
     DEFINE A AS val >= 0, B AS val >= 0
 )
 ORDER BY id;
--- Expected: ERROR: cannot use FRAME option RANGE with row pattern recognition
 
 -- GROUPS frame with RPR (not permitted)
 SELECT id, val, COUNT(*) OVER w as cnt
@@ -556,7 +535,6 @@ WINDOW w AS (
     DEFINE A AS val >= 0, B AS val >= 0
 )
 ORDER BY id;
--- Expected: ERROR: cannot use FRAME option GROUPS with row pattern recognition
 
 DROP TABLE rpr_frame;
 
@@ -596,7 +574,6 @@ WINDOW w AS (
     DEFINE A AS val >= 10, B AS val >= 20
 )
 ORDER BY id;
--- Expected: ERROR: cannot use FRAME option RANGE with row pattern recognition
 
 DROP TABLE rpr_partition;
 
@@ -604,7 +581,6 @@ DROP TABLE rpr_partition;
 -- PATTERN Syntax Tests
 -- ============================================================
 
-
 CREATE TABLE rpr_pattern (id INT, val INT);
 INSERT INTO rpr_pattern VALUES
     (1, 5), (2, 10), (3, 15), (4, 20), (5, 25),
@@ -700,7 +676,6 @@ DROP TABLE rpr_pattern;
 -- Quantifiers Tests
 -- ============================================================
 
-
 CREATE TABLE rpr_quant (id INT, val INT);
 INSERT INTO rpr_quant VALUES
     (1, 10), (2, 20), (3, 30), (4, 40), (5, 50),
@@ -753,7 +728,6 @@ WINDOW w AS (
     DEFINE A AS val > 1000, B AS val > 0
 )
 ORDER BY id;
--- Expected: ERROR: quantifier bound must be between 1 and 2147483646
 
 -- {0,0} is not allowed (max must be >= 1)
 SELECT id, val, COUNT(*) OVER w as cnt
@@ -765,7 +739,6 @@ WINDOW w AS (
     DEFINE A AS val > 1000, B AS val > 0
 )
 ORDER BY id;
--- Expected: ERROR: quantifier bounds must be between 0 and 2147483646 with max >= 1
 
 -- {0,1} (equivalent to ?)
 SELECT id, val, COUNT(*) OVER w as cnt
@@ -837,6 +810,7 @@ CREATE TABLE rpr_reluctant (id INT, val INT);
 INSERT INTO rpr_reluctant VALUES (1, 10), (2, 20), (3, 30);
 
 -- *? (zero or more, reluctant)
+-- Reluctant quantifier: prefer shortest match
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -845,9 +819,9 @@ WINDOW w AS (
     PATTERN (A*?)
     DEFINE A AS val > 0
 );
--- Reluctant quantifier: prefer shortest match
 
 -- +? (one or more, reluctant)
+-- Reluctant quantifier: prefer shortest match
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -856,9 +830,9 @@ WINDOW w AS (
     PATTERN (A+?)
     DEFINE A AS val > 0
 );
--- Reluctant quantifier: prefer shortest match
 
 -- ?? (zero or one, reluctant)
+-- Reluctant quantifier: prefer shortest match
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -867,9 +841,9 @@ WINDOW w AS (
     PATTERN (A??)
     DEFINE A AS val > 0
 );
--- Reluctant quantifier: prefer shortest match
 
 -- {n,}? (n or more, reluctant)
+-- Reluctant quantifier: prefer shortest match
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -878,9 +852,9 @@ WINDOW w AS (
     PATTERN (A{2,}?)
     DEFINE A AS val > 0
 );
--- Reluctant quantifier: prefer shortest match
 
 -- {n,m}? (n to m, reluctant)
+-- Reluctant quantifier: prefer shortest match
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -889,9 +863,9 @@ WINDOW w AS (
     PATTERN (A{1,3}?)
     DEFINE A AS val > 0
 );
--- Reluctant quantifier: prefer shortest match
 
 -- {n}? (exactly n, reluctant)
+-- Reluctant quantifier: prefer shortest match
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -900,9 +874,9 @@ WINDOW w AS (
     PATTERN (A{2}?)
     DEFINE A AS val > 0
 );
--- Reluctant quantifier: prefer shortest match
 
 -- {,m}? (up to m, reluctant) - COMPLETELY UNTESTED RULE!
+-- Reluctant quantifier: prefer shortest match
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -911,9 +885,6 @@ WINDOW w AS (
     PATTERN (A{,3}?)
     DEFINE A AS val > 0
 );
--- Reluctant quantifier: prefer shortest match
-
--- Invalid reluctant patterns (wrong token after quantifier)
 
 -- {2}+ (should be {2}? not {2}+)
 SELECT COUNT(*) OVER w
@@ -924,7 +895,6 @@ WINDOW w AS (
     PATTERN (A{2}+)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: syntax error at or near "+"
 
 -- {2,}* (should be {2,}? not {2,}*)
 SELECT COUNT(*) OVER w
@@ -935,7 +905,6 @@ WINDOW w AS (
     PATTERN (A{2,}*)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: syntax error at or near "*"
 
 -- {,3}* (should be {,3}? not {,3}*)
 SELECT COUNT(*) OVER w
@@ -946,7 +915,6 @@ WINDOW w AS (
     PATTERN (A{,3}*)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: syntax error at or near "*"
 
 -- {1,3}+ (should be {1,3}? not {1,3}+)
 SELECT COUNT(*) OVER w
@@ -957,11 +925,10 @@ WINDOW w AS (
     PATTERN (A{1,3}+)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: syntax error at or near "+"
 
 -- Boundary errors in reluctant quantifiers
 
--- {-1}? (negative bound)
+-- negative bound is not allowed
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -970,9 +937,8 @@ WINDOW w AS (
     PATTERN (A{-1}?)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: syntax error at or near "-"
 
--- {2147483647}? (INT_MAX)
+-- ERROR: quantifier bound exceeds limits
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -981,9 +947,8 @@ WINDOW w AS (
     PATTERN (A{2147483647}?)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: quantifier bound must be between 1 and 2147483646
 
--- {-1,}? (negative lower bound)
+-- negative lower bound is not allowed
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -992,9 +957,8 @@ WINDOW w AS (
     PATTERN (A{-1,}?)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: syntax error at or near "-"
 
--- {2147483647,}? (INT_MAX lower bound)
+-- ERROR: quantifier lower bound exceeds limits
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1003,9 +967,8 @@ WINDOW w AS (
     PATTERN (A{2147483647,}?)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: quantifier bound must be between 0 and 2147483646
 
--- {,0}? (zero upper bound)
+-- zero upper bound is not allowed
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1014,9 +977,8 @@ WINDOW w AS (
     PATTERN (A{,0}?)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: quantifier bound must be between 1 and 2147483646
 
--- {,2147483647}? (INT_MAX upper bound)
+-- ERROR: {,2147483647}? (upper bound in range exceeds limits)
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1025,9 +987,8 @@ WINDOW w AS (
     PATTERN (A{,2147483647}?)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: quantifier bound must be between 1 and 2147483646
 
--- {-1,3}? (negative lower in range)
+-- ERROR: {-1,3}? (negative lower bound in range is not allowed)
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1036,9 +997,8 @@ WINDOW w AS (
     PATTERN (A{-1,3}?)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: syntax error at or near "-"
 
--- {1,2147483647}? (INT_MAX upper in range)
+-- ERROR: {1,2147483647}? (upper bound in range exceeds limits)
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1047,9 +1007,8 @@ WINDOW w AS (
     PATTERN (A{1,2147483647}?)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: quantifier bounds must be between 0 and 2147483646 with max >= 1
 
--- {5,3}? (min > max)
+-- ERROR: {5,3}? (min > max is not allowed)
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1058,12 +1017,12 @@ WINDOW w AS (
     PATTERN (A{5,3}?)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: quantifier minimum bound must not exceed maximum
 
 -- Token-separated reluctant quantifiers (space between quantifier and ?)
 -- These may be tokenized differently by the lexer
 
 -- * ? (token separated)
+-- Reluctant quantifier: prefer shortest match
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1072,9 +1031,9 @@ WINDOW w AS (
     PATTERN (A* ?)
     DEFINE A AS val > 0
 );
--- Reluctant quantifier: prefer shortest match
 
 -- + ? (token separated)
+-- Reluctant quantifier: prefer shortest match
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1083,9 +1042,9 @@ WINDOW w AS (
     PATTERN (A+ ?)
     DEFINE A AS val > 0
 );
--- Reluctant quantifier: prefer shortest match
 
 -- {2,} ? (token separated)
+-- Reluctant quantifier: prefer shortest match
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1094,9 +1053,6 @@ WINDOW w AS (
     PATTERN (A{2,} ?)
     DEFINE A AS val > 0
 );
--- Reluctant quantifier: prefer shortest match
-
--- Invalid token combinations
 
 -- * + (invalid combination)
 SELECT COUNT(*) OVER w
@@ -1107,7 +1063,6 @@ WINDOW w AS (
     PATTERN (A* +)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: syntax error at or near "+"
 
 -- + * (invalid combination)
 SELECT COUNT(*) OVER w
@@ -1118,9 +1073,9 @@ WINDOW w AS (
     PATTERN (A+ *)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: syntax error at or near "*"
 
 -- ? ? (parsed as ?? reluctant quantifier)
+-- Reluctant quantifier: prefer shortest match
 SELECT COUNT(*) OVER w
 FROM rpr_reluctant
 WINDOW w AS (
@@ -1129,7 +1084,6 @@ WINDOW w AS (
     PATTERN (A? ?)
     DEFINE A AS val > 0
 );
--- Reluctant quantifier: prefer shortest match
 
 DROP TABLE rpr_reluctant;
 
@@ -1138,7 +1092,7 @@ DROP TABLE rpr_reluctant;
 CREATE TABLE rpr_bounds (id INT);
 INSERT INTO rpr_bounds VALUES (1), (2);
 
--- min > max
+-- ERROR: quantifier lower bound must not exceed upper bound
 SELECT COUNT(*) OVER w
 FROM rpr_bounds
 WINDOW w AS (
@@ -1147,7 +1101,6 @@ WINDOW w AS (
     PATTERN (A{5,3})
     DEFINE A AS id > 0
 );
--- Expected: ERROR: quantifier minimum bound must not exceed maximum
 
 -- Large bounds
 SELECT COUNT(*) OVER w
@@ -1179,7 +1132,7 @@ WINDOW w AS (
     DEFINE A AS id > 0
 );
 
--- INT_MAX = 2147483647 (over limit)
+-- ERROR: quantifier bound exceeds limits
 SELECT COUNT(*) OVER w
 FROM rpr_bounds
 WINDOW w AS (
@@ -1188,11 +1141,10 @@ WINDOW w AS (
     PATTERN (A{2147483647})
     DEFINE A AS id > 0
 );
--- Expected: ERROR: quantifier bound must be between 1 and 2147483646
 
 -- {n,} boundary errors
 
--- Negative lower bound in {n,}
+-- ERROR: negative lower bound in {n,} is not allowed
 SELECT COUNT(*) OVER w
 FROM rpr_bounds
 WINDOW w AS (
@@ -1201,9 +1153,8 @@ WINDOW w AS (
     PATTERN (A{-1,})
     DEFINE A AS id > 0
 );
--- Expected: ERROR: syntax error at or near "-"
 
--- INT_MAX in {n,}
+-- ERROR: quantifier lower bound exceeds limits
 SELECT COUNT(*) OVER w
 FROM rpr_bounds
 WINDOW w AS (
@@ -1212,7 +1163,6 @@ WINDOW w AS (
     PATTERN (A{2147483647,})
     DEFINE A AS id > 0
 );
--- Expected: ERROR: quantifier bound must be between 0 and 2147483646
 
 -- {,m} boundary errors
 
@@ -1225,9 +1175,8 @@ WINDOW w AS (
     PATTERN (A{,0})
     DEFINE A AS id > 0
 );
--- Expected: ERROR: quantifier bound must be between 1 and 2147483646
 
--- INT_MAX in {,m}
+-- ERROR: quantifier upper bound exceeds limits
 SELECT COUNT(*) OVER w
 FROM rpr_bounds
 WINDOW w AS (
@@ -1236,7 +1185,6 @@ WINDOW w AS (
     PATTERN (A{,2147483647})
     DEFINE A AS id > 0
 );
--- Expected: ERROR: quantifier bound must be between 1 and 2147483646
 
 DROP TABLE rpr_bounds;
 
@@ -1244,7 +1192,6 @@ DROP TABLE rpr_bounds;
 -- Navigation Functions Tests (PREV / NEXT / FIRST / LAST)
 -- ============================================================
 
-
 CREATE TABLE rpr_nav (id INT, val INT);
 INSERT INTO rpr_nav VALUES
     (1, 10), (2, 20), (3, 15), (4, 25), (5, 30);
@@ -1301,7 +1248,6 @@ WINDOW w AS (
         B AS val > PREV(val)
 )
 ORDER BY id;
--- Expected: ERROR: cannot use prev outside a DEFINE clause
 
 -- NEXT function cannot be used other than in DEFINE
 SELECT NEXT(id), id, val, COUNT(*) OVER w as cnt
@@ -1315,7 +1261,6 @@ WINDOW w AS (
         B AS val > PREV(val)
 )
 ORDER BY id;
--- Expected: ERROR: cannot use next outside a DEFINE clause
 
 -- FIRST function - reference match_start row
 SELECT id, val, COUNT(*) OVER w as cnt
@@ -1358,11 +1303,9 @@ ORDER BY id;
 
 -- FIRST function cannot be used other than in DEFINE
 SELECT FIRST(id), id, val FROM rpr_nav;
--- Expected: ERROR: cannot use first outside a DEFINE clause
 
 -- LAST function cannot be used other than in DEFINE
 SELECT LAST(id), id, val FROM rpr_nav;
--- Expected: ERROR: cannot use last outside a DEFINE clause
 
 DROP TABLE rpr_nav;
 
@@ -1370,7 +1313,6 @@ DROP TABLE rpr_nav;
 -- SKIP TO / INITIAL Tests
 -- ============================================================
 
-
 CREATE TABLE rpr_skip (id INT, val INT);
 INSERT INTO rpr_skip VALUES
     (1, 1), (2, 2), (3, 3), (4, 4), (5, 5),
@@ -1492,15 +1434,15 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > 0
 );
--- Expected: ERROR: SEEK is not supported
--- HINT: Use INITIAL instead.
 
 DROP TABLE rpr_seek;
 
 -- ============================================================
 -- Serialization/Deserialization Tests
 -- ============================================================
-
+-- RPR-defining views and tables here are intentionally left in place (not
+-- dropped) so that pg_dump/pg_upgrade exercise the deparse-then-re-parse
+-- round-trip of the RPR window clause.
 
 -- View creation and deparsing
 
@@ -2190,13 +2132,10 @@ DROP TABLE rpr_glue;
 -- Error Cases Tests
 -- ============================================================
 
-
 DROP TABLE IF EXISTS rpr_err;
 CREATE TABLE rpr_err (id INT, val INT);
 INSERT INTO rpr_err VALUES (1, 10), (2, 20);
 
--- Syntax errors
-
 -- Invalid quantifier syntax
 SELECT COUNT(*) OVER w
 FROM rpr_err
@@ -2222,7 +2161,7 @@ EXCEPTION
 END $$;
 SET client_min_messages = WARNING;
 
--- Empty DEFINE
+-- ERROR: empty DEFINE not allowed
 SELECT COUNT(*) OVER w
 FROM rpr_err
 WINDOW w AS (
@@ -2233,7 +2172,7 @@ WINDOW w AS (
 );
 -- Expected: Syntax error
 
--- Empty PATTERN
+-- ERROR: empty PATTERN not allowed
 SELECT COUNT(*) OVER w
 FROM rpr_err
 WINDOW w AS (
@@ -2244,7 +2183,7 @@ WINDOW w AS (
 );
 -- Expected: Syntax error
 
--- DEFINE without PATTERN (PATTERN and DEFINE must be used together)
+-- ERROR: DEFINE without PATTERN (PATTERN and DEFINE must be used together)
 SELECT COUNT(*) OVER w
 FROM rpr_err
 WINDOW w AS (
@@ -2265,7 +2204,6 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS A.val > 0
 );
--- Expected: ERROR: pattern variable qualified expression "a.val" is not supported
 
 -- PATTERN-only variable qualified name: not supported even without DEFINE entry
 SELECT COUNT(*) OVER w
@@ -2276,7 +2214,6 @@ WINDOW w AS (
     PATTERN (A+ B+)
     DEFINE A AS B.val > 0
 );
--- Expected: ERROR: pattern variable qualified expression "b.val" is not supported
 
 -- DEFINE-only variable qualified name: still a pattern variable, not a range variable
 SELECT COUNT(*) OVER w
@@ -2287,7 +2224,6 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > 0, B AS B.val > 0
 );
--- Expected: ERROR: pattern variable qualified expression "b.val" is not supported
 
 -- FROM-clause range variable qualified name: not allowed (prohibited by ISO/IEC 19075-5 6.5)
 SELECT COUNT(*) OVER w
@@ -2298,7 +2234,6 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS rpr_err.val > 0
 );
--- Expected: ERROR: range variable qualified expression "rpr_err.val" is not allowed
 
 -- Unknown qualifier (neither pattern var nor range var): the DEFINE pre-check
 -- must fall through so that normal column resolution produces a sensible error.
@@ -2310,7 +2245,6 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS nosuch.val > 0
 );
--- Expected: ERROR: missing FROM-clause entry for table "nosuch"
 
 -- Unqualified composite field access in DEFINE works: no qualifier means no
 -- pattern/range-var navigation, so the pre-check skips and normal resolution
@@ -2340,7 +2274,6 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS (A.items).amount > 10
 );
--- Expected: ERROR: pattern variable qualified expression "a.items" is not supported
 SELECT COUNT(*) OVER w
 FROM rpr_composite
 WINDOW w AS (
@@ -2349,13 +2282,10 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS (rpr_composite.items).amount > 10
 );
--- Expected: ERROR: range variable qualified expression "rpr_composite.items" is not allowed
 DROP TABLE rpr_composite;
 DROP TYPE rpr_item;
 
--- Semantic errors
-
--- Undefined column in DEFINE
+-- ERROR: undefined column in DEFINE
 SELECT COUNT(*) OVER w
 FROM rpr_err
 WINDOW w AS (
@@ -2364,9 +2294,8 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS nonexistent_column > 0
 );
--- Expected: ERROR: column "nonexistent_column" does not exist
 
--- Type mismatch
+-- ERROR: type mismatch
 SELECT COUNT(*) OVER w
 FROM rpr_err
 WINDOW w AS (
@@ -2375,9 +2304,8 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > 'string'
 );
--- Expected: ERROR: invalid input syntax for type integer: "string"
 
--- Aggregate function in DEFINE (if not allowed)
+-- ERROR: aggregate function in DEFINE is not supported
 SELECT COUNT(*) OVER w
 FROM rpr_err
 WINDOW w AS (
@@ -2386,9 +2314,8 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS COUNT(*) > 0
 );
--- Expected: ERROR: aggregate functions are not allowed in DEFINE
 
--- Subquery in DEFINE (NOT SUPPORTED)
+-- Subquery in DEFINE is not supported
 SELECT COUNT(*) OVER w
 FROM rpr_err
 WINDOW w AS (
@@ -2397,9 +2324,6 @@ WINDOW w AS (
     PATTERN (A+)
     DEFINE A AS val > (SELECT max(val) FROM rpr_err)
 );
--- Expected: ERROR: cannot use subquery in DEFINE expression
-
--- Edge cases
 
 -- Pattern variable not used (should work, extra vars ignored)
 SELECT id, val, COUNT(*) OVER w as cnt
@@ -3414,7 +3338,6 @@ WINDOW w AS (
     PATTERN ((A{2000000000,2147483647}){2})
     DEFINE A AS val > 0
 );
--- Expected: ERROR at parse time before optimization
 
 -- Test: nested unbounded with large min causes overflow fallback
 EXPLAIN (COSTS OFF)
@@ -3563,7 +3486,6 @@ WINDOW w AS (
 )
 GROUP BY category
 ORDER BY category;
--- Expected: ERROR: syntax error at or near "GROUP"
 -- (GROUP BY after WINDOW clause is not valid SQL syntax)
 
 -- ============================================================
@@ -3937,7 +3859,6 @@ INSERT INTO rpr_sort VALUES
     (4, 'B', 40), (5, 'A', 50), (6, 'B', 60);
 
 -- RPR with GROUP BY (aggregate in DEFINE -> ERROR before GROUP BY interaction)
--- Expected: ERROR: aggregate functions are not allowed in DEFINE
 
 SELECT category,
        COUNT(*) as group_cnt,
@@ -3954,7 +3875,6 @@ WINDOW w AS (
 ORDER BY category;
 
 -- RPR with HAVING (same aggregate-in-DEFINE error)
--- Expected: ERROR: aggregate functions are not allowed in DEFINE
 
 SELECT category,
        COUNT(*) as group_cnt,
@@ -4189,7 +4109,7 @@ WINDOW w AS (
 );
 -- Expected: Success - exactly at RPR_VARID_MAX boundary
 
--- Test: 241 variables in PATTERN, 240 in DEFINE (exceeds limit with implicit TRUE)
+-- ERROR: 241 variables in PATTERN, 240 in DEFINE (exceeds limit with implicit TRUE)
 SELECT COUNT(*) OVER w FROM rpr_errors
 WINDOW w AS (
     ORDER BY id
@@ -4221,7 +4141,6 @@ WINDOW w AS (
     V221 AS val > 0, V222 AS val > 0, V223 AS val > 0, V224 AS val > 0, V225 AS val > 0, V226 AS val > 0, V227 AS val > 0, V228 AS val > 0, V229 AS val > 0, V230 AS val > 0,
     V231 AS val > 0, V232 AS val > 0, V233 AS val > 0, V234 AS val > 0, V235 AS val > 0, V236 AS val > 0, V237 AS val > 0, V238 AS val > 0, V239 AS val > 0, V240 AS val > 0
 );
--- Expected: ERROR - too many pattern variables (Maximum is 240)
 
 -- Test: Pattern nesting at maximum depth (depth 253)
 -- Note: 253 nested GROUP{3,7}? quantifiers; reluctant quantifiers are not
@@ -4247,14 +4166,12 @@ WINDOW w AS (
     PATTERN (((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((A{3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?)
     DEFINE A AS val > 0
 );
--- Expected: ERROR - pattern nesting too deep
 
 DROP TABLE rpr_errors;
 
 -- ============================================================
--- Jacob's Patterns
+-- Basic Pattern Matching
 -- ============================================================
--- Basic pattern matching tests from jacob branch
 
 -- Test: A? (optional, greedy)
 SELECT id, val, count(*) OVER w AS c
@@ -4453,3 +4370,4 @@ FROM (SELECT id, val,
 ) s;
 
 DROP TABLE rpr_plan;
+RESET client_min_messages;
-- 
2.50.1 (Apple Git-155)



  [text/plain] nocfbot-0068-Use-foreach_node-and-friends-in-row-pattern-recog.txt (23.5K, 70-nocfbot-0068-Use-foreach_node-and-friends-in-row-pattern-recog.txt)
  download | inline diff:
From b010580c9b62244ba323520bb7202aa348c18195 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Mon, 8 Jun 2026 21:27:44 +0900
Subject: [PATCH 68/68] Use foreach_node and friends in row pattern recognition
 code

Replace the hand-written foreach()/lfirst() loops in the row pattern
recognition code with foreach_node(), foreach_ptr(), foreach_current_index()
and forboth(), dropping the now-unneeded ListCell declarations, casts and
manual index counters.  No behavior change.
---
 src/backend/executor/execRPR.c            | 10 +--
 src/backend/executor/nodeWindowAgg.c      | 12 +--
 src/backend/optimizer/path/allpaths.c     |  9 +--
 src/backend/optimizer/path/costsize.c     |  5 +-
 src/backend/optimizer/plan/createplan.c   |  5 +-
 src/backend/optimizer/plan/rpr.c          | 90 +++++++----------------
 src/backend/optimizer/prep/prepjointree.c |  4 +-
 src/backend/parser/parse_cte.c            |  3 +-
 src/backend/parser/parse_expr.c           |  5 +-
 src/backend/parser/parse_rpr.c            | 38 +++-------
 src/backend/utils/adt/ruleutils.c         | 18 ++---
 11 files changed, 62 insertions(+), 137 deletions(-)

diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 1c89875a306..5ae54495968 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -1595,8 +1595,6 @@ nfa_reevaluate_dependent_vars(WindowAggState *winstate, RPRNFAContext *ctx,
 	ExprContext *econtext = winstate->ss.ps.ps_ExprContext;
 	int64		saved_match_start = winstate->nav_match_start;
 	int64		saved_pos = winstate->currentpos;
-	int			varIdx = 0;
-	ListCell   *lc;
 
 	/* Temporarily set nav_match_start and currentpos for FIRST/LAST */
 	winstate->nav_match_start = ctx->matchStartRow;
@@ -1605,11 +1603,12 @@ nfa_reevaluate_dependent_vars(WindowAggState *winstate, RPRNFAContext *ctx,
 	/* Invalidate nav_slot cache since match_start changed */
 	winstate->nav_slot_pos = -1;
 
-	foreach(lc, winstate->defineClauseList)
+	foreach_ptr(ExprState, exprState, winstate->defineClauseList)
 	{
+		int			varIdx = foreach_current_index(exprState);
+
 		if (bms_is_member(varIdx, winstate->defineMatchStartDependent))
 		{
-			ExprState  *exprState = (ExprState *) lfirst(lc);
 			Datum		result;
 			bool		isnull;
 
@@ -1618,8 +1617,7 @@ nfa_reevaluate_dependent_vars(WindowAggState *winstate, RPRNFAContext *ctx,
 			winstate->nfaVarMatched[varIdx] = (!isnull && DatumGetBool(result));
 		}
 
-		varIdx++;
-		if (varIdx >= list_length(winstate->defineVariableList))
+		if (varIdx + 1 >= list_length(winstate->defineVariableList))
 			break;
 	}
 
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 13661181986..15579ae5ea4 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -3084,9 +3084,8 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
 		 * by EEOP_RPR_NAV_SET/RESTORE opcodes emitted during ExecInitExpr, so
 		 * no varno rewriting is needed here.
 		 */
-		foreach(l, node->defineClause)
+		foreach_node(TargetEntry, te, node->defineClause)
 		{
-			TargetEntry *te = lfirst(l);
 			char	   *name = te->resname;
 			Expr	   *expr = te->expr;
 			ExprState  *exps;
@@ -4173,7 +4172,6 @@ eval_define_offsets(WindowAggState *winstate, List *defineClause)
 {
 	EvalDefineOffsetsContext ctx;
 	NavTraversal trav;
-	ListCell   *lc;
 	bool		needsMax = (winstate->navMaxOffsetKind == RPR_NAV_OFFSET_NEEDS_EVAL);
 	bool		needsFirst = (winstate->hasFirstNav &&
 							  winstate->navFirstOffsetKind == RPR_NAV_OFFSET_NEEDS_EVAL);
@@ -4189,10 +4187,8 @@ eval_define_offsets(WindowAggState *winstate, List *defineClause)
 	trav.visit = visit_nav_exec;
 	trav.data = &ctx;
 
-	foreach(lc, defineClause)
+	foreach_node(TargetEntry, te, defineClause)
 	{
-		TargetEntry *te = (TargetEntry *) lfirst(lc);
-
 		nav_traversal_walker((Node *) te->expr, &trav);
 	}
 
@@ -4586,7 +4582,6 @@ nfa_evaluate_row(WindowObject winobj, int64 pos, bool *varMatched)
 	WindowAggState *winstate = winobj->winstate;
 	ExprContext *econtext = winstate->ss.ps.ps_ExprContext;
 	int			numDefineVars = list_length(winstate->defineVariableList);
-	ListCell   *lc;
 	int			varIdx = 0;
 	TupleTableSlot *slot;
 	int64		saved_pos;
@@ -4609,9 +4604,8 @@ nfa_evaluate_row(WindowObject winobj, int64 pos, bool *varMatched)
 	/* Invalidate nav_slot cache so PREV/NEXT re-fetch for new row */
 	winstate->nav_slot_pos = -1;
 
-	foreach(lc, winstate->defineClauseList)
+	foreach_ptr(ExprState, exprState, winstate->defineClauseList)
 	{
-		ExprState  *exprState = (ExprState *) lfirst(lc);
 		Datum		result;
 		bool		isnull;
 
diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c
index 22339f7491f..f3c9f3c0bd6 100644
--- a/src/backend/optimizer/path/allpaths.c
+++ b/src/backend/optimizer/path/allpaths.c
@@ -4760,21 +4760,16 @@ remove_unused_subquery_outputs(Query *subquery, RelOptInfo *rel,
 		if (IsA(texpr, Var))
 		{
 			Var		   *var = (Var *) texpr;
-			ListCell   *wlc;
 			bool		needed_by_define = false;
 
-			foreach(wlc, subquery->windowClause)
+			foreach_node(WindowClause, wc, subquery->windowClause)
 			{
-				WindowClause *wc = lfirst_node(WindowClause, wlc);
-
 				if (wc->defineClause != NIL)
 				{
 					List	   *vars = pull_var_clause((Node *) wc->defineClause, 0);
-					ListCell   *vlc;
 
-					foreach(vlc, vars)
+					foreach_node(Var, dvar, vars)
 					{
-						Var		   *dvar = (Var *) lfirst(vlc);
 
 						/*
 						 * Match varno as well as varattno: a Var pulled from
diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c
index b38cad9f121..82472c3fe96 100644
--- a/src/backend/optimizer/path/costsize.c
+++ b/src/backend/optimizer/path/costsize.c
@@ -3236,14 +3236,13 @@ cost_windowagg(Path *path, PlannerInfo *root,
 	if (winclause->rpPattern)
 	{
 		List	   *pattern_vars;
-		ListCell   *lc2;
 		QualCost	defcosts;
 
 		pattern_vars = collectPatternVariables(winclause->rpPattern);
 
-		foreach(lc2, pattern_vars)
+		foreach_node(String, pv, pattern_vars)
 		{
-			char	   *ptname = strVal(lfirst(lc2));
+			char	   *ptname = strVal(pv);
 
 			foreach_node(TargetEntry, def, winclause->defineClause)
 			{
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index d2e19d61d58..cca4126e511 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -2736,7 +2736,6 @@ compute_define_metadata(List *defineClause,
 {
 	DefineMetadataContext ctx;
 	NavTraversal trav;
-	ListCell   *lc;
 
 	ctx.maxOffset = 0;
 	ctx.maxNeedsEval = false;
@@ -2750,10 +2749,8 @@ compute_define_metadata(List *defineClause,
 	trav.visit = visit_nav_plan;
 	trav.data = &ctx;
 
-	foreach(lc, defineClause)
+	foreach_node(TargetEntry, te, defineClause)
 	{
-		TargetEntry *te = (TargetEntry *) lfirst(lc);
-
 		nav_traversal_walker((Node *) te->expr, &trav);
 		ctx.curVarIdx++;
 	}
diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index d768422deeb..664c942e575 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -191,12 +191,10 @@ tryUnwrapSingleChild(RPRPatternNode *pattern)
 static List *
 flattenSeqChildren(List *children)
 {
-	ListCell   *lc;
 	List	   *newChildren = NIL;
 
-	foreach(lc, children)
+	foreach_node(RPRPatternNode, child, children)
 	{
-		RPRPatternNode *child = (RPRPatternNode *) lfirst(lc);
 		RPRPatternNode *opt = optimizeRPRPattern(child);
 
 		/* GROUP{1,1} should have been unwrapped by optimizeGroupPattern */
@@ -229,14 +227,11 @@ flattenSeqChildren(List *children)
 static List *
 mergeConsecutiveVars(List *children)
 {
-	ListCell   *lc;
 	List	   *mergedChildren = NIL;
 	RPRPatternNode *prev = NULL;
 
-	foreach(lc, children)
+	foreach_node(RPRPatternNode, child, children)
 	{
-		RPRPatternNode *child = (RPRPatternNode *) lfirst(lc);
-
 		if (child->nodeType == RPR_PATTERN_VAR && child->reluctant == false)
 		{
 			/* ----------------------
@@ -306,14 +301,11 @@ mergeConsecutiveVars(List *children)
 static List *
 mergeConsecutiveGroups(List *children)
 {
-	ListCell   *lc;
 	List	   *mergedChildren = NIL;
 	RPRPatternNode *prev = NULL;
 
-	foreach(lc, children)
+	foreach_node(RPRPatternNode, child, children)
 	{
-		RPRPatternNode *child = (RPRPatternNode *) lfirst(lc);
-
 		if (child->nodeType == RPR_PATTERN_GROUP && child->reluctant == false)
 		{
 			/* ----------------------
@@ -385,15 +377,12 @@ mergeConsecutiveGroups(List *children)
 static List *
 mergeConsecutiveAlts(List *children)
 {
-	ListCell   *lc;
 	List	   *mergedChildren = NIL;
 	RPRPatternNode *prev = NULL;
 	int			count = 0;
 
-	foreach(lc, children)
+	foreach_node(RPRPatternNode, child, children)
 	{
-		RPRPatternNode *child = (RPRPatternNode *) lfirst(lc);
-
 		if (child->nodeType == RPR_PATTERN_ALT && child->reluctant == false)
 		{
 			if (prev != NULL &&
@@ -702,12 +691,10 @@ optimizeSeqPattern(RPRPatternNode *pattern)
 static List *
 flattenAltChildren(List *children)
 {
-	ListCell   *lc;
 	List	   *newChildren = NIL;
 
-	foreach(lc, children)
+	foreach_node(RPRPatternNode, child, children)
 	{
-		RPRPatternNode *child = (RPRPatternNode *) lfirst(lc);
 		RPRPatternNode *opt = optimizeRPRPattern(child);
 
 		if (opt->nodeType == RPR_PATTERN_ALT)
@@ -732,18 +719,15 @@ flattenAltChildren(List *children)
 static List *
 removeDuplicateAlternatives(List *children)
 {
-	ListCell   *lc;
-	ListCell   *lc2;
 	List	   *uniqueChildren = NIL;
 
-	foreach(lc, children)
+	foreach_node(RPRPatternNode, child, children)
 	{
-		RPRPatternNode *child = (RPRPatternNode *) lfirst(lc);
 		bool		isDuplicate = false;
 
-		foreach(lc2, uniqueChildren)
+		foreach_node(RPRPatternNode, uchild, uniqueChildren)
 		{
-			if (rprPatternEqual((RPRPatternNode *) lfirst(lc2), child))
+			if (rprPatternEqual(uchild, child))
 			{
 				isDuplicate = true;
 				break;
@@ -940,16 +924,13 @@ tryUnwrapGroup(RPRPatternNode *pattern)
 static RPRPatternNode *
 optimizeGroupPattern(RPRPatternNode *pattern)
 {
-	ListCell   *lc;
 	List	   *newChildren;
 	RPRPatternNode *result;
 
 	/* Recursively optimize children */
 	newChildren = NIL;
-	foreach(lc, pattern->children)
+	foreach_node(RPRPatternNode, child, pattern->children)
 	{
-		RPRPatternNode *child = (RPRPatternNode *) lfirst(lc);
-
 		newChildren = lappend(newChildren, optimizeRPRPattern(child));
 	}
 	pattern->children = newChildren;
@@ -1005,15 +986,14 @@ optimizeRPRPattern(RPRPatternNode *pattern)
 static int
 collectDefineVariables(List *defineVariableList, char **varNames)
 {
-	ListCell   *lc;
 	int			numVars = 0;
 
-	foreach(lc, defineVariableList)
+	foreach_node(String, varname, defineVariableList)
 	{
 		/* Parser already checked this limit in transformDefineClause */
 		Assert(numVars <= RPR_VARID_MAX);
 
-		varNames[numVars++] = strVal(lfirst(lc));
+		varNames[numVars++] = strVal(varname);
 	}
 
 	return numVars;
@@ -1031,7 +1011,6 @@ static void
 scanRPRPatternRecursive(RPRPatternNode *node, char **varNames, int *numVars,
 						int *numElements, RPRDepth depth, RPRDepth *maxDepth)
 {
-	ListCell   *lc;
 	int			i;
 
 	/* Pattern nodes from parser are never NULL */
@@ -1075,9 +1054,9 @@ scanRPRPatternRecursive(RPRPatternNode *node, char **varNames, int *numVars,
 
 		case RPR_PATTERN_SEQ:
 			/* Sequence: just recurse into children */
-			foreach(lc, node->children)
+			foreach_node(RPRPatternNode, child, node->children)
 			{
-				scanRPRPatternRecursive((RPRPatternNode *) lfirst(lc), varNames,
+				scanRPRPatternRecursive(child, varNames,
 										numVars, numElements, depth, maxDepth);
 			}
 			break;
@@ -1088,9 +1067,9 @@ scanRPRPatternRecursive(RPRPatternNode *node, char **varNames, int *numVars,
 				(*numElements)++;
 
 			/* Recurse into children at increased depth */
-			foreach(lc, node->children)
+			foreach_node(RPRPatternNode, child, node->children)
 			{
-				scanRPRPatternRecursive((RPRPatternNode *) lfirst(lc), varNames,
+				scanRPRPatternRecursive(child, varNames,
 										numVars, numElements, depth + 1, maxDepth);
 			}
 
@@ -1104,9 +1083,9 @@ scanRPRPatternRecursive(RPRPatternNode *node, char **varNames, int *numVars,
 			(*numElements)++;
 
 			/* Recurse into children at increased depth */
-			foreach(lc, node->children)
+			foreach_node(RPRPatternNode, child, node->children)
 			{
-				scanRPRPatternRecursive((RPRPatternNode *) lfirst(lc), varNames,
+				scanRPRPatternRecursive(child, varNames,
 										numVars, numElements, depth + 1, maxDepth);
 			}
 			break;
@@ -1248,7 +1227,6 @@ fillRPRPatternVar(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth dept
 static bool
 fillRPRPatternGroup(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth depth)
 {
-	ListCell   *lc;
 	int			groupStartIdx = *idx;
 	int			beginIdx = -1;
 	bool		bodyNullable = true;
@@ -1275,9 +1253,9 @@ fillRPRPatternGroup(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth de
 		groupStartIdx = *idx;	/* children start after BEGIN */
 	}
 
-	foreach(lc, node->children)
+	foreach_node(RPRPatternNode, child, node->children)
 	{
-		if (!fillRPRPattern((RPRPatternNode *) lfirst(lc), pat, idx, depth + 1))
+		if (!fillRPRPattern(child, pat, idx, depth + 1))
 			bodyNullable = false;
 	}
 
@@ -1350,9 +1328,8 @@ fillRPRPatternAlt(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth dept
 	(*idx)++;
 
 	/* Fill each alternative */
-	foreach(lc, node->children)
+	foreach_node(RPRPatternNode, alt, node->children)
 	{
-		RPRPatternNode *alt = (RPRPatternNode *) lfirst(lc);
 		int			branchStart = *idx;
 
 		altBranchStarts = lappend_int(altBranchStarts, branchStart);
@@ -1372,9 +1349,8 @@ fillRPRPatternAlt(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth dept
 
 	/* Set next on last element of each alternative to after the alternation */
 	afterAltIdx = *idx;
-	lc2 = list_head(altBranchStarts);
 
-	foreach(lc, altEndPositions)
+	forboth(lc, altEndPositions, lc2, altBranchStarts)
 	{
 		int			endPos = lfirst_int(lc);
 		int			branchStart = lfirst_int(lc2);
@@ -1399,8 +1375,6 @@ fillRPRPatternAlt(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth dept
 		{
 			pat->elements[endPos].next = afterAltIdx;
 		}
-
-		lc2 = lnext(altBranchStarts, lc2);
 	}
 
 	list_free(altBranchStarts);
@@ -1422,7 +1396,6 @@ fillRPRPatternAlt(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth dept
 static bool
 fillRPRPattern(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth depth)
 {
-	ListCell   *lc;
 	bool		allNullable = true;
 
 	/* Pattern nodes from parser are never NULL */
@@ -1433,9 +1406,9 @@ fillRPRPattern(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth depth)
 	switch (node->nodeType)
 	{
 		case RPR_PATTERN_SEQ:
-			foreach(lc, node->children)
+			foreach_node(RPRPatternNode, child, node->children)
 			{
-				if (!fillRPRPattern((RPRPatternNode *) lfirst(lc), pat, idx, depth))
+				if (!fillRPRPattern(child, pat, idx, depth))
 					allNullable = false;
 			}
 			return allNullable;
@@ -1856,8 +1829,6 @@ computeAbsorbability(RPRPattern *pattern)
 static void
 collectPatternVariablesRecursive(RPRPatternNode *node, List **varNames)
 {
-	ListCell   *lc;
-
 	Assert(node != NULL);
 
 	check_stack_depth();
@@ -1866,9 +1837,9 @@ collectPatternVariablesRecursive(RPRPatternNode *node, List **varNames)
 	{
 		case RPR_PATTERN_VAR:
 			/* Add variable if not already in list */
-			foreach(lc, *varNames)
+			foreach_node(String, varname, *varNames)
 			{
-				if (strcmp(strVal(lfirst(lc)), node->varName) == 0)
+				if (strcmp(strVal(varname), node->varName) == 0)
 					return;		/* Already collected */
 			}
 			*varNames = lappend(*varNames, makeString(pstrdup(node->varName)));
@@ -1877,10 +1848,9 @@ collectPatternVariablesRecursive(RPRPatternNode *node, List **varNames)
 		case RPR_PATTERN_SEQ:
 		case RPR_PATTERN_ALT:
 		case RPR_PATTERN_GROUP:
-			foreach(lc, node->children)
+			foreach_node(RPRPatternNode, child, node->children)
 			{
-				collectPatternVariablesRecursive((RPRPatternNode *) lfirst(lc),
-												 varNames);
+				collectPatternVariablesRecursive(child, varNames);
 			}
 			break;
 	}
@@ -1917,14 +1887,10 @@ collectPatternVariables(RPRPatternNode *pattern)
 void
 buildDefineVariableList(List *defineClause, List **defineVariableList)
 {
-	ListCell   *lc;
-
 	*defineVariableList = NIL;
 
-	foreach(lc, defineClause)
+	foreach_node(TargetEntry, te, defineClause)
 	{
-		TargetEntry *te = (TargetEntry *) lfirst(lc);
-
 		*defineVariableList = lappend(*defineVariableList,
 									  makeString(pstrdup(te->resname)));
 	}
diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c
index 02898a9106b..74dc486b1f5 100644
--- a/src/backend/optimizer/prep/prepjointree.c
+++ b/src/backend/optimizer/prep/prepjointree.c
@@ -2593,10 +2593,8 @@ perform_pullup_replace_vars(PlannerInfo *root,
 	parse->returningList = (List *)
 		pullup_replace_vars((Node *) parse->returningList, rvcontext);
 
-	foreach(lc, parse->windowClause)
+	foreach_node(WindowClause, wc, parse->windowClause)
 	{
-		WindowClause *wc = lfirst_node(WindowClause, lc);
-
 		if (wc->defineClause != NIL)
 			wc->defineClause = (List *)
 				pullup_replace_vars((Node *) wc->defineClause, rvcontext);
diff --git a/src/backend/parser/parse_cte.c b/src/backend/parser/parse_cte.c
index 0974b43d028..3e493beba0b 100644
--- a/src/backend/parser/parse_cte.c
+++ b/src/backend/parser/parse_cte.c
@@ -182,9 +182,8 @@ transformWithClause(ParseState *pstate, WithClause *withClause)
 		 * rewritten to WITH RECURSIVE by makeRecursiveViewSelect() and so
 		 * flows through here as well.
 		 */
-		foreach(lc, withClause->ctes)
+		foreach_node(CommonTableExpr, cte, withClause->ctes)
 		{
-			CommonTableExpr *cte = (CommonTableExpr *) lfirst(lc);
 			ContainRPRContext ctx;
 
 			ctx.location = -1;
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index f65a270d20e..0f7207577b3 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -652,12 +652,11 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 		list_length(cref->fields) != 1)
 	{
 		char	   *qualifier = strVal(linitial(cref->fields));
-		ListCell   *lc;
 		bool		is_pattern_var = false;
 
-		foreach(lc, pstate->p_rpr_pattern_vars)
+		foreach_node(String, pv, pstate->p_rpr_pattern_vars)
 		{
-			if (strcmp(strVal(lfirst(lc)), qualifier) == 0)
+			if (strcmp(strVal(pv), qualifier) == 0)
 			{
 				is_pattern_var = true;
 				break;
diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index 4e1d2650cf6..d12a0d45c94 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -208,8 +208,6 @@ static void
 validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
 						   List *rpDefs, List **varNames)
 {
-	ListCell   *lc;
-
 	/* Pattern node must exist - parser always provides non-NULL root */
 	Assert(node != NULL);
 
@@ -228,9 +226,9 @@ validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
 			{
 				bool		found = false;
 
-				foreach(lc, *varNames)
+				foreach_node(String, varname, *varNames)
 				{
-					if (strcmp(strVal(lfirst(lc)), node->varName) == 0)
+					if (strcmp(strVal(varname), node->varName) == 0)
 					{
 						found = true;
 						break;
@@ -261,10 +259,9 @@ validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
 		case RPR_PATTERN_ALT:
 		case RPR_PATTERN_GROUP:
 			/* Recurse into children */
-			foreach(lc, node->children)
+			foreach_node(RPRPatternNode, child, node->children)
 			{
-				validateRPRPatternVarCount(pstate, (RPRPatternNode *) lfirst(lc),
-										   NULL, varNames);
+				validateRPRPatternVarCount(pstate, child, NULL, varNames);
 			}
 			break;
 	}
@@ -277,15 +274,13 @@ validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
 	 */
 	if (rpDefs)
 	{
-		foreach(lc, rpDefs)
+		foreach_node(ResTarget, rt, rpDefs)
 		{
-			ResTarget  *rt = (ResTarget *) lfirst(lc);
-			ListCell   *lc2;
 			bool		found = false;
 
-			foreach(lc2, *varNames)
+			foreach_node(String, varname, *varNames)
 			{
-				if (strcmp(strVal(lfirst(lc2)), rt->name) == 0)
+				if (strcmp(strVal(varname), rt->name) == 0)
 				{
 					found = true;
 					break;
@@ -327,10 +322,6 @@ static List *
 transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 					  List **targetlist)
 {
-	ListCell   *lc,
-			   *l;
-	ResTarget  *restarget,
-			   *r;
 	List	   *restargets;
 	List	   *defineClause = NIL;
 	char	   *name;
@@ -357,18 +348,16 @@ transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 	 * equivalent.
 	 */
 	restargets = NIL;
-	foreach(lc, windef->rpCommonSyntax->rpDefs)
+	foreach_node(ResTarget, restarget, windef->rpCommonSyntax->rpDefs)
 	{
 		TargetEntry *teDefine;
 
-		restarget = (ResTarget *) lfirst(lc);
 		name = restarget->name;
 
-		foreach(l, restargets)
+		foreach_node(ResTarget, r, restargets)
 		{
 			char	   *n;
 
-			r = (ResTarget *) lfirst(l);
 			n = r->name;
 
 			if (!strcmp(n, name))
@@ -394,7 +383,6 @@ transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 		{
 			Node	   *expr;
 			List	   *vars;
-			ListCell   *lc2;
 
 			expr = transformExpr(pstate, restarget->val,
 								 EXPR_KIND_RPR_DEFINE);
@@ -407,16 +395,12 @@ transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
 			 * evaluation.
 			 */
 			vars = pull_var_clause(expr, 0);
-			foreach(lc2, vars)
+			foreach_node(Var, var, vars)
 			{
-				Var		   *var = (Var *) lfirst(lc2);
 				bool		found = false;
-				ListCell   *tl;
 
-				foreach(tl, *targetlist)
+				foreach_node(TargetEntry, tle, *targetlist)
 				{
-					TargetEntry *tle = (TargetEntry *) lfirst(tl);
-
 					if (IsA(tle->expr, Var) &&
 						((Var *) tle->expr)->varno == var->varno &&
 						((Var *) tle->expr)->varattno == var->varattno)
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index d588cd8263d..415da6417d4 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -7153,7 +7153,6 @@ static void
 get_rule_pattern_node(RPRPatternNode *node, deparse_context *context)
 {
 	StringInfo	buf = context->buf;
-	ListCell   *lc;
 	const char *sep;
 
 	Assert(node != NULL);
@@ -7167,20 +7166,20 @@ get_rule_pattern_node(RPRPatternNode *node, deparse_context *context)
 
 		case RPR_PATTERN_SEQ:
 			sep = "";
-			foreach(lc, node->children)
+			foreach_node(RPRPatternNode, child, node->children)
 			{
 				appendStringInfoString(buf, sep);
-				get_rule_pattern_node((RPRPatternNode *) lfirst(lc), context);
+				get_rule_pattern_node(child, context);
 				sep = " ";
 			}
 			break;
 
 		case RPR_PATTERN_ALT:
 			sep = "";
-			foreach(lc, node->children)
+			foreach_node(RPRPatternNode, child, node->children)
 			{
 				appendStringInfoString(buf, sep);
-				get_rule_pattern_node((RPRPatternNode *) lfirst(lc), context);
+				get_rule_pattern_node(child, context);
 				sep = " | ";
 			}
 			break;
@@ -7188,10 +7187,10 @@ get_rule_pattern_node(RPRPatternNode *node, deparse_context *context)
 		case RPR_PATTERN_GROUP:
 			appendStringInfoChar(buf, '(');
 			sep = "";
-			foreach(lc, node->children)
+			foreach_node(RPRPatternNode, child, node->children)
 			{
 				appendStringInfoString(buf, sep);
-				get_rule_pattern_node((RPRPatternNode *) lfirst(lc), context);
+				get_rule_pattern_node(child, context);
 				sep = " ";
 			}
 			appendStringInfoChar(buf, ')');
@@ -7221,14 +7220,11 @@ get_rule_define(List *defineClause, deparse_context *context)
 {
 	StringInfo	buf = context->buf;
 	const char *sep;
-	ListCell   *lc_def;
 
 	sep = "  ";
 
-	foreach(lc_def, defineClause)
+	foreach_node(TargetEntry, te, defineClause)
 	{
-		TargetEntry *te = (TargetEntry *) lfirst(lc_def);
-
 		appendStringInfo(buf, "%s%s AS ", sep, quote_identifier(te->resname));
 		get_rule_expr((Node *) te->expr, context, false);
 		sep = ",\n  ";
-- 
2.50.1 (Apple Git-155)



view thread (109+ messages)  latest in thread

reply

Reply instructions:

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

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

  To: [email protected]
  Cc: [email protected], [email protected], [email protected], [email protected], [email protected], [email protected], [email protected], [email protected], [email protected], [email protected]
  Subject: Re: Row pattern recognition
  In-Reply-To: <CAAAe_zCd1pA6vCaMD7e3hB3Ou+=mZziB1=CO_tBybuAiE5K=vQ@mail.gmail.com>

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

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