v50-0018-coverage-tests.patch
application/octet-stream
Filename: v50-0018-coverage-tests.patch
Type: application/octet-stream
Part: 26
Message:
Re: Row pattern recognition
Patch
Same data as JSON:
GET /api/v1/attachments/:id/patch
the parsed metadata as JSON — format, series position, per-file stats; never the diff bytes.
API reference →
Format: format-patch
Series: patch v50-0018
Subject: Add row pattern recognition coverage tests and tidy unreachable code
| File | + | − |
|---|---|---|
| src/backend/commands/explain.c | 3 | 6 |
| src/backend/executor/execExprInterp.c | 21 | 6 |
| src/backend/executor/nodeWindowAgg.c | 2 | 3 |
| src/backend/nodes/equalfuncs.c | 0 | 34 |
| src/backend/nodes/outfuncs.c | 14 | 18 |
| src/backend/nodes/readfuncs.c | 42 | 47 |
| src/include/nodes/plannodes.h | 7 | 1 |
| src/test/regress/expected/rpr_base.out | 39 | 0 |
| src/test/regress/expected/rpr_explain.out | 137 | 0 |
| src/test/regress/expected/rpr_integration.out | 4 | 0 |
| src/test/regress/expected/rpr_nfa.out | 55 | 0 |
| src/test/regress/expected/rpr.out | 84 | 0 |
| src/test/regress/expected/window.out | 36 | 0 |
| src/test/regress/sql/rpr_base.sql | 30 | 0 |
| src/test/regress/sql/rpr_explain.sql | 77 | 0 |
| src/test/regress/sql/rpr_integration.sql | 4 | 0 |
| src/test/regress/sql/rpr_nfa.sql | 42 | 0 |
| src/test/regress/sql/rpr.sql | 49 | 0 |
| src/test/regress/sql/window.sql | 8 | 0 |
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)