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