From 20ef73dc4564fd71c2f7c1eb204c16376aeca74d Mon Sep 17 00:00:00 2001
From: Henson Choi <assam258@gmail.com>
Date: Tue, 23 Jun 2026 14:31:21 +0900
Subject: [PATCH v50 27/29] Add row pattern recognition coverage tests and tidy
 unreachable code

Extend the RPR regression suite to exercise paths that had no coverage:
int64 overflow in navigation offsets (NEXT/NEXT(FIRST)/NEXT(LAST)),
executor-side NEEDS_EVAL offset evaluation in visit_nav_exec (bare LAST,
compound NEXT(LAST)/PREV(FIRST), simple FIRST), the runtime "Nav Mark
Lookahead" plan output, a typmod coercion applied to a navigation result,
the pattern element-count limit in scanRPRPattern, and an EXCLUDE TIES
nth_value/last_value frame case.

Tidy the defensive code these tests cannot reach, with no behavioral change
for valid input:

- Convert the provably non-underflowing arithmetic guards in
  ExecEvalRPRNavSet (PREV / PREV_FIRST / PREV_LAST) to assertions.

- RPRPattern is a plan/exec-only node that equal() never compares, so mark
  it no_equal and drop the hand-written _equalRPRPattern.

- Replace the unreachable alternation scope-end clamp in deparse_rpr_node
  with an assertion, drop the dead RPR_NAV_OFFSET_RETAIN_ALL lookahead arm,
  simplify the symmetric _outRPRPattern/_readRPRPattern element-array
  handling, and make the IGNORE NULLS funcname guard a safe fallback.
---
 src/backend/commands/explain.c                |   9 +-
 src/backend/executor/execExprInterp.c         |  27 +++-
 src/backend/executor/nodeWindowAgg.c          |   5 +-
 src/backend/nodes/equalfuncs.c                |  34 -----
 src/backend/nodes/outfuncs.c                  |  32 ++--
 src/backend/nodes/readfuncs.c                 |  89 ++++++------
 src/include/nodes/plannodes.h                 |   8 +-
 src/test/regress/expected/rpr.out             |  84 +++++++++++
 src/test/regress/expected/rpr_base.out        |  39 +++++
 src/test/regress/expected/rpr_explain.out     | 137 ++++++++++++++++++
 src/test/regress/expected/rpr_integration.out |   4 +
 src/test/regress/expected/rpr_nfa.out         |  55 +++++++
 src/test/regress/expected/window.out          |  36 +++++
 src/test/regress/sql/rpr.sql                  |  49 +++++++
 src/test/regress/sql/rpr_base.sql             |  30 ++++
 src/test/regress/sql/rpr_explain.sql          |  77 ++++++++++
 src/test/regress/sql/rpr_integration.sql      |   4 +
 src/test/regress/sql/rpr_nfa.sql              |  42 ++++++
 src/test/regress/sql/window.sql               |   8 +
 19 files changed, 654 insertions(+), 115 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index b3fc324718d..aeb17c1f793 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -3069,8 +3069,8 @@ deparse_rpr_node(RPRPattern *pattern, int idx, int limit, StringInfo buf)
 		int			b;
 		bool		first = true;
 
-		if (altEnd > limit)
-			altEnd = limit;
+		/* an alternation's depth-derived scope end never exceeds the limit */
+		Assert(altEnd <= limit);
 
 		appendStringInfoChar(buf, '(');
 		b = idx + 1;
@@ -3262,10 +3262,6 @@ show_window_def(WindowAggState *planstate, List *ancestors, ExplainState *es)
 					ExplainPropertyText("Nav Mark Lookahead", "runtime",
 										es);
 					break;
-				case RPR_NAV_OFFSET_RETAIN_ALL:
-					ExplainPropertyText("Nav Mark Lookahead", "retain all",
-										es);
-					break;
 				case RPR_NAV_OFFSET_FIXED:
 					if (firstOffset == PG_INT64_MAX)
 						ExplainPropertyText("Nav Mark Lookahead", "infinite",
@@ -3275,6 +3271,7 @@ show_window_def(WindowAggState *planstate, List *ancestors, ExplainState *es)
 											   firstOffset, es);
 					break;
 				default:
+					/* RPR_NAV_OFFSET_RETAIN_ALL is lookback-only, never here */
 					elog(ERROR, "unrecognized RPR nav offset kind: %d",
 						 firstKind);
 					break;
diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c
index 805c8583fb2..5b6281e9e31 100644
--- a/src/backend/executor/execExprInterp.c
+++ b/src/backend/executor/execExprInterp.c
@@ -6098,8 +6098,14 @@ ExecEvalRPRNavSet(ExprState *state, ExprEvalStep *op, ExprContext *econtext)
 	switch (op->d.rpr_nav.kind)
 	{
 		case RPR_NAV_PREV:
-			if (pg_sub_s64_overflow(winstate->currentpos, offset, &target_pos))
-				target_pos = -1;
+
+			/*
+			 * currentpos and offset are both non-negative, so the subtraction
+			 * cannot underflow; assert the invariant rather than guarding an
+			 * unreachable overflow.
+			 */
+			Assert(!pg_sub_s64_overflow(winstate->currentpos, offset, &target_pos));
+			target_pos = winstate->currentpos - offset;
 			break;
 		case RPR_NAV_NEXT:
 			if (pg_add_s64_overflow(winstate->currentpos, offset, &target_pos))
@@ -6144,8 +6150,12 @@ ExecEvalRPRNavSet(ExprState *state, ExprEvalStep *op, ExprContext *econtext)
 				/* Apply outer: PREV subtracts, NEXT adds */
 				if (op->d.rpr_nav.kind == RPR_NAV_PREV_FIRST)
 				{
-					if (pg_sub_s64_overflow(inner_pos, compound_offset, &target_pos))
-						target_pos = -1;
+					/*
+					 * inner_pos is in [0, currentpos] and compound_offset is
+					 * non-negative, so this cannot underflow.
+					 */
+					Assert(!pg_sub_s64_overflow(inner_pos, compound_offset, &target_pos));
+					target_pos = inner_pos - compound_offset;
 				}
 				else
 				{
@@ -6179,8 +6189,13 @@ ExecEvalRPRNavSet(ExprState *state, ExprEvalStep *op, ExprContext *econtext)
 				/* Apply outer: PREV subtracts, NEXT adds */
 				if (op->d.rpr_nav.kind == RPR_NAV_PREV_LAST)
 				{
-					if (pg_sub_s64_overflow(inner_pos, compound_offset, &target_pos))
-						target_pos = -1;
+					/*
+					 * inner_pos is in [nav_match_start, currentpos] (>= 0)
+					 * and compound_offset is non-negative, so this cannot
+					 * underflow.
+					 */
+					Assert(!pg_sub_s64_overflow(inner_pos, compound_offset, &target_pos));
+					target_pos = inner_pos - compound_offset;
 				}
 				else
 				{
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index eb1d616b49a..ae0fb036867 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -4938,12 +4938,11 @@ WinCheckAndInitializeNullTreatment(WindowObject winobj,
 	{
 		const char *funcname = get_func_name(fcinfo->flinfo->fn_oid);
 
-		if (!funcname)
-			elog(ERROR, "could not get function name");
+		/* the executing function's name always resolves; stay safe regardless */
 		ereport(ERROR,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("function %s does not allow RESPECT/IGNORE NULLS",
-						funcname)));
+						funcname ? funcname : "?")));
 	}
 	else if (winobj->ignore_nulls == PARSER_IGNORE_NULLS)
 		winobj->ignore_nulls = IGNORE_NULLS;
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 328199918b8..1bbda9ad367 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -150,40 +150,6 @@ _equalBitmapset(const Bitmapset *a, const Bitmapset *b)
 	return bms_equal(a, b);
 }
 
-static bool
-_equalRPRPattern(const RPRPattern *a, const RPRPattern *b)
-{
-	COMPARE_SCALAR_FIELD(numVars);
-	COMPARE_SCALAR_FIELD(maxDepth);
-	COMPARE_SCALAR_FIELD(numElements);
-
-	/* Compare varNames array */
-	if (a->numVars > 0)
-	{
-		if (a->varNames == NULL || b->varNames == NULL)
-			return false;
-		for (int i = 0; i < a->numVars; i++)
-		{
-			if (strcmp(a->varNames[i], b->varNames[i]) != 0)
-				return false;
-		}
-	}
-
-	/* Compare elements array */
-	if (a->numElements > 0)
-	{
-		if (a->elements == NULL || b->elements == NULL)
-			return false;
-		if (memcmp(a->elements, b->elements,
-				   a->numElements * sizeof(RPRPatternElement)) != 0)
-			return false;
-	}
-
-	COMPARE_SCALAR_FIELD(isAbsorbable);
-
-	return true;
-}
-
 /*
  * Lists are handled specially
  */
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index e6ea9ce22d9..0b145329cbf 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -753,27 +753,23 @@ _outRPRPattern(StringInfo str, const RPRPattern *node)
 	else
 		appendStringInfoString(str, " <>");
 
-	/* Write elements array */
+	/* Write elements array (makeRPRPattern guarantees numElements >= 2) */
 	appendStringInfoString(str, " :elements");
-	if (node->numElements > 0 && node->elements != NULL)
+	Assert(node->numElements > 0 && node->elements != NULL);
+	appendStringInfoChar(str, ' ');
+	for (int i = 0; i < node->numElements; i++)
 	{
-		appendStringInfoChar(str, ' ');
-		for (int i = 0; i < node->numElements; i++)
-		{
-			const RPRPatternElement *elem = &node->elements[i];
-
-			appendStringInfo(str, "(%d %d %u %d %d %d %d)",
-							 (int) elem->varId,
-							 (int) elem->depth,
-							 (unsigned) elem->flags,
-							 (int) elem->min,
-							 (int) elem->max,
-							 (int) elem->next,
-							 (int) elem->jump);
-		}
+		const RPRPatternElement *elem = &node->elements[i];
+
+		appendStringInfo(str, "(%d %d %u %d %d %d %d)",
+						 (int) elem->varId,
+						 (int) elem->depth,
+						 (unsigned) elem->flags,
+						 (int) elem->min,
+						 (int) elem->max,
+						 (int) elem->next,
+						 (int) elem->jump);
 	}
-	else
-		appendStringInfoString(str, " <>");
 
 	WRITE_BOOL_FIELD(isAbsorbable);
 }
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
index 6c39c6fe06d..e83e3d2f784 100644
--- a/src/backend/nodes/readfuncs.c
+++ b/src/backend/nodes/readfuncs.c
@@ -597,54 +597,49 @@ _readRPRPattern(void)
 
 	/* Read elements array */
 	token = pg_strtok(&length); /* skip :elements */
-	token = pg_strtok(&length); /* get '(' or '<>' */
-	if (local_node->numElements > 0 && token[0] == '(')
-	{
-		local_node->elements = palloc0_array(RPRPatternElement, local_node->numElements);
-		for (int i = 0; i < local_node->numElements; i++)
-		{
-			RPRPatternElement *elem = &local_node->elements[i];
-			int			varId,
-						flags,
-						depth,
-						min,
-						max,
-						next,
-						jump;
-
-			/* Parse "(varId depth flags min max next jump)" */
-			token = pg_strtok(&length);
-			varId = atoi(token);
-			token = pg_strtok(&length);
-			depth = atoi(token);
-			token = pg_strtok(&length);
-			flags = atoi(token);
-			token = pg_strtok(&length);
-			min = atoi(token);
-			token = pg_strtok(&length);
-			max = atoi(token);
-			token = pg_strtok(&length);
-			next = atoi(token);
-			token = pg_strtok(&length);
-			jump = atoi(token);
-			token = pg_strtok(&length); /* skip ')' */
-
-			elem->varId = (RPRVarId) varId;
-			elem->flags = (RPRElemFlags) flags;
-			elem->depth = (RPRDepth) depth;
-			elem->min = (RPRQuantity) min;
-			elem->max = (RPRQuantity) max;
-			elem->next = (RPRElemIdx) next;
-			elem->jump = (RPRElemIdx) jump;
-
-			/* Read next element's '(' or end */
-			if (i < local_node->numElements - 1)
-				token = pg_strtok(&length); /* get '(' */
-		}
-	}
-	else
+	token = pg_strtok(&length); /* get '(' */
+	/* out always emits the array (makeRPRPattern guarantees numElements >= 2) */
+	Assert(local_node->numElements > 0 && token[0] == '(');
+	local_node->elements = palloc0_array(RPRPatternElement, local_node->numElements);
+	for (int i = 0; i < local_node->numElements; i++)
 	{
-		local_node->elements = NULL;
+		RPRPatternElement *elem = &local_node->elements[i];
+		int			varId,
+					flags,
+					depth,
+					min,
+					max,
+					next,
+					jump;
+
+		/* Parse "(varId depth flags min max next jump)" */
+		token = pg_strtok(&length);
+		varId = atoi(token);
+		token = pg_strtok(&length);
+		depth = atoi(token);
+		token = pg_strtok(&length);
+		flags = atoi(token);
+		token = pg_strtok(&length);
+		min = atoi(token);
+		token = pg_strtok(&length);
+		max = atoi(token);
+		token = pg_strtok(&length);
+		next = atoi(token);
+		token = pg_strtok(&length);
+		jump = atoi(token);
+		token = pg_strtok(&length); /* skip ')' */
+
+		elem->varId = (RPRVarId) varId;
+		elem->flags = (RPRElemFlags) flags;
+		elem->depth = (RPRDepth) depth;
+		elem->min = (RPRQuantity) min;
+		elem->max = (RPRQuantity) max;
+		elem->next = (RPRElemIdx) next;
+		elem->jump = (RPRElemIdx) jump;
+
+		/* Read next element's '(' or end */
+		if (i < local_node->numElements - 1)
+			token = pg_strtok(&length); /* get '(' */
 	}
 
 	READ_BOOL_FIELD(isAbsorbable);
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index d45d93d79a2..7c4eacdf82d 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -1282,7 +1282,13 @@ typedef struct RPRPatternElement
  */
 typedef struct RPRPattern
 {
-	pg_node_attr(custom_copy_equal, custom_read_write)
+	/*
+	 * RPRPattern is a plan/exec-only node with arrays that need a
+	 * hand-written copy (custom_copy_equal), but it is never compared by
+	 * equal() -- it does not appear in parse/rewrite trees, and equal() has
+	 * no Plan-node routines -- so equal support is suppressed with no_equal.
+	 */
+	pg_node_attr(custom_copy_equal, custom_read_write, no_equal)
 
 	NodeTag		type;			/* T_RPRPattern */
 	int			numVars;		/* number of pattern variables */
diff --git a/src/test/regress/expected/rpr.out b/src/test/regress/expected/rpr.out
index dc5ac981fec..52be54039e7 100644
--- a/src/test/regress/expected/rpr.out
+++ b/src/test/regress/expected/rpr.out
@@ -1852,6 +1852,27 @@ WINDOW w AS (
  company2 | 07-10-2023 |   1300 |             |            |     0
 (20 rows)
 
+-- Typmod coercion over a navigation result: casting PREV(p) (a numeric(10,3)
+-- column) to a narrower numeric(8,2) inside DEFINE forces coerce_type_typmod,
+-- which calls exprTypmod() on the RPRNavExpr.
+CREATE TEMP TABLE rpr_typmod (id int, p numeric(10,3));
+INSERT INTO rpr_typmod VALUES (1, 1.5), (2, 2.5), (3, 3.5);
+SELECT id, count(*) OVER w AS cnt
+FROM rpr_typmod
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE A AS TRUE, B AS CAST(PREV(p) AS numeric(8,2)) > 0
+);
+ id | cnt 
+----+-----
+  1 |   3
+  2 |   0
+  3 |   0
+(3 rows)
+
+DROP TABLE rpr_typmod;
 --
 -- FIRST/LAST navigation
 --
@@ -2283,6 +2304,59 @@ SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS (
     DEFINE A AS NEXT(LAST(val), -1) IS NULL
 );
 ERROR:  row pattern navigation offset must not be negative
+-- Outer offset overflows int64: target position out of range -> NULL.
+-- Plain NEXT(val, INT64_MAX): currentpos + INT64_MAX overflows.
+SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS NEXT(val, 9223372036854775807) IS NULL
+);
+ id | val | count 
+----+-----+-------
+  1 |  10 |     6
+  2 |  20 |     0
+  3 |  30 |     0
+  4 |  10 |     0
+  5 |  50 |     0
+  6 |  10 |     0
+(6 rows)
+
+-- Compound NEXT(FIRST()): outer offset overflow.  Inner offset 1 forces
+-- inner_pos >= 1, so inner_pos + INT64_MAX overflows at every match.
+SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE A AS TRUE, B AS NEXT(FIRST(val, 1), 9223372036854775807) IS NULL
+);
+ id | val | count 
+----+-----+-------
+  1 |  10 |     6
+  2 |  20 |     0
+  3 |  30 |     0
+  4 |  10 |     0
+  5 |  50 |     0
+  6 |  10 |     0
+(6 rows)
+
+-- Compound NEXT(LAST()): outer offset overflow.
+SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS NEXT(LAST(val), 9223372036854775807) IS NULL
+);
+ id | val | count 
+----+-----+-------
+  1 |  10 |     6
+  2 |  20 |     0
+  3 |  30 |     0
+  4 |  10 |     0
+  5 |  50 |     0
+  6 |  10 |     0
+(6 rows)
+
 -- Compound: default offsets on both sides
 -- PREV(FIRST(val)): inner=0 (match_start), outer=1 -> target = match_start - 1
 SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt
@@ -2338,6 +2412,16 @@ SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS (
     DEFINE A AS NEXT(LAST(val, -1), 1) IS NULL
 );
 ERROR:  row pattern navigation offset must not be negative
+-- Offset argument whose type has no implicit cast to bigint (parse error)
+SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE A AS TRUE, B AS val > PREV(val, 1.5)
+);
+ERROR:  offset argument of PREV must be type bigint, not type numeric
+LINE 5:     DEFINE A AS TRUE, B AS val > PREV(val, 1.5)
+                                                   ^
 -- Compound + host variable offsets
 PREPARE test_compound_offset(int8, int8) AS
 SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index 3cee15c7bd3..33fcb40b1a8 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -1703,6 +1703,21 @@ ERROR:  quantifier bound must be between 1 and 2147483646
 LINE 6:     PATTERN (A{,2147483647})
                         ^
 DROP TABLE rpr_bounds;
+-- Pattern element-count boundary at RPR_ELEMIDX_MAX (32767).  Alternating
+-- distinct variables stop the optimizer from merging consecutive elements, so
+-- each "A B" pair contributes two elements; scanRPRPattern adds one FIN marker.
+-- ECHO is silenced so the generated multi-thousand-token patterns do not flood
+-- the expected output.
+--   16383 pairs         -> 32766 + 1 FIN = 32767 = maximum, accepted.
+--   16383 pairs + one A -> 32767 + 1 FIN = 32768 > maximum, rejected.
+\set ECHO none
+ count 
+-------
+     0
+(1 row)
+
+ERROR:  pattern too complex
+DETAIL:  Pattern has 32768 elements, maximum is 32767.
 -- ============================================================
 -- Navigation Functions Tests (PREV / NEXT / FIRST / LAST)
 -- ============================================================
@@ -2198,6 +2213,30 @@ SELECT pg_get_viewdef('navns_fn');
 (1 row)
 
 DROP VIEW navns_nav, navns_fn;
+-- A qualified last() in DEFINE must stay schema-qualified on deparse so that
+-- it does not reparse as the LAST navigation function (force-qualify path)
+CREATE FUNCTION rpr_navns.last(integer) RETURNS integer AS 'SELECT -999' LANGUAGE sql IMMUTABLE;
+CREATE VIEW navns_fn_last AS
+  SELECT id, count(*) OVER w AS cnt FROM nt
+  WINDOW w AS (PARTITION BY g ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+    PATTERN (A+) DEFINE A AS rpr_navns.last(val) = -999);
+SELECT pg_get_viewdef('navns_fn_last');
+                                       pg_get_viewdef                                        
+---------------------------------------------------------------------------------------------
+  SELECT id,                                                                                +
+     count(*) OVER w AS cnt                                                                 +
+    FROM nt                                                                                 +
+   WINDOW w AS (PARTITION BY g ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
+   AFTER MATCH SKIP PAST LAST ROW                                                           +
+   INITIAL                                                                                  +
+   PATTERN (a+)                                                                             +
+   DEFINE                                                                                   +
+   a AS (rpr_navns.last(val) = '-999'::integer) );
+(1 row)
+
+DROP VIEW navns_fn_last;
+DROP FUNCTION rpr_navns.last(integer);
 -- Attribute notation is field selection only, never a function fallback
 CREATE TYPE rpr_navns_pair AS (first int, last int);
 CREATE TABLE ct (id int, p rpr_navns_pair);
diff --git a/src/test/regress/expected/rpr_explain.out b/src/test/regress/expected/rpr_explain.out
index 8672b4c3055..d8343c9accc 100644
--- a/src/test/regress/expected/rpr_explain.out
+++ b/src/test/regress/expected/rpr_explain.out
@@ -6023,6 +6023,31 @@ EXPLAIN (COSTS OFF, ANALYZE, TIMING OFF, SUMMARY OFF)
 
 RESET plan_cache_mode;
 DEALLOCATE test_overflow_lookahead;
+-- Runtime (non-overflow) lookahead: a small parameter offset under a generic
+-- plan keeps the FIRST offset unresolved at plan time, so EXPLAIN reports
+-- "Nav Mark Lookahead: runtime" rather than a concrete value.
+PREPARE p_first_runtime(int8, int8) AS
+SELECT count(*) OVER w
+FROM generate_series(1,10) s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS NEXT(FIRST(v, $1), $2) IS NOT NULL
+);
+SET plan_cache_mode = force_generic_plan;
+EXPLAIN (COSTS OFF) EXECUTE p_first_runtime(1, 1);
+                            QUERY PLAN                             
+-------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: a+
+   Nav Mark Lookback: 0
+   Nav Mark Lookahead: runtime
+   ->  Function Scan on generate_series s
+(6 rows)
+
+RESET plan_cache_mode;
+DEALLOCATE p_first_runtime;
 -- PREV(v) + PREV(v, $1): NEEDS_EVAL path must account for implicit lookback=1
 -- Previously, eval_nav_max_offset_walker skipped PREV(v) when offset_arg was
 -- NULL, causing maxOffset=0 when $1=0, which would trim the row needed by
@@ -6051,6 +6076,118 @@ EXECUTE test_prev_implicit_offset(0);
 (10 rows)
 
 DEALLOCATE test_prev_implicit_offset;
+-- NEEDS_EVAL executor offset paths: a Param nav offset stays non-Const under a
+-- generic plan, so the planner marks the offset NEEDS_EVAL and the executor
+-- resolves it at init via eval_define_offsets -> visit_nav_exec.  Each query
+-- below exercises a different navigation arm of that walker.
+-- Simple FIRST(v, $1): forward-reach FIRST arm.
+PREPARE test_eval_first(int8) AS
+SELECT count(*) OVER w
+FROM generate_series(1,10) s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS FIRST(v, $1) IS NOT NULL
+);
+SET plan_cache_mode = force_generic_plan;
+EXECUTE test_eval_first(1);
+ count 
+-------
+     0
+     0
+     0
+     0
+     0
+     0
+     0
+     0
+     0
+     0
+(10 rows)
+
+RESET plan_cache_mode;
+DEALLOCATE test_eval_first;
+-- Bare LAST(v, $1): backward-reach LAST-with-offset arm.
+PREPARE test_eval_last(int8) AS
+SELECT count(*) OVER w
+FROM generate_series(1,10) s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS LAST(v, $1) >= 0
+);
+SET plan_cache_mode = force_generic_plan;
+EXECUTE test_eval_last(1);
+ count 
+-------
+     0
+     0
+     0
+     0
+     0
+     0
+     0
+     0
+     0
+     0
+(10 rows)
+
+RESET plan_cache_mode;
+DEALLOCATE test_eval_last;
+-- Compound NEXT(LAST(v, $1), $2): backward-reach NEXT_LAST arm.
+PREPARE test_eval_nextlast(int8, int8) AS
+SELECT count(*) OVER w
+FROM generate_series(1,10) s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS NEXT(LAST(v, $1), $2) > 0
+);
+SET plan_cache_mode = force_generic_plan;
+EXECUTE test_eval_nextlast(1, 1);
+ count 
+-------
+     0
+     0
+     0
+     0
+     0
+     0
+     0
+     0
+     0
+     0
+(10 rows)
+
+RESET plan_cache_mode;
+DEALLOCATE test_eval_nextlast;
+-- Compound PREV(FIRST(v, $1), $2): forward-reach PREV_FIRST arm.
+PREPARE test_eval_prevfirst(int8, int8) AS
+SELECT count(*) OVER w
+FROM generate_series(1,10) s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS PREV(FIRST(v, $1), $2) IS NOT NULL
+);
+SET plan_cache_mode = force_generic_plan;
+EXECUTE test_eval_prevfirst(1, 1);
+ count 
+-------
+     0
+     0
+     0
+     0
+     0
+     0
+     0
+     0
+     0
+     0
+(10 rows)
+
+RESET plan_cache_mode;
+DEALLOCATE test_eval_prevfirst;
 -- Runtime error: negative offset at execution time
 PREPARE test_runtime_neg_offset(int8) AS
 SELECT count(*) OVER w
diff --git a/src/test/regress/expected/rpr_integration.out b/src/test/regress/expected/rpr_integration.out
index 652989927d7..541ad35de4f 100644
--- a/src/test/regress/expected/rpr_integration.out
+++ b/src/test/regress/expected/rpr_integration.out
@@ -1098,6 +1098,10 @@ EXECUTE rpr_prev(1);
  10 |  45 |   0
 (10 rows)
 
+-- Negative runtime nav offset under the generic plan: init clamps it to 0 for
+-- trim sizing, but the per-row navigation rejects the negative offset.
+EXECUTE rpr_prev(-1);
+ERROR:  row pattern navigation offset must not be negative
 RESET plan_cache_mode;
 DEALLOCATE rpr_prev;
 -- ============================================================
diff --git a/src/test/regress/expected/rpr_nfa.out b/src/test/regress/expected/rpr_nfa.out
index 1fc85a365f6..22d244707c6 100644
--- a/src/test/regress/expected/rpr_nfa.out
+++ b/src/test/regress/expected/rpr_nfa.out
@@ -1287,6 +1287,27 @@ ORDER BY id;
   4 |  0 |  0 |  0 |  0 |   0 |  0 |  0
 (4 rows)
 
+-- Doubly-nested reluctant nullable group: (((A??){2,}?){2,}?).  Reluctant
+-- quantifiers disable optimizer flattening, so both levels survive and the
+-- inner group's END->next lands on the outer END.  This exercises the
+-- END->END count increment in the EMPTY_LOOP fast-forward (count < min).
+WITH t(id, isa) AS (VALUES (1, true), (2, true), (3, false))
+SELECT id, count(*) OVER w AS c
+FROM t
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (((A??){2,}?){2,}?)
+    DEFINE A AS isa
+)
+ORDER BY id;
+ id | c 
+----+---
+  1 | 0
+  2 | 0
+  3 | 0
+(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, which already honors reluctant ordering.
@@ -1323,6 +1344,40 @@ WINDOW w AS (
   4 | {C}   |             |          
 (4 rows)
 
+-- Reluctant nullable group with a required follower: ((A??){2,}? B).
+-- min=2 forces the reluctant fast-forward to loop back (it cannot exit to
+-- FIN until B matches), exercising the loop-back route_to_elem second call
+-- site in nfa_advance_end.  B fails at the group's exit row, so only the
+-- first match survives.
+WITH test_reluctant_nullable_follower AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['B']),
+        (4, ARRAY['X'])
+    ) AS t(id, flags)
+)
+SELECT id, flags,
+       first_value(id) OVER w AS match_start,
+       last_value(id) OVER w AS match_end
+FROM test_reluctant_nullable_follower
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A??){2,}? B)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end 
+----+-------+-------------+-----------
+  1 | {A}   |           1 |         3
+  2 | {A}   |             |          
+  3 | {B}   |             |          
+  4 | {X}   |             |          
+(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/expected/window.out b/src/test/regress/expected/window.out
index 59c3df8cf0a..071cf2fe2f4 100644
--- a/src/test/regress/expected/window.out
+++ b/src/test/regress/expected/window.out
@@ -1037,6 +1037,42 @@ FROM tenk1 WHERE unique1 < 10;
           7 |       7 |    3
 (10 rows)
 
+SELECT nth_value(unique1,2) over (ORDER BY four rows between current row and 3 following exclude ties),
+	unique1, four
+FROM tenk1 WHERE unique1 < 10;
+ nth_value | unique1 | four 
+-----------+---------+------
+         5 |       0 |    0
+         5 |       8 |    0
+         5 |       4 |    0
+         6 |       5 |    1
+         6 |       9 |    1
+         6 |       1 |    1
+         3 |       6 |    2
+         3 |       2 |    2
+           |       3 |    3
+           |       7 |    3
+(10 rows)
+
+SELECT last_value(unique1) over (ORDER BY four rows between 1 following and 2 following exclude ties),
+	unique1, four
+FROM tenk1 WHERE unique1 < 12 ORDER BY four, unique1;
+ last_value | unique1 | four 
+------------+---------+------
+          1 |       0 |    0
+            |       4 |    0
+          5 |       8 |    0
+          6 |       1 |    1
+            |       5 |    1
+         10 |       9 |    1
+          3 |       2 |    2
+            |       6 |    2
+          7 |      10 |    2
+            |       3 |    3
+            |       7 |    3
+            |      11 |    3
+(12 rows)
+
 SELECT sum(unique1) over (rows between 2 preceding and 1 preceding),
 	unique1, four
 FROM tenk1 WHERE unique1 < 10;
diff --git a/src/test/regress/sql/rpr.sql b/src/test/regress/sql/rpr.sql
index 59ce3d0200f..2013f399b32 100644
--- a/src/test/regress/sql/rpr.sql
+++ b/src/test/regress/sql/rpr.sql
@@ -948,6 +948,21 @@ WINDOW w AS (
         B AS PREV(price::numeric, 1) > PREV(price::numeric, 2)
 );
 
+-- Typmod coercion over a navigation result: casting PREV(p) (a numeric(10,3)
+-- column) to a narrower numeric(8,2) inside DEFINE forces coerce_type_typmod,
+-- which calls exprTypmod() on the RPRNavExpr.
+CREATE TEMP TABLE rpr_typmod (id int, p numeric(10,3));
+INSERT INTO rpr_typmod VALUES (1, 1.5), (2, 2.5), (3, 3.5);
+SELECT id, count(*) OVER w AS cnt
+FROM rpr_typmod
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE A AS TRUE, B AS CAST(PREV(p) AS numeric(8,2)) > 0
+);
+DROP TABLE rpr_typmod;
+
 --
 -- FIRST/LAST navigation
 --
@@ -1206,6 +1221,32 @@ SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS (
     DEFINE A AS NEXT(LAST(val), -1) IS NULL
 );
 
+-- Outer offset overflows int64: target position out of range -> NULL.
+-- Plain NEXT(val, INT64_MAX): currentpos + INT64_MAX overflows.
+SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS NEXT(val, 9223372036854775807) IS NULL
+);
+
+-- Compound NEXT(FIRST()): outer offset overflow.  Inner offset 1 forces
+-- inner_pos >= 1, so inner_pos + INT64_MAX overflows at every match.
+SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE A AS TRUE, B AS NEXT(FIRST(val, 1), 9223372036854775807) IS NULL
+);
+
+-- Compound NEXT(LAST()): outer offset overflow.
+SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS NEXT(LAST(val), 9223372036854775807) IS NULL
+);
+
 -- Compound: default offsets on both sides
 -- PREV(FIRST(val)): inner=0 (match_start), outer=1 -> target = match_start - 1
 SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt
@@ -1243,6 +1284,14 @@ SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS (
     DEFINE A AS NEXT(LAST(val, -1), 1) IS NULL
 );
 
+-- Offset argument whose type has no implicit cast to bigint (parse error)
+SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A B+)
+    DEFINE A AS TRUE, B AS val > PREV(val, 1.5)
+);
+
 -- Compound + host variable offsets
 PREPARE test_compound_offset(int8, int8) AS
 SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index e77e5813b9e..61a5bcf0b8c 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -1247,6 +1247,24 @@ WINDOW w AS (
 
 DROP TABLE rpr_bounds;
 
+-- Pattern element-count boundary at RPR_ELEMIDX_MAX (32767).  Alternating
+-- distinct variables stop the optimizer from merging consecutive elements, so
+-- each "A B" pair contributes two elements; scanRPRPattern adds one FIN marker.
+-- ECHO is silenced so the generated multi-thousand-token patterns do not flood
+-- the expected output.
+--   16383 pairs         -> 32766 + 1 FIN = 32767 = maximum, accepted.
+--   16383 pairs + one A -> 32767 + 1 FIN = 32768 > maximum, rejected.
+\set ECHO none
+SELECT format($$SELECT count(*) OVER w FROM (SELECT 1 i) t
+  WINDOW w AS (ORDER BY i ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+  INITIAL PATTERN (%s) DEFINE A AS TRUE, B AS TRUE)$$,
+  repeat('A B ', 16383)) \gexec
+SELECT format($$SELECT count(*) OVER w FROM (SELECT 1 i) t
+  WINDOW w AS (ORDER BY i ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+  INITIAL PATTERN (%s A) DEFINE A AS TRUE, B AS TRUE)$$,
+  repeat('A B ', 16383)) \gexec
+\set ECHO all
+
 -- ============================================================
 -- Navigation Functions Tests (PREV / NEXT / FIRST / LAST)
 -- ============================================================
@@ -1531,6 +1549,18 @@ SELECT pg_get_viewdef('navns_nav');
 SELECT pg_get_viewdef('navns_fn');
 DROP VIEW navns_nav, navns_fn;
 
+-- A qualified last() in DEFINE must stay schema-qualified on deparse so that
+-- it does not reparse as the LAST navigation function (force-qualify path)
+CREATE FUNCTION rpr_navns.last(integer) RETURNS integer AS 'SELECT -999' LANGUAGE sql IMMUTABLE;
+CREATE VIEW navns_fn_last AS
+  SELECT id, count(*) OVER w AS cnt FROM nt
+  WINDOW w AS (PARTITION BY g ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+    PATTERN (A+) DEFINE A AS rpr_navns.last(val) = -999);
+SELECT pg_get_viewdef('navns_fn_last');
+DROP VIEW navns_fn_last;
+DROP FUNCTION rpr_navns.last(integer);
+
 -- Attribute notation is field selection only, never a function fallback
 CREATE TYPE rpr_navns_pair AS (first int, last int);
 CREATE TABLE ct (id int, p rpr_navns_pair);
diff --git a/src/test/regress/sql/rpr_explain.sql b/src/test/regress/sql/rpr_explain.sql
index 115402e304d..3cd94af6bb8 100644
--- a/src/test/regress/sql/rpr_explain.sql
+++ b/src/test/regress/sql/rpr_explain.sql
@@ -3406,6 +3406,22 @@ EXPLAIN (COSTS OFF, ANALYZE, TIMING OFF, SUMMARY OFF)
 RESET plan_cache_mode;
 DEALLOCATE test_overflow_lookahead;
 
+-- Runtime (non-overflow) lookahead: a small parameter offset under a generic
+-- plan keeps the FIRST offset unresolved at plan time, so EXPLAIN reports
+-- "Nav Mark Lookahead: runtime" rather than a concrete value.
+PREPARE p_first_runtime(int8, int8) AS
+SELECT count(*) OVER w
+FROM generate_series(1,10) s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS NEXT(FIRST(v, $1), $2) IS NOT NULL
+);
+SET plan_cache_mode = force_generic_plan;
+EXPLAIN (COSTS OFF) EXECUTE p_first_runtime(1, 1);
+RESET plan_cache_mode;
+DEALLOCATE p_first_runtime;
+
 -- PREV(v) + PREV(v, $1): NEEDS_EVAL path must account for implicit lookback=1
 -- Previously, eval_nav_max_offset_walker skipped PREV(v) when offset_arg was
 -- NULL, causing maxOffset=0 when $1=0, which would trim the row needed by
@@ -3421,6 +3437,67 @@ WINDOW w AS (
 EXECUTE test_prev_implicit_offset(0);
 DEALLOCATE test_prev_implicit_offset;
 
+-- NEEDS_EVAL executor offset paths: a Param nav offset stays non-Const under a
+-- generic plan, so the planner marks the offset NEEDS_EVAL and the executor
+-- resolves it at init via eval_define_offsets -> visit_nav_exec.  Each query
+-- below exercises a different navigation arm of that walker.
+
+-- Simple FIRST(v, $1): forward-reach FIRST arm.
+PREPARE test_eval_first(int8) AS
+SELECT count(*) OVER w
+FROM generate_series(1,10) s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS FIRST(v, $1) IS NOT NULL
+);
+SET plan_cache_mode = force_generic_plan;
+EXECUTE test_eval_first(1);
+RESET plan_cache_mode;
+DEALLOCATE test_eval_first;
+
+-- Bare LAST(v, $1): backward-reach LAST-with-offset arm.
+PREPARE test_eval_last(int8) AS
+SELECT count(*) OVER w
+FROM generate_series(1,10) s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS LAST(v, $1) >= 0
+);
+SET plan_cache_mode = force_generic_plan;
+EXECUTE test_eval_last(1);
+RESET plan_cache_mode;
+DEALLOCATE test_eval_last;
+
+-- Compound NEXT(LAST(v, $1), $2): backward-reach NEXT_LAST arm.
+PREPARE test_eval_nextlast(int8, int8) AS
+SELECT count(*) OVER w
+FROM generate_series(1,10) s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS NEXT(LAST(v, $1), $2) > 0
+);
+SET plan_cache_mode = force_generic_plan;
+EXECUTE test_eval_nextlast(1, 1);
+RESET plan_cache_mode;
+DEALLOCATE test_eval_nextlast;
+
+-- Compound PREV(FIRST(v, $1), $2): forward-reach PREV_FIRST arm.
+PREPARE test_eval_prevfirst(int8, int8) AS
+SELECT count(*) OVER w
+FROM generate_series(1,10) s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (A+)
+    DEFINE A AS PREV(FIRST(v, $1), $2) IS NOT NULL
+);
+SET plan_cache_mode = force_generic_plan;
+EXECUTE test_eval_prevfirst(1, 1);
+RESET plan_cache_mode;
+DEALLOCATE test_eval_prevfirst;
+
 -- Runtime error: negative offset at execution time
 PREPARE test_runtime_neg_offset(int8) AS
 SELECT count(*) OVER w
diff --git a/src/test/regress/sql/rpr_integration.sql b/src/test/regress/sql/rpr_integration.sql
index 2b990b24704..23164473d0b 100644
--- a/src/test/regress/sql/rpr_integration.sql
+++ b/src/test/regress/sql/rpr_integration.sql
@@ -670,6 +670,10 @@ SET plan_cache_mode = force_generic_plan;
 EXPLAIN (COSTS OFF) EXECUTE rpr_prev(1);
 EXECUTE rpr_prev(1);
 
+-- Negative runtime nav offset under the generic plan: init clamps it to 0 for
+-- trim sizing, but the per-row navigation rejects the negative offset.
+EXECUTE rpr_prev(-1);
+
 RESET plan_cache_mode;
 DEALLOCATE rpr_prev;
 
diff --git a/src/test/regress/sql/rpr_nfa.sql b/src/test/regress/sql/rpr_nfa.sql
index c82b8340977..79f99922870 100644
--- a/src/test/regress/sql/rpr_nfa.sql
+++ b/src/test/regress/sql/rpr_nfa.sql
@@ -912,6 +912,21 @@ WINDOW gg  AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATT
        cs  AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*?)        DEFINE A AS isa)
 ORDER BY id;
 
+-- Doubly-nested reluctant nullable group: (((A??){2,}?){2,}?).  Reluctant
+-- quantifiers disable optimizer flattening, so both levels survive and the
+-- inner group's END->next lands on the outer END.  This exercises the
+-- END->END count increment in the EMPTY_LOOP fast-forward (count < min).
+WITH t(id, isa) AS (VALUES (1, true), (2, true), (3, false))
+SELECT id, count(*) OVER w AS c
+FROM t
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN (((A??){2,}?){2,}?)
+    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, which already honors reluctant ordering.
@@ -941,6 +956,33 @@ WINDOW w AS (
         C AS 'C' = ANY(flags)
 );
 
+-- Reluctant nullable group with a required follower: ((A??){2,}? B).
+-- min=2 forces the reluctant fast-forward to loop back (it cannot exit to
+-- FIN until B matches), exercising the loop-back route_to_elem second call
+-- site in nfa_advance_end.  B fails at the group's exit row, so only the
+-- first match survives.
+WITH test_reluctant_nullable_follower AS (
+    SELECT * FROM (VALUES
+        (1, ARRAY['A']),
+        (2, ARRAY['A']),
+        (3, ARRAY['B']),
+        (4, ARRAY['X'])
+    ) AS t(id, flags)
+)
+SELECT id, flags,
+       first_value(id) OVER w AS match_start,
+       last_value(id) OVER w AS match_end
+FROM test_reluctant_nullable_follower
+WINDOW w AS (
+    ORDER BY id
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    AFTER MATCH SKIP PAST LAST ROW
+    PATTERN ((A??){2,}? B)
+    DEFINE
+        A AS 'A' = ANY(flags),
+        B AS 'B' = 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 (
diff --git a/src/test/regress/sql/window.sql b/src/test/regress/sql/window.sql
index 17261135dc3..e844ec10d1a 100644
--- a/src/test/regress/sql/window.sql
+++ b/src/test/regress/sql/window.sql
@@ -235,6 +235,14 @@ SELECT last_value(unique1) over (ORDER BY four rows between current row and 2 fo
 	unique1, four
 FROM tenk1 WHERE unique1 < 10;
 
+SELECT nth_value(unique1,2) over (ORDER BY four rows between current row and 3 following exclude ties),
+	unique1, four
+FROM tenk1 WHERE unique1 < 10;
+
+SELECT last_value(unique1) over (ORDER BY four rows between 1 following and 2 following exclude ties),
+	unique1, four
+FROM tenk1 WHERE unique1 < 12 ORDER BY four, unique1;
+
 SELECT sum(unique1) over (rows between 2 preceding and 1 preceding),
 	unique1, four
 FROM tenk1 WHERE unique1 < 10;
-- 
2.50.1 (Apple Git-155)

