nocfbot-0017-Implement-1-slot-PREV-NEXT-navigation-for-RPR.txt
text/plain
Filename: nocfbot-0017-Implement-1-slot-PREV-NEXT-navigation-for-RPR.txt
Type: text/plain
Part: 16
Message:
Re: Row pattern recognition
From 79855c1edcf7c411c63ddeab09ac238f2b7d9986 Mon Sep 17 00:00:00 2001
From: Henson Choi <assam258@gmail.com>
Date: Fri, 20 Mar 2026 23:52:42 +0900
Subject: [PATCH 17/40] Implement 1-slot PREV/NEXT navigation for RPR
Add PREV(value [, offset]) and NEXT(value [, offset]) navigation
functions for use in the DEFINE clause of row pattern recognition.
These functions return the column value at a row offset rows
before/after the current row within the partition, returning NULL
if the target row is outside the partition. The offset defaults
to 1 if omitted; offset=0 refers to the current row itself;
NULL or negative offset raises an error.
Key design: instead of the previous 3-slot model (outer/scan/inner),
a single-slot swap model is used. EEOP_RPR_NAV_SET temporarily
replaces ecxt_outertuple with the target row, the argument
expression evaluates against it, and EEOP_RPR_NAV_RESTORE restores
the original slot. This eliminates varno rewriting and naturally
supports arbitrary offsets.
A dedicated nav_winobj with its own tuplestore read pointer avoids
interference with aggregate processing. A mark pointer pinned at
position 0 prevents tuplestore truncation so that PREV(expr, N)
can reach any prior row.
RPRNavExpr is a new expression node that replaces the previous
approach of identifying PREV/NEXT by funcid. The parser transforms
PREV/NEXT function calls into RPRNavExpr nodes in ParseFuncOrColumn().
Validation in parse_rpr.c rejects nested PREV/NEXT, requires at
least one column reference in the first argument, and ensures
the offset is a run-time constant.
RPRNavKind uses plain enum values (not -1/+1) so that FIRST/LAST
can be added later without arithmetic tricks.
LLVM JIT falls back to the interpreter for expressions containing
RPR navigation opcodes, because JIT code caches the outertuple's
tts_values/tts_isnull pointers in the entry block and the
mid-expression slot swap leaves them stale. Only DEFINE clause
expressions with PREV/NEXT are affected; other expressions in
the same query are still JIT-compiled normally.
---
doc/src/sgml/func/func-window.sgml | 22 +-
src/backend/executor/execExpr.c | 56 ++
src/backend/executor/execExprInterp.c | 110 ++++
src/backend/executor/nodeWindowAgg.c | 243 ++++----
src/backend/jit/llvm/llvmjit_expr.c | 40 ++
src/backend/jit/llvm/llvmjit_types.c | 2 +
src/backend/nodes/nodeFuncs.c | 33 ++
src/backend/parser/parse_func.c | 30 +-
src/backend/parser/parse_rpr.c | 86 +++
src/backend/utils/adt/ruleutils.c | 16 +
src/backend/utils/adt/windowfuncs.c | 52 +-
src/include/catalog/pg_proc.dat | 6 +
src/include/executor/execExpr.h | 18 +
src/include/executor/nodeWindowAgg.h | 3 +
src/include/nodes/execnodes.h | 10 +-
src/include/nodes/primnodes.h | 31 +
src/test/regress/expected/rpr.out | 551 +++++++++++++++++-
src/test/regress/expected/rpr_base.out | 2 +-
src/test/regress/expected/rpr_explain.out | 80 +++
src/test/regress/expected/rpr_integration.out | 57 +-
src/test/regress/sql/rpr.sql | 322 +++++++++-
src/test/regress/sql/rpr_explain.sql | 56 ++
src/tools/pgindent/typedefs.list | 3 +-
23 files changed, 1655 insertions(+), 174 deletions(-)
diff --git a/doc/src/sgml/func/func-window.sgml b/doc/src/sgml/func/func-window.sgml
index ae36e0f3135..1b9b993a817 100644
--- a/doc/src/sgml/func/func-window.sgml
+++ b/doc/src/sgml/func/func-window.sgml
@@ -304,12 +304,17 @@
<indexterm>
<primary>prev</primary>
</indexterm>
- <function>prev</function> ( <parameter>value</parameter> <type>anyelement</type> )
+ <function>prev</function> ( <parameter>value</parameter> <type>anyelement</type> [, <parameter>offset</parameter> <type>bigint</type> ] )
<returnvalue>anyelement</returnvalue>
</para>
<para>
- Returns the column value at the previous row;
- returns NULL if there is no previous row in the window frame.
+ Returns the column value at the row <parameter>offset</parameter>
+ rows before the current row within the window frame;
+ returns NULL if the target row is outside the window frame.
+ <parameter>offset</parameter> defaults to 1 if omitted.
+ <parameter>offset</parameter> must be a non-negative integer;
+ an offset of 0 refers to the current row itself.
+ <parameter>offset</parameter> must not be NULL.
</para></entry>
</row>
@@ -318,12 +323,17 @@
<indexterm>
<primary>next</primary>
</indexterm>
- <function>next</function> ( <parameter>value</parameter> <type>anyelement</type> )
+ <function>next</function> ( <parameter>value</parameter> <type>anyelement</type> [, <parameter>offset</parameter> <type>bigint</type> ] )
<returnvalue>anyelement</returnvalue>
</para>
<para>
- Returns the column value at the next row;
- returns NULL if there is no next row in the window frame.
+ Returns the column value at the row <parameter>offset</parameter>
+ rows after the current row within the window frame;
+ returns NULL if the target row is outside the window frame.
+ <parameter>offset</parameter> defaults to 1 if omitted.
+ <parameter>offset</parameter> must be a non-negative integer;
+ an offset of 0 refers to the current row itself.
+ <parameter>offset</parameter> must not be NULL.
</para></entry>
</row>
diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c
index 77229141b38..dbed4f48a0f 100644
--- a/src/backend/executor/execExpr.c
+++ b/src/backend/executor/execExpr.c
@@ -1222,6 +1222,62 @@ ExecInitExprRec(Expr *node, ExprState *state,
break;
}
+ case T_RPRNavExpr:
+ {
+ /*
+ * RPR navigation functions (PREV/NEXT) are compiled into
+ * EEOP_RPR_NAV_SET / EEOP_RPR_NAV_RESTORE opcodes instead of
+ * a normal function call. The SET opcode 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.
+ */
+ RPRNavExpr *nav = (RPRNavExpr *) node;
+ WindowAggState *winstate;
+
+ 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;
+
+ if (nav->offset_arg != NULL)
+ {
+ /*
+ * Allocate storage for the runtime offset value. The
+ * offset expression is compiled below so it runs before
+ * EEOP_RPR_NAV_SET.
+ */
+ Datum *offset_value = palloc_object(Datum);
+ bool *offset_isnull = palloc_object(bool);
+
+ /* Compile the offset expression into the temp storage */
+ 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
+ {
+ scratch.d.rpr_nav.offset_value = NULL;
+ scratch.d.rpr_nav.offset_isnull = NULL;
+ }
+
+ ExprEvalPushStep(state, &scratch);
+
+ /* Compile the argument expression normally */
+ ExecInitExprRec(nav->arg, state, resv, resnull);
+
+ /* Emit RESTORE opcode: restore original slot */
+ scratch.opcode = EEOP_RPR_NAV_RESTORE;
+ scratch.d.rpr_nav.winstate = winstate;
+ ExprEvalPushStep(state, &scratch);
+ break;
+ }
+
case T_FuncExpr:
{
FuncExpr *func = (FuncExpr *) node;
diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c
index 3c4843cde86..e41faa95be3 100644
--- a/src/backend/executor/execExprInterp.c
+++ b/src/backend/executor/execExprInterp.c
@@ -56,12 +56,14 @@
*/
#include "postgres.h"
+#include "common/int.h"
#include "access/heaptoast.h"
#include "access/tupconvert.h"
#include "catalog/pg_type.h"
#include "commands/sequence.h"
#include "executor/execExpr.h"
#include "executor/nodeSubplan.h"
+#include "executor/nodeWindowAgg.h"
#include "funcapi.h"
#include "miscadmin.h"
#include "nodes/miscnodes.h"
@@ -578,6 +580,8 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull)
&&CASE_EEOP_WINDOW_FUNC,
&&CASE_EEOP_MERGE_SUPPORT_FUNC,
&&CASE_EEOP_SUBPLAN,
+ &&CASE_EEOP_RPR_NAV_SET,
+ &&CASE_EEOP_RPR_NAV_RESTORE,
&&CASE_EEOP_AGG_STRICT_DESERIALIZE,
&&CASE_EEOP_AGG_DESERIALIZE,
&&CASE_EEOP_AGG_STRICT_INPUT_CHECK_ARGS,
@@ -2005,6 +2009,24 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull)
EEO_NEXT();
}
+ /* RPR navigation: swap slot to target row */
+ EEO_CASE(EEOP_RPR_NAV_SET)
+ {
+ ExecEvalRPRNavSet(state, op, econtext);
+ outerslot = econtext->ecxt_outertuple;
+
+ EEO_NEXT();
+ }
+
+ /* RPR navigation: restore slot to original row */
+ EEO_CASE(EEOP_RPR_NAV_RESTORE)
+ {
+ ExecEvalRPRNavRestore(state, op, econtext);
+ outerslot = econtext->ecxt_outertuple;
+
+ EEO_NEXT();
+ }
+
/* evaluate a strict aggregate deserialization function */
EEO_CASE(EEOP_AGG_STRICT_DESERIALIZE)
{
@@ -5918,3 +5940,91 @@ ExecAggPlainTransByRef(AggState *aggstate, AggStatePerTrans pertrans,
MemoryContextSwitchTo(oldContext);
}
+
+/*
+ * Evaluate RPR PREV/NEXT navigation: swap slot to target row.
+ *
+ * Saves the current outertuple into winstate for later restore, computes
+ * the target row position, fetches the corresponding slot from the
+ * tuplestore, and replaces econtext->ecxt_outertuple with it.
+ *
+ * This is called both from the interpreter inline handler and from
+ * JIT-compiled expressions via build_EvalXFunc.
+ */
+void
+ExecEvalRPRNavSet(ExprState *state, ExprEvalStep *op, ExprContext *econtext)
+{
+ WindowAggState *winstate = op->d.rpr_nav.winstate;
+ int64 offset;
+ int64 target_pos;
+ TupleTableSlot *target_slot;
+
+ /* Save current slot for later restore */
+ winstate->nav_saved_outertuple = econtext->ecxt_outertuple;
+
+ /*
+ * Determine the unsigned offset. For 2-arg PREV/NEXT the offset
+ * expression has already been evaluated into offset_value. NULL or
+ * negative offsets are errors per the SQL standard (ISO/IEC 9075-2,
+ * Subclause 5.6.2).
+ */
+ if (op->d.rpr_nav.offset_value != NULL)
+ {
+ if (*op->d.rpr_nav.offset_isnull)
+ ereport(ERROR,
+ (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
+ errmsg("PREV/NEXT offset must not be null")));
+
+ offset = DatumGetInt64(*op->d.rpr_nav.offset_value);
+
+ if (offset < 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("PREV/NEXT offset must not be negative")));
+ }
+ else
+ offset = 1;
+
+ /*
+ * 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)
+ {
+ case RPR_NAV_PREV:
+ if (pg_sub_s64_overflow(winstate->currentpos, offset, &target_pos))
+ target_pos = -1;
+ break;
+ case RPR_NAV_NEXT:
+ if (pg_add_s64_overflow(winstate->currentpos, offset, &target_pos))
+ target_pos = -1;
+ break;
+ }
+
+ /* Fetch target row slot (returns nav_null_slot if out of range) */
+ target_slot = ExecRPRNavGetSlot(winstate, target_pos);
+
+ /*
+ * Update econtext to point to the target slot. Also decompress the new
+ * slot's attributes since FETCHSOME already ran for the original slot.
+ * The caller (interpreter or JIT) is responsible for updating any local
+ * slot cache (e.g. outerslot) from econtext after we return.
+ */
+ slot_getallattrs(target_slot);
+ econtext->ecxt_outertuple = target_slot;
+}
+
+/*
+ * Evaluate RPR PREV/NEXT navigation: restore slot to original row.
+ *
+ * Restores econtext->ecxt_outertuple from the saved slot in winstate.
+ * The caller is responsible for updating any local slot cache.
+ */
+void
+ExecEvalRPRNavRestore(ExprState *state, ExprEvalStep *op,
+ ExprContext *econtext)
+{
+ WindowAggState *winstate = op->d.rpr_nav.winstate;
+
+ econtext->ecxt_outertuple = winstate->nav_saved_outertuple;
+}
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 0202c508323..4e643df94cf 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -53,7 +53,6 @@
#include "utils/acl.h"
#include "utils/builtins.h"
#include "utils/datum.h"
-#include "utils/fmgroids.h"
#include "utils/expandeddatum.h"
#include "utils/lsyscache.h"
#include "utils/memutils.h"
@@ -177,14 +176,6 @@ typedef struct WindowStatePerAggData
bool restart; /* need to restart this agg in this cycle? */
} WindowStatePerAggData;
-/*
- * Structure used by check_rpr_navigation() and rpr_navigation_walker().
- */
-typedef struct NavigationInfo
-{
- bool is_prev; /* true if PREV */
- int num_vars; /* number of var nodes */
-} NavigationInfo;
static void initialize_windowaggregate(WindowAggState *winstate,
WindowStatePerFunc perfuncstate,
@@ -243,9 +234,6 @@ static uint8 get_notnull_info(WindowObject winobj,
int64 pos, int argno);
static void put_notnull_info(WindowObject winobj,
int64 pos, int argno, bool isnull);
-static void attno_map(Node *node);
-static bool attno_map_walker(Node *node, void *context);
-
static bool rpr_is_defined(WindowAggState *winstate);
static int64 row_is_in_reduced_frame(WindowObject winobj, int64 pos);
@@ -253,9 +241,6 @@ static void clear_reduced_frame(WindowAggState *winstate);
static int get_reduced_frame_status(WindowAggState *winstate, int64 pos);
static void update_reduced_frame(WindowObject winobj, int64 pos);
-static void check_rpr_navigation(Node *node, bool is_prev);
-static bool rpr_navigation_walker(Node *node, void *context);
-
/* Forward declarations - NFA row evaluation */
static bool nfa_evaluate_row(WindowObject winobj, int64 pos, bool *varMatched);
@@ -1280,6 +1265,25 @@ prepare_tuplestore(WindowAggState *winstate)
}
}
+ /* Create read/mark pointers for RPR navigation if needed */
+ if (winstate->nav_winobj)
+ {
+ /*
+ * Allocate a mark pointer pinned at position 0 so that the tuplestore
+ * never truncates rows that a PREV(expr, N) might need.
+ *
+ * XXX This retains the entire partition in the tuplestore. If the
+ * DEFINE clause only uses PREV/NEXT with small constant offsets, we
+ * could advance the mark to (currentpos - max_offset) instead.
+ */
+ winstate->nav_winobj->markptr =
+ tuplestore_alloc_read_pointer(winstate->buffer, 0);
+ winstate->nav_winobj->readptr =
+ tuplestore_alloc_read_pointer(winstate->buffer,
+ EXEC_FLAG_BACKWARD);
+ winstate->nav_winobj->markpos = 0;
+ }
+
/*
* If we are in RANGE or GROUPS mode, then determining frame boundaries
* requires physical access to the frame endpoint rows, except in certain
@@ -1391,6 +1395,13 @@ begin_partition(WindowAggState *winstate)
winstate->aggregatedupto = 0;
}
+ /* reset mark and seek positions for RPR navigation */
+ if (winstate->nav_winobj)
+ {
+ winstate->nav_winobj->markpos = -1;
+ winstate->nav_winobj->seekpos = -1;
+ }
+
/* reset mark and seek positions for each real window function */
for (int i = 0; i < numfuncs; i++)
{
@@ -2726,15 +2737,18 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
winstate->temp_slot_2 = ExecInitExtraTupleSlot(estate, scanDesc,
&TTSOpsMinimalTuple);
- winstate->prev_slot = ExecInitExtraTupleSlot(estate, scanDesc,
- &TTSOpsMinimalTuple);
+ if (node->rpPattern != NULL)
+ {
+ winstate->nav_slot = ExecInitExtraTupleSlot(estate, scanDesc,
+ &TTSOpsMinimalTuple);
+ winstate->nav_slot_pos = -1;
- winstate->next_slot = ExecInitExtraTupleSlot(estate, scanDesc,
- &TTSOpsMinimalTuple);
+ winstate->nav_null_slot = ExecInitExtraTupleSlot(estate, scanDesc,
+ &TTSOpsMinimalTuple);
+ winstate->nav_null_slot = ExecStoreAllNullTuple(winstate->nav_null_slot);
- winstate->null_slot = ExecInitExtraTupleSlot(estate, scanDesc,
- &TTSOpsMinimalTuple);
- winstate->null_slot = ExecStoreAllNullTuple(winstate->null_slot);
+ winstate->nav_saved_outertuple = NULL;
+ }
/*
* create frame head and tail slots only if needed (must create slots in
@@ -2904,6 +2918,23 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
winstate->agg_winobj = agg_winobj;
}
+ /*
+ * Set up WindowObject for RPR navigation opcodes. This is separate from
+ * agg_winobj because it needs its own read pointer to avoid interfering
+ * with aggregate processing.
+ */
+ if (node->rpPattern != NULL)
+ {
+ WindowObject nav_winobj = makeNode(WindowObjectData);
+
+ nav_winobj->winstate = winstate;
+ nav_winobj->argstates = NIL;
+ nav_winobj->localmem = NULL;
+ nav_winobj->markptr = -1;
+ nav_winobj->readptr = -1;
+ winstate->nav_winobj = nav_winobj;
+ }
+
/* Set the status to running */
winstate->status = WINDOWAGG_RUN;
@@ -2944,7 +2975,9 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
if (node->defineClause != NIL)
{
/*
- * Tweak arg var of PREV/NEXT so that it refers to scan/inner slot.
+ * Compile DEFINE clause expressions. PREV/NEXT navigation is handled
+ * by EEOP_RPR_NAV_SET/RESTORE opcodes emitted during ExecInitExpr, so
+ * no varno rewriting is needed here.
*/
foreach(l, node->defineClause)
{
@@ -2956,7 +2989,6 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
winstate->defineVariableList =
lappend(winstate->defineVariableList,
makeString(pstrdup(name)));
- attno_map((Node *) expr);
exps = ExecInitExpr(expr, (PlanState *) winstate);
winstate->defineClauseList =
lappend(winstate->defineClauseList, exps);
@@ -2991,107 +3023,38 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
}
/*
- * Rewrite varno of Var nodes that are the argument of PREV/NET so that they
- * see scan tuple (PREV) or inner tuple (NEXT). Also we check the arguments
- * of PREV/NEXT include at least 1 column reference. This is required by the
- * SQL standard.
+ * ExecRPRNavGetSlot
+ *
+ * Fetch tuple at given position for RPR navigation opcodes.
+ * Returns nav_slot with the tuple loaded, or nav_null_slot if out of range.
*/
-static void
-attno_map(Node *node)
+TupleTableSlot *
+ExecRPRNavGetSlot(WindowAggState *winstate, int64 pos)
{
- (void) expression_tree_walker(node, attno_map_walker, NULL);
-}
+ WindowObject winobj = winstate->nav_winobj;
+ TupleTableSlot *slot = winstate->nav_slot;
-static bool
-attno_map_walker(Node *node, void *context)
-{
- FuncExpr *func;
- int nargs;
- bool is_prev;
+ if (pos < 0)
+ return winstate->nav_null_slot;
- if (node == NULL)
- return false;
+ /*
+ * If nav_slot already holds this position, return it without re-fetching.
+ * This is critical when multiple PREV/NEXT calls in the same expression
+ * navigate to the same row, because re-fetching would free the slot's
+ * tuple memory and invalidate any pass-by-ref Datum pointers from earlier
+ * navigation results.
+ */
+ if (winstate->nav_slot_pos == pos)
+ return slot;
- if (IsA(node, FuncExpr))
+ if (!window_gettupleslot(winobj, pos, slot))
{
- func = (FuncExpr *) node;
-
- if (func->funcid == F_PREV || func->funcid == F_NEXT)
- {
- /*
- * The SQL standard allows to have two more arguments form of
- * PREV/NEXT. But currently we allow only 1 argument form.
- */
- nargs = list_length(func->args);
- if (list_length(func->args) != 1)
- elog(ERROR, "PREV/NEXT must have 1 argument but function %d has %d args",
- func->funcid, nargs);
-
- /*
- * Check expr of PREV/NEXT aruguments and replace varno.
- */
- is_prev = (func->funcid == F_PREV) ? true : false;
- check_rpr_navigation(node, is_prev);
- }
+ winstate->nav_slot_pos = -1;
+ return winstate->nav_null_slot;
}
- return expression_tree_walker(node, attno_map_walker, NULL);
-}
-/*
- * Rewrite varno of Var of RPR navigation operations (PREV/NEXT).
- * If is_prev is true, we take care PREV, otherwise NEXT.
- */
-static void
-check_rpr_navigation(Node *node, bool is_prev)
-{
- NavigationInfo context;
-
- context.is_prev = is_prev;
- context.num_vars = 0;
- (void) expression_tree_walker(node, rpr_navigation_walker, &context);
- if (context.num_vars < 1)
- ereport(ERROR,
- errmsg("row pattern navigation operation's argument must include at least one column reference"));
-}
-
-static bool
-rpr_navigation_walker(Node *node, void *context)
-{
- NavigationInfo *nav = (NavigationInfo *) context;
-
- if (node == NULL)
- return false;
-
- switch (nodeTag(node))
- {
- case T_Var:
- {
- Var *var = (Var *) node;
-
- nav->num_vars++;
-
- if (nav->is_prev)
- {
- /*
- * Rewrite varno from OUTER_VAR to regular var no so that
- * the var references scan tuple.
- */
- var->varno = var->varnosyn;
- }
- else
- var->varno = INNER_VAR;
- }
- break;
- case T_Const:
- case T_FuncExpr:
- case T_OpExpr:
- break;
-
- default:
- ereport(ERROR,
- errmsg("row pattern navigation operation's argument includes unsupported expression"));
- }
- return expression_tree_walker(node, rpr_navigation_walker, context);
+ winstate->nav_slot_pos = pos;
+ return slot;
}
@@ -3152,8 +3115,8 @@ ExecReScanWindowAgg(WindowAggState *node)
ExecClearTuple(node->agg_row_slot);
ExecClearTuple(node->temp_slot_1);
ExecClearTuple(node->temp_slot_2);
- ExecClearTuple(node->prev_slot);
- ExecClearTuple(node->next_slot);
+ if (node->nav_slot)
+ ExecClearTuple(node->nav_slot);
if (node->framehead_slot)
ExecClearTuple(node->framehead_slot);
if (node->frametail_slot)
@@ -4218,6 +4181,10 @@ register_result:
* Returns true if the row exists, false if out of partition.
* If row exists, fills varMatched array.
* varMatched[i] = true if variable i matched at current row.
+ *
+ * Uses 1-slot model: only ecxt_outertuple is set to the current row.
+ * PREV/NEXT navigation is handled by EEOP_RPR_NAV_SET/RESTORE opcodes
+ * during expression evaluation, which temporarily swap the slot.
*/
static bool
nfa_evaluate_row(WindowObject winobj, int64 pos, bool *varMatched)
@@ -4228,37 +4195,25 @@ nfa_evaluate_row(WindowObject winobj, int64 pos, bool *varMatched)
ListCell *lc;
int varIdx = 0;
TupleTableSlot *slot;
+ int64 saved_pos;
- /*
- * Set up slots for current, previous, and next rows. We don't call
- * get_slots() here to avoid recursion through row_is_in_frame ->
- * update_reduced_frame -> ExecRPRProcessRow.
- */
-
- /* Current row -> ecxt_outertuple */
+ /* Fetch current row into temp_slot_1 */
slot = winstate->temp_slot_1;
if (!window_gettupleslot(winobj, pos, slot))
return false; /* No row exists */
+
+ /* Set up 1-slot context: only ecxt_outertuple */
econtext->ecxt_outertuple = slot;
- /* Previous row -> ecxt_scantuple (for PREV) */
- if (pos > 0)
- {
- slot = winstate->prev_slot;
- if (!window_gettupleslot(winobj, pos - 1, slot))
- econtext->ecxt_scantuple = winstate->null_slot;
- else
- econtext->ecxt_scantuple = slot;
- }
- else
- econtext->ecxt_scantuple = winstate->null_slot;
+ /*
+ * Save and set currentpos so that EEOP_RPR_NAV_SET opcodes can calculate
+ * target positions (currentpos +/- offset).
+ */
+ saved_pos = winstate->currentpos;
+ winstate->currentpos = pos;
- /* Next row -> ecxt_innertuple (for NEXT) */
- slot = winstate->next_slot;
- if (!window_gettupleslot(winobj, pos + 1, slot))
- econtext->ecxt_innertuple = winstate->null_slot;
- else
- econtext->ecxt_innertuple = slot;
+ /* Invalidate nav_slot cache so PREV/NEXT re-fetch for new row */
+ winstate->nav_slot_pos = -1;
foreach(lc, winstate->defineClauseList)
{
@@ -4276,6 +4231,8 @@ nfa_evaluate_row(WindowObject winobj, int64 pos, bool *varMatched)
break;
}
+ winstate->currentpos = saved_pos;
+
return true; /* Row exists */
}
diff --git a/src/backend/jit/llvm/llvmjit_expr.c b/src/backend/jit/llvm/llvmjit_expr.c
index 650f1d42a93..d158e37e7b5 100644
--- a/src/backend/jit/llvm/llvmjit_expr.c
+++ b/src/backend/jit/llvm/llvmjit_expr.c
@@ -296,6 +296,40 @@ llvm_compile_expr(ExprState *state)
FIELDNO_EXPRCONTEXT_AGGNULLS,
"v.econtext.aggnulls");
+ /*
+ * RPR navigation opcodes (PREV/NEXT) swap ecxt_outertuple to a different
+ * row mid-expression. The JIT code loads v_outervalues and v_outernulls
+ * once in the entry block and reuses them for all EEOP_OUTER_VAR steps.
+ * After a slot swap, these pointers become stale because the new slot has
+ * its own tts_values/tts_isnull arrays. Fall back to the interpreter for
+ * these expressions.
+ *
+ * XXX To JIT-compile these expressions properly, the NAV_SET and
+ * NAV_RESTORE handlers would need to reload the tts_values and tts_isnull
+ * pointers from the new slot. However, LLVM uses SSA (Static Single
+ * Assignment) form where each value is defined exactly once. When
+ * different basic blocks produce different values for the same pointer,
+ * LLVM requires PHI nodes at the merge point to select the correct one.
+ * Without that plumbing, OUTER_VAR steps after a slot swap would read
+ * from the wrong pointer.
+ */
+ if (parent && IsA(parent, WindowAggState) &&
+ ((WindowAgg *) parent->plan)->rpPattern != NULL)
+ {
+ for (int opno = 0; opno < state->steps_len; opno++)
+ {
+ ExprEvalOp opcode = ExecEvalStepOp(state, &state->steps[opno]);
+
+ if (opcode == EEOP_RPR_NAV_SET ||
+ opcode == EEOP_RPR_NAV_RESTORE)
+ {
+ LLVMDeleteFunction(eval_fn);
+ LLVMDisposeBuilder(b);
+ return false;
+ }
+ }
+ }
+
/* allocate blocks for each op upfront, so we can do jumps easily */
opblocks = palloc_array(LLVMBasicBlockRef, state->steps_len);
for (int opno = 0; opno < state->steps_len; opno++)
@@ -2432,6 +2466,12 @@ llvm_compile_expr(ExprState *state)
LLVMBuildBr(b, opblocks[opno + 1]);
break;
+ case EEOP_RPR_NAV_SET:
+ case EEOP_RPR_NAV_RESTORE:
+ /* unreachable: filtered out by the pre-scan above */
+ Assert(false);
+ return false;
+
case EEOP_AGG_STRICT_DESERIALIZE:
case EEOP_AGG_DESERIALIZE:
{
diff --git a/src/backend/jit/llvm/llvmjit_types.c b/src/backend/jit/llvm/llvmjit_types.c
index c8a1f841293..e78b31d775f 100644
--- a/src/backend/jit/llvm/llvmjit_types.c
+++ b/src/backend/jit/llvm/llvmjit_types.c
@@ -168,6 +168,8 @@ void *referenced_functions[] =
ExecEvalScalarArrayOp,
ExecEvalHashedScalarArrayOp,
ExecEvalSubPlan,
+ ExecEvalRPRNavSet,
+ ExecEvalRPRNavRestore,
ExecEvalSysVar,
ExecEvalWholeRowVar,
ExecEvalXmlExpr,
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index 1adda7c5d84..d2f19584070 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -69,6 +69,9 @@ exprType(const Node *expr)
case T_MergeSupportFunc:
type = ((const MergeSupportFunc *) expr)->msftype;
break;
+ case T_RPRNavExpr:
+ type = ((const RPRNavExpr *) expr)->resulttype;
+ break;
case T_SubscriptingRef:
type = ((const SubscriptingRef *) expr)->refrestype;
break;
@@ -853,6 +856,9 @@ exprCollation(const Node *expr)
case T_MergeSupportFunc:
coll = ((const MergeSupportFunc *) expr)->msfcollid;
break;
+ case T_RPRNavExpr:
+ coll = ((const RPRNavExpr *) expr)->resultcollid;
+ break;
case T_SubscriptingRef:
coll = ((const SubscriptingRef *) expr)->refcollid;
break;
@@ -1154,6 +1160,9 @@ exprSetCollation(Node *expr, Oid collation)
case T_MergeSupportFunc:
((MergeSupportFunc *) expr)->msfcollid = collation;
break;
+ case T_RPRNavExpr:
+ ((RPRNavExpr *) expr)->resultcollid = collation;
+ break;
case T_SubscriptingRef:
((SubscriptingRef *) expr)->refcollid = collation;
break;
@@ -1426,6 +1435,9 @@ exprLocation(const Node *expr)
case T_MergeSupportFunc:
loc = ((const MergeSupportFunc *) expr)->location;
break;
+ case T_RPRNavExpr:
+ loc = ((const RPRNavExpr *) expr)->location;
+ break;
case T_SubscriptingRef:
/* just use container argument's location */
loc = exprLocation((Node *) ((const SubscriptingRef *) expr)->refexpr);
@@ -2187,6 +2199,16 @@ expression_tree_walker_impl(Node *node,
return true;
}
break;
+ case T_RPRNavExpr:
+ {
+ RPRNavExpr *expr = (RPRNavExpr *) node;
+
+ if (WALK(expr->arg))
+ return true;
+ if (expr->offset_arg && WALK(expr->offset_arg))
+ return true;
+ }
+ break;
case T_SubscriptingRef:
{
SubscriptingRef *sbsref = (SubscriptingRef *) node;
@@ -3116,6 +3138,17 @@ expression_tree_mutator_impl(Node *node,
return (Node *) newnode;
}
break;
+ case T_RPRNavExpr:
+ {
+ RPRNavExpr *nav = (RPRNavExpr *) node;
+ RPRNavExpr *newnode;
+
+ FLATCOPY(newnode, nav, RPRNavExpr);
+ MUTATE(newnode->arg, nav->arg, Expr *);
+ MUTATE(newnode->offset_arg, nav->offset_arg, Expr *);
+ return (Node *) newnode;
+ }
+ break;
case T_SubscriptingRef:
{
SubscriptingRef *sbsref = (SubscriptingRef *) node;
diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c
index 23b02fb3bc0..e14ff4dc494 100644
--- a/src/backend/parser/parse_func.c
+++ b/src/backend/parser/parse_func.c
@@ -760,7 +760,8 @@ ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs,
/* next() and prev() are only allowed in a WINDOW DEFINE clause */
if (fdresult == FUNCDETAIL_NORMAL &&
pstate->p_expr_kind != EXPR_KIND_RPR_DEFINE &&
- (funcid == F_PREV || funcid == F_NEXT))
+ (funcid == F_PREV_ANYELEMENT || funcid == F_NEXT_ANYELEMENT ||
+ funcid == F_PREV_ANYELEMENT_INT8 || funcid == F_NEXT_ANYELEMENT_INT8))
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("%s can only be used in a DEFINE clause",
@@ -768,7 +769,32 @@ ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs,
parser_errposition(pstate, location)));
/* build the appropriate output structure */
- if (fdresult == FUNCDETAIL_NORMAL || fdresult == FUNCDETAIL_PROCEDURE)
+ if (fdresult == FUNCDETAIL_NORMAL &&
+ (funcid == F_PREV_ANYELEMENT || funcid == F_NEXT_ANYELEMENT ||
+ funcid == F_PREV_ANYELEMENT_INT8 || funcid == F_NEXT_ANYELEMENT_INT8))
+ {
+ /*
+ * PREV() and NEXT() are compiled into EEOP_RPR_NAV_SET /
+ * EEOP_RPR_NAV_RESTORE opcodes instead of a normal function call.
+ * Represent them as RPRNavExpr nodes so that later stages can
+ * identify them without relying on funcid comparisons.
+ */
+ bool is_next = (funcid == F_NEXT_ANYELEMENT ||
+ funcid == F_NEXT_ANYELEMENT_INT8);
+ bool has_offset = (funcid == F_PREV_ANYELEMENT_INT8 ||
+ funcid == F_NEXT_ANYELEMENT_INT8);
+ RPRNavExpr *navexpr = makeNode(RPRNavExpr);
+
+ navexpr->kind = is_next ? RPR_NAV_NEXT : RPR_NAV_PREV;
+ navexpr->arg = (Expr *) linitial(fargs);
+ navexpr->offset_arg = has_offset ? (Expr *) lsecond(fargs) : NULL;
+ navexpr->resulttype = rettype;
+ /* resultcollid will be set by parse_collate.c */
+ navexpr->location = location;
+
+ retval = (Node *) navexpr;
+ }
+ else if (fdresult == FUNCDETAIL_NORMAL || fdresult == FUNCDETAIL_PROCEDURE)
{
FuncExpr *funcexpr = makeNode(FuncExpr);
diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index db1309ca311..3fb5d94abe9 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -42,6 +42,8 @@ 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);
/*
* transformRPR
@@ -410,6 +412,10 @@ 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 */
+ foreach_ptr(TargetEntry, te, defineClause)
+ (void) check_rpr_nav_nesting_walker((Node *) te->expr, pstate);
+
/* mark column origins */
markTargetListOrigins(pstate, defineClause);
@@ -418,3 +424,83 @@ transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
return defineClause;
}
+
+/*
+ * check_rpr_nav_expr
+ * Validate a single RPRNavExpr node by walking its arg and offset_arg
+ * subtrees in a single pass each. Checks for nested PREV/NEXT, missing
+ * column references, and non-constant offset expressions.
+ */
+typedef struct
+{
+ bool has_nav; /* RPRNavExpr found (nesting) */
+ bool has_column_ref; /* Var found */
+} NavCheckResult;
+
+static bool
+nav_check_walker(Node *node, void *context)
+{
+ NavCheckResult *result = (NavCheckResult *) context;
+
+ if (node == NULL)
+ return false;
+ if (IsA(node, RPRNavExpr))
+ result->has_nav = true;
+ 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;
+
+ /* Check arg subtree: nesting + column reference in one walk */
+ memset(&result, 0, sizeof(result));
+ (void) nav_check_walker((Node *) nav->arg, &result);
+
+ if (result.has_nav)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("PREV and NEXT cannot be nested"),
+ 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)));
+
+ /* 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("PREV/NEXT offset must be a run-time constant"),
+ parser_errposition(pstate, nav->location)));
+ }
+}
+
+/*
+ * 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;
+ }
+ return expression_tree_walker(node, check_rpr_nav_nesting_walker, context);
+}
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index e93c03a351c..a4fe725646c 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -10103,6 +10103,22 @@ get_rule_expr(Node *node, deparse_context *context,
get_func_expr((FuncExpr *) node, context, showimplicit);
break;
+ case T_RPRNavExpr:
+ {
+ RPRNavExpr *nav = (RPRNavExpr *) node;
+
+ appendStringInfoString(buf,
+ nav->kind == RPR_NAV_PREV ? "PREV(" : "NEXT(");
+ get_rule_expr((Node *) nav->arg, context, showimplicit);
+ if (nav->offset_arg != NULL)
+ {
+ appendStringInfoString(buf, ", ");
+ get_rule_expr((Node *) nav->offset_arg, context, showimplicit);
+ }
+ appendStringInfoChar(buf, ')');
+ }
+ break;
+
case T_NamedArgExpr:
{
NamedArgExpr *na = (NamedArgExpr *) node;
diff --git a/src/backend/utils/adt/windowfuncs.c b/src/backend/utils/adt/windowfuncs.c
index 74ef109f72e..091260d2cce 100644
--- a/src/backend/utils/adt/windowfuncs.c
+++ b/src/backend/utils/adt/windowfuncs.c
@@ -726,22 +726,62 @@ window_nth_value(PG_FUNCTION_ARGS)
/*
* prev
- * Dummy function to invoke RPR's navigation operator "PREV".
- * This is *not* a window function.
+ * Catalog placeholder for RPR's PREV navigation operator.
+ *
+ * The parser transforms prev() calls inside DEFINE into RPRNavExpr nodes,
+ * so this function is never reached during normal RPR execution. It exists
+ * only so that the parser can resolve the function name from pg_proc.
+ * Calls outside DEFINE are rejected by parse_func.c (EXPR_KIND_RPR_DEFINE
+ * check). The error below is a defensive measure in case that check is
+ * bypassed (e.g., direct C-level function invocation).
*/
Datum
window_prev(PG_FUNCTION_ARGS)
{
- PG_RETURN_DATUM(PG_GETARG_DATUM(0));
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("prev() can only be used in a DEFINE clause")));
+ PG_RETURN_NULL(); /* not reached */
}
/*
* next
- * Dummy function to invoke RPR's navigation operation "NEXT".
- * This is *not* a window function.
+ * Catalog placeholder for RPR's NEXT navigation operator.
+ * See window_prev() for details.
*/
Datum
window_next(PG_FUNCTION_ARGS)
{
- PG_RETURN_DATUM(PG_GETARG_DATUM(0));
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("next() can only be used in a DEFINE clause")));
+ PG_RETURN_NULL(); /* not reached */
+}
+
+/*
+ * prev(value, offset)
+ * Catalog placeholder for RPR's PREV navigation operator with offset.
+ * See window_prev() for details.
+ */
+Datum
+window_prev_offset(PG_FUNCTION_ARGS)
+{
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("prev() can only be used in a DEFINE clause")));
+ PG_RETURN_NULL(); /* not reached */
+}
+
+/*
+ * next(value, offset)
+ * Catalog placeholder for RPR's NEXT navigation operator with offset.
+ * See window_prev() for details.
+ */
+Datum
+window_next_offset(PG_FUNCTION_ARGS)
+{
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("next() can only be used in a DEFINE clause")));
+ PG_RETURN_NULL(); /* not reached */
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index ddf922d16d7..8e95169b7b0 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -10975,9 +10975,15 @@
{ oid => '8126', descr => 'previous value',
proname => 'prev', provolatile => 's', prorettype => 'anyelement',
proargtypes => 'anyelement', prosrc => 'window_prev' },
+{ oid => '8128', descr => 'previous value at offset',
+ proname => 'prev', provolatile => 's', proisstrict => 'f', prorettype => 'anyelement',
+ proargtypes => 'anyelement int8', prosrc => 'window_prev_offset' },
{ oid => '8127', descr => 'next value',
proname => 'next', provolatile => 's', prorettype => 'anyelement',
proargtypes => 'anyelement', prosrc => 'window_next' },
+{ oid => '8129', descr => 'next value at offset',
+ proname => 'next', provolatile => 's', proisstrict => 'f', prorettype => 'anyelement',
+ proargtypes => 'anyelement int8', prosrc => 'window_next_offset' },
# functions for range types
{ oid => '3832', descr => 'I/O',
diff --git a/src/include/executor/execExpr.h b/src/include/executor/execExpr.h
index aa9b361fa31..fac37c96896 100644
--- a/src/include/executor/execExpr.h
+++ b/src/include/executor/execExpr.h
@@ -274,6 +274,10 @@ typedef enum ExprEvalOp
EEOP_MERGE_SUPPORT_FUNC,
EEOP_SUBPLAN,
+ /* row pattern navigation (RPR PREV/NEXT) */
+ EEOP_RPR_NAV_SET,
+ EEOP_RPR_NAV_RESTORE,
+
/* aggregation related nodes */
EEOP_AGG_STRICT_DESERIALIZE,
EEOP_AGG_DESERIALIZE,
@@ -691,6 +695,16 @@ typedef struct ExprEvalStep
SubPlanState *sstate;
} subplan;
+ /* for EEOP_RPR_NAV_SET / EEOP_RPR_NAV_RESTORE */
+ struct
+ {
+ WindowAggState *winstate;
+ RPRNavKind kind; /* PREV or NEXT */
+ Datum *offset_value; /* 2-arg: runtime offset value, or
+ * NULL */
+ bool *offset_isnull; /* 2-arg: runtime offset null flag */
+ } rpr_nav;
+
/* for EEOP_AGG_*DESERIALIZE */
struct
{
@@ -898,6 +912,10 @@ extern void ExecEvalMergeSupportFunc(ExprState *state, ExprEvalStep *op,
ExprContext *econtext);
extern void ExecEvalSubPlan(ExprState *state, ExprEvalStep *op,
ExprContext *econtext);
+extern void ExecEvalRPRNavSet(ExprState *state, ExprEvalStep *op,
+ ExprContext *econtext);
+extern void ExecEvalRPRNavRestore(ExprState *state, ExprEvalStep *op,
+ ExprContext *econtext);
extern void ExecEvalWholeRowVar(ExprState *state, ExprEvalStep *op,
ExprContext *econtext);
extern void ExecEvalSysVar(ExprState *state, ExprEvalStep *op,
diff --git a/src/include/executor/nodeWindowAgg.h b/src/include/executor/nodeWindowAgg.h
index ada4a1c458c..f6f6645131c 100644
--- a/src/include/executor/nodeWindowAgg.h
+++ b/src/include/executor/nodeWindowAgg.h
@@ -20,4 +20,7 @@ extern WindowAggState *ExecInitWindowAgg(WindowAgg *node, EState *estate, int ef
extern void ExecEndWindowAgg(WindowAggState *node);
extern void ExecReScanWindowAgg(WindowAggState *node);
+/* RPR navigation support for expression evaluation opcodes */
+extern TupleTableSlot *ExecRPRNavGetSlot(WindowAggState *winstate, int64 pos);
+
#endif /* NODEWINDOWAGG_H */
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index c672d29f35b..74a6b682132 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -2691,10 +2691,12 @@ typedef struct WindowAggState
TupleTableSlot *temp_slot_1;
TupleTableSlot *temp_slot_2;
- /* temporary slots for RPR */
- TupleTableSlot *prev_slot; /* PREV row navigation operator */
- TupleTableSlot *next_slot; /* NEXT row navigation operator */
- TupleTableSlot *null_slot; /* all NULL slot */
+ /* RPR navigation */
+ struct WindowObjectData *nav_winobj; /* winobj for RPR nav fetch */
+ int64 nav_slot_pos; /* position cached in nav_slot, or -1 */
+ TupleTableSlot *nav_slot; /* slot for PREV/NEXT target row */
+ TupleTableSlot *nav_saved_outertuple; /* saved slot during nav swap */
+ TupleTableSlot *nav_null_slot; /* all NULL slot */
/* RPR current match result */
bool rpr_match_valid; /* true if a match result is set */
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index f5b6b45664a..94723a3b909 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -648,6 +648,37 @@ typedef struct WindowFuncRunCondition
Expr *arg;
} WindowFuncRunCondition;
+/*
+ * RPRNavExpr
+ *
+ * Represents a PREV() or NEXT() navigation call in an RPR DEFINE clause.
+ * At expression compile time this is translated into EEOP_RPR_NAV_SET /
+ * EEOP_RPR_NAV_RESTORE opcodes rather than a normal function call.
+ *
+ * kind: RPR_NAV_PREV or RPR_NAV_NEXT
+ * arg: the expression to evaluate against the target row
+ * offset_arg: optional explicit offset expression (2-arg form); NULL for
+ * the 1-arg form which uses an implicit offset of 1
+ */
+typedef enum RPRNavKind
+{
+ RPR_NAV_PREV,
+ RPR_NAV_NEXT,
+} RPRNavKind;
+
+typedef struct RPRNavExpr
+{
+ Expr xpr;
+ RPRNavKind kind; /* PREV or NEXT */
+ Expr *arg; /* argument expression */
+ Expr *offset_arg; /* offset expression, or NULL for 1-arg form */
+ Oid resulttype; /* result type (same as arg's type) */
+ /* OID of collation of result */
+ Oid resultcollid pg_node_attr(query_jumble_ignore);
+ /* token location, or -1 if unknown */
+ ParseLoc location;
+} RPRNavExpr;
+
/*
* MergeSupportFunc
*
diff --git a/src/test/regress/expected/rpr.out b/src/test/regress/expected/rpr.out
index e72171050c7..d586e17e0a1 100644
--- a/src/test/regress/expected/rpr.out
+++ b/src/test/regress/expected/rpr.out
@@ -1018,6 +1018,555 @@ WINDOW w AS (
5 | 0.1 | 0
(5 rows)
+--
+-- Error cases: PREV/NEXT usage restrictions
+--
+-- PREV outside DEFINE clause
+SELECT prev(price) FROM stock;
+ERROR: prev can only be used in a DEFINE clause
+LINE 1: SELECT prev(price) FROM stock;
+ ^
+-- NEXT outside DEFINE clause
+SELECT next(price) FROM stock;
+ERROR: next can only be used in a DEFINE clause
+LINE 1: SELECT next(price) FROM stock;
+ ^
+-- Nested PREV
+SELECT price FROM stock
+WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (A)
+ DEFINE A AS price > PREV(PREV(price))
+);
+ERROR: PREV and NEXT cannot be nested
+LINE 7: DEFINE A AS price > PREV(PREV(price))
+ ^
+-- Nested NEXT
+SELECT price FROM stock
+WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (A)
+ DEFINE A AS price > NEXT(NEXT(price))
+);
+ERROR: PREV and NEXT cannot be nested
+LINE 7: DEFINE A AS price > NEXT(NEXT(price))
+ ^
+-- PREV nested inside NEXT
+SELECT price FROM stock
+WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (A)
+ DEFINE A AS price > NEXT(PREV(price))
+);
+ERROR: PREV and NEXT cannot be nested
+LINE 7: DEFINE A AS price > NEXT(PREV(price))
+ ^
+-- PREV nested inside expression inside NEXT
+SELECT price FROM stock
+WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (A)
+ DEFINE A AS price > NEXT(price * PREV(price))
+);
+ERROR: PREV and NEXT cannot be nested
+LINE 7: DEFINE A AS price > NEXT(price * PREV(price))
+ ^
+-- Triple nesting: error reported at outermost PREV
+SELECT price FROM stock
+WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (A)
+ DEFINE A AS price > PREV(PREV(PREV(price)))
+);
+ERROR: PREV and NEXT cannot be nested
+LINE 7: DEFINE A AS price > PREV(PREV(PREV(price)))
+ ^
+-- No column reference in PREV/NEXT argument
+-- PREV(1): constant only, no column reference
+SELECT price FROM stock
+WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (A)
+ DEFINE A AS PREV(1) > 0
+);
+ERROR: argument of row pattern navigation operation must include at least one column reference
+LINE 7: DEFINE A AS PREV(1) > 0
+ ^
+-- NEXT(1 + 2): constant expression, no column reference
+SELECT price FROM stock
+WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (A)
+ DEFINE A AS NEXT(1 + 2) > 0
+);
+ERROR: argument of row pattern navigation operation must include at least one column reference
+LINE 7: DEFINE A AS NEXT(1 + 2) > 0
+ ^
+-- 2-arg form: PREV(1, 1): constant expression as first arg
+SELECT price FROM stock
+WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (A)
+ DEFINE A AS PREV(1, 1) > 0
+);
+ERROR: argument of row pattern navigation operation must include at least one column reference
+LINE 7: DEFINE A AS PREV(1, 1) > 0
+ ^
+-- Non-constant offset: column reference as 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(price, price) > 0
+);
+ERROR: PREV/NEXT offset must be a run-time constant
+LINE 7: DEFINE A AS PREV(price, price) > 0
+ ^
+-- Non-constant offset: volatile function as 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(price, random()::int) > 0
+);
+ERROR: PREV/NEXT offset must be a run-time constant
+LINE 7: DEFINE A AS PREV(price, random()::int) > 0
+ ^
+-- Non-constant offset: subquery as 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(price, (SELECT 1)) > 0
+);
+ERROR: cannot use subquery in DEFINE expression
+LINE 7: DEFINE A AS PREV(price, (SELECT 1)) > 0
+ ^
+-- First arg: subquery (caught by DEFINE-level subquery restriction)
+SELECT price FROM stock
+WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (A)
+ DEFINE A AS PREV(price + (SELECT 1)) > 0
+);
+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)
+SELECT company, tdate, price,
+ first_value(price) OVER w, last_value(price) OVER w, count(*) OVER w
+FROM stock
+WINDOW w AS (
+ PARTITION BY company ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ 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)
+
+--
+-- 2-arg PREV/NEXT: functional tests
+--
+-- PREV(price, 2): match rows where current price > price 2 rows back
+-- stock: 100, 90, 80, 95, 110
+-- Pattern (A B+): A=any, B where price > PREV(price, 2)
+-- At pos 2 (80): A matches. pos 3 (95): 95 > PREV(95,2)=90 TRUE.
+-- pos 4 (110): 110 > PREV(110,2)=80 TRUE. Match!
+SELECT company, tdate, price,
+ first_value(price) OVER w, last_value(price) OVER w, count(*) OVER w
+FROM stock
+WINDOW w AS (
+ PARTITION BY company ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE
+ A AS TRUE,
+ B AS price > PREV(price, 2)
+);
+ company | tdate | price | first_value | last_value | count
+----------+------------+-------+-------------+------------+-------
+ company1 | 07-01-2023 | 100 | | | 0
+ company1 | 07-02-2023 | 200 | 200 | 150 | 2
+ 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 | 110 | 120 | 3
+ 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 | 1500 | 2
+ 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 | 1100 | 1200 | 3
+ company2 | 07-08-2023 | 1300 | | | 0
+ company2 | 07-09-2023 | 1200 | | | 0
+ company2 | 07-10-2023 | 1300 | | | 0
+(20 rows)
+
+-- NEXT(price, 2): match rows where current price > price 2 rows ahead
+-- pos 0 (100): NEXT(100,2)=80, 100>80 TRUE. pos 1 (90): NEXT(90,2)=95, 90>95 FALSE. Match ends.
+SELECT company, tdate, price,
+ first_value(price) OVER w, last_value(price) OVER w, count(*) OVER w
+FROM stock
+WINDOW w AS (
+ PARTITION BY company ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS price > NEXT(price, 2)
+);
+ company | tdate | price | first_value | last_value | count
+----------+------------+-------+-------------+------------+-------
+ company1 | 07-01-2023 | 100 | | | 0
+ company1 | 07-02-2023 | 200 | 200 | 200 | 1
+ company1 | 07-03-2023 | 150 | | | 0
+ company1 | 07-04-2023 | 140 | 140 | 150 | 2
+ 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 | 2000 | 1
+ company2 | 07-03-2023 | 1500 | | | 0
+ company2 | 07-04-2023 | 1400 | 1400 | 1500 | 2
+ 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)
+
+-- Expressions inside PREV/NEXT arg: expr is evaluated on target row
+-- PREV(price - 50, 1): fetches (price - 50) from 1 row back
+SELECT company, tdate, price,
+ first_value(price) OVER w, last_value(price) OVER w, count(*) OVER w
+FROM stock
+WINDOW w AS (
+ PARTITION BY company ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS price > PREV(price - 50, 1)
+);
+ company | tdate | price | first_value | last_value | count
+----------+------------+-------+-------------+------------+-------
+ company1 | 07-01-2023 | 100 | | | 0
+ company1 | 07-02-2023 | 200 | 200 | 200 | 1
+ company1 | 07-03-2023 | 150 | | | 0
+ company1 | 07-04-2023 | 140 | 140 | 150 | 2
+ company1 | 07-05-2023 | 150 | | | 0
+ company1 | 07-06-2023 | 90 | | | 0
+ company1 | 07-07-2023 | 110 | 110 | 130 | 4
+ 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 | 2000 | 1
+ company2 | 07-03-2023 | 1500 | | | 0
+ company2 | 07-04-2023 | 1400 | | | 0
+ company2 | 07-05-2023 | 1500 | 1500 | 1500 | 1
+ company2 | 07-06-2023 | 60 | | | 0
+ company2 | 07-07-2023 | 1100 | 1100 | 1300 | 2
+ company2 | 07-08-2023 | 1300 | | | 0
+ company2 | 07-09-2023 | 1200 | | | 0
+ company2 | 07-10-2023 | 1300 | 1300 | 1300 | 1
+(20 rows)
+
+-- NEXT(price * 2, 1): fetches (price * 2) from 1 row ahead
+SELECT company, tdate, price,
+ first_value(price) OVER w, last_value(price) OVER w, count(*) OVER w
+FROM stock
+WINDOW w AS (
+ PARTITION BY company ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS price < NEXT(price * 2, 1)
+);
+ company | tdate | price | first_value | last_value | count
+----------+------------+-------+-------------+------------+-------
+ company1 | 07-01-2023 | 100 | 100 | 120 | 9
+ company1 | 07-02-2023 | 200 | | | 0
+ 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 | 50 | 1400 | 4
+ company2 | 07-02-2023 | 2000 | | | 0
+ company2 | 07-03-2023 | 1500 | | | 0
+ company2 | 07-04-2023 | 1400 | | | 0
+ company2 | 07-05-2023 | 1500 | | | 0
+ company2 | 07-06-2023 | 60 | 60 | 1200 | 4
+ 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)
+
+-- Large offset: PREV(val, 999) on 1000-row series matches only last row
+-- NEXT(val, 999) matches only first row
+SELECT val, first_value(val) OVER w, last_value(val) OVER w, count(*) OVER w
+FROM generate_series(1, 1000) AS t(val)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS PREV(val, 999) = 1
+)
+ORDER BY val DESC LIMIT 3;
+ val | first_value | last_value | count
+------+-------------+------------+-------
+ 1000 | 1000 | 1000 | 1
+ 999 | | | 0
+ 998 | | | 0
+(3 rows)
+
+SELECT val, first_value(val) OVER w, last_value(val) OVER w, count(*) OVER w
+FROM generate_series(1, 1000) AS t(val)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS NEXT(val, 999) = 1000
+)
+LIMIT 3;
+ val | first_value | last_value | count
+-----+-------------+------------+-------
+ 1 | 1 | 1 | 1
+ 2 | | | 0
+ 3 | | | 0
+(3 rows)
+
+-- PREV(price, 0): offset 0 means current row, always equal to price
+-- A+ matches entire partition as one group; count = partition size
+SELECT company, tdate, price,
+ first_value(price) OVER w, last_value(price) OVER w, count(*) OVER w
+FROM stock
+WINDOW w AS (
+ PARTITION BY company ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS PREV(price, 0) = price
+);
+ company | tdate | price | first_value | last_value | count
+----------+------------+-------+-------------+------------+-------
+ company1 | 07-01-2023 | 100 | 100 | 130 | 10
+ company1 | 07-02-2023 | 200 | | | 0
+ 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 | 50 | 1300 | 10
+ company2 | 07-02-2023 | 2000 | | | 0
+ 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)
+
+-- 2-arg PREV/NEXT outside DEFINE clause
+SELECT prev(price, 2) FROM stock;
+ERROR: prev can only be used in a DEFINE clause
+LINE 1: SELECT prev(price, 2) FROM stock;
+ ^
+SELECT next(price, 2) FROM stock;
+ERROR: next can only be used in a DEFINE clause
+LINE 1: SELECT next(price, 2) FROM stock;
+ ^
+-- 2-arg PREV/NEXT: negative offset
+SELECT company, tdate, price, first_value(price) OVER w
+FROM stock
+WINDOW w AS (
+ PARTITION BY company ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS PREV(price, -1) IS NOT NULL
+);
+ERROR: PREV/NEXT offset must not be negative
+-- 2-arg PREV/NEXT: NULL offset (typed)
+SELECT company, tdate, price, first_value(price) OVER w
+FROM stock
+WINDOW w AS (
+ PARTITION BY company ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS PREV(price, NULL::int8) IS NOT NULL
+);
+ERROR: PREV/NEXT offset must not be null
+-- 2-arg PREV/NEXT: NULL offset (untyped)
+SELECT company, tdate, price, first_value(price) OVER w
+FROM stock
+WINDOW w AS (
+ PARTITION BY company ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS PREV(price, NULL) IS NOT NULL
+);
+ERROR: PREV/NEXT offset must not be null
+-- 2-arg PREV/NEXT: host variable negative and NULL
+PREPARE test_prev_offset(int8) AS
+SELECT company, tdate, price, first_value(price) OVER w
+FROM stock
+WINDOW w AS (
+ PARTITION BY company ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS price > PREV(price, $1)
+);
+EXECUTE test_prev_offset(-1);
+ERROR: PREV/NEXT offset must not be negative
+EXECUTE test_prev_offset(NULL);
+ERROR: PREV/NEXT offset must not be null
+DEALLOCATE test_prev_offset;
+-- 2-arg PREV/NEXT: host variable with expression (0 + $1)
+PREPARE test_prev_offset(int8) AS
+SELECT company, tdate, price, first_value(price) OVER w
+FROM stock
+WINDOW w AS (
+ PARTITION BY company ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS price > PREV(price, 0 + $1)
+);
+EXECUTE test_prev_offset(-1);
+ERROR: PREV/NEXT offset must not be negative
+EXECUTE test_prev_offset(NULL);
+ERROR: PREV/NEXT offset must not be null
+DEALLOCATE test_prev_offset;
+-- 2-arg: two PREV with different offsets in same DEFINE clause
+-- B: price exceeds both 1-back and 2-back values
+SELECT company, tdate, price,
+ first_value(price) OVER w, last_value(price) OVER w, count(*) OVER w
+FROM stock
+WINDOW w AS (
+ PARTITION BY company ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B+)
+ DEFINE
+ A AS TRUE,
+ B AS price > PREV(price, 1) AND price > PREV(price, 2)
+);
+ company | tdate | price | first_value | last_value | count
+----------+------------+-------+-------------+------------+-------
+ company1 | 07-01-2023 | 100 | | | 0
+ company1 | 07-02-2023 | 200 | | | 0
+ 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 | 110 | 130 | 2
+ 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 | | | 0
+ 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 | 1100 | 1300 | 2
+ company2 | 07-08-2023 | 1300 | | | 0
+ company2 | 07-09-2023 | 1200 | | | 0
+ company2 | 07-10-2023 | 1300 | | | 0
+(20 rows)
+
+-- 2-arg: PREV and NEXT with explicit offsets in same DEFINE clause
+-- A: price exceeds 1-back and is below 1-ahead (ascending interior point)
+SELECT company, tdate, price,
+ first_value(price) OVER w, last_value(price) OVER w, count(*) OVER w
+FROM stock
+WINDOW w AS (
+ PARTITION BY company ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+)
+ DEFINE A AS price > PREV(price, 1) AND price < NEXT(price, 1)
+);
+ company | tdate | price | first_value | last_value | count
+----------+------------+-------+-------------+------------+-------
+ company1 | 07-01-2023 | 100 | | | 0
+ company1 | 07-02-2023 | 200 | | | 0
+ 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 | 110 | 110 | 1
+ 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 | | | 0
+ 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 | 1100 | 1100 | 1
+ company2 | 07-08-2023 | 1300 | | | 0
+ company2 | 07-09-2023 | 1200 | | | 0
+ company2 | 07-10-2023 | 1300 | | | 0
+(20 rows)
+
--
-- SKIP TO / Backtracking / Frame boundary
--
@@ -1479,7 +2028,7 @@ count(*) OVER w
(14 rows)
-- ReScan test: LATERAL join forces WindowAgg rescan with RPR
--- Tests ExecReScanWindowAgg clearing prev_slot/next_slot
+-- Tests ExecReScanWindowAgg clearing nav_slot
SELECT g.x, sub.*
FROM generate_series(1, 2) g(x),
LATERAL (
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index 6526365dd6a..37aa81ebdea 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -2273,7 +2273,7 @@ SELECT pg_get_viewdef('rpr_serial_quoted'::regclass);
PATTERN ("Start" "Up"+) +
DEFINE +
"Start" AS true, +
- "Up" AS (val > prev(val)) );
+ "Up" AS (val > PREV(val)) );
(1 row)
-- Materialized view (if supported)
diff --git a/src/test/regress/expected/rpr_explain.out b/src/test/regress/expected/rpr_explain.out
index a68ec61e10f..dc3075e6bd3 100644
--- a/src/test/regress/expected/rpr_explain.out
+++ b/src/test/regress/expected/rpr_explain.out
@@ -3847,6 +3847,86 @@ WINDOW w AS (
-> Function Scan on generate_series s (actual rows=30.00 loops=1)
(8 rows)
+-- Using 1-arg PREV (implicit offset 1)
+CREATE VIEW rpr_ev_nav_prev1 AS
+SELECT count(*) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B+)
+ DEFINE
+ A AS TRUE,
+ B AS v > PREV(v)
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_nav_prev1'), E'\n')) AS line WHERE line ~ 'PATTERN|DEFINE|PREV|NEXT';
+ line
+-------------------------
+ PATTERN (a b+)
+ DEFINE
+ b AS (v > PREV(v)) );
+(3 rows)
+
+-- Using 1-arg NEXT (implicit offset 1)
+CREATE VIEW rpr_ev_nav_next1 AS
+SELECT count(*) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B+)
+ DEFINE
+ A AS TRUE,
+ B AS v < NEXT(v)
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_nav_next1'), E'\n')) AS line WHERE line ~ 'PATTERN|DEFINE|PREV|NEXT';
+ line
+-------------------------
+ PATTERN (a b+)
+ DEFINE
+ b AS (v < NEXT(v)) );
+(3 rows)
+
+-- Using 2-arg PREV (explicit offset)
+CREATE VIEW rpr_ev_nav_prev2 AS
+SELECT count(*) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B+)
+ DEFINE
+ A AS TRUE,
+ B AS v > PREV(v, 2)
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_nav_prev2'), E'\n')) AS line WHERE line ~ 'PATTERN|DEFINE|PREV|NEXT';
+ line
+--------------------------------------
+ PATTERN (a b+)
+ DEFINE
+ b AS (v > PREV(v, (2)::bigint)) );
+(3 rows)
+
+-- Using 2-arg NEXT (explicit offset)
+CREATE VIEW rpr_ev_nav_next2 AS
+SELECT count(*) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B+)
+ DEFINE
+ A AS TRUE,
+ B AS v < NEXT(v, 2)
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_nav_next2'), E'\n')) AS line WHERE line ~ 'PATTERN|DEFINE|PREV|NEXT';
+ line
+--------------------------------------
+ PATTERN (a b+)
+ DEFINE
+ b AS (v < NEXT(v, (2)::bigint)) );
+(3 rows)
+
-- Using NULL comparisons
CREATE VIEW rpr_ev_def_null AS
SELECT count(*) OVER w
diff --git a/src/test/regress/expected/rpr_integration.out b/src/test/regress/expected/rpr_integration.out
index 0ee612b74fb..f9f70a814ca 100644
--- a/src/test/regress/expected/rpr_integration.out
+++ b/src/test/regress/expected/rpr_integration.out
@@ -1026,27 +1026,66 @@ WINDOW w AS (ORDER BY id
PATTERN (A B+)
DEFINE B AS val > PREV(val, $1))
ORDER BY id;
-ERROR: function prev(integer, integer) does not exist
-LINE 7: DEFINE B AS val > PREV(val, $1))
- ^
-DETAIL: No function of that name accepts the given number of arguments.
-- Custom plan: Nav Mark Lookback resolved to the literal 1.
SET plan_cache_mode = force_custom_plan;
EXPLAIN (COSTS OFF) EXECUTE rpr_prev(1);
-ERROR: prepared statement "rpr_prev" does not exist
+ QUERY PLAN
+-------------------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a b+
+ -> Sort
+ Sort Key: id
+ -> Seq Scan on rpr_integ
+(6 rows)
+
EXECUTE rpr_prev(1);
-ERROR: prepared statement "rpr_prev" does not exist
+ 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)
+
-- Generic plan: Nav Mark Lookback deferred to execution, shown as
-- "runtime" in the plan. Result must match the custom-plan result
-- exactly.
SET plan_cache_mode = force_generic_plan;
EXPLAIN (COSTS OFF) EXECUTE rpr_prev(1);
-ERROR: prepared statement "rpr_prev" does not exist
+ QUERY PLAN
+-------------------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a b+
+ -> Sort
+ Sort Key: id
+ -> Seq Scan on rpr_integ
+(6 rows)
+
EXECUTE rpr_prev(1);
-ERROR: prepared statement "rpr_prev" does not exist
+ 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)
+
RESET plan_cache_mode;
DEALLOCATE rpr_prev;
-ERROR: prepared statement "rpr_prev" does not exist
-- ============================================================
-- B5. RPR + Partitioned table
-- ============================================================
diff --git a/src/test/regress/sql/rpr.sql b/src/test/regress/sql/rpr.sql
index 95794d409e1..504476a2b02 100644
--- a/src/test/regress/sql/rpr.sql
+++ b/src/test/regress/sql/rpr.sql
@@ -440,6 +440,326 @@ WINDOW w AS (
B AS val > PREV(val) * 0.99
);
+--
+-- Error cases: PREV/NEXT usage restrictions
+--
+
+-- PREV outside DEFINE clause
+SELECT prev(price) FROM stock;
+
+-- NEXT outside DEFINE clause
+SELECT next(price) FROM stock;
+
+-- Nested PREV
+SELECT price FROM stock
+WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (A)
+ DEFINE A AS price > PREV(PREV(price))
+);
+
+-- Nested NEXT
+SELECT price FROM stock
+WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (A)
+ DEFINE A AS price > NEXT(NEXT(price))
+);
+
+-- PREV nested inside NEXT
+SELECT price FROM stock
+WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (A)
+ DEFINE A AS price > NEXT(PREV(price))
+);
+
+-- PREV nested inside expression inside NEXT
+SELECT price FROM stock
+WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (A)
+ DEFINE A AS price > NEXT(price * PREV(price))
+);
+
+-- Triple nesting: error reported at outermost PREV
+SELECT price FROM stock
+WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (A)
+ DEFINE A AS price > PREV(PREV(PREV(price)))
+);
+
+-- No column reference in PREV/NEXT argument
+-- PREV(1): constant only, no column reference
+SELECT price FROM stock
+WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (A)
+ DEFINE A AS PREV(1) > 0
+);
+
+-- NEXT(1 + 2): constant expression, no column reference
+SELECT price FROM stock
+WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (A)
+ DEFINE A AS NEXT(1 + 2) > 0
+);
+
+-- 2-arg form: PREV(1, 1): constant expression as first arg
+SELECT price FROM stock
+WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (A)
+ DEFINE A AS PREV(1, 1) > 0
+);
+
+-- Non-constant offset: column reference as 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(price, price) > 0
+);
+
+-- Non-constant offset: volatile function as 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(price, random()::int) > 0
+);
+
+-- Non-constant offset: subquery as 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(price, (SELECT 1)) > 0
+);
+
+-- First arg: subquery (caught by DEFINE-level subquery restriction)
+SELECT price FROM stock
+WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (A)
+ DEFINE A AS PREV(price + (SELECT 1)) > 0
+);
+
+-- First arg: volatile function is allowed (evaluated on target row)
+SELECT company, tdate, price,
+ first_value(price) OVER w, last_value(price) OVER w, count(*) OVER w
+FROM stock
+WINDOW w AS (
+ PARTITION BY company ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS PREV(price + random() * 0) >= 0
+);
+
+--
+-- 2-arg PREV/NEXT: functional tests
+--
+
+-- PREV(price, 2): match rows where current price > price 2 rows back
+-- stock: 100, 90, 80, 95, 110
+-- Pattern (A B+): A=any, B where price > PREV(price, 2)
+-- At pos 2 (80): A matches. pos 3 (95): 95 > PREV(95,2)=90 TRUE.
+-- pos 4 (110): 110 > PREV(110,2)=80 TRUE. Match!
+SELECT company, tdate, price,
+ first_value(price) OVER w, last_value(price) OVER w, count(*) OVER w
+FROM stock
+WINDOW w AS (
+ PARTITION BY company ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE
+ A AS TRUE,
+ B AS price > PREV(price, 2)
+);
+
+-- NEXT(price, 2): match rows where current price > price 2 rows ahead
+-- pos 0 (100): NEXT(100,2)=80, 100>80 TRUE. pos 1 (90): NEXT(90,2)=95, 90>95 FALSE. Match ends.
+SELECT company, tdate, price,
+ first_value(price) OVER w, last_value(price) OVER w, count(*) OVER w
+FROM stock
+WINDOW w AS (
+ PARTITION BY company ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS price > NEXT(price, 2)
+);
+
+-- Expressions inside PREV/NEXT arg: expr is evaluated on target row
+-- PREV(price - 50, 1): fetches (price - 50) from 1 row back
+SELECT company, tdate, price,
+ first_value(price) OVER w, last_value(price) OVER w, count(*) OVER w
+FROM stock
+WINDOW w AS (
+ PARTITION BY company ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS price > PREV(price - 50, 1)
+);
+
+-- NEXT(price * 2, 1): fetches (price * 2) from 1 row ahead
+SELECT company, tdate, price,
+ first_value(price) OVER w, last_value(price) OVER w, count(*) OVER w
+FROM stock
+WINDOW w AS (
+ PARTITION BY company ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS price < NEXT(price * 2, 1)
+);
+
+-- Large offset: PREV(val, 999) on 1000-row series matches only last row
+-- NEXT(val, 999) matches only first row
+SELECT val, first_value(val) OVER w, last_value(val) OVER w, count(*) OVER w
+FROM generate_series(1, 1000) AS t(val)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS PREV(val, 999) = 1
+)
+ORDER BY val DESC LIMIT 3;
+
+SELECT val, first_value(val) OVER w, last_value(val) OVER w, count(*) OVER w
+FROM generate_series(1, 1000) AS t(val)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS NEXT(val, 999) = 1000
+)
+LIMIT 3;
+
+-- PREV(price, 0): offset 0 means current row, always equal to price
+-- A+ matches entire partition as one group; count = partition size
+SELECT company, tdate, price,
+ first_value(price) OVER w, last_value(price) OVER w, count(*) OVER w
+FROM stock
+WINDOW w AS (
+ PARTITION BY company ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS PREV(price, 0) = price
+);
+
+-- 2-arg PREV/NEXT outside DEFINE clause
+SELECT prev(price, 2) FROM stock;
+SELECT next(price, 2) FROM stock;
+
+-- 2-arg PREV/NEXT: negative offset
+SELECT company, tdate, price, first_value(price) OVER w
+FROM stock
+WINDOW w AS (
+ PARTITION BY company ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS PREV(price, -1) IS NOT NULL
+);
+
+-- 2-arg PREV/NEXT: NULL offset (typed)
+SELECT company, tdate, price, first_value(price) OVER w
+FROM stock
+WINDOW w AS (
+ PARTITION BY company ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS PREV(price, NULL::int8) IS NOT NULL
+);
+
+-- 2-arg PREV/NEXT: NULL offset (untyped)
+SELECT company, tdate, price, first_value(price) OVER w
+FROM stock
+WINDOW w AS (
+ PARTITION BY company ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS PREV(price, NULL) IS NOT NULL
+);
+
+-- 2-arg PREV/NEXT: host variable negative and NULL
+PREPARE test_prev_offset(int8) AS
+SELECT company, tdate, price, first_value(price) OVER w
+FROM stock
+WINDOW w AS (
+ PARTITION BY company ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS price > PREV(price, $1)
+);
+EXECUTE test_prev_offset(-1);
+EXECUTE test_prev_offset(NULL);
+DEALLOCATE test_prev_offset;
+
+-- 2-arg PREV/NEXT: host variable with expression (0 + $1)
+PREPARE test_prev_offset(int8) AS
+SELECT company, tdate, price, first_value(price) OVER w
+FROM stock
+WINDOW w AS (
+ PARTITION BY company ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS price > PREV(price, 0 + $1)
+);
+EXECUTE test_prev_offset(-1);
+EXECUTE test_prev_offset(NULL);
+DEALLOCATE test_prev_offset;
+
+-- 2-arg: two PREV with different offsets in same DEFINE clause
+-- B: price exceeds both 1-back and 2-back values
+SELECT company, tdate, price,
+ first_value(price) OVER w, last_value(price) OVER w, count(*) OVER w
+FROM stock
+WINDOW w AS (
+ PARTITION BY company ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B+)
+ DEFINE
+ A AS TRUE,
+ B AS price > PREV(price, 1) AND price > PREV(price, 2)
+);
+
+-- 2-arg: PREV and NEXT with explicit offsets in same DEFINE clause
+-- A: price exceeds 1-back and is below 1-ahead (ascending interior point)
+SELECT company, tdate, price,
+ first_value(price) OVER w, last_value(price) OVER w, count(*) OVER w
+FROM stock
+WINDOW w AS (
+ PARTITION BY company ORDER BY tdate
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+)
+ DEFINE A AS price > PREV(price, 1) AND price < NEXT(price, 1)
+);
+
--
-- SKIP TO / Backtracking / Frame boundary
--
@@ -671,7 +991,7 @@ count(*) OVER w
);
-- ReScan test: LATERAL join forces WindowAgg rescan with RPR
--- Tests ExecReScanWindowAgg clearing prev_slot/next_slot
+-- Tests ExecReScanWindowAgg clearing nav_slot
SELECT g.x, sub.*
FROM generate_series(1, 2) g(x),
LATERAL (
diff --git a/src/test/regress/sql/rpr_explain.sql b/src/test/regress/sql/rpr_explain.sql
index 703ecd3b23b..e339edd7e91 100644
--- a/src/test/regress/sql/rpr_explain.sql
+++ b/src/test/regress/sql/rpr_explain.sql
@@ -2214,6 +2214,62 @@ WINDOW w AS (
D AS v < PREV(v)
);');
+-- Using 1-arg PREV (implicit offset 1)
+CREATE VIEW rpr_ev_nav_prev1 AS
+SELECT count(*) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B+)
+ DEFINE
+ A AS TRUE,
+ B AS v > PREV(v)
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_nav_prev1'), E'\n')) AS line WHERE line ~ 'PATTERN|DEFINE|PREV|NEXT';
+
+-- Using 1-arg NEXT (implicit offset 1)
+CREATE VIEW rpr_ev_nav_next1 AS
+SELECT count(*) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B+)
+ DEFINE
+ A AS TRUE,
+ B AS v < NEXT(v)
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_nav_next1'), E'\n')) AS line WHERE line ~ 'PATTERN|DEFINE|PREV|NEXT';
+
+-- Using 2-arg PREV (explicit offset)
+CREATE VIEW rpr_ev_nav_prev2 AS
+SELECT count(*) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B+)
+ DEFINE
+ A AS TRUE,
+ B AS v > PREV(v, 2)
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_nav_prev2'), E'\n')) AS line WHERE line ~ 'PATTERN|DEFINE|PREV|NEXT';
+
+-- Using 2-arg NEXT (explicit offset)
+CREATE VIEW rpr_ev_nav_next2 AS
+SELECT count(*) OVER w
+FROM generate_series(1, 30) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B+)
+ DEFINE
+ A AS TRUE,
+ B AS v < NEXT(v, 2)
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_nav_next2'), E'\n')) AS line WHERE line ~ 'PATTERN|DEFINE|PREV|NEXT';
+
-- Using NULL comparisons
CREATE VIEW rpr_ev_def_null AS
SELECT count(*) OVER w
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 6090c2d8950..16de1421302 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1801,7 +1801,6 @@ NamedLWLockTrancheRequest
NamedTuplestoreScan
NamedTuplestoreScanState
NamespaceInfo
-NavigationInfo
NestLoop
NestLoopParam
NestLoopState
@@ -2478,6 +2477,8 @@ QuerySource
QueueBackendStatus
QueuePosition
QuitSignalReason
+RPRNavExpr
+RPRNavKind
RBTNode
RBTOrderControl
RBTree
--
2.50.1 (Apple Git-155)