From 20ef73dc4564fd71c2f7c1eb204c16376aeca74d Mon Sep 17 00:00:00 2001 From: Henson Choi 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)