From 5fa46211c1ed7b0adf72ed7e3bee1dd32d1cebb8 Mon Sep 17 00:00:00 2001 From: jian he Date: Thu, 2 Jul 2026 17:23:51 +0800 Subject: [PATCH] Resolve RPR navigation offsets in the executor, not the plan tree Row pattern navigation operations (PREV, NEXT, FIRST, LAST and the compound forms) resolved their offset expressions in four separate places: ExecInitExprRec, ExecEvalRPRNavSet, extract_const_offset and eval_define_offsets. Each turned the same expression into the same int64. Resolve them in one place instead. build_define_offsets(), reached from ExecInitWindowAgg, records one RPRNavOffsets entry per navigation, and resolve_nav_offsets() settles the values once per scan; each navigation's RPRNavState holds a back-link so ExecEvalRPRNavSet() only reads them. The offsets live in executor state rather than on the RPRNavExpr because the plan tree is read-only and may be shared by concurrent executions. An offset is a run-time constant, not a plan-time one. Only a Const is settled at executor startup; a PARAM_EXEC offset from a function inline or a LATERAL reference has no value at init and changes per outer row, and a bind parameter under a generic plan reaches the same path. Those are compiled into the navigation's RPRNavState as NullableDatum and evaluated per scan, with tuplestore trim disabled for the partition since the reach cannot be bounded ahead of time. ExecEvalRPRNavSet() rejects a null or negative resolved offset. RPRNavOffsetKind therefore distinguishes three states rather than two: FIXED for a settled constant, NEEDS_EVAL for an offset resolved per scan, and RETAIN_ALL for a backward reach that overflows int64. EXPLAIN reads the kind and the resolved value from the planstate, since ExecInitWindowAgg runs even for EXPLAIN without ANALYZE, so it prints a concrete offset, "runtime", or "retain all" as appropriate. That lets the per-invocation offset machinery go away: the Datum arrays in the eval step, rpr_nav_get_compound_offset(), and the NULL and negative checks scattered across the callers, which eval_nav_offset() -- renamed from eval_nav_offset_helper -- now does in one place. The planner walk, now named compute_matchStartDependent(), only classifies match_start dependency, which is all buildRPRPattern() needs to decide context absorption. The generic nav_traversal_walker() goes away as well. Both call sites assert that an RPRNavExpr never nests inside another RPRNavExpr, so a NavTraversal struct carrying a function pointer and a void * context, shared between the planner and the executor, is more apparatus than the job needs. Each site gets a small static RPRNavExpr_walker() instead, and nodeWindowAgg.c no longer includes optimizer/rpr.h. finalize_plan() now walks the WindowAgg's defineClause, so a correlated PARAM_EXEC used only in DEFINE reaches extParam and a caching node above the WindowAgg rescans instead of returning stale rows. compute_nav_offsets() folds minFirstOffset inside each branch that computes a reach rather than once after them. The plain FIRST case updates minFirstOffset directly and leaves reach at its initial zero, so a trailing Min() would clamp every FIRST(v, N) forward reach to zero. rpr_explain covered FIRST(v) without an offset and the compound PREV_FIRST and NEXT_FIRST forms, but not a simple FIRST(v, N) with a positive constant offset. Add a case asserting that its forward reach shows up as "Nav Mark Lookahead: N"; that is the case the clamping above would break. Each site gets a small static RPRNavExpr_walker() instead, and nodeWindowAgg.c no longer includes optimizer/rpr.h. --- src/backend/commands/explain.c | 59 +-- src/backend/executor/README.rpr | 9 +- src/backend/executor/execExpr.c | 90 ++-- src/backend/executor/execExprInterp.c | 103 ++-- src/backend/executor/nodeWindowAgg.c | 466 +++++++++++------- src/backend/optimizer/plan/createplan.c | 329 ++----------- src/backend/optimizer/plan/rpr.c | 33 -- src/backend/optimizer/plan/subselect.c | 2 + src/include/executor/execExpr.h | 8 +- src/include/nodes/execnodes.h | 57 ++- src/include/nodes/parsenodes.h | 27 +- src/include/nodes/plannodes.h | 21 - src/include/nodes/primnodes.h | 29 +- src/include/optimizer/rpr.h | 22 - src/test/regress/expected/rpr.out | 18 + src/test/regress/expected/rpr_base.out | 51 ++ src/test/regress/expected/rpr_explain.out | 69 ++- src/test/regress/expected/rpr_integration.out | 143 ++++++ src/test/regress/sql/rpr.sql | 17 + src/test/regress/sql/rpr_base.sql | 40 ++ src/test/regress/sql/rpr_explain.sql | 48 +- src/test/regress/sql/rpr_integration.sql | 84 ++++ src/tools/pgindent/typedefs.list | 4 +- 23 files changed, 972 insertions(+), 757 deletions(-) diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c index 1ee7d351c24..5b55dea96dc 100644 --- a/src/backend/commands/explain.c +++ b/src/backend/commands/explain.c @@ -3202,11 +3202,6 @@ show_window_def(WindowAggState *planstate, List *ancestors, ExplainState *es) /* Show Row Pattern Recognition pattern if present */ if (wagg->rpPattern != NULL) { - RPRNavOffsetKind maxKind = wagg->navMaxOffsetKind; - int64 maxOffset = wagg->navMaxOffset; - RPRNavOffsetKind firstKind = wagg->navFirstOffsetKind; - int64 firstOffset = wagg->navFirstOffset; - char *patternStr = deparse_rpr_pattern(wagg->rpPattern); ExplainPropertyText("Pattern", patternStr, es); @@ -3214,19 +3209,11 @@ show_window_def(WindowAggState *planstate, List *ancestors, ExplainState *es) pfree(patternStr); /* - * Show navigation offsets for tuplestore trim. For EXPLAIN ANALYZE, - * use the executor-resolved values (which may differ from the plan - * when NEEDS_EVAL was resolved to FIXED or RETAIN_ALL at init). + * Navigation offsets for tuplestore trim are resolved at executor + * init, which runs even for plain EXPLAIN, so read the resolved value + * and its kind from the planstate. */ - if (es->analyze) - { - maxKind = planstate->navMaxOffsetKind; - maxOffset = planstate->navMaxOffset; - firstKind = planstate->navFirstOffsetKind; - firstOffset = planstate->navFirstOffset; - } - - switch (maxKind) + switch (planstate->navMaxOffsetKind) { case RPR_NAV_OFFSET_NEEDS_EVAL: ExplainPropertyText("Nav Mark Lookback", "runtime", es); @@ -3234,38 +3221,20 @@ show_window_def(WindowAggState *planstate, List *ancestors, ExplainState *es) case RPR_NAV_OFFSET_RETAIN_ALL: ExplainPropertyText("Nav Mark Lookback", "retain all", es); break; - case RPR_NAV_OFFSET_FIXED: - ExplainPropertyInteger("Nav Mark Lookback", NULL, - maxOffset, es); - break; default: - elog(ERROR, "unrecognized RPR nav offset kind: %d", - maxKind); - break; + ExplainPropertyInteger("Nav Mark Lookback", NULL, + planstate->navMaxOffset, es); } - if (wagg->hasFirstNav) + if (planstate->hasFirstNav) { - switch (firstKind) - { - case RPR_NAV_OFFSET_NEEDS_EVAL: - ExplainPropertyText("Nav Mark Lookahead", "runtime", - es); - break; - case RPR_NAV_OFFSET_FIXED: - if (firstOffset == PG_INT64_MAX) - ExplainPropertyText("Nav Mark Lookahead", "infinite", - es); - else - ExplainPropertyInteger("Nav Mark Lookahead", NULL, - firstOffset, es); - break; - default: - /* RPR_NAV_OFFSET_RETAIN_ALL is lookback-only, never here */ - elog(ERROR, "unrecognized RPR nav offset kind: %d", - firstKind); - break; - } + if (planstate->navFirstOffsetKind == RPR_NAV_OFFSET_NEEDS_EVAL) + ExplainPropertyText("Nav Mark Lookahead", "runtime", es); + else if (planstate->navFirstOffset == PG_INT64_MAX) + ExplainPropertyText("Nav Mark Lookahead", "infinite", es); + else + ExplainPropertyInteger("Nav Mark Lookahead", NULL, + planstate->navFirstOffset, es); } } } diff --git a/src/backend/executor/README.rpr b/src/backend/executor/README.rpr index 8d24383b0e3..23d979111bd 100644 --- a/src/backend/executor/README.rpr +++ b/src/backend/executor/README.rpr @@ -742,7 +742,7 @@ VI-5. Tuplestore Mark and Trim (nodeWindowAgg.c) Navigation functions require access to past rows via the tuplestore. To allow tuplestore_trim() to free rows that are no longer reachable, -the planner computes two offsets (see compute_define_metadata): +the executor computes two offsets at init (see build_define_offsets): navMaxOffset (Nav Mark Lookback): Maximum backward reach from currentpos. Contributed by PREV, @@ -758,9 +758,10 @@ the planner computes two offsets (see compute_define_metadata): The actual mark is set to: min(lookback_mark, lookahead_mark). This ensures all rows reachable by any navigation function are retained. -When offsets contain non-constant expressions (Param), the planner sets -navMaxOffsetKind/navFirstOffsetKind to RPR_NAV_OFFSET_NEEDS_EVAL and the -executor evaluates them at init time. On overflow, the kind is set to +When offsets contain non-constant expressions (Param), the executor sets +navMaxOffsetKind/navFirstOffsetKind to RPR_NAV_OFFSET_NEEDS_EVAL; constant +and bind-parameter offsets are resolved at init, and a PARAM_EXEC offset is +re-resolved per scan (resolve_nav_offsets). On overflow, the kind is set to RPR_NAV_OFFSET_RETAIN_ALL, disabling trim for that dimension. VI-6. ExecRPRProcessRow(): 3-Phase Processing diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c index 8e812fdcdc5..64b0df6c9ba 100644 --- a/src/backend/executor/execExpr.c +++ b/src/backend/executor/execExpr.c @@ -36,6 +36,7 @@ #include "catalog/pg_type.h" #include "executor/execExpr.h" #include "executor/nodeSubplan.h" +#include "executor/nodeWindowAgg.h" #include "funcapi.h" #include "jit/jit.h" #include "miscadmin.h" @@ -1177,72 +1178,51 @@ ExecInitExprRec(Expr *node, ExprState *state, * swaps ecxt_outertuple to the target row, the argument * expression is compiled normally (reads from the swapped * slot), and the RESTORE opcode restores the original slot. - * - * Default offset when offset_arg is NULL: PREV/NEXT: 1 - * (physical offset from currentpos) FIRST/LAST: 0 (logical - * offset from match boundary) */ + RPRNavState *rprnavstate = makeNode(RPRNavState); RPRNavExpr *nav = (RPRNavExpr *) node; WindowAggState *winstate; + bool find_navexpr = false; Assert(state->parent && IsA(state->parent, WindowAggState)); winstate = (WindowAggState *) state->parent; - /* Emit SET opcode: swap slot to target row */ - scratch.opcode = EEOP_RPR_NAV_SET; - scratch.d.rpr_nav.winstate = winstate; - scratch.d.rpr_nav.kind = nav->kind; + rprnavstate->winstate = winstate; + rprnavstate->rprnavexpr = nav; - if (nav->kind >= RPR_NAV_PREV_FIRST) + /* + * Link this RPRNavState to the navigation's RPRNavOffsets + * entry (built by build_define_offsets() at executor + * startup). The offset is a run-time constant resolved once + * per scan by resolve_nav_offsets(), which pins the value + * here through the back-link; seed with a placeholder now. + * + * The offsets live in executor state rather than on the + * RPRNavExpr because the plan tree is read-only and may be + * shared by concurrent executions. + */ + foreach_ptr(RPRNavOffsets, entry, winstate->rprNavOffsets) { - /* - * Compound navigation: allocate array of 2 for inner [0] - * and outer [1] offsets. - */ - Datum *offset_values = palloc_array(Datum, 2); - bool *offset_isnulls = palloc_array(bool, 2); - - /* Inner offset (default 0 for FIRST/LAST) */ - if (nav->offset_arg != NULL) - ExecInitExprRec(nav->offset_arg, state, - &offset_values[0], &offset_isnulls[0]); - else - { - offset_values[0] = Int64GetDatum(0); - offset_isnulls[0] = false; - } + if (entry->nav == nav && find_navexpr) + elog(ERROR, "RPRNavExpr occruence more than once"); - /* Outer offset (default 1 for PREV/NEXT) */ - if (nav->compound_offset_arg != NULL) - ExecInitExprRec(nav->compound_offset_arg, state, - &offset_values[1], &offset_isnulls[1]); - else + if (entry->nav == nav) { - offset_values[1] = Int64GetDatum(1); - offset_isnulls[1] = false; + entry->rprnavstate = rprnavstate; + rprnavstate->offset.isnull = false; + rprnavstate->offset.value = Int64GetDatum(entry->offset); + rprnavstate->compound_offset.isnull = false; + rprnavstate->compound_offset.value = + Int64GetDatum(entry->compound_offset); + + find_navexpr = true; } - - scratch.d.rpr_nav.offset_value = offset_values; - scratch.d.rpr_nav.offset_isnull = offset_isnulls; - } - else if (nav->offset_arg != NULL) - { - /* Simple navigation with explicit offset */ - Datum *offset_value = palloc_object(Datum); - bool *offset_isnull = palloc_object(bool); - - ExecInitExprRec(nav->offset_arg, state, - offset_value, offset_isnull); - scratch.d.rpr_nav.offset_value = offset_value; - scratch.d.rpr_nav.offset_isnull = offset_isnull; - } - else - { - /* Simple navigation with default offset */ - scratch.d.rpr_nav.offset_value = NULL; - scratch.d.rpr_nav.offset_isnull = NULL; } + /* Emit SET opcode: swap slot to target row */ + scratch.opcode = EEOP_RPR_NAV_SET; + scratch.d.rpr_nav.rprnavstate = rprnavstate; + ExprEvalPushStep(state, &scratch); /* Compile the argument expression normally */ @@ -1252,10 +1232,10 @@ ExecInitExprRec(Expr *node, ExprState *state, scratch.opcode = EEOP_RPR_NAV_RESTORE; scratch.resvalue = resv; scratch.resnull = resnull; - scratch.d.rpr_nav.winstate = winstate; + scratch.d.rpr_nav.rprnavstate = rprnavstate; get_typlenbyval(nav->resulttype, - &scratch.d.rpr_nav.resulttyplen, - &scratch.d.rpr_nav.resulttypbyval); + &rprnavstate->resulttyplen, + &rprnavstate->resulttypbyval); ExprEvalPushStep(state, &scratch); break; } diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c index ca28b7b152c..d8f4494fb5b 100644 --- a/src/backend/executor/execExprInterp.c +++ b/src/backend/executor/execExprInterp.c @@ -6011,34 +6011,6 @@ ExecAggPlainTransByRef(AggState *aggstate, AggStatePerTrans pertrans, MemoryContextSwitchTo(oldContext); } -/* - * Extract compound (outer) offset from step data. - * For compound nav, offset_value is an array: [0]=inner, [1]=outer. - * Returns the outer offset; errors on NULL or negative. - * Default is 1 (like PREV/NEXT implicit offset). - */ -static int64 -rpr_nav_get_compound_offset(ExprEvalStep *op) -{ - int64 val; - - Assert(op->d.rpr_nav.offset_value != NULL); - - if (op->d.rpr_nav.offset_isnull[1]) - ereport(ERROR, - errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), - errmsg("row pattern navigation offset must not be null")); - - val = DatumGetInt64(op->d.rpr_nav.offset_value[1]); - - if (val < 0) - ereport(ERROR, - errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("row pattern navigation offset must not be negative")); - - return val; -} - /* * Evaluate RPR navigation (PREV/NEXT/FIRST/LAST): swap slot to target row. * @@ -6052,50 +6024,36 @@ rpr_nav_get_compound_offset(ExprEvalStep *op) void ExecEvalRPRNavSet(ExprState *state, ExprEvalStep *op, ExprContext *econtext) { - WindowAggState *winstate = op->d.rpr_nav.winstate; + WindowAggState *winstate; int64 offset; + int64 compound_offset; int64 target_pos; TupleTableSlot *target_slot; + RPRNavState *rprnavstate = op->d.rpr_nav.rprnavstate; + + winstate = rprnavstate->winstate; /* Save current slot for later restore */ winstate->nav_saved_outertuple = econtext->ecxt_outertuple; - /* - * Determine the inner offset. NULL or negative offsets are errors per - * the SQL standard. - * - * Default offset when offset_arg is NULL: PREV/NEXT: 1 (standard 5.6.2) - * FIRST/LAST and compound: 0 for inner, 1 for outer - */ - if (op->d.rpr_nav.offset_value != NULL) - { - if (*op->d.rpr_nav.offset_isnull) - ereport(ERROR, - errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), - errmsg("row pattern navigation offset must not be null")); + if (rprnavstate->offset.isnull || rprnavstate->compound_offset.isnull) + ereport(ERROR, + errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), + errmsg("row pattern navigation offset must not be null")); - offset = DatumGetInt64(*op->d.rpr_nav.offset_value); + offset = DatumGetInt64(rprnavstate->offset.value); + compound_offset = DatumGetInt64(rprnavstate->compound_offset.value); - if (offset < 0) - ereport(ERROR, - errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("row pattern navigation offset must not be negative")); - } - else - { - /* Default offset: 1 for simple PREV/NEXT, 0 otherwise */ - if (op->d.rpr_nav.kind == RPR_NAV_PREV || - op->d.rpr_nav.kind == RPR_NAV_NEXT) - offset = 1; - else - offset = 0; - } + if (offset < 0 || compound_offset < 0) + ereport(ERROR, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("row pattern navigation offset must not be negative")); /* * Calculate target position based on navigation direction. On overflow, * use -1 so that ExecRPRNavGetSlot treats it as out of range. */ - switch (op->d.rpr_nav.kind) + switch (rprnavstate->rprnavexpr->kind) { case RPR_NAV_PREV: @@ -6129,7 +6087,6 @@ ExecEvalRPRNavSet(ExprState *state, ExprEvalStep *op, ExprContext *econtext) case RPR_NAV_PREV_FIRST: case RPR_NAV_NEXT_FIRST: { - int64 compound_offset; int64 inner_pos; /* Inner: match_start + offset */ @@ -6144,11 +6101,8 @@ ExecEvalRPRNavSet(ExprState *state, ExprEvalStep *op, ExprContext *econtext) break; } - /* Outer offset */ - compound_offset = rpr_nav_get_compound_offset(op); - /* Apply outer: PREV subtracts, NEXT adds */ - if (op->d.rpr_nav.kind == RPR_NAV_PREV_FIRST) + if (rprnavstate->rprnavexpr->kind == RPR_NAV_PREV_FIRST) { /* * inner_pos is in [0, currentpos] and compound_offset is @@ -6168,7 +6122,6 @@ ExecEvalRPRNavSet(ExprState *state, ExprEvalStep *op, ExprContext *econtext) case RPR_NAV_PREV_LAST: case RPR_NAV_NEXT_LAST: { - int64 compound_offset; int64 inner_pos; /* Inner: currentpos - offset */ @@ -6183,11 +6136,8 @@ ExecEvalRPRNavSet(ExprState *state, ExprEvalStep *op, ExprContext *econtext) break; } - /* Outer offset */ - compound_offset = rpr_nav_get_compound_offset(op); - /* Apply outer: PREV subtracts, NEXT adds */ - if (op->d.rpr_nav.kind == RPR_NAV_PREV_LAST) + if (rprnavstate->rprnavexpr->kind == RPR_NAV_PREV_LAST) { /* * inner_pos is in [nav_match_start, currentpos] (>= 0) @@ -6206,7 +6156,7 @@ ExecEvalRPRNavSet(ExprState *state, ExprEvalStep *op, ExprContext *econtext) break; default: elog(ERROR, "unrecognized RPR navigation kind: %d", - op->d.rpr_nav.kind); + (int) rprnavstate->rprnavexpr->kind); break; } @@ -6238,8 +6188,6 @@ ExecEvalRPRNavSet(ExprState *state, ExprEvalStep *op, ExprContext *econtext) * Evaluate RPR navigation: restore slot to original row. * * Restores econtext->ecxt_outertuple from the saved slot in winstate. - * When slot swap was elided (target == currentpos), this is a harmless - * no-op since saved and current slots are identical. * The caller is responsible for updating any local slot cache. * * For pass-by-reference result types, the result datum points into @@ -6253,12 +6201,19 @@ void ExecEvalRPRNavRestore(ExprState *state, ExprEvalStep *op, ExprContext *econtext) { - WindowAggState *winstate = op->d.rpr_nav.winstate; + WindowAggState *winstate = op->d.rpr_nav.rprnavstate->winstate; + + /* + * When slot swap was elided (target == currentpos), this is a harmless + * no-op since saved and current slots are identical. + */ + if (econtext->ecxt_outertuple == winstate->nav_saved_outertuple) + return; econtext->ecxt_outertuple = winstate->nav_saved_outertuple; /* Stabilize pass-by-ref result against nav_slot re-fetch */ - if (!op->d.rpr_nav.resulttypbyval && + if (!op->d.rpr_nav.rprnavstate->resulttypbyval && !*op->resnull) { MemoryContext oldContext; @@ -6266,7 +6221,7 @@ ExecEvalRPRNavRestore(ExprState *state, ExprEvalStep *op, oldContext = MemoryContextSwitchTo(econtext->ecxt_per_tuple_memory); *op->resvalue = datumCopy(*op->resvalue, false, - op->d.rpr_nav.resulttyplen); + op->d.rpr_nav.rprnavstate->resulttyplen); MemoryContextSwitchTo(oldContext); } } diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c index 0688d4e7b46..1361d03a293 100644 --- a/src/backend/executor/nodeWindowAgg.c +++ b/src/backend/executor/nodeWindowAgg.c @@ -47,7 +47,6 @@ #include "nodes/plannodes.h" #include "optimizer/clauses.h" #include "optimizer/optimizer.h" -#include "optimizer/rpr.h" #include "parser/parse_agg.h" #include "parser/parse_coerce.h" #include "utils/acl.h" @@ -177,6 +176,20 @@ typedef struct WindowStatePerAggData bool restart; /* need to restart this agg in this cycle? */ } WindowStatePerAggData; +typedef struct +{ + WindowAggState *winstate; + 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) */ + bool hasFirst; /* any FIRST-based nav found */ + bool validate; /* fail-closed on a null/negative offset? + * false at init (display only), true at + * execution */ +} EvalDefineOffsetsContext; + static void initialize_windowaggregate(WindowAggState *winstate, WindowStatePerFunc perfuncstate, WindowStatePerAgg peraggstate); @@ -248,9 +261,11 @@ static void update_reduced_frame(WindowObject winobj, int64 pos); /* Forward declarations - DEFINE row evaluation */ static bool rpr_prepare_row(WindowObject winobj, int64 pos, RPRVarMatch *varMatched); - -/* Forward declarations - navigation offset evaluation */ -static void eval_define_offsets(WindowAggState *winstate, List *defineClause); +static void build_define_offsets(WindowAggState *winstate, List *defineClause); +static void resolve_nav_offsets(WindowAggState *winstate); +static void resolve_one_nav(RPRNavOffsets *entry, EvalDefineOffsetsContext *context); +static bool RPRNavExpr_walker(Node *node, WindowAggState *winstate); +static void build_nav_offsets(RPRNavExpr *nav, WindowAggState *winstate); /* * Not null info bit array consists of 2-bit items @@ -1262,13 +1277,12 @@ prepare_tuplestore(WindowAggState *winstate) /* * Allocate mark and read pointers for RPR navigation. * - * If navMaxOffsetKind == RPR_NAV_OFFSET_FIXED, we advance the mark - * based on (currentpos - navMaxOffset) and optionally + * When the trim offset is FIXED we advance the mark based on + * (currentpos - navMaxOffset) and optionally * (nfaContext->matchStartRow + navFirstOffset), allowing - * tuplestore_trim() to free rows that are no longer reachable. - * - * RPR_NAV_OFFSET_NEEDS_EVAL is resolved at executor init; by this - * point it is either FIXED or RETAIN_ALL. + * tuplestore_trim() to free rows that are no longer reachable. A + * parameterized offset is still NEEDS_EVAL here and gets resolved at + * execution by resolve_nav_offsets(); RETAIN_ALL disables trim. */ winstate->nav_winobj->markptr = tuplestore_alloc_read_pointer(winstate->buffer, 0); @@ -2420,6 +2434,15 @@ ExecWindowAgg(PlanState *pstate) if (unlikely(winstate->all_first)) calculate_frame_offsets(pstate); + /* + * Resolve navigation offsets the same way, during first call (or after a + * rescan). Constant and bind-parameter offsets are already resolved at + * init; only a PARAM_EXEC offset reaches here, its value changing per + * scan. + */ + if (unlikely(winstate->navResolvePending)) + resolve_nav_offsets(winstate); + /* We need to loop as the runCondition or qual may filter out tuples */ for (;;) { @@ -3038,13 +3061,8 @@ 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; resolve any NEEDS_EVAL kinds */ - winstate->navMaxOffsetKind = node->navMaxOffsetKind; - winstate->navMaxOffset = node->navMaxOffset; - winstate->hasFirstNav = node->hasFirstNav; - winstate->navFirstOffsetKind = node->navFirstOffsetKind; - winstate->navFirstOffset = node->navFirstOffset; - eval_define_offsets(winstate, node->defineClause); + /* Build nav offset bookkeeping; values are resolved per scan */ + build_define_offsets(winstate, node->defineClause); /* Copy match_start dependency bitmapset for per-context evaluation */ winstate->defineMatchStartDependent = bms_copy(node->defineMatchStartDependent); @@ -3104,6 +3122,12 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags) else winstate->nfaVarMatched = NULL; winstate->all_first = true; + + /* + * Nav offsets are resolved (and validated) at execution, like frame + * offsets: on the first scan and after each rescan, for every RPR window. + */ + winstate->navResolvePending = (winstate->rprNavOffsets != NIL); winstate->partition_spooled = false; winstate->more_partitions = false; winstate->next_partition = true; @@ -3194,6 +3218,8 @@ ExecReScanWindowAgg(WindowAggState *node) node->status = WINDOWAGG_RUN; node->all_first = true; + /* offsets are re-resolved and re-validated at the next scan */ + node->navResolvePending = (node->rprNavOffsets != NIL); /* release tuplestore et al */ release_partition(node); @@ -3979,226 +4005,336 @@ put_notnull_info(WindowObject winobj, int64 pos, int argno, bool isnull) } /* - * eval_nav_offset_helper - * Pre-evaluate a navigation offset expression at executor init time, to - * bound how far navigation can reach (which sizes the frame trim). - * Returns the offset value, or 0 for a NULL or negative offset. + * eval_nav_offset + * Evaluate a pre-built row pattern navigation offset ExprState. * - * The offset is not validated here. A NULL or negative value is caught later, - * per row, on the navigation path that consumes it (see EEOP_RPR_NAV_SET in - * execExprInterp.c), which errors out before navigation produces any result; - * the trim sizing computed from such an offset is therefore never used, and 0 - * is returned as a harmless placeholder. + * The offset is a run-time constant (the parser rejects column references in a + * navigation offset), so it is evaluated once per scan -- when any parameter + * is bound. Returns the offset as an int64; a NULL or negative result is an + * error per the SQL standard (fail-closed, re-checked on every scan). */ static int64 -eval_nav_offset_helper(WindowAggState *winstate, Expr *offset_expr, - int64 defaultOffset) +eval_nav_offset(WindowAggState *winstate, ExprState *estate, bool validate) { ExprContext *econtext = winstate->ss.ps.ps_ExprContext; - ExprState *estate; Datum val; bool isnull; int64 offset; - if (offset_expr == NULL) - return defaultOffset; - - estate = ExecInitExpr(offset_expr, (PlanState *) winstate); val = ExecEvalExprSwitchContext(estate, econtext, &isnull); if (isnull) - return 0; + { + if (validate) + ereport(ERROR, + errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), + errmsg("row pattern navigation offset must not be null")); + return 0; /* placeholder for display; execution + * revalidates */ + } offset = DatumGetInt64(val); - if (offset < 0) - return 0; + + if (offset < 0 && validate) + ereport(ERROR, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("row pattern navigation offset must not be negative")); return offset; } -typedef struct -{ - WindowAggState *winstate; - 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; - /* - * 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: + * build_nav_offsets + * Create the per-navigation offset bookkeeping entry at executor init and + * compile its offset argument expression(s). * - * - 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 PG_INT64_MAX on - * overflow; always >= 0 so never updates minFirstOffset in practice) - * - * 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. + * The offsets are not evaluated here: a PARAM_EXEC offset (function inlining or + * a LATERAL reference) has no value until the node is (re)scanned. The + * concrete value is resolved per scan by resolve_nav_offsets(), mirroring how + * calculate_frame_offsets() handles the window frame bounds. */ static void -visit_nav_exec(NavTraversal *t, RPRNavExpr *nav) +build_nav_offsets(RPRNavExpr *nav, WindowAggState *winstate) { - EvalDefineOffsetsContext *context = (EvalDefineOffsetsContext *) t->data; + RPRNavOffsets *entry = palloc0_object(RPRNavOffsets); /* - * 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. + * Parser guarantee (mirrors compute_matchStartDependent): 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)); - /* Backward reach: PREV, LAST-with-offset */ - if (!context->maxOverflow) + entry->nav = nav; + if (nav->offset_arg != NULL) + entry->offset_state = ExecInitExpr(nav->offset_arg, + (PlanState *) winstate); + if (nav->compound_offset_arg != NULL) + entry->compound_offset_state = ExecInitExpr(nav->compound_offset_arg, + (PlanState *) winstate); + + winstate->rprNavOffsets = lappend(winstate->rprNavOffsets, entry); +} + +static bool +RPRNavExpr_walker(Node *node, WindowAggState *winstate) +{ + if (node == NULL) + return false; + if (IsA(node, RPRNavExpr)) + build_nav_offsets(castNode(RPRNavExpr, node), winstate); + + return expression_tree_walker(node, RPRNavExpr_walker, winstate); +} + +/* + * build_define_offsets + * At executor init, create one RPRNavOffsets entry per navigation in the + * DEFINE clause and compile its offset argument expressions. + * + * The concrete offset values -- and the tuplestore trim bounds derived from + * them -- are resolved later, per scan, by resolve_nav_offsets(). A non-RPR + * window has an empty DEFINE clause and falls through to the defaults. + */ +static void +build_define_offsets(WindowAggState *winstate, List *defineClause) +{ + EvalDefineOffsetsContext ctx; + + winstate->navMaxOffset = 0; + winstate->navMaxOffsetKind = RPR_NAV_OFFSET_FIXED; + winstate->hasFirstNav = false; + winstate->navFirstOffset = 0; + winstate->navFirstOffsetKind = RPR_NAV_OFFSET_FIXED; + winstate->rprNavOffsets = NIL; + + if (defineClause == NIL) + return; + + foreach_node(TargetEntry, te, defineClause) { - int64 reach = 0; - bool gotReach = false; + RPRNavExpr_walker((Node *) te->expr, winstate); + } - 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) + /* + * Resolve the offsets that are already constant at plan time, so EXPLAIN + * (which never executes, hence never reaches resolve_nav_offsets()) shows + * the real trim bounds. A parameterized offset (PARAM_EXTERN under a + * generic plan, or a PARAM_EXEC) has no value yet and is left for + * resolve_nav_offsets() to bound per scan. + */ + ctx.winstate = winstate; + ctx.maxOffset = 0; + ctx.maxOverflow = false; + ctx.minFirstOffset = PG_INT64_MAX; + ctx.hasFirst = false; + ctx.validate = false; /* init resolution is for EXPLAIN display only */ + + foreach_ptr(RPRNavOffsets, entry, winstate->rprNavOffsets) + { + RPRNavExpr *nav = entry->nav; + bool is_const; + + is_const = (nav->offset_arg == NULL || IsA(nav->offset_arg, Const)) && + (nav->compound_offset_arg == NULL || + IsA(nav->compound_offset_arg, Const)); + + if (is_const) { - reach = eval_nav_offset_helper(context->winstate, - nav->offset_arg, 0); - gotReach = true; + /* constant offset: resolvable now, for EXPLAIN and the scan */ + resolve_one_nav(entry, &ctx); } - else if (nav->kind == RPR_NAV_PREV_LAST || - nav->kind == RPR_NAV_NEXT_LAST) + 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); - - if (nav->kind == RPR_NAV_PREV_LAST) - { - if (pg_add_s64_overflow(inner, outer, &reach)) - context->maxOverflow = true; - else - gotReach = true; - } - else + /* + * A parameterized offset (a bind PARAM_EXTERN or, via + * SRF/function inlining, a correlated PARAM_EXEC) has no + * dependable value at init. Like a window frame offset it is + * resolved at execution by resolve_nav_offsets(), and EXPLAIN + * shows "runtime". + */ + if (nav->kind == RPR_NAV_PREV || nav->kind == RPR_NAV_LAST || + nav->kind == RPR_NAV_PREV_LAST || nav->kind == RPR_NAV_NEXT_LAST) + winstate->navMaxOffsetKind = RPR_NAV_OFFSET_NEEDS_EVAL; + if (nav->kind == RPR_NAV_FIRST || nav->kind == RPR_NAV_PREV_FIRST || + nav->kind == RPR_NAV_NEXT_FIRST) { - reach = Max(inner - outer, 0); - gotReach = true; + ctx.hasFirst = true; + winstate->navFirstOffsetKind = RPR_NAV_OFFSET_NEEDS_EVAL; } } - - if (gotReach) - context->maxOffset = Max(context->maxOffset, reach); } - /* Forward reach from match_start: FIRST, compound PREV_FIRST/NEXT_FIRST */ - if (nav->kind == RPR_NAV_FIRST) + if (ctx.maxOverflow) { - int64 reach; + /* + * a const/bind overflow forces retain-all, unless a param already + * made this dimension "runtime" (NEEDS_EVAL wins for display) + */ + if (winstate->navMaxOffsetKind != RPR_NAV_OFFSET_NEEDS_EVAL) + winstate->navMaxOffsetKind = RPR_NAV_OFFSET_RETAIN_ALL; + } + else + winstate->navMaxOffset = ctx.maxOffset; - reach = eval_nav_offset_helper(context->winstate, - nav->offset_arg, 0); - context->minFirstOffset = Min(context->minFirstOffset, reach); + winstate->hasFirstNav = ctx.hasFirst; + if (ctx.hasFirst && ctx.minFirstOffset < PG_INT64_MAX) + winstate->navFirstOffset = ctx.minFirstOffset; + else if (ctx.hasFirst) + winstate->navFirstOffset = PG_INT64_MAX; +} + +/* + * resolve_one_nav + * Evaluate one navigation's offset(s) for the current scan, pin the + * resolved values into its RPRNavState, and accumulate the backward and + * forward reach used to size the tuplestore trim. + */ +static void +resolve_one_nav(RPRNavOffsets *entry, EvalDefineOffsetsContext *context) +{ + RPRNavExpr *nav = entry->nav; + int64 inner; + int64 outer; + + /* Inner offset */ + if (entry->offset_state != NULL) + inner = eval_nav_offset(context->winstate, entry->offset_state, + context->validate); + else if (nav->kind == RPR_NAV_PREV || nav->kind == RPR_NAV_NEXT) + inner = 1; + else + inner = 0; + + /* Outer (compound) offset */ + if (entry->compound_offset_state != NULL) + outer = eval_nav_offset(context->winstate, entry->compound_offset_state, + context->validate); + else + outer = 1; + + entry->offset = inner; + entry->compound_offset = outer; + + /* + * Pin the resolved values into the compiled navigation's RPRNavState, so + * ExecEvalRPRNavSet() reads this scan's constant instead of re-evaluating + * the offset per row. + */ + if (entry->rprnavstate != NULL) + { + entry->rprnavstate->offset.isnull = false; + entry->rprnavstate->offset.value = Int64GetDatum(inner); + entry->rprnavstate->compound_offset.isnull = false; + entry->rprnavstate->compound_offset.value = Int64GetDatum(outer); } - else if (nav->kind == RPR_NAV_PREV_FIRST || - nav->kind == RPR_NAV_NEXT_FIRST) + + /* Backward reach: PREV, LAST-with-offset */ + if (!context->maxOverflow && + (nav->kind == RPR_NAV_PREV || + nav->kind == RPR_NAV_LAST || + nav->kind == RPR_NAV_PREV_LAST || + nav->kind == RPR_NAV_NEXT_LAST)) { - 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; + int64 reach = 0; - if (nav->kind == RPR_NAV_PREV_FIRST) + if (nav->kind == RPR_NAV_PREV || nav->kind == RPR_NAV_LAST) + reach = inner; + else if (nav->kind == RPR_NAV_PREV_LAST) { - /* - * reach = inner - outer. Both are non-negative, so the result >= - * -PG_INT64_MAX, which cannot underflow int64. - */ - reach = inner - outer; + if (pg_add_s64_overflow(inner, outer, &reach)) + context->maxOverflow = true; } + else + reach = Max(inner - outer, 0); + + if (!context->maxOverflow) + context->maxOffset = Max(context->maxOffset, reach); + } + + /* Forward reach from match_start: FIRST, compound PREV_FIRST/NEXT_FIRST */ + if (nav->kind == RPR_NAV_FIRST || + nav->kind == RPR_NAV_PREV_FIRST || + nav->kind == RPR_NAV_NEXT_FIRST) + { + int64 reach; + + context->hasFirst = true; + + if (nav->kind == RPR_NAV_FIRST) + reach = inner; + else if (nav->kind == RPR_NAV_PREV_FIRST) + reach = inner - outer; /* both >= 0, cannot underflow int64 */ else { - /* - * 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 PG_INT64_MAX on overflow. - */ + /* NEXT_FIRST: inner + outer, always >= 0; clamp on overflow */ if (pg_add_s64_overflow(inner, outer, &reach)) reach = PG_INT64_MAX; } + context->minFirstOffset = Min(context->minFirstOffset, reach); } } /* - * eval_define_offsets - * Evaluate non-constant nav offsets at executor init time. + * resolve_nav_offsets + * Resolve every navigation offset for the current scan and store the + * tuplestore trim bounds in the WindowAggState. * - * 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. - * - * On backward-reach overflow, sets navMaxOffsetKind to - * RPR_NAV_OFFSET_RETAIN_ALL so that tuplestore trim is disabled for - * backward navigation. + * Called from ExecWindowAgg on the first call and after every rescan -- the + * same place calculate_frame_offsets() resolves the window frame bounds. By + * then every parameter (PARAM_EXTERN and PARAM_EXEC alike) is bound, and the + * offset is a run-time constant, so a single evaluation per scan is correct. + * This keeps the trim finite for a parameterized offset (no retain-all) and + * revalidates it (fail-closed) on each scan. */ static void -eval_define_offsets(WindowAggState *winstate, List *defineClause) +resolve_nav_offsets(WindowAggState *winstate) { EvalDefineOffsetsContext ctx; - NavTraversal trav; - bool needsMax = (winstate->navMaxOffsetKind == RPR_NAV_OFFSET_NEEDS_EVAL); - bool needsFirst = (winstate->hasFirstNav && - winstate->navFirstOffsetKind == RPR_NAV_OFFSET_NEEDS_EVAL); - if (!needsMax && !needsFirst) + /* Servicing the request now; clear the per-scan pending flag */ + winstate->navResolvePending = false; + + winstate->navMaxOffset = 0; + winstate->navMaxOffsetKind = RPR_NAV_OFFSET_FIXED; + winstate->hasFirstNav = false; + winstate->navFirstOffset = 0; + winstate->navFirstOffsetKind = RPR_NAV_OFFSET_FIXED; + + if (winstate->rprNavOffsets == NIL) return; ctx.winstate = winstate; ctx.maxOffset = 0; ctx.maxOverflow = false; ctx.minFirstOffset = PG_INT64_MAX; + ctx.hasFirst = false; + ctx.validate = true; /* execution: fail-closed on null/negative */ - trav.visit = visit_nav_exec; - trav.data = &ctx; - - foreach_node(TargetEntry, te, defineClause) + foreach_ptr(RPRNavOffsets, entry, winstate->rprNavOffsets) { - nav_traversal_walker((Node *) te->expr, &trav); + resolve_one_nav(entry, &ctx); } - if (needsMax) - { - if (ctx.maxOverflow) - { - winstate->navMaxOffsetKind = RPR_NAV_OFFSET_RETAIN_ALL; - winstate->navMaxOffset = 0; - } - else - { - winstate->navMaxOffsetKind = RPR_NAV_OFFSET_FIXED; - winstate->navMaxOffset = ctx.maxOffset; - } - } + /* + * Backward (PREV/LAST) reach. On int64 overflow the lookback cannot be + * bounded, so mark the dimension RETAIN_ALL; advance_nav_mark() reads it + * to disable tuplestore trim. + */ + if (ctx.maxOverflow) + winstate->navMaxOffsetKind = RPR_NAV_OFFSET_RETAIN_ALL; + else + winstate->navMaxOffset = ctx.maxOffset; + + /* Forward (FIRST) reach; never needs a retain-all sentinel */ + winstate->hasFirstNav = ctx.hasFirst; - if (needsFirst) + if (ctx.hasFirst) { - winstate->navFirstOffsetKind = RPR_NAV_OFFSET_FIXED; if (ctx.minFirstOffset < PG_INT64_MAX) winstate->navFirstOffset = ctx.minFirstOffset; else @@ -4387,13 +4523,10 @@ advance_nav_mark(WindowAggState *winstate, int64 currentPos) if (winstate->nav_winobj == NULL) return; - /* RETAIN_ALL disables trim for the backward (PREV/LAST) dimension */ + /* RETAIN_ALL (offset overflow) disables trim for the backward dimension */ if (winstate->navMaxOffsetKind == RPR_NAV_OFFSET_RETAIN_ALL) return; - /* navMax is FIXED here: NEEDS_EVAL resolved, RETAIN_ALL returned */ - Assert(winstate->navMaxOffsetKind == RPR_NAV_OFFSET_FIXED); - if (currentPos > winstate->navMaxOffset) navmarkpos = currentPos - winstate->navMaxOffset; else @@ -4403,9 +4536,6 @@ advance_nav_mark(WindowAggState *winstate, int64 currentPos) { int64 firstreach; - /* navFirst is always FIXED; it never takes RETAIN_ALL */ - Assert(winstate->navFirstOffsetKind == RPR_NAV_OFFSET_FIXED); - /* * Head context has the smallest matchStartRow (contexts appended in * nondecreasing order), so bounding by it covers every FIRST reach. diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c index 1808ccb3d6f..a2831b1eb78 100644 --- a/src/backend/optimizer/plan/createplan.c +++ b/src/backend/optimizer/plan/createplan.c @@ -299,9 +299,6 @@ static WindowAgg *make_windowagg(List *tlist, WindowClause *wc, List *runCondition, RPRPattern *compiledPattern, 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, @@ -2484,84 +2481,30 @@ create_minmaxagg_plan(PlannerInfo *root, MinMaxAggPath *best_path) * * The driver sets curVarIdx to the index of the variable being walked * before each invocation; the walker uses it to populate matchStartDependent. + * + * XXX: TODO, the above comments need change. */ typedef struct DefineMetadataContext { - int64 maxOffset; /* max PREV/LAST backward offset (>= 0) */ - bool maxNeedsEval; /* non-constant PREV/LAST offset found */ - bool maxOverflow; /* constant offset overflow detected */ - int64 firstOffset; /* min FIRST offset (may be negative for - * PREV_FIRST) */ - bool hasFirst; /* any FIRST node found */ - bool firstNeedsEval; /* non-constant FIRST offset found */ 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. - * If expr is NULL, returns defaultOffset. - * Returns true if constant, false if non-constant (Param, cast, etc.). - */ -static bool -extract_const_offset(Expr *expr, int64 defaultOffset, int64 *result) -{ - if (expr == NULL) - { - *result = defaultOffset; - return true; - } - - if (IsA(expr, Const)) - { - Const *c = (Const *) expr; - - if (c->constisnull) - *result = 0; /* runtime error; safe placeholder */ - else - { - *result = DatumGetInt64(c->constvalue); - if (*result < 0) - *result = 0; /* runtime error; safe placeholder */ - } - return true; - } - - return false; /* non-constant */ -} - -/* - * 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) + * compute_matchStartDependent * - * 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 + * 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. * - * 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. + * Classification uses only the outer nav kind: parser nesting restrictions + * prevent FIRST/LAST inside a PREV/NEXT value subexpression. */ static void -visit_nav_plan(NavTraversal *t, RPRNavExpr *nav) +compute_matchStartDependent(RPRNavExpr *nav, DefineMetadataContext *context) { - DefineMetadataContext *context = (DefineMetadataContext *) t->data; - /* * Parser guarantee: by the time the planner sees a DEFINE expression, * compound nesting has been flattened into a single RPRNavExpr and any @@ -2574,130 +2517,6 @@ visit_nav_plan(NavTraversal *t, RPRNavExpr *nav) Assert(nav->compound_offset_arg == NULL || !IsA(nav->compound_offset_arg, 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)) - { - if (!context->maxNeedsEval) - { - int64 offset; - - /* - * default 1 is for PREV; the guarded LAST sub-case never uses it. - */ - 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; - - if (!context->firstNeedsEval) - { - int64 offset; - - 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) - { - if (!context->maxNeedsEval) - { - 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 (nav->kind == RPR_NAV_PREV_LAST) - { - if (pg_add_s64_overflow(inner, outer, &reach)) - context->maxOverflow = true; - else - context->maxOffset = Max(context->maxOffset, reach); - } - else - { - 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) - { - context->hasFirst = true; - - if (!context->firstNeedsEval) - { - 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 (nav->kind == RPR_NAV_PREV_FIRST) - { - /* - * reach = inner - outer. Both are non-negative, so the - * result >= -PG_INT64_MAX, which cannot underflow int64. - * No overflow check needed. - */ - reach = inner - outer; - } - else - { - /* - * 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 - * PG_INT64_MAX on overflow. - */ - if (pg_add_s64_overflow(inner, outer, &reach)) - reach = PG_INT64_MAX; - } - - context->firstOffset = Min(context->firstOffset, reach); - } - else - context->firstNeedsEval = true; - } - } - /* * Match-start dependency: classify the outer nav kind. A constant * LAST(x, 0) is conservatively included (offset_arg is a non-NULL Const), @@ -2716,90 +2535,47 @@ visit_nav_plan(NavTraversal *t, RPRNavExpr *nav) context->curVarIdx); } +static bool +RPRNavExpr_walker(Node *node, DefineMetadataContext *ctx) +{ + if (node == NULL) + return false; + if (IsA(node, RPRNavExpr)) + compute_matchStartDependent(castNode(RPRNavExpr, node), ctx); + + return expression_tree_walker(node, RPRNavExpr_walker, ctx); +} + /* * compute_define_metadata - * Compute navigation offsets and match_start dependency for the - * DEFINE clause in a single pass per variable. + * Classify which DEFINE variables depend on the match start. + * + * Walks each DEFINE variable expression once and returns the set of variable + * indices whose navigation reaches 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, and + * their presence disqualifies the pattern from context absorption. * - * 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. + * Navigation offsets for tuplestore trim are not computed here; they are + * resolved at executor init (eval_define_offsets), which can evaluate + * non-constant offsets that the planner cannot fold. */ static void -compute_define_metadata(List *defineClause, - RPRNavOffsetKind *maxKind, int64 *maxResult, - bool *hasFirst, - RPRNavOffsetKind *firstKind, int64 *firstResult, - Bitmapset **matchStartDependent) +compute_define_metadata(List *defineClause, Bitmapset **matchStartDependent) { DefineMetadataContext ctx; - NavTraversal trav; - - ctx.maxOffset = 0; - ctx.maxNeedsEval = false; - ctx.maxOverflow = false; - ctx.firstOffset = PG_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_node(TargetEntry, te, defineClause) { - nav_traversal_walker((Node *) te->expr, &trav); - ctx.curVarIdx++; - } - - *matchStartDependent = ctx.matchStartDependent; + ctx.curVarIdx = foreach_current_index(te); - /* Max backward offset */ - if (ctx.maxOverflow) - { - *maxKind = RPR_NAV_OFFSET_RETAIN_ALL; - *maxResult = 0; - } - else if (ctx.maxNeedsEval) - { - *maxKind = RPR_NAV_OFFSET_NEEDS_EVAL; - *maxResult = 0; - } - else - { - *maxKind = RPR_NAV_OFFSET_FIXED; - *maxResult = ctx.maxOffset; + RPRNavExpr_walker((Node *) te->expr, &ctx); } - /* First offset (can be negative for compound PREV_FIRST) */ - *hasFirst = ctx.hasFirst; - if (ctx.hasFirst) - { - if (ctx.firstNeedsEval) - { - *firstKind = RPR_NAV_OFFSET_NEEDS_EVAL; - *firstResult = 0; - } - else - { - *firstKind = RPR_NAV_OFFSET_FIXED; - *firstResult = ctx.firstOffset; /* may be negative; PG_INT64_MAX - * if overflowed */ - } - } - else - { - *firstKind = RPR_NAV_OFFSET_FIXED; - *firstResult = 0; - } + *matchStartDependent = ctx.matchStartDependent; } /* @@ -2828,12 +2604,6 @@ create_windowagg_plan(PlannerInfo *root, WindowAggPath *best_path) ListCell *lc; 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; - /* * Choice of tlist here is motivated by the fact that WindowAgg will be @@ -2888,15 +2658,11 @@ create_windowagg_plan(PlannerInfo *root, WindowAggPath *best_path) if (wc->rpPattern) { /* - * Walk DEFINE once: collect nav offsets (for tuplestore trim) and the - * bitmapset of match_start-dependent variables (for absorption - * suppression in buildRPRPattern). + * Classify which DEFINE variables depend on match_start (for + * absorption suppression in buildRPRPattern). Nav offsets for + * tuplestore trim are resolved later, at executor init. */ - compute_define_metadata(wc->defineClause, - &navMaxOffsetKind, &navMaxOffset, - &hasFirstNav, - &navFirstOffsetKind, &navFirstOffset, - &matchStartDependent); + compute_define_metadata(wc->defineClause, &matchStartDependent); /* Compile and optimize RPR patterns */ compiledPattern = buildRPRPattern(wc->rpPattern, @@ -2920,11 +2686,6 @@ create_windowagg_plan(PlannerInfo *root, WindowAggPath *best_path) best_path->runCondition, compiledPattern, matchStartDependent, - navMaxOffsetKind, - navMaxOffset, - hasFirstNav, - navFirstOffsetKind, - navFirstOffset, best_path->qual, best_path->topwindow, subplan); @@ -7025,9 +6786,6 @@ make_windowagg(List *tlist, WindowClause *wc, List *runCondition, RPRPattern *compiledPattern, Bitmapset *defineMatchStartDependent, - RPRNavOffsetKind navMaxOffsetKind, int64 navMaxOffset, - bool hasFirstNav, - RPRNavOffsetKind navFirstOffsetKind, int64 navFirstOffset, List *qual, bool topWindow, Plan *lefttree) { WindowAgg *node = makeNode(WindowAgg); @@ -7065,13 +6823,6 @@ make_windowagg(List *tlist, WindowClause *wc, /* Store pre-computed match_start dependency bitmapset */ node->defineMatchStartDependent = defineMatchStartDependent; - /* 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; plan->righttree = NULL; diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c index 3462e9a922e..b529be44b14 100644 --- a/src/backend/optimizer/plan/rpr.c +++ b/src/backend/optimizer/plan/rpr.c @@ -2165,36 +2165,3 @@ buildRPRPattern(RPRPatternNode *pattern, List *defineClause, 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/optimizer/plan/subselect.c b/src/backend/optimizer/plan/subselect.c index 6aa8971c95d..20422a48a8c 100644 --- a/src/backend/optimizer/plan/subselect.c +++ b/src/backend/optimizer/plan/subselect.c @@ -3050,6 +3050,8 @@ finalize_plan(PlannerInfo *root, Plan *plan, &context); finalize_primnode(((WindowAgg *) plan)->endOffset, &context); + finalize_primnode((Node *) ((WindowAgg *) plan)->defineClause, + &context); break; case T_Gather: diff --git a/src/include/executor/execExpr.h b/src/include/executor/execExpr.h index db66ebe313c..ac1b0be0c2a 100644 --- a/src/include/executor/execExpr.h +++ b/src/include/executor/execExpr.h @@ -702,13 +702,7 @@ typedef struct ExprEvalStep /* for EEOP_RPR_NAV_SET / EEOP_RPR_NAV_RESTORE */ struct { - WindowAggState *winstate; - RPRNavKind kind; /* navigation kind (simple or compound) */ - Datum *offset_value; /* offset value(s), or NULL */ - bool *offset_isnull; /* offset null flag(s) */ - /* For compound nav: offset_value[0] = inner, [1] = outer */ - int16 resulttyplen; /* RESTORE: result type length */ - bool resulttypbyval; /* RESTORE: result pass-by-value? */ + RPRNavState *rprnavstate; } rpr_nav; /* for EEOP_AGG_*DESERIALIZE */ diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index 84844a2ea6c..6a3216bbf8d 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -70,6 +70,7 @@ typedef struct TupleTableSlot TupleTableSlot; typedef struct TupleTableSlotOps TupleTableSlotOps; typedef struct WalUsage WalUsage; typedef struct WorkerNodeInstrumentation WorkerNodeInstrumentation; +typedef struct WindowAggState WindowAggState; /* ---------------- @@ -1074,6 +1075,43 @@ typedef struct SubPlanState ExprState *cur_eq_comp; /* equality comparator for LHS vs. table */ } SubPlanState; +typedef struct RPRNavState +{ + NodeTag type; + + WindowAggState *winstate; + RPRNavExpr *rprnavexpr; + + /* + * Resolved navigation offsets for this execution, captured from + * winstate->rprNavOffsets at expression compile time. These live in + * executor state (not on the RPRNavExpr) because plan trees are read-only + * and may be shared by concurrent executions. + */ + NullableDatum offset; /* inner offset */ + NullableDatum compound_offset; /* outer offset for compound nav */ + int16 resulttyplen; /* RESTORE: result type length */ + bool resulttypbyval; /* RESTORE: result pass-by-value? */ +} RPRNavState; + +/* + * RPRNavOffsets - one entry of WindowAggState.rprNavOffsets + * + * Associates an RPRNavExpr from the (read-only) plan tree with its offsets + * as resolved by eval_define_offsets() at executor startup. The plan node + * pointer serves as lookup key; ExecInitExprRec copies the values into the + * RPRNavState of each compiled navigation expression. + */ +typedef struct RPRNavOffsets +{ + RPRNavExpr *nav; /* plan-tree node (lookup key) */ + int64 offset; /* resolved inner offset */ + int64 compound_offset; /* resolved outer offset */ + ExprState *offset_state; /* inner offset expr, evaluated once per scan */ + ExprState *compound_offset_state; /* outer (compound) offset expr */ + RPRNavState *rprnavstate; /* back-link, to pin resolved values per scan */ +} RPRNavOffsets; + /* * DomainConstraintState - one item to check during CoerceToDomain * @@ -2741,14 +2779,27 @@ typedef struct WindowAggState TupleTableSlot *temp_slot_2; /* RPR navigation */ - RPRNavOffsetKind navMaxOffsetKind; /* status of navMaxOffset */ + + /* + * per-execution resolved nav offsets: list of RPRNavOffsets, keyed by + * RPRNavExpr pointer; built by eval_define_offsets() + */ + List *rprNavOffsets; int64 navMaxOffset; /* max backward nav offset (when FIXED) */ + RPRNavOffsetKind navMaxOffsetKind; /* status of navMaxOffset */ bool hasFirstNav; /* FIRST() present in DEFINE */ - RPRNavOffsetKind navFirstOffsetKind; /* status of navFirstOffset */ int64 navFirstOffset; /* min FIRST() offset (when FIXED) */ + RPRNavOffsetKind navFirstOffsetKind; /* status of navFirstOffset */ + bool navResolvePending; /* nav offsets need (re)resolving at the + * next ExecWindowAgg call; set at init + * and rescan, cleared by + * resolve_nav_offsets() */ struct WindowObjectData *nav_winobj; /* winobj for RPR */ int64 nav_slot_pos; /* position cached in nav_slot, or -1 */ - TupleTableSlot *nav_slot; /* slot for PREV/NEXT/FIRST/LAST target row */ + TupleTableSlot *nav_slot; /* slot holding the resolved navigation target + * row (simple or compound + * PREV/NEXT/FIRST/LAST) */ + TupleTableSlot *nav_saved_outertuple; /* saved slot during nav swap */ TupleTableSlot *nav_null_slot; /* all NULL slot */ int64 nav_match_start; /* match_start for FIRST/LAST nav */ diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index fb3496c5476..ed3328b576d 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -594,21 +594,6 @@ typedef enum RPSkipTo ST_PAST_LAST_ROW, /* SKIP TO PAST LAST ROW */ } RPSkipTo; -/* - * RPRNavOffsetKind - status of navigation offset for tuplestore trim. - * - * The planner computes navMaxOffset/navFirstOffset for tuplestore mark - * optimization. This enum tracks whether the value is a resolved constant, - * needs runtime evaluation, or cannot be determined (retain all rows). - */ -typedef enum RPRNavOffsetKind -{ - RPR_NAV_OFFSET_FIXED, /* resolved constant; use the offset value */ - RPR_NAV_OFFSET_NEEDS_EVAL, /* non-constant offset; evaluate at executor - * init */ - RPR_NAV_OFFSET_RETAIN_ALL, /* cannot determine; retain all rows (no trim) */ -} RPRNavOffsetKind; - /* * RPRPatternNodeType - Row Pattern Recognition pattern node types */ @@ -620,6 +605,18 @@ typedef enum RPRPatternNodeType RPR_PATTERN_GROUP, /* group (parentheses) */ } RPRPatternNodeType; +/* + * RPRNavOffsetKind - status of a resolved navigation trim offset + * (WindowAggState.navMaxOffset / navFirstOffset) + */ +typedef enum RPRNavOffsetKind +{ + RPR_NAV_OFFSET_FIXED, /* resolved constant; use the offset value */ + RPR_NAV_OFFSET_NEEDS_EVAL, /* non-constant offset; shows "runtime", + * resolved per scan */ + RPR_NAV_OFFSET_RETAIN_ALL, /* offset overflow; retain all rows (no trim) */ +} RPRNavOffsetKind; + /* * RPR_QUANTITY_INF is the sentinel stored in RPRPatternNode.max for an * unbounded quantifier (*, +, or {n,}); later stages treat this max as diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h index d8fc3615246..904d71ca643 100644 --- a/src/include/nodes/plannodes.h +++ b/src/include/nodes/plannodes.h @@ -1415,27 +1415,6 @@ typedef struct WindowAgg */ Bitmapset *defineMatchStartDependent; - /* - * Navigation offset status and values for tuplestore mark optimization. - * See RPRNavOffsetKind in nodes/parsenodes.h. - * - * navMaxOffset: maximum backward reach from currentpos (contributed by - * PREV, LAST-with-offset, compound PREV_LAST/NEXT_LAST). Only valid when - * navMaxOffsetKind == RPR_NAV_OFFSET_FIXED. - * - * navFirstOffset: minimum forward offset from match_start (contributed by - * FIRST, compound PREV_FIRST/NEXT_FIRST). Can be negative for compound - * PREV_FIRST. Only valid when navFirstOffsetKind == RPR_NAV_OFFSET_FIXED - * and hasFirstNav == true. - */ - RPRNavOffsetKind navMaxOffsetKind; - int64 navMaxOffset; - - /* true if FIRST-based navigation (FIRST, PREV_FIRST, NEXT_FIRST) is used */ - bool hasFirstNav; - RPRNavOffsetKind navFirstOffsetKind; - int64 navFirstOffset; - /* * false for all apart from the WindowAgg that's closest to the root of * the plan diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 962b843f8d2..e9c3bcdc914 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -668,25 +668,28 @@ typedef struct WindowFuncRunCondition */ typedef enum RPRNavKind { - RPR_NAV_PREV, - RPR_NAV_NEXT, - RPR_NAV_FIRST, - RPR_NAV_LAST, + RPR_NAV_PREV, /* offset default: 1 */ + RPR_NAV_NEXT, /* offset default: 1 */ + RPR_NAV_FIRST, /* offset default: 0 */ + RPR_NAV_LAST, /* offset default: 0 */ /* compound: outer(inner(arg)) */ - RPR_NAV_PREV_FIRST, - RPR_NAV_PREV_LAST, - RPR_NAV_NEXT_FIRST, - RPR_NAV_NEXT_LAST, + RPR_NAV_PREV_FIRST, /* (offset, compound_offset) default: (0, 1) */ + RPR_NAV_PREV_LAST, /* (offset, compound_offset) default: (0, 1) */ + RPR_NAV_NEXT_FIRST, /* (offset, compound_offset) default: (0, 1) */ + RPR_NAV_NEXT_LAST, /* (offset, compound_offset) default: (0, 1) */ } RPRNavKind; typedef struct RPRNavExpr { Expr xpr; - RPRNavKind kind; /* navigation kind */ - Expr *arg; /* argument expression */ - Expr *offset_arg; /* offset expression, or NULL for default */ - Expr *compound_offset_arg; /* outer offset for compound nav, or - * NULL if simple */ + RPRNavKind kind; + /* argument expression */ + Expr *arg; + /* offset expression */ + Expr *offset_arg; + /* outer offset for compound navigation */ + Expr *compound_offset_arg; + /* result type (same as arg's type) */ Oid resulttype pg_node_attr(query_jumble_ignore); /* OID of collation of result */ diff --git a/src/include/optimizer/rpr.h b/src/include/optimizer/rpr.h index 800f547cbb1..20847d89a4a 100644 --- a/src/include/optimizer/rpr.h +++ b/src/include/optimizer/rpr.h @@ -84,26 +84,4 @@ extern RPRPattern *buildRPRPattern(RPRPatternNode *pattern, List *defineClause, 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 /* RPR_H */ diff --git a/src/test/regress/expected/rpr.out b/src/test/regress/expected/rpr.out index 6080398f583..71ac279f6fc 100644 --- a/src/test/regress/expected/rpr.out +++ b/src/test/regress/expected/rpr.out @@ -2326,6 +2326,24 @@ SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS ( DEFINE A AS NEXT(LAST(val), -1) IS NULL ); ERROR: row pattern navigation offset must not be negative +-- Compound: an out-of-range inner offset must not skip validation of an +-- illegal outer offset. The early exit on the inner used to let a NULL or +-- negative outer offset through, so the same illegal query passed or errored +-- depending on the data; both must error now. +SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE A AS TRUE, B AS PREV(FIRST(val, 99), NULL::int8) IS NULL +); +ERROR: row pattern navigation offset must not be null +SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE A AS TRUE, B AS NEXT(LAST(val, 99), -1) IS NULL +); +ERROR: row pattern navigation offset must not be negative -- Outer offset overflows int64: target position out of range -> NULL. -- Plain NEXT(val, INT64_MAX): currentpos + INT64_MAX overflows. SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS ( diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out index 54fa490a4e1..4ae8d81445b 100644 --- a/src/test/regress/expected/rpr_base.out +++ b/src/test/regress/expected/rpr_base.out @@ -1743,6 +1743,57 @@ DETAIL: Pattern has 32768 elements, maximum is 32767. -- ============================================================ -- Navigation Functions Tests (PREV / NEXT / FIRST / LAST) -- ============================================================ +CREATE TEMP TABLE rpr_nav0 (id int, v int); +INSERT INTO rpr_nav0 SELECT g, g*10 FROM generate_series(1, 5) g; +-- Two concurrently open portals of the SAME cached generic plan, with different +-- offset parameters. +-- +-- The parameterized cursor 'c' compiles to one plpgsql statement -> one SPI +-- cached plan. The recursive call OPENs a second portal of that same plan +-- (with a different offset) while the outer portal is already started but has +-- not yet FETCHed. +CREATE OR REPLACE FUNCTION rpr_nested(p_off int, depth int) +RETURNS SETOF text LANGUAGE plpgsql AS $$ +DECLARE + c CURSOR (o int) FOR + SELECT id, count(*) OVER w AS cnt + FROM rpr_nav0 + WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A) + DEFINE A AS PREV(v, o) IS NULL); + r record; +BEGIN + OPEN c(p_off); + IF depth > 0 THEN + RETURN QUERY SELECT * FROM rpr_nested(p_off + 2, depth - 1); + END IF; + + LOOP + FETCH c INTO r; + EXIT WHEN NOT FOUND; + RETURN NEXT format('off=%s id=%s cnt=%s', p_off, r.id, r.cnt); + END LOOP; + CLOSE c; +END $$; +SET plan_cache_mode = force_generic_plan; +SELECT * FROM rpr_nested(1, 1); + rpr_nested +------------------ + off=3 id=1 cnt=1 + off=3 id=2 cnt=1 + off=3 id=3 cnt=1 + off=3 id=4 cnt=0 + off=3 id=5 cnt=0 + off=1 id=1 cnt=1 + off=1 id=2 cnt=0 + off=1 id=3 cnt=0 + off=1 id=4 cnt=0 + off=1 id=5 cnt=0 +(10 rows) + +RESET plan_cache_mode; +DROP FUNCTION rpr_nested(int, int); CREATE TABLE rpr_nav (id INT, val INT); INSERT INTO rpr_nav VALUES (1, 10), (2, 20), (3, 15), (4, 25), (5, 30); diff --git a/src/test/regress/expected/rpr_explain.out b/src/test/regress/expected/rpr_explain.out index d83146d77cf..eaf5e8e1f62 100644 --- a/src/test/regress/expected/rpr_explain.out +++ b/src/test/regress/expected/rpr_explain.out @@ -6013,6 +6013,26 @@ EXPLAIN (COSTS OFF) EXECUTE rpr_nav_offset_prep(2); RESET plan_cache_mode; DEALLOCATE rpr_nav_offset_prep; +-- EXPLAIN (GENERIC_PLAN) of an unbound parameter offset must not evaluate the +-- parameter: the offset stays "runtime" instead of failing with "no value +-- found for parameter 1". +EXPLAIN (GENERIC_PLAN, COSTS OFF) +SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS v > PREV(v, $1) +); + QUERY PLAN +------------------------------------------------------------------- + WindowAgg + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+" + Nav Mark Lookback: runtime + -> Function Scan on generate_series s +(5 rows) + -- FIRST(v): retain all (references match_start row) EXPLAIN (COSTS OFF) SELECT count(*) OVER w FROM generate_series(1,10) s(v) @@ -6031,6 +6051,24 @@ WINDOW w AS ( -> Function Scan on generate_series s (6 rows) +-- FIRST(v, 5): forward reach 5 +EXPLAIN (COSTS OFF) SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS v > FIRST(v, 5) +); + QUERY PLAN +------------------------------------------------------------------- + WindowAgg + Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+ + Nav Mark Lookback: 0 + Nav Mark Lookahead: 5 + -> Function Scan on generate_series s +(6 rows) + -- LAST(v, 1): backward reach 1, same as PREV(v, 1) EXPLAIN (COSTS OFF) SELECT count(*) OVER w FROM generate_series(1,10) s(v) @@ -6311,9 +6349,9 @@ EXECUTE test_prev_implicit_offset(0); DEALLOCATE test_prev_implicit_offset; -- NEEDS_EVAL executor offset paths: a Param nav offset stays non-Const under a --- generic plan, so the planner marks the offset NEEDS_EVAL and the executor --- resolves it at init via eval_define_offsets -> visit_nav_exec. Each query --- below exercises a different navigation arm of that walker. +-- generic plan, so build_define_offsets() marks the offset NEEDS_EVAL and +-- resolve_nav_offsets() settles it once per scan. Each query below exercises +-- a different navigation arm of that walker. -- Simple FIRST(v, $1): forward-reach FIRST arm. PREPARE test_eval_first(int8) AS SELECT count(*) OVER w @@ -6446,3 +6484,28 @@ WINDOW w AS ( EXECUTE test_runtime_null_offset(NULL); ERROR: row pattern navigation offset must not be null DEALLOCATE test_runtime_null_offset; +-- A correlated PARAM_EXEC nav offset (reaching the offset via SRF inlining) is +-- resolved per scan by resolve_nav_offsets(); after execution EXPLAIN ANALYZE +-- must display the concrete resolved bound (a number), not "runtime" -- that is, +-- navMaxOffsetKind resolves to FIXED. Plain EXPLAIN of the same query shows +-- "runtime"; only ANALYZE exercises the per-scan clear. +CREATE TABLE rpr_exp_srf (v int); +INSERT INTO rpr_exp_srf SELECT generate_series(1, 10); +CREATE FUNCTION rpr_exp_srf_f(k int) RETURNS SETOF bigint AS $$ + SELECT count(*) OVER w + FROM rpr_exp_srf + WINDOW w AS (ORDER BY v ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) DEFINE A AS v > PREV(v, k)) +$$ LANGUAGE sql STABLE; +SELECT t FROM rpr_explain_filter( + 'EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS OFF) + SELECT g.n, max(s) FROM (VALUES (2), (2)) g(n), LATERAL rpr_exp_srf_f(g.n) s + GROUP BY g.n') AS t +WHERE t LIKE '%Nav Mark Lookback%'; + t +------------------------------------ + Nav Mark Lookback: 2 +(1 row) + +DROP FUNCTION rpr_exp_srf_f(int); +DROP TABLE rpr_exp_srf; diff --git a/src/test/regress/expected/rpr_integration.out b/src/test/regress/expected/rpr_integration.out index d0b5cc5a44f..3017b387ecc 100644 --- a/src/test/regress/expected/rpr_integration.out +++ b/src/test/regress/expected/rpr_integration.out @@ -1603,6 +1603,149 @@ SELECT cnt FROM ( (16 rows) DROP TABLE rpr_over1, rpr_over2; +-- A row pattern navigation offset that resolves to a correlated PARAM_EXEC +-- (here through SRF inlining of rpr_srf_f(g.n)) must be re-resolved on every +-- rescan, not frozen at executor init. The inlined WindowAgg is the inner +-- side of a nestloop and is rescanned once per outer row, so each row sees its +-- own PREV(v, n) offset; a frozen offset would report the same value for all. +CREATE TABLE rpr_srf (v int); +INSERT INTO rpr_srf SELECT generate_series(1, 10); +CREATE FUNCTION rpr_srf_f(k int) RETURNS SETOF bigint AS $$ + SELECT count(*) OVER w + FROM rpr_srf + WINDOW w AS (ORDER BY v ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) DEFINE A AS v > PREV(v, k)) +$$ LANGUAGE sql STABLE; +-- The offset reads "runtime"; the WindowAgg inlines into the nestloop and is +-- rescanned per outer row. +EXPLAIN (COSTS OFF) +SELECT g.n, max(s) FROM (VALUES (1), (2), (3)) g(n), LATERAL rpr_srf_f(g.n) s +GROUP BY g.n ORDER BY g.n; + QUERY PLAN +-------------------------------------------------------------------------------------------------------- + Sort + Sort Key: "*VALUES*".column1 + -> HashAggregate + Group Key: "*VALUES*".column1 + -> Nested Loop + -> Values Scan on "*VALUES*" + -> WindowAgg + Window: w AS (ORDER BY rpr_srf.v ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+" + Nav Mark Lookback: runtime + -> Sort + Sort Key: rpr_srf.v + -> Seq Scan on rpr_srf +(13 rows) + +-- Each outer row yields its own offset (9, 8, 7), not one frozen value. +SELECT g.n, max(s) AS m FROM (VALUES (1), (2), (3)) g(n), LATERAL rpr_srf_f(g.n) s +GROUP BY g.n ORDER BY g.n; + n | m +---+--- + 1 | 9 + 2 | 8 + 3 | 7 +(3 rows) + +-- A forward FIRST-family offset with a correlated PARAM_EXEC must likewise be +-- re-resolved per scan (navFirstOffset / navFirstOffsetKind), not frozen at init. +-- PATTERN (B A+) anchors the match start at B so A can reference FIRST(v, k) +-- k rows ahead; each outer k yields its own forward offset (k=0 matches all +-- ten rows, k>=1 makes the first A fail), proving per-scan re-resolution. +CREATE FUNCTION rpr_srf_first(k int) RETURNS SETOF bigint AS $$ + SELECT count(*) OVER w + FROM rpr_srf + WINDOW w AS (ORDER BY v ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (B A+) DEFINE A AS v > FIRST(v, k)) +$$ LANGUAGE sql STABLE; +-- The forward offset reads "runtime" and the WindowAgg inlines into the nestloop. +EXPLAIN (COSTS OFF) +SELECT g.n, max(s) FROM (VALUES (0), (1), (2)) g(n), LATERAL rpr_srf_first(g.n) s +GROUP BY g.n ORDER BY g.n; + QUERY PLAN +-------------------------------------------------------------------------------------------------------- + Sort + Sort Key: "*VALUES*".column1 + -> HashAggregate + Group Key: "*VALUES*".column1 + -> Nested Loop + -> Values Scan on "*VALUES*" + -> WindowAgg + Window: w AS (ORDER BY rpr_srf.v ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: b a+ + Nav Mark Lookback: 0 + Nav Mark Lookahead: runtime + -> Sort + Sort Key: rpr_srf.v + -> Seq Scan on rpr_srf +(14 rows) + +-- k=0 -> 10, k>=1 -> 0: distinct per outer row, so the forward offset is not +-- frozen at ExecInit. +SELECT g.n, max(s) AS m FROM (VALUES (0), (1), (2)) g(n), LATERAL rpr_srf_first(g.n) s +GROUP BY g.n ORDER BY g.n; + n | m +---+---- + 0 | 10 + 1 | 0 + 2 | 0 +(3 rows) + +DROP FUNCTION rpr_srf_first(int); +-- A compound navigation whose OUTER offset is a correlated PARAM_EXEC must be +-- re-resolved per scan as well. An outer offset that overflows int64 flips the +-- backward trim to RETAIN_ALL for that scan only; a smaller offset on the next +-- rescan must go back to FIXED. k=1 -> 9, k=3 -> 7, k=overflow -> 0 (out of +-- range, no match), proving both the per-scan outer-offset resolution and the +-- RETAIN_ALL <-> FIXED toggle across rescans. +CREATE FUNCTION rpr_srf_cmp(k int8) RETURNS SETOF bigint AS $$ + SELECT count(*) OVER w + FROM rpr_srf + WINDOW w AS (ORDER BY v ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) DEFINE B AS v > PREV(LAST(v, 1), k)) +$$ LANGUAGE sql STABLE; +SELECT g.n, max(s) AS m +FROM (VALUES (1::int8), (3::int8), (9223372036854775807::int8)) g(n), + LATERAL rpr_srf_cmp(g.n) s +GROUP BY g.n ORDER BY g.n; + n | m +---------------------+--- + 1 | 9 + 3 | 7 + 9223372036854775807 | 0 +(3 rows) + +DROP FUNCTION rpr_srf_cmp(int8); +DROP FUNCTION rpr_srf_f(int); +DROP TABLE rpr_srf; +-- A correlated PARAM_EXEC that appears only inside DEFINE must be registered in +-- the WindowAgg's extParam so the chgParam signal reaches a caching node above. +-- Here the inlined function's argument is used only in DEFINE, DISTINCT is +-- planned as a HashAgg, and HashAgg rescan is gated on chgParam; without the +-- param in extParam the hash table built for the first outer row would be +-- re-served for the rest. Each threshold must get its own answer set +-- (10 -> {0, 90}, 200 -> {0}); a stale cache would add a spurious 200|90. +CREATE TABLE rpr_hcache_thr (threshold int); +INSERT INTO rpr_hcache_thr VALUES (10), (200); +CREATE TABLE rpr_hcache_stock (price int); +INSERT INTO rpr_hcache_stock SELECT g FROM generate_series(1, 100) g; +CREATE FUNCTION rpr_hcache_fn(th int) RETURNS SETOF bigint LANGUAGE sql STABLE AS $$ + SELECT DISTINCT count(*) OVER w FROM rpr_hcache_stock + WINDOW w AS (ORDER BY price ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL PATTERN (a+) DEFINE a AS price > th) $$; +SELECT o.threshold, f FROM rpr_hcache_thr o, LATERAL rpr_hcache_fn(o.threshold) f +ORDER BY 1, 2; + threshold | f +-----------+---- + 10 | 0 + 10 | 90 + 200 | 0 +(3 rows) + +DROP FUNCTION rpr_hcache_fn(int); +DROP TABLE rpr_hcache_thr, rpr_hcache_stock; -- Cleanup DROP TABLE rpr_integ; DROP TABLE rpr_integ2; diff --git a/src/test/regress/sql/rpr.sql b/src/test/regress/sql/rpr.sql index 4796d42d9c2..b18018a7bca 100644 --- a/src/test/regress/sql/rpr.sql +++ b/src/test/regress/sql/rpr.sql @@ -1240,6 +1240,23 @@ SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS ( DEFINE A AS NEXT(LAST(val), -1) IS NULL ); +-- Compound: an out-of-range inner offset must not skip validation of an +-- illegal outer offset. The early exit on the inner used to let a NULL or +-- negative outer offset through, so the same illegal query passed or errored +-- depending on the data; both must error now. +SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE A AS TRUE, B AS PREV(FIRST(val, 99), NULL::int8) IS NULL +); +SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE A AS TRUE, B AS NEXT(LAST(val, 99), -1) IS NULL +); + -- Outer offset overflows int64: target position out of range -> NULL. -- Plain NEXT(val, INT64_MAX): currentpos + INT64_MAX overflows. SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS ( diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql index 33856187d62..72083964d14 100644 --- a/src/test/regress/sql/rpr_base.sql +++ b/src/test/regress/sql/rpr_base.sql @@ -1285,6 +1285,46 @@ SELECT format($$SELECT count(*) OVER w FROM (SELECT 1 i) t -- ============================================================ -- Navigation Functions Tests (PREV / NEXT / FIRST / LAST) -- ============================================================ +CREATE TEMP TABLE rpr_nav0 (id int, v int); +INSERT INTO rpr_nav0 SELECT g, g*10 FROM generate_series(1, 5) g; + +-- Two concurrently open portals of the SAME cached generic plan, with different +-- offset parameters. +-- +-- The parameterized cursor 'c' compiles to one plpgsql statement -> one SPI +-- cached plan. The recursive call OPENs a second portal of that same plan +-- (with a different offset) while the outer portal is already started but has +-- not yet FETCHed. +CREATE OR REPLACE FUNCTION rpr_nested(p_off int, depth int) +RETURNS SETOF text LANGUAGE plpgsql AS $$ +DECLARE + c CURSOR (o int) FOR + SELECT id, count(*) OVER w AS cnt + FROM rpr_nav0 + WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A) + DEFINE A AS PREV(v, o) IS NULL); + r record; +BEGIN + OPEN c(p_off); + IF depth > 0 THEN + RETURN QUERY SELECT * FROM rpr_nested(p_off + 2, depth - 1); + END IF; + + LOOP + FETCH c INTO r; + EXIT WHEN NOT FOUND; + RETURN NEXT format('off=%s id=%s cnt=%s', p_off, r.id, r.cnt); + END LOOP; + CLOSE c; +END $$; + +SET plan_cache_mode = force_generic_plan; +SELECT * FROM rpr_nested(1, 1); +RESET plan_cache_mode; + +DROP FUNCTION rpr_nested(int, int); CREATE TABLE rpr_nav (id INT, val INT); INSERT INTO rpr_nav VALUES diff --git a/src/test/regress/sql/rpr_explain.sql b/src/test/regress/sql/rpr_explain.sql index af79a2521c7..0512e4a6491 100644 --- a/src/test/regress/sql/rpr_explain.sql +++ b/src/test/regress/sql/rpr_explain.sql @@ -3418,6 +3418,18 @@ EXPLAIN (COSTS OFF) EXECUTE rpr_nav_offset_prep(2); RESET plan_cache_mode; DEALLOCATE rpr_nav_offset_prep; +-- EXPLAIN (GENERIC_PLAN) of an unbound parameter offset must not evaluate the +-- parameter: the offset stays "runtime" instead of failing with "no value +-- found for parameter 1". +EXPLAIN (GENERIC_PLAN, COSTS OFF) +SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS v > PREV(v, $1) +); + -- FIRST(v): retain all (references match_start row) EXPLAIN (COSTS OFF) SELECT count(*) OVER w FROM generate_series(1,10) s(v) @@ -3427,6 +3439,15 @@ WINDOW w AS ( DEFINE A AS v > FIRST(v) ); +-- FIRST(v, 5): forward reach 5 +EXPLAIN (COSTS OFF) SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS v > FIRST(v, 5) +); + -- LAST(v, 1): backward reach 1, same as PREV(v, 1) EXPLAIN (COSTS OFF) SELECT count(*) OVER w FROM generate_series(1,10) s(v) @@ -3576,9 +3597,9 @@ EXECUTE test_prev_implicit_offset(0); DEALLOCATE test_prev_implicit_offset; -- NEEDS_EVAL executor offset paths: a Param nav offset stays non-Const under a --- generic plan, so the planner marks the offset NEEDS_EVAL and the executor --- resolves it at init via eval_define_offsets -> visit_nav_exec. Each query --- below exercises a different navigation arm of that walker. +-- generic plan, so build_define_offsets() marks the offset NEEDS_EVAL and +-- resolve_nav_offsets() settles it once per scan. Each query below exercises +-- a different navigation arm of that walker. -- Simple FIRST(v, $1): forward-reach FIRST arm. PREPARE test_eval_first(int8) AS @@ -3659,3 +3680,24 @@ WINDOW w AS ( ); EXECUTE test_runtime_null_offset(NULL); DEALLOCATE test_runtime_null_offset; + +-- A correlated PARAM_EXEC nav offset (reaching the offset via SRF inlining) is +-- resolved per scan by resolve_nav_offsets(); after execution EXPLAIN ANALYZE +-- must display the concrete resolved bound (a number), not "runtime" -- that is, +-- navMaxOffsetKind resolves to FIXED. Plain EXPLAIN of the same query shows +-- "runtime"; only ANALYZE exercises the per-scan clear. +CREATE TABLE rpr_exp_srf (v int); +INSERT INTO rpr_exp_srf SELECT generate_series(1, 10); +CREATE FUNCTION rpr_exp_srf_f(k int) RETURNS SETOF bigint AS $$ + SELECT count(*) OVER w + FROM rpr_exp_srf + WINDOW w AS (ORDER BY v ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) DEFINE A AS v > PREV(v, k)) +$$ LANGUAGE sql STABLE; +SELECT t FROM rpr_explain_filter( + 'EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS OFF) + SELECT g.n, max(s) FROM (VALUES (2), (2)) g(n), LATERAL rpr_exp_srf_f(g.n) s + GROUP BY g.n') AS t +WHERE t LIKE '%Nav Mark Lookback%'; +DROP FUNCTION rpr_exp_srf_f(int); +DROP TABLE rpr_exp_srf; diff --git a/src/test/regress/sql/rpr_integration.sql b/src/test/regress/sql/rpr_integration.sql index c5f5e850925..f3a2cf86ffd 100644 --- a/src/test/regress/sql/rpr_integration.sql +++ b/src/test/regress/sql/rpr_integration.sql @@ -1004,6 +1004,90 @@ SELECT cnt FROM ( ) s; DROP TABLE rpr_over1, rpr_over2; +-- A row pattern navigation offset that resolves to a correlated PARAM_EXEC +-- (here through SRF inlining of rpr_srf_f(g.n)) must be re-resolved on every +-- rescan, not frozen at executor init. The inlined WindowAgg is the inner +-- side of a nestloop and is rescanned once per outer row, so each row sees its +-- own PREV(v, n) offset; a frozen offset would report the same value for all. +CREATE TABLE rpr_srf (v int); +INSERT INTO rpr_srf SELECT generate_series(1, 10); +CREATE FUNCTION rpr_srf_f(k int) RETURNS SETOF bigint AS $$ + SELECT count(*) OVER w + FROM rpr_srf + WINDOW w AS (ORDER BY v ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) DEFINE A AS v > PREV(v, k)) +$$ LANGUAGE sql STABLE; +-- The offset reads "runtime"; the WindowAgg inlines into the nestloop and is +-- rescanned per outer row. +EXPLAIN (COSTS OFF) +SELECT g.n, max(s) FROM (VALUES (1), (2), (3)) g(n), LATERAL rpr_srf_f(g.n) s +GROUP BY g.n ORDER BY g.n; +-- Each outer row yields its own offset (9, 8, 7), not one frozen value. +SELECT g.n, max(s) AS m FROM (VALUES (1), (2), (3)) g(n), LATERAL rpr_srf_f(g.n) s +GROUP BY g.n ORDER BY g.n; + +-- A forward FIRST-family offset with a correlated PARAM_EXEC must likewise be +-- re-resolved per scan (navFirstOffset / navFirstOffsetKind), not frozen at init. +-- PATTERN (B A+) anchors the match start at B so A can reference FIRST(v, k) +-- k rows ahead; each outer k yields its own forward offset (k=0 matches all +-- ten rows, k>=1 makes the first A fail), proving per-scan re-resolution. +CREATE FUNCTION rpr_srf_first(k int) RETURNS SETOF bigint AS $$ + SELECT count(*) OVER w + FROM rpr_srf + WINDOW w AS (ORDER BY v ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (B A+) DEFINE A AS v > FIRST(v, k)) +$$ LANGUAGE sql STABLE; +-- The forward offset reads "runtime" and the WindowAgg inlines into the nestloop. +EXPLAIN (COSTS OFF) +SELECT g.n, max(s) FROM (VALUES (0), (1), (2)) g(n), LATERAL rpr_srf_first(g.n) s +GROUP BY g.n ORDER BY g.n; +-- k=0 -> 10, k>=1 -> 0: distinct per outer row, so the forward offset is not +-- frozen at ExecInit. +SELECT g.n, max(s) AS m FROM (VALUES (0), (1), (2)) g(n), LATERAL rpr_srf_first(g.n) s +GROUP BY g.n ORDER BY g.n; +DROP FUNCTION rpr_srf_first(int); + +-- A compound navigation whose OUTER offset is a correlated PARAM_EXEC must be +-- re-resolved per scan as well. An outer offset that overflows int64 flips the +-- backward trim to RETAIN_ALL for that scan only; a smaller offset on the next +-- rescan must go back to FIXED. k=1 -> 9, k=3 -> 7, k=overflow -> 0 (out of +-- range, no match), proving both the per-scan outer-offset resolution and the +-- RETAIN_ALL <-> FIXED toggle across rescans. +CREATE FUNCTION rpr_srf_cmp(k int8) RETURNS SETOF bigint AS $$ + SELECT count(*) OVER w + FROM rpr_srf + WINDOW w AS (ORDER BY v ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) DEFINE B AS v > PREV(LAST(v, 1), k)) +$$ LANGUAGE sql STABLE; +SELECT g.n, max(s) AS m +FROM (VALUES (1::int8), (3::int8), (9223372036854775807::int8)) g(n), + LATERAL rpr_srf_cmp(g.n) s +GROUP BY g.n ORDER BY g.n; +DROP FUNCTION rpr_srf_cmp(int8); +DROP FUNCTION rpr_srf_f(int); +DROP TABLE rpr_srf; + +-- A correlated PARAM_EXEC that appears only inside DEFINE must be registered in +-- the WindowAgg's extParam so the chgParam signal reaches a caching node above. +-- Here the inlined function's argument is used only in DEFINE, DISTINCT is +-- planned as a HashAgg, and HashAgg rescan is gated on chgParam; without the +-- param in extParam the hash table built for the first outer row would be +-- re-served for the rest. Each threshold must get its own answer set +-- (10 -> {0, 90}, 200 -> {0}); a stale cache would add a spurious 200|90. +CREATE TABLE rpr_hcache_thr (threshold int); +INSERT INTO rpr_hcache_thr VALUES (10), (200); +CREATE TABLE rpr_hcache_stock (price int); +INSERT INTO rpr_hcache_stock SELECT g FROM generate_series(1, 100) g; +CREATE FUNCTION rpr_hcache_fn(th int) RETURNS SETOF bigint LANGUAGE sql STABLE AS $$ + SELECT DISTINCT count(*) OVER w FROM rpr_hcache_stock + WINDOW w AS (ORDER BY price ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL PATTERN (a+) DEFINE a AS price > th) $$; +SELECT o.threshold, f FROM rpr_hcache_thr o, LATERAL rpr_hcache_fn(o.threshold) f +ORDER BY 1, 2; +DROP FUNCTION rpr_hcache_fn(int); +DROP TABLE rpr_hcache_thr, rpr_hcache_stock; + -- Cleanup DROP TABLE rpr_integ; DROP TABLE rpr_integ2; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 0bc36d9fccc..78694213f65 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1843,8 +1843,6 @@ NamedLWLockTrancheRequest NamedTuplestoreScan NamedTuplestoreScanState NamespaceInfo -NavTraversal -NavVisitFn NestLoop NestLoopParam NestLoopState @@ -2545,6 +2543,8 @@ RPRNFAState RPRNavExpr RPRNavKind RPRNavOffsetKind +RPRNavOffsets +RPRNavState RPRPattern RPRPatternElement RPRPatternNode -- 2.50.1 (Apple Git-155)