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