public inbox for [email protected]
help / color / mirror / Atom feedFrom: Henson Choi <[email protected]>
To: Tatsuo Ishii <[email protected]>
Cc: [email protected]
Cc: [email protected]
Cc: [email protected]
Cc: [email protected]
Cc: [email protected]
Cc: [email protected]
Cc: [email protected]
Cc: [email protected]
Subject: Re: Row pattern recognition
Date: Sun, 12 Apr 2026 16:27:26 +0900
Message-ID: <CAAAe_zB7rAEJtT6hXgF85=_Tj8Nti45ZHbQw26gxTF2DBs3hJw@mail.gmail.com> (raw)
In-Reply-To: <[email protected]>
References: <CAAAe_zDEDtJEf6aO=x3qhPOs8kXmsha7Q8ROJXE8VZNp+Dxe2A@mail.gmail.com>
<[email protected]>
<CAAAe_zCCnCJYd2aovB1wf1BFXCSR1raFmVZS1dZjj76_Vtyt-Q@mail.gmail.com>
<[email protected]>
Hi Tatsuo,
When I started this work a little over three months ago, I had
no idea it would grow into what it is today. There is still
much to refine, but I would not have come this far without
your guidance and support along the way.
This series completes RPR navigation support, including
PREV/NEXT, FIRST/LAST, and compound navigation such as
PREV(FIRST(col)).
Attached are 31 incremental patches on top of v46 (replacing
the previous 15-patch set). Here is a summary of the changes
from the previous version:
0001-0007: Unchanged from the previous set.
0008: Replace reduced frame map with single match result
(revised: int -> int64 type widening for
row_is_in_reduced_frame() and related variables,
cascading from 0025)
0009: Add fixed-length group absorption for RPR
(unchanged, design note [1])
0010: Rename rpr_explain test views to descriptive
names (unchanged)
0011: Fix quote_identifier() for RPR pattern variable
name deparse (new)
Pattern variable names that match SQL keywords were
not being quoted in pg_get_viewdef() output, causing
the deparsed view to fail on re-parse.
0012: Fix execRPR.o ordering in executor Makefile to
match meson.build (new)
Ensures consistent build file ordering between Make
and Meson build systems.
0013: Remove unused force_colno parameter from RPR
deparse functions (new)
Dead parameter cleanup in get_rpr_pattern() and
get_rpr_define() in ruleutils.c.
0014: Add CHECK_FOR_INTERRUPTS to RPR context cleanup
and finalize loops (new)
Extends interrupt checking to ExecRPRCleanupContexts()
and ExecRPRFinalizeMatch() loops, complementing the
earlier additions in 0002 and 0003.
0015: Narrow variable scope in ExecInitWindowAgg DEFINE
clause loop (new)
Moves local variable declarations into their
respective loop bodies for clarity.
0016: Normalize RPR element flag macros to return bool
(new)
RPR_ELEM_IS_* macros now return bool instead of the
raw bitmask value, consistent with PostgreSQL style.
0017: Implement 1-slot PREV/NEXT navigation for RPR
(was 0011, context changes from 0008 int64)
0018: Add JIT compilation support for RPR PREV/NEXT
(was 0012, unchanged)
0019: Add tuplestore trim optimization for RPR PREV
navigation (was 0013, context changes from 0008
int64)
0020: Update RPR code comments to reflect 1-slot
navigation model (was 0014, context only)
0021: Enable JIT compilation for PREV/NEXT navigation
tests in RPR (new)
Adds test coverage for JIT-compiled PREV/NEXT
expressions under jit_above_cost=0, verifying
that the slot reload path produces correct results.
0022: Add 2-arg PREV/NEXT test for row pattern
navigation with host variable (new)
Tests the PREV(value, offset) form where offset
is a host variable (prepared statement parameter),
exercising the RPR_NAV_OFFSET_NEEDS_EVAL path.
0023: Add Nav Mark Lookback to EXPLAIN and fix
compute_nav_max_offset() (new)
EXPLAIN now displays "Nav Mark Lookback: N" for
RPR windows with PREV navigation, showing how
many rows the tuplestore retains. Also fixes
compute_nav_max_offset() to correctly handle
non-constant offset expressions.
0024: Implement FIRST/LAST and compound navigation
for RPR (was 0015, completed)
The previous WIP patch is now complete with:
- Compound navigation: PREV(FIRST(col)),
NEXT(LAST(col, N)), etc. per SQL standard 5.6.4.
Flattened into a single RPRNavExpr node with
two offset values (inner position + outer offset).
- RPRNavOffsetKind enum replaces sentinel macros
for offset classification (CONSTANT, NEEDS_EVAL,
FIRST_BASED).
- Per-context DEFINE re-evaluation for match_start-
dependent expressions (FIRST, LAST with offset),
tracked via defineMatchStartDependent bitmapset
computed by the planner.
- Tuplestore mark handling extended for FIRST-based
navigation: mark position accounts for both
backward reach (PREV) and forward-from-match-start
reach (FIRST).
- Documentation added to func-window.sgml.
See the design note [2] for the absorption safety
analysis and evaluation sharing rules.
0025: Guard against int64 overflow in RPR bounded frame
end computation (new)
Widens frame position variables from int to int64
in row_is_in_reduced_frame() and related functions.
Prevents overflow when frame end position exceeds
INT_MAX in large partitions.
0026: Fix RPR error message style: hint format,
terminology, capitalization (new)
Aligns RPR error messages with PostgreSQL conventions:
lowercase first word, consistent use of "row pattern"
terminology, HINT format corrections.
0027: Fix comment typos, grammar, and inaccuracies in
RPR code (new)
Comment-only changes across execRPR.c, parse_rpr.c,
nodeWindowAgg.c, and ruleutils.c. No functional
changes.
0028: Fix RPR documentation: synopsis, grammar, and
terminology (new)
Updates select.sgml, advanced.sgml, and
func-window.sgml for consistent terminology
and corrected synopsis examples.
0029: Fix nav_slot pass-by-ref dangling pointer in RPR
navigation (new)
When a DEFINE expression contains multiple navigation
calls targeting different positions (e.g.,
PREV(x,1) > PREV(x,2)), the second call re-fetches
nav_slot, freeing the previous tuple via pfree. Any
pass-by-ref datum extracted from the first navigation
becomes a dangling pointer. Fix by copying pass-by-ref
results into per-tuple memory in the RESTORE step.
0030: Add inline comments for complex RPR algorithms and
design notes (new)
Adds explanatory comments to key algorithms in
execRPR.c and rpr.c (planner). Covers NFA absorption
logic, match evaluation flow, navigation mark
computation, and DEFINE re-evaluation decisions.
Comment-only changes, no functional changes.
0031: Remove unused include and fix header ordering in RPR
files (new)
Removes an unnecessary include from execExprInterp.c,
drops a redundant nodeWindowAgg.h include from
nodeWindowAgg.c, and fixes header ordering in
parse_rpr.c to match PostgreSQL convention.
Changes from the previous version:
- 0015 (FIRST/LAST) is now complete as 0024, with compound
navigation, RPRNavOffsetKind, defineMatchStartDependent,
and full test coverage.
- 16 new patches (0011-0016, 0021-0023, 0025-0031) added
since the previous set. These are all quality improvements:
bug fixes, safety guards, style normalization, comments,
and test coverage.
- New features: compound navigation in 0024 (was "not
yet done" in previous set), Nav Mark Lookback in
EXPLAIN output (0023). All other new patches are
quality improvements.
I am planning to submit PREFIX pattern absorption [3]
as a separate patch after this set is committed. The
changes are confined to the Absorb phase with no
external dependencies, so it can be submitted
independently. The design itself has some complexity
(shadow path tracking), and I would like to refine it
further until I am fully confident in the approach.
With this set I have covered the features I could
foresee. Whether anything else belongs in scope is
your call. From here, I think the emphasis should
shift from adding features to raising overall
quality.
If there are directions you think should be explored,
I am happy to work on them. Otherwise, I plan to focus
on code review, test coverage, and stabilization, and
will continue to submit bug fixes and test additions
as I find them.
I also very much welcome refactoring or restructuring
suggestions from a committer's maintainability
perspective. If anything in the current code will be
painful to maintain long-term, I would much rather
address it now than leave it for after commit.
For stabilization, I am thinking of the same approach
as in early January: gcov analysis, Valgrind runs, and
expanding the test suite.
**WITH THE PLANNER**, the biggest risk is what we do
not know we are missing -- there are too many possible
feature interactions to enumerate up front. To surface
these, I plan to run RPR-only tests under gcov and
review the uncovered planner paths: any line that RPR
could plausibly reach but does not becomes a candidate
risk, and a candidate for a new test. That turns the
unknown into a concrete review list rather than a guess
about which interactions to try. Does that seem like a
good direction?
If anything comes to mind that should be added or
fixed -- at any time -- please let me know.
[1] Fixed-length group absorption design note
https://www.postgresql.org/message-id/CAAAe_zAKAGKpK9iHx3ZSuG59sP93r5dfootqv5tCfaMt%3Dw6wzA%40mail.g...
[2] FIRST/LAST navigation design note
https://www.postgresql.org/message-id/CAAAe_zCUrKGBgZdaazdO_v9QWHsS_1DXuP%3DrLeNhO3iwsHwwbg%40mail.g...
[3] PREFIX pattern absorption design note
https://www.postgresql.org/message-id/CAAAe_zDq7R8CaDfmh8C%2BH3_639Y5LtJD%2B2Z%3D1txDt%3DvaOr90rQ%40...
Best regards,
Henson
From f79d4358bc0033bc4fe8b4f8c9e32904d3df6a93 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Tue, 24 Mar 2026 19:04:19 +0900
Subject: [PATCH] Remove unused regex/regex.h include from nodeWindowAgg.c
---
src/backend/executor/nodeWindowAgg.c | 1 -
1 file changed, 1 deletion(-)
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 4f882b877b1..185d7a0d5ae 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -50,7 +50,6 @@
#include "optimizer/rpr.h"
#include "parser/parse_agg.h"
#include "parser/parse_coerce.h"
-#include "regex/regex.h"
#include "utils/acl.h"
#include "utils/builtins.h"
#include "utils/datum.h"
--
2.50.1 (Apple Git-155)
From 31e07dcbd5391b7ff9ef8293fcb090cf8f845c71 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Wed, 25 Mar 2026 00:25:40 +0900
Subject: [PATCH] Add CHECK_FOR_INTERRUPTS() to nfa_add_state_unique() for
state explosion patterns
---
src/backend/executor/execRPR.c | 2 ++
1 file changed, 2 insertions(+)
diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index bab5257f68f..cf54e0c76c3 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -1763,6 +1763,8 @@ nfa_add_state_unique(WindowAggState *winstate, RPRNFAContext *ctx, RPRNFAState *
/* Check for duplicate and find tail */
for (s = ctx->states; s != NULL; s = s->next)
{
+ CHECK_FOR_INTERRUPTS();
+
if (nfa_states_equal(winstate, s, state))
{
/*
--
2.50.1 (Apple Git-155)
From 0f15fdabc01fc1503f2a13253df65844ece4c86d Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Wed, 25 Mar 2026 11:03:39 +0900
Subject: [PATCH] Add CHECK_FOR_INTERRUPTS() to nfa_try_absorb_context() loop
---
src/backend/executor/execRPR.c | 2 ++
1 file changed, 2 insertions(+)
diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index cf54e0c76c3..58f9da0b814 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -2084,6 +2084,8 @@ nfa_try_absorb_context(WindowAggState *winstate, RPRNFAContext *ctx)
for (older = ctx->prev; older != NULL; older = older->prev)
{
+ CHECK_FOR_INTERRUPTS();
+
/*
* By invariant: ctx->prev chain is in creation order (oldest first),
* and each row creates at most one context. So all contexts in this
--
2.50.1 (Apple Git-155)
From 6601dda3d297ee8928bbe1c035102d683c78251f Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Wed, 25 Mar 2026 00:20:05 +0900
Subject: [PATCH] Fix in-place modification of defineClause TargetEntry in
setrefs.c
---
src/backend/optimizer/plan/setrefs.c | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c
index 69cd1861e9b..813a326bd78 100644
--- a/src/backend/optimizer/plan/setrefs.c
+++ b/src/backend/optimizer/plan/setrefs.c
@@ -2633,7 +2633,7 @@ set_upper_references(PlannerInfo *root, Plan *plan, int rtoffset)
NUM_EXEC_QUAL(plan));
/*
- * Modifies an expression tree in each DEFINE clause so that all Var
+ * Replace an expression tree in each DEFINE clause so that all Var
* nodes's varno refers to OUTER_VAR.
*/
if (IsA(plan, WindowAgg))
@@ -2646,6 +2646,7 @@ set_upper_references(PlannerInfo *root, Plan *plan, int rtoffset)
{
TargetEntry *tle = (TargetEntry *) lfirst(l);
+ tle = flatCopyTargetEntry(tle);
tle->expr = (Expr *)
fix_upper_expr(root,
(Node *) tle->expr,
@@ -2654,6 +2655,7 @@ set_upper_references(PlannerInfo *root, Plan *plan, int rtoffset)
rtoffset,
NRM_EQUAL,
NUM_EXEC_QUAL(plan));
+ lfirst(l) = tle;
}
}
}
--
2.50.1 (Apple Git-155)
From 31e7dacb8b0fa6ead63ff92c19aa5dfb0cde76a1 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Wed, 25 Mar 2026 00:37:50 +0900
Subject: [PATCH] Fix mark handling for last_value() under RPR
Enable mark advancement in window_last_value() for
better tuplestore memory usage in non-RPR cases, while
adding a guard in WinGetFuncArgInFrame to suppress it
for RPR SEEK_TAIL to prevent position invalidation
from reduced frame shifts.
---
src/backend/executor/nodeWindowAgg.c | 10 ++++++++++
src/backend/utils/adt/windowfuncs.c | 2 +-
2 files changed, 11 insertions(+), 1 deletion(-)
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 185d7a0d5ae..aed7cbef99a 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -4932,7 +4932,17 @@ WinGetSlotInFrame(WindowObject winobj, TupleTableSlot *slot,
if (isout)
*isout = false;
if (set_mark)
+ {
+ /*
+ * If RPR is enabled and seek type is WINDOW_SEEK_TAIL, we set the
+ * mark position unconditionally to frameheadpos. In this case the
+ * frame always starts at CURRENT_ROW and never goes back, thus
+ * setting the mark at the position is safe.
+ */
+ if (winstate->rpPattern != NULL && seektype == WINDOW_SEEK_TAIL)
+ mark_pos = winstate->frameheadpos;
WinSetMarkPosition(winobj, mark_pos);
+ }
return 0;
out_of_frame:
diff --git a/src/backend/utils/adt/windowfuncs.c b/src/backend/utils/adt/windowfuncs.c
index efb60c99052..74ef109f72e 100644
--- a/src/backend/utils/adt/windowfuncs.c
+++ b/src/backend/utils/adt/windowfuncs.c
@@ -682,7 +682,7 @@ window_last_value(PG_FUNCTION_ARGS)
WinCheckAndInitializeNullTreatment(winobj, true, fcinfo);
result = WinGetFuncArgInFrame(winobj, 0,
- 0, WINDOW_SEEK_TAIL, false,
+ 0, WINDOW_SEEK_TAIL, true,
&isnull, NULL);
if (isnull)
PG_RETURN_NULL();
--
2.50.1 (Apple Git-155)
From 7dd896a8b279a1f02e8a5d9f241d9a59a32ee588 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Thu, 2 Apr 2026 11:55:02 +0900
Subject: [PATCH] Fix DEFINE expression handling in RPR window planning
transformDefineClause() added full DEFINE expressions including
RPRNavExpr (PREV/NEXT) nodes to the query targetlist. These
propagated to upper WindowAgg nodes that lack RPR navigation state,
causing a SIGSEGV when RPR and non-RPR windows coexist in the same
query.
Add only the Var nodes referenced by DEFINE expressions to the
targetlist, and protect those Vars from removal by
remove_unused_subquery_outputs() so they remain available in the
tuplestore slot for pattern matching evaluation.
Move the subquery wrapping tests from rpr.sql to rpr_integration.sql.
---
src/backend/optimizer/path/allpaths.c | 68 +++++++++++++++++
src/backend/parser/parse_rpr.c | 106 ++++++++++++++------------
2 files changed, 126 insertions(+), 48 deletions(-)
diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c
index f42a2bae14a..f0ef4e289a6 100644
--- a/src/backend/optimizer/path/allpaths.c
+++ b/src/backend/optimizer/path/allpaths.c
@@ -4750,6 +4750,74 @@ remove_unused_subquery_outputs(Query *subquery, RelOptInfo *rel,
if (contain_volatile_functions(texpr))
continue;
+ /*
+ * If any RPR window clause references this column in its DEFINE
+ * clause, don't remove it. The DEFINE expression needs these columns
+ * in the tuplestore slot for pattern matching evaluation, even if the
+ * outer query doesn't reference them.
+ */
+ if (IsA(texpr, Var))
+ {
+ Var *var = (Var *) texpr;
+ ListCell *wlc;
+ bool needed_by_define = false;
+
+ foreach(wlc, subquery->windowClause)
+ {
+ WindowClause *wc = lfirst_node(WindowClause, wlc);
+
+ if (wc->defineClause != NIL)
+ {
+ List *vars = pull_var_clause((Node *) wc->defineClause, 0);
+ ListCell *vlc;
+
+ foreach(vlc, vars)
+ {
+ Var *dvar = (Var *) lfirst(vlc);
+
+ if (dvar->varattno == var->varattno)
+ {
+ needed_by_define = true;
+ break;
+ }
+ }
+ list_free(vars);
+ if (needed_by_define)
+ break;
+ }
+ }
+ if (needed_by_define)
+ continue;
+ }
+
+ /*
+ * If it's a window function referencing a window clause with RPR (Row
+ * Pattern Recognition), don't remove it. Even when the window
+ * function result is unused by the outer query, the RPR pattern
+ * matching (frame reduction via DEFINE/PATTERN) must still execute.
+ * Replacing this with NULL would leave no active window functions for
+ * the WindowClause, causing the planner to omit the WindowAgg node
+ * entirely.
+ */
+ if (IsA(texpr, WindowFunc))
+ {
+ WindowFunc *wfunc = (WindowFunc *) texpr;
+ ListCell *wlc;
+
+ foreach(wlc, subquery->windowClause)
+ {
+ WindowClause *wc = lfirst_node(WindowClause, wlc);
+
+ if (wc->winref == wfunc->winref &&
+ wc->defineClause != NIL)
+ {
+ break;
+ }
+ }
+ if (wlc != NULL)
+ continue;
+ }
+
/*
* OK, we don't need it. Replace the expression with a NULL constant.
* Preserve the exposed type of the expression, in case something
diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index 55283ab4bbe..db1309ca311 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -28,6 +28,7 @@
#include "miscadmin.h"
#include "nodes/makefuncs.h"
#include "nodes/nodeFuncs.h"
+#include "optimizer/optimizer.h"
#include "optimizer/rpr.h"
#include "parser/parse_clause.h"
#include "parser/parse_collate.h"
@@ -310,9 +311,7 @@ transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
restargets = NIL;
foreach(lc, windef->rpCommonSyntax->rpDefs)
{
- TargetEntry *te,
- *teDefine;
- int defineExprLocation;
+ TargetEntry *teDefine;
restarget = (ResTarget *) lfirst(lc);
name = restarget->name;
@@ -335,57 +334,68 @@ transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
restargets = lappend(restargets, restarget);
/*
- * Transform the DEFINE expression (restarget->val) and add it to the
- * targetlist as a TargetEntry if not already present, so the planner
- * can propagate the referenced columns to the outer plan's
- * targetlist.
+ * Transform the DEFINE expression. We must NOT add the whole
+ * expression to the query targetlist, because it may contain
+ * RPRNavExpr nodes (PREV/NEXT) that can only be evaluated inside the
+ * owning WindowAgg.
*
- * Note: findTargetlistEntrySQL99 transforms and clobbers
- * restarget->val.
+ * Instead, we transform the expression directly and only ensure that
+ * the individual Var nodes it references are present in the
+ * targetlist, so the planner can propagate the referenced columns.
*/
+ {
+ Node *expr;
+ List *vars;
+ ListCell *lc2;
- /*
- * Save the original expression location before transformation.
- * findTargetlistEntrySQL99 may return an existing TargetEntry whose
- * location points to where it was originally created (e.g., ORDER
- * BY), not the DEFINE clause. We need to preserve the DEFINE location
- * for accurate error reporting.
- */
- defineExprLocation = exprLocation(restarget->val);
+ expr = transformExpr(pstate, restarget->val,
+ EXPR_KIND_RPR_DEFINE);
+
+ /*
+ * Pull out Var nodes from the transformed expression and ensure
+ * each one is present in the targetlist. This is needed so the
+ * planner propagates the referenced columns through the plan
+ * tree, making them available to the WindowAgg's DEFINE
+ * evaluation.
+ */
+ vars = pull_var_clause(expr, 0);
+ foreach(lc2, vars)
+ {
+ Var *var = (Var *) lfirst(lc2);
+ bool found = false;
+ ListCell *tl;
- te = findTargetlistEntrySQL99(pstate, restarget->val,
- targetlist, EXPR_KIND_RPR_DEFINE);
+ foreach(tl, *targetlist)
+ {
+ TargetEntry *tle = (TargetEntry *) lfirst(tl);
- /* -------------------
- * Copy the TargetEntry for defineClause and always set the pattern
- * variable name. We use copyObject so the original targetlist entry
- * is not modified.
- *
- * Note: We must always set resname to the pattern variable name.
- * findTargetlistEntrySQL99 creates new TEs with resname = NULL
- * (resjunk entries), but returns existing TEs unchanged when the
- * expression already exists in targetlist.
- *
- * Example: "SELECT id, flag, ... WINDOW w AS (... DEFINE T AS flag)"
- *
- * 1. SELECT list processing creates: TE{resname="flag", expr=flag}
- * 2. DEFINE T AS flag: findTargetlistEntrySQL99 finds existing TE
- * 3. te->resname is "flag" (from SELECT), not NULL
- * 4. Without unconditionally setting resname, teDefine->resname
- * would remain "flag" instead of pattern variable name "T"
- * 5. buildRPRPattern builds defineVariableList from resname, so
- * it would contain ["flag"] instead of ["T"]
- * 6. Pattern variable "T" not found -> Assert failure crash
- */
- teDefine = copyObject(te);
- teDefine->resname = pstrdup(name);
+ if (IsA(tle->expr, Var) &&
+ ((Var *) tle->expr)->varno == var->varno &&
+ ((Var *) tle->expr)->varattno == var->varattno)
+ {
+ found = true;
+ break;
+ }
+ }
+ if (!found)
+ {
+ TargetEntry *newtle;
- /*
- * Update the expression location to point to the DEFINE clause. This
- * ensures error messages reference the correct source location.
- */
- if (defineExprLocation >= 0 && IsA(teDefine->expr, Var))
- ((Var *) teDefine->expr)->location = defineExprLocation;
+ newtle = makeTargetEntry((Expr *) copyObject(var),
+ list_length(*targetlist) + 1,
+ NULL,
+ true);
+ *targetlist = lappend(*targetlist, newtle);
+ }
+ }
+ list_free(vars);
+
+ /* Build the defineClause entry directly from the transformed expr */
+ teDefine = makeTargetEntry((Expr *) expr,
+ list_length(defineClause) + 1,
+ pstrdup(name),
+ true);
+ }
/* build transformed DEFINE clause (list of TargetEntry) */
defineClause = lappend(defineClause, teDefine);
--
2.50.1 (Apple Git-155)
From 4141670cfe47e9aa2ceac717dba0d09015fa89c1 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Thu, 2 Apr 2026 14:09:53 +0900
Subject: [PATCH] Add RPR planner integration tests
Add rpr_integration.sql covering planner optimization interactions
with RPR windows: frame optimization, run condition pushdown, window
deduplication, unused window removal, inverse transition, cost
estimation, subquery flattening, DEFINE expression propagation, and
LIMIT. Also covers integration scenarios with partitioned tables,
LATERAL, recursive CTEs, incremental sort, volatile functions, and
correlated subqueries.
---
src/test/regress/expected/rpr_integration.out | 885 ++++++++++++++++++
src/test/regress/parallel_schedule | 2 +-
src/test/regress/sql/rpr_integration.sql | 537 +++++++++++
3 files changed, 1423 insertions(+), 1 deletion(-)
create mode 100644 src/test/regress/expected/rpr_integration.out
create mode 100644 src/test/regress/sql/rpr_integration.sql
diff --git a/src/test/regress/expected/rpr_integration.out b/src/test/regress/expected/rpr_integration.out
new file mode 100644
index 00000000000..a21ac5a8588
--- /dev/null
+++ b/src/test/regress/expected/rpr_integration.out
@@ -0,0 +1,885 @@
+-- ============================================================
+-- RPR Integration Tests
+-- Planner optimization interaction tests for Row Pattern Recognition
+-- ============================================================
+--
+-- Verifies that each planner optimization correctly handles RPR windows.
+-- Even if individual optimizations are tested elsewhere, this file
+-- provides a single checkpoint for all planner/RPR interactions.
+--
+-- A. Planner Optimization Protection Tests
+-- A1. Frame optimization bypass
+-- A2. Run condition pushdown bypass
+-- A3. Window dedup prevention (planner)
+-- A4. Window dedup prevention (parser)
+-- A5. Unused window removal prevention
+-- A6. Inverse transition bypass
+-- A7. Cost estimation RPR awareness
+-- A8. Subquery flattening prevention
+-- A9. DEFINE expression non-propagation
+-- A10. RPR + LIMIT
+--
+-- B. Integration Scenario Tests
+-- B1. RPR + CTE
+-- B2. RPR + JOIN
+-- B3. RPR + Set operations
+-- B4. RPR + Prepared statements
+-- B5. RPR + Partitioned table
+-- B6. RPR + LATERAL
+-- B7. RPR + Recursive CTE
+-- B8. RPR + Incremental sort
+-- B9. RPR + Volatile function in DEFINE
+-- B10. RPR + Correlated subquery
+--
+CREATE TABLE rpr_integ (id INT, val INT);
+INSERT INTO rpr_integ VALUES
+ (1, 10), (2, 20), (3, 15), (4, 25), (5, 5),
+ (6, 30), (7, 35), (8, 20), (9, 40), (10, 45);
+-- ============================================================
+-- A1. Frame optimization bypass
+-- ============================================================
+-- optimize_window_clauses() must not apply frame optimization to RPR windows.
+-- Non-RPR: frame is optimized (RANGE -> ROWS conversion, etc.)
+-- RPR: frame must stay as specified (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+-- Non-RPR window with default frame -> frame optimization applied
+EXPLAIN (COSTS OFF)
+SELECT count(*) OVER w FROM rpr_integ
+WINDOW w AS (ORDER BY id);
+ QUERY PLAN
+-----------------------------------
+ WindowAgg
+ Window: w AS (ORDER BY id)
+ -> Sort
+ Sort Key: id
+ -> Seq Scan on rpr_integ
+(5 rows)
+
+-- RPR window -> frame optimization must NOT change the frame
+EXPLAIN (COSTS OFF)
+SELECT count(*) OVER w FROM rpr_integ
+WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val));
+ 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)
+
+-- ============================================================
+-- A2. Run condition pushdown bypass
+-- ============================================================
+-- find_window_run_conditions() must not push monotonic filters as Run Conditions
+-- for RPR windows, because RPR results are match-dependent, not monotonic.
+-- Non-RPR: count(*) > 0 pushed down as Run Condition
+EXPLAIN (COSTS OFF)
+SELECT * FROM (
+ SELECT count(*) OVER w AS cnt
+ FROM rpr_integ
+ WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+) t WHERE cnt > 0;
+ QUERY PLAN
+-----------------------------------------------------------------------------------------------
+ Subquery Scan on t
+ -> WindowAgg
+ Window: w AS (ORDER BY rpr_integ.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Run Condition: (count(*) OVER w > 0)
+ -> Sort
+ Sort Key: rpr_integ.id
+ -> Seq Scan on rpr_integ
+(7 rows)
+
+-- RPR: count(*) > 0 must NOT be pushed down (appears as Filter, not Run Condition)
+EXPLAIN (COSTS OFF)
+SELECT * FROM (
+ SELECT count(*) OVER w AS cnt
+ FROM rpr_integ
+ WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val))
+) t WHERE cnt > 0;
+ QUERY PLAN
+-----------------------------------------------------------------------------------------------
+ Subquery Scan on t
+ Filter: (t.cnt > 0)
+ -> WindowAgg
+ Window: w AS (ORDER BY rpr_integ.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a b+
+ -> Sort
+ Sort Key: rpr_integ.id
+ -> Seq Scan on rpr_integ
+(8 rows)
+
+-- Verify results are correct
+SELECT * FROM (
+ SELECT id, val, count(*) OVER w AS cnt
+ FROM rpr_integ
+ WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val))
+) t WHERE cnt > 0
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 2
+ 3 | 15 | 2
+ 5 | 5 | 3
+ 8 | 20 | 3
+(4 rows)
+
+-- ============================================================
+-- A3. Window dedup prevention (planner)
+-- ============================================================
+-- optimize_window_clauses() must not merge RPR and non-RPR windows
+-- even if they share the same base frame.
+-- Two windows with same ORDER BY: one RPR, one non-RPR -> separate WindowAgg nodes
+EXPLAIN (COSTS OFF)
+SELECT
+ count(*) OVER w_rpr AS rpr_cnt,
+ count(*) OVER w_normal AS normal_cnt
+FROM rpr_integ
+WINDOW
+ w_rpr AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val)),
+ w_normal AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING);
+ QUERY PLAN
+-----------------------------------------------------------------------------------------
+ WindowAgg
+ Window: w_normal AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ -> WindowAgg
+ Window: w_rpr AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a b+
+ -> Sort
+ Sort Key: id
+ -> Seq Scan on rpr_integ
+(8 rows)
+
+-- Verify both produce different results
+SELECT
+ id, val,
+ count(*) OVER w_rpr AS rpr_cnt,
+ count(*) OVER w_normal AS normal_cnt
+FROM rpr_integ
+WINDOW
+ w_rpr AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val)),
+ w_normal AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ORDER BY id;
+ id | val | rpr_cnt | normal_cnt
+----+-----+---------+------------
+ 1 | 10 | 2 | 10
+ 2 | 20 | 0 | 9
+ 3 | 15 | 2 | 8
+ 4 | 25 | 0 | 7
+ 5 | 5 | 3 | 6
+ 6 | 30 | 0 | 5
+ 7 | 35 | 0 | 4
+ 8 | 20 | 3 | 3
+ 9 | 40 | 0 | 2
+ 10 | 45 | 0 | 1
+(10 rows)
+
+-- ============================================================
+-- A4. Window dedup prevention (parser)
+-- ============================================================
+-- transformWindowFuncCall() must not merge two RPR windows with different DEFINE.
+-- Two RPR windows: same PATTERN structure, different DEFINE conditions.
+-- EXPLAIN: must show two separate WindowAgg nodes
+EXPLAIN (COSTS OFF)
+SELECT
+ count(*) OVER w1 AS cnt_up,
+ count(*) OVER w2 AS cnt_down
+FROM rpr_integ
+WINDOW
+ w1 AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val)),
+ w2 AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val < PREV(val));
+ QUERY PLAN
+--------------------------------------------------------------------------------------
+ WindowAgg
+ Window: w2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a b+
+ -> WindowAgg
+ Window: w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a b+
+ -> Sort
+ Sort Key: id
+ -> Seq Scan on rpr_integ
+(9 rows)
+
+-- Verify results differ
+SELECT
+ id, val,
+ count(*) OVER w1 AS cnt_up,
+ count(*) OVER w2 AS cnt_down
+FROM rpr_integ
+WINDOW
+ w1 AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val)),
+ w2 AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val < PREV(val))
+ORDER BY id;
+ id | val | cnt_up | cnt_down
+----+-----+--------+----------
+ 1 | 10 | 2 | 0
+ 2 | 20 | 0 | 2
+ 3 | 15 | 2 | 0
+ 4 | 25 | 0 | 2
+ 5 | 5 | 3 | 0
+ 6 | 30 | 0 | 0
+ 7 | 35 | 0 | 2
+ 8 | 20 | 3 | 0
+ 9 | 40 | 0 | 0
+ 10 | 45 | 0 | 0
+(10 rows)
+
+-- ============================================================
+-- A5. Unused window removal prevention
+-- ============================================================
+-- remove_unused_subquery_outputs() must not remove RPR window functions
+-- even when their output columns are not referenced by the outer query.
+-- RPR window output not directly referenced, but pattern matching must still execute
+SELECT count(*) FROM (
+ SELECT count(*) OVER w FROM rpr_integ
+ WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > PREV(val))
+) t;
+ count
+-------
+ 10
+(1 row)
+
+-- With PREV in DEFINE: window must be preserved
+SELECT count(*), sum(c) FROM (
+ SELECT count(*) OVER w AS c FROM rpr_integ
+ WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > PREV(val))
+) t;
+ count | sum
+-------+-----
+ 10 | 6
+(1 row)
+
+-- Without PREV but with DEFINE: window must still be preserved
+SELECT count(*), sum(c) FROM (
+ SELECT count(*) OVER w AS c FROM rpr_integ
+ WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS TRUE)
+) t;
+ count | sum
+-------+-----
+ 10 | 10
+(1 row)
+
+-- ============================================================
+-- A6. Inverse transition bypass
+-- ============================================================
+-- RPR windows must not use moving aggregate (inverse transition) optimization.
+-- Moving aggregates recalculate by subtracting leaving rows, but RPR's
+-- reduced frames make this invalid.
+-- sum() supports inverse transition. With RPR, it must not be used.
+-- Verify correctness: sum with RPR pattern matching
+SELECT id, val,
+ sum(val) OVER w AS pattern_sum
+FROM rpr_integ
+WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val))
+ORDER BY id;
+ id | val | pattern_sum
+----+-----+-------------
+ 1 | 10 | 30
+ 2 | 20 |
+ 3 | 15 | 40
+ 4 | 25 |
+ 5 | 5 | 70
+ 6 | 30 |
+ 7 | 35 |
+ 8 | 20 | 105
+ 9 | 40 |
+ 10 | 45 |
+(10 rows)
+
+-- ============================================================
+-- A7. Cost estimation RPR awareness
+-- ============================================================
+-- cost_windowagg() must account for DEFINE expression evaluation cost.
+-- Verify RPR WindowAgg cost > non-RPR WindowAgg cost.
+CREATE FUNCTION get_windowagg_cost(query text) RETURNS numeric AS $$
+DECLARE
+ plan json;
+ cost numeric;
+BEGIN
+ EXECUTE 'EXPLAIN (FORMAT JSON) ' || query INTO plan;
+ cost := (plan->0->'Plan'->>'Total Cost')::numeric;
+ RETURN cost;
+END;
+$$ LANGUAGE plpgsql;
+SELECT get_windowagg_cost(
+ 'SELECT count(*) OVER w FROM rpr_integ
+ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+ C+) DEFINE B AS val > PREV(val), C AS val < PREV(val))')
+ >
+ get_windowagg_cost(
+ 'SELECT count(*) OVER w FROM rpr_integ
+ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)')
+ AS rpr_cost_is_higher;
+ rpr_cost_is_higher
+--------------------
+ t
+(1 row)
+
+DROP FUNCTION get_windowagg_cost(text);
+-- ============================================================
+-- A8. Subquery flattening prevention
+-- ============================================================
+-- is_simple_subquery() prevents pullup of subqueries with window functions.
+-- RPR subquery must not be flattened into the outer query.
+-- EXPLAIN must show Subquery Scan (not flattened).
+EXPLAIN (COSTS OFF)
+SELECT * FROM (
+ SELECT id, val, count(*) OVER w AS cnt
+ FROM rpr_integ
+ WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val))
+) sub
+WHERE cnt > 0;
+ QUERY PLAN
+-----------------------------------------------------------------------------------------------
+ Subquery Scan on sub
+ Filter: (sub.cnt > 0)
+ -> WindowAgg
+ Window: w AS (ORDER BY rpr_integ.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a b+
+ -> Sort
+ Sort Key: rpr_integ.id
+ -> Seq Scan on rpr_integ
+(8 rows)
+
+-- ============================================================
+-- A9. DEFINE expression non-propagation
+-- ============================================================
+-- DEFINE expressions must not appear in upper WindowAgg's targetlist.
+-- Only Var nodes should be propagated, not the full DEFINE expression.
+-- EXPLAIN VERBOSE: outer WindowAgg output must NOT contain DEFINE expression.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT
+ count(*) OVER w_rpr AS rpr_cnt,
+ count(*) OVER w_normal AS normal_cnt
+FROM rpr_integ
+WINDOW
+ w_rpr AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val)),
+ w_normal AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING);
+ QUERY PLAN
+---------------------------------------------------------------------------------------------------
+ WindowAgg
+ Output: (count(*) OVER w_rpr), count(*) OVER w_normal, id, val
+ Window: w_normal AS (ORDER BY rpr_integ.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ -> WindowAgg
+ Output: id, val, count(*) OVER w_rpr
+ Window: w_rpr AS (ORDER BY rpr_integ.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a b+
+ -> Sort
+ Output: id, val
+ Sort Key: rpr_integ.id
+ -> Seq Scan on public.rpr_integ
+ Output: id, val
+(12 rows)
+
+-- ============================================================
+-- A10. RPR + LIMIT
+-- ============================================================
+-- LIMIT must not interfere with RPR pattern matching.
+-- The result should be a prefix of the unlimited query.
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_integ
+WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val))
+ORDER BY id
+LIMIT 5;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 2
+ 2 | 20 | 0
+ 3 | 15 | 2
+ 4 | 25 | 0
+ 5 | 5 | 3
+(5 rows)
+
+-- ============================================================
+-- B1. RPR + CTE
+-- ============================================================
+-- CTE with RPR, outer query aggregates
+WITH rpr_result AS (
+ SELECT id, val, count(*) OVER w AS cnt
+ FROM rpr_integ
+ WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val))
+)
+SELECT count(*) AS total_rows,
+ count(*) FILTER (WHERE cnt > 0) AS matched_rows,
+ max(cnt) AS max_match_len
+FROM rpr_result;
+ total_rows | matched_rows | max_match_len
+------------+--------------+---------------
+ 10 | 4 | 3
+(1 row)
+
+-- Multiple CTE references
+WITH rpr_result AS (
+ SELECT id, val, count(*) OVER w AS cnt
+ FROM rpr_integ
+ WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val))
+)
+SELECT r1.id, r1.cnt
+FROM rpr_result r1
+JOIN rpr_result r2 ON r1.id = r2.id AND r1.cnt = r2.cnt
+WHERE r1.cnt > 0
+ORDER BY r1.id;
+ id | cnt
+----+-----
+ 1 | 2
+ 3 | 2
+ 5 | 3
+ 8 | 3
+(4 rows)
+
+-- ============================================================
+-- B2. RPR + JOIN
+-- ============================================================
+CREATE TABLE rpr_integ2 (id INT, label TEXT);
+INSERT INTO rpr_integ2 VALUES
+ (1, 'a'), (2, 'b'), (3, 'c'), (4, 'd'), (5, 'e'),
+ (6, 'f'), (7, 'g'), (8, 'h'), (9, 'i'), (10, 'j');
+-- RPR subquery joined with another table
+SELECT r.id, r.cnt, j.label
+FROM (
+ SELECT id, count(*) OVER w AS cnt
+ FROM rpr_integ
+ WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val))
+) r
+JOIN rpr_integ2 j ON r.id = j.id
+WHERE r.cnt > 0
+ORDER BY r.id;
+ id | cnt | label
+----+-----+-------
+ 1 | 2 | a
+ 3 | 2 | c
+ 5 | 3 | e
+ 8 | 3 | h
+(4 rows)
+
+-- ============================================================
+-- B3. RPR + Set operations
+-- ============================================================
+-- UNION of RPR and non-RPR results
+SELECT id, cnt, 'rpr' AS source FROM (
+ SELECT id, count(*) OVER w AS cnt
+ FROM rpr_integ
+ WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val))
+) t WHERE cnt > 0
+UNION ALL
+SELECT id, count(*) OVER (ORDER BY id) AS cnt, 'normal' AS source
+FROM rpr_integ
+ORDER BY source, id;
+ id | cnt | source
+----+-----+--------
+ 1 | 1 | normal
+ 2 | 2 | normal
+ 3 | 3 | normal
+ 4 | 4 | normal
+ 5 | 5 | normal
+ 6 | 6 | normal
+ 7 | 7 | normal
+ 8 | 8 | normal
+ 9 | 9 | normal
+ 10 | 10 | normal
+ 1 | 2 | rpr
+ 3 | 2 | rpr
+ 5 | 3 | rpr
+ 8 | 3 | rpr
+(14 rows)
+
+-- ============================================================
+-- B4. RPR + Prepared statements
+-- ============================================================
+-- Plan caching must correctly serialize/deserialize RPR fields
+PREPARE rpr_prep AS
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_integ
+WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val))
+ORDER BY id;
+-- First execution (custom plan)
+EXECUTE rpr_prep;
+ 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)
+
+-- Subsequent executions (may use cached plan)
+EXECUTE rpr_prep;
+ 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)
+
+EXECUTE rpr_prep;
+ 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)
+
+EXECUTE rpr_prep;
+ 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)
+
+EXECUTE rpr_prep;
+ 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)
+
+EXECUTE rpr_prep;
+ 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)
+
+-- Verify cached plan produces same result
+EXECUTE rpr_prep;
+ 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)
+
+DEALLOCATE rpr_prep;
+-- ============================================================
+-- B5. RPR + Partitioned table
+-- ============================================================
+-- RPR pattern matching must work correctly across partitions.
+CREATE TABLE rpr_part (id INT, val INT) PARTITION BY RANGE (id);
+CREATE TABLE rpr_part_1 PARTITION OF rpr_part FOR VALUES FROM (1) TO (6);
+CREATE TABLE rpr_part_2 PARTITION OF rpr_part FOR VALUES FROM (6) TO (11);
+INSERT INTO rpr_part SELECT id, val FROM rpr_integ;
+-- Same query on partitioned vs non-partitioned should produce identical results
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_part
+WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val))
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 2
+ 2 | 20 | 0
+ 3 | 15 | 2
+ 4 | 25 | 0
+ 5 | 5 | 3
+ 6 | 30 | 0
+ 7 | 35 | 0
+ 8 | 20 | 3
+ 9 | 40 | 0
+ 10 | 45 | 0
+(10 rows)
+
+-- EXPLAIN: Append under Sort under WindowAgg
+EXPLAIN (COSTS OFF)
+SELECT count(*) OVER w FROM rpr_part
+WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val));
+ QUERY PLAN
+----------------------------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ORDER BY rpr_part.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a b+
+ -> Sort
+ Sort Key: rpr_part.id
+ -> Append
+ -> Seq Scan on rpr_part_1
+ -> Seq Scan on rpr_part_2
+(8 rows)
+
+DROP TABLE rpr_part;
+-- ============================================================
+-- B6. RPR + LATERAL
+-- ============================================================
+-- RPR inside a LATERAL subquery. Qualified column references from the
+-- outer query are not yet supported in DEFINE, so this tests the basic
+-- case where LATERAL provides filtering but DEFINE uses only local columns.
+SELECT o.id AS outer_id, r.id, r.cnt
+FROM rpr_integ o,
+LATERAL (
+ SELECT id, count(*) OVER w AS cnt
+ FROM rpr_integ
+ WHERE id <= o.id
+ WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val))
+) r
+WHERE r.cnt > 0 AND o.id IN (5, 10)
+ORDER BY o.id, r.id;
+ outer_id | id | cnt
+----------+----+-----
+ 5 | 1 | 2
+ 5 | 3 | 2
+ 10 | 1 | 2
+ 10 | 3 | 2
+ 10 | 5 | 3
+ 10 | 8 | 3
+(6 rows)
+
+-- ============================================================
+-- B7. RPR + Recursive CTE
+-- ============================================================
+-- RPR applied to results of a recursive CTE.
+WITH RECURSIVE seq AS (
+ SELECT 1 AS id, 10 AS val
+ UNION ALL
+ SELECT id + 1, val + (CASE WHEN id % 3 = 0 THEN -15 ELSE 10 END)
+ FROM seq WHERE id < 10
+)
+SELECT id, val, count(*) OVER w AS cnt
+FROM seq
+WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val))
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 3
+ 2 | 20 | 0
+ 3 | 30 | 0
+ 4 | 15 | 3
+ 5 | 25 | 0
+ 6 | 35 | 0
+ 7 | 20 | 3
+ 8 | 30 | 0
+ 9 | 40 | 0
+ 10 | 25 | 0
+(10 rows)
+
+-- ============================================================
+-- B8. RPR + Incremental sort
+-- ============================================================
+-- Incremental sort may be used when data is partially sorted.
+CREATE INDEX rpr_integ_id_idx ON rpr_integ (id);
+SET enable_seqscan = off;
+EXPLAIN (COSTS OFF)
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_integ
+WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val));
+ QUERY PLAN
+-------------------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a b+
+ -> Index Scan using rpr_integ_id_idx on rpr_integ
+(4 rows)
+
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_integ
+WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val))
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 2
+ 2 | 20 | 0
+ 3 | 15 | 2
+ 4 | 25 | 0
+ 5 | 5 | 3
+ 6 | 30 | 0
+ 7 | 35 | 0
+ 8 | 20 | 3
+ 9 | 40 | 0
+ 10 | 45 | 0
+(10 rows)
+
+RESET enable_seqscan;
+DROP INDEX rpr_integ_id_idx;
+-- ============================================================
+-- B9. RPR + Volatile function in DEFINE
+-- ============================================================
+-- Volatile functions must be evaluated per-row, not optimized away.
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_integ
+WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val) AND random() >= 0.0)
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 2
+ 2 | 20 | 0
+ 3 | 15 | 2
+ 4 | 25 | 0
+ 5 | 5 | 3
+ 6 | 30 | 0
+ 7 | 35 | 0
+ 8 | 20 | 3
+ 9 | 40 | 0
+ 10 | 45 | 0
+(10 rows)
+
+-- ============================================================
+-- B10. RPR + Correlated subquery in WHERE
+-- ============================================================
+-- RPR window in a correlated subquery. DEFINE references only local
+-- columns (no qualified refs), while correlation is in WHERE.
+SELECT o.id, o.val,
+ (SELECT count(*) OVER w
+ FROM rpr_integ i
+ WHERE i.id <= o.id
+ WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val))
+ ORDER BY id
+ LIMIT 1) AS first_cnt
+FROM rpr_integ o
+ORDER BY o.id;
+ id | val | first_cnt
+----+-----+-----------
+ 1 | 10 | 0
+ 2 | 20 | 2
+ 3 | 15 | 2
+ 4 | 25 | 2
+ 5 | 5 | 2
+ 6 | 30 | 2
+ 7 | 35 | 2
+ 8 | 20 | 2
+ 9 | 40 | 2
+ 10 | 45 | 2
+(10 rows)
+
+-- Cleanup
+DROP TABLE rpr_integ;
+DROP TABLE rpr_integ2;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 6a2d19d4410..dbfa3e8f2cc 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -107,7 +107,7 @@ test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combo
# ----------
# Row Pattern Recognition tests
# ----------
-test: rpr rpr_base rpr_explain rpr_nfa
+test: rpr rpr_base rpr_explain rpr_nfa rpr_integration
# ----------
# Another group of parallel tests (JSON related)
diff --git a/src/test/regress/sql/rpr_integration.sql b/src/test/regress/sql/rpr_integration.sql
new file mode 100644
index 00000000000..2ae42c92a27
--- /dev/null
+++ b/src/test/regress/sql/rpr_integration.sql
@@ -0,0 +1,537 @@
+-- ============================================================
+-- RPR Integration Tests
+-- Planner optimization interaction tests for Row Pattern Recognition
+-- ============================================================
+--
+-- Verifies that each planner optimization correctly handles RPR windows.
+-- Even if individual optimizations are tested elsewhere, this file
+-- provides a single checkpoint for all planner/RPR interactions.
+--
+-- A. Planner Optimization Protection Tests
+-- A1. Frame optimization bypass
+-- A2. Run condition pushdown bypass
+-- A3. Window dedup prevention (planner)
+-- A4. Window dedup prevention (parser)
+-- A5. Unused window removal prevention
+-- A6. Inverse transition bypass
+-- A7. Cost estimation RPR awareness
+-- A8. Subquery flattening prevention
+-- A9. DEFINE expression non-propagation
+-- A10. RPR + LIMIT
+--
+-- B. Integration Scenario Tests
+-- B1. RPR + CTE
+-- B2. RPR + JOIN
+-- B3. RPR + Set operations
+-- B4. RPR + Prepared statements
+-- B5. RPR + Partitioned table
+-- B6. RPR + LATERAL
+-- B7. RPR + Recursive CTE
+-- B8. RPR + Incremental sort
+-- B9. RPR + Volatile function in DEFINE
+-- B10. RPR + Correlated subquery
+--
+
+CREATE TABLE rpr_integ (id INT, val INT);
+INSERT INTO rpr_integ VALUES
+ (1, 10), (2, 20), (3, 15), (4, 25), (5, 5),
+ (6, 30), (7, 35), (8, 20), (9, 40), (10, 45);
+
+-- ============================================================
+-- A1. Frame optimization bypass
+-- ============================================================
+-- optimize_window_clauses() must not apply frame optimization to RPR windows.
+-- Non-RPR: frame is optimized (RANGE -> ROWS conversion, etc.)
+-- RPR: frame must stay as specified (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+
+-- Non-RPR window with default frame -> frame optimization applied
+EXPLAIN (COSTS OFF)
+SELECT count(*) OVER w FROM rpr_integ
+WINDOW w AS (ORDER BY id);
+
+-- RPR window -> frame optimization must NOT change the frame
+EXPLAIN (COSTS OFF)
+SELECT count(*) OVER w FROM rpr_integ
+WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val));
+
+-- ============================================================
+-- A2. Run condition pushdown bypass
+-- ============================================================
+-- find_window_run_conditions() must not push monotonic filters as Run Conditions
+-- for RPR windows, because RPR results are match-dependent, not monotonic.
+
+-- Non-RPR: count(*) > 0 pushed down as Run Condition
+EXPLAIN (COSTS OFF)
+SELECT * FROM (
+ SELECT count(*) OVER w AS cnt
+ FROM rpr_integ
+ WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+) t WHERE cnt > 0;
+
+-- RPR: count(*) > 0 must NOT be pushed down (appears as Filter, not Run Condition)
+EXPLAIN (COSTS OFF)
+SELECT * FROM (
+ SELECT count(*) OVER w AS cnt
+ FROM rpr_integ
+ WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val))
+) t WHERE cnt > 0;
+
+-- Verify results are correct
+SELECT * FROM (
+ SELECT id, val, count(*) OVER w AS cnt
+ FROM rpr_integ
+ WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val))
+) t WHERE cnt > 0
+ORDER BY id;
+
+-- ============================================================
+-- A3. Window dedup prevention (planner)
+-- ============================================================
+-- optimize_window_clauses() must not merge RPR and non-RPR windows
+-- even if they share the same base frame.
+
+-- Two windows with same ORDER BY: one RPR, one non-RPR -> separate WindowAgg nodes
+EXPLAIN (COSTS OFF)
+SELECT
+ count(*) OVER w_rpr AS rpr_cnt,
+ count(*) OVER w_normal AS normal_cnt
+FROM rpr_integ
+WINDOW
+ w_rpr AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val)),
+ w_normal AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING);
+
+-- Verify both produce different results
+SELECT
+ id, val,
+ count(*) OVER w_rpr AS rpr_cnt,
+ count(*) OVER w_normal AS normal_cnt
+FROM rpr_integ
+WINDOW
+ w_rpr AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val)),
+ w_normal AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ORDER BY id;
+
+-- ============================================================
+-- A4. Window dedup prevention (parser)
+-- ============================================================
+-- transformWindowFuncCall() must not merge two RPR windows with different DEFINE.
+-- Two RPR windows: same PATTERN structure, different DEFINE conditions.
+
+-- EXPLAIN: must show two separate WindowAgg nodes
+EXPLAIN (COSTS OFF)
+SELECT
+ count(*) OVER w1 AS cnt_up,
+ count(*) OVER w2 AS cnt_down
+FROM rpr_integ
+WINDOW
+ w1 AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val)),
+ w2 AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val < PREV(val));
+
+-- Verify results differ
+SELECT
+ id, val,
+ count(*) OVER w1 AS cnt_up,
+ count(*) OVER w2 AS cnt_down
+FROM rpr_integ
+WINDOW
+ w1 AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val)),
+ w2 AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val < PREV(val))
+ORDER BY id;
+
+-- ============================================================
+-- A5. Unused window removal prevention
+-- ============================================================
+-- remove_unused_subquery_outputs() must not remove RPR window functions
+-- even when their output columns are not referenced by the outer query.
+
+-- RPR window output not directly referenced, but pattern matching must still execute
+SELECT count(*) FROM (
+ SELECT count(*) OVER w FROM rpr_integ
+ WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > PREV(val))
+) t;
+
+-- With PREV in DEFINE: window must be preserved
+SELECT count(*), sum(c) FROM (
+ SELECT count(*) OVER w AS c FROM rpr_integ
+ WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > PREV(val))
+) t;
+
+-- Without PREV but with DEFINE: window must still be preserved
+SELECT count(*), sum(c) FROM (
+ SELECT count(*) OVER w AS c FROM rpr_integ
+ WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS TRUE)
+) t;
+
+-- ============================================================
+-- A6. Inverse transition bypass
+-- ============================================================
+-- RPR windows must not use moving aggregate (inverse transition) optimization.
+-- Moving aggregates recalculate by subtracting leaving rows, but RPR's
+-- reduced frames make this invalid.
+
+-- sum() supports inverse transition. With RPR, it must not be used.
+-- Verify correctness: sum with RPR pattern matching
+SELECT id, val,
+ sum(val) OVER w AS pattern_sum
+FROM rpr_integ
+WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val))
+ORDER BY id;
+
+-- ============================================================
+-- A7. Cost estimation RPR awareness
+-- ============================================================
+-- cost_windowagg() must account for DEFINE expression evaluation cost.
+-- Verify RPR WindowAgg cost > non-RPR WindowAgg cost.
+
+CREATE FUNCTION get_windowagg_cost(query text) RETURNS numeric AS $$
+DECLARE
+ plan json;
+ cost numeric;
+BEGIN
+ EXECUTE 'EXPLAIN (FORMAT JSON) ' || query INTO plan;
+ cost := (plan->0->'Plan'->>'Total Cost')::numeric;
+ RETURN cost;
+END;
+$$ LANGUAGE plpgsql;
+
+SELECT get_windowagg_cost(
+ 'SELECT count(*) OVER w FROM rpr_integ
+ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+ C+) DEFINE B AS val > PREV(val), C AS val < PREV(val))')
+ >
+ get_windowagg_cost(
+ 'SELECT count(*) OVER w FROM rpr_integ
+ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)')
+ AS rpr_cost_is_higher;
+
+DROP FUNCTION get_windowagg_cost(text);
+
+-- ============================================================
+-- A8. Subquery flattening prevention
+-- ============================================================
+-- is_simple_subquery() prevents pullup of subqueries with window functions.
+-- RPR subquery must not be flattened into the outer query.
+-- EXPLAIN must show Subquery Scan (not flattened).
+
+EXPLAIN (COSTS OFF)
+SELECT * FROM (
+ SELECT id, val, count(*) OVER w AS cnt
+ FROM rpr_integ
+ WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val))
+) sub
+WHERE cnt > 0;
+
+-- ============================================================
+-- A9. DEFINE expression non-propagation
+-- ============================================================
+-- DEFINE expressions must not appear in upper WindowAgg's targetlist.
+-- Only Var nodes should be propagated, not the full DEFINE expression.
+-- EXPLAIN VERBOSE: outer WindowAgg output must NOT contain DEFINE expression.
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT
+ count(*) OVER w_rpr AS rpr_cnt,
+ count(*) OVER w_normal AS normal_cnt
+FROM rpr_integ
+WINDOW
+ w_rpr AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val)),
+ w_normal AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING);
+
+-- ============================================================
+-- A10. RPR + LIMIT
+-- ============================================================
+-- LIMIT must not interfere with RPR pattern matching.
+-- The result should be a prefix of the unlimited query.
+
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_integ
+WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val))
+ORDER BY id
+LIMIT 5;
+
+-- ============================================================
+-- B1. RPR + CTE
+-- ============================================================
+
+-- CTE with RPR, outer query aggregates
+WITH rpr_result AS (
+ SELECT id, val, count(*) OVER w AS cnt
+ FROM rpr_integ
+ WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val))
+)
+SELECT count(*) AS total_rows,
+ count(*) FILTER (WHERE cnt > 0) AS matched_rows,
+ max(cnt) AS max_match_len
+FROM rpr_result;
+
+-- Multiple CTE references
+WITH rpr_result AS (
+ SELECT id, val, count(*) OVER w AS cnt
+ FROM rpr_integ
+ WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val))
+)
+SELECT r1.id, r1.cnt
+FROM rpr_result r1
+JOIN rpr_result r2 ON r1.id = r2.id AND r1.cnt = r2.cnt
+WHERE r1.cnt > 0
+ORDER BY r1.id;
+
+-- ============================================================
+-- B2. RPR + JOIN
+-- ============================================================
+
+CREATE TABLE rpr_integ2 (id INT, label TEXT);
+INSERT INTO rpr_integ2 VALUES
+ (1, 'a'), (2, 'b'), (3, 'c'), (4, 'd'), (5, 'e'),
+ (6, 'f'), (7, 'g'), (8, 'h'), (9, 'i'), (10, 'j');
+
+-- RPR subquery joined with another table
+SELECT r.id, r.cnt, j.label
+FROM (
+ SELECT id, count(*) OVER w AS cnt
+ FROM rpr_integ
+ WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val))
+) r
+JOIN rpr_integ2 j ON r.id = j.id
+WHERE r.cnt > 0
+ORDER BY r.id;
+
+-- ============================================================
+-- B3. RPR + Set operations
+-- ============================================================
+
+-- UNION of RPR and non-RPR results
+SELECT id, cnt, 'rpr' AS source FROM (
+ SELECT id, count(*) OVER w AS cnt
+ FROM rpr_integ
+ WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val))
+) t WHERE cnt > 0
+UNION ALL
+SELECT id, count(*) OVER (ORDER BY id) AS cnt, 'normal' AS source
+FROM rpr_integ
+ORDER BY source, id;
+
+-- ============================================================
+-- B4. RPR + Prepared statements
+-- ============================================================
+
+-- Plan caching must correctly serialize/deserialize RPR fields
+PREPARE rpr_prep AS
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_integ
+WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val))
+ORDER BY id;
+
+-- First execution (custom plan)
+EXECUTE rpr_prep;
+
+-- Subsequent executions (may use cached plan)
+EXECUTE rpr_prep;
+EXECUTE rpr_prep;
+EXECUTE rpr_prep;
+EXECUTE rpr_prep;
+EXECUTE rpr_prep;
+
+-- Verify cached plan produces same result
+EXECUTE rpr_prep;
+
+DEALLOCATE rpr_prep;
+
+-- ============================================================
+-- B5. RPR + Partitioned table
+-- ============================================================
+-- RPR pattern matching must work correctly across partitions.
+
+CREATE TABLE rpr_part (id INT, val INT) PARTITION BY RANGE (id);
+CREATE TABLE rpr_part_1 PARTITION OF rpr_part FOR VALUES FROM (1) TO (6);
+CREATE TABLE rpr_part_2 PARTITION OF rpr_part FOR VALUES FROM (6) TO (11);
+INSERT INTO rpr_part SELECT id, val FROM rpr_integ;
+
+-- Same query on partitioned vs non-partitioned should produce identical results
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_part
+WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val))
+ORDER BY id;
+
+-- EXPLAIN: Append under Sort under WindowAgg
+EXPLAIN (COSTS OFF)
+SELECT count(*) OVER w FROM rpr_part
+WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val));
+
+DROP TABLE rpr_part;
+
+-- ============================================================
+-- B6. RPR + LATERAL
+-- ============================================================
+-- RPR inside a LATERAL subquery. Qualified column references from the
+-- outer query are not yet supported in DEFINE, so this tests the basic
+-- case where LATERAL provides filtering but DEFINE uses only local columns.
+
+SELECT o.id AS outer_id, r.id, r.cnt
+FROM rpr_integ o,
+LATERAL (
+ SELECT id, count(*) OVER w AS cnt
+ FROM rpr_integ
+ WHERE id <= o.id
+ WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val))
+) r
+WHERE r.cnt > 0 AND o.id IN (5, 10)
+ORDER BY o.id, r.id;
+
+-- ============================================================
+-- B7. RPR + Recursive CTE
+-- ============================================================
+-- RPR applied to results of a recursive CTE.
+
+WITH RECURSIVE seq AS (
+ SELECT 1 AS id, 10 AS val
+ UNION ALL
+ SELECT id + 1, val + (CASE WHEN id % 3 = 0 THEN -15 ELSE 10 END)
+ FROM seq WHERE id < 10
+)
+SELECT id, val, count(*) OVER w AS cnt
+FROM seq
+WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val))
+ORDER BY id;
+
+-- ============================================================
+-- B8. RPR + Incremental sort
+-- ============================================================
+-- Incremental sort may be used when data is partially sorted.
+
+CREATE INDEX rpr_integ_id_idx ON rpr_integ (id);
+SET enable_seqscan = off;
+
+EXPLAIN (COSTS OFF)
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_integ
+WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val));
+
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_integ
+WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val))
+ORDER BY id;
+
+RESET enable_seqscan;
+DROP INDEX rpr_integ_id_idx;
+
+-- ============================================================
+-- B9. RPR + Volatile function in DEFINE
+-- ============================================================
+-- Volatile functions must be evaluated per-row, not optimized away.
+
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_integ
+WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val) AND random() >= 0.0)
+ORDER BY id;
+
+-- ============================================================
+-- B10. RPR + Correlated subquery in WHERE
+-- ============================================================
+-- RPR window in a correlated subquery. DEFINE references only local
+-- columns (no qualified refs), while correlation is in WHERE.
+
+SELECT o.id, o.val,
+ (SELECT count(*) OVER w
+ FROM rpr_integ i
+ WHERE i.id <= o.id
+ WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val))
+ ORDER BY id
+ LIMIT 1) AS first_cnt
+FROM rpr_integ o
+ORDER BY o.id;
+
+-- Cleanup
+DROP TABLE rpr_integ;
+DROP TABLE rpr_integ2;
--
2.50.1 (Apple Git-155)
From 59e99c6b9322f402df560bc693863491487e12db Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Thu, 2 Apr 2026 14:09:12 +0900
Subject: [PATCH] Replace reduced frame map with single match result
The reduced frame map was a per-row byte array tracking match status.
Since rows are processed sequentially and only one match is active
at a time, replace it with four scalar fields: valid, matched,
start, and length.
Also distinguish empty matches (FIN reached with zero rows consumed)
from unmatched rows via RF_EMPTY_MATCH, counted as matched in NFA
statistics.
Widen row_is_in_reduced_frame() return type from int to int64,
since it returns rpr_match_length which is int64.
---
src/backend/executor/execRPR.c | 56 +++---
src/backend/executor/nodeWindowAgg.c | 233 +++++++++-------------
src/include/nodes/execnodes.h | 21 +-
src/test/regress/expected/rpr_explain.out | 8 +-
4 files changed, 132 insertions(+), 186 deletions(-)
diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 58f9da0b814..1cbe8e14780 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -549,7 +549,7 @@
*
* (1) Find or create a context for the target row
* (2) Enter the row processing loop
- * (3) After the loop ends, record the result in reduced_frame_map
+ * (3) After the loop ends, record the match result
*
* Pseudocode of the row processing loop:
*
@@ -923,18 +923,19 @@
* Chapter X Match Result Processing
* ============================================================================
*
- * X-1. Reduced Frame Map
+ * X-1. Match Result
*
- * RPR match results are recorded in a byte array called reduced_frame_map.
- * One byte is allocated per row, and the value is one of the following:
+ * RPR tracks the current match result as a single entry in WindowAggState
+ * with four fields: rpr_match_valid, rpr_match_matched, rpr_match_start,
+ * and rpr_match_length. When valid is true, the entry describes the
+ * match result for the position at rpr_match_start: matched indicates
+ * success or failure, and length gives the number of rows consumed.
+ * A match with length 0 represents an empty match (pattern matched but
+ * consumed no rows). When valid is false, the position has not been
+ * evaluated yet (RF_NOT_DETERMINED).
*
- * RF_NOT_DETERMINED (0) Not yet processed
- * RF_FRAME_HEAD (1) Start row of the match
- * RF_SKIPPED (2) Interior row of the match (skipped in frame)
- * RF_UNMATCHED (3) Match failure
- *
- * The window function references this map to determine frame inclusion for
- * each row.
+ * The window function calls get_reduced_frame_status() to look up a
+ * row's status against the current match result.
*
* X-2. AFTER MATCH SKIP
*
@@ -1028,8 +1029,7 @@
* Phase 3 (Advance): skipped (no states)
*
* C0.states is empty, so the loop terminates.
- * matchEndRow < matchStartRow -> RF_UNMATCHED.
- * register_reduced_frame_map(0, RF_UNMATCHED).
+ * matchEndRow < matchStartRow -> unmatched.
*
* --- Row 1 (price=110) ---
*
@@ -1113,9 +1113,7 @@
*
* C1.states is empty and matchEndRow=3 >= matchStartRow=1 -> match succeeds.
*
- * register_reduced_frame_map(1, RF_FRAME_HEAD)
- * register_reduced_frame_map(2, RF_SKIPPED)
- * register_reduced_frame_map(3, RF_SKIPPED)
+ * rpr_match_start = 1, rpr_match_length = 3
*
* --- Row 4 (price=130) ---
*
@@ -1128,15 +1126,15 @@
* B: 130 < PREV(115) -> false
*
* ... No subsequent rows, so ExecRPRFinalizeAllContexts() is called.
- * Match incomplete -> RF_UNMATCHED.
+ * Match incomplete -> unmatched.
*
* XI-5. Final Result
*
- * Row 0: RF_UNMATCHED -> frame = the row itself
- * Row 1: RF_FRAME_HEAD -> frame = rows 1 through 3
- * Row 2: RF_SKIPPED -> inside row 1's match
- * Row 3: RF_SKIPPED -> inside row 1's match
- * Row 4: RF_UNMATCHED -> frame = the row itself
+ * Row 0: unmatched -> frame = the row itself
+ * Row 1: match head -> frame = rows 1 through 3
+ * Row 2: inside match -> skipped
+ * Row 3: inside match -> skipped
+ * Row 4: unmatched -> frame = the row itself
*
* Chapter XII Summary of Key Design Decisions
* ============================================================================
@@ -1579,12 +1577,14 @@ static void nfa_advance(WindowAggState *winstate, RPRNFAContext *ctx,
*
* - Empty match handling: The initial advance uses currentPos =
* startPos - 1 (before any row is consumed). If FIN is reached via
- * epsilon transitions alone, matchEndRow = startPos - 1 < matchStartRow,
- * resulting in UNMATCHED. For reluctant min=0 patterns (A*?, A??),
- * the skip path reaches FIN first and early termination prunes enter
- * paths, yielding an immediate empty (unmatched) result. For
- * greedy patterns (A*), the enter path adds VAR states first, then
- * the skip FIN is recorded but VAR states survive for later matching.
+ * epsilon transitions alone, matchEndRow = startPos - 1 < matchStartRow.
+ * If matchedState is set (FIN was reached), this is an empty match
+ * (RF_EMPTY_MATCH); otherwise it is unmatched (RF_UNMATCHED).
+ * For reluctant min=0 patterns (A*?, A??), the skip path reaches
+ * FIN first and early termination prunes enter paths, yielding an
+ * immediate empty match result. For greedy patterns (A*), the enter
+ * path adds VAR states first, then the skip FIN is recorded but VAR
+ * states survive for later matching.
*
* Context Absorption Runtime:
* ---------------------------
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index aed7cbef99a..dca2de570e8 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -247,13 +247,10 @@ static void attno_map(Node *node);
static bool attno_map_walker(Node *node, void *context);
static bool rpr_is_defined(WindowAggState *winstate);
-static int row_is_in_reduced_frame(WindowObject winobj, int64 pos);
+static int64 row_is_in_reduced_frame(WindowObject winobj, int64 pos);
-static void create_reduced_frame_map(WindowAggState *winstate);
-static void clear_reduced_frame_map(WindowAggState *winstate);
-static int get_reduced_frame_map(WindowAggState *winstate, int64 pos);
-static void register_reduced_frame_map(WindowAggState *winstate, int64 pos,
- int val);
+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);
@@ -1035,13 +1032,7 @@ eval_windowaggregates(WindowAggState *winstate)
*/
for (;;)
{
- int ret;
-
-#ifdef RPR_DEBUG
- printf("===== loop in frame starts: aggregatedupto: " INT64_FORMAT " aggregatedbase: " INT64_FORMAT "\n",
- winstate->aggregatedupto,
- winstate->aggregatedbase);
-#endif
+ int64 ret;
/* Fetch next row if we didn't already */
if (TupIsNull(agg_row_slot))
@@ -1065,27 +1056,18 @@ eval_windowaggregates(WindowAggState *winstate)
if (rpr_is_defined(winstate))
{
-#ifdef RPR_DEBUG
- printf("reduced_frame_map: %d aggregatedupto: " INT64_FORMAT " aggregatedbase: " INT64_FORMAT "\n",
- get_reduced_frame_map(winstate,
- winstate->aggregatedupto),
- winstate->aggregatedupto,
- winstate->aggregatedbase);
-#endif
-
/*
- * If the row status at currentpos is already decided and current
- * row status is not decided yet, it means we passed the last
- * reduced frame. Time to break the loop.
+ * If currentpos is already decided but aggregatedupto is not yet
+ * determined, we've passed the last reduced frame.
*/
- if (get_reduced_frame_map(winstate, winstate->currentpos)
+ if (get_reduced_frame_status(winstate, winstate->currentpos)
!= RF_NOT_DETERMINED &&
- get_reduced_frame_map(winstate, winstate->aggregatedupto)
+ get_reduced_frame_status(winstate, winstate->aggregatedupto)
== RF_NOT_DETERMINED)
break;
/*
- * Otherwise we need to calculate the reduced frame.
+ * Calculate the reduced frame for aggregatedupto.
*/
ret = row_is_in_reduced_frame(winstate->agg_winobj,
winstate->aggregatedupto);
@@ -1093,17 +1075,13 @@ eval_windowaggregates(WindowAggState *winstate)
break;
/*
- * Check if current row needs to be skipped due to no match.
+ * Check if current row is inside a match but not the head
+ * (skipped), and it's the base row for aggregation.
*/
- if (get_reduced_frame_map(winstate,
- winstate->aggregatedupto) == RF_SKIPPED &&
+ if (get_reduced_frame_status(winstate,
+ winstate->aggregatedupto) == RF_SKIPPED &&
winstate->aggregatedupto == winstate->aggregatedbase)
- {
-#ifdef RPR_DEBUG
- printf("skip current row for aggregation\n");
-#endif
break;
- }
}
/* Set tuple context for evaluation of aggregate arguments */
@@ -1358,7 +1336,8 @@ begin_partition(WindowAggState *winstate)
winstate->framehead_valid = false;
winstate->frametail_valid = false;
winstate->grouptail_valid = false;
- create_reduced_frame_map(winstate);
+ if (rpr_is_defined(winstate))
+ clear_reduced_frame(winstate);
winstate->spooled_rows = 0;
winstate->currentpos = 0;
winstate->frameheadpos = 0;
@@ -1581,9 +1560,8 @@ release_partition(WindowAggState *winstate)
winstate->partition_spooled = false;
winstate->next_partition = true;
- /* Reset RPR reduced frame map */
- winstate->reduced_frame_map = NULL;
- winstate->alloc_sz = 0;
+ /* Reset RPR match results */
+ clear_reduced_frame(winstate);
/* Reset NFA state for new partition */
winstate->nfaContext = NULL;
@@ -2366,11 +2344,6 @@ ExecWindowAgg(PlanState *pstate)
CHECK_FOR_INTERRUPTS();
-#ifdef RPR_DEBUG
- printf("ExecWindowAgg called. pos: " INT64_FORMAT "\n",
- winstate->currentpos);
-#endif
-
if (winstate->status == WINDOWAGG_DONE)
return NULL;
@@ -2480,14 +2453,13 @@ ExecWindowAgg(PlanState *pstate)
if (winstate->status == WINDOWAGG_RUN)
{
/*
- * If RPR is defined and skip mode is next row, we need to clear
- * existing reduced frame info so that we newly calculate the info
- * starting from current row.
+ * If RPR is defined and skip mode is next row, clear the current
+ * match so the next row triggers re-evaluation.
*/
if (rpr_is_defined(winstate))
{
if (winstate->rpSkipTo == ST_NEXT_ROW)
- clear_reduced_frame_map(winstate);
+ clear_reduced_frame(winstate);
}
/*
@@ -2986,9 +2958,6 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
name = te->resname;
expr = te->expr;
-#ifdef RPR_DEBUG
- printf("defineVariable name: %s\n", name);
-#endif
winstate->defineVariableList =
lappend(winstate->defineVariableList,
makeString(pstrdup(name)));
@@ -3668,7 +3637,7 @@ ignorenulls_getfuncarginframe(WindowObject winobj, int argno,
int notnull_offset;
int notnull_relpos;
int forward;
- int num_reduced_frame;
+ int64 num_reduced_frame;
Assert(WindowObjectIsValid(winobj));
winstate = winobj->winstate;
@@ -3968,14 +3937,12 @@ rpr_is_defined(WindowAggState *winstate)
* AFTER MATCH SKIP PAST LAST ROW
* -----------------
*/
-static int
+static int64
row_is_in_reduced_frame(WindowObject winobj, int64 pos)
{
WindowAggState *winstate = winobj->winstate;
int state;
- int rtn;
- int64 i;
- int num_reduced_rows;
+ int64 rtn;
if (!rpr_is_defined(winstate))
{
@@ -3984,14 +3951,10 @@ row_is_in_reduced_frame(WindowObject winobj, int64 pos)
* window frame.
*/
rtn = 0;
-#ifdef RPR_DEBUG
- printf("row_is_in_reduced_frame returns %d: pos: " INT64_FORMAT "\n",
- rtn, pos);
-#endif
return rtn;
}
- state = get_reduced_frame_map(winstate, pos);
+ state = get_reduced_frame_status(winstate, pos);
if (state == RF_NOT_DETERMINED)
{
@@ -3999,16 +3962,12 @@ row_is_in_reduced_frame(WindowObject winobj, int64 pos)
update_reduced_frame(winobj, pos);
}
- state = get_reduced_frame_map(winstate, pos);
+ state = get_reduced_frame_status(winstate, pos);
switch (state)
{
case RF_FRAME_HEAD:
- num_reduced_rows = 1;
- for (i = pos + 1;
- get_reduced_frame_map(winstate, i) == RF_SKIPPED; i++)
- num_reduced_rows++;
- rtn = num_reduced_rows;
+ rtn = winstate->rpr_match_length;
break;
case RF_SKIPPED:
@@ -4016,6 +3975,7 @@ row_is_in_reduced_frame(WindowObject winobj, int64 pos)
break;
case RF_UNMATCHED:
+ case RF_EMPTY_MATCH:
rtn = -1;
break;
@@ -4025,91 +3985,56 @@ row_is_in_reduced_frame(WindowObject winobj, int64 pos)
break;
}
-#ifdef RPR_DEBUG
- printf("row_is_in_reduced_frame returns %d: pos: " INT64_FORMAT "\n",
- rtn, pos);
-#endif
return rtn;
}
-#define REDUCED_FRAME_MAP_INIT_SIZE 1024L
-
/*
- * create_reduced_frame_map
- * Create reduced frame map
+ * clear_reduced_frame
+ * Clear reduced frame status
*/
static void
-create_reduced_frame_map(WindowAggState *winstate)
+clear_reduced_frame(WindowAggState *winstate)
{
- winstate->reduced_frame_map =
- MemoryContextAlloc(winstate->partcontext,
- REDUCED_FRAME_MAP_INIT_SIZE);
- winstate->alloc_sz = REDUCED_FRAME_MAP_INIT_SIZE;
- clear_reduced_frame_map(winstate);
+ winstate->rpr_match_valid = false;
+ winstate->rpr_match_matched = false;
+ winstate->rpr_match_start = -1;
+ winstate->rpr_match_length = 0;
}
/*
- * clear_reduced_frame_map
- * Clear reduced frame map
- */
-static void
-clear_reduced_frame_map(WindowAggState *winstate)
-{
- Assert(winstate->reduced_frame_map != NULL);
- MemSet(winstate->reduced_frame_map, RF_NOT_DETERMINED,
- winstate->alloc_sz);
-}
-
-/*
- * get_reduced_frame_map
- * Get reduced frame map specified by pos
+ * get_reduced_frame_status
+ * Look up a position against the current match.
+ *
+ * Returns one of the RF_* constants:
+ * RF_NOT_DETERMINED pos has not been processed yet
+ * RF_FRAME_HEAD pos is the start of the current match
+ * RF_SKIPPED pos is inside the current match but not the start
+ * RF_UNMATCHED pos is processed but not part of any match
*/
static int
-get_reduced_frame_map(WindowAggState *winstate, int64 pos)
+get_reduced_frame_status(WindowAggState *winstate, int64 pos)
{
- Assert(winstate->reduced_frame_map != NULL);
- Assert(pos >= 0);
+ int64 start = winstate->rpr_match_start;
+ int64 length = winstate->rpr_match_length;
- /*
- * If pos is not in the reduced frame map, it means that any info
- * regarding the pos has not been registered yet. So we return
- * RF_NOT_DETERMINED.
- */
- if (pos >= winstate->alloc_sz)
+ if (!winstate->rpr_match_valid)
return RF_NOT_DETERMINED;
- return winstate->reduced_frame_map[pos];
-}
+ /* Empty match: covers only the start position */
+ if (pos == start && winstate->rpr_match_matched && length == 0)
+ return RF_EMPTY_MATCH;
-/*
- * register_reduced_frame_map
- * Add/replace reduced frame map member at pos.
- * If there's no enough space, expand the map.
- */
-static void
-register_reduced_frame_map(WindowAggState *winstate, int64 pos, int val)
-{
- int64 realloc_sz;
-
- Assert(winstate->reduced_frame_map != NULL);
-
- if (pos < 0)
- elog(ERROR, "wrong pos: " INT64_FORMAT, pos);
-
- while (pos > winstate->alloc_sz - 1)
- {
- realloc_sz = winstate->alloc_sz * 2;
-
- winstate->reduced_frame_map =
- repalloc(winstate->reduced_frame_map, realloc_sz);
+ /* Outside the result range */
+ if (pos < start || pos >= start + length)
+ return RF_NOT_DETERMINED;
- MemSet(winstate->reduced_frame_map + winstate->alloc_sz,
- RF_NOT_DETERMINED, realloc_sz - winstate->alloc_sz);
+ if (!winstate->rpr_match_matched)
+ return RF_UNMATCHED;
- winstate->alloc_sz = realloc_sz;
- }
+ if (pos == start)
+ return RF_FRAME_HEAD;
- winstate->reduced_frame_map[pos] = val;
+ return RF_SKIPPED;
}
/*
@@ -4156,7 +4081,11 @@ update_reduced_frame(WindowObject winobj, int64 pos)
if (winstate->nfaContext != NULL &&
pos < winstate->nfaContext->matchStartRow)
{
- register_reduced_frame_map(winstate, pos, RF_UNMATCHED);
+ /* already processed, unmatched */
+ winstate->rpr_match_valid = true;
+ winstate->rpr_match_matched = false;
+ winstate->rpr_match_start = pos;
+ winstate->rpr_match_length = 1;
return;
}
@@ -4173,7 +4102,11 @@ update_reduced_frame(WindowObject winobj, int64 pos)
*/
if (pos <= winstate->nfaLastProcessedRow)
{
- register_reduced_frame_map(winstate, pos, RF_UNMATCHED);
+ /* already processed, unmatched */
+ winstate->rpr_match_valid = true;
+ winstate->rpr_match_matched = false;
+ winstate->rpr_match_start = pos;
+ winstate->rpr_match_length = 1;
return;
}
/* Not yet processed - create new context and start fresh */
@@ -4245,26 +4178,38 @@ register_result:
Assert(pos == targetCtx->matchStartRow);
/*
- * Register reduced frame map based on match result.
+ * Record match result.
*/
+ winstate->rpr_match_valid = true;
+ winstate->rpr_match_start = targetCtx->matchStartRow;
+
if (targetCtx->matchEndRow < targetCtx->matchStartRow)
{
matchLen = targetCtx->lastProcessedRow - targetCtx->matchStartRow + 1;
- register_reduced_frame_map(winstate, targetCtx->matchStartRow, RF_UNMATCHED);
- ExecRPRRecordContextFailure(winstate, matchLen);
+ if (targetCtx->matchedState != NULL)
+ {
+ /* Empty match: FIN reached but 0 rows consumed */
+ winstate->rpr_match_matched = true;
+ winstate->rpr_match_length = 0;
+ ExecRPRRecordContextSuccess(winstate, 0);
+ }
+ else
+ {
+ /* No match */
+ winstate->rpr_match_matched = false;
+ winstate->rpr_match_length = 1;
+ ExecRPRRecordContextFailure(winstate, matchLen);
+ }
ExecRPRFreeContext(winstate, targetCtx);
return;
}
- /* Match succeeded - register frame map and record statistics */
+ /* Match succeeded */
matchLen = targetCtx->matchEndRow - targetCtx->matchStartRow + 1;
- register_reduced_frame_map(winstate, targetCtx->matchStartRow, RF_FRAME_HEAD);
- for (int64 i = targetCtx->matchStartRow + 1; i <= targetCtx->matchEndRow; i++)
- {
- register_reduced_frame_map(winstate, i, RF_SKIPPED);
- }
+ winstate->rpr_match_matched = true;
+ winstate->rpr_match_length = matchLen;
ExecRPRRecordContextSuccess(winstate, matchLen);
/* Remove the matched context */
@@ -4747,7 +4692,7 @@ WinGetSlotInFrame(WindowObject winobj, TupleTableSlot *slot,
WindowAggState *winstate;
int64 abs_pos;
int64 mark_pos;
- int num_reduced_frame;
+ int64 num_reduced_frame;
Assert(WindowObjectIsValid(winobj));
winstate = winobj->winstate;
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 33028c3f10b..c672d29f35b 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -2499,10 +2499,12 @@ typedef enum WindowAggStatus
* tuples during spool */
} WindowAggStatus;
-#define RF_NOT_DETERMINED 0
-#define RF_FRAME_HEAD 1
-#define RF_SKIPPED 2
-#define RF_UNMATCHED 3
+/* RPR reduced frame states returned by get_reduced_frame_status() */
+#define RF_NOT_DETERMINED 0 /* not yet processed */
+#define RF_FRAME_HEAD 1 /* start row of a match */
+#define RF_SKIPPED 2 /* interior row of a match */
+#define RF_UNMATCHED 3 /* no match at this row */
+#define RF_EMPTY_MATCH 4 /* empty match (0 rows); treated as unmatched */
/*
* RPRNFAState - single NFA state for pattern matching
@@ -2694,12 +2696,11 @@ typedef struct WindowAggState
TupleTableSlot *next_slot; /* NEXT row navigation operator */
TupleTableSlot *null_slot; /* all NULL slot */
- /*
- * Each byte corresponds to a row positioned at absolute its pos in
- * partition. See above definition for RF_*. Used for RPR.
- */
- char *reduced_frame_map;
- int64 alloc_sz; /* size of the map */
+ /* RPR current match result */
+ bool rpr_match_valid; /* true if a match result is set */
+ bool rpr_match_matched; /* true if the result was a match */
+ int64 rpr_match_start; /* start position of the match result */
+ int64 rpr_match_length; /* number of rows matched (0 = empty) */
} WindowAggState;
/* ----------------
diff --git a/src/test/regress/expected/rpr_explain.out b/src/test/regress/expected/rpr_explain.out
index bd345906133..79cbc246039 100644
--- a/src/test/regress/expected/rpr_explain.out
+++ b/src/test/regress/expected/rpr_explain.out
@@ -3348,8 +3348,8 @@ WINDOW w AS (
Pattern: ((a' b')+" c)*
Storage: Memory Maximum Storage: NkB
NFA States: 9 peak, 178 total, 0 merged
- NFA Contexts: 4 peak, 61 total, 22 pruned
- NFA: 1 matched (len 57/57/57.0), 0 mismatched
+ NFA Contexts: 4 peak, 61 total, 20 pruned
+ NFA: 3 matched (len 0/57/19.0), 0 mismatched
NFA: 0 absorbed, 37 skipped (len 1/3/2.0)
-> Function Scan on generate_series s (actual rows=60.00 loops=1)
(9 rows)
@@ -3385,8 +3385,8 @@ WINDOW w AS (
Pattern: (a (b c)+)*
Storage: Memory Maximum Storage: NkB
NFA States: 7 peak, 160 total, 0 merged
- NFA Contexts: 4 peak, 61 total, 22 pruned
- NFA: 1 matched (len 57/57/57.0), 0 mismatched
+ NFA Contexts: 4 peak, 61 total, 20 pruned
+ NFA: 3 matched (len 0/57/19.0), 0 mismatched
NFA: 0 absorbed, 37 skipped (len 1/3/2.0)
-> Function Scan on generate_series s (actual rows=60.00 loops=1)
(9 rows)
--
2.50.1 (Apple Git-155)
From 7d9c68fffcf6eeebea395033da550c966456def2 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Sat, 4 Apr 2026 21:47:01 +0900
Subject: [PATCH] Add fixed-length group absorption for RPR
Extend context absorption to unbounded groups with fixed-length
children (min == max, recursively). Patterns like (A B{2})+ or
((A (B C){2}){2})+ are now absorbable, equivalent to unrolling to
{1,1} VARs at compile time without actually unrolling.
isFixedLengthChildren() recursively verifies min == max for all
children including nested subgroups, extending the existing Case 2
in isUnboundedStart().
Absorption comparison in nfa_states_covered requires states to be at
an ABSORBABLE judgment point, where count-dominance is guaranteed.
The inline advance in nfa_match is generalized to advance bounded
VARs within the absorbable region through END chains to reach the
judgment point.
Fix isAbsorbable propagation in nfa_advance_var and nfa_advance_end
exit paths, where reusing a state object skipped recomputation.
Mark VAR elements in the DFS visited bitmap at nfa_add_state_unique
instead of at nfa_advance_state entry, so that loop-back through ALT
to the same VAR is not incorrectly blocked by cycle detection.
---
src/backend/executor/execRPR.c | 110 ++++++--
src/backend/optimizer/plan/rpr.c | 136 +++++++--
src/test/regress/expected/rpr_base.out | 83 +++++-
src/test/regress/expected/rpr_explain.out | 206 +++++++++++++-
src/test/regress/expected/rpr_nfa.out | 321 ++++++++++++++++++++++
src/test/regress/sql/rpr_base.sql | 36 +++
src/test/regress/sql/rpr_explain.sql | 114 ++++++++
src/test/regress/sql/rpr_nfa.sql | 168 +++++++++++
8 files changed, 1111 insertions(+), 63 deletions(-)
diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 1cbe8e14780..8f0457e2b3c 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -1760,6 +1760,10 @@ nfa_add_state_unique(WindowAggState *winstate, RPRNFAContext *ctx, RPRNFAState *
RPRNFAState *s;
RPRNFAState *tail = NULL;
+ /* Mark VAR in visited before duplicate check to prevent DFS loops */
+ winstate->nfaVisitedElems[WORDNUM(state->elemIdx)] |=
+ ((bitmapword) 1 << BITNUM(state->elemIdx));
+
/* Check for duplicate and find tail */
for (s = ctx->states; s != NULL; s = s->next)
{
@@ -2033,6 +2037,14 @@ nfa_states_covered(RPRPattern *pattern, RPRNFAContext *older, RPRNFAContext *new
elem = &pattern->elements[newerState->elemIdx];
depth = elem->depth;
+ /*
+ * Only compare at absorption judgment points (RPR_ELEM_ABSORBABLE).
+ * Judgment points are where count-dominance guarantees the newer
+ * context's future matches are a subset of the older's.
+ */
+ if (!RPRElemIsAbsorbable(elem))
+ return false;
+
for (olderState = older->states; olderState != NULL; olderState = olderState->next)
{
CHECK_FOR_INTERRUPTS();
@@ -2175,9 +2187,10 @@ nfa_eval_var_match(WindowAggState *winstate, RPRPatternElement *elem,
* - not matched: remove state (exit alternatives already exist from
* previous advance when count >= min was satisfied)
*
- * For simple VARs (min=max=1) followed by END:
- * - Advance to END and update group count before absorb phase
- * - This ensures absorption can compare states by group completion
+ * For VARs that reached max count followed by END:
+ * - Advance through END chain to reach absorption judgment point
+ * - Only deterministic exits (count >= max, max != INF) are handled
+ * - Chains through END elements while count >= max (must-exit path)
*
* Non-VAR elements (ALT, END, FIN) are kept as-is for advance phase.
*/
@@ -2191,9 +2204,9 @@ nfa_match(WindowAggState *winstate, RPRNFAContext *ctx, bool *varMatched)
RPRNFAState *nextState;
/*
- * Evaluate VAR elements against current row. For simple VARs with END
- * next, advance to END and update group count inline so absorb phase can
- * compare states properly.
+ * Evaluate VAR elements against current row. For VARs that reach max
+ * count with END next, advance through END chain inline so absorb phase
+ * can compare states at judgment points.
*/
for (state = ctx->states; state != NULL; state = nextState)
{
@@ -2223,34 +2236,61 @@ nfa_match(WindowAggState *winstate, RPRNFAContext *ctx, bool *varMatched)
state->counts[depth] = count;
/*
- * For simple VAR (min=max=1) with END next, advance to END
- * and update group count inline. This keeps state in place,
- * preserving lexical order.
+ * For VAR at max count with END next, advance through END
+ * chain to reach the absorption judgment point. Only
+ * deterministic exits (count >= max, max finite) are handled;
+ * unbounded VARs stay for advance phase.
*/
- if (elem->min == 1 && elem->max == 1 &&
+ if (RPRElemIsAbsorbableBranch(elem) &&
+ !RPRElemIsAbsorbable(elem) &&
+ count >= elem->max &&
RPRElemIsEnd(&elements[elem->next]))
{
RPRPatternElement *endElem = &elements[elem->next];
int endDepth = endElem->depth;
int32 endCount = state->counts[endDepth];
- Assert(count == 1);
-
- /* Increment group count with overflow protection */
+ /* Increment group count */
if (endCount < RPR_COUNT_MAX)
endCount++;
-
- /*
- * END's max can never be exceeded here because
- * nfa_advance_end only loops when count < max, so
- * endCount entering inline advance is at most max-1, and
- * incrementing yields at most max.
- */
Assert(endElem->max == RPR_QUANTITY_INF ||
endCount <= endElem->max);
state->elemIdx = elem->next;
state->counts[endDepth] = endCount;
+
+ /*
+ * Chain through END elements within the absorbable region
+ * (ABSORBABLE_BRANCH) until reaching the judgment point
+ * (ABSORBABLE). Continue only on must-exit path (count
+ * >= max) with END next.
+ */
+ while (RPRElemIsAbsorbableBranch(endElem) &&
+ !RPRElemIsAbsorbable(endElem) &&
+ endCount >= endElem->max &&
+ RPRElemIsEnd(&elements[endElem->next]))
+ {
+ RPRPatternElement *outerEnd = &elements[endElem->next];
+ int outerDepth = outerEnd->depth;
+ int32 outerCount = state->counts[outerDepth];
+
+ /* Reset exited group's count */
+ state->counts[endDepth] = 0;
+
+ /* Increment outer group count */
+ if (outerCount < RPR_COUNT_MAX)
+ outerCount++;
+ Assert(outerEnd->max == RPR_QUANTITY_INF ||
+ outerCount <= outerEnd->max);
+
+ state->elemIdx = endElem->next;
+ state->counts[outerDepth] = outerCount;
+
+ /* Advance to next END in chain */
+ endElem = outerEnd;
+ endDepth = outerDepth;
+ endCount = outerCount;
+ }
}
/* else: stay at VAR for advance phase */
}
@@ -2468,6 +2508,10 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
state->elemIdx = elem->next;
nextElem = &elements[state->elemIdx];
+ /* Update isAbsorbable for target element (monotonic) */
+ state->isAbsorbable = state->isAbsorbable &&
+ RPRElemIsAbsorbableBranch(nextElem);
+
/* END->END: increment outer END's count */
if (RPRElemIsEnd(nextElem) && state->counts[nextElem->depth] < RPR_COUNT_MAX)
state->counts[nextElem->depth]++;
@@ -2621,6 +2665,13 @@ nfa_advance_var(WindowAggState *winstate, RPRNFAContext *ctx,
state->elemIdx = elem->next;
nextElem = &elements[state->elemIdx];
+ /*
+ * Update isAbsorbable for target element (monotonic: AND
+ * preserves false)
+ */
+ state->isAbsorbable = state->isAbsorbable &&
+ RPRElemIsAbsorbableBranch(nextElem);
+
/*
* When exiting directly to an outer END, increment its iteration
* count. Simple VARs (min=max=1) handle this via inline advance
@@ -2650,6 +2701,13 @@ nfa_advance_var(WindowAggState *winstate, RPRNFAContext *ctx,
state->elemIdx = elem->next;
nextElem = &elements[state->elemIdx];
+ /*
+ * Update isAbsorbable for target element (monotonic: AND preserves
+ * false)
+ */
+ state->isAbsorbable = state->isAbsorbable &&
+ RPRElemIsAbsorbableBranch(nextElem);
+
/* See comment above: increment outer END count for quantified VARs */
if (RPRElemIsEnd(nextElem))
{
@@ -2686,11 +2744,19 @@ nfa_advance_state(WindowAggState *winstate, RPRNFAContext *ctx,
nfa_state_free(winstate, state);
return;
}
- winstate->nfaVisitedElems[WORDNUM(state->elemIdx)] |=
- ((bitmapword) 1 << BITNUM(state->elemIdx));
elem = &pattern->elements[state->elemIdx];
+ /*
+ * Mark epsilon elements (END, ALT, BEGIN, FIN) in visited to prevent
+ * infinite epsilon cycles. VAR elements are marked later when added to
+ * the state list (nfa_add_state_unique), allowing legitimate loop-back to
+ * the same VAR in a new iteration.
+ */
+ if (!RPRElemIsVar(elem))
+ winstate->nfaVisitedElems[WORDNUM(state->elemIdx)] |=
+ ((bitmapword) 1 << BITNUM(state->elemIdx));
+
switch (elem->varId)
{
case RPR_VARID_FIN:
diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index 2728a0b9fca..c0e9d134aa9 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -92,6 +92,8 @@ static bool fillRPRPattern(RPRPatternNode *node, RPRPattern *pat,
static void finalizeRPRPattern(RPRPattern *result);
/* Forward declarations - context absorption */
+static bool isFixedLengthChildren(RPRPattern *pattern, RPRElemIdx idx,
+ RPRDepth scopeDepth);
static bool isUnboundedStart(RPRPattern *pattern, RPRElemIdx idx);
static void computeAbsorbabilityRecursive(RPRPattern *pattern,
RPRElemIdx startIdx,
@@ -1524,6 +1526,70 @@ finalizeRPRPattern(RPRPattern *result)
*-------------------------------------------------------------------------
*/
+/*
+ * isFixedLengthChildren
+ * Check if all children at scopeDepth have fixed-length quantifiers
+ * (min == max), recursively for nested subgroups.
+ *
+ * A fixed-length group is semantically equivalent to unrolling each child
+ * to {1,1} copies, which is the existing Case 2 already proven correct
+ * for absorption. This check recognizes fixed-length groups at compile
+ * time without actually unrolling them.
+ *
+ * Traverses the flat element array starting at idx. For VAR elements,
+ * checks min == max. For BEGIN elements (nested subgroups), recurses
+ * into the subgroup and also checks the subgroup's END quantifier.
+ * ALT elements are rejected (alternation inside absorbable group is
+ * not supported).
+ *
+ * Returns true if all children are fixed-length, stopping at the END
+ * element at scopeDepth - 1.
+ */
+static bool
+isFixedLengthChildren(RPRPattern *pattern, RPRElemIdx idx, RPRDepth scopeDepth)
+{
+ RPRPatternElement *e = &pattern->elements[idx];
+
+ check_stack_depth();
+
+ while (e->depth == scopeDepth)
+ {
+ if (RPRElemIsVar(e))
+ {
+ if (e->min != e->max)
+ return false;
+ }
+ else if (RPRElemIsBegin(e))
+ {
+ RPRElemIdx childIdx = e->next;
+
+ /* Recurse into subgroup children at scopeDepth + 1 */
+ if (!isFixedLengthChildren(pattern, childIdx, scopeDepth + 1))
+ return false;
+
+ /* Advance past the subgroup to its END element */
+ e = &pattern->elements[e->next];
+ while (e->depth > scopeDepth)
+ e = &pattern->elements[e->next];
+
+ /* e is now the END at scopeDepth; check its quantifier */
+ Assert(RPRElemIsEnd(e) && e->depth == scopeDepth);
+ if (e->min != e->max)
+ return false;
+ }
+ else
+ {
+ /* ALT inside group: not supported for absorption */
+ return false;
+ }
+
+ Assert(e->next != RPR_ELEMIDX_INVALID);
+ e = &pattern->elements[e->next];
+ }
+
+ return true;
+}
+
/*
* isUnboundedStart
* Check if the element at idx starts an unbounded greedy sequence.
@@ -1533,29 +1599,31 @@ finalizeRPRPattern(RPRPattern *result)
* - Greedy (not reluctant)
* - At the start of current scope
*
- * Algorithm:
- * - Traverse elements within current scope (parentDepth to startDepth)
- * - For GROUP: must be unbounded greedy AND contain only simple {1,1} VARs
- * - Sets ABSORBABLE and ABSORBABLE_BRANCH flags on matching elements
- *
* Two cases are handled:
* 1. Simple VAR: A+ B C - A has max=INF, gets both flags
- * 2. Group: (A B)+ C - END has max=INF, all children are {1,1} VARs
- * A,B,END get ABSORBABLE_BRANCH, only END gets ABSORBABLE
+ * 2. Unbounded GROUP with fixed-length children: (A B{2})+ C
+ * All children must have min == max (recursively for nested subgroups).
+ * This is equivalent to unrolling to {1,1} VARs, e.g., (A B B)+ C.
+ * All elements within the group get ABSORBABLE_BRANCH.
+ * Only the unbounded END gets ABSORBABLE (judgment point).
+ * Examples:
+ * (A B{2})+ C - B{2} has min==max, step=3
+ * (A (B C){2} D)+ E - nested {2} subgroup, step=6
+ * ((A (B C){2}){2})+ - doubly nested {2}, step=10
+ * (A ((B C{3}){2} D){2} E)+ F - deep nesting, step=20
*
* Returns false for patterns where absorption cannot work:
* - A B+ (unbounded not at start)
* - A+? B (reluctant quantifier)
* - (A | B)+ (ALT inside group)
- * - (A B+)+ (unbounded element inside group)
- * - ((A B)+ C)+ (nested unbounded groups)
+ * - (A B+)+ (variable-length element inside group)
+ * - (A B{2,5})+ (min != max inside group)
*/
static bool
isUnboundedStart(RPRPattern *pattern, RPRElemIdx idx)
{
RPRPatternElement *elem = &pattern->elements[idx];
RPRDepth startDepth = elem->depth;
- RPRPatternElement *nextElem;
RPRPatternElement *e;
/* Case 1: Simple unbounded VAR at start (greedy only) */
@@ -1568,21 +1636,19 @@ isUnboundedStart(RPRPattern *pattern, RPRElemIdx idx)
}
/*
- * Case 2: Unbounded GROUP - traverse siblings at startDepth and check if
- * they're all simple {1,1} VARs, then check if END at startDepth - 1 is
- * unbounded greedy.
+ * Case 2: Unbounded GROUP with fixed-length children. Each child must
+ * have min == max (recursively for nested subgroups), ensuring a fixed
+ * step size per iteration so that count-dominance holds.
*/
- for (e = elem; e->depth == startDepth; e = nextElem)
- {
- /* Must be simple {1,1} VAR */
- if (!RPRElemIsVar(e) || e->min != 1 || e->max != 1)
- return false;
+ if (!isFixedLengthChildren(pattern, idx, startDepth))
+ return false;
- Assert(e->next != RPR_ELEMIDX_INVALID);
- nextElem = &pattern->elements[e->next];
- }
+ /* Find the END element at startDepth - 1 */
+ e = &pattern->elements[idx];
+ while (e->depth >= startDepth)
+ e = &pattern->elements[e->next];
- /* Now e should be END at startDepth - 1 */
+ /* END must be unbounded greedy */
if (e->depth == startDepth - 1 &&
RPRElemIsEnd(e) && e->max == RPR_QUANTITY_INF &&
!RPRElemIsReluctant(e))
@@ -1590,7 +1656,8 @@ isUnboundedStart(RPRPattern *pattern, RPRElemIdx idx)
Assert(e->jump == idx); /* END points back to first child */
/* Set ABSORBABLE_BRANCH on all children, ABSORBABLE on END only */
- for (e = elem; !RPRElemIsEnd(e); e = &pattern->elements[e->next])
+ for (e = elem; !RPRElemIsEnd(e) || e->depth >= startDepth;
+ e = &pattern->elements[e->next])
e->flags |= RPR_ELEM_ABSORBABLE_BRANCH;
e->flags |= RPR_ELEM_ABSORBABLE_BRANCH | RPR_ELEM_ABSORBABLE;
return true;
@@ -1654,12 +1721,25 @@ computeAbsorbabilityRecursive(RPRPattern *pattern, RPRElemIdx startIdx,
}
else if (RPRElemIsBegin(elem))
{
- /* BEGIN: skip to first child and check that */
- computeAbsorbabilityRecursive(pattern, elem->next, hasAbsorbable);
-
- /* Mark BEGIN element if contents are absorbable */
- if (*hasAbsorbable)
+ /*
+ * BEGIN: first try to treat this BEGIN's children as an unbounded
+ * group directly (handles nested fixed-length groups like ((A{2}
+ * B{3}){2})+). If that fails, skip to first child and recurse as
+ * before.
+ */
+ if (isUnboundedStart(pattern, elem->next))
+ {
+ *hasAbsorbable = true;
elem->flags |= RPR_ELEM_ABSORBABLE_BRANCH;
+ }
+ else
+ {
+ computeAbsorbabilityRecursive(pattern, elem->next, hasAbsorbable);
+
+ /* Mark BEGIN element if contents are absorbable */
+ if (*hasAbsorbable)
+ elem->flags |= RPR_ELEM_ABSORBABLE_BRANCH;
+ }
}
else
{
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index 3168468d0ae..7452cf1b3ab 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -3312,7 +3312,7 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-------------------------------------------------------------------------------
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
- Pattern: (a{2}){2,}
+ Pattern: (a{2}'){2,}"
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
@@ -4095,6 +4095,87 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-> Seq Scan on rpr_plan
(6 rows)
+-- Fixed-length group absorbable: (A{2} B{3})+ -> (a{2}' b{3}'){2,}"
+-- All children have min == max, equivalent to unrolling to {1,1}
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW PATTERN ((A{2} B{3})+)
+ DEFINE A AS val <= 50, B AS val > 50);
+ QUERY PLAN
+-------------------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a{2}' b{3}')+"
+ -> Sort
+ Sort Key: id
+ -> Seq Scan on rpr_plan
+(6 rows)
+
+-- Nested fixed-length group: (A (B C){2} D)+ -> absorbable
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW PATTERN ((A (B C){2} D)+)
+ DEFINE A AS val <= 20, B AS val <= 40, C AS val <= 60, D AS val > 60);
+ QUERY PLAN
+-------------------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a' (b' c'){2}' d')+"
+ -> Sort
+ Sort Key: id
+ -> Seq Scan on rpr_plan
+(6 rows)
+
+-- Nested fixed-length with inner quantifier: ((A{2} B{3}){2})+ -> absorbable
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW PATTERN (((A{2} B{3}){2})+)
+ DEFINE A AS val <= 50, B AS val > 50);
+ QUERY PLAN
+-------------------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: ((a{2}' b{3}'){2}')+"
+ -> Sort
+ Sort Key: id
+ -> Seq Scan on rpr_plan
+(6 rows)
+
+-- Non-absorbable fixed-length: (A B{2,5})+ -> no markers (min != max)
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW PATTERN ((A B{2,5})+)
+ DEFINE A AS val <= 50, B AS val > 50);
+ QUERY PLAN
+-------------------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a b{2,5})+
+ -> Sort
+ Sort Key: id
+ -> Seq Scan on rpr_plan
+(6 rows)
+
+-- Non-absorbable fixed-length: (A B?)+ -> no markers (min != max)
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW PATTERN ((A B?)+)
+ DEFINE A AS val <= 50, B AS val > 50);
+ 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_plan
+(6 rows)
+
-- Non-absorbable (unbounded not at start): A B+ -> a b+ (no markers)
EXPLAIN (COSTS OFF)
SELECT COUNT(*) OVER w FROM rpr_plan
diff --git a/src/test/regress/expected/rpr_explain.out b/src/test/regress/expected/rpr_explain.out
index 79cbc246039..560f21f44c2 100644
--- a/src/test/regress/expected/rpr_explain.out
+++ b/src/test/regress/expected/rpr_explain.out
@@ -462,10 +462,10 @@ WINDOW w AS (
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a' b')+"
Storage: Memory Maximum Storage: NkB
- NFA States: 3 peak, 91 total, 0 merged
- NFA Contexts: 2 peak, 61 total, 0 pruned
+ NFA States: 4 peak, 91 total, 0 merged
+ NFA Contexts: 3 peak, 61 total, 0 pruned
NFA: 1 matched (len 60/60/60.0), 0 mismatched
- NFA: 29 absorbed (len 1/1/1.0), 30 skipped (len 1/1/1.0)
+ NFA: 29 absorbed (len 2/2/2.0), 30 skipped (len 1/1/1.0)
-> Function Scan on generate_series s (actual rows=60.00 loops=1)
(9 rows)
@@ -904,6 +904,188 @@ WINDOW w AS (
-> Function Scan on generate_series s (actual rows=100.00 loops=1)
(9 rows)
+-- Fixed-length group absorption: (A B B)+ C
+-- B B merged to B{2}; absorbable with fixed-length check
+-- step_size=3 (A + B + B); v % 7 cycle gives 2 iterations per match
+CREATE VIEW rpr_ev20b AS
+SELECT count(*) OVER w
+FROM generate_series(1, 70) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A B B)+ C)
+ DEFINE A AS v % 7 IN (1, 4), B AS v % 7 IN (2, 3, 5, 6), C AS v % 7 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev20b'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+-------------------------
+ PATTERN ((a b b)+ c)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 70) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A B B)+ C)
+ DEFINE A AS v % 7 IN (1, 4), B AS v % 7 IN (2, 3, 5, 6), C AS v % 7 = 0
+);');
+ rpr_explain_filter
+----------------------------------------------------------------------
+ WindowAgg (actual rows=70.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a' b{2}')+" c
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 3 peak, 91 total, 0 merged
+ NFA Contexts: 4 peak, 71 total, 40 pruned
+ NFA: 10 matched (len 7/7/7.0), 0 mismatched
+ NFA: 10 absorbed (len 3/3/3.0), 10 skipped (len 1/1/1.0)
+ -> Function Scan on generate_series s (actual rows=70.00 loops=1)
+(9 rows)
+
+-- Nested fixed-length group absorption: (A (B C){2} D)+ E
+-- step_size = 1 + (1+1)*2 + 1 = 6; v % 13 cycle gives 2 iterations + E
+CREATE VIEW rpr_ev20c AS
+SELECT count(*) OVER w
+FROM generate_series(1, 65) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A (B C){2} D)+ E)
+ DEFINE A AS v % 13 IN (1, 7), B AS v % 13 IN (2, 4, 8, 10),
+ C AS v % 13 IN (3, 5, 9, 11), D AS v % 13 IN (6, 12),
+ E AS v % 13 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev20c'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+--------------------------------
+ PATTERN ((a (b c){2} d)+ e)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 65) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A (B C){2} D)+ E)
+ DEFINE A AS v % 13 IN (1, 7), B AS v % 13 IN (2, 4, 8, 10),
+ C AS v % 13 IN (3, 5, 9, 11), D AS v % 13 IN (6, 12),
+ E AS v % 13 = 0
+);');
+ rpr_explain_filter
+----------------------------------------------------------------------
+ WindowAgg (actual rows=65.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a' (b' c'){2}' d')+" e
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 3 peak, 76 total, 0 merged
+ NFA Contexts: 4 peak, 66 total, 50 pruned
+ NFA: 5 matched (len 13/13/13.0), 0 mismatched
+ NFA: 5 absorbed (len 6/6/6.0), 5 skipped (len 1/1/1.0)
+ -> Function Scan on generate_series s (actual rows=65.00 loops=1)
+(9 rows)
+
+-- Doubly nested fixed-length group absorption: (A ((B C{3}){2} D){2} E)+ F
+-- step_size = 1 + ((1+3)*2+1)*2 + 1 = 20; v % 41 cycle gives 2 iterations + F
+CREATE VIEW rpr_ev20d AS
+SELECT count(*) OVER w
+FROM generate_series(1, 82) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A ((B C C C){2} D){2} E)+ F)
+ DEFINE A AS v % 41 IN (1, 21),
+ B AS v % 41 IN (2, 6, 11, 15, 22, 26, 31, 35),
+ C AS v % 41 IN (3,4,5, 7,8,9, 12,13,14, 16,17,18,
+ 23,24,25, 27,28,29, 32,33,34, 36,37,38),
+ D AS v % 41 IN (10, 19, 30, 39),
+ E AS v % 41 IN (20, 40),
+ F AS v % 41 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev20d'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+-------------------------------------------
+ PATTERN ((a ((b c c c){2} d){2} e)+ f)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 82) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A ((B C C C){2} D){2} E)+ F)
+ DEFINE A AS v % 41 IN (1, 21),
+ B AS v % 41 IN (2, 6, 11, 15, 22, 26, 31, 35),
+ C AS v % 41 IN (3,4,5, 7,8,9, 12,13,14, 16,17,18,
+ 23,24,25, 27,28,29, 32,33,34, 36,37,38),
+ D AS v % 41 IN (10, 19, 30, 39),
+ E AS v % 41 IN (20, 40),
+ F AS v % 41 = 0
+);');
+ rpr_explain_filter
+----------------------------------------------------------------------
+ WindowAgg (actual rows=82.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a' ((b' c{3}'){2}' d'){2}' e')+" f
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 3 peak, 87 total, 0 merged
+ NFA Contexts: 4 peak, 83 total, 76 pruned
+ NFA: 2 matched (len 41/41/41.0), 0 mismatched
+ NFA: 2 absorbed (len 20/20/20.0), 2 skipped (len 1/1/1.0)
+ -> Function Scan on generate_series s (actual rows=82.00 loops=1)
+(9 rows)
+
+-- 3-level END chain absorption: ((A (B C){2}){2})+
+-- step_size = (1 + (1+1)*2) * 2 = 10; v % 21 cycle gives 2 iterations
+-- END chain: END(BC{2}) -> END(A..{2}) -> END(+, ABSORBABLE)
+CREATE VIEW rpr_ev20e AS
+SELECT count(*) OVER w
+FROM generate_series(1, 42) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (((A (B C){2}){2})+)
+ DEFINE A AS v % 21 IN (1, 6, 11, 16),
+ B AS v % 21 IN (2, 4, 7, 9, 12, 14, 17, 19),
+ C AS v % 21 IN (3, 5, 8, 10, 13, 15, 18, 20)
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev20e'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+---------------------------------
+ PATTERN (((a (b c){2}){2})+)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 42) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (((A (B C){2}){2})+)
+ DEFINE A AS v % 21 IN (1, 6, 11, 16),
+ B AS v % 21 IN (2, 4, 7, 9, 12, 14, 17, 19),
+ C AS v % 21 IN (3, 5, 8, 10, 13, 15, 18, 20)
+);');
+ rpr_explain_filter
+----------------------------------------------------------------------
+ WindowAgg (actual rows=42.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: ((a' (b' c'){2}'){2}')+"
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 5 peak, 47 total, 0 merged
+ NFA Contexts: 5 peak, 43 total, 30 pruned
+ NFA: 2 matched (len 20/20/20.0), 0 mismatched
+ NFA: 2 absorbed (len 10/10/10.0), 8 skipped (len 1/5/3.0)
+ -> Function Scan on generate_series s (actual rows=42.00 loops=1)
+(9 rows)
+
-- ============================================================
-- Match Length Statistics Tests
-- ============================================================
@@ -1894,10 +2076,10 @@ WINDOW w AS (
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a' b' c')+"
Storage: Memory Maximum Storage: NkB
- NFA States: 3 peak, 81 total, 0 merged
- NFA Contexts: 3 peak, 61 total, 20 pruned
+ NFA States: 4 peak, 81 total, 0 merged
+ NFA Contexts: 4 peak, 61 total, 20 pruned
NFA: 1 matched (len 60/60/60.0), 0 mismatched
- NFA: 19 absorbed (len 1/1/1.0), 20 skipped (len 1/1/1.0)
+ NFA: 19 absorbed (len 3/3/3.0), 20 skipped (len 1/1/1.0)
-> Function Scan on generate_series s (actual rows=60.00 loops=1)
(9 rows)
@@ -2461,7 +2643,7 @@ WINDOW w AS (
NFA States: 4 peak, 102 total, 0 merged
NFA Contexts: 2 peak, 41 total, 10 pruned
NFA: 10 matched (len 3/3/3.0), 0 mismatched
- NFA: 20 absorbed (len 1/1/1.0), 0 skipped
+ NFA: 10 absorbed (len 1/1/1.0), 10 skipped (len 1/1/1.0)
-> Function Scan on generate_series s (actual rows=40.00 loops=1)
(9 rows)
@@ -3158,10 +3340,10 @@ WINDOW w AS (
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a' b')+"
Storage: Memory Maximum Storage: NkB
- NFA States: 3 peak, 61 total, 0 merged
- NFA Contexts: 2 peak, 41 total, 0 pruned
+ NFA States: 4 peak, 61 total, 0 merged
+ NFA Contexts: 3 peak, 41 total, 0 pruned
NFA: 1 matched (len 40/40/40.0), 0 mismatched
- NFA: 19 absorbed (len 1/1/1.0), 20 skipped (len 1/1/1.0)
+ NFA: 19 absorbed (len 2/2/2.0), 20 skipped (len 1/1/1.0)
-> Function Scan on generate_series s (actual rows=40.00 loops=1)
(9 rows)
@@ -3234,12 +3416,12 @@ WINDOW w AS (
----------------------------------------------------------------------
WindowAgg (actual rows=60.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
- Pattern: ((a b){2})+
+ Pattern: ((a' b'){2}')+"
Storage: Memory Maximum Storage: NkB
NFA States: 5 peak, 76 total, 0 merged
NFA Contexts: 4 peak, 61 total, 15 pruned
NFA: 1 matched (len 60/60/60.0), 0 mismatched
- NFA: 0 absorbed, 44 skipped (len 1/4/2.3)
+ NFA: 14 absorbed (len 4/4/4.0), 30 skipped (len 1/2/1.5)
-> Function Scan on generate_series s (actual rows=60.00 loops=1)
(9 rows)
diff --git a/src/test/regress/expected/rpr_nfa.out b/src/test/regress/expected/rpr_nfa.out
index 7b5a17fb671..250f7f131b1 100644
--- a/src/test/regress/expected/rpr_nfa.out
+++ b/src/test/regress/expected/rpr_nfa.out
@@ -447,6 +447,327 @@ WINDOW w AS (
7 | {X} | |
(7 rows)
+-- Fixed-length group absorption: (A B{2})+ C
+-- B{2} has min == max, equivalent to unrolling to (A B B)+ C
+WITH test_absorb_fixedlen AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['B']),
+ (4, ARRAY['A']),
+ (5, ARRAY['B']),
+ (6, ARRAY['B']),
+ (7, ARRAY['A']),
+ (8, ARRAY['B']),
+ (9, ARRAY['B']),
+ (10, ARRAY['C']),
+ (11, ARRAY['X'])
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_absorb_fixedlen
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A B{2})+ C)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 10
+ 2 | {B} | |
+ 3 | {B} | |
+ 4 | {A} | |
+ 5 | {B} | |
+ 6 | {B} | |
+ 7 | {A} | |
+ 8 | {B} | |
+ 9 | {B} | |
+ 10 | {C} | |
+ 11 | {X} | |
+(11 rows)
+
+-- Consecutive vars merged to fixed-length: (A B B)+ -> (A B{2})+
+-- mergeConsecutiveVars produces B{2}; now absorbable with fixed-length check
+WITH test_absorb_consecutive AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['B']),
+ (4, ARRAY['A']),
+ (5, ARRAY['B']),
+ (6, ARRAY['B']),
+ (7, ARRAY['A']),
+ (8, ARRAY['B']),
+ (9, ARRAY['B']),
+ (10, ARRAY['X'])
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_absorb_consecutive
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A B B)+)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 9
+ 2 | {B} | |
+ 3 | {B} | |
+ 4 | {A} | |
+ 5 | {B} | |
+ 6 | {B} | |
+ 7 | {A} | |
+ 8 | {B} | |
+ 9 | {B} | |
+ 10 | {X} | |
+(10 rows)
+
+-- Nested fixed-length group absorption: (A (B C){2} D)+ E
+-- Inner group {2} has min == max; absorbable via recursive check
+-- step_size = 1 + (1+1)*2 + 1 = 6
+WITH test_absorb_nested_fixedlen AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['C']),
+ (4, ARRAY['B']),
+ (5, ARRAY['C']),
+ (6, ARRAY['D']),
+ (7, ARRAY['A']),
+ (8, ARRAY['B']),
+ (9, ARRAY['C']),
+ (10, ARRAY['B']),
+ (11, ARRAY['C']),
+ (12, ARRAY['D']),
+ (13, ARRAY['E']),
+ (14, ARRAY['X'])
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_absorb_nested_fixedlen
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A (B C){2} D)+ E)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags),
+ D AS 'D' = ANY(flags),
+ E AS 'E' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 13
+ 2 | {B} | |
+ 3 | {C} | |
+ 4 | {B} | |
+ 5 | {C} | |
+ 6 | {D} | |
+ 7 | {A} | |
+ 8 | {B} | |
+ 9 | {C} | |
+ 10 | {B} | |
+ 11 | {C} | |
+ 12 | {D} | |
+ 13 | {E} | |
+ 14 | {X} | |
+(14 rows)
+
+-- Doubly nested fixed-length group absorption: (A ((B C{3}){2} D){2} E)+ F
+-- step_size = 1 + ((1+3)*2+1)*2 + 1 = 20; 2 iterations + F = 41 rows
+WITH test_absorb_doubly_nested AS (
+ SELECT v AS id, ARRAY[
+ CASE
+ WHEN v % 41 IN (1, 21) THEN 'A'
+ WHEN v % 41 IN (2, 6, 11, 15, 22, 26, 31, 35) THEN 'B'
+ WHEN v % 41 IN (3,4,5, 7,8,9, 12,13,14, 16,17,18,
+ 23,24,25, 27,28,29, 32,33,34, 36,37,38) THEN 'C'
+ WHEN v % 41 IN (10, 19, 30, 39) THEN 'D'
+ WHEN v % 41 IN (20, 40) THEN 'E'
+ WHEN v % 41 = 0 THEN 'F'
+ ELSE 'X'
+ END
+ ] AS flags
+ FROM generate_series(1, 82) AS s(v)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_absorb_doubly_nested
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A ((B C C C){2} D){2} E)+ F)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags),
+ D AS 'D' = ANY(flags),
+ E AS 'E' = ANY(flags),
+ F AS 'F' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 41
+ 2 | {B} | |
+ 3 | {C} | |
+ 4 | {C} | |
+ 5 | {C} | |
+ 6 | {B} | |
+ 7 | {C} | |
+ 8 | {C} | |
+ 9 | {C} | |
+ 10 | {D} | |
+ 11 | {B} | |
+ 12 | {C} | |
+ 13 | {C} | |
+ 14 | {C} | |
+ 15 | {B} | |
+ 16 | {C} | |
+ 17 | {C} | |
+ 18 | {C} | |
+ 19 | {D} | |
+ 20 | {E} | |
+ 21 | {A} | |
+ 22 | {B} | |
+ 23 | {C} | |
+ 24 | {C} | |
+ 25 | {C} | |
+ 26 | {B} | |
+ 27 | {C} | |
+ 28 | {C} | |
+ 29 | {C} | |
+ 30 | {D} | |
+ 31 | {B} | |
+ 32 | {C} | |
+ 33 | {C} | |
+ 34 | {C} | |
+ 35 | {B} | |
+ 36 | {C} | |
+ 37 | {C} | |
+ 38 | {C} | |
+ 39 | {D} | |
+ 40 | {E} | |
+ 41 | {F} | |
+ 42 | {A} | 42 | 82
+ 43 | {B} | |
+ 44 | {C} | |
+ 45 | {C} | |
+ 46 | {C} | |
+ 47 | {B} | |
+ 48 | {C} | |
+ 49 | {C} | |
+ 50 | {C} | |
+ 51 | {D} | |
+ 52 | {B} | |
+ 53 | {C} | |
+ 54 | {C} | |
+ 55 | {C} | |
+ 56 | {B} | |
+ 57 | {C} | |
+ 58 | {C} | |
+ 59 | {C} | |
+ 60 | {D} | |
+ 61 | {E} | |
+ 62 | {A} | |
+ 63 | {B} | |
+ 64 | {C} | |
+ 65 | {C} | |
+ 66 | {C} | |
+ 67 | {B} | |
+ 68 | {C} | |
+ 69 | {C} | |
+ 70 | {C} | |
+ 71 | {D} | |
+ 72 | {B} | |
+ 73 | {C} | |
+ 74 | {C} | |
+ 75 | {C} | |
+ 76 | {B} | |
+ 77 | {C} | |
+ 78 | {C} | |
+ 79 | {C} | |
+ 80 | {D} | |
+ 81 | {E} | |
+ 82 | {F} | |
+(82 rows)
+
+-- 3-level END chain: ((A (B C){2}){2})+
+-- Tests END(BC{2}) -> END(A..{2}) -> END(+) chaining
+-- 2 iterations of +, each 10 rows: (A B C B C)(A B C B C)
+WITH test_absorb_3level_end AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']), -- 1st + iter, 1st {2}, A
+ (2, ARRAY['B']),
+ (3, ARRAY['C']),
+ (4, ARRAY['B']),
+ (5, ARRAY['C']), -- 1st (BC){2} done
+ (6, ARRAY['A']), -- 1st + iter, 2nd {2}, A
+ (7, ARRAY['B']),
+ (8, ARRAY['C']),
+ (9, ARRAY['B']),
+ (10, ARRAY['C']), -- 2nd (BC){2} done, 1st {2} done, 1st + iter done
+ (11, ARRAY['A']), -- 2nd + iter, 1st {2}, A
+ (12, ARRAY['B']),
+ (13, ARRAY['C']),
+ (14, ARRAY['B']),
+ (15, ARRAY['C']),
+ (16, ARRAY['A']), -- 2nd + iter, 2nd {2}, A
+ (17, ARRAY['B']),
+ (18, ARRAY['C']),
+ (19, ARRAY['B']),
+ (20, ARRAY['C']), -- 2nd + iter done
+ (21, ARRAY['X']) -- no match, + ends
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_absorb_3level_end
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (((A (B C){2}){2})+)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 20
+ 2 | {B} | |
+ 3 | {C} | |
+ 4 | {B} | |
+ 5 | {C} | |
+ 6 | {A} | |
+ 7 | {B} | |
+ 8 | {C} | |
+ 9 | {B} | |
+ 10 | {C} | |
+ 11 | {A} | |
+ 12 | {B} | |
+ 13 | {C} | |
+ 14 | {B} | |
+ 15 | {C} | |
+ 16 | {A} | |
+ 17 | {B} | |
+ 18 | {C} | |
+ 19 | {B} | |
+ 20 | {C} | |
+ 21 | {X} | |
+(21 rows)
+
-- Multiple unbounded: A+ B+ (first element unbounded enables absorption)
WITH test_multi_unbounded AS (
SELECT * FROM (VALUES
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index cf6c062ae85..8c23c7598a3 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -2600,6 +2600,42 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
AFTER MATCH SKIP PAST LAST ROW PATTERN ((A+ B | A B)*)
DEFINE A AS val <= 50, B AS val > 50);
+-- Fixed-length group absorbable: (A{2} B{3})+ -> (a{2}' b{3}'){2,}"
+-- All children have min == max, equivalent to unrolling to {1,1}
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW PATTERN ((A{2} B{3})+)
+ DEFINE A AS val <= 50, B AS val > 50);
+
+-- Nested fixed-length group: (A (B C){2} D)+ -> absorbable
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW PATTERN ((A (B C){2} D)+)
+ DEFINE A AS val <= 20, B AS val <= 40, C AS val <= 60, D AS val > 60);
+
+-- Nested fixed-length with inner quantifier: ((A{2} B{3}){2})+ -> absorbable
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW PATTERN (((A{2} B{3}){2})+)
+ DEFINE A AS val <= 50, B AS val > 50);
+
+-- Non-absorbable fixed-length: (A B{2,5})+ -> no markers (min != max)
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW PATTERN ((A B{2,5})+)
+ DEFINE A AS val <= 50, B AS val > 50);
+
+-- Non-absorbable fixed-length: (A B?)+ -> no markers (min != max)
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW PATTERN ((A B?)+)
+ DEFINE A AS val <= 50, B AS val > 50);
+
-- Non-absorbable (unbounded not at start): A B+ -> a b+ (no markers)
EXPLAIN (COSTS OFF)
SELECT COUNT(*) OVER w FROM rpr_plan
diff --git a/src/test/regress/sql/rpr_explain.sql b/src/test/regress/sql/rpr_explain.sql
index 93e06b0cbdf..237f0366631 100644
--- a/src/test/regress/sql/rpr_explain.sql
+++ b/src/test/regress/sql/rpr_explain.sql
@@ -578,6 +578,120 @@ WINDOW w AS (
DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
);');
+-- Fixed-length group absorption: (A B B)+ C
+-- B B merged to B{2}; absorbable with fixed-length check
+-- step_size=3 (A + B + B); v % 7 cycle gives 2 iterations per match
+CREATE VIEW rpr_ev20b AS
+SELECT count(*) OVER w
+FROM generate_series(1, 70) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A B B)+ C)
+ DEFINE A AS v % 7 IN (1, 4), B AS v % 7 IN (2, 3, 5, 6), C AS v % 7 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev20b'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 70) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A B B)+ C)
+ DEFINE A AS v % 7 IN (1, 4), B AS v % 7 IN (2, 3, 5, 6), C AS v % 7 = 0
+);');
+
+-- Nested fixed-length group absorption: (A (B C){2} D)+ E
+-- step_size = 1 + (1+1)*2 + 1 = 6; v % 13 cycle gives 2 iterations + E
+CREATE VIEW rpr_ev20c AS
+SELECT count(*) OVER w
+FROM generate_series(1, 65) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A (B C){2} D)+ E)
+ DEFINE A AS v % 13 IN (1, 7), B AS v % 13 IN (2, 4, 8, 10),
+ C AS v % 13 IN (3, 5, 9, 11), D AS v % 13 IN (6, 12),
+ E AS v % 13 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev20c'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 65) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A (B C){2} D)+ E)
+ DEFINE A AS v % 13 IN (1, 7), B AS v % 13 IN (2, 4, 8, 10),
+ C AS v % 13 IN (3, 5, 9, 11), D AS v % 13 IN (6, 12),
+ E AS v % 13 = 0
+);');
+
+-- Doubly nested fixed-length group absorption: (A ((B C{3}){2} D){2} E)+ F
+-- step_size = 1 + ((1+3)*2+1)*2 + 1 = 20; v % 41 cycle gives 2 iterations + F
+CREATE VIEW rpr_ev20d AS
+SELECT count(*) OVER w
+FROM generate_series(1, 82) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A ((B C C C){2} D){2} E)+ F)
+ DEFINE A AS v % 41 IN (1, 21),
+ B AS v % 41 IN (2, 6, 11, 15, 22, 26, 31, 35),
+ C AS v % 41 IN (3,4,5, 7,8,9, 12,13,14, 16,17,18,
+ 23,24,25, 27,28,29, 32,33,34, 36,37,38),
+ D AS v % 41 IN (10, 19, 30, 39),
+ E AS v % 41 IN (20, 40),
+ F AS v % 41 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev20d'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 82) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A ((B C C C){2} D){2} E)+ F)
+ DEFINE A AS v % 41 IN (1, 21),
+ B AS v % 41 IN (2, 6, 11, 15, 22, 26, 31, 35),
+ C AS v % 41 IN (3,4,5, 7,8,9, 12,13,14, 16,17,18,
+ 23,24,25, 27,28,29, 32,33,34, 36,37,38),
+ D AS v % 41 IN (10, 19, 30, 39),
+ E AS v % 41 IN (20, 40),
+ F AS v % 41 = 0
+);');
+
+-- 3-level END chain absorption: ((A (B C){2}){2})+
+-- step_size = (1 + (1+1)*2) * 2 = 10; v % 21 cycle gives 2 iterations
+-- END chain: END(BC{2}) -> END(A..{2}) -> END(+, ABSORBABLE)
+CREATE VIEW rpr_ev20e AS
+SELECT count(*) OVER w
+FROM generate_series(1, 42) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (((A (B C){2}){2})+)
+ DEFINE A AS v % 21 IN (1, 6, 11, 16),
+ B AS v % 21 IN (2, 4, 7, 9, 12, 14, 17, 19),
+ C AS v % 21 IN (3, 5, 8, 10, 13, 15, 18, 20)
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev20e'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 42) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (((A (B C){2}){2})+)
+ DEFINE A AS v % 21 IN (1, 6, 11, 16),
+ B AS v % 21 IN (2, 4, 7, 9, 12, 14, 17, 19),
+ C AS v % 21 IN (3, 5, 8, 10, 13, 15, 18, 20)
+);');
+
-- ============================================================
-- Match Length Statistics Tests
-- ============================================================
diff --git a/src/test/regress/sql/rpr_nfa.sql b/src/test/regress/sql/rpr_nfa.sql
index 5edcb3357e6..aaa7b44f789 100644
--- a/src/test/regress/sql/rpr_nfa.sql
+++ b/src/test/regress/sql/rpr_nfa.sql
@@ -346,6 +346,174 @@ WINDOW w AS (
B AS 'B' = ANY(flags)
);
+-- Fixed-length group absorption: (A B{2})+ C
+-- B{2} has min == max, equivalent to unrolling to (A B B)+ C
+WITH test_absorb_fixedlen AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['B']),
+ (4, ARRAY['A']),
+ (5, ARRAY['B']),
+ (6, ARRAY['B']),
+ (7, ARRAY['A']),
+ (8, ARRAY['B']),
+ (9, ARRAY['B']),
+ (10, ARRAY['C']),
+ (11, ARRAY['X'])
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_absorb_fixedlen
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A B{2})+ C)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags)
+);
+
+-- Consecutive vars merged to fixed-length: (A B B)+ -> (A B{2})+
+-- mergeConsecutiveVars produces B{2}; now absorbable with fixed-length check
+WITH test_absorb_consecutive AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['B']),
+ (4, ARRAY['A']),
+ (5, ARRAY['B']),
+ (6, ARRAY['B']),
+ (7, ARRAY['A']),
+ (8, ARRAY['B']),
+ (9, ARRAY['B']),
+ (10, ARRAY['X'])
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_absorb_consecutive
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A B B)+)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+
+-- Nested fixed-length group absorption: (A (B C){2} D)+ E
+-- Inner group {2} has min == max; absorbable via recursive check
+-- step_size = 1 + (1+1)*2 + 1 = 6
+WITH test_absorb_nested_fixedlen AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['C']),
+ (4, ARRAY['B']),
+ (5, ARRAY['C']),
+ (6, ARRAY['D']),
+ (7, ARRAY['A']),
+ (8, ARRAY['B']),
+ (9, ARRAY['C']),
+ (10, ARRAY['B']),
+ (11, ARRAY['C']),
+ (12, ARRAY['D']),
+ (13, ARRAY['E']),
+ (14, ARRAY['X'])
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_absorb_nested_fixedlen
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A (B C){2} D)+ E)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags),
+ D AS 'D' = ANY(flags),
+ E AS 'E' = ANY(flags)
+);
+
+-- Doubly nested fixed-length group absorption: (A ((B C{3}){2} D){2} E)+ F
+-- step_size = 1 + ((1+3)*2+1)*2 + 1 = 20; 2 iterations + F = 41 rows
+WITH test_absorb_doubly_nested AS (
+ SELECT v AS id, ARRAY[
+ CASE
+ WHEN v % 41 IN (1, 21) THEN 'A'
+ WHEN v % 41 IN (2, 6, 11, 15, 22, 26, 31, 35) THEN 'B'
+ WHEN v % 41 IN (3,4,5, 7,8,9, 12,13,14, 16,17,18,
+ 23,24,25, 27,28,29, 32,33,34, 36,37,38) THEN 'C'
+ WHEN v % 41 IN (10, 19, 30, 39) THEN 'D'
+ WHEN v % 41 IN (20, 40) THEN 'E'
+ WHEN v % 41 = 0 THEN 'F'
+ ELSE 'X'
+ END
+ ] AS flags
+ FROM generate_series(1, 82) AS s(v)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_absorb_doubly_nested
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A ((B C C C){2} D){2} E)+ F)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags),
+ D AS 'D' = ANY(flags),
+ E AS 'E' = ANY(flags),
+ F AS 'F' = ANY(flags)
+);
+
+-- 3-level END chain: ((A (B C){2}){2})+
+-- Tests END(BC{2}) -> END(A..{2}) -> END(+) chaining
+-- 2 iterations of +, each 10 rows: (A B C B C)(A B C B C)
+WITH test_absorb_3level_end AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']), -- 1st + iter, 1st {2}, A
+ (2, ARRAY['B']),
+ (3, ARRAY['C']),
+ (4, ARRAY['B']),
+ (5, ARRAY['C']), -- 1st (BC){2} done
+ (6, ARRAY['A']), -- 1st + iter, 2nd {2}, A
+ (7, ARRAY['B']),
+ (8, ARRAY['C']),
+ (9, ARRAY['B']),
+ (10, ARRAY['C']), -- 2nd (BC){2} done, 1st {2} done, 1st + iter done
+ (11, ARRAY['A']), -- 2nd + iter, 1st {2}, A
+ (12, ARRAY['B']),
+ (13, ARRAY['C']),
+ (14, ARRAY['B']),
+ (15, ARRAY['C']),
+ (16, ARRAY['A']), -- 2nd + iter, 2nd {2}, A
+ (17, ARRAY['B']),
+ (18, ARRAY['C']),
+ (19, ARRAY['B']),
+ (20, ARRAY['C']), -- 2nd + iter done
+ (21, ARRAY['X']) -- no match, + ends
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_absorb_3level_end
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (((A (B C){2}){2})+)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags)
+);
+
-- Multiple unbounded: A+ B+ (first element unbounded enables absorption)
WITH test_multi_unbounded AS (
SELECT * FROM (VALUES
--
2.50.1 (Apple Git-155)
From 3e4d65b9f75444ff58cb1b7858558bbe6c77616a Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Sat, 4 Apr 2026 22:00:50 +0900
Subject: [PATCH] Rename rpr_explain test views from sequential numbers to
descriptive names
Replace rpr_ev01..rpr_ev89 with section-based names like
rpr_ev_ctx_absorb_group, rpr_ev_alt_nested_start, etc.
This avoids numbering conflicts when inserting new tests.
---
src/test/regress/expected/rpr_explain.out | 378 +++++++++++-----------
src/test/regress/sql/rpr_explain.sql | 372 ++++++++++-----------
2 files changed, 375 insertions(+), 375 deletions(-)
diff --git a/src/test/regress/expected/rpr_explain.out b/src/test/regress/expected/rpr_explain.out
index 560f21f44c2..f66caf8908e 100644
--- a/src/test/regress/expected/rpr_explain.out
+++ b/src/test/regress/expected/rpr_explain.out
@@ -111,7 +111,7 @@ VALUES
-- Basic NFA Statistics Tests
-- ============================================================
-- Simple pattern - should show basic statistics
-CREATE VIEW rpr_ev01 AS
+CREATE VIEW rpr_ev_basic_simple AS
SELECT count(*) OVER w
FROM rpr_nfa_test
WINDOW w AS (
@@ -120,7 +120,7 @@ WINDOW w AS (
PATTERN (A B)
DEFINE A AS cat = 'A', B AS cat = 'B'
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev01'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_basic_simple'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
------------------
PATTERN (a b)
@@ -150,7 +150,7 @@ WINDOW w AS (
(9 rows)
-- Pattern with no matches - 0 matched
-CREATE VIEW rpr_ev02 AS
+CREATE VIEW rpr_ev_basic_nomatch AS
SELECT count(*) OVER w
FROM rpr_nfa_test
WINDOW w AS (
@@ -159,7 +159,7 @@ WINDOW w AS (
PATTERN (X Y Z)
DEFINE X AS cat = 'X', Y AS cat = 'Y', Z AS cat = 'Z'
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev02'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_basic_nomatch'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
--------------------
PATTERN (x y z)
@@ -188,7 +188,7 @@ WINDOW w AS (
(8 rows)
-- Pattern matching every row - high match count
-CREATE VIEW rpr_ev03 AS
+CREATE VIEW rpr_ev_basic_allrows AS
SELECT count(*) OVER w
FROM rpr_nfa_test
WINDOW w AS (
@@ -197,7 +197,7 @@ WINDOW w AS (
PATTERN (R)
DEFINE R AS TRUE
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev03'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_basic_allrows'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
----------------
PATTERN (r)
@@ -227,7 +227,7 @@ WINDOW w AS (
-- Regression test: Space before parenthesis in pattern deparse
-- Verifies that "A (B | C)" correctly outputs as "a (b | c)" with space
-CREATE VIEW rpr_ev04 AS
+CREATE VIEW rpr_ev_basic_deparse_space AS
SELECT count(*) OVER w
FROM generate_series(1, 20) AS s(v)
WINDOW w AS (
@@ -235,7 +235,7 @@ WINDOW w AS (
PATTERN (A (B | C))
DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev04'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_basic_deparse_space'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
------------------------
PATTERN (a (b | c))
@@ -266,7 +266,7 @@ WINDOW w AS (
-- Regression test: Sequential alternations at same depth
-- Verifies that "((B | C) (D | E))" correctly outputs as "(b | c) (d | e)"
-- Previously failed due to missing parentheses on ALT depth decrease
-CREATE VIEW rpr_ev05 AS
+CREATE VIEW rpr_ev_basic_deparse_seqalt AS
SELECT count(*) OVER w
FROM generate_series(1, 30) AS s(v)
WINDOW w AS (
@@ -274,7 +274,7 @@ WINDOW w AS (
PATTERN (A ((B | C) (D | E))*)
DEFINE A AS v % 5 = 1, B AS v % 5 = 2, C AS v % 5 = 3, D AS v % 5 = 4, E AS v % 5 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev05'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_basic_deparse_seqalt'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
-----------------------------------
PATTERN (a ((b | c) (d | e))*)
@@ -305,7 +305,7 @@ WINDOW w AS (
-- State Statistics Tests (peak, total, merged)
-- ============================================================
-- Simple quantifier pattern - A+ with short matches (no merging)
-CREATE VIEW rpr_ev06 AS
+CREATE VIEW rpr_ev_state_simple_quant AS
SELECT count(*) OVER w
FROM generate_series(1, 50) AS s(v)
WINDOW w AS (
@@ -314,7 +314,7 @@ WINDOW w AS (
PATTERN (A+)
DEFINE A AS v % 2 = 1
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev06'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_state_simple_quant'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
-----------------
PATTERN (a+)
@@ -343,7 +343,7 @@ WINDOW w AS (
(8 rows)
-- Alternation pattern - multiple state branches
-CREATE VIEW rpr_ev07 AS
+CREATE VIEW rpr_ev_state_alt AS
SELECT count(*) OVER w
FROM rpr_nfa_test
WINDOW w AS (
@@ -354,7 +354,7 @@ WINDOW w AS (
A AS cat = 'A', B AS cat = 'B', C AS cat = 'C',
D AS cat = 'D', E AS cat = 'E'
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev07'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_state_alt'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
----------------------------------
PATTERN ((a | b | c) (d | e))
@@ -386,7 +386,7 @@ WINDOW w AS (
(9 rows)
-- Complex pattern with high state count
-CREATE VIEW rpr_ev08 AS
+CREATE VIEW rpr_ev_state_complex AS
SELECT count(*) OVER w
FROM generate_series(1, 100) AS s(v)
WINDOW w AS (
@@ -398,7 +398,7 @@ WINDOW w AS (
B AS v % 3 = 2,
C AS v % 3 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev08'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_state_complex'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
-----------------------
PATTERN (a+ b* c+)
@@ -431,7 +431,7 @@ WINDOW w AS (
(9 rows)
-- Grouped pattern with quantifier - state count with grouping
-CREATE VIEW rpr_ev09 AS
+CREATE VIEW rpr_ev_state_group_quant AS
SELECT count(*) OVER w
FROM generate_series(1, 60) AS s(v)
WINDOW w AS (
@@ -440,7 +440,7 @@ WINDOW w AS (
PATTERN ((A B)+)
DEFINE A AS v % 2 = 1, B AS v % 2 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev09'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_state_group_quant'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
---------------------
PATTERN ((a b)+)
@@ -471,7 +471,7 @@ WINDOW w AS (
-- State explosion pattern - many alternations
-- Pattern (A|B)(A|B)(A|B)(A|B) can create many parallel states
-CREATE VIEW rpr_ev10 AS
+CREATE VIEW rpr_ev_state_explosion AS
SELECT count(*) OVER w
FROM generate_series(1, 100) AS s(v)
WINDOW w AS (
@@ -480,7 +480,7 @@ WINDOW w AS (
PATTERN ((A | B) (A | B) (A | B) (A | B) (A | B) (A | B) (A | B) (A | B))
DEFINE A AS v % 2 = 1, B AS v % 2 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev10'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_state_explosion'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
------------------------------------------------------------------------------
PATTERN ((a | b) (a | b) (a | b) (a | b) (a | b) (a | b) (a | b) (a | b))
@@ -511,7 +511,7 @@ WINDOW w AS (
-- Consecutive ALT merge followed by different ALT
-- Tests mergeConsecutiveAlts flush on ALT change: (A|B){2} (C|D)
-CREATE VIEW rpr_ev11 AS
+CREATE VIEW rpr_ev_state_alt_merge_alt AS
SELECT count(*) OVER w
FROM generate_series(1, 40) AS s(v)
WINDOW w AS (
@@ -520,7 +520,7 @@ WINDOW w AS (
PATTERN ((A | B) (A | B) (C | D))
DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev11'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_state_alt_merge_alt'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
--------------------------------------
PATTERN ((a | b) (a | b) (c | d))
@@ -551,7 +551,7 @@ WINDOW w AS (
-- Consecutive ALT merge followed by non-ALT element
-- Tests mergeConsecutiveAlts flush on non-ALT: (A|B){2} c
-CREATE VIEW rpr_ev12 AS
+CREATE VIEW rpr_ev_state_alt_merge_nonalt AS
SELECT count(*) OVER w
FROM generate_series(1, 40) AS s(v)
WINDOW w AS (
@@ -560,7 +560,7 @@ WINDOW w AS (
PATTERN ((A | B) (A | B) C)
DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev12'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_state_alt_merge_nonalt'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
--------------------------------
PATTERN ((a | b) (a | b) c)
@@ -590,7 +590,7 @@ WINDOW w AS (
(9 rows)
-- ALT prefix/suffix absorbed into GROUP: (A|B) (A|B)+ (A|B) -> (A|B){3,}
-CREATE VIEW rpr_ev13 AS
+CREATE VIEW rpr_ev_state_alt_absorb_group AS
SELECT count(*) OVER w
FROM generate_series(1, 40) AS s(v)
WINDOW w AS (
@@ -599,7 +599,7 @@ WINDOW w AS (
PATTERN ((A | B) (A | B)+ (A | B))
DEFINE A AS v % 2 = 0, B AS v % 2 = 1
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev13'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_state_alt_absorb_group'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
---------------------------------------
PATTERN ((a | b) (a | b)+ (a | b))
@@ -629,7 +629,7 @@ WINDOW w AS (
(9 rows)
-- High state count - alternation with plus quantifier
-CREATE VIEW rpr_ev14 AS
+CREATE VIEW rpr_ev_state_alt_plus AS
SELECT count(*) OVER w
FROM generate_series(1, 100) AS s(v)
WINDOW w AS (
@@ -638,7 +638,7 @@ WINDOW w AS (
PATTERN ((A | B | C)+ D)
DEFINE A AS v % 4 = 1, B AS v % 4 = 2, C AS v % 4 = 3, D AS v % 4 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev14'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_state_alt_plus'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
-----------------------------
PATTERN ((a | b | c)+ d)
@@ -669,7 +669,7 @@ WINDOW w AS (
-- Early termination: first ALT branch (A) reaches FIN immediately,
-- pruning second branch (A B+) before it can accumulate B repetitions.
-CREATE VIEW rpr_ev15 AS
+CREATE VIEW rpr_ev_state_alt_prune AS
SELECT count(*) OVER w
FROM generate_series(1, 100) AS s(v)
WINDOW w AS (
@@ -678,7 +678,7 @@ WINDOW w AS (
PATTERN ((A | A B)+)
DEFINE A AS v = 1, B AS v > 1
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev15'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_state_alt_prune'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
-------------------------
PATTERN ((a | a b)+)
@@ -707,7 +707,7 @@ WINDOW w AS (
(8 rows)
-- Nested quantifiers causing state growth
-CREATE VIEW rpr_ev16 AS
+CREATE VIEW rpr_ev_state_nested_quant AS
SELECT count(*) OVER w
FROM generate_series(1, 1000) AS s(v)
WINDOW w AS (
@@ -716,7 +716,7 @@ WINDOW w AS (
PATTERN (((A | B)+)+)
DEFINE A AS v % 3 = 1, B AS v % 3 = 2
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev16'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_state_nested_quant'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
--------------------------
PATTERN (((a | b)+)+)
@@ -749,7 +749,7 @@ WINDOW w AS (
-- Context Statistics Tests (peak, total, pruned + absorbed/skipped)
-- ============================================================
-- Context absorption with unbounded quantifier at start
-CREATE VIEW rpr_ev17 AS
+CREATE VIEW rpr_ev_ctx_absorb_unbounded AS
SELECT count(*) OVER w
FROM generate_series(1, 50) AS s(v)
WINDOW w AS (
@@ -758,7 +758,7 @@ WINDOW w AS (
PATTERN (A+ B)
DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev17'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_absorb_unbounded'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
-------------------
PATTERN (a+ b)
@@ -788,7 +788,7 @@ WINDOW w AS (
(9 rows)
-- No absorption - bounded quantifier
-CREATE VIEW rpr_ev18 AS
+CREATE VIEW rpr_ev_ctx_no_absorb AS
SELECT count(*) OVER w
FROM generate_series(1, 50) AS s(v)
WINDOW w AS (
@@ -797,7 +797,7 @@ WINDOW w AS (
PATTERN (A{2,4} B)
DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev18'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_no_absorb'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
-----------------------
PATTERN (a{2,4} b)
@@ -827,7 +827,7 @@ WINDOW w AS (
(9 rows)
-- Contexts skipped by SKIP PAST LAST ROW
-CREATE VIEW rpr_ev19 AS
+CREATE VIEW rpr_ev_ctx_skip AS
SELECT count(*) OVER w
FROM generate_series(1, 100) AS s(v)
WINDOW w AS (
@@ -836,7 +836,7 @@ WINDOW w AS (
PATTERN (A B C)
DEFINE A AS v % 10 = 1, B AS v % 10 = 2, C AS v % 10 = 3
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev19'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_skip'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
--------------------
PATTERN (a b c)
@@ -866,7 +866,7 @@ WINDOW w AS (
(9 rows)
-- High context absorption - unbounded group
-CREATE VIEW rpr_ev20 AS
+CREATE VIEW rpr_ev_ctx_absorb_group AS
SELECT count(*) OVER w
FROM generate_series(1, 100) AS s(v)
WINDOW w AS (
@@ -875,7 +875,7 @@ WINDOW w AS (
PATTERN ((A B)+ C)
DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev20'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_absorb_group'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
-----------------------
PATTERN ((a b)+ c)
@@ -907,7 +907,7 @@ WINDOW w AS (
-- Fixed-length group absorption: (A B B)+ C
-- B B merged to B{2}; absorbable with fixed-length check
-- step_size=3 (A + B + B); v % 7 cycle gives 2 iterations per match
-CREATE VIEW rpr_ev20b AS
+CREATE VIEW rpr_ev_ctx_absorb_fixedvar AS
SELECT count(*) OVER w
FROM generate_series(1, 70) AS s(v)
WINDOW w AS (
@@ -916,7 +916,7 @@ WINDOW w AS (
PATTERN ((A B B)+ C)
DEFINE A AS v % 7 IN (1, 4), B AS v % 7 IN (2, 3, 5, 6), C AS v % 7 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev20b'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_absorb_fixedvar'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
-------------------------
PATTERN ((a b b)+ c)
@@ -947,7 +947,7 @@ WINDOW w AS (
-- Nested fixed-length group absorption: (A (B C){2} D)+ E
-- step_size = 1 + (1+1)*2 + 1 = 6; v % 13 cycle gives 2 iterations + E
-CREATE VIEW rpr_ev20c AS
+CREATE VIEW rpr_ev_ctx_absorb_nested AS
SELECT count(*) OVER w
FROM generate_series(1, 65) AS s(v)
WINDOW w AS (
@@ -958,7 +958,7 @@ WINDOW w AS (
C AS v % 13 IN (3, 5, 9, 11), D AS v % 13 IN (6, 12),
E AS v % 13 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev20c'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_absorb_nested'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
--------------------------------
PATTERN ((a (b c){2} d)+ e)
@@ -991,7 +991,7 @@ WINDOW w AS (
-- Doubly nested fixed-length group absorption: (A ((B C{3}){2} D){2} E)+ F
-- step_size = 1 + ((1+3)*2+1)*2 + 1 = 20; v % 41 cycle gives 2 iterations + F
-CREATE VIEW rpr_ev20d AS
+CREATE VIEW rpr_ev_ctx_absorb_deep AS
SELECT count(*) OVER w
FROM generate_series(1, 82) AS s(v)
WINDOW w AS (
@@ -1006,7 +1006,7 @@ WINDOW w AS (
E AS v % 41 IN (20, 40),
F AS v % 41 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev20d'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_absorb_deep'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
-------------------------------------------
PATTERN ((a ((b c c c){2} d){2} e)+ f)
@@ -1044,7 +1044,7 @@ WINDOW w AS (
-- 3-level END chain absorption: ((A (B C){2}){2})+
-- step_size = (1 + (1+1)*2) * 2 = 10; v % 21 cycle gives 2 iterations
-- END chain: END(BC{2}) -> END(A..{2}) -> END(+, ABSORBABLE)
-CREATE VIEW rpr_ev20e AS
+CREATE VIEW rpr_ev_ctx_absorb_endchain AS
SELECT count(*) OVER w
FROM generate_series(1, 42) AS s(v)
WINDOW w AS (
@@ -1055,7 +1055,7 @@ WINDOW w AS (
B AS v % 21 IN (2, 4, 7, 9, 12, 14, 17, 19),
C AS v % 21 IN (3, 5, 8, 10, 13, 15, 18, 20)
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev20e'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_absorb_endchain'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
---------------------------------
PATTERN (((a (b c){2}){2})+)
@@ -1090,7 +1090,7 @@ WINDOW w AS (
-- Match Length Statistics Tests
-- ============================================================
-- Fixed length matches - all same length
-CREATE VIEW rpr_ev21 AS
+CREATE VIEW rpr_ev_mlen_fixed AS
SELECT count(*) OVER w
FROM rpr_nfa_test
WINDOW w AS (
@@ -1101,7 +1101,7 @@ WINDOW w AS (
A AS cat = 'A', B AS cat = 'B', C AS cat = 'C',
D AS cat = 'D', E AS cat = 'E'
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev21'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_mlen_fixed'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
------------------------
PATTERN (a b c d e)
@@ -1133,7 +1133,7 @@ WINDOW w AS (
(9 rows)
-- Variable length matches - min/max/avg differ
-CREATE VIEW rpr_ev22 AS
+CREATE VIEW rpr_ev_mlen_variable AS
SELECT count(*) OVER w
FROM generate_series(1, 100) AS s(v)
WINDOW w AS (
@@ -1142,7 +1142,7 @@ WINDOW w AS (
PATTERN (A+ B)
DEFINE A AS v % 10 <> 0, B AS v % 10 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev22'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_mlen_variable'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
-------------------
PATTERN (a+ b)
@@ -1172,7 +1172,7 @@ WINDOW w AS (
(9 rows)
-- Very long matches
-CREATE VIEW rpr_ev23 AS
+CREATE VIEW rpr_ev_mlen_long AS
SELECT count(*) OVER w
FROM generate_series(1, 200) AS s(v)
WINDOW w AS (
@@ -1181,7 +1181,7 @@ WINDOW w AS (
PATTERN (A+ B)
DEFINE A AS v <= 195, B AS v > 195
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev23'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_mlen_long'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
-------------------
PATTERN (a+ b)
@@ -1211,7 +1211,7 @@ WINDOW w AS (
(9 rows)
-- Uniform match length with mismatches from gap rows (v%20 = 11..15)
-CREATE VIEW rpr_ev24 AS
+CREATE VIEW rpr_ev_mlen_with_mismatch AS
SELECT count(*) OVER w
FROM generate_series(1, 100) AS s(v)
WINDOW w AS (
@@ -1222,7 +1222,7 @@ WINDOW w AS (
A AS (v % 20 <> 0) AND (v % 20 <= 10 OR v % 20 > 15),
B AS v % 20 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev24'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_mlen_with_mismatch'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
-------------------
PATTERN (a+ b)
@@ -1258,7 +1258,7 @@ WINDOW w AS (
-- ============================================================
-- Pattern with complete match every cycle: 0 mismatched
-- A(1,2,3) B(4,5) C(6) repeats perfectly; X rows are pruned, not mismatched
-CREATE VIEW rpr_ev25 AS
+CREATE VIEW rpr_ev_mlen_no_mismatch AS
SELECT count(*) OVER w
FROM (
SELECT v,
@@ -1274,7 +1274,7 @@ WINDOW w AS (
PATTERN (A+ B+ C)
DEFINE A AS cat = 'A', B AS cat = 'B', C AS cat = 'C'
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev25'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_mlen_no_mismatch'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
----------------------
PATTERN (a+ b+ c)
@@ -1311,7 +1311,7 @@ WINDOW w AS (
(9 rows)
-- Long partial matches that fail
-CREATE VIEW rpr_ev26 AS
+CREATE VIEW rpr_ev_mlen_long_partial AS
SELECT count(*) OVER w
FROM (
SELECT i AS v,
@@ -1332,7 +1332,7 @@ WINDOW w AS (
PATTERN (A+ B+ C)
DEFINE A AS cat = 'A', B AS cat = 'B', C AS cat = 'C'
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev26'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_mlen_long_partial'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
----------------------
PATTERN (a+ b+ c)
@@ -1377,7 +1377,7 @@ WINDOW w AS (
-- JSON Format Tests
-- ============================================================
-- JSON format output with all statistics
-CREATE VIEW rpr_ev27 AS
+CREATE VIEW rpr_ev_json_basic AS
SELECT count(*) OVER w
FROM generate_series(1, 50) AS s(v)
WINDOW w AS (
@@ -1386,7 +1386,7 @@ WINDOW w AS (
PATTERN (A+ B+)
DEFINE A AS v % 3 = 1, B AS v % 3 = 2
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev27'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_json_basic'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
--------------------
PATTERN (a+ b+)
@@ -1454,7 +1454,7 @@ WINDOW w AS (
(1 row)
-- JSON format with match length statistics
-CREATE VIEW rpr_ev28 AS
+CREATE VIEW rpr_ev_json_matchlen AS
SELECT count(*) OVER w
FROM generate_series(1, 100) AS s(v)
WINDOW w AS (
@@ -1463,7 +1463,7 @@ WINDOW w AS (
PATTERN (A+ B)
DEFINE A AS v % 10 <> 0, B AS v % 10 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev28'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_json_matchlen'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
-------------------
PATTERN (a+ b)
@@ -1535,7 +1535,7 @@ WINDOW w AS (
-- JSON format with mismatch statistics
-- Pattern A B C expects 1,2,3 but gets 1,2,4 twice causing mismatches
-CREATE VIEW rpr_ev29 AS
+CREATE VIEW rpr_ev_json_mismatch AS
SELECT count(*) OVER w
FROM (VALUES (1),(2),(4), (1),(2),(4), (1),(2),(3)) AS t(v)
WINDOW w AS (
@@ -1544,7 +1544,7 @@ WINDOW w AS (
PATTERN (A B C)
DEFINE A AS v = 1, B AS v = 2, C AS v = 3
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev29'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_json_mismatch'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
--------------------
PATTERN (a b c)
@@ -1615,7 +1615,7 @@ WINDOW w AS (
-- JSON format with skipped context statistics
-- Alternation pattern with SKIP PAST LAST ROW causes many contexts to be skipped
-CREATE VIEW rpr_ev30 AS
+CREATE VIEW rpr_ev_json_skip AS
SELECT count(*) OVER w
FROM generate_series(1, 100) AS s(v)
WINDOW w AS (
@@ -1624,7 +1624,7 @@ WINDOW w AS (
PATTERN ((A | B) (A | B) (A | B) (A | B) (A | B) (A | B) (A | B) (A | B))
DEFINE A AS v % 2 = 1, B AS v % 2 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev30'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_json_skip'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
------------------------------------------------------------------------------
PATTERN ((a | b) (a | b) (a | b) (a | b) (a | b) (a | b) (a | b) (a | b))
@@ -1698,7 +1698,7 @@ WINDOW w AS (
-- XML Format Tests
-- ============================================================
-- XML format output
-CREATE VIEW rpr_ev31 AS
+CREATE VIEW rpr_ev_xml_basic AS
SELECT count(*) OVER w
FROM generate_series(1, 30) AS s(v)
WINDOW w AS (
@@ -1707,7 +1707,7 @@ WINDOW w AS (
PATTERN (A B)
DEFINE A AS v % 2 = 1, B AS v % 2 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev31'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_xml_basic'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
------------------
PATTERN (a b)
@@ -1778,7 +1778,7 @@ WINDOW w AS (
-- Multiple Partitions Tests
-- ============================================================
-- Statistics across multiple partitions
-CREATE VIEW rpr_ev32 AS
+CREATE VIEW rpr_ev_part_multi AS
SELECT count(*) OVER w
FROM (
SELECT p, v
@@ -1792,7 +1792,7 @@ WINDOW w AS (
PATTERN (A+ B)
DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev32'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_part_multi'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
-------------------
PATTERN (a+ b)
@@ -1832,7 +1832,7 @@ WINDOW w AS (
(14 rows)
-- Different pattern behavior per partition
-CREATE VIEW rpr_ev33 AS
+CREATE VIEW rpr_ev_part_diff AS
SELECT count(*) OVER w
FROM (
SELECT
@@ -1847,7 +1847,7 @@ WINDOW w AS (
PATTERN (A+ B)
DEFINE A AS val < 5, B AS val >= 5
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev33'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_part_diff'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
-------------------
PATTERN (a+ b)
@@ -1889,7 +1889,7 @@ WINDOW w AS (
-- Edge Cases
-- ============================================================
-- Empty result set
-CREATE VIEW rpr_ev34 AS
+CREATE VIEW rpr_ev_edge_empty AS
SELECT count(*) OVER w
FROM generate_series(1, 0) AS s(v)
WINDOW w AS (
@@ -1898,7 +1898,7 @@ WINDOW w AS (
PATTERN (A B)
DEFINE A AS v = 1, B AS v = 2
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev34'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_edge_empty'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
------------------
PATTERN (a b)
@@ -1923,7 +1923,7 @@ WINDOW w AS (
(4 rows)
-- Single row
-CREATE VIEW rpr_ev35 AS
+CREATE VIEW rpr_ev_edge_single_row AS
SELECT count(*) OVER w
FROM generate_series(1, 1) AS s(v)
WINDOW w AS (
@@ -1932,7 +1932,7 @@ WINDOW w AS (
PATTERN (A)
DEFINE A AS TRUE
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev35'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_edge_single_row'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
----------------
PATTERN (a)
@@ -1961,7 +1961,7 @@ WINDOW w AS (
(8 rows)
-- Pattern longer than data
-CREATE VIEW rpr_ev36 AS
+CREATE VIEW rpr_ev_edge_pattern_longer AS
SELECT count(*) OVER w
FROM generate_series(1, 5) AS s(v)
WINDOW w AS (
@@ -1972,7 +1972,7 @@ WINDOW w AS (
A AS v = 1, B AS v = 2, C AS v = 3, D AS v = 4, E AS v = 5,
F AS v = 6, G AS v = 7, H AS v = 8, I AS v = 9, J AS v = 10
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev36'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_edge_pattern_longer'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
----------------------------------
PATTERN (a b c d e f g h i j)
@@ -2003,7 +2003,7 @@ WINDOW w AS (
(8 rows)
-- All rows match as single match
-CREATE VIEW rpr_ev37 AS
+CREATE VIEW rpr_ev_edge_single_match AS
SELECT count(*) OVER w
FROM generate_series(1, 50) AS s(v)
WINDOW w AS (
@@ -2012,7 +2012,7 @@ WINDOW w AS (
PATTERN (A+)
DEFINE A AS TRUE
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev37'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_edge_single_match'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
-----------------
PATTERN (a+)
@@ -2045,7 +2045,7 @@ WINDOW w AS (
-- Complex Pattern Tests
-- ============================================================
-- Nested groups
-CREATE VIEW rpr_ev38 AS
+CREATE VIEW rpr_ev_cpx_nested AS
SELECT count(*) OVER w
FROM generate_series(1, 60) AS s(v)
WINDOW w AS (
@@ -2054,7 +2054,7 @@ WINDOW w AS (
PATTERN (((A B) C)+)
DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev38'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_cpx_nested'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
-------------------------
PATTERN (((a b) c)+)
@@ -2084,7 +2084,7 @@ WINDOW w AS (
(9 rows)
-- Multiple alternations
-CREATE VIEW rpr_ev39 AS
+CREATE VIEW rpr_ev_cpx_multi_alt AS
SELECT count(*) OVER w
FROM rpr_nfa_test
WINDOW w AS (
@@ -2095,7 +2095,7 @@ WINDOW w AS (
A AS cat = 'A', B AS cat = 'B', C AS cat = 'C',
D AS cat = 'D', E AS cat = 'E'
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev39'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_cpx_multi_alt'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
----------------------------------
PATTERN ((a | b) (c | d | e))
@@ -2127,7 +2127,7 @@ WINDOW w AS (
(9 rows)
-- Optional elements
-CREATE VIEW rpr_ev40 AS
+CREATE VIEW rpr_ev_cpx_optional AS
SELECT count(*) OVER w
FROM generate_series(1, 50) AS s(v)
WINDOW w AS (
@@ -2136,7 +2136,7 @@ WINDOW w AS (
PATTERN (A B? C)
DEFINE A AS v % 4 = 1, B AS v % 4 = 2, C AS v % 4 = 3
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev40'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_cpx_optional'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
---------------------
PATTERN (a b? c)
@@ -2166,7 +2166,7 @@ WINDOW w AS (
(9 rows)
-- Bounded quantifiers
-CREATE VIEW rpr_ev41 AS
+CREATE VIEW rpr_ev_cpx_bounded AS
SELECT count(*) OVER w
FROM generate_series(1, 100) AS s(v)
WINDOW w AS (
@@ -2175,7 +2175,7 @@ WINDOW w AS (
PATTERN (A{2,5} B)
DEFINE A AS v % 10 <> 0, B AS v % 10 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev41'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_cpx_bounded'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
-----------------------
PATTERN (a{2,5} b)
@@ -2205,7 +2205,7 @@ WINDOW w AS (
(9 rows)
-- Star quantifier
-CREATE VIEW rpr_ev42 AS
+CREATE VIEW rpr_ev_cpx_star AS
SELECT count(*) OVER w
FROM generate_series(1, 50) AS s(v)
WINDOW w AS (
@@ -2214,7 +2214,7 @@ WINDOW w AS (
PATTERN (A B* C)
DEFINE A AS v % 10 = 1, B AS v % 10 IN (2,3,4,5,6,7,8), C AS v % 10 = 9
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev42'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_cpx_star'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
---------------------
PATTERN (a b* c)
@@ -2247,7 +2247,7 @@ WINDOW w AS (
-- Real-world Pattern Examples
-- ============================================================
-- Stock price pattern - V-shape (down then up)
-CREATE VIEW rpr_ev43 AS
+CREATE VIEW rpr_ev_real_vshape AS
SELECT count(*) OVER w
FROM rpr_nfa_complex
WINDOW w AS (
@@ -2256,7 +2256,7 @@ WINDOW w AS (
PATTERN (D+ U+)
DEFINE D AS trend = 'D', U AS trend = 'U'
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev43'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_real_vshape'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
--------------------
PATTERN (d+ u+)
@@ -2286,7 +2286,7 @@ WINDOW w AS (
(9 rows)
-- Stock price pattern - peak (up, stable, down)
-CREATE VIEW rpr_ev44 AS
+CREATE VIEW rpr_ev_real_peak AS
SELECT count(*) OVER w
FROM rpr_nfa_complex
WINDOW w AS (
@@ -2295,7 +2295,7 @@ WINDOW w AS (
PATTERN (U+ S* D+)
DEFINE U AS trend = 'U', S AS trend = 'S', D AS trend = 'D'
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev44'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_real_peak'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
-----------------------
PATTERN (u+ s* d+)
@@ -2325,7 +2325,7 @@ WINDOW w AS (
(9 rows)
-- Consecutive increasing values (using PREV)
-CREATE VIEW rpr_ev45 AS
+CREATE VIEW rpr_ev_real_increasing AS
SELECT count(*) OVER w
FROM generate_series(1, 50) AS s(v)
WINDOW w AS (
@@ -2334,7 +2334,7 @@ WINDOW w AS (
PATTERN (A{3,})
DEFINE A AS v > PREV(v) OR PREV(v) IS NULL
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev45'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_real_increasing'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
--------------------
PATTERN (a{3,})
@@ -2367,7 +2367,7 @@ WINDOW w AS (
-- Performance-oriented Tests
-- ============================================================
-- Large dataset with simple pattern
-CREATE VIEW rpr_ev46 AS
+CREATE VIEW rpr_ev_perf_large_simple AS
SELECT count(*) OVER w
FROM generate_series(1, 1000) AS s(v)
WINDOW w AS (
@@ -2376,7 +2376,7 @@ WINDOW w AS (
PATTERN (A B)
DEFINE A AS v % 2 = 1, B AS v % 2 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev46'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_perf_large_simple'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
------------------
PATTERN (a b)
@@ -2406,7 +2406,7 @@ WINDOW w AS (
(9 rows)
-- Large dataset with absorption
-CREATE VIEW rpr_ev47 AS
+CREATE VIEW rpr_ev_perf_large_absorb AS
SELECT count(*) OVER w
FROM generate_series(1, 1000) AS s(v)
WINDOW w AS (
@@ -2415,7 +2415,7 @@ WINDOW w AS (
PATTERN (A+ B)
DEFINE A AS v % 100 <> 0, B AS v % 100 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev47'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_perf_large_absorb'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
-------------------
PATTERN (a+ b)
@@ -2445,7 +2445,7 @@ WINDOW w AS (
(9 rows)
-- High state merge ratio
-CREATE VIEW rpr_ev48 AS
+CREATE VIEW rpr_ev_perf_high_merge AS
SELECT count(*) OVER w
FROM generate_series(1, 500) AS s(v)
WINDOW w AS (
@@ -2454,7 +2454,7 @@ WINDOW w AS (
PATTERN ((A | B)+ C)
DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev48'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_perf_high_merge'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
-------------------------
PATTERN ((a | b)+ c)
@@ -2487,7 +2487,7 @@ WINDOW w AS (
-- INITIAL vs no INITIAL comparison
-- ============================================================
-- With INITIAL keyword
-CREATE VIEW rpr_ev49 AS
+CREATE VIEW rpr_ev_initial_with AS
SELECT count(*) OVER w
FROM generate_series(1, 50) AS s(v)
WINDOW w AS (
@@ -2497,7 +2497,7 @@ WINDOW w AS (
PATTERN (A+ B)
DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev49'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_initial_with'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
-------------------
PATTERN (a+ b)
@@ -2528,7 +2528,7 @@ WINDOW w AS (
(9 rows)
-- Without INITIAL keyword (same behavior currently)
-CREATE VIEW rpr_ev50 AS
+CREATE VIEW rpr_ev_initial_without AS
SELECT count(*) OVER w
FROM generate_series(1, 50) AS s(v)
WINDOW w AS (
@@ -2537,7 +2537,7 @@ WINDOW w AS (
PATTERN (A+ B)
DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev50'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_initial_without'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
-------------------
PATTERN (a+ b)
@@ -2570,7 +2570,7 @@ WINDOW w AS (
-- Quantifier Variations
-- ============================================================
-- Plus quantifier
-CREATE VIEW rpr_ev51 AS
+CREATE VIEW rpr_ev_quant_plus AS
SELECT count(*) OVER w
FROM generate_series(1, 40) AS s(v)
WINDOW w AS (
@@ -2579,7 +2579,7 @@ WINDOW w AS (
PATTERN (A+)
DEFINE A AS v % 4 <> 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev51'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_quant_plus'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
-----------------
PATTERN (a+)
@@ -2609,7 +2609,7 @@ WINDOW w AS (
(9 rows)
-- Star quantifier (zero or more)
-CREATE VIEW rpr_ev52 AS
+CREATE VIEW rpr_ev_quant_star AS
SELECT count(*) OVER w
FROM generate_series(1, 40) AS s(v)
WINDOW w AS (
@@ -2618,7 +2618,7 @@ WINDOW w AS (
PATTERN (A* B)
DEFINE A AS v % 4 IN (1, 2), B AS v % 4 = 3
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev52'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_quant_star'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
-------------------
PATTERN (a* b)
@@ -2648,7 +2648,7 @@ WINDOW w AS (
(9 rows)
-- Question mark (zero or one)
-CREATE VIEW rpr_ev53 AS
+CREATE VIEW rpr_ev_quant_question AS
SELECT count(*) OVER w
FROM generate_series(1, 40) AS s(v)
WINDOW w AS (
@@ -2657,7 +2657,7 @@ WINDOW w AS (
PATTERN (A? B C)
DEFINE A AS v % 4 = 1, B AS v % 4 = 2, C AS v % 4 = 3
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev53'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_quant_question'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
---------------------
PATTERN (a? b c)
@@ -2687,7 +2687,7 @@ WINDOW w AS (
(9 rows)
-- Exact count {n}
-CREATE VIEW rpr_ev54 AS
+CREATE VIEW rpr_ev_quant_exact AS
SELECT count(*) OVER w
FROM generate_series(1, 50) AS s(v)
WINDOW w AS (
@@ -2696,7 +2696,7 @@ WINDOW w AS (
PATTERN (A{3} B)
DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev54'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_quant_exact'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
---------------------
PATTERN (a{3} b)
@@ -2726,7 +2726,7 @@ WINDOW w AS (
(9 rows)
-- Range {n,m}
-CREATE VIEW rpr_ev55 AS
+CREATE VIEW rpr_ev_quant_range AS
SELECT count(*) OVER w
FROM generate_series(1, 50) AS s(v)
WINDOW w AS (
@@ -2735,7 +2735,7 @@ WINDOW w AS (
PATTERN (A{2,4} B)
DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev55'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_quant_range'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
-----------------------
PATTERN (a{2,4} b)
@@ -2765,7 +2765,7 @@ WINDOW w AS (
(9 rows)
-- At least {n,}
-CREATE VIEW rpr_ev56 AS
+CREATE VIEW rpr_ev_quant_atleast AS
SELECT count(*) OVER w
FROM generate_series(1, 50) AS s(v)
WINDOW w AS (
@@ -2774,7 +2774,7 @@ WINDOW w AS (
PATTERN (A{3,} B)
DEFINE A AS v % 10 <> 0, B AS v % 10 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev56'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_quant_atleast'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
----------------------
PATTERN (a{3,} b)
@@ -2808,7 +2808,7 @@ WINDOW w AS (
-- ============================================================
-- Verify state count accuracy
-- Pattern A+ B with 20 rows should show predictable state behavior
-CREATE VIEW rpr_ev57 AS
+CREATE VIEW rpr_ev_reg_state_count AS
SELECT count(*) OVER w
FROM generate_series(1, 20) AS s(v)
WINDOW w AS (
@@ -2817,7 +2817,7 @@ WINDOW w AS (
PATTERN (A+ B)
DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev57'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_reg_state_count'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
-------------------
PATTERN (a+ b)
@@ -2847,7 +2847,7 @@ WINDOW w AS (
(9 rows)
-- Verify context count with known absorption
-CREATE VIEW rpr_ev58 AS
+CREATE VIEW rpr_ev_reg_ctx_absorb AS
SELECT count(*) OVER w
FROM generate_series(1, 30) AS s(v)
WINDOW w AS (
@@ -2856,7 +2856,7 @@ WINDOW w AS (
PATTERN (A+ B C)
DEFINE A AS v % 10 IN (1,2,3,4,5,6,7), B AS v % 10 = 8, C AS v % 10 = 9
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev58'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_reg_ctx_absorb'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
---------------------
PATTERN (a+ b c)
@@ -2886,7 +2886,7 @@ WINDOW w AS (
(9 rows)
-- Verify match length with fixed-length pattern
-CREATE VIEW rpr_ev59 AS
+CREATE VIEW rpr_ev_reg_matchlen AS
SELECT count(*) OVER w
FROM generate_series(1, 30) AS s(v)
WINDOW w AS (
@@ -2895,7 +2895,7 @@ WINDOW w AS (
PATTERN (A B C)
DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev59'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_reg_matchlen'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
--------------------
PATTERN (a b c)
@@ -2928,7 +2928,7 @@ WINDOW w AS (
-- Alternation Pattern Tests
-- ============================================================
-- Simple alternation
-CREATE VIEW rpr_ev60 AS
+CREATE VIEW rpr_ev_alt_simple AS
SELECT count(*) OVER w
FROM rpr_nfa_test
WINDOW w AS (
@@ -2937,7 +2937,7 @@ WINDOW w AS (
PATTERN ((A | B) C)
DEFINE A AS cat = 'A', B AS cat = 'B', C AS cat = 'C'
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev60'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_simple'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
------------------------
PATTERN ((a | b) c)
@@ -2967,7 +2967,7 @@ WINDOW w AS (
(9 rows)
-- Multiple items in alternation
-CREATE VIEW rpr_ev61 AS
+CREATE VIEW rpr_ev_alt_multi_item AS
SELECT count(*) OVER w
FROM rpr_nfa_test
WINDOW w AS (
@@ -2978,7 +2978,7 @@ WINDOW w AS (
A AS cat = 'A', B AS cat = 'B', C AS cat = 'C',
D AS cat = 'D', E AS cat = 'E'
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev61'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_multi_item'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
--------------------------------
PATTERN ((a | b | c | d) e)
@@ -3010,7 +3010,7 @@ WINDOW w AS (
(9 rows)
-- Alternation with quantifiers
-CREATE VIEW rpr_ev62 AS
+CREATE VIEW rpr_ev_alt_with_quant AS
SELECT count(*) OVER w
FROM generate_series(1, 50) AS s(v)
WINDOW w AS (
@@ -3019,7 +3019,7 @@ WINDOW w AS (
PATTERN ((A | B)+ C)
DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev62'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_with_quant'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
-------------------------
PATTERN ((a | b)+ c)
@@ -3049,7 +3049,7 @@ WINDOW w AS (
(9 rows)
-- Multiple alternatives (4+)
-CREATE VIEW rpr_ev63 AS
+CREATE VIEW rpr_ev_alt_four_plus AS
SELECT count(*) OVER w
FROM generate_series(1, 100) AS s(v)
WINDOW w AS (
@@ -3057,7 +3057,7 @@ WINDOW w AS (
PATTERN (A | B | C | D | E)
DEFINE A AS v % 5 = 0, B AS v % 5 = 1, C AS v % 5 = 2, D AS v % 5 = 3, E AS v % 5 = 4
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev63'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_four_plus'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
--------------------------------
PATTERN (a | b | c | d | e)
@@ -3085,7 +3085,7 @@ WINDOW w AS (
(8 rows)
-- Alternation at start
-CREATE VIEW rpr_ev64 AS
+CREATE VIEW rpr_ev_alt_at_start AS
SELECT count(*) OVER w
FROM generate_series(1, 60) AS s(v)
WINDOW w AS (
@@ -3093,7 +3093,7 @@ WINDOW w AS (
PATTERN ((A | B) C D)
DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev64'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_at_start'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
--------------------------
PATTERN ((a | b) c d)
@@ -3122,7 +3122,7 @@ WINDOW w AS (
(9 rows)
-- Multiple sequential alternations
-CREATE VIEW rpr_ev65 AS
+CREATE VIEW rpr_ev_alt_sequential AS
SELECT count(*) OVER w
FROM generate_series(1, 100) AS s(v)
WINDOW w AS (
@@ -3130,7 +3130,7 @@ WINDOW w AS (
PATTERN ((A | B) C (D | E) F)
DEFINE A AS v % 6 = 0, B AS v % 6 = 1, C AS v % 6 = 2, D AS v % 6 = 3, E AS v % 6 = 4, F AS v % 6 = 5
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev65'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_sequential'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
----------------------------------
PATTERN ((a | b) c (d | e) f)
@@ -3158,7 +3158,7 @@ WINDOW w AS (
(8 rows)
-- Quantified alternatives
-CREATE VIEW rpr_ev66 AS
+CREATE VIEW rpr_ev_alt_quantified AS
SELECT count(*) OVER w
FROM generate_series(1, 60) AS s(v)
WINDOW w AS (
@@ -3166,7 +3166,7 @@ WINDOW w AS (
PATTERN ((A+ | B+) C)
DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev66'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_quantified'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
--------------------------
PATTERN ((a+ | b+) c)
@@ -3195,7 +3195,7 @@ WINDOW w AS (
(9 rows)
-- Alternation at end
-CREATE VIEW rpr_ev67 AS
+CREATE VIEW rpr_ev_alt_at_end AS
SELECT count(*) OVER w
FROM generate_series(1, 60) AS s(v)
WINDOW w AS (
@@ -3203,7 +3203,7 @@ WINDOW w AS (
PATTERN (A B (C | D))
DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev67'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_at_end'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
--------------------------
PATTERN (a b (c | d))
@@ -3233,7 +3233,7 @@ WINDOW w AS (
-- Nested ALT at start of branch inside outer ALT
-- Pattern: (A ((B | C) D | E)) - preceding VAR + inner ALT as first branch element
-CREATE VIEW rpr_ev68 AS
+CREATE VIEW rpr_ev_alt_nested_start AS
SELECT count(*) OVER w
FROM generate_series(1, 20) AS s(v)
WINDOW w AS (
@@ -3241,7 +3241,7 @@ WINDOW w AS (
PATTERN (A ((B | C) D | E))
DEFINE A AS v % 5 = 0, B AS v % 5 = 1, C AS v % 5 = 2, D AS v % 5 = 3, E AS v % 5 = 4
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev68'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_nested_start'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
--------------------------------
PATTERN (a ((b | c) d | e))
@@ -3270,7 +3270,7 @@ WINDOW w AS (
-- Nested ALT at end of branch inside outer ALT
-- Pattern: (C (A | B) | D) - inner ALT is last element in outer branch
-CREATE VIEW rpr_ev69 AS
+CREATE VIEW rpr_ev_alt_nested_end AS
SELECT count(*) OVER w
FROM generate_series(1, 20) AS s(v)
WINDOW w AS (
@@ -3278,7 +3278,7 @@ WINDOW w AS (
PATTERN (C (A | B) | D)
DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev69'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_nested_end'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
----------------------------
PATTERN (c (a | b) | d)
@@ -3309,7 +3309,7 @@ WINDOW w AS (
-- Group Pattern Tests
-- ============================================================
-- Simple group
-CREATE VIEW rpr_ev70 AS
+CREATE VIEW rpr_ev_grp_simple AS
SELECT count(*) OVER w
FROM generate_series(1, 40) AS s(v)
WINDOW w AS (
@@ -3318,7 +3318,7 @@ WINDOW w AS (
PATTERN ((A B)+)
DEFINE A AS v % 2 = 1, B AS v % 2 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev70'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_grp_simple'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
---------------------
PATTERN ((a b)+)
@@ -3348,7 +3348,7 @@ WINDOW w AS (
(9 rows)
-- Group with bounded quantifier
-CREATE VIEW rpr_ev71 AS
+CREATE VIEW rpr_ev_grp_bounded AS
SELECT count(*) OVER w
FROM generate_series(1, 40) AS s(v)
WINDOW w AS (
@@ -3357,7 +3357,7 @@ WINDOW w AS (
PATTERN ((A B){2,4})
DEFINE A AS v % 2 = 1, B AS v % 2 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev71'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_grp_bounded'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
-------------------------
PATTERN ((a b){2,4})
@@ -3387,7 +3387,7 @@ WINDOW w AS (
(9 rows)
-- Nested groups
-CREATE VIEW rpr_ev72 AS
+CREATE VIEW rpr_ev_grp_nested AS
SELECT count(*) OVER w
FROM generate_series(1, 60) AS s(v)
WINDOW w AS (
@@ -3396,7 +3396,7 @@ WINDOW w AS (
PATTERN (((A B){2})+)
DEFINE A AS v % 2 = 1, B AS v % 2 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev72'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_grp_nested'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
--------------------------
PATTERN (((a b){2})+)
@@ -3426,7 +3426,7 @@ WINDOW w AS (
(9 rows)
-- Deep nesting (3+ levels)
-CREATE VIEW rpr_ev73 AS
+CREATE VIEW rpr_ev_grp_deep AS
SELECT count(*) OVER w
FROM generate_series(1, 40) AS s(v)
WINDOW w AS (
@@ -3434,7 +3434,7 @@ WINDOW w AS (
PATTERN ((((A | B)+)+)+)
DEFINE A AS v % 2 = 0, B AS v % 2 = 1
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev73'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_grp_deep'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
-----------------------------
PATTERN ((((a | b)+)+)+)
@@ -3463,7 +3463,7 @@ WINDOW w AS (
(9 rows)
-- Bounded quantifier on alternation
-CREATE VIEW rpr_ev74 AS
+CREATE VIEW rpr_ev_grp_bounded_alt AS
SELECT count(*) OVER w
FROM generate_series(1, 60) AS s(v)
WINDOW w AS (
@@ -3471,7 +3471,7 @@ WINDOW w AS (
PATTERN ((A | B){2,3} C)
DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev74'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_grp_bounded_alt'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
-----------------------------
PATTERN ((a | b){2,3} c)
@@ -3500,7 +3500,7 @@ WINDOW w AS (
(9 rows)
-- Nested groups with quantifiers
-CREATE VIEW rpr_ev75 AS
+CREATE VIEW rpr_ev_grp_nested_quant AS
SELECT count(*) OVER w
FROM generate_series(1, 60) AS s(v)
WINDOW w AS (
@@ -3508,7 +3508,7 @@ WINDOW w AS (
PATTERN (((A B)+ C)*)
DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev75'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_grp_nested_quant'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
--------------------------
PATTERN (((a b)+ c)*)
@@ -3537,7 +3537,7 @@ WINDOW w AS (
(9 rows)
-- Partial nested quantification
-CREATE VIEW rpr_ev76 AS
+CREATE VIEW rpr_ev_grp_partial_quant AS
SELECT count(*) OVER w
FROM generate_series(1, 60) AS s(v)
WINDOW w AS (
@@ -3545,7 +3545,7 @@ WINDOW w AS (
PATTERN ((A (B C)+)*)
DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev76'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_grp_partial_quant'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
--------------------------
PATTERN ((a (b c)+)*)
@@ -3577,7 +3577,7 @@ WINDOW w AS (
-- Window Function Combinations
-- ============================================================
-- count(*) with pattern
-CREATE VIEW rpr_ev77 AS
+CREATE VIEW rpr_ev_wfn_count AS
SELECT count(*) OVER w
FROM generate_series(1, 30) AS s(v)
WINDOW w AS (
@@ -3586,7 +3586,7 @@ WINDOW w AS (
PATTERN (A+ B)
DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev77'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_wfn_count'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
-------------------
PATTERN (a+ b)
@@ -3616,7 +3616,7 @@ WINDOW w AS (
(9 rows)
-- first_value with pattern
-CREATE VIEW rpr_ev78 AS
+CREATE VIEW rpr_ev_wfn_first_value AS
SELECT first_value(v) OVER w
FROM generate_series(1, 30) AS s(v)
WINDOW w AS (
@@ -3625,7 +3625,7 @@ WINDOW w AS (
PATTERN (A+ B)
DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev78'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_wfn_first_value'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
-------------------
PATTERN (a+ b)
@@ -3655,7 +3655,7 @@ WINDOW w AS (
(9 rows)
-- last_value with pattern
-CREATE VIEW rpr_ev79 AS
+CREATE VIEW rpr_ev_wfn_last_value AS
SELECT last_value(v) OVER w
FROM generate_series(1, 30) AS s(v)
WINDOW w AS (
@@ -3664,7 +3664,7 @@ WINDOW w AS (
PATTERN (A+ B)
DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev79'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_wfn_last_value'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
-------------------
PATTERN (a+ b)
@@ -3694,7 +3694,7 @@ WINDOW w AS (
(9 rows)
-- Multiple window functions
-CREATE VIEW rpr_ev80 AS
+CREATE VIEW rpr_ev_wfn_multi AS
SELECT
count(*) OVER w,
first_value(v) OVER w,
@@ -3706,7 +3706,7 @@ WINDOW w AS (
PATTERN (A+ B)
DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev80'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_wfn_multi'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
-------------------
PATTERN (a+ b)
@@ -3742,7 +3742,7 @@ WINDOW w AS (
-- DEFINE Expression Variations
-- ============================================================
-- Complex boolean expressions
-CREATE VIEW rpr_ev81 AS
+CREATE VIEW rpr_ev_def_complex_bool AS
SELECT count(*) OVER w
FROM generate_series(1, 50) AS s(v)
WINDOW w AS (
@@ -3753,7 +3753,7 @@ WINDOW w AS (
A AS (v % 5 <> 0) AND (v % 3 <> 0),
B AS (v % 5 = 0) OR (v % 3 = 0)
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev81'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_def_complex_bool'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
-------------------
PATTERN (a+ b)
@@ -3785,7 +3785,7 @@ WINDOW w AS (
(9 rows)
-- Using PREV function
-CREATE VIEW rpr_ev82 AS
+CREATE VIEW rpr_ev_def_prev AS
SELECT count(*) OVER w
FROM generate_series(1, 30) AS s(v)
WINDOW w AS (
@@ -3797,7 +3797,7 @@ WINDOW w AS (
U AS v > PREV(v),
D AS v < PREV(v)
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev82'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_def_prev'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
----------------------
PATTERN (s u+ d+)
@@ -3829,7 +3829,7 @@ WINDOW w AS (
(8 rows)
-- Using NULL comparisons
-CREATE VIEW rpr_ev83 AS
+CREATE VIEW rpr_ev_def_null AS
SELECT count(*) OVER w
FROM (
SELECT CASE WHEN v % 5 = 0 THEN NULL ELSE v END AS v
@@ -3841,7 +3841,7 @@ WINDOW w AS (
PATTERN (A+ B)
DEFINE A AS v IS NOT NULL, B AS v IS NULL
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev83'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_def_null'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
-------------------
PATTERN (a+ b)
@@ -3877,7 +3877,7 @@ WINDOW w AS (
-- Large Scale Statistics Verification
-- ============================================================
-- 500 rows - verify statistics scale correctly
-CREATE VIEW rpr_ev84 AS
+CREATE VIEW rpr_ev_scale_500rows AS
SELECT count(*) OVER w
FROM generate_series(1, 500) AS s(v)
WINDOW w AS (
@@ -3886,7 +3886,7 @@ WINDOW w AS (
PATTERN (A+ B C)
DEFINE A AS v % 10 < 7, B AS v % 10 = 7, C AS v % 10 = 8
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev84'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_scale_500rows'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
---------------------
PATTERN (a+ b c)
@@ -3916,7 +3916,7 @@ WINDOW w AS (
(9 rows)
-- High match count scenario
-CREATE VIEW rpr_ev85 AS
+CREATE VIEW rpr_ev_scale_high_match AS
SELECT count(*) OVER w
FROM generate_series(1, 500) AS s(v)
WINDOW w AS (
@@ -3925,7 +3925,7 @@ WINDOW w AS (
PATTERN (A B)
DEFINE A AS v % 2 = 1, B AS v % 2 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev85'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_scale_high_match'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
------------------
PATTERN (a b)
@@ -3955,7 +3955,7 @@ WINDOW w AS (
(9 rows)
-- High skip count scenario
-CREATE VIEW rpr_ev86 AS
+CREATE VIEW rpr_ev_scale_high_skip AS
SELECT count(*) OVER w
FROM generate_series(1, 500) AS s(v)
WINDOW w AS (
@@ -3969,7 +3969,7 @@ WINDOW w AS (
D AS v % 100 = 4,
E AS v % 100 = 5
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev86'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_scale_high_skip'), E'\n')) AS line WHERE line ~ 'PATTERN';
line
------------------------
PATTERN (a b c d e)
@@ -4013,17 +4013,17 @@ WINDOW w AS (
-- Test with row_number() as representative case.
--
-- Without RPR: row_number() frame is optimized to ROWS UNBOUNDED PRECEDING
-CREATE VIEW rpr_ev87 AS
+CREATE VIEW rpr_ev_opt_no_rpr AS
SELECT row_number() OVER w
FROM generate_series(1, 10) AS s(v)
WINDOW w AS (
ORDER BY v
ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
);
-EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev87;
+EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev_opt_no_rpr;
QUERY PLAN
--------------------------------------------------------------
- Subquery Scan on rpr_ev87
+ Subquery Scan on rpr_ev_opt_no_rpr
-> WindowAgg
Window: w AS (ORDER BY s.v ROWS UNBOUNDED PRECEDING)
-> Sort
@@ -4032,7 +4032,7 @@ EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev87;
(6 rows)
-- With RPR: frame must remain ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-CREATE VIEW rpr_ev88 AS
+CREATE VIEW rpr_ev_opt_with_rpr AS
SELECT row_number() OVER w
FROM generate_series(1, 10) AS s(v)
WINDOW w AS (
@@ -4043,10 +4043,10 @@ WINDOW w AS (
DEFINE
B AS v > PREV(v)
);
-EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev88;
+EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev_opt_with_rpr;
QUERY PLAN
--------------------------------------------------------------------------------------
- Subquery Scan on rpr_ev88
+ Subquery Scan on rpr_ev_opt_with_rpr
-> WindowAgg
Window: w AS (ORDER BY s.v ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a b+
@@ -4059,7 +4059,7 @@ EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev88;
-- Planner optimization: non-RPR and RPR windows that share the same base frame
-- after frame optimization are kept as separate WindowAgg nodes.
--
-CREATE VIEW rpr_ev89 AS
+CREATE VIEW rpr_ev_opt_mixed AS
SELECT
row_number() OVER w_normal AS rn_normal,
row_number() OVER w_rpr AS rn_rpr
@@ -4072,10 +4072,10 @@ WINDOW
PATTERN (A+)
DEFINE A AS v > 1
);
-EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev89;
+EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev_opt_mixed;
QUERY PLAN
------------------------------------------------------------------------------------------
- Subquery Scan on rpr_ev89
+ Subquery Scan on rpr_ev_opt_mixed
-> WindowAgg
Window: w_rpr AS (ORDER BY s.v ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a+"
diff --git a/src/test/regress/sql/rpr_explain.sql b/src/test/regress/sql/rpr_explain.sql
index 237f0366631..65a775fdad9 100644
--- a/src/test/regress/sql/rpr_explain.sql
+++ b/src/test/regress/sql/rpr_explain.sql
@@ -118,7 +118,7 @@ VALUES
-- ============================================================
-- Simple pattern - should show basic statistics
-CREATE VIEW rpr_ev01 AS
+CREATE VIEW rpr_ev_basic_simple AS
SELECT count(*) OVER w
FROM rpr_nfa_test
WINDOW w AS (
@@ -127,7 +127,7 @@ WINDOW w AS (
PATTERN (A B)
DEFINE A AS cat = 'A', B AS cat = 'B'
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev01'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_basic_simple'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -140,7 +140,7 @@ WINDOW w AS (
)');
-- Pattern with no matches - 0 matched
-CREATE VIEW rpr_ev02 AS
+CREATE VIEW rpr_ev_basic_nomatch AS
SELECT count(*) OVER w
FROM rpr_nfa_test
WINDOW w AS (
@@ -149,7 +149,7 @@ WINDOW w AS (
PATTERN (X Y Z)
DEFINE X AS cat = 'X', Y AS cat = 'Y', Z AS cat = 'Z'
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev02'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_basic_nomatch'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -162,7 +162,7 @@ WINDOW w AS (
);');
-- Pattern matching every row - high match count
-CREATE VIEW rpr_ev03 AS
+CREATE VIEW rpr_ev_basic_allrows AS
SELECT count(*) OVER w
FROM rpr_nfa_test
WINDOW w AS (
@@ -171,7 +171,7 @@ WINDOW w AS (
PATTERN (R)
DEFINE R AS TRUE
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev03'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_basic_allrows'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -185,7 +185,7 @@ WINDOW w AS (
-- Regression test: Space before parenthesis in pattern deparse
-- Verifies that "A (B | C)" correctly outputs as "a (b | c)" with space
-CREATE VIEW rpr_ev04 AS
+CREATE VIEW rpr_ev_basic_deparse_space AS
SELECT count(*) OVER w
FROM generate_series(1, 20) AS s(v)
WINDOW w AS (
@@ -193,7 +193,7 @@ WINDOW w AS (
PATTERN (A (B | C))
DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev04'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_basic_deparse_space'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -207,7 +207,7 @@ WINDOW w AS (
-- Regression test: Sequential alternations at same depth
-- Verifies that "((B | C) (D | E))" correctly outputs as "(b | c) (d | e)"
-- Previously failed due to missing parentheses on ALT depth decrease
-CREATE VIEW rpr_ev05 AS
+CREATE VIEW rpr_ev_basic_deparse_seqalt AS
SELECT count(*) OVER w
FROM generate_series(1, 30) AS s(v)
WINDOW w AS (
@@ -215,7 +215,7 @@ WINDOW w AS (
PATTERN (A ((B | C) (D | E))*)
DEFINE A AS v % 5 = 1, B AS v % 5 = 2, C AS v % 5 = 3, D AS v % 5 = 4, E AS v % 5 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev05'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_basic_deparse_seqalt'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -231,7 +231,7 @@ WINDOW w AS (
-- ============================================================
-- Simple quantifier pattern - A+ with short matches (no merging)
-CREATE VIEW rpr_ev06 AS
+CREATE VIEW rpr_ev_state_simple_quant AS
SELECT count(*) OVER w
FROM generate_series(1, 50) AS s(v)
WINDOW w AS (
@@ -240,7 +240,7 @@ WINDOW w AS (
PATTERN (A+)
DEFINE A AS v % 2 = 1
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev06'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_state_simple_quant'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -253,7 +253,7 @@ WINDOW w AS (
);');
-- Alternation pattern - multiple state branches
-CREATE VIEW rpr_ev07 AS
+CREATE VIEW rpr_ev_state_alt AS
SELECT count(*) OVER w
FROM rpr_nfa_test
WINDOW w AS (
@@ -264,7 +264,7 @@ WINDOW w AS (
A AS cat = 'A', B AS cat = 'B', C AS cat = 'C',
D AS cat = 'D', E AS cat = 'E'
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev07'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_state_alt'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -279,7 +279,7 @@ WINDOW w AS (
);');
-- Complex pattern with high state count
-CREATE VIEW rpr_ev08 AS
+CREATE VIEW rpr_ev_state_complex AS
SELECT count(*) OVER w
FROM generate_series(1, 100) AS s(v)
WINDOW w AS (
@@ -291,7 +291,7 @@ WINDOW w AS (
B AS v % 3 = 2,
C AS v % 3 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev08'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_state_complex'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -307,7 +307,7 @@ WINDOW w AS (
);');
-- Grouped pattern with quantifier - state count with grouping
-CREATE VIEW rpr_ev09 AS
+CREATE VIEW rpr_ev_state_group_quant AS
SELECT count(*) OVER w
FROM generate_series(1, 60) AS s(v)
WINDOW w AS (
@@ -316,7 +316,7 @@ WINDOW w AS (
PATTERN ((A B)+)
DEFINE A AS v % 2 = 1, B AS v % 2 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev09'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_state_group_quant'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -330,7 +330,7 @@ WINDOW w AS (
-- State explosion pattern - many alternations
-- Pattern (A|B)(A|B)(A|B)(A|B) can create many parallel states
-CREATE VIEW rpr_ev10 AS
+CREATE VIEW rpr_ev_state_explosion AS
SELECT count(*) OVER w
FROM generate_series(1, 100) AS s(v)
WINDOW w AS (
@@ -339,7 +339,7 @@ WINDOW w AS (
PATTERN ((A | B) (A | B) (A | B) (A | B) (A | B) (A | B) (A | B) (A | B))
DEFINE A AS v % 2 = 1, B AS v % 2 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev10'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_state_explosion'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -353,7 +353,7 @@ WINDOW w AS (
-- Consecutive ALT merge followed by different ALT
-- Tests mergeConsecutiveAlts flush on ALT change: (A|B){2} (C|D)
-CREATE VIEW rpr_ev11 AS
+CREATE VIEW rpr_ev_state_alt_merge_alt AS
SELECT count(*) OVER w
FROM generate_series(1, 40) AS s(v)
WINDOW w AS (
@@ -362,7 +362,7 @@ WINDOW w AS (
PATTERN ((A | B) (A | B) (C | D))
DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev11'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_state_alt_merge_alt'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -376,7 +376,7 @@ WINDOW w AS (
-- Consecutive ALT merge followed by non-ALT element
-- Tests mergeConsecutiveAlts flush on non-ALT: (A|B){2} c
-CREATE VIEW rpr_ev12 AS
+CREATE VIEW rpr_ev_state_alt_merge_nonalt AS
SELECT count(*) OVER w
FROM generate_series(1, 40) AS s(v)
WINDOW w AS (
@@ -385,7 +385,7 @@ WINDOW w AS (
PATTERN ((A | B) (A | B) C)
DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev12'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_state_alt_merge_nonalt'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -398,7 +398,7 @@ WINDOW w AS (
);');
-- ALT prefix/suffix absorbed into GROUP: (A|B) (A|B)+ (A|B) -> (A|B){3,}
-CREATE VIEW rpr_ev13 AS
+CREATE VIEW rpr_ev_state_alt_absorb_group AS
SELECT count(*) OVER w
FROM generate_series(1, 40) AS s(v)
WINDOW w AS (
@@ -407,7 +407,7 @@ WINDOW w AS (
PATTERN ((A | B) (A | B)+ (A | B))
DEFINE A AS v % 2 = 0, B AS v % 2 = 1
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev13'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_state_alt_absorb_group'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -420,7 +420,7 @@ WINDOW w AS (
);');
-- High state count - alternation with plus quantifier
-CREATE VIEW rpr_ev14 AS
+CREATE VIEW rpr_ev_state_alt_plus AS
SELECT count(*) OVER w
FROM generate_series(1, 100) AS s(v)
WINDOW w AS (
@@ -429,7 +429,7 @@ WINDOW w AS (
PATTERN ((A | B | C)+ D)
DEFINE A AS v % 4 = 1, B AS v % 4 = 2, C AS v % 4 = 3, D AS v % 4 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev14'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_state_alt_plus'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -443,7 +443,7 @@ WINDOW w AS (
-- Early termination: first ALT branch (A) reaches FIN immediately,
-- pruning second branch (A B+) before it can accumulate B repetitions.
-CREATE VIEW rpr_ev15 AS
+CREATE VIEW rpr_ev_state_alt_prune AS
SELECT count(*) OVER w
FROM generate_series(1, 100) AS s(v)
WINDOW w AS (
@@ -452,7 +452,7 @@ WINDOW w AS (
PATTERN ((A | A B)+)
DEFINE A AS v = 1, B AS v > 1
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev15'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_state_alt_prune'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -465,7 +465,7 @@ WINDOW w AS (
);');
-- Nested quantifiers causing state growth
-CREATE VIEW rpr_ev16 AS
+CREATE VIEW rpr_ev_state_nested_quant AS
SELECT count(*) OVER w
FROM generate_series(1, 1000) AS s(v)
WINDOW w AS (
@@ -474,7 +474,7 @@ WINDOW w AS (
PATTERN (((A | B)+)+)
DEFINE A AS v % 3 = 1, B AS v % 3 = 2
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev16'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_state_nested_quant'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -491,7 +491,7 @@ WINDOW w AS (
-- ============================================================
-- Context absorption with unbounded quantifier at start
-CREATE VIEW rpr_ev17 AS
+CREATE VIEW rpr_ev_ctx_absorb_unbounded AS
SELECT count(*) OVER w
FROM generate_series(1, 50) AS s(v)
WINDOW w AS (
@@ -500,7 +500,7 @@ WINDOW w AS (
PATTERN (A+ B)
DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev17'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_absorb_unbounded'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -513,7 +513,7 @@ WINDOW w AS (
);');
-- No absorption - bounded quantifier
-CREATE VIEW rpr_ev18 AS
+CREATE VIEW rpr_ev_ctx_no_absorb AS
SELECT count(*) OVER w
FROM generate_series(1, 50) AS s(v)
WINDOW w AS (
@@ -522,7 +522,7 @@ WINDOW w AS (
PATTERN (A{2,4} B)
DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev18'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_no_absorb'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -535,7 +535,7 @@ WINDOW w AS (
);');
-- Contexts skipped by SKIP PAST LAST ROW
-CREATE VIEW rpr_ev19 AS
+CREATE VIEW rpr_ev_ctx_skip AS
SELECT count(*) OVER w
FROM generate_series(1, 100) AS s(v)
WINDOW w AS (
@@ -544,7 +544,7 @@ WINDOW w AS (
PATTERN (A B C)
DEFINE A AS v % 10 = 1, B AS v % 10 = 2, C AS v % 10 = 3
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev19'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_skip'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -557,7 +557,7 @@ WINDOW w AS (
);');
-- High context absorption - unbounded group
-CREATE VIEW rpr_ev20 AS
+CREATE VIEW rpr_ev_ctx_absorb_group AS
SELECT count(*) OVER w
FROM generate_series(1, 100) AS s(v)
WINDOW w AS (
@@ -566,7 +566,7 @@ WINDOW w AS (
PATTERN ((A B)+ C)
DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev20'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_absorb_group'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -581,7 +581,7 @@ WINDOW w AS (
-- Fixed-length group absorption: (A B B)+ C
-- B B merged to B{2}; absorbable with fixed-length check
-- step_size=3 (A + B + B); v % 7 cycle gives 2 iterations per match
-CREATE VIEW rpr_ev20b AS
+CREATE VIEW rpr_ev_ctx_absorb_fixedvar AS
SELECT count(*) OVER w
FROM generate_series(1, 70) AS s(v)
WINDOW w AS (
@@ -590,7 +590,7 @@ WINDOW w AS (
PATTERN ((A B B)+ C)
DEFINE A AS v % 7 IN (1, 4), B AS v % 7 IN (2, 3, 5, 6), C AS v % 7 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev20b'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_absorb_fixedvar'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -604,7 +604,7 @@ WINDOW w AS (
-- Nested fixed-length group absorption: (A (B C){2} D)+ E
-- step_size = 1 + (1+1)*2 + 1 = 6; v % 13 cycle gives 2 iterations + E
-CREATE VIEW rpr_ev20c AS
+CREATE VIEW rpr_ev_ctx_absorb_nested AS
SELECT count(*) OVER w
FROM generate_series(1, 65) AS s(v)
WINDOW w AS (
@@ -615,7 +615,7 @@ WINDOW w AS (
C AS v % 13 IN (3, 5, 9, 11), D AS v % 13 IN (6, 12),
E AS v % 13 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev20c'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_absorb_nested'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -631,7 +631,7 @@ WINDOW w AS (
-- Doubly nested fixed-length group absorption: (A ((B C{3}){2} D){2} E)+ F
-- step_size = 1 + ((1+3)*2+1)*2 + 1 = 20; v % 41 cycle gives 2 iterations + F
-CREATE VIEW rpr_ev20d AS
+CREATE VIEW rpr_ev_ctx_absorb_deep AS
SELECT count(*) OVER w
FROM generate_series(1, 82) AS s(v)
WINDOW w AS (
@@ -646,7 +646,7 @@ WINDOW w AS (
E AS v % 41 IN (20, 40),
F AS v % 41 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev20d'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_absorb_deep'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -667,7 +667,7 @@ WINDOW w AS (
-- 3-level END chain absorption: ((A (B C){2}){2})+
-- step_size = (1 + (1+1)*2) * 2 = 10; v % 21 cycle gives 2 iterations
-- END chain: END(BC{2}) -> END(A..{2}) -> END(+, ABSORBABLE)
-CREATE VIEW rpr_ev20e AS
+CREATE VIEW rpr_ev_ctx_absorb_endchain AS
SELECT count(*) OVER w
FROM generate_series(1, 42) AS s(v)
WINDOW w AS (
@@ -678,7 +678,7 @@ WINDOW w AS (
B AS v % 21 IN (2, 4, 7, 9, 12, 14, 17, 19),
C AS v % 21 IN (3, 5, 8, 10, 13, 15, 18, 20)
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev20e'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_absorb_endchain'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -697,7 +697,7 @@ WINDOW w AS (
-- ============================================================
-- Fixed length matches - all same length
-CREATE VIEW rpr_ev21 AS
+CREATE VIEW rpr_ev_mlen_fixed AS
SELECT count(*) OVER w
FROM rpr_nfa_test
WINDOW w AS (
@@ -708,7 +708,7 @@ WINDOW w AS (
A AS cat = 'A', B AS cat = 'B', C AS cat = 'C',
D AS cat = 'D', E AS cat = 'E'
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev21'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_mlen_fixed'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -723,7 +723,7 @@ WINDOW w AS (
);');
-- Variable length matches - min/max/avg differ
-CREATE VIEW rpr_ev22 AS
+CREATE VIEW rpr_ev_mlen_variable AS
SELECT count(*) OVER w
FROM generate_series(1, 100) AS s(v)
WINDOW w AS (
@@ -732,7 +732,7 @@ WINDOW w AS (
PATTERN (A+ B)
DEFINE A AS v % 10 <> 0, B AS v % 10 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev22'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_mlen_variable'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -745,7 +745,7 @@ WINDOW w AS (
);');
-- Very long matches
-CREATE VIEW rpr_ev23 AS
+CREATE VIEW rpr_ev_mlen_long AS
SELECT count(*) OVER w
FROM generate_series(1, 200) AS s(v)
WINDOW w AS (
@@ -754,7 +754,7 @@ WINDOW w AS (
PATTERN (A+ B)
DEFINE A AS v <= 195, B AS v > 195
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev23'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_mlen_long'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -767,7 +767,7 @@ WINDOW w AS (
);');
-- Uniform match length with mismatches from gap rows (v%20 = 11..15)
-CREATE VIEW rpr_ev24 AS
+CREATE VIEW rpr_ev_mlen_with_mismatch AS
SELECT count(*) OVER w
FROM generate_series(1, 100) AS s(v)
WINDOW w AS (
@@ -778,7 +778,7 @@ WINDOW w AS (
A AS (v % 20 <> 0) AND (v % 20 <= 10 OR v % 20 > 15),
B AS v % 20 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev24'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_mlen_with_mismatch'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -798,7 +798,7 @@ WINDOW w AS (
-- Pattern with complete match every cycle: 0 mismatched
-- A(1,2,3) B(4,5) C(6) repeats perfectly; X rows are pruned, not mismatched
-CREATE VIEW rpr_ev25 AS
+CREATE VIEW rpr_ev_mlen_no_mismatch AS
SELECT count(*) OVER w
FROM (
SELECT v,
@@ -814,7 +814,7 @@ WINDOW w AS (
PATTERN (A+ B+ C)
DEFINE A AS cat = 'A', B AS cat = 'B', C AS cat = 'C'
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev25'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_mlen_no_mismatch'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -834,7 +834,7 @@ WINDOW w AS (
);');
-- Long partial matches that fail
-CREATE VIEW rpr_ev26 AS
+CREATE VIEW rpr_ev_mlen_long_partial AS
SELECT count(*) OVER w
FROM (
SELECT i AS v,
@@ -855,7 +855,7 @@ WINDOW w AS (
PATTERN (A+ B+ C)
DEFINE A AS cat = 'A', B AS cat = 'B', C AS cat = 'C'
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev26'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_mlen_long_partial'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -884,7 +884,7 @@ WINDOW w AS (
-- ============================================================
-- JSON format output with all statistics
-CREATE VIEW rpr_ev27 AS
+CREATE VIEW rpr_ev_json_basic AS
SELECT count(*) OVER w
FROM generate_series(1, 50) AS s(v)
WINDOW w AS (
@@ -893,7 +893,7 @@ WINDOW w AS (
PATTERN (A+ B+)
DEFINE A AS v % 3 = 1, B AS v % 3 = 2
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev27'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_json_basic'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF, FORMAT JSON)
SELECT count(*) OVER w
@@ -906,7 +906,7 @@ WINDOW w AS (
)');
-- JSON format with match length statistics
-CREATE VIEW rpr_ev28 AS
+CREATE VIEW rpr_ev_json_matchlen AS
SELECT count(*) OVER w
FROM generate_series(1, 100) AS s(v)
WINDOW w AS (
@@ -915,7 +915,7 @@ WINDOW w AS (
PATTERN (A+ B)
DEFINE A AS v % 10 <> 0, B AS v % 10 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev28'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_json_matchlen'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF, FORMAT JSON)
SELECT count(*) OVER w
@@ -929,7 +929,7 @@ WINDOW w AS (
-- JSON format with mismatch statistics
-- Pattern A B C expects 1,2,3 but gets 1,2,4 twice causing mismatches
-CREATE VIEW rpr_ev29 AS
+CREATE VIEW rpr_ev_json_mismatch AS
SELECT count(*) OVER w
FROM (VALUES (1),(2),(4), (1),(2),(4), (1),(2),(3)) AS t(v)
WINDOW w AS (
@@ -938,7 +938,7 @@ WINDOW w AS (
PATTERN (A B C)
DEFINE A AS v = 1, B AS v = 2, C AS v = 3
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev29'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_json_mismatch'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF, FORMAT JSON)
SELECT count(*) OVER w
@@ -952,7 +952,7 @@ WINDOW w AS (
-- JSON format with skipped context statistics
-- Alternation pattern with SKIP PAST LAST ROW causes many contexts to be skipped
-CREATE VIEW rpr_ev30 AS
+CREATE VIEW rpr_ev_json_skip AS
SELECT count(*) OVER w
FROM generate_series(1, 100) AS s(v)
WINDOW w AS (
@@ -961,7 +961,7 @@ WINDOW w AS (
PATTERN ((A | B) (A | B) (A | B) (A | B) (A | B) (A | B) (A | B) (A | B))
DEFINE A AS v % 2 = 1, B AS v % 2 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev30'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_json_skip'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF, FORMAT JSON)
SELECT count(*) OVER w
@@ -978,7 +978,7 @@ WINDOW w AS (
-- ============================================================
-- XML format output
-CREATE VIEW rpr_ev31 AS
+CREATE VIEW rpr_ev_xml_basic AS
SELECT count(*) OVER w
FROM generate_series(1, 30) AS s(v)
WINDOW w AS (
@@ -987,7 +987,7 @@ WINDOW w AS (
PATTERN (A B)
DEFINE A AS v % 2 = 1, B AS v % 2 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev31'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_xml_basic'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF, FORMAT XML)
SELECT count(*) OVER w
@@ -1004,7 +1004,7 @@ WINDOW w AS (
-- ============================================================
-- Statistics across multiple partitions
-CREATE VIEW rpr_ev32 AS
+CREATE VIEW rpr_ev_part_multi AS
SELECT count(*) OVER w
FROM (
SELECT p, v
@@ -1018,7 +1018,7 @@ WINDOW w AS (
PATTERN (A+ B)
DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev32'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_part_multi'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -1036,7 +1036,7 @@ WINDOW w AS (
);');
-- Different pattern behavior per partition
-CREATE VIEW rpr_ev33 AS
+CREATE VIEW rpr_ev_part_diff AS
SELECT count(*) OVER w
FROM (
SELECT
@@ -1051,7 +1051,7 @@ WINDOW w AS (
PATTERN (A+ B)
DEFINE A AS val < 5, B AS val >= 5
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev33'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_part_diff'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -1074,7 +1074,7 @@ WINDOW w AS (
-- ============================================================
-- Empty result set
-CREATE VIEW rpr_ev34 AS
+CREATE VIEW rpr_ev_edge_empty AS
SELECT count(*) OVER w
FROM generate_series(1, 0) AS s(v)
WINDOW w AS (
@@ -1083,7 +1083,7 @@ WINDOW w AS (
PATTERN (A B)
DEFINE A AS v = 1, B AS v = 2
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev34'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_edge_empty'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -1096,7 +1096,7 @@ WINDOW w AS (
);');
-- Single row
-CREATE VIEW rpr_ev35 AS
+CREATE VIEW rpr_ev_edge_single_row AS
SELECT count(*) OVER w
FROM generate_series(1, 1) AS s(v)
WINDOW w AS (
@@ -1105,7 +1105,7 @@ WINDOW w AS (
PATTERN (A)
DEFINE A AS TRUE
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev35'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_edge_single_row'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -1118,7 +1118,7 @@ WINDOW w AS (
);');
-- Pattern longer than data
-CREATE VIEW rpr_ev36 AS
+CREATE VIEW rpr_ev_edge_pattern_longer AS
SELECT count(*) OVER w
FROM generate_series(1, 5) AS s(v)
WINDOW w AS (
@@ -1129,7 +1129,7 @@ WINDOW w AS (
A AS v = 1, B AS v = 2, C AS v = 3, D AS v = 4, E AS v = 5,
F AS v = 6, G AS v = 7, H AS v = 8, I AS v = 9, J AS v = 10
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev36'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_edge_pattern_longer'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -1144,7 +1144,7 @@ WINDOW w AS (
);');
-- All rows match as single match
-CREATE VIEW rpr_ev37 AS
+CREATE VIEW rpr_ev_edge_single_match AS
SELECT count(*) OVER w
FROM generate_series(1, 50) AS s(v)
WINDOW w AS (
@@ -1153,7 +1153,7 @@ WINDOW w AS (
PATTERN (A+)
DEFINE A AS TRUE
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev37'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_edge_single_match'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -1170,7 +1170,7 @@ WINDOW w AS (
-- ============================================================
-- Nested groups
-CREATE VIEW rpr_ev38 AS
+CREATE VIEW rpr_ev_cpx_nested AS
SELECT count(*) OVER w
FROM generate_series(1, 60) AS s(v)
WINDOW w AS (
@@ -1179,7 +1179,7 @@ WINDOW w AS (
PATTERN (((A B) C)+)
DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev38'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_cpx_nested'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -1192,7 +1192,7 @@ WINDOW w AS (
);');
-- Multiple alternations
-CREATE VIEW rpr_ev39 AS
+CREATE VIEW rpr_ev_cpx_multi_alt AS
SELECT count(*) OVER w
FROM rpr_nfa_test
WINDOW w AS (
@@ -1203,7 +1203,7 @@ WINDOW w AS (
A AS cat = 'A', B AS cat = 'B', C AS cat = 'C',
D AS cat = 'D', E AS cat = 'E'
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev39'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_cpx_multi_alt'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -1218,7 +1218,7 @@ WINDOW w AS (
);');
-- Optional elements
-CREATE VIEW rpr_ev40 AS
+CREATE VIEW rpr_ev_cpx_optional AS
SELECT count(*) OVER w
FROM generate_series(1, 50) AS s(v)
WINDOW w AS (
@@ -1227,7 +1227,7 @@ WINDOW w AS (
PATTERN (A B? C)
DEFINE A AS v % 4 = 1, B AS v % 4 = 2, C AS v % 4 = 3
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev40'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_cpx_optional'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -1240,7 +1240,7 @@ WINDOW w AS (
);');
-- Bounded quantifiers
-CREATE VIEW rpr_ev41 AS
+CREATE VIEW rpr_ev_cpx_bounded AS
SELECT count(*) OVER w
FROM generate_series(1, 100) AS s(v)
WINDOW w AS (
@@ -1249,7 +1249,7 @@ WINDOW w AS (
PATTERN (A{2,5} B)
DEFINE A AS v % 10 <> 0, B AS v % 10 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev41'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_cpx_bounded'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -1262,7 +1262,7 @@ WINDOW w AS (
);');
-- Star quantifier
-CREATE VIEW rpr_ev42 AS
+CREATE VIEW rpr_ev_cpx_star AS
SELECT count(*) OVER w
FROM generate_series(1, 50) AS s(v)
WINDOW w AS (
@@ -1271,7 +1271,7 @@ WINDOW w AS (
PATTERN (A B* C)
DEFINE A AS v % 10 = 1, B AS v % 10 IN (2,3,4,5,6,7,8), C AS v % 10 = 9
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev42'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_cpx_star'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -1288,7 +1288,7 @@ WINDOW w AS (
-- ============================================================
-- Stock price pattern - V-shape (down then up)
-CREATE VIEW rpr_ev43 AS
+CREATE VIEW rpr_ev_real_vshape AS
SELECT count(*) OVER w
FROM rpr_nfa_complex
WINDOW w AS (
@@ -1297,7 +1297,7 @@ WINDOW w AS (
PATTERN (D+ U+)
DEFINE D AS trend = 'D', U AS trend = 'U'
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev43'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_real_vshape'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -1310,7 +1310,7 @@ WINDOW w AS (
);');
-- Stock price pattern - peak (up, stable, down)
-CREATE VIEW rpr_ev44 AS
+CREATE VIEW rpr_ev_real_peak AS
SELECT count(*) OVER w
FROM rpr_nfa_complex
WINDOW w AS (
@@ -1319,7 +1319,7 @@ WINDOW w AS (
PATTERN (U+ S* D+)
DEFINE U AS trend = 'U', S AS trend = 'S', D AS trend = 'D'
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev44'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_real_peak'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -1332,7 +1332,7 @@ WINDOW w AS (
);');
-- Consecutive increasing values (using PREV)
-CREATE VIEW rpr_ev45 AS
+CREATE VIEW rpr_ev_real_increasing AS
SELECT count(*) OVER w
FROM generate_series(1, 50) AS s(v)
WINDOW w AS (
@@ -1341,7 +1341,7 @@ WINDOW w AS (
PATTERN (A{3,})
DEFINE A AS v > PREV(v) OR PREV(v) IS NULL
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev45'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_real_increasing'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -1358,7 +1358,7 @@ WINDOW w AS (
-- ============================================================
-- Large dataset with simple pattern
-CREATE VIEW rpr_ev46 AS
+CREATE VIEW rpr_ev_perf_large_simple AS
SELECT count(*) OVER w
FROM generate_series(1, 1000) AS s(v)
WINDOW w AS (
@@ -1367,7 +1367,7 @@ WINDOW w AS (
PATTERN (A B)
DEFINE A AS v % 2 = 1, B AS v % 2 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev46'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_perf_large_simple'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -1380,7 +1380,7 @@ WINDOW w AS (
);');
-- Large dataset with absorption
-CREATE VIEW rpr_ev47 AS
+CREATE VIEW rpr_ev_perf_large_absorb AS
SELECT count(*) OVER w
FROM generate_series(1, 1000) AS s(v)
WINDOW w AS (
@@ -1389,7 +1389,7 @@ WINDOW w AS (
PATTERN (A+ B)
DEFINE A AS v % 100 <> 0, B AS v % 100 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev47'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_perf_large_absorb'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -1402,7 +1402,7 @@ WINDOW w AS (
);');
-- High state merge ratio
-CREATE VIEW rpr_ev48 AS
+CREATE VIEW rpr_ev_perf_high_merge AS
SELECT count(*) OVER w
FROM generate_series(1, 500) AS s(v)
WINDOW w AS (
@@ -1411,7 +1411,7 @@ WINDOW w AS (
PATTERN ((A | B)+ C)
DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev48'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_perf_high_merge'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -1428,7 +1428,7 @@ WINDOW w AS (
-- ============================================================
-- With INITIAL keyword
-CREATE VIEW rpr_ev49 AS
+CREATE VIEW rpr_ev_initial_with AS
SELECT count(*) OVER w
FROM generate_series(1, 50) AS s(v)
WINDOW w AS (
@@ -1438,7 +1438,7 @@ WINDOW w AS (
PATTERN (A+ B)
DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev49'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_initial_with'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -1452,7 +1452,7 @@ WINDOW w AS (
);');
-- Without INITIAL keyword (same behavior currently)
-CREATE VIEW rpr_ev50 AS
+CREATE VIEW rpr_ev_initial_without AS
SELECT count(*) OVER w
FROM generate_series(1, 50) AS s(v)
WINDOW w AS (
@@ -1461,7 +1461,7 @@ WINDOW w AS (
PATTERN (A+ B)
DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev50'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_initial_without'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -1478,7 +1478,7 @@ WINDOW w AS (
-- ============================================================
-- Plus quantifier
-CREATE VIEW rpr_ev51 AS
+CREATE VIEW rpr_ev_quant_plus AS
SELECT count(*) OVER w
FROM generate_series(1, 40) AS s(v)
WINDOW w AS (
@@ -1487,7 +1487,7 @@ WINDOW w AS (
PATTERN (A+)
DEFINE A AS v % 4 <> 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev51'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_quant_plus'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -1500,7 +1500,7 @@ WINDOW w AS (
);');
-- Star quantifier (zero or more)
-CREATE VIEW rpr_ev52 AS
+CREATE VIEW rpr_ev_quant_star AS
SELECT count(*) OVER w
FROM generate_series(1, 40) AS s(v)
WINDOW w AS (
@@ -1509,7 +1509,7 @@ WINDOW w AS (
PATTERN (A* B)
DEFINE A AS v % 4 IN (1, 2), B AS v % 4 = 3
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev52'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_quant_star'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -1522,7 +1522,7 @@ WINDOW w AS (
);');
-- Question mark (zero or one)
-CREATE VIEW rpr_ev53 AS
+CREATE VIEW rpr_ev_quant_question AS
SELECT count(*) OVER w
FROM generate_series(1, 40) AS s(v)
WINDOW w AS (
@@ -1531,7 +1531,7 @@ WINDOW w AS (
PATTERN (A? B C)
DEFINE A AS v % 4 = 1, B AS v % 4 = 2, C AS v % 4 = 3
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev53'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_quant_question'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -1544,7 +1544,7 @@ WINDOW w AS (
);');
-- Exact count {n}
-CREATE VIEW rpr_ev54 AS
+CREATE VIEW rpr_ev_quant_exact AS
SELECT count(*) OVER w
FROM generate_series(1, 50) AS s(v)
WINDOW w AS (
@@ -1553,7 +1553,7 @@ WINDOW w AS (
PATTERN (A{3} B)
DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev54'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_quant_exact'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -1566,7 +1566,7 @@ WINDOW w AS (
);');
-- Range {n,m}
-CREATE VIEW rpr_ev55 AS
+CREATE VIEW rpr_ev_quant_range AS
SELECT count(*) OVER w
FROM generate_series(1, 50) AS s(v)
WINDOW w AS (
@@ -1575,7 +1575,7 @@ WINDOW w AS (
PATTERN (A{2,4} B)
DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev55'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_quant_range'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -1588,7 +1588,7 @@ WINDOW w AS (
);');
-- At least {n,}
-CREATE VIEW rpr_ev56 AS
+CREATE VIEW rpr_ev_quant_atleast AS
SELECT count(*) OVER w
FROM generate_series(1, 50) AS s(v)
WINDOW w AS (
@@ -1597,7 +1597,7 @@ WINDOW w AS (
PATTERN (A{3,} B)
DEFINE A AS v % 10 <> 0, B AS v % 10 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev56'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_quant_atleast'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -1615,7 +1615,7 @@ WINDOW w AS (
-- Verify state count accuracy
-- Pattern A+ B with 20 rows should show predictable state behavior
-CREATE VIEW rpr_ev57 AS
+CREATE VIEW rpr_ev_reg_state_count AS
SELECT count(*) OVER w
FROM generate_series(1, 20) AS s(v)
WINDOW w AS (
@@ -1624,7 +1624,7 @@ WINDOW w AS (
PATTERN (A+ B)
DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev57'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_reg_state_count'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -1637,7 +1637,7 @@ WINDOW w AS (
);');
-- Verify context count with known absorption
-CREATE VIEW rpr_ev58 AS
+CREATE VIEW rpr_ev_reg_ctx_absorb AS
SELECT count(*) OVER w
FROM generate_series(1, 30) AS s(v)
WINDOW w AS (
@@ -1646,7 +1646,7 @@ WINDOW w AS (
PATTERN (A+ B C)
DEFINE A AS v % 10 IN (1,2,3,4,5,6,7), B AS v % 10 = 8, C AS v % 10 = 9
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev58'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_reg_ctx_absorb'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -1659,7 +1659,7 @@ WINDOW w AS (
);');
-- Verify match length with fixed-length pattern
-CREATE VIEW rpr_ev59 AS
+CREATE VIEW rpr_ev_reg_matchlen AS
SELECT count(*) OVER w
FROM generate_series(1, 30) AS s(v)
WINDOW w AS (
@@ -1668,7 +1668,7 @@ WINDOW w AS (
PATTERN (A B C)
DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev59'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_reg_matchlen'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -1685,7 +1685,7 @@ WINDOW w AS (
-- ============================================================
-- Simple alternation
-CREATE VIEW rpr_ev60 AS
+CREATE VIEW rpr_ev_alt_simple AS
SELECT count(*) OVER w
FROM rpr_nfa_test
WINDOW w AS (
@@ -1694,7 +1694,7 @@ WINDOW w AS (
PATTERN ((A | B) C)
DEFINE A AS cat = 'A', B AS cat = 'B', C AS cat = 'C'
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev60'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_simple'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -1707,7 +1707,7 @@ WINDOW w AS (
);');
-- Multiple items in alternation
-CREATE VIEW rpr_ev61 AS
+CREATE VIEW rpr_ev_alt_multi_item AS
SELECT count(*) OVER w
FROM rpr_nfa_test
WINDOW w AS (
@@ -1718,7 +1718,7 @@ WINDOW w AS (
A AS cat = 'A', B AS cat = 'B', C AS cat = 'C',
D AS cat = 'D', E AS cat = 'E'
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev61'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_multi_item'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -1733,7 +1733,7 @@ WINDOW w AS (
);');
-- Alternation with quantifiers
-CREATE VIEW rpr_ev62 AS
+CREATE VIEW rpr_ev_alt_with_quant AS
SELECT count(*) OVER w
FROM generate_series(1, 50) AS s(v)
WINDOW w AS (
@@ -1742,7 +1742,7 @@ WINDOW w AS (
PATTERN ((A | B)+ C)
DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev62'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_with_quant'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -1755,7 +1755,7 @@ WINDOW w AS (
);');
-- Multiple alternatives (4+)
-CREATE VIEW rpr_ev63 AS
+CREATE VIEW rpr_ev_alt_four_plus AS
SELECT count(*) OVER w
FROM generate_series(1, 100) AS s(v)
WINDOW w AS (
@@ -1763,7 +1763,7 @@ WINDOW w AS (
PATTERN (A | B | C | D | E)
DEFINE A AS v % 5 = 0, B AS v % 5 = 1, C AS v % 5 = 2, D AS v % 5 = 3, E AS v % 5 = 4
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev63'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_four_plus'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -1775,7 +1775,7 @@ WINDOW w AS (
);');
-- Alternation at start
-CREATE VIEW rpr_ev64 AS
+CREATE VIEW rpr_ev_alt_at_start AS
SELECT count(*) OVER w
FROM generate_series(1, 60) AS s(v)
WINDOW w AS (
@@ -1783,7 +1783,7 @@ WINDOW w AS (
PATTERN ((A | B) C D)
DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev64'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_at_start'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -1795,7 +1795,7 @@ WINDOW w AS (
);');
-- Multiple sequential alternations
-CREATE VIEW rpr_ev65 AS
+CREATE VIEW rpr_ev_alt_sequential AS
SELECT count(*) OVER w
FROM generate_series(1, 100) AS s(v)
WINDOW w AS (
@@ -1803,7 +1803,7 @@ WINDOW w AS (
PATTERN ((A | B) C (D | E) F)
DEFINE A AS v % 6 = 0, B AS v % 6 = 1, C AS v % 6 = 2, D AS v % 6 = 3, E AS v % 6 = 4, F AS v % 6 = 5
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev65'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_sequential'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -1815,7 +1815,7 @@ WINDOW w AS (
);');
-- Quantified alternatives
-CREATE VIEW rpr_ev66 AS
+CREATE VIEW rpr_ev_alt_quantified AS
SELECT count(*) OVER w
FROM generate_series(1, 60) AS s(v)
WINDOW w AS (
@@ -1823,7 +1823,7 @@ WINDOW w AS (
PATTERN ((A+ | B+) C)
DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev66'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_quantified'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -1835,7 +1835,7 @@ WINDOW w AS (
);');
-- Alternation at end
-CREATE VIEW rpr_ev67 AS
+CREATE VIEW rpr_ev_alt_at_end AS
SELECT count(*) OVER w
FROM generate_series(1, 60) AS s(v)
WINDOW w AS (
@@ -1843,7 +1843,7 @@ WINDOW w AS (
PATTERN (A B (C | D))
DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev67'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_at_end'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -1856,7 +1856,7 @@ WINDOW w AS (
-- Nested ALT at start of branch inside outer ALT
-- Pattern: (A ((B | C) D | E)) - preceding VAR + inner ALT as first branch element
-CREATE VIEW rpr_ev68 AS
+CREATE VIEW rpr_ev_alt_nested_start AS
SELECT count(*) OVER w
FROM generate_series(1, 20) AS s(v)
WINDOW w AS (
@@ -1864,7 +1864,7 @@ WINDOW w AS (
PATTERN (A ((B | C) D | E))
DEFINE A AS v % 5 = 0, B AS v % 5 = 1, C AS v % 5 = 2, D AS v % 5 = 3, E AS v % 5 = 4
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev68'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_nested_start'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -1877,7 +1877,7 @@ WINDOW w AS (
-- Nested ALT at end of branch inside outer ALT
-- Pattern: (C (A | B) | D) - inner ALT is last element in outer branch
-CREATE VIEW rpr_ev69 AS
+CREATE VIEW rpr_ev_alt_nested_end AS
SELECT count(*) OVER w
FROM generate_series(1, 20) AS s(v)
WINDOW w AS (
@@ -1885,7 +1885,7 @@ WINDOW w AS (
PATTERN (C (A | B) | D)
DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev69'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_nested_end'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -1901,7 +1901,7 @@ WINDOW w AS (
-- ============================================================
-- Simple group
-CREATE VIEW rpr_ev70 AS
+CREATE VIEW rpr_ev_grp_simple AS
SELECT count(*) OVER w
FROM generate_series(1, 40) AS s(v)
WINDOW w AS (
@@ -1910,7 +1910,7 @@ WINDOW w AS (
PATTERN ((A B)+)
DEFINE A AS v % 2 = 1, B AS v % 2 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev70'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_grp_simple'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -1923,7 +1923,7 @@ WINDOW w AS (
);');
-- Group with bounded quantifier
-CREATE VIEW rpr_ev71 AS
+CREATE VIEW rpr_ev_grp_bounded AS
SELECT count(*) OVER w
FROM generate_series(1, 40) AS s(v)
WINDOW w AS (
@@ -1932,7 +1932,7 @@ WINDOW w AS (
PATTERN ((A B){2,4})
DEFINE A AS v % 2 = 1, B AS v % 2 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev71'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_grp_bounded'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -1945,7 +1945,7 @@ WINDOW w AS (
);');
-- Nested groups
-CREATE VIEW rpr_ev72 AS
+CREATE VIEW rpr_ev_grp_nested AS
SELECT count(*) OVER w
FROM generate_series(1, 60) AS s(v)
WINDOW w AS (
@@ -1954,7 +1954,7 @@ WINDOW w AS (
PATTERN (((A B){2})+)
DEFINE A AS v % 2 = 1, B AS v % 2 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev72'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_grp_nested'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -1967,7 +1967,7 @@ WINDOW w AS (
);');
-- Deep nesting (3+ levels)
-CREATE VIEW rpr_ev73 AS
+CREATE VIEW rpr_ev_grp_deep AS
SELECT count(*) OVER w
FROM generate_series(1, 40) AS s(v)
WINDOW w AS (
@@ -1975,7 +1975,7 @@ WINDOW w AS (
PATTERN ((((A | B)+)+)+)
DEFINE A AS v % 2 = 0, B AS v % 2 = 1
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev73'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_grp_deep'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -1987,7 +1987,7 @@ WINDOW w AS (
);');
-- Bounded quantifier on alternation
-CREATE VIEW rpr_ev74 AS
+CREATE VIEW rpr_ev_grp_bounded_alt AS
SELECT count(*) OVER w
FROM generate_series(1, 60) AS s(v)
WINDOW w AS (
@@ -1995,7 +1995,7 @@ WINDOW w AS (
PATTERN ((A | B){2,3} C)
DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev74'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_grp_bounded_alt'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -2007,7 +2007,7 @@ WINDOW w AS (
);');
-- Nested groups with quantifiers
-CREATE VIEW rpr_ev75 AS
+CREATE VIEW rpr_ev_grp_nested_quant AS
SELECT count(*) OVER w
FROM generate_series(1, 60) AS s(v)
WINDOW w AS (
@@ -2015,7 +2015,7 @@ WINDOW w AS (
PATTERN (((A B)+ C)*)
DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev75'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_grp_nested_quant'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -2027,7 +2027,7 @@ WINDOW w AS (
);');
-- Partial nested quantification
-CREATE VIEW rpr_ev76 AS
+CREATE VIEW rpr_ev_grp_partial_quant AS
SELECT count(*) OVER w
FROM generate_series(1, 60) AS s(v)
WINDOW w AS (
@@ -2035,7 +2035,7 @@ WINDOW w AS (
PATTERN ((A (B C)+)*)
DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev76'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_grp_partial_quant'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -2051,7 +2051,7 @@ WINDOW w AS (
-- ============================================================
-- count(*) with pattern
-CREATE VIEW rpr_ev77 AS
+CREATE VIEW rpr_ev_wfn_count AS
SELECT count(*) OVER w
FROM generate_series(1, 30) AS s(v)
WINDOW w AS (
@@ -2060,7 +2060,7 @@ WINDOW w AS (
PATTERN (A+ B)
DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev77'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_wfn_count'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -2073,7 +2073,7 @@ WINDOW w AS (
);');
-- first_value with pattern
-CREATE VIEW rpr_ev78 AS
+CREATE VIEW rpr_ev_wfn_first_value AS
SELECT first_value(v) OVER w
FROM generate_series(1, 30) AS s(v)
WINDOW w AS (
@@ -2082,7 +2082,7 @@ WINDOW w AS (
PATTERN (A+ B)
DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev78'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_wfn_first_value'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT first_value(v) OVER w
@@ -2095,7 +2095,7 @@ WINDOW w AS (
);');
-- last_value with pattern
-CREATE VIEW rpr_ev79 AS
+CREATE VIEW rpr_ev_wfn_last_value AS
SELECT last_value(v) OVER w
FROM generate_series(1, 30) AS s(v)
WINDOW w AS (
@@ -2104,7 +2104,7 @@ WINDOW w AS (
PATTERN (A+ B)
DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev79'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_wfn_last_value'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT last_value(v) OVER w
@@ -2117,7 +2117,7 @@ WINDOW w AS (
);');
-- Multiple window functions
-CREATE VIEW rpr_ev80 AS
+CREATE VIEW rpr_ev_wfn_multi AS
SELECT
count(*) OVER w,
first_value(v) OVER w,
@@ -2129,7 +2129,7 @@ WINDOW w AS (
PATTERN (A+ B)
DEFINE A AS v % 5 <> 0, B AS v % 5 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev80'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_wfn_multi'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT
@@ -2149,7 +2149,7 @@ WINDOW w AS (
-- ============================================================
-- Complex boolean expressions
-CREATE VIEW rpr_ev81 AS
+CREATE VIEW rpr_ev_def_complex_bool AS
SELECT count(*) OVER w
FROM generate_series(1, 50) AS s(v)
WINDOW w AS (
@@ -2160,7 +2160,7 @@ WINDOW w AS (
A AS (v % 5 <> 0) AND (v % 3 <> 0),
B AS (v % 5 = 0) OR (v % 3 = 0)
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev81'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_def_complex_bool'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -2175,7 +2175,7 @@ WINDOW w AS (
);');
-- Using PREV function
-CREATE VIEW rpr_ev82 AS
+CREATE VIEW rpr_ev_def_prev AS
SELECT count(*) OVER w
FROM generate_series(1, 30) AS s(v)
WINDOW w AS (
@@ -2187,7 +2187,7 @@ WINDOW w AS (
U AS v > PREV(v),
D AS v < PREV(v)
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev82'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_def_prev'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -2203,7 +2203,7 @@ WINDOW w AS (
);');
-- Using NULL comparisons
-CREATE VIEW rpr_ev83 AS
+CREATE VIEW rpr_ev_def_null AS
SELECT count(*) OVER w
FROM (
SELECT CASE WHEN v % 5 = 0 THEN NULL ELSE v END AS v
@@ -2215,7 +2215,7 @@ WINDOW w AS (
PATTERN (A+ B)
DEFINE A AS v IS NOT NULL, B AS v IS NULL
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev83'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_def_null'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -2235,7 +2235,7 @@ WINDOW w AS (
-- ============================================================
-- 500 rows - verify statistics scale correctly
-CREATE VIEW rpr_ev84 AS
+CREATE VIEW rpr_ev_scale_500rows AS
SELECT count(*) OVER w
FROM generate_series(1, 500) AS s(v)
WINDOW w AS (
@@ -2244,7 +2244,7 @@ WINDOW w AS (
PATTERN (A+ B C)
DEFINE A AS v % 10 < 7, B AS v % 10 = 7, C AS v % 10 = 8
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev84'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_scale_500rows'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -2257,7 +2257,7 @@ WINDOW w AS (
);');
-- High match count scenario
-CREATE VIEW rpr_ev85 AS
+CREATE VIEW rpr_ev_scale_high_match AS
SELECT count(*) OVER w
FROM generate_series(1, 500) AS s(v)
WINDOW w AS (
@@ -2266,7 +2266,7 @@ WINDOW w AS (
PATTERN (A B)
DEFINE A AS v % 2 = 1, B AS v % 2 = 0
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev85'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_scale_high_match'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -2279,7 +2279,7 @@ WINDOW w AS (
);');
-- High skip count scenario
-CREATE VIEW rpr_ev86 AS
+CREATE VIEW rpr_ev_scale_high_skip AS
SELECT count(*) OVER w
FROM generate_series(1, 500) AS s(v)
WINDOW w AS (
@@ -2293,7 +2293,7 @@ WINDOW w AS (
D AS v % 100 = 4,
E AS v % 100 = 5
);
-SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev86'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_scale_high_skip'), E'\n')) AS line WHERE line ~ 'PATTERN';
SELECT rpr_explain_filter('
EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
SELECT count(*) OVER w
@@ -2321,7 +2321,7 @@ WINDOW w AS (
--
-- Without RPR: row_number() frame is optimized to ROWS UNBOUNDED PRECEDING
-CREATE VIEW rpr_ev87 AS
+CREATE VIEW rpr_ev_opt_no_rpr AS
SELECT row_number() OVER w
FROM generate_series(1, 10) AS s(v)
WINDOW w AS (
@@ -2329,10 +2329,10 @@ WINDOW w AS (
ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
);
-EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev87;
+EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev_opt_no_rpr;
-- With RPR: frame must remain ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-CREATE VIEW rpr_ev88 AS
+CREATE VIEW rpr_ev_opt_with_rpr AS
SELECT row_number() OVER w
FROM generate_series(1, 10) AS s(v)
WINDOW w AS (
@@ -2344,13 +2344,13 @@ WINDOW w AS (
B AS v > PREV(v)
);
-EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev88;
+EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev_opt_with_rpr;
--
-- Planner optimization: non-RPR and RPR windows that share the same base frame
-- after frame optimization are kept as separate WindowAgg nodes.
--
-CREATE VIEW rpr_ev89 AS
+CREATE VIEW rpr_ev_opt_mixed AS
SELECT
row_number() OVER w_normal AS rn_normal,
row_number() OVER w_rpr AS rn_rpr
@@ -2364,7 +2364,7 @@ WINDOW
DEFINE A AS v > 1
);
-EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev89;
+EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev_opt_mixed;
--
-- Planner optimization: find_window_run_conditions must not push down
--
2.50.1 (Apple Git-155)
From 115425a057767ebf2b6e4c877641ed156b0ed07e Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Tue, 7 Apr 2026 08:53:46 +0900
Subject: [PATCH] Fix quote_identifier() for RPR pattern variable name deparse
Add quote_identifier() to PATTERN and DEFINE variable name output
in ruleutils.c and explain.c. Without quoting, mixed-case or
reserved-word variable names (e.g., "Start", "Up") lose their
case or conflict with keywords in pg_get_viewdef() output,
breaking pg_dump/pg_restore round-trips.
Add regression test with quoted identifiers ("Start", "Up") to
verify correct deparse in both pg_get_viewdef and EXPLAIN output.
---
src/backend/commands/explain.c | 2 +-
src/backend/utils/adt/ruleutils.c | 4 ++--
src/test/regress/expected/rpr_base.out | 24 +++++++++++++++++++++++
src/test/regress/expected/rpr_explain.out | 19 ++++++++++++++++++
src/test/regress/sql/rpr_base.sql | 10 ++++++++++
src/test/regress/sql/rpr_explain.sql | 12 ++++++++++++
6 files changed, 68 insertions(+), 3 deletions(-)
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 7f0367ce546..933eadab71e 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -3176,7 +3176,7 @@ deparse_rpr_var(RPRPattern *pattern, int *idx, StringInfoData *buf,
appendStringInfoChar(buf, ' ');
Assert(elem->varId < pattern->numVars);
- appendStringInfoString(buf, pattern->varNames[elem->varId]);
+ appendStringInfoString(buf, quote_identifier(pattern->varNames[elem->varId]));
append_rpr_quantifier(buf, elem);
*needSpace = true;
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index cfe24de43cf..c755a42efd6 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -7160,7 +7160,7 @@ get_rule_pattern_node(RPRPatternNode *node, deparse_context *context)
switch (node->nodeType)
{
case RPR_PATTERN_VAR:
- appendStringInfoString(buf, node->varName);
+ appendStringInfoString(buf, quote_identifier(node->varName));
append_pattern_quantifier(buf, node);
break;
@@ -7229,7 +7229,7 @@ get_rule_define(List *defineClause, bool force_colno, deparse_context *context)
{
TargetEntry *te = (TargetEntry *) lfirst(lc_def);
- appendStringInfo(buf, "%s%s AS ", sep, te->resname);
+ appendStringInfo(buf, "%s%s AS ", sep, quote_identifier(te->resname));
get_rule_expr((Node *) te->expr, context, false);
sep = ",\n ";
}
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index 7452cf1b3ab..6526365dd6a 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -2252,6 +2252,30 @@ SELECT pg_get_viewdef('rpr_quant_reluctant_v'::regclass);
b AS (val > 0) );
(1 row)
+-- Quoted identifier round-trip: mixed case and reserved words need quoting
+CREATE VIEW rpr_serial_quoted AS
+SELECT id, val, count(*) OVER w
+FROM rpr_serial
+WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ("Start" "Up"+)
+ DEFINE "Start" AS TRUE, "Up" AS val > PREV(val));
+SELECT pg_get_viewdef('rpr_serial_quoted'::regclass);
+ pg_get_viewdef
+------------------------------------------------------------------------------
+ SELECT id, +
+ val, +
+ count(*) OVER w AS count +
+ FROM rpr_serial +
+ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
+ AFTER MATCH SKIP PAST LAST ROW +
+ INITIAL +
+ PATTERN ("Start" "Up"+) +
+ DEFINE +
+ "Start" AS true, +
+ "Up" AS (val > prev(val)) );
+(1 row)
+
-- Materialized view (if supported)
CREATE TABLE rpr_mview (id INT, val INT);
INSERT INTO rpr_mview VALUES (1, 10), (2, 20), (3, 30);
diff --git a/src/test/regress/expected/rpr_explain.out b/src/test/regress/expected/rpr_explain.out
index f66caf8908e..a68ec61e10f 100644
--- a/src/test/regress/expected/rpr_explain.out
+++ b/src/test/regress/expected/rpr_explain.out
@@ -301,6 +301,25 @@ WINDOW w AS (
-> Function Scan on generate_series s (actual rows=30.00 loops=1)
(8 rows)
+-- Regression test: Quoted identifiers in EXPLAIN pattern deparse
+-- Mixed case names must be quoted to preserve round-trip safety
+SELECT rpr_explain_filter('
+EXPLAIN (COSTS OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 10) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ("Start" "Up"+)
+ DEFINE "Start" AS TRUE, "Up" AS v > PREV(v)
+);');
+ rpr_explain_filter
+-------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: "Start" "Up"+
+ -> Function Scan on generate_series s
+(4 rows)
+
-- ============================================================
-- State Statistics Tests (peak, total, merged)
-- ============================================================
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index 8c23c7598a3..3accecb73ba 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -1559,6 +1559,16 @@ WINDOW w AS (ORDER BY id
DEFINE A AS val > 0, B AS val > 0);
SELECT pg_get_viewdef('rpr_quant_reluctant_v'::regclass);
+-- Quoted identifier round-trip: mixed case and reserved words need quoting
+CREATE VIEW rpr_serial_quoted AS
+SELECT id, val, count(*) OVER w
+FROM rpr_serial
+WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ("Start" "Up"+)
+ DEFINE "Start" AS TRUE, "Up" AS val > PREV(val));
+SELECT pg_get_viewdef('rpr_serial_quoted'::regclass);
+
-- Materialized view (if supported)
CREATE TABLE rpr_mview (id INT, val INT);
diff --git a/src/test/regress/sql/rpr_explain.sql b/src/test/regress/sql/rpr_explain.sql
index 65a775fdad9..703ecd3b23b 100644
--- a/src/test/regress/sql/rpr_explain.sql
+++ b/src/test/regress/sql/rpr_explain.sql
@@ -226,6 +226,18 @@ WINDOW w AS (
DEFINE A AS v % 5 = 1, B AS v % 5 = 2, C AS v % 5 = 3, D AS v % 5 = 4, E AS v % 5 = 0
);');
+-- Regression test: Quoted identifiers in EXPLAIN pattern deparse
+-- Mixed case names must be quoted to preserve round-trip safety
+SELECT rpr_explain_filter('
+EXPLAIN (COSTS OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 10) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ("Start" "Up"+)
+ DEFINE "Start" AS TRUE, "Up" AS v > PREV(v)
+);');
+
-- ============================================================
-- State Statistics Tests (peak, total, merged)
-- ============================================================
--
2.50.1 (Apple Git-155)
From b46e0efc2dac822d7b68734345cd9e051e8aa235 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Tue, 7 Apr 2026 09:14:59 +0900
Subject: [PATCH] Fix execRPR.o ordering in executor Makefile to match
meson.build
---
src/backend/executor/Makefile | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/backend/executor/Makefile b/src/backend/executor/Makefile
index eeed9a904e5..2b257427795 100644
--- a/src/backend/executor/Makefile
+++ b/src/backend/executor/Makefile
@@ -25,8 +25,8 @@ OBJS = \
execParallel.o \
execPartition.o \
execProcnode.o \
- execReplication.o \
execRPR.o \
+ execReplication.o \
execSRF.o \
execScan.o \
execTuples.o \
--
2.50.1 (Apple Git-155)
From 4d64cb3907e95d3ffe61d206040b65207b31950d Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Tue, 7 Apr 2026 09:40:53 +0900
Subject: [PATCH] Remove unused force_colno parameter from RPR deparse
functions
---
src/backend/utils/adt/ruleutils.c | 15 ++++++---------
1 file changed, 6 insertions(+), 9 deletions(-)
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index c755a42efd6..e93c03a351c 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -449,10 +449,8 @@ static void get_rule_orderby(List *orderList, List *targetList,
bool force_colno, deparse_context *context);
static void append_pattern_quantifier(StringInfo buf, RPRPatternNode *node);
static void get_rule_pattern_node(RPRPatternNode *node, deparse_context *context);
-static void get_rule_pattern(RPRPatternNode *rpPattern, bool force_colno,
- deparse_context *context);
-static void get_rule_define(List *defineClause, bool force_colno,
- deparse_context *context);
+static void get_rule_pattern(RPRPatternNode *rpPattern, deparse_context *context);
+static void get_rule_define(List *defineClause, deparse_context *context);
static void get_rule_windowclause(Query *query, deparse_context *context);
static void get_rule_windowspec(WindowClause *wc, List *targetList,
deparse_context *context);
@@ -7203,8 +7201,7 @@ get_rule_pattern_node(RPRPatternNode *node, deparse_context *context)
* Display a PATTERN clause.
*/
static void
-get_rule_pattern(RPRPatternNode *rpPattern, bool force_colno,
- deparse_context *context)
+get_rule_pattern(RPRPatternNode *rpPattern, deparse_context *context)
{
StringInfo buf = context->buf;
@@ -7217,7 +7214,7 @@ get_rule_pattern(RPRPatternNode *rpPattern, bool force_colno,
* Display a DEFINE clause.
*/
static void
-get_rule_define(List *defineClause, bool force_colno, deparse_context *context)
+get_rule_define(List *defineClause, deparse_context *context)
{
StringInfo buf = context->buf;
const char *sep;
@@ -7356,7 +7353,7 @@ get_rule_windowspec(WindowClause *wc, List *targetList,
if (needspace)
appendStringInfoChar(buf, ' ');
appendStringInfoString(buf, "\n PATTERN ");
- get_rule_pattern(wc->rpPattern, false, context);
+ get_rule_pattern(wc->rpPattern, context);
needspace = true;
}
@@ -7365,7 +7362,7 @@ get_rule_windowspec(WindowClause *wc, List *targetList,
if (needspace)
appendStringInfoChar(buf, ' ');
appendStringInfoString(buf, "\n DEFINE\n");
- get_rule_define(wc->defineClause, false, context);
+ get_rule_define(wc->defineClause, context);
appendStringInfoChar(buf, ' ');
}
--
2.50.1 (Apple Git-155)
From 4f779ed3658d7b4d97819b5cca39d6a1d555d67b Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Tue, 7 Apr 2026 09:43:07 +0900
Subject: [PATCH] Add CHECK_FOR_INTERRUPTS to RPR context cleanup and finalize
loops
---
src/backend/executor/execRPR.c | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 8f0457e2b3c..97efa9a4924 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -3068,6 +3068,8 @@ ExecRPRCleanupDeadContexts(WindowAggState *winstate, RPRNFAContext *excludeCtx)
for (ctx = winstate->nfaContext; ctx != NULL; ctx = next)
{
+ CHECK_FOR_INTERRUPTS();
+
next = ctx->next;
/* Skip the target context and contexts still processing */
@@ -3108,6 +3110,8 @@ ExecRPRFinalizeAllContexts(WindowAggState *winstate, int64 lastPos)
for (ctx = winstate->nfaContext; ctx != NULL; ctx = ctx->next)
{
+ CHECK_FOR_INTERRUPTS();
+
if (ctx->states != NULL)
{
nfa_match(winstate, ctx, NULL);
--
2.50.1 (Apple Git-155)
From 366068f89fd0385c3231b2f1bd7d7e75f9641718 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Tue, 7 Apr 2026 09:48:55 +0900
Subject: [PATCH] Narrow variable scope in ExecInitWindowAgg DEFINE clause loop
---
src/backend/executor/nodeWindowAgg.c | 11 +++--------
1 file changed, 3 insertions(+), 8 deletions(-)
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index dca2de570e8..0202c508323 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -2628,9 +2628,6 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
TupleDesc scanDesc;
ListCell *l;
- TargetEntry *te;
- Expr *expr;
-
/* check for unsupported flags */
Assert(!(eflags & (EXEC_FLAG_BACKWARD | EXEC_FLAG_MARK)));
@@ -2951,13 +2948,11 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
*/
foreach(l, node->defineClause)
{
- char *name;
+ TargetEntry *te = lfirst(l);
+ char *name = te->resname;
+ Expr *expr = te->expr;
ExprState *exps;
- te = lfirst(l);
- name = te->resname;
- expr = te->expr;
-
winstate->defineVariableList =
lappend(winstate->defineVariableList,
makeString(pstrdup(name)));
--
2.50.1 (Apple Git-155)
From d4454b9a6b576a79adca7f2ff96abd0b28db3a86 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Tue, 7 Apr 2026 11:32:56 +0900
Subject: [PATCH] Normalize RPR element flag macros to return bool
---
src/include/optimizer/rpr.h | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/include/optimizer/rpr.h b/src/include/optimizer/rpr.h
index e78092678bb..360e1bb777f 100644
--- a/src/include/optimizer/rpr.h
+++ b/src/include/optimizer/rpr.h
@@ -44,10 +44,10 @@
#define RPR_ELEM_ABSORBABLE 0x08 /* absorption judgment point */
/* Accessor macros for RPRPatternElement */
-#define RPRElemIsReluctant(e) ((e)->flags & RPR_ELEM_RELUCTANT)
-#define RPRElemCanEmptyLoop(e) ((e)->flags & RPR_ELEM_EMPTY_LOOP)
-#define RPRElemIsAbsorbableBranch(e) ((e)->flags & RPR_ELEM_ABSORBABLE_BRANCH)
-#define RPRElemIsAbsorbable(e) ((e)->flags & RPR_ELEM_ABSORBABLE)
+#define RPRElemIsReluctant(e) (((e)->flags & RPR_ELEM_RELUCTANT) != 0)
+#define RPRElemCanEmptyLoop(e) (((e)->flags & RPR_ELEM_EMPTY_LOOP) != 0)
+#define RPRElemIsAbsorbableBranch(e) (((e)->flags & RPR_ELEM_ABSORBABLE_BRANCH) != 0)
+#define RPRElemIsAbsorbable(e) (((e)->flags & RPR_ELEM_ABSORBABLE) != 0)
#define RPRElemIsVar(e) ((e)->varId <= RPR_VARID_MAX)
#define RPRElemIsBegin(e) ((e)->varId == RPR_VARID_BEGIN)
#define RPRElemIsEnd(e) ((e)->varId == RPR_VARID_END)
--
2.50.1 (Apple Git-155)
From 87d9b0b288b9f07663dcb5191db0c34a3edb5473 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Fri, 20 Mar 2026 23:52:42 +0900
Subject: [PATCH] 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/sql/rpr.sql | 322 ++++++++++++-
src/test/regress/sql/rpr_explain.sql | 56 +++
src/tools/pgindent/typedefs.list | 3 +-
22 files changed, 1607 insertions(+), 165 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/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)
From 49555669b00a11ec6b106026f075b03825f6bc3f Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Thu, 2 Apr 2026 10:54:30 +0900
Subject: [PATCH] Add JIT compilation support for RPR PREV/NEXT navigation
---
src/backend/jit/llvm/llvmjit_expr.c | 72 +++++++++++++++++++++--------
src/test/regress/expected/rpr.out | 31 +++++++++++++
src/test/regress/sql/rpr.sql | 27 +++++++++++
3 files changed, 111 insertions(+), 19 deletions(-)
diff --git a/src/backend/jit/llvm/llvmjit_expr.c b/src/backend/jit/llvm/llvmjit_expr.c
index d158e37e7b5..4901b2a7ff4 100644
--- a/src/backend/jit/llvm/llvmjit_expr.c
+++ b/src/backend/jit/llvm/llvmjit_expr.c
@@ -127,6 +127,9 @@ llvm_compile_expr(ExprState *state)
LLVMValueRef v_aggvalues;
LLVMValueRef v_aggnulls;
+ /* RPR navigation: when true, EEOP_OUTER_VAR reloads from econtext */
+ bool has_rpr_nav;
+
instr_time starttime;
instr_time deform_starttime;
instr_time endtime;
@@ -300,19 +303,16 @@ llvm_compile_expr(ExprState *state)
* 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.
+ * After a slot swap, these cached pointers become stale because the new
+ * slot has its own tts_values/tts_isnull arrays.
*
- * 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.
+ * When RPR navigation opcodes are present, EEOP_OUTER_VAR reloads the
+ * slot pointer from econtext->ecxt_outertuple on every access instead of
+ * using the cached entry-block values. This avoids the SSA/PHI
+ * complexity while keeping the rest of the expression JIT-compiled.
+ * Expressions without RPR navigation use the cached values as before.
*/
+ has_rpr_nav = false;
if (parent && IsA(parent, WindowAggState) &&
((WindowAgg *) parent->plan)->rpPattern != NULL)
{
@@ -323,9 +323,8 @@ llvm_compile_expr(ExprState *state)
if (opcode == EEOP_RPR_NAV_SET ||
opcode == EEOP_RPR_NAV_RESTORE)
{
- LLVMDeleteFunction(eval_fn);
- LLVMDisposeBuilder(b);
- return false;
+ has_rpr_nav = true;
+ break;
}
}
}
@@ -492,8 +491,37 @@ llvm_compile_expr(ExprState *state)
}
else if (opcode == EEOP_OUTER_VAR)
{
- v_values = v_outervalues;
- v_nulls = v_outernulls;
+ if (has_rpr_nav)
+ {
+ /*
+ * RPR navigation swaps ecxt_outertuple
+ * mid-expression. Reload slot pointer from
+ * econtext on every access so we read from the
+ * current (possibly swapped) slot.
+ */
+ LLVMValueRef v_tmpslot;
+
+ v_tmpslot = l_load_struct_gep(b,
+ StructExprContext,
+ v_econtext,
+ FIELDNO_EXPRCONTEXT_OUTERTUPLE,
+ "v_outerslot_reload");
+ v_values = l_load_struct_gep(b,
+ StructTupleTableSlot,
+ v_tmpslot,
+ FIELDNO_TUPLETABLESLOT_VALUES,
+ "v_outervalues_reload");
+ v_nulls = l_load_struct_gep(b,
+ StructTupleTableSlot,
+ v_tmpslot,
+ FIELDNO_TUPLETABLESLOT_ISNULL,
+ "v_outernulls_reload");
+ }
+ else
+ {
+ v_values = v_outervalues;
+ v_nulls = v_outernulls;
+ }
}
else if (opcode == EEOP_SCAN_VAR)
{
@@ -2467,10 +2495,16 @@ llvm_compile_expr(ExprState *state)
break;
case EEOP_RPR_NAV_SET:
+ build_EvalXFunc(b, mod, "ExecEvalRPRNavSet",
+ v_state, op, v_econtext);
+ LLVMBuildBr(b, opblocks[opno + 1]);
+ break;
+
case EEOP_RPR_NAV_RESTORE:
- /* unreachable: filtered out by the pre-scan above */
- Assert(false);
- return false;
+ build_EvalXFunc(b, mod, "ExecEvalRPRNavRestore",
+ v_state, op, v_econtext);
+ LLVMBuildBr(b, opblocks[opno + 1]);
+ break;
case EEOP_AGG_STRICT_DESERIALIZE:
case EEOP_AGG_DESERIALIZE:
diff --git a/src/test/regress/expected/rpr.out b/src/test/regress/expected/rpr.out
index d586e17e0a1..de6ce4fba8a 100644
--- a/src/test/regress/expected/rpr.out
+++ b/src/test/regress/expected/rpr.out
@@ -2153,6 +2153,37 @@ SELECT match_first, match_last, match_len FROM result WHERE match_len > 0;
0 | 99998 | 99999
(1 row)
+-- JIT PREV/NEXT navigation test: 100K rows with PREV in DEFINE.
+-- Exercises EEOP_RPR_NAV_SET/RESTORE JIT code paths (has_rpr_nav reload)
+-- at scale. V-shape: price rises then falls, repeated across partition.
+SET jit_above_cost = 0;
+WITH data AS (
+ SELECT i, abs(50000 - i) AS price
+ FROM generate_series(1, 100000) i
+),
+result AS (
+ SELECT i, price,
+ count(*) OVER w AS match_len,
+ first_value(price) OVER w AS match_first
+ FROM data
+ WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ INITIAL
+ PATTERN (DOWN+ UP+)
+ DEFINE
+ DOWN AS price < PREV(price),
+ UP AS price > PREV(price)
+ )
+)
+SELECT count(*) AS matched_rows, max(match_len) AS longest_match
+FROM result WHERE match_len > 0;
+ matched_rows | longest_match
+--------------+---------------
+ 1 | 99999
+(1 row)
+
+RESET jit_above_cost;
--
-- IGNORE NULLS
--
diff --git a/src/test/regress/sql/rpr.sql b/src/test/regress/sql/rpr.sql
index 504476a2b02..b3bbc8254c4 100644
--- a/src/test/regress/sql/rpr.sql
+++ b/src/test/regress/sql/rpr.sql
@@ -1084,6 +1084,33 @@ result AS (
-- Should match: A (33333 rows) + B (33333 rows) + C (33333 rows) = 99999 rows
SELECT match_first, match_last, match_len FROM result WHERE match_len > 0;
+-- JIT PREV/NEXT navigation test: 100K rows with PREV in DEFINE.
+-- Exercises EEOP_RPR_NAV_SET/RESTORE JIT code paths (has_rpr_nav reload)
+-- at scale. V-shape: price rises then falls, repeated across partition.
+SET jit_above_cost = 0;
+WITH data AS (
+ SELECT i, abs(50000 - i) AS price
+ FROM generate_series(1, 100000) i
+),
+result AS (
+ SELECT i, price,
+ count(*) OVER w AS match_len,
+ first_value(price) OVER w AS match_first
+ FROM data
+ WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ INITIAL
+ PATTERN (DOWN+ UP+)
+ DEFINE
+ DOWN AS price < PREV(price),
+ UP AS price > PREV(price)
+ )
+)
+SELECT count(*) AS matched_rows, max(match_len) AS longest_match
+FROM result WHERE match_len > 0;
+RESET jit_above_cost;
+
--
-- IGNORE NULLS
--
--
2.50.1 (Apple Git-155)
From 0b71c6b947e6b279bff33d34c2e263833f9f5408 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Sat, 4 Apr 2026 11:49:32 +0900
Subject: [PATCH] Add tuplestore trim optimization for RPR PREV navigation
Advance the tuplestore mark pointer based on the maximum PREV offset
found in DEFINE clause expressions, allowing tuplestore_trim() to
free rows that PREV can no longer reach.
The planner walks DEFINE expressions to find the maximum PREV offset.
If all offsets are constants, navMaxOffset is set directly. If any
offset is non-constant (parameter or expression), the planner sets
RPR_NAV_OFFSET_NEEDS_EVAL and the executor evaluates all PREV offsets
at init time. The executor then advances the mark to
(currentpos - navMaxOffset) each row.
NEXT offsets are ignored since they look forward and do not affect
trim. RPR_NAV_OFFSET_RETAIN_ALL is reserved for future navigation
functions (FIRST/LAST) that require the entire partition.
---
src/backend/executor/nodeWindowAgg.c | 136 ++++++++++++++++++++++--
src/backend/optimizer/plan/createplan.c | 101 ++++++++++++++++++
src/include/nodes/execnodes.h | 1 +
src/include/nodes/plannodes.h | 9 ++
src/include/optimizer/rpr.h | 9 ++
5 files changed, 246 insertions(+), 10 deletions(-)
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 4e643df94cf..9787ef7756f 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -244,6 +244,10 @@ static void update_reduced_frame(WindowObject winobj, int64 pos);
/* Forward declarations - NFA row evaluation */
static bool nfa_evaluate_row(WindowObject winobj, int64 pos, bool *varMatched);
+/* Forward declarations - navigation offset evaluation */
+static bool collect_prev_offset_walker(Node *node, List **offsets);
+static void eval_nav_max_offset(WindowAggState *winstate, List *defineClause);
+
/*
* Not null info bit array consists of 2-bit items
*/
@@ -934,12 +938,18 @@ eval_windowaggregates(WindowAggState *winstate)
if (rpr_is_defined(winstate))
{
/*
- * If RPR is used, it is possible PREV wants to look at the
- * previous row. So the mark pos should be frameheadpos - 1
- * unless it is below 0.
+ * If RPR is used, PREV may need to look at rows before the frame
+ * head. Adjust mark by navMaxOffset if known, otherwise retain
+ * from position 0.
*/
- markpos -= 1;
- if (markpos < 0)
+ if (winstate->navMaxOffset >= 0)
+ {
+ if (markpos > winstate->navMaxOffset)
+ markpos -= winstate->navMaxOffset;
+ else
+ markpos = 0;
+ }
+ else
markpos = 0;
}
WinSetMarkPosition(agg_winobj, markpos);
@@ -1269,12 +1279,15 @@ prepare_tuplestore(WindowAggState *winstate)
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.
+ * Allocate mark and read pointers for PREV/NEXT navigation.
+ *
+ * If navMaxOffset >= 0, we advance the mark to (currentpos -
+ * navMaxOffset) as rows are processed, allowing tuplestore_trim() to
+ * free rows that are no longer reachable.
*
- * 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.
+ * If navMaxOffset < 0 (RPR_NAV_OFFSET_NEEDS_EVAL or
+ * RPR_NAV_OFFSET_RETAIN_ALL), the mark stays at 0, retaining the
+ * entire partition in the tuplestore.
*/
winstate->nav_winobj->markptr =
tuplestore_alloc_read_pointer(winstate->buffer, 0);
@@ -2512,6 +2525,24 @@ ExecWindowAgg(PlanState *pstate)
if (winstate->grouptail_ptr >= 0)
update_grouptailpos(winstate);
+ /*
+ * Advance RPR navigation mark pointer if possible, so that
+ * tuplestore_trim() can free rows no longer reachable by PREV.
+ */
+ if (winstate->nav_winobj &&
+ winstate->rpPattern != NULL &&
+ winstate->navMaxOffset >= 0)
+ {
+ int64 navmarkpos;
+
+ if (winstate->currentpos > winstate->navMaxOffset)
+ navmarkpos = winstate->currentpos - winstate->navMaxOffset;
+ else
+ navmarkpos = 0;
+ if (navmarkpos > winstate->nav_winobj->markpos)
+ WinSetMarkPosition(winstate->nav_winobj, navmarkpos);
+ }
+
/*
* Truncate any no-longer-needed rows from the tuplestore.
*/
@@ -2957,6 +2988,10 @@ 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 max PREV offset for tuplestore trim */
+ winstate->navMaxOffset = node->navMaxOffset;
+ if (winstate->navMaxOffset == RPR_NAV_OFFSET_NEEDS_EVAL)
+ eval_nav_max_offset(winstate, node->defineClause);
/* Calculate NFA state size and allocate cycle detection bitmap */
if (node->rpPattern != NULL)
@@ -3867,6 +3902,87 @@ put_notnull_info(WindowObject winobj, int64 pos, int argno, bool isnull)
mbp[bpos] = mb;
}
+/*
+ * collect_prev_offset_walker
+ * Walk expression tree to collect PREV offset_arg expressions.
+ */
+static bool
+collect_prev_offset_walker(Node *node, List **offsets)
+{
+ if (node == NULL)
+ return false;
+
+ if (IsA(node, RPRNavExpr))
+ {
+ RPRNavExpr *nav = (RPRNavExpr *) node;
+
+ if (nav->kind == RPR_NAV_PREV && nav->offset_arg != NULL)
+ *offsets = lappend(*offsets, nav->offset_arg);
+
+ /* Don't walk into RPRNavExpr children */
+ return false;
+ }
+
+ return expression_tree_walker(node, collect_prev_offset_walker, offsets);
+}
+
+/*
+ * eval_nav_max_offset
+ * Evaluate non-constant PREV offsets at executor init time.
+ *
+ * Called when the planner set navMaxOffset to RPR_NAV_OFFSET_NEEDS_EVAL
+ * because some PREV offset contains a parameter or non-foldable expression.
+ * Walks the original defineClause expression trees, compiles and evaluates
+ * each PREV offset_arg, and stores the maximum in winstate->navMaxOffset.
+ */
+static void
+eval_nav_max_offset(WindowAggState *winstate, List *defineClause)
+{
+ ExprContext *econtext = winstate->ss.ps.ps_ExprContext;
+ List *offsets = NIL;
+ ListCell *lc;
+ int64 maxOffset = 0;
+
+ /* Collect all PREV offset expressions from DEFINE clause */
+ foreach(lc, defineClause)
+ {
+ TargetEntry *te = (TargetEntry *) lfirst(lc);
+
+ collect_prev_offset_walker((Node *) te->expr, &offsets);
+ }
+
+ /* Evaluate each offset and find maximum */
+ foreach(lc, offsets)
+ {
+ Expr *offset_expr = (Expr *) lfirst(lc);
+ ExprState *estate;
+ Datum val;
+ bool isnull;
+ int64 offset;
+
+ estate = ExecInitExpr(offset_expr, (PlanState *) winstate);
+ val = ExecEvalExprSwitchContext(estate, econtext, &isnull);
+
+ /*
+ * NULL or negative offsets will cause a runtime error when PREV is
+ * actually evaluated. For trim purposes, treat them as 0.
+ */
+ if (isnull)
+ continue;
+
+ offset = DatumGetInt64(val);
+ if (offset < 0)
+ continue;
+
+ if (offset > maxOffset)
+ maxOffset = offset;
+ }
+
+ winstate->navMaxOffset = maxOffset;
+
+ list_free(offsets);
+}
+
/*
* rpr_is_defined
* return true if Row pattern recognition is defined.
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index 9ac24cc222d..ee2d53b5924 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -2461,6 +2461,104 @@ create_minmaxagg_plan(PlannerInfo *root, MinMaxAggPath *best_path)
return plan;
}
+/*
+ * nav_max_offset_walker
+ * Walk expression tree to find the maximum PREV offset.
+ *
+ * Only PREV is relevant for tuplestore trim since it looks backward;
+ * NEXT looks forward and never references already-trimmed rows.
+ *
+ * Returns true (to stop walking) if a non-constant PREV offset is found,
+ * in which case *maxOffset is set to -1. Otherwise accumulates the
+ * maximum constant offset value.
+ */
+static bool
+nav_max_offset_walker(Node *node, int64 *maxOffset)
+{
+ if (node == NULL)
+ return false;
+
+ if (IsA(node, RPRNavExpr))
+ {
+ RPRNavExpr *nav = (RPRNavExpr *) node;
+
+ /* Only PREV looks backward; NEXT is irrelevant for trim */
+ if (nav->kind == RPR_NAV_PREV)
+ {
+ int64 offset;
+
+ if (nav->offset_arg == NULL)
+ {
+ /* 1-arg form: implicit offset of 1 */
+ offset = 1;
+ }
+ else if (IsA(nav->offset_arg, Const))
+ {
+ Const *c = (Const *) nav->offset_arg;
+
+ if (c->constisnull)
+ {
+ /*
+ * NULL offset causes a runtime error, so this path is
+ * never actually reached during execution. Use 0 as a
+ * safe placeholder for planning purposes.
+ */
+ offset = 0;
+ }
+ else
+ {
+ offset = DatumGetInt64(c->constvalue);
+ if (offset < 0)
+ offset = 0; /* negative offset causes runtime error */
+ }
+ }
+ else
+ {
+ /*
+ * Non-constant offset (Param, stable function, etc.). The
+ * parser guarantees offset is a runtime constant, so it can
+ * be evaluated at executor init time.
+ */
+ *maxOffset = RPR_NAV_OFFSET_NEEDS_EVAL;
+ return true; /* stop walking */
+ }
+
+ if (offset > *maxOffset)
+ *maxOffset = offset;
+ }
+
+ /* Don't walk into RPRNavExpr children - offset_arg already handled */
+ return false;
+ }
+
+ return expression_tree_walker(node, nav_max_offset_walker, maxOffset);
+}
+
+/*
+ * compute_nav_max_offset
+ * Compute the maximum PREV offset from DEFINE clause expressions.
+ *
+ * Returns the maximum constant offset found, or -1 if any PREV offset
+ * cannot be determined statically. NEXT offsets are ignored since they
+ * look forward and don't affect tuplestore trim.
+ */
+static int64
+compute_nav_max_offset(List *defineClause)
+{
+ int64 maxOffset = 0;
+ ListCell *lc;
+
+ foreach(lc, defineClause)
+ {
+ TargetEntry *te = (TargetEntry *) lfirst(lc);
+
+ if (nav_max_offset_walker((Node *) te->expr, &maxOffset))
+ return RPR_NAV_OFFSET_NEEDS_EVAL;
+ }
+
+ return maxOffset;
+}
+
/*
* create_windowagg_plan
*
@@ -6678,6 +6776,9 @@ make_windowagg(List *tlist, WindowClause *wc,
node->defineClause = defineClause;
+ /* Compute max PREV offset for tuplestore trim optimization */
+ node->navMaxOffset = compute_nav_max_offset(defineClause);
+
plan->targetlist = tlist;
plan->lefttree = lefttree;
plan->righttree = NULL;
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 74a6b682132..ff6d7d70a60 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -2692,6 +2692,7 @@ typedef struct WindowAggState
TupleTableSlot *temp_slot_2;
/* RPR navigation */
+ int64 navMaxOffset; /* max PREV offset; see RPR_NAV_OFFSET_* */
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 */
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index ceaab4d97b0..27a2e7b48c7 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -1386,6 +1386,15 @@ typedef struct WindowAgg
/* Row Pattern DEFINE clause (list of TargetEntry) */
List *defineClause;
+ /*
+ * Maximum PREV offset for tuplestore mark optimization. >= 0: statically
+ * determined max offset (mark = currentpos - offset).
+ * RPR_NAV_OFFSET_NEEDS_EVAL: has non-constant offset; evaluate at
+ * executor init. RPR_NAV_OFFSET_RETAIN_ALL: must retain entire partition
+ * (no trim possible).
+ */
+ int64 navMaxOffset;
+
/*
* false for all apart from the WindowAgg that's closest to the root of
* the plan
diff --git a/src/include/optimizer/rpr.h b/src/include/optimizer/rpr.h
index 360e1bb777f..00a28abe2b4 100644
--- a/src/include/optimizer/rpr.h
+++ b/src/include/optimizer/rpr.h
@@ -55,6 +55,15 @@
#define RPRElemIsFin(e) ((e)->varId == RPR_VARID_FIN)
#define RPRElemCanSkip(e) ((e)->min == 0)
+/*
+ * navMaxOffset sentinel values.
+ * Non-negative values represent a statically determined maximum PREV offset.
+ */
+#define RPR_NAV_OFFSET_NEEDS_EVAL (-1) /* has non-constant PREV offset;
+ * evaluate at executor init */
+#define RPR_NAV_OFFSET_RETAIN_ALL (-2) /* must retain entire partition
+ * (e.g., future FIRST/LAST) */
+
extern List *collectPatternVariables(RPRPatternNode *pattern);
extern void buildDefineVariableList(List *defineClause,
List **defineVariableList);
--
2.50.1 (Apple Git-155)
From 08c98ae40028559e6047cdbd4f20f2388ccab86c Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Thu, 2 Apr 2026 16:06:06 +0900
Subject: [PATCH] Update RPR code comments to reflect 1-slot navigation model
---
src/backend/executor/execRPR.c | 45 ++++++++++++++++++++++------------
src/backend/parser/parse_rpr.c | 3 ++-
2 files changed, 32 insertions(+), 16 deletions(-)
diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 97efa9a4924..995acdd7be5 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -191,10 +191,15 @@
* transformDefineClause() processes each DEFINE variable as follows:
*
* (1) Checks for duplicate variable names
- * (2) Transforms the expression into a standard SQL expression
- * (3) Coerces to Boolean type (coerce_to_boolean)
+ * (2) Transforms the expression via transformExpr()
+ * (3) Extracts Var nodes via pull_var_clause() and ensures each is
+ * present in the query targetlist, so the planner propagates the
+ * referenced columns through the plan tree
* (4) Wraps in a TargetEntry with the variable name set in resname
*
+ * After all variables are processed:
+ * (5) Coerces each expression to Boolean type (coerce_to_boolean)
+ *
* Variables that are used in PATTERN but not defined in DEFINE are implicitly
* evaluated as TRUE (matching all rows).
*
@@ -431,8 +436,9 @@
*
* Case 1: Simple VAR+ (e.g., A+)
* -> ABSORBABLE | ABSORBABLE_BRANCH set on the VAR
- * Case 2: GROUP+ whose body consists only of {1,1} VARs (e.g., (A B)+)
- * -> ABSORBABLE_BRANCH on children,
+ * Case 2: GROUP+ with fixed-length children (min == max, recursively)
+ * e.g., (A B)+, (A B{2})+, ((A (B C){2}){2})+
+ * -> ABSORBABLE_BRANCH on all elements within the group,
* ABSORBABLE | ABSORBABLE_BRANCH on END
* Case 3: GROUP+ whose body starts with VAR+ (e.g., (A+ B)+)
* -> Recurses from BEGIN into the body, applying Case 1.
@@ -604,11 +610,17 @@
* varMatched[i] = (not null and true)
*
* To support row navigation operators such as PREV() and NEXT(),
- * the previous row, current row, and next row are set in separate slots:
+ * a 1-slot model is used: only ecxt_outertuple is set to the current
+ * row. PREV/NEXT navigation is handled by EEOP_RPR_NAV_SET/RESTORE
+ * opcodes emitted during DEFINE expression compilation:
+ *
+ * NAV_SET: save ecxt_outertuple, swap in target row via nav_slot
+ * (evaluate): argument expression reads from swapped slot
+ * NAV_RESTORE: restore original ecxt_outertuple
*
- * ecxt_scantuple = previous row (for PREV reference)
- * ecxt_outertuple = current row (default reference)
- * ecxt_innertuple = next row (for NEXT reference)
+ * nav_slot caches the last fetched position (nav_slot_pos) to avoid
+ * redundant tuplestore lookups when multiple PREV/NEXT calls target
+ * the same row.
*
* The varMatched array is referenced later in Phase 1 (Match).
*
@@ -908,8 +920,11 @@
*
* (2) At runtime: initialize the nfaVisitedElems bitmap immediately before
* DFS expansion of each state within advance (once per state).
- * During DFS, set the corresponding elemIdx bit when visiting each
- * element.
+ * During DFS, epsilon elements (END, ALT, BEGIN) are marked in the
+ * bitmap at nfa_advance_state entry. VAR elements are marked later
+ * when added to the state list (nfa_add_state_unique), so that
+ * legitimate loop-back to the same VAR in a new group iteration
+ * (e.g., END -> ALT -> same VAR) is not blocked.
* If a previously visited elemIdx is revisited, that path is terminated.
*
* Note: the bitmap tracks only elemIdx and does not consider counts.
@@ -1216,11 +1231,11 @@
* (3) State Deduplication (IX-5)
*
* During advance, DFS may generate states with the same (elemIdx,
- * counts) combination through multiple paths. Additionally, unlike
- * VAR repetition, group repetition cannot perform absorption
- * comparison using VAR states, so inline advance is performed from
- * after Phase 1 match through to END; this process can also produce
- * duplicate states reaching the same END.
+ * counts) combination through multiple paths. Additionally, for
+ * group absorption, nfa_match performs inline advance from bounded
+ * VARs (count >= max) within the absorbable region (ABSORBABLE_BRANCH)
+ * through END chains to reach the judgment point (ABSORBABLE END).
+ * This process can also produce duplicate states reaching the same END.
* nfa_add_state_unique() blocks duplicate addition of identical states
* in both cases.
*
diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index 3fb5d94abe9..d1e02e52e53 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -265,7 +265,8 @@ validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
*
* Then for each DEFINE variable:
* 2. Checks for duplicate variable names in DEFINE clause
- * 3. Transforms expressions and adds to targetlist via findTargetlistEntrySQL99
+ * 3. Transforms expression via transformExpr() and ensures referenced
+ * Var nodes are present in the query targetlist (via pull_var_clause)
* 4. Creates defineClause entry with proper resname (pattern variable name)
* 5. Coerces expressions to boolean type
* 6. Marks column origins and assigns collation information
--
2.50.1 (Apple Git-155)
From 74883a5f7ec23d20f0352d59595839099b141f89 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Mon, 6 Apr 2026 09:48:54 +0900
Subject: [PATCH] Enable JIT compilation for PREV/NEXT navigation tests in RPR
---
src/test/regress/expected/rpr.out | 2 ++
src/test/regress/sql/rpr.sql | 2 ++
2 files changed, 4 insertions(+)
diff --git a/src/test/regress/expected/rpr.out b/src/test/regress/expected/rpr.out
index de6ce4fba8a..5a460e9bd52 100644
--- a/src/test/regress/expected/rpr.out
+++ b/src/test/regress/expected/rpr.out
@@ -2156,6 +2156,7 @@ SELECT match_first, match_last, match_len FROM result WHERE match_len > 0;
-- JIT PREV/NEXT navigation test: 100K rows with PREV in DEFINE.
-- Exercises EEOP_RPR_NAV_SET/RESTORE JIT code paths (has_rpr_nav reload)
-- at scale. V-shape: price rises then falls, repeated across partition.
+SET jit = on;
SET jit_above_cost = 0;
WITH data AS (
SELECT i, abs(50000 - i) AS price
@@ -2184,6 +2185,7 @@ FROM result WHERE match_len > 0;
(1 row)
RESET jit_above_cost;
+RESET jit;
--
-- IGNORE NULLS
--
diff --git a/src/test/regress/sql/rpr.sql b/src/test/regress/sql/rpr.sql
index b3bbc8254c4..e417789eb2b 100644
--- a/src/test/regress/sql/rpr.sql
+++ b/src/test/regress/sql/rpr.sql
@@ -1087,6 +1087,7 @@ SELECT match_first, match_last, match_len FROM result WHERE match_len > 0;
-- JIT PREV/NEXT navigation test: 100K rows with PREV in DEFINE.
-- Exercises EEOP_RPR_NAV_SET/RESTORE JIT code paths (has_rpr_nav reload)
-- at scale. V-shape: price rises then falls, repeated across partition.
+SET jit = on;
SET jit_above_cost = 0;
WITH data AS (
SELECT i, abs(50000 - i) AS price
@@ -1110,6 +1111,7 @@ result AS (
SELECT count(*) AS matched_rows, max(match_len) AS longest_match
FROM result WHERE match_len > 0;
RESET jit_above_cost;
+RESET jit;
--
-- IGNORE NULLS
--
2.50.1 (Apple Git-155)
From c29f3eb34d13f2c0211d3070abaf1062da0090ae Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Mon, 6 Apr 2026 09:53:14 +0900
Subject: [PATCH] Add 2-arg PREV/NEXT test for row pattern navigation with host
variable
---
src/test/regress/expected/rpr.out | 63 +++++++++++++++++++++++++++++++
src/test/regress/sql/rpr.sql | 16 ++++++++
2 files changed, 79 insertions(+)
diff --git a/src/test/regress/expected/rpr.out b/src/test/regress/expected/rpr.out
index 5a460e9bd52..c02dbd4c08d 100644
--- a/src/test/regress/expected/rpr.out
+++ b/src/test/regress/expected/rpr.out
@@ -1492,6 +1492,69 @@ 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 positive value
+-- Exercises RPR_NAV_OFFSET_NEEDS_EVAL -> eval_nav_max_offset() path
+PREPARE test_prev_offset(int8) AS
+SELECT company, tdate, price, first_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)
+);
+EXECUTE test_prev_offset(1);
+ company | tdate | price | first_value | count
+----------+------------+-------+-------------+-------
+ company1 | 07-01-2023 | 100 | 100 | 2
+ company1 | 07-02-2023 | 200 | | 0
+ company1 | 07-03-2023 | 150 | | 0
+ company1 | 07-04-2023 | 140 | 140 | 2
+ company1 | 07-05-2023 | 150 | | 0
+ company1 | 07-06-2023 | 90 | 90 | 3
+ company1 | 07-07-2023 | 110 | | 0
+ company1 | 07-08-2023 | 130 | | 0
+ company1 | 07-09-2023 | 120 | 120 | 2
+ company1 | 07-10-2023 | 130 | | 0
+ company2 | 07-01-2023 | 50 | 50 | 2
+ company2 | 07-02-2023 | 2000 | | 0
+ company2 | 07-03-2023 | 1500 | | 0
+ company2 | 07-04-2023 | 1400 | 1400 | 2
+ company2 | 07-05-2023 | 1500 | | 0
+ company2 | 07-06-2023 | 60 | 60 | 3
+ company2 | 07-07-2023 | 1100 | | 0
+ company2 | 07-08-2023 | 1300 | | 0
+ company2 | 07-09-2023 | 1200 | 1200 | 2
+ company2 | 07-10-2023 | 1300 | | 0
+(20 rows)
+
+EXECUTE test_prev_offset(2);
+ company | tdate | price | first_value | count
+----------+------------+-------+-------------+-------
+ company1 | 07-01-2023 | 100 | | 0
+ company1 | 07-02-2023 | 200 | 200 | 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 | 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 | 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 | 3
+ company2 | 07-08-2023 | 1300 | | 0
+ company2 | 07-09-2023 | 1200 | | 0
+ company2 | 07-10-2023 | 1300 | | 0
+(20 rows)
+
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
diff --git a/src/test/regress/sql/rpr.sql b/src/test/regress/sql/rpr.sql
index e417789eb2b..47f33904690 100644
--- a/src/test/regress/sql/rpr.sql
+++ b/src/test/regress/sql/rpr.sql
@@ -732,6 +732,22 @@ EXECUTE test_prev_offset(-1);
EXECUTE test_prev_offset(NULL);
DEALLOCATE test_prev_offset;
+-- 2-arg PREV/NEXT: host variable with positive value
+-- Exercises RPR_NAV_OFFSET_NEEDS_EVAL -> eval_nav_max_offset() path
+PREPARE test_prev_offset(int8) AS
+SELECT company, tdate, price, first_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)
+);
+EXECUTE test_prev_offset(1);
+EXECUTE test_prev_offset(2);
+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,
--
2.50.1 (Apple Git-155)
From 8b1ef0227cb3e353a93b38a07602d51caa2a26d6 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Mon, 6 Apr 2026 10:46:38 +0900
Subject: [PATCH] Add Nav Mark Lookback to EXPLAIN and fix
compute_nav_max_offset()
Show the planner-computed navMaxOffset in EXPLAIN output as
"Nav Mark Lookback" for RPR windows, so that tuplestore trim
behavior is visible in query plans. Displayed as an integer for
constant offsets, "runtime" for non-constant (Param) offsets, and
"retain all" when the entire partition must be preserved.
Fix compute_nav_max_offset() to return the walker-set value instead
of unconditionally returning RPR_NAV_OFFSET_NEEDS_EVAL when the
walker stops early.
Add regression tests covering constant offsets, NEXT-only, multiple
PREV offsets, and host variable offsets under both custom and generic
plans.
---
src/backend/commands/explain.c | 9 +
src/backend/optimizer/plan/createplan.c | 2 +-
src/test/regress/expected/rpr_base.out | 231 ++++++----
src/test/regress/expected/rpr_explain.out | 397 ++++++++++++++----
src/test/regress/expected/rpr_integration.out | 25 +-
src/test/regress/sql/rpr_explain.sql | 71 ++++
6 files changed, 560 insertions(+), 175 deletions(-)
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 933eadab71e..1848de9de7a 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -3254,6 +3254,15 @@ show_window_def(WindowAggState *planstate, List *ancestors, ExplainState *es)
ExplainPropertyText("Pattern", patternStr, es);
pfree(patternStr);
}
+
+ /* Show navigation offsets for tuplestore trim */
+ if (wagg->navMaxOffset == RPR_NAV_OFFSET_RETAIN_ALL)
+ ExplainPropertyText("Nav Mark Lookback", "retain all", es);
+ else if (wagg->navMaxOffset == RPR_NAV_OFFSET_NEEDS_EVAL)
+ ExplainPropertyText("Nav Mark Lookback", "runtime", es);
+ else
+ ExplainPropertyInteger("Nav Mark Lookback", NULL,
+ wagg->navMaxOffset, es);
}
}
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index ee2d53b5924..8ee3ccf6d0d 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -2553,7 +2553,7 @@ compute_nav_max_offset(List *defineClause)
TargetEntry *te = (TargetEntry *) lfirst(lc);
if (nav_max_offset_walker((Node *) te->expr, &maxOffset))
- return RPR_NAV_OFFSET_NEEDS_EVAL;
+ return maxOffset; /* NEEDS_EVAL or RETAIN_ALL */
}
return maxOffset;
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index 37aa81ebdea..1e450a07ced 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -3065,10 +3065,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a{3}
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- Consecutive VAR merge: A{2} A{3} -> a{5}
EXPLAIN (COSTS OFF)
@@ -3080,10 +3081,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a{5}
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- Consecutive VAR merge: A+ A* -> a+
EXPLAIN (COSTS OFF)
@@ -3095,10 +3097,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a+"
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- Consecutive VAR merge: A A+ -> a{2,}
-- Tests line 251: child->max == RPR_QUANTITY_INF branch in mergeConsecutiveVars
@@ -3112,10 +3115,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a{2,}"
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- Consecutive GROUP merge with finite quantifiers: ((A B){5}) ((A B){10}) -> merged
EXPLAIN (COSTS OFF)
@@ -3127,10 +3131,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a b){15}
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- Consecutive GROUP merge with unbounded: (A B)+ (A B)+ -> (a b){2,}
EXPLAIN (COSTS OFF)
@@ -3142,10 +3147,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a' b'){2,}"
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- Consecutive GROUP merge: (A B){2} (A B)+ -> (a b){3,}
-- Tests line 325: child->max == RPR_QUANTITY_INF branch in mergeConsecutiveGroups
@@ -3159,10 +3165,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a' b'){3,}"
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- PREFIX merge: A B (A B)+ -> (a b){2,}
EXPLAIN (COSTS OFF)
@@ -3174,10 +3181,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a' b'){2,}"
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- PREFIX and SUFFIX merge: A B (A B)+ A B -> (a b){3,}
EXPLAIN (COSTS OFF)
@@ -3189,10 +3197,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a' b'){3,}"
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- Flatten nested: A ((B) (C)) -> a b c
EXPLAIN (COSTS OFF)
@@ -3204,10 +3213,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a b c
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- Data execution: SEQ flatten produces correct results
SELECT id, val, count(*) OVER w AS cnt
@@ -3239,10 +3249,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a | b | c)+
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- ALT deduplicate: (A | B | A) -> (a | b)
EXPLAIN (COSTS OFF)
@@ -3254,10 +3265,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a | b)+
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- Data execution: ALT dedup produces correct results
SELECT id, val, count(*) OVER w AS cnt
@@ -3289,10 +3301,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a{6}
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- Quantifier multiply with child range: (A{2,3}){3} -> a{6,9}
-- outer exact, child range - optimization applies
@@ -3305,10 +3318,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a{6,9}
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- Quantifier NO multiply: (A{2}){2,3} stays as (a{2}){2,3}
-- outer range - gaps would occur (4,6 not 4,5,6), no optimization
@@ -3321,10 +3335,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a{2}){2,3}
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- Quantifier NO multiply: (A{2}){2,} stays as (a{2}){2,}
-- outer unbounded - gaps would occur (4,6,8,... not 4,5,6,...), no optimization
@@ -3337,10 +3352,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a{2}'){2,}"
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- Quantifier multiply: (A){2,} -> a{2,}
-- child exact 1 - no gaps, optimization applies
@@ -3353,10 +3369,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a{2,}"
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- Quantifier multiply: (A)+ -> a+
-- child exact 1 - no gaps, optimization applies
@@ -3369,10 +3386,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a+"
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- Quantifier NO multiply: (A{2}){3,5} stays as (a{2}){3,5}
-- outer range, child exact > 1 - gaps would occur (6,8,10 not 6,7,8,9,10)
@@ -3385,10 +3403,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a{2}){3,5}
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- Quantifier NO multiply: (A{2,3}){2,3} stays as (a{2,3}){2,3}
-- outer range, child range - gaps possible (e.g., (A{4,5}){2,3} misses 11)
@@ -3401,10 +3420,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a{2,3}){2,3}
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- Nested unbounded: (A*)* -> a*
EXPLAIN (COSTS OFF)
@@ -3416,10 +3436,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a*"
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- Nested unbounded: (A+)* -> a*
EXPLAIN (COSTS OFF)
@@ -3431,10 +3452,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a*"
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- Nested unbounded: (A+)+ -> a+
EXPLAIN (COSTS OFF)
@@ -3446,10 +3468,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a+"
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- Unwrap GROUP{1,1}: (A) -> a
EXPLAIN (COSTS OFF)
@@ -3461,10 +3484,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- Unwrap GROUP{1,1}: (A B) -> a b
EXPLAIN (COSTS OFF)
@@ -3476,10 +3500,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a b
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- Combined optimization: A A (B B)+ B B C C C -> a{2} (b{2}){2,} c{3}
EXPLAIN (COSTS OFF)
@@ -3492,10 +3517,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a{2} (b{2}){2,} c{3}
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- Consecutive GROUP merge with unbounded: (A+) (A+) -> a{2,}
-- Tests mergeConsecutiveGroups with child->max == INF
@@ -3508,10 +3534,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a{2,}"
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- Consecutive GROUP merge finite: (A{10}){20} -> a{200}
-- Tests mergeConsecutiveGroups with both finite
@@ -3524,10 +3551,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a{200}
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- Different GROUP prevents merge: (A B){2} (C D){3}
-- Tests mergeConsecutiveGroups flush previous
@@ -3542,10 +3570,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a b){2} (c d){3}
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- Different children count prevents merge: (A B)+ (A B C)+
-- Tests rprPatternChildrenEqual length check
@@ -3559,10 +3588,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a' b')+" (a b c)+
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- PREFIX only merge: A B (A B)+ -> (a b){2,}
-- Tests mergeGroupPrefixSuffix: absorb preceding elements into GROUP min
@@ -3575,10 +3605,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a' b'){2,}"
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- SUFFIX only merge: (A B)+ A B -> (a b){2,}
-- Tests mergeGroupPrefixSuffix: absorb following elements into GROUP min
@@ -3591,10 +3622,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a' b'){2,}"
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- Multiple SUFFIX absorption with skipUntil: (A B)+ A B A B C
-- Tests mergeGroupPrefixSuffix: skip absorbed suffix elements
@@ -3608,10 +3640,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a' b'){3,}" c
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- PREFIX merge with remaining prefix: A B C D (C D)+
-- Tests mergeGroupPrefixSuffix: trimmed list reconstruction
@@ -3626,10 +3659,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a b (c d){2,}
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- PREFIX merge with quantifiers: A B* (A B*)+ -> (a b*){2,}
-- Tests mergeGroupPrefixSuffix: quantifier comparison in rprPatternEqual
@@ -3643,10 +3677,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a b*){2,}
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- PREFIX merge with multiple quantifiers: A+ B* C? (A+ B* C?)+ -> (a+ b* c?){2,}
-- Tests mergeGroupPrefixSuffix: complex quantifier patterns
@@ -3660,10 +3695,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a+" b* c?){2,}
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- SUFFIX merge with quantifiers: (A B*)+ A B* -> (a b*){2,}
-- Tests mergeGroupPrefixSuffix: suffix with quantifiers
@@ -3677,10 +3713,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a b*){2,}
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- Unwrap GROUP{1,1}: ((A | B | C)) -> (a | b | c)
-- Tests tryUnwrapGroup removing redundant outer GROUP
@@ -3693,10 +3730,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a | b | c)
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- Data execution: GROUP unwrap produces correct results
SELECT id, val, count(*) OVER w AS cnt
@@ -3729,10 +3767,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a+? a
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- Reluctant optimization bypass: GROUP merge
-- (A B)+? (A B) stays separate (greedy merges to (a b){2,})
@@ -3745,10 +3784,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a b)+? a b
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- Reluctant optimization bypass: quantifier multiply (outer reluctant)
-- (A{2}){3}? stays as (a{2}){3}? (greedy merges to a{6})
@@ -3761,10 +3801,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a{2}){3}?
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- Reluctant optimization bypass: quantifier multiply (inner reluctant)
-- (A{2}?){3} stays as (a{2}?){3} (greedy merges to a{6})
@@ -3777,10 +3818,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a{2}?){3}
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- Reluctant optimization bypass: PREFIX merge
-- A B (A B)+? stays separate (greedy merges to (a b){2,})
@@ -3793,10 +3835,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a b (a b)+?
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- Reluctant optimization bypass: SUFFIX merge
-- (A B)+? A B stays separate (greedy merges to (a b){2,})
@@ -3809,10 +3852,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a b)+? a b
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- GROUP unwrap with quantifier propagation: (A)?? B -> a?? b
-- Single VAR child {1,1} receives GROUP's quantifier and reluctant
@@ -3825,10 +3869,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a?? b
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- Reluctant preserved through ALT flatten
-- (A | (B | C))+? flattens to (a | b | c)+? - inner ALT flattened, reluctant kept
@@ -3841,10 +3886,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a | b | c)+?
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- Reluctant optimization bypass: absorption flags
-- A+? with SKIP PAST LAST ROW - no absorption markers (greedy A+ gets a+")
@@ -3857,10 +3903,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a+?
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- Duplicate GROUP removal: ((A | B)+ | (A | B)+) -> (a | b)+
EXPLAIN (COSTS OFF)
@@ -3872,10 +3919,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a | b)+
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- Consecutive VAR merge with zero-min: A* A+ -> a+
EXPLAIN (COSTS OFF)
@@ -3887,10 +3935,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a+"
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- Consecutive VAR merge (4-element): A A{2} A+ A{3} -> a{7,}
EXPLAIN (COSTS OFF)
@@ -3902,10 +3951,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a{7,}"
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- PREFIX+SUFFIX merge (5-way): A B A B (A B)+ A B A B -> (a b){5,}
EXPLAIN (COSTS OFF)
@@ -3918,10 +3968,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a' b'){5,}"
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- Unwrap single-item ALT after dedup: (A | A)+ -> a+
-- ALT dedup reduces to single-item, then GROUP unwrap
@@ -3934,10 +3985,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a+"
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- GROUP{1,1} to SEQ with flatten: ((A B)(C D)) -> a b c d
EXPLAIN (COSTS OFF)
@@ -3951,10 +4003,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a b c d
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- Nested ALT pattern: ((A B) | C) D | A B C
EXPLAIN (COSTS OFF)
@@ -3968,10 +4021,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: ((a b | c) d | a b c)
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- Nested ALT with unbounded: ((A+ B) | C) D | A B C
EXPLAIN (COSTS OFF)
@@ -3985,10 +4039,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: ((a+" b | c) d | a b c)
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- ============================================================
-- Absorption Flag Display Tests
@@ -4006,10 +4061,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a+"
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- GROUP unbounded: (A B)+ -> (a' b')+" (branch + judgment)
EXPLAIN (COSTS OFF)
@@ -4021,10 +4077,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a' b')+"
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- ALT both absorbable: A+ | B+ -> (a+" | b+")
EXPLAIN (COSTS OFF)
@@ -4036,10 +4093,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a+" | b+")
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- ALT one absorbable: A+ | B -> (a+" | b)
EXPLAIN (COSTS OFF)
@@ -4051,10 +4109,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a+" | b)
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- Sequence with absorbable start: A+ B -> a+" b
EXPLAIN (COSTS OFF)
@@ -4066,10 +4125,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a+" b
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- Complex nested: ((A+ B) | C) D | A B C - deeply nested ALT
EXPLAIN (COSTS OFF)
@@ -4082,10 +4142,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: ((a+" b | c) d | a b c)
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- Nested unbounded: (A+ | B)+ -> (a+" | b)+ (first iteration absorbable)
EXPLAIN (COSTS OFF)
@@ -4098,10 +4159,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a+" | b)+
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- ALT inside unbounded GROUP: (A+ B | A B)* -> (a+" b | a b)* (first iteration absorbable)
EXPLAIN (COSTS OFF)
@@ -4114,10 +4176,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a+" b | a b)*
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- Fixed-length group absorbable: (A{2} B{3})+ -> (a{2}' b{3}'){2,}"
-- All children have min == max, equivalent to unrolling to {1,1}
@@ -4131,10 +4194,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a{2}' b{3}')+"
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- Nested fixed-length group: (A (B C){2} D)+ -> absorbable
EXPLAIN (COSTS OFF)
@@ -4147,10 +4211,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a' (b' c'){2}' d')+"
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- Nested fixed-length with inner quantifier: ((A{2} B{3}){2})+ -> absorbable
EXPLAIN (COSTS OFF)
@@ -4163,10 +4228,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: ((a{2}' b{3}'){2}')+"
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- Non-absorbable fixed-length: (A B{2,5})+ -> no markers (min != max)
EXPLAIN (COSTS OFF)
@@ -4179,10 +4245,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a b{2,5})+
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- Non-absorbable fixed-length: (A B?)+ -> no markers (min != max)
EXPLAIN (COSTS OFF)
@@ -4195,10 +4262,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a b?)+
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- Non-absorbable (unbounded not at start): A B+ -> a b+ (no markers)
EXPLAIN (COSTS OFF)
@@ -4210,10 +4278,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a b+
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- Non-absorbable (no unbounded branch): (A | B){2,} -> (a | b){2,} (no markers)
EXPLAIN (COSTS OFF)
@@ -4225,10 +4294,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a | b){2,}
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- Non-absorbable (SKIP TO NEXT ROW): A+ -> a+ (no markers)
EXPLAIN (COSTS OFF)
@@ -4240,10 +4310,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a+
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- Non-absorbable (limited frame): A+ -> a+ (no markers)
EXPLAIN (COSTS OFF)
@@ -4255,10 +4326,11 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND 10 FOLLOWING
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND '10'::bigint FOLLOWING)
Pattern: a+
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- Reluctant {1}? quantifier deparse
-- A{1}? is a reluctant {1,1} quantifier. The deparse code must
@@ -4277,10 +4349,11 @@ WINDOW w AS (
WindowAgg
Window: w AS (ORDER BY val ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a{1}? b
+ Nav Mark Lookback: 0
-> Sort
Sort Key: val
-> Seq Scan on rpr_plan
-(6 rows)
+(7 rows)
-- ============================================================
-- Absorption Analysis Tests
@@ -4775,10 +4848,11 @@ WINDOW w AS (
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a{2000000000}){2}
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_fallback
-(6 rows)
+(7 rows)
-- Expected: Fallback - pattern not merged due to min overflow (4000000000 > INT32_MAX)
-- Test: max-only quantifier overflow causes optimization fallback
@@ -4795,10 +4869,11 @@ WINDOW w AS (
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a{1,2000000000}){2}
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_fallback
-(6 rows)
+(7 rows)
-- Expected: Fallback - min OK (2*1=2), but max overflow (2*2000000000 > INT32_MAX)
-- Test: max quantifier exceeds valid range (2147483647 = INT_MAX, limit is 2147483646)
@@ -4828,10 +4903,11 @@ WINDOW w AS (
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a{2000000000,}"){2000000000,}
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_fallback
-(6 rows)
+(7 rows)
-- Expected: Fallback - min overflow (2000000000 * 2000000000 > INT32_MAX)
-- Test: prefix mismatch causes optimization fallback
@@ -4848,10 +4924,11 @@ WINDOW w AS (
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a b (c d)+
+ Nav Mark Lookback: 0
-> Sort
Sort Key: id
-> Seq Scan on rpr_fallback
-(6 rows)
+(7 rows)
-- Expected: Fallback - prefix elements don't match GROUP content
DROP TABLE rpr_fallback;
diff --git a/src/test/regress/expected/rpr_explain.out b/src/test/regress/expected/rpr_explain.out
index dc3075e6bd3..77ab25a2289 100644
--- a/src/test/regress/expected/rpr_explain.out
+++ b/src/test/regress/expected/rpr_explain.out
@@ -36,6 +36,7 @@
-- Window Function Combinations
-- DEFINE Expression Variations
-- Large Scale Statistics Verification
+-- Nav Mark Lookback (tuplestore trim)
-- ============================================================
-- Filter function to normalize platform-dependent memory values (not NFA statistics).
-- NFA statistics should not change between platforms; if they do, it could
@@ -141,13 +142,14 @@ WINDOW w AS (
WindowAgg (actual rows=100.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a b
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 2 peak, 101 total, 0 merged
NFA Contexts: 2 peak, 101 total, 60 pruned
NFA: 20 matched (len 2/2/2.0), 0 mismatched
NFA: 0 absorbed, 20 skipped (len 1/1/1.0)
-> Seq Scan on rpr_nfa_test (actual rows=100.00 loops=1)
-(9 rows)
+(10 rows)
-- Pattern with no matches - 0 matched
CREATE VIEW rpr_ev_basic_nomatch AS
@@ -180,12 +182,13 @@ WINDOW w AS (
WindowAgg (actual rows=100.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: x y z
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 1 peak, 101 total, 0 merged
NFA Contexts: 2 peak, 101 total, 100 pruned
NFA: 0 matched, 0 mismatched
-> Seq Scan on rpr_nfa_test (actual rows=100.00 loops=1)
-(8 rows)
+(9 rows)
-- Pattern matching every row - high match count
CREATE VIEW rpr_ev_basic_allrows AS
@@ -218,12 +221,13 @@ WINDOW w AS (
WindowAgg (actual rows=100.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: r
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 2 peak, 101 total, 0 merged
NFA Contexts: 2 peak, 101 total, 0 pruned
NFA: 100 matched (len 1/1/1.0), 0 mismatched
-> Seq Scan on rpr_nfa_test (actual rows=100.00 loops=1)
-(8 rows)
+(9 rows)
-- Regression test: Space before parenthesis in pattern deparse
-- Verifies that "A (B | C)" correctly outputs as "a (b | c)" with space
@@ -255,13 +259,14 @@ WINDOW w AS (
WindowAgg (actual rows=20.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a (b | c)
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 3 peak, 35 total, 0 merged
NFA Contexts: 2 peak, 21 total, 6 pruned
NFA: 7 matched (len 2/2/2.0), 0 mismatched
NFA: 0 absorbed, 7 skipped (len 1/1/1.0)
-> Function Scan on generate_series s (actual rows=20.00 loops=1)
-(9 rows)
+(10 rows)
-- Regression test: Sequential alternations at same depth
-- Verifies that "((B | C) (D | E))" correctly outputs as "(b | c) (d | e)"
@@ -294,12 +299,13 @@ WINDOW w AS (
WindowAgg (actual rows=30.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a ((b | c) (d | e))*
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 4 peak, 61 total, 0 merged
NFA Contexts: 3 peak, 31 total, 24 pruned
NFA: 6 matched (len 1/1/1.0), 0 mismatched
-> Function Scan on generate_series s (actual rows=30.00 loops=1)
-(8 rows)
+(9 rows)
-- Regression test: Quoted identifiers in EXPLAIN pattern deparse
-- Mixed case names must be quoted to preserve round-trip safety
@@ -317,8 +323,9 @@ WINDOW w AS (
WindowAgg
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: "Start" "Up"+
+ Nav Mark Lookback: 1
-> Function Scan on generate_series s
-(4 rows)
+(5 rows)
-- ============================================================
-- State Statistics Tests (peak, total, merged)
@@ -354,12 +361,13 @@ WINDOW w AS (
WindowAgg (actual rows=50.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a+"
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 3 peak, 76 total, 0 merged
NFA Contexts: 3 peak, 51 total, 25 pruned
NFA: 25 matched (len 1/1/1.0), 0 mismatched
-> Function Scan on generate_series s (actual rows=50.00 loops=1)
-(8 rows)
+(9 rows)
-- Alternation pattern - multiple state branches
CREATE VIEW rpr_ev_state_alt AS
@@ -396,13 +404,14 @@ WINDOW w AS (
WindowAgg (actual rows=100.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a | b | c) (d | e)
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 6 peak, 524 total, 0 merged
NFA Contexts: 3 peak, 101 total, 20 pruned
NFA: 20 matched (len 2/2/2.0), 40 mismatched (len 2/2/2.0)
NFA: 0 absorbed, 20 skipped (len 1/1/1.0)
-> Seq Scan on rpr_nfa_test (actual rows=100.00 loops=1)
-(9 rows)
+(10 rows)
-- Complex pattern with high state count
CREATE VIEW rpr_ev_state_complex AS
@@ -441,13 +450,14 @@ WINDOW w AS (
WindowAgg (actual rows=100.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a+" b* c+
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 5 peak, 235 total, 0 merged
NFA Contexts: 3 peak, 101 total, 34 pruned
NFA: 33 matched (len 3/3/3.0), 0 mismatched
NFA: 0 absorbed, 33 skipped (len 1/1/1.0)
-> Function Scan on generate_series s (actual rows=100.00 loops=1)
-(9 rows)
+(10 rows)
-- Grouped pattern with quantifier - state count with grouping
CREATE VIEW rpr_ev_state_group_quant AS
@@ -480,13 +490,14 @@ WINDOW w AS (
WindowAgg (actual rows=60.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a' b')+"
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 4 peak, 91 total, 0 merged
NFA Contexts: 3 peak, 61 total, 0 pruned
NFA: 1 matched (len 60/60/60.0), 0 mismatched
NFA: 29 absorbed (len 2/2/2.0), 30 skipped (len 1/1/1.0)
-> Function Scan on generate_series s (actual rows=60.00 loops=1)
-(9 rows)
+(10 rows)
-- State explosion pattern - many alternations
-- Pattern (A|B)(A|B)(A|B)(A|B) can create many parallel states
@@ -520,13 +531,14 @@ WINDOW w AS (
WindowAgg (actual rows=100.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a | b){8}
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 17 peak, 995 total, 0 merged
NFA Contexts: 8 peak, 101 total, 1 pruned
NFA: 12 matched (len 8/8/8.0), 3 mismatched (len 2/4/3.0)
NFA: 0 absorbed, 84 skipped (len 1/7/4.0)
-> Function Scan on generate_series s (actual rows=100.00 loops=1)
-(9 rows)
+(10 rows)
-- Consecutive ALT merge followed by different ALT
-- Tests mergeConsecutiveAlts flush on ALT change: (A|B){2} (C|D)
@@ -560,13 +572,14 @@ WINDOW w AS (
WindowAgg (actual rows=40.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a | b){2} (c | d)
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 7 peak, 181 total, 0 merged
NFA Contexts: 3 peak, 41 total, 12 pruned
NFA: 9 matched (len 3/3/3.0), 1 mismatched (len 2/2/2.0)
NFA: 0 absorbed, 18 skipped (len 1/2/1.5)
-> Function Scan on generate_series s (actual rows=40.00 loops=1)
-(9 rows)
+(10 rows)
-- Consecutive ALT merge followed by non-ALT element
-- Tests mergeConsecutiveAlts flush on non-ALT: (A|B){2} c
@@ -600,13 +613,14 @@ WINDOW w AS (
WindowAgg (actual rows=40.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a | b){2} c
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 6 peak, 177 total, 0 merged
NFA Contexts: 3 peak, 41 total, 2 pruned
NFA: 12 matched (len 3/3/3.0), 2 mismatched (len 2/2/2.0)
NFA: 0 absorbed, 24 skipped (len 1/2/1.5)
-> Function Scan on generate_series s (actual rows=40.00 loops=1)
-(9 rows)
+(10 rows)
-- ALT prefix/suffix absorbed into GROUP: (A|B) (A|B)+ (A|B) -> (A|B){3,}
CREATE VIEW rpr_ev_state_alt_absorb_group AS
@@ -639,13 +653,14 @@ WINDOW w AS (
WindowAgg (actual rows=40.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a | b){3,}
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 7 peak, 243 total, 0 merged
NFA Contexts: 3 peak, 41 total, 0 pruned
NFA: 1 matched (len 40/40/40.0), 0 mismatched
NFA: 0 absorbed, 39 skipped (len 1/2/1.0)
-> Function Scan on generate_series s (actual rows=40.00 loops=1)
-(9 rows)
+(10 rows)
-- High state count - alternation with plus quantifier
CREATE VIEW rpr_ev_state_alt_plus AS
@@ -678,13 +693,14 @@ WINDOW w AS (
WindowAgg (actual rows=100.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a | b | c)+ d
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 16 peak, 1004 total, 0 merged
NFA Contexts: 4 peak, 101 total, 0 pruned
NFA: 25 matched (len 4/4/4.0), 0 mismatched
NFA: 0 absorbed, 75 skipped (len 1/3/2.0)
-> Function Scan on generate_series s (actual rows=100.00 loops=1)
-(9 rows)
+(10 rows)
-- Early termination: first ALT branch (A) reaches FIN immediately,
-- pruning second branch (A B+) before it can accumulate B repetitions.
@@ -718,12 +734,13 @@ WINDOW w AS (
WindowAgg (actual rows=100.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a | a b)+
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 6 peak, 306 total, 0 merged
NFA Contexts: 3 peak, 101 total, 99 pruned
NFA: 1 matched (len 1/1/1.0), 0 mismatched
-> Function Scan on generate_series s (actual rows=100.00 loops=1)
-(8 rows)
+(9 rows)
-- Nested quantifiers causing state growth
CREATE VIEW rpr_ev_state_nested_quant AS
@@ -756,13 +773,14 @@ WINDOW w AS (
WindowAgg (actual rows=1000.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a | b)+
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 6 peak, 5004 total, 0 merged
NFA Contexts: 3 peak, 1001 total, 333 pruned
NFA: 334 matched (len 1/2/2.0), 0 mismatched
NFA: 0 absorbed, 333 skipped (len 1/1/1.0)
-> Function Scan on generate_series s (actual rows=1000.00 loops=1)
-(9 rows)
+(10 rows)
-- ============================================================
-- Context Statistics Tests (peak, total, pruned + absorbed/skipped)
@@ -798,13 +816,14 @@ WINDOW w AS (
WindowAgg (actual rows=50.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a+" b
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 3 peak, 91 total, 0 merged
NFA Contexts: 2 peak, 51 total, 0 pruned
NFA: 10 matched (len 5/5/5.0), 0 mismatched
NFA: 30 absorbed (len 1/1/1.0), 10 skipped (len 1/1/1.0)
-> Function Scan on generate_series s (actual rows=50.00 loops=1)
-(9 rows)
+(10 rows)
-- No absorption - bounded quantifier
CREATE VIEW rpr_ev_ctx_no_absorb AS
@@ -837,13 +856,14 @@ WINDOW w AS (
WindowAgg (actual rows=50.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a{2,4} b
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 7 peak, 101 total, 0 merged
NFA Contexts: 5 peak, 51 total, 0 pruned
NFA: 10 matched (len 5/5/5.0), 0 mismatched
NFA: 0 absorbed, 40 skipped (len 1/4/2.5)
-> Function Scan on generate_series s (actual rows=50.00 loops=1)
-(9 rows)
+(10 rows)
-- Contexts skipped by SKIP PAST LAST ROW
CREATE VIEW rpr_ev_ctx_skip AS
@@ -876,13 +896,14 @@ WINDOW w AS (
WindowAgg (actual rows=100.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a b c
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 2 peak, 101 total, 0 merged
NFA Contexts: 3 peak, 101 total, 80 pruned
NFA: 10 matched (len 3/3/3.0), 0 mismatched
NFA: 0 absorbed, 10 skipped (len 1/1/1.0)
-> Function Scan on generate_series s (actual rows=100.00 loops=1)
-(9 rows)
+(10 rows)
-- High context absorption - unbounded group
CREATE VIEW rpr_ev_ctx_absorb_group AS
@@ -915,13 +936,14 @@ WINDOW w AS (
WindowAgg (actual rows=100.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a' b')+" c
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 3 peak, 134 total, 0 merged
NFA Contexts: 3 peak, 101 total, 34 pruned
NFA: 33 matched (len 3/3/3.0), 0 mismatched
NFA: 0 absorbed, 33 skipped (len 1/1/1.0)
-> Function Scan on generate_series s (actual rows=100.00 loops=1)
-(9 rows)
+(10 rows)
-- Fixed-length group absorption: (A B B)+ C
-- B B merged to B{2}; absorbable with fixed-length check
@@ -956,13 +978,14 @@ WINDOW w AS (
WindowAgg (actual rows=70.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a' b{2}')+" c
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 3 peak, 91 total, 0 merged
NFA Contexts: 4 peak, 71 total, 40 pruned
NFA: 10 matched (len 7/7/7.0), 0 mismatched
NFA: 10 absorbed (len 3/3/3.0), 10 skipped (len 1/1/1.0)
-> Function Scan on generate_series s (actual rows=70.00 loops=1)
-(9 rows)
+(10 rows)
-- Nested fixed-length group absorption: (A (B C){2} D)+ E
-- step_size = 1 + (1+1)*2 + 1 = 6; v % 13 cycle gives 2 iterations + E
@@ -1000,13 +1023,14 @@ WINDOW w AS (
WindowAgg (actual rows=65.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a' (b' c'){2}' d')+" e
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 3 peak, 76 total, 0 merged
NFA Contexts: 4 peak, 66 total, 50 pruned
NFA: 5 matched (len 13/13/13.0), 0 mismatched
NFA: 5 absorbed (len 6/6/6.0), 5 skipped (len 1/1/1.0)
-> Function Scan on generate_series s (actual rows=65.00 loops=1)
-(9 rows)
+(10 rows)
-- Doubly nested fixed-length group absorption: (A ((B C{3}){2} D){2} E)+ F
-- step_size = 1 + ((1+3)*2+1)*2 + 1 = 20; v % 41 cycle gives 2 iterations + F
@@ -1052,13 +1076,14 @@ WINDOW w AS (
WindowAgg (actual rows=82.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a' ((b' c{3}'){2}' d'){2}' e')+" f
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 3 peak, 87 total, 0 merged
NFA Contexts: 4 peak, 83 total, 76 pruned
NFA: 2 matched (len 41/41/41.0), 0 mismatched
NFA: 2 absorbed (len 20/20/20.0), 2 skipped (len 1/1/1.0)
-> Function Scan on generate_series s (actual rows=82.00 loops=1)
-(9 rows)
+(10 rows)
-- 3-level END chain absorption: ((A (B C){2}){2})+
-- step_size = (1 + (1+1)*2) * 2 = 10; v % 21 cycle gives 2 iterations
@@ -1097,13 +1122,14 @@ WINDOW w AS (
WindowAgg (actual rows=42.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: ((a' (b' c'){2}'){2}')+"
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 5 peak, 47 total, 0 merged
NFA Contexts: 5 peak, 43 total, 30 pruned
NFA: 2 matched (len 20/20/20.0), 0 mismatched
NFA: 2 absorbed (len 10/10/10.0), 8 skipped (len 1/5/3.0)
-> Function Scan on generate_series s (actual rows=42.00 loops=1)
-(9 rows)
+(10 rows)
-- ============================================================
-- Match Length Statistics Tests
@@ -1143,13 +1169,14 @@ WINDOW w AS (
WindowAgg (actual rows=100.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a b c d e
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 2 peak, 101 total, 0 merged
NFA Contexts: 3 peak, 101 total, 60 pruned
NFA: 20 matched (len 5/5/5.0), 0 mismatched
NFA: 0 absorbed, 20 skipped (len 1/1/1.0)
-> Seq Scan on rpr_nfa_test (actual rows=100.00 loops=1)
-(9 rows)
+(10 rows)
-- Variable length matches - min/max/avg differ
CREATE VIEW rpr_ev_mlen_variable AS
@@ -1182,13 +1209,14 @@ WINDOW w AS (
WindowAgg (actual rows=100.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a+" b
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 3 peak, 191 total, 0 merged
NFA Contexts: 2 peak, 101 total, 0 pruned
NFA: 10 matched (len 10/10/10.0), 0 mismatched
NFA: 80 absorbed (len 1/1/1.0), 10 skipped (len 1/1/1.0)
-> Function Scan on generate_series s (actual rows=100.00 loops=1)
-(9 rows)
+(10 rows)
-- Very long matches
CREATE VIEW rpr_ev_mlen_long AS
@@ -1221,13 +1249,14 @@ WINDOW w AS (
WindowAgg (actual rows=200.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a+" b
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 3 peak, 396 total, 0 merged
NFA Contexts: 2 peak, 201 total, 4 pruned
NFA: 1 matched (len 196/196/196.0), 0 mismatched
NFA: 194 absorbed (len 1/1/1.0), 1 skipped (len 1/1/1.0)
-> Function Scan on generate_series s (actual rows=200.00 loops=1)
-(9 rows)
+(10 rows)
-- Uniform match length with mismatches from gap rows (v%20 = 11..15)
CREATE VIEW rpr_ev_mlen_with_mismatch AS
@@ -1264,13 +1293,14 @@ WINDOW w AS (
WindowAgg (actual rows=100.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a+" b
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 3 peak, 171 total, 0 merged
NFA Contexts: 3 peak, 101 total, 25 pruned
NFA: 5 matched (len 5/5/5.0), 5 mismatched (len 11/11/11.0)
NFA: 60 absorbed (len 1/1/1.0), 5 skipped (len 1/1/1.0)
-> Function Scan on generate_series s (actual rows=100.00 loops=1)
-(9 rows)
+(10 rows)
-- ============================================================
-- Mismatch Length Statistics Tests
@@ -1321,13 +1351,14 @@ WINDOW w AS (
WindowAgg (actual rows=100.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a+" b+ c
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 3 peak, 151 total, 0 merged
NFA Contexts: 3 peak, 101 total, 60 pruned
NFA: 10 matched (len 6/6/6.0), 0 mismatched
NFA: 20 absorbed (len 1/1/1.0), 10 skipped (len 1/1/1.0)
-> Function Scan on generate_series s (actual rows=100.00 loops=1)
-(9 rows)
+(10 rows)
-- Long partial matches that fail
CREATE VIEW rpr_ev_mlen_long_partial AS
@@ -1384,13 +1415,14 @@ WINDOW w AS (
WindowAgg (actual rows=60.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a+" b+ c
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 3 peak, 115 total, 0 merged
NFA Contexts: 3 peak, 61 total, 15 pruned
NFA: 1 matched (len 30/30/30.0), 1 mismatched (len 26/26/26.0)
NFA: 42 absorbed (len 1/1/1.0), 1 skipped (len 1/1/1.0)
-> Function Scan on generate_series i (actual rows=60.00 loops=1)
-(9 rows)
+(10 rows)
-- ============================================================
-- JSON Format Tests
@@ -1434,6 +1466,7 @@ WINDOW w AS (
"Disabled": false, +
"Window": "w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)",+
"Pattern": "a+\" b+", +
+ "Nav Mark Lookback": 0, +
"Storage": "Memory", +
"Maximum Storage": 0, +
"NFA States Peak": 3, +
@@ -1511,6 +1544,7 @@ WINDOW w AS (
"Disabled": false, +
"Window": "w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)",+
"Pattern": "a+\" b", +
+ "Nav Mark Lookback": 0, +
"Storage": "Memory", +
"Maximum Storage": 0, +
"NFA States Peak": 3, +
@@ -1592,6 +1626,7 @@ WINDOW w AS (
"Disabled": false, +
"Window": "w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)",+
"Pattern": "a b c", +
+ "Nav Mark Lookback": 0, +
"Storage": "Memory", +
"Maximum Storage": 0, +
"NFA States Peak": 2, +
@@ -1672,6 +1707,7 @@ WINDOW w AS (
"Disabled": false, +
"Window": "w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)",+
"Pattern": "(a | b){8}", +
+ "Nav Mark Lookback": 0, +
"Storage": "Memory", +
"Maximum Storage": 0, +
"NFA States Peak": 17, +
@@ -1755,6 +1791,7 @@ WINDOW w AS (
<Disabled>false</Disabled> +
<Window>w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)</Window>+
<Pattern>a b</Pattern> +
+ <Nav-Mark-Lookback>0</Nav-Mark-Lookback> +
<Storage>Memory</Storage> +
<Maximum-Storage>0</Maximum-Storage> +
<NFA-States-Peak>2</NFA-States-Peak> +
@@ -1837,6 +1874,7 @@ WINDOW w AS (
WindowAgg (actual rows=90.00 loops=1)
Window: w AS (PARTITION BY p.p ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a+" b
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 3 peak, 165 total, 0 merged
NFA Contexts: 2 peak, 93 total, 0 pruned
@@ -1848,7 +1886,7 @@ WINDOW w AS (
-> Nested Loop (actual rows=90.00 loops=1)
-> Function Scan on generate_series p (actual rows=3.00 loops=1)
-> Function Scan on generate_series v (actual rows=30.00 loops=3)
-(14 rows)
+(15 rows)
-- Different pattern behavior per partition
CREATE VIEW rpr_ev_part_diff AS
@@ -1893,6 +1931,7 @@ WINDOW w AS (
WindowAgg (actual rows=50.00 loops=1)
Window: w AS (PARTITION BY (CASE WHEN (v.v <= 25) THEN 1 ELSE 2 END) ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a+" b
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 3 peak, 77 total, 0 merged
NFA Contexts: 2 peak, 52 total, 21 pruned
@@ -1902,7 +1941,7 @@ WINDOW w AS (
Sort Key: (CASE WHEN (v.v <= 25) THEN 1 ELSE 2 END)
Sort Method: quicksort Memory: NkB
-> Function Scan on generate_series v (actual rows=50.00 loops=1)
-(12 rows)
+(13 rows)
-- ============================================================
-- Edge Cases
@@ -1938,8 +1977,9 @@ WINDOW w AS (
WindowAgg (actual rows=0.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a b
+ Nav Mark Lookback: 0
-> Function Scan on generate_series s (actual rows=0.00 loops=1)
-(4 rows)
+(5 rows)
-- Single row
CREATE VIEW rpr_ev_edge_single_row AS
@@ -1972,12 +2012,13 @@ WINDOW w AS (
WindowAgg (actual rows=1.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 2 peak, 2 total, 0 merged
NFA Contexts: 2 peak, 2 total, 0 pruned
NFA: 1 matched (len 1/1/1.0), 0 mismatched
-> Function Scan on generate_series s (actual rows=1.00 loops=1)
-(8 rows)
+(9 rows)
-- Pattern longer than data
CREATE VIEW rpr_ev_edge_pattern_longer AS
@@ -2014,12 +2055,13 @@ WINDOW w AS (
WindowAgg (actual rows=5.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a b c d e f g h i j
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 2 peak, 6 total, 0 merged
NFA Contexts: 3 peak, 6 total, 4 pruned
NFA: 0 matched, 1 mismatched (len 5/5/5.0)
-> Function Scan on generate_series s (actual rows=5.00 loops=1)
-(8 rows)
+(9 rows)
-- All rows match as single match
CREATE VIEW rpr_ev_edge_single_match AS
@@ -2052,13 +2094,14 @@ WINDOW w AS (
WindowAgg (actual rows=50.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a+"
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 3 peak, 101 total, 0 merged
NFA Contexts: 2 peak, 51 total, 0 pruned
NFA: 1 matched (len 50/50/50.0), 0 mismatched
NFA: 49 absorbed (len 1/1/1.0), 0 skipped
-> Function Scan on generate_series s (actual rows=50.00 loops=1)
-(9 rows)
+(10 rows)
-- ============================================================
-- Complex Pattern Tests
@@ -2094,13 +2137,14 @@ WINDOW w AS (
WindowAgg (actual rows=60.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a' b' c')+"
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 4 peak, 81 total, 0 merged
NFA Contexts: 4 peak, 61 total, 20 pruned
NFA: 1 matched (len 60/60/60.0), 0 mismatched
NFA: 19 absorbed (len 3/3/3.0), 20 skipped (len 1/1/1.0)
-> Function Scan on generate_series s (actual rows=60.00 loops=1)
-(9 rows)
+(10 rows)
-- Multiple alternations
CREATE VIEW rpr_ev_cpx_multi_alt AS
@@ -2137,13 +2181,14 @@ WINDOW w AS (
WindowAgg (actual rows=100.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a | b) (c | d | e)
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 6 peak, 423 total, 0 merged
NFA Contexts: 3 peak, 101 total, 40 pruned
NFA: 20 matched (len 2/2/2.0), 20 mismatched (len 2/2/2.0)
NFA: 0 absorbed, 20 skipped (len 1/1/1.0)
-> Seq Scan on rpr_nfa_test (actual rows=100.00 loops=1)
-(9 rows)
+(10 rows)
-- Optional elements
CREATE VIEW rpr_ev_cpx_optional AS
@@ -2176,13 +2221,14 @@ WINDOW w AS (
WindowAgg (actual rows=50.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a b? c
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 3 peak, 64 total, 0 merged
NFA Contexts: 3 peak, 51 total, 25 pruned
NFA: 12 matched (len 3/3/3.0), 1 mismatched (len 2/2/2.0)
NFA: 0 absorbed, 12 skipped (len 1/1/1.0)
-> Function Scan on generate_series s (actual rows=50.00 loops=1)
-(9 rows)
+(10 rows)
-- Bounded quantifiers
CREATE VIEW rpr_ev_cpx_bounded AS
@@ -2215,13 +2261,14 @@ WINDOW w AS (
WindowAgg (actual rows=100.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a{2,5} b
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 9 peak, 311 total, 0 merged
NFA Contexts: 7 peak, 101 total, 0 pruned
NFA: 10 matched (len 6/6/6.0), 40 mismatched (len 6/6/6.0)
NFA: 0 absorbed, 50 skipped (len 1/5/3.0)
-> Function Scan on generate_series s (actual rows=100.00 loops=1)
-(9 rows)
+(10 rows)
-- Star quantifier
CREATE VIEW rpr_ev_cpx_star AS
@@ -2254,13 +2301,14 @@ WINDOW w AS (
WindowAgg (actual rows=50.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a b* c
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 3 peak, 91 total, 0 merged
NFA Contexts: 3 peak, 51 total, 40 pruned
NFA: 5 matched (len 9/9/9.0), 0 mismatched
NFA: 0 absorbed, 5 skipped (len 1/1/1.0)
-> Function Scan on generate_series s (actual rows=50.00 loops=1)
-(9 rows)
+(10 rows)
-- ============================================================
-- Real-world Pattern Examples
@@ -2296,13 +2344,14 @@ WINDOW w AS (
WindowAgg (actual rows=30.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: d+" u+
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 4 peak, 58 total, 0 merged
NFA Contexts: 3 peak, 31 total, 3 pruned
NFA: 3 matched (len 3/14/8.0), 1 mismatched (len 3/3/3.0)
NFA: 9 absorbed (len 1/1/1.0), 14 skipped (len 1/1/1.0)
-> Seq Scan on rpr_nfa_complex (actual rows=30.00 loops=1)
-(9 rows)
+(10 rows)
-- Stock price pattern - peak (up, stable, down)
CREATE VIEW rpr_ev_real_peak AS
@@ -2335,13 +2384,14 @@ WINDOW w AS (
WindowAgg (actual rows=30.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: u+" s* d+
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 5 peak, 76 total, 0 merged
NFA Contexts: 3 peak, 31 total, 1 pruned
NFA: 4 matched (len 3/11/7.2), 0 mismatched
NFA: 12 absorbed (len 1/1/1.0), 13 skipped (len 1/1/1.0)
-> Seq Scan on rpr_nfa_complex (actual rows=30.00 loops=1)
-(9 rows)
+(10 rows)
-- Consecutive increasing values (using PREV)
CREATE VIEW rpr_ev_real_increasing AS
@@ -2374,13 +2424,14 @@ WINDOW w AS (
WindowAgg (actual rows=50.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a{3,}"
+ Nav Mark Lookback: 1
Storage: Memory Maximum Storage: NkB
NFA States: 3 peak, 99 total, 0 merged
NFA Contexts: 2 peak, 51 total, 0 pruned
NFA: 1 matched (len 50/50/50.0), 0 mismatched
NFA: 49 absorbed (len 1/1/1.0), 0 skipped
-> Function Scan on generate_series s (actual rows=50.00 loops=1)
-(9 rows)
+(10 rows)
-- ============================================================
-- Performance-oriented Tests
@@ -2416,13 +2467,14 @@ WINDOW w AS (
WindowAgg (actual rows=1000.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a b
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 2 peak, 1001 total, 0 merged
NFA Contexts: 2 peak, 1001 total, 0 pruned
NFA: 500 matched (len 2/2/2.0), 0 mismatched
NFA: 0 absorbed, 500 skipped (len 1/1/1.0)
-> Function Scan on generate_series s (actual rows=1000.00 loops=1)
-(9 rows)
+(10 rows)
-- Large dataset with absorption
CREATE VIEW rpr_ev_perf_large_absorb AS
@@ -2455,13 +2507,14 @@ WINDOW w AS (
WindowAgg (actual rows=1000.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a+" b
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 3 peak, 1991 total, 0 merged
NFA Contexts: 2 peak, 1001 total, 0 pruned
NFA: 10 matched (len 100/100/100.0), 0 mismatched
NFA: 980 absorbed (len 1/1/1.0), 10 skipped (len 1/1/1.0)
-> Function Scan on generate_series s (actual rows=1000.00 loops=1)
-(9 rows)
+(10 rows)
-- High state merge ratio
CREATE VIEW rpr_ev_perf_high_merge AS
@@ -2494,13 +2547,14 @@ WINDOW w AS (
WindowAgg (actual rows=500.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a | b)+ c
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 9 peak, 3006 total, 0 merged
NFA Contexts: 3 peak, 501 total, 1 pruned
NFA: 166 matched (len 3/3/3.0), 1 mismatched (len 2/2/2.0)
NFA: 0 absorbed, 332 skipped (len 1/2/1.5)
-> Function Scan on generate_series s (actual rows=500.00 loops=1)
-(9 rows)
+(10 rows)
-- ============================================================
-- INITIAL vs no INITIAL comparison
@@ -2538,13 +2592,14 @@ WINDOW w AS (
WindowAgg (actual rows=50.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a+" b
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 3 peak, 91 total, 0 merged
NFA Contexts: 2 peak, 51 total, 0 pruned
NFA: 10 matched (len 5/5/5.0), 0 mismatched
NFA: 30 absorbed (len 1/1/1.0), 10 skipped (len 1/1/1.0)
-> Function Scan on generate_series s (actual rows=50.00 loops=1)
-(9 rows)
+(10 rows)
-- Without INITIAL keyword (same behavior currently)
CREATE VIEW rpr_ev_initial_without AS
@@ -2577,13 +2632,14 @@ WINDOW w AS (
WindowAgg (actual rows=50.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a+" b
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 3 peak, 91 total, 0 merged
NFA Contexts: 2 peak, 51 total, 0 pruned
NFA: 10 matched (len 5/5/5.0), 0 mismatched
NFA: 30 absorbed (len 1/1/1.0), 10 skipped (len 1/1/1.0)
-> Function Scan on generate_series s (actual rows=50.00 loops=1)
-(9 rows)
+(10 rows)
-- ============================================================
-- Quantifier Variations
@@ -2619,13 +2675,14 @@ WINDOW w AS (
WindowAgg (actual rows=40.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a+"
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 3 peak, 71 total, 0 merged
NFA Contexts: 3 peak, 41 total, 10 pruned
NFA: 10 matched (len 3/3/3.0), 0 mismatched
NFA: 20 absorbed (len 1/1/1.0), 0 skipped
-> Function Scan on generate_series s (actual rows=40.00 loops=1)
-(9 rows)
+(10 rows)
-- Star quantifier (zero or more)
CREATE VIEW rpr_ev_quant_star AS
@@ -2658,13 +2715,14 @@ WINDOW w AS (
WindowAgg (actual rows=40.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a*" b
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 4 peak, 102 total, 0 merged
NFA Contexts: 2 peak, 41 total, 10 pruned
NFA: 10 matched (len 3/3/3.0), 0 mismatched
NFA: 10 absorbed (len 1/1/1.0), 10 skipped (len 1/1/1.0)
-> Function Scan on generate_series s (actual rows=40.00 loops=1)
-(9 rows)
+(10 rows)
-- Question mark (zero or one)
CREATE VIEW rpr_ev_quant_question AS
@@ -2697,13 +2755,14 @@ WINDOW w AS (
WindowAgg (actual rows=40.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a? b c
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 4 peak, 82 total, 0 merged
NFA Contexts: 3 peak, 41 total, 10 pruned
NFA: 10 matched (len 3/3/3.0), 0 mismatched
NFA: 0 absorbed, 20 skipped (len 1/2/1.5)
-> Function Scan on generate_series s (actual rows=40.00 loops=1)
-(9 rows)
+(10 rows)
-- Exact count {n}
CREATE VIEW rpr_ev_quant_exact AS
@@ -2736,13 +2795,14 @@ WINDOW w AS (
WindowAgg (actual rows=50.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a{3} b
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 4 peak, 51 total, 0 merged
NFA Contexts: 5 peak, 51 total, 0 pruned
NFA: 10 matched (len 4/4/4.0), 10 mismatched (len 4/4/4.0)
NFA: 0 absorbed, 30 skipped (len 1/3/2.0)
-> Function Scan on generate_series s (actual rows=50.00 loops=1)
-(9 rows)
+(10 rows)
-- Range {n,m}
CREATE VIEW rpr_ev_quant_range AS
@@ -2775,13 +2835,14 @@ WINDOW w AS (
WindowAgg (actual rows=50.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a{2,4} b
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 7 peak, 101 total, 0 merged
NFA Contexts: 5 peak, 51 total, 0 pruned
NFA: 10 matched (len 5/5/5.0), 0 mismatched
NFA: 0 absorbed, 40 skipped (len 1/4/2.5)
-> Function Scan on generate_series s (actual rows=50.00 loops=1)
-(9 rows)
+(10 rows)
-- At least {n,}
CREATE VIEW rpr_ev_quant_atleast AS
@@ -2814,13 +2875,14 @@ WINDOW w AS (
WindowAgg (actual rows=50.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a{3,}" b
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 3 peak, 86 total, 0 merged
NFA Contexts: 2 peak, 51 total, 0 pruned
NFA: 5 matched (len 10/10/10.0), 0 mismatched
NFA: 40 absorbed (len 1/1/1.0), 5 skipped (len 1/1/1.0)
-> Function Scan on generate_series s (actual rows=50.00 loops=1)
-(9 rows)
+(10 rows)
-- ============================================================
-- Regression Tests for Statistics Accuracy
@@ -2857,13 +2919,14 @@ WINDOW w AS (
WindowAgg (actual rows=20.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a+" b
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 3 peak, 37 total, 0 merged
NFA Contexts: 2 peak, 21 total, 0 pruned
NFA: 4 matched (len 5/5/5.0), 0 mismatched
NFA: 12 absorbed (len 1/1/1.0), 4 skipped (len 1/1/1.0)
-> Function Scan on generate_series s (actual rows=20.00 loops=1)
-(9 rows)
+(10 rows)
-- Verify context count with known absorption
CREATE VIEW rpr_ev_reg_ctx_absorb AS
@@ -2896,13 +2959,14 @@ WINDOW w AS (
WindowAgg (actual rows=30.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a+" b c
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 3 peak, 52 total, 0 merged
NFA Contexts: 3 peak, 31 total, 6 pruned
NFA: 3 matched (len 9/9/9.0), 0 mismatched
NFA: 18 absorbed (len 1/1/1.0), 3 skipped (len 1/1/1.0)
-> Function Scan on generate_series s (actual rows=30.00 loops=1)
-(9 rows)
+(10 rows)
-- Verify match length with fixed-length pattern
CREATE VIEW rpr_ev_reg_matchlen AS
@@ -2935,13 +2999,14 @@ WINDOW w AS (
WindowAgg (actual rows=30.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a b c
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 2 peak, 31 total, 0 merged
NFA Contexts: 3 peak, 31 total, 10 pruned
NFA: 10 matched (len 3/3/3.0), 0 mismatched
NFA: 0 absorbed, 10 skipped (len 1/1/1.0)
-> Function Scan on generate_series s (actual rows=30.00 loops=1)
-(9 rows)
+(10 rows)
-- ============================================================
-- Alternation Pattern Tests
@@ -2977,13 +3042,14 @@ WINDOW w AS (
WindowAgg (actual rows=100.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a | b) c
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 4 peak, 303 total, 0 merged
NFA Contexts: 3 peak, 101 total, 40 pruned
NFA: 20 matched (len 2/2/2.0), 20 mismatched (len 2/2/2.0)
NFA: 0 absorbed, 20 skipped (len 1/1/1.0)
-> Seq Scan on rpr_nfa_test (actual rows=100.00 loops=1)
-(9 rows)
+(10 rows)
-- Multiple items in alternation
CREATE VIEW rpr_ev_alt_multi_item AS
@@ -3020,13 +3086,14 @@ WINDOW w AS (
WindowAgg (actual rows=100.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a | b | c | d) e
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 6 peak, 505 total, 0 merged
NFA Contexts: 3 peak, 101 total, 0 pruned
NFA: 20 matched (len 2/2/2.0), 60 mismatched (len 2/2/2.0)
NFA: 0 absorbed, 20 skipped (len 1/1/1.0)
-> Seq Scan on rpr_nfa_test (actual rows=100.00 loops=1)
-(9 rows)
+(10 rows)
-- Alternation with quantifiers
CREATE VIEW rpr_ev_alt_with_quant AS
@@ -3059,13 +3126,14 @@ WINDOW w AS (
WindowAgg (actual rows=50.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a | b)+ c
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 9 peak, 306 total, 0 merged
NFA Contexts: 3 peak, 51 total, 1 pruned
NFA: 16 matched (len 3/3/3.0), 1 mismatched (len 2/2/2.0)
NFA: 0 absorbed, 32 skipped (len 1/2/1.5)
-> Function Scan on generate_series s (actual rows=50.00 loops=1)
-(9 rows)
+(10 rows)
-- Multiple alternatives (4+)
CREATE VIEW rpr_ev_alt_four_plus AS
@@ -3096,12 +3164,13 @@ WINDOW w AS (
WindowAgg (actual rows=100.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a | b | c | d | e)
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 7 peak, 606 total, 0 merged
NFA Contexts: 2 peak, 101 total, 0 pruned
NFA: 100 matched (len 1/1/1.0), 0 mismatched
-> Function Scan on generate_series s (actual rows=100.00 loops=1)
-(8 rows)
+(9 rows)
-- Alternation at start
CREATE VIEW rpr_ev_alt_at_start AS
@@ -3132,13 +3201,14 @@ WINDOW w AS (
WindowAgg (actual rows=60.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a | b) c d
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 4 peak, 183 total, 0 merged
NFA Contexts: 3 peak, 61 total, 16 pruned
NFA: 15 matched (len 3/3/3.0), 14 mismatched (len 2/2/2.0)
NFA: 0 absorbed, 15 skipped (len 1/1/1.0)
-> Function Scan on generate_series s (actual rows=60.00 loops=1)
-(9 rows)
+(10 rows)
-- Multiple sequential alternations
CREATE VIEW rpr_ev_alt_sequential AS
@@ -3169,12 +3239,13 @@ WINDOW w AS (
WindowAgg (actual rows=100.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a | b) c (d | e) f
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 5 peak, 337 total, 0 merged
NFA Contexts: 3 peak, 101 total, 67 pruned
NFA: 0 matched, 33 mismatched (len 2/4/3.0)
-> Function Scan on generate_series s (actual rows=100.00 loops=1)
-(8 rows)
+(9 rows)
-- Quantified alternatives
CREATE VIEW rpr_ev_alt_quantified AS
@@ -3205,13 +3276,14 @@ WINDOW w AS (
WindowAgg (actual rows=60.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a+" | b+") c
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 5 peak, 223 total, 0 merged
NFA Contexts: 3 peak, 61 total, 1 pruned
NFA: 20 matched (len 2/2/2.0), 19 mismatched (len 2/2/2.0)
NFA: 0 absorbed, 20 skipped (len 1/1/1.0)
-> Function Scan on generate_series s (actual rows=60.00 loops=1)
-(9 rows)
+(10 rows)
-- Alternation at end
CREATE VIEW rpr_ev_alt_at_end AS
@@ -3242,13 +3314,14 @@ WINDOW w AS (
WindowAgg (actual rows=60.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a b (c | d)
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 3 peak, 89 total, 0 merged
NFA Contexts: 3 peak, 61 total, 32 pruned
NFA: 14 matched (len 3/3/3.0), 0 mismatched
NFA: 0 absorbed, 14 skipped (len 1/1/1.0)
-> Function Scan on generate_series s (actual rows=60.00 loops=1)
-(9 rows)
+(10 rows)
-- Nested ALT at start of branch inside outer ALT
-- Pattern: (A ((B | C) D | E)) - preceding VAR + inner ALT as first branch element
@@ -3280,12 +3353,13 @@ WINDOW w AS (
WindowAgg (actual rows=20.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a ((b | c) d | e)
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 4 peak, 37 total, 0 merged
NFA Contexts: 3 peak, 21 total, 17 pruned
NFA: 0 matched, 3 mismatched (len 3/3/3.0)
-> Function Scan on generate_series s (actual rows=20.00 loops=1)
-(8 rows)
+(9 rows)
-- Nested ALT at end of branch inside outer ALT
-- Pattern: (C (A | B) | D) - inner ALT is last element in outer branch
@@ -3317,12 +3391,13 @@ WINDOW w AS (
WindowAgg (actual rows=20.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (c (a | b) | d)
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 5 peak, 73 total, 0 merged
NFA Contexts: 3 peak, 21 total, 10 pruned
NFA: 5 matched (len 1/1/1.0), 5 mismatched (len 2/2/2.0)
-> Function Scan on generate_series s (actual rows=20.00 loops=1)
-(8 rows)
+(9 rows)
-- ============================================================
-- Group Pattern Tests
@@ -3358,13 +3433,14 @@ WINDOW w AS (
WindowAgg (actual rows=40.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a' b')+"
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 4 peak, 61 total, 0 merged
NFA Contexts: 3 peak, 41 total, 0 pruned
NFA: 1 matched (len 40/40/40.0), 0 mismatched
NFA: 19 absorbed (len 2/2/2.0), 20 skipped (len 1/1/1.0)
-> Function Scan on generate_series s (actual rows=40.00 loops=1)
-(9 rows)
+(10 rows)
-- Group with bounded quantifier
CREATE VIEW rpr_ev_grp_bounded AS
@@ -3397,13 +3473,14 @@ WINDOW w AS (
WindowAgg (actual rows=40.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a b){2,4}
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 4 peak, 51 total, 0 merged
NFA Contexts: 3 peak, 41 total, 5 pruned
NFA: 5 matched (len 8/8/8.0), 0 mismatched
NFA: 0 absorbed, 30 skipped (len 1/2/1.5)
-> Function Scan on generate_series s (actual rows=40.00 loops=1)
-(9 rows)
+(10 rows)
-- Nested groups
CREATE VIEW rpr_ev_grp_nested AS
@@ -3436,13 +3513,14 @@ WINDOW w AS (
WindowAgg (actual rows=60.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: ((a' b'){2}')+"
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 5 peak, 76 total, 0 merged
NFA Contexts: 4 peak, 61 total, 15 pruned
NFA: 1 matched (len 60/60/60.0), 0 mismatched
NFA: 14 absorbed (len 4/4/4.0), 30 skipped (len 1/2/1.5)
-> Function Scan on generate_series s (actual rows=60.00 loops=1)
-(9 rows)
+(10 rows)
-- Deep nesting (3+ levels)
CREATE VIEW rpr_ev_grp_deep AS
@@ -3473,13 +3551,14 @@ WINDOW w AS (
WindowAgg (actual rows=40.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a | b)+
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 6 peak, 243 total, 0 merged
NFA Contexts: 2 peak, 41 total, 0 pruned
NFA: 1 matched (len 40/40/40.0), 0 mismatched
NFA: 0 absorbed, 39 skipped (len 1/1/1.0)
-> Function Scan on generate_series s (actual rows=40.00 loops=1)
-(9 rows)
+(10 rows)
-- Bounded quantifier on alternation
CREATE VIEW rpr_ev_grp_bounded_alt AS
@@ -3510,13 +3589,14 @@ WINDOW w AS (
WindowAgg (actual rows=60.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a | b){2,3} c
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 8 peak, 320 total, 0 merged
NFA Contexts: 3 peak, 61 total, 2 pruned
NFA: 19 matched (len 3/3/3.0), 1 mismatched (len 2/2/2.0)
NFA: 0 absorbed, 38 skipped (len 1/2/1.5)
-> Function Scan on generate_series s (actual rows=60.00 loops=1)
-(9 rows)
+(10 rows)
-- Nested groups with quantifiers
CREATE VIEW rpr_ev_grp_nested_quant AS
@@ -3547,13 +3627,14 @@ WINDOW w AS (
WindowAgg (actual rows=60.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: ((a' b')+" c)*
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 9 peak, 178 total, 0 merged
NFA Contexts: 4 peak, 61 total, 20 pruned
NFA: 3 matched (len 0/57/19.0), 0 mismatched
NFA: 0 absorbed, 37 skipped (len 1/3/2.0)
-> Function Scan on generate_series s (actual rows=60.00 loops=1)
-(9 rows)
+(10 rows)
-- Partial nested quantification
CREATE VIEW rpr_ev_grp_partial_quant AS
@@ -3584,13 +3665,14 @@ WINDOW w AS (
WindowAgg (actual rows=60.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a (b c)+)*
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 7 peak, 160 total, 0 merged
NFA Contexts: 4 peak, 61 total, 20 pruned
NFA: 3 matched (len 0/57/19.0), 0 mismatched
NFA: 0 absorbed, 37 skipped (len 1/3/2.0)
-> Function Scan on generate_series s (actual rows=60.00 loops=1)
-(9 rows)
+(10 rows)
-- ============================================================
-- Window Function Combinations
@@ -3626,13 +3708,14 @@ WINDOW w AS (
WindowAgg (actual rows=30.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a+" b
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 3 peak, 55 total, 0 merged
NFA Contexts: 2 peak, 31 total, 0 pruned
NFA: 6 matched (len 5/5/5.0), 0 mismatched
NFA: 18 absorbed (len 1/1/1.0), 6 skipped (len 1/1/1.0)
-> Function Scan on generate_series s (actual rows=30.00 loops=1)
-(9 rows)
+(10 rows)
-- first_value with pattern
CREATE VIEW rpr_ev_wfn_first_value AS
@@ -3665,13 +3748,14 @@ WINDOW w AS (
WindowAgg (actual rows=30.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a+" b
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 3 peak, 55 total, 0 merged
NFA Contexts: 2 peak, 31 total, 0 pruned
NFA: 6 matched (len 5/5/5.0), 0 mismatched
NFA: 18 absorbed (len 1/1/1.0), 6 skipped (len 1/1/1.0)
-> Function Scan on generate_series s (actual rows=30.00 loops=1)
-(9 rows)
+(10 rows)
-- last_value with pattern
CREATE VIEW rpr_ev_wfn_last_value AS
@@ -3704,13 +3788,14 @@ WINDOW w AS (
WindowAgg (actual rows=30.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a+" b
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 3 peak, 55 total, 0 merged
NFA Contexts: 2 peak, 31 total, 0 pruned
NFA: 6 matched (len 5/5/5.0), 0 mismatched
NFA: 18 absorbed (len 1/1/1.0), 6 skipped (len 1/1/1.0)
-> Function Scan on generate_series s (actual rows=30.00 loops=1)
-(9 rows)
+(10 rows)
-- Multiple window functions
CREATE VIEW rpr_ev_wfn_multi AS
@@ -3749,13 +3834,14 @@ WINDOW w AS (
WindowAgg (actual rows=30.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a+" b
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 3 peak, 55 total, 0 merged
NFA Contexts: 2 peak, 31 total, 0 pruned
NFA: 6 matched (len 5/5/5.0), 0 mismatched
NFA: 18 absorbed (len 1/1/1.0), 6 skipped (len 1/1/1.0)
-> Function Scan on generate_series s (actual rows=30.00 loops=1)
-(9 rows)
+(10 rows)
-- ============================================================
-- DEFINE Expression Variations
@@ -3795,13 +3881,14 @@ WINDOW w AS (
WindowAgg (actual rows=50.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a+" b
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 3 peak, 78 total, 0 merged
NFA Contexts: 2 peak, 51 total, 6 pruned
NFA: 17 matched (len 2/3/2.6), 0 mismatched
NFA: 10 absorbed (len 1/1/1.0), 17 skipped (len 1/1/1.0)
-> Function Scan on generate_series s (actual rows=50.00 loops=1)
-(9 rows)
+(10 rows)
-- Using PREV function
CREATE VIEW rpr_ev_def_prev AS
@@ -3840,12 +3927,13 @@ WINDOW w AS (
WindowAgg (actual rows=30.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: s u+ d+
+ Nav Mark Lookback: 1
Storage: Memory Maximum Storage: NkB
NFA States: 60 peak, 466 total, 0 merged
NFA Contexts: 31 peak, 31 total, 1 pruned
NFA: 0 matched, 29 mismatched (len 2/30/16.0)
-> Function Scan on generate_series s (actual rows=30.00 loops=1)
-(8 rows)
+(9 rows)
-- Using 1-arg PREV (implicit offset 1)
CREATE VIEW rpr_ev_nav_prev1 AS
@@ -3964,13 +4052,14 @@ WINDOW w AS (
WindowAgg (actual rows=30.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a+" b
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 3 peak, 55 total, 0 merged
NFA Contexts: 2 peak, 31 total, 0 pruned
NFA: 6 matched (len 5/5/5.0), 0 mismatched
NFA: 18 absorbed (len 1/1/1.0), 6 skipped (len 1/1/1.0)
-> Function Scan on generate_series v (actual rows=30.00 loops=1)
-(9 rows)
+(10 rows)
-- ============================================================
-- Large Scale Statistics Verification
@@ -4006,13 +4095,14 @@ WINDOW w AS (
WindowAgg (actual rows=500.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a+" b c
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 3 peak, 851 total, 0 merged
NFA Contexts: 3 peak, 501 total, 101 pruned
NFA: 50 matched (len 8/9/9.0), 0 mismatched
NFA: 299 absorbed (len 1/1/1.0), 50 skipped (len 1/1/1.0)
-> Function Scan on generate_series s (actual rows=500.00 loops=1)
-(9 rows)
+(10 rows)
-- High match count scenario
CREATE VIEW rpr_ev_scale_high_match AS
@@ -4045,13 +4135,14 @@ WINDOW w AS (
WindowAgg (actual rows=500.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a b
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 2 peak, 501 total, 0 merged
NFA Contexts: 2 peak, 501 total, 0 pruned
NFA: 250 matched (len 2/2/2.0), 0 mismatched
NFA: 0 absorbed, 250 skipped (len 1/1/1.0)
-> Function Scan on generate_series s (actual rows=500.00 loops=1)
-(9 rows)
+(10 rows)
-- High skip count scenario
CREATE VIEW rpr_ev_scale_high_skip AS
@@ -4094,13 +4185,14 @@ WINDOW w AS (
WindowAgg (actual rows=500.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a b c d e
+ Nav Mark Lookback: 0
Storage: Memory Maximum Storage: NkB
NFA States: 2 peak, 501 total, 0 merged
NFA Contexts: 3 peak, 501 total, 490 pruned
NFA: 5 matched (len 5/5/5.0), 0 mismatched
NFA: 0 absorbed, 5 skipped (len 1/1/1.0)
-> Function Scan on generate_series s (actual rows=500.00 loops=1)
-(9 rows)
+(10 rows)
--
-- Planner optimization: optimize_window_clauses must not alter RPR frame
@@ -4149,10 +4241,11 @@ EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev_opt_with_rpr;
-> WindowAgg
Window: w AS (ORDER BY s.v ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a b+
+ Nav Mark Lookback: 1
-> Sort
Sort Key: s.v
-> Function Scan on generate_series s
-(7 rows)
+(8 rows)
--
-- Planner optimization: non-RPR and RPR windows that share the same base frame
@@ -4178,12 +4271,13 @@ EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev_opt_mixed;
-> WindowAgg
Window: w_rpr AS (ORDER BY s.v ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a+"
+ Nav Mark Lookback: 0
-> WindowAgg
Window: w_normal AS (ORDER BY s.v ROWS UNBOUNDED PRECEDING)
-> Sort
Sort Key: s.v
-> Function Scan on generate_series s
-(9 rows)
+(10 rows)
--
-- Planner optimization: find_window_run_conditions must not push down
@@ -4242,8 +4336,133 @@ SELECT * FROM (
-> WindowAgg
Window: w AS (ORDER BY s.v ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a b+
+ Nav Mark Lookback: 1
-> Sort
Sort Key: s.v
-> Function Scan on generate_series s
-(8 rows)
+(9 rows)
+
+-- ============================================================
+-- Nav Mark Lookback Tests
+-- Verifies planner-computed navigation offset for tuplestore trim.
+-- Lookback: how far back from currentpos (PREV/LAST).
+-- ============================================================
+-- Prepare statement for host variable offset test below
+PREPARE rpr_nav_offset_prep(int8) AS
+SELECT count(*) OVER w
+FROM generate_series(1,10) s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS v > PREV(v, $1)
+);
+-- No navigation function: offset 0
+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 > 0
+);
+ QUERY PLAN
+-------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a+"
+ Nav Mark Lookback: 0
+ -> Function Scan on generate_series s
+(5 rows)
+
+-- NEXT only: no backward navigation, offset 0
+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 < NEXT(v)
+);
+ QUERY PLAN
+-------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a+"
+ Nav Mark Lookback: 0
+ -> Function Scan on generate_series s
+(5 rows)
+
+-- PREV(v): implicit offset 1
+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 > PREV(v)
+);
+ QUERY PLAN
+-------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a+"
+ Nav Mark Lookback: 1
+ -> Function Scan on generate_series s
+(5 rows)
+
+-- PREV(v, 3): explicit constant offset 3
+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 > PREV(v, 3)
+);
+ QUERY PLAN
+-------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a+"
+ Nav Mark Lookback: 3
+ -> Function Scan on generate_series s
+(5 rows)
+
+-- Two PREV with different offsets: max(1, 5) = 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 PREV(v, 1) < v AND PREV(v, 5) < v
+);
+ QUERY PLAN
+-------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a+"
+ Nav Mark Lookback: 5
+ -> Function Scan on generate_series s
+(5 rows)
+
+-- Host variable offset: custom plan resolves $1=2 to constant 2
+EXPLAIN (COSTS OFF) EXECUTE rpr_nav_offset_prep(2);
+ QUERY PLAN
+-------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a+"
+ Nav Mark Lookback: 2
+ -> Function Scan on generate_series s
+(5 rows)
+
+-- Force generic plan: offset becomes "runtime" (Param node)
+SET plan_cache_mode = force_generic_plan;
+EXPLAIN (COSTS OFF) EXECUTE rpr_nav_offset_prep(2);
+ 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)
+RESET plan_cache_mode;
+DEALLOCATE rpr_nav_offset_prep;
diff --git a/src/test/regress/expected/rpr_integration.out b/src/test/regress/expected/rpr_integration.out
index a21ac5a8588..20c9fe89cec 100644
--- a/src/test/regress/expected/rpr_integration.out
+++ b/src/test/regress/expected/rpr_integration.out
@@ -66,10 +66,11 @@ WINDOW w AS (ORDER BY id
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a b+
+ Nav Mark Lookback: 1
-> Sort
Sort Key: id
-> Seq Scan on rpr_integ
-(6 rows)
+(7 rows)
-- ============================================================
-- A2. Run condition pushdown bypass
@@ -112,10 +113,11 @@ SELECT * FROM (
-> WindowAgg
Window: w AS (ORDER BY rpr_integ.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a b+
+ Nav Mark Lookback: 1
-> Sort
Sort Key: rpr_integ.id
-> Seq Scan on rpr_integ
-(8 rows)
+(9 rows)
-- Verify results are correct
SELECT * FROM (
@@ -160,10 +162,11 @@ WINDOW
-> WindowAgg
Window: w_rpr AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a b+
+ Nav Mark Lookback: 1
-> Sort
Sort Key: id
-> Seq Scan on rpr_integ
-(8 rows)
+(9 rows)
-- Verify both produce different results
SELECT
@@ -218,13 +221,15 @@ WINDOW
WindowAgg
Window: w2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a b+
+ Nav Mark Lookback: 1
-> WindowAgg
Window: w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a b+
+ Nav Mark Lookback: 1
-> Sort
Sort Key: id
-> Seq Scan on rpr_integ
-(9 rows)
+(11 rows)
-- Verify results differ
SELECT
@@ -383,10 +388,11 @@ WHERE cnt > 0;
-> WindowAgg
Window: w AS (ORDER BY rpr_integ.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a b+
+ Nav Mark Lookback: 1
-> Sort
Sort Key: rpr_integ.id
-> Seq Scan on rpr_integ
-(8 rows)
+(9 rows)
-- ============================================================
-- A9. DEFINE expression non-propagation
@@ -415,12 +421,13 @@ WINDOW
Output: id, val, count(*) OVER w_rpr
Window: w_rpr AS (ORDER BY rpr_integ.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a b+
+ Nav Mark Lookback: 1
-> Sort
Output: id, val
Sort Key: rpr_integ.id
-> Seq Scan on public.rpr_integ
Output: id, val
-(12 rows)
+(13 rows)
-- ============================================================
-- A10. RPR + LIMIT
@@ -712,12 +719,13 @@ WINDOW w AS (ORDER BY id
WindowAgg
Window: w AS (ORDER BY rpr_part.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a b+
+ Nav Mark Lookback: 1
-> Sort
Sort Key: rpr_part.id
-> Append
-> Seq Scan on rpr_part_1
-> Seq Scan on rpr_part_2
-(8 rows)
+(9 rows)
DROP TABLE rpr_part;
-- ============================================================
@@ -798,8 +806,9 @@ WINDOW w AS (ORDER BY id
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: a b+
+ Nav Mark Lookback: 1
-> Index Scan using rpr_integ_id_idx on rpr_integ
-(4 rows)
+(5 rows)
SELECT id, val, count(*) OVER w AS cnt
FROM rpr_integ
diff --git a/src/test/regress/sql/rpr_explain.sql b/src/test/regress/sql/rpr_explain.sql
index e339edd7e91..5082cc2b5de 100644
--- a/src/test/regress/sql/rpr_explain.sql
+++ b/src/test/regress/sql/rpr_explain.sql
@@ -36,6 +36,7 @@
-- Window Function Combinations
-- DEFINE Expression Variations
-- Large Scale Statistics Verification
+-- Nav Mark Lookback (tuplestore trim)
-- ============================================================
-- Filter function to normalize platform-dependent memory values (not NFA statistics).
@@ -2476,3 +2477,73 @@ SELECT * FROM (
)
) t WHERE cnt > 0;
+-- ============================================================
+-- Nav Mark Lookback Tests
+-- Verifies planner-computed navigation offset for tuplestore trim.
+-- Lookback: how far back from currentpos (PREV/LAST).
+-- ============================================================
+
+-- Prepare statement for host variable offset test below
+PREPARE rpr_nav_offset_prep(int8) AS
+SELECT count(*) OVER w
+FROM generate_series(1,10) s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS v > PREV(v, $1)
+);
+
+-- No navigation function: offset 0
+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 > 0
+);
+
+-- NEXT only: no backward navigation, offset 0
+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 < NEXT(v)
+);
+
+-- PREV(v): implicit offset 1
+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 > PREV(v)
+);
+
+-- PREV(v, 3): explicit constant offset 3
+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 > PREV(v, 3)
+);
+
+-- Two PREV with different offsets: max(1, 5) = 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 PREV(v, 1) < v AND PREV(v, 5) < v
+);
+
+-- Host variable offset: custom plan resolves $1=2 to constant 2
+EXPLAIN (COSTS OFF) EXECUTE rpr_nav_offset_prep(2);
+
+-- Force generic plan: offset becomes "runtime" (Param node)
+SET plan_cache_mode = force_generic_plan;
+EXPLAIN (COSTS OFF) EXECUTE rpr_nav_offset_prep(2);
+RESET plan_cache_mode;
+DEALLOCATE rpr_nav_offset_prep;
+
--
2.50.1 (Apple Git-155)
From e5c7033b71a8c0302fb3fa48790157f6cd70585e Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Sun, 5 Apr 2026 00:56:20 +0900
Subject: [PATCH] Implement FIRST/LAST and compound navigation for RPR
Add FIRST(expr [, offset]) and LAST(expr [, offset]) navigation
functions for DEFINE clauses. FIRST references the match start row,
LAST references the current row. Includes slot swap elision,
per-context DEFINE re-evaluation for match_start-dependent variables,
and planner-level absorption disabling when FIRST or LAST-with-offset
is present.
Add compound navigation: PREV(FIRST()), NEXT(FIRST()),
PREV(LAST()), NEXT(LAST()) per SQL standard 5.6.4. The parser
flattens nested RPRNavExpr into a single compound node with two
offsets. The executor computes the target position in two steps
with range validation at each step. ruleutils restores the nested
syntax for deparsing.
Extend tuplestore trim to handle FIRST navigation via
hasFirstNav/navFirstOffset, allowing mark advance based on
oldest active context's matchStartRow. Compound offsets are
computed by the unified nav_offset_walker in a single expression
tree walk. Add "Nav Mark Lookahead" to EXPLAIN for FIRST-based
navigation.
---
doc/src/sgml/func/func-window.sgml | 53 ++
src/backend/commands/explain.c | 68 ++-
src/backend/executor/execExpr.c | 55 +-
src/backend/executor/execExprInterp.c | 155 ++++-
src/backend/executor/execRPR.c | 152 ++++-
src/backend/executor/nodeWindowAgg.c | 343 +++++++++---
src/backend/nodes/nodeFuncs.c | 3 +
src/backend/optimizer/plan/createplan.c | 387 +++++++++++--
src/backend/optimizer/plan/rpr.c | 6 +-
src/backend/parser/parse_func.c | 70 ++-
src/backend/parser/parse_rpr.c | 112 +++-
src/backend/utils/adt/ruleutils.c | 81 ++-
src/backend/utils/adt/windowfuncs.c | 56 ++
src/include/catalog/pg_proc.dat | 12 +
src/include/executor/execExpr.h | 8 +-
src/include/nodes/execnodes.h | 12 +-
src/include/nodes/parsenodes.h | 17 +-
src/include/nodes/plannodes.h | 30 +-
src/include/nodes/primnodes.h | 37 +-
src/include/optimizer/rpr.h | 12 +-
src/test/regress/expected/rpr.out | 652 +++++++++++++++++++++-
src/test/regress/expected/rpr_base.out | 257 ++++++++-
src/test/regress/expected/rpr_explain.out | 425 +++++++++++++-
src/test/regress/sql/rpr.sql | 374 +++++++++++++
src/test/regress/sql/rpr_base.sql | 136 ++++-
src/test/regress/sql/rpr_explain.sql | 242 +++++++-
src/tools/pgindent/typedefs.list | 5 +
27 files changed, 3509 insertions(+), 251 deletions(-)
diff --git a/doc/src/sgml/func/func-window.sgml b/doc/src/sgml/func/func-window.sgml
index 1b9b993a817..ab80690f7be 100644
--- a/doc/src/sgml/func/func-window.sgml
+++ b/doc/src/sgml/func/func-window.sgml
@@ -337,10 +337,63 @@
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>first</primary>
+ </indexterm>
+ <function>first</function> ( <parameter>value</parameter> <type>anyelement</type> [, <parameter>offset</parameter> <type>bigint</type> ] )
+ <returnvalue>anyelement</returnvalue>
+ </para>
+ <para>
+ Returns the column value at the row <parameter>offset</parameter>
+ rows after the match start row;
+ returns NULL if the target row is beyond the current row.
+ <parameter>offset</parameter> defaults to 0 if omitted, referring to the
+ match start row itself.
+ <parameter>offset</parameter> must be a non-negative integer.
+ <parameter>offset</parameter> must not be NULL.
+ Can only be used in a <literal>DEFINE</literal> clause.
+ </para></entry>
+ </row>
+
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>last</primary>
+ </indexterm>
+ <function>last</function> ( <parameter>value</parameter> <type>anyelement</type> [, <parameter>offset</parameter> <type>bigint</type> ] )
+ <returnvalue>anyelement</returnvalue>
+ </para>
+ <para>
+ Returns the column value at the row <parameter>offset</parameter>
+ rows before the current row within the match;
+ returns NULL if the target row is before the match start row.
+ <parameter>offset</parameter> defaults to 0 if omitted, referring to the
+ current row itself.
+ <parameter>offset</parameter> must be a non-negative integer.
+ <parameter>offset</parameter> must not be NULL.
+ Can only be used in a <literal>DEFINE</literal> clause.
+ </para></entry>
+ </row>
+
</tbody>
</tgroup>
</table>
+ <para>
+ <function>PREV</function> and <function>NEXT</function> may wrap
+ <function>FIRST</function> or <function>LAST</function> for compound
+ navigation. For example,
+ <literal>PREV(FIRST(val, 2), 3)</literal> fetches the value at
+ 3 rows before the row that is 2 rows after the match start.
+ The reverse nesting (<function>FIRST</function>/<function>LAST</function>
+ wrapping <function>PREV</function>/<function>NEXT</function>) is not
+ permitted. Same-category nesting (e.g.,
+ <function>PREV</function> inside <function>PREV</function>) is also
+ prohibited.
+ </para>
+
<note>
<para>
The SQL standard defines a <literal>FROM FIRST</literal> or <literal>FROM LAST</literal>
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 1848de9de7a..221d9a49e0d 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -3255,14 +3255,66 @@ show_window_def(WindowAggState *planstate, List *ancestors, ExplainState *es)
pfree(patternStr);
}
- /* Show navigation offsets for tuplestore trim */
- if (wagg->navMaxOffset == RPR_NAV_OFFSET_RETAIN_ALL)
- ExplainPropertyText("Nav Mark Lookback", "retain all", es);
- else if (wagg->navMaxOffset == RPR_NAV_OFFSET_NEEDS_EVAL)
- ExplainPropertyText("Nav Mark Lookback", "runtime", es);
- else
- ExplainPropertyInteger("Nav Mark Lookback", NULL,
- wagg->navMaxOffset, es);
+ /*
+ * 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).
+ */
+ {
+ RPRNavOffsetKind maxKind = wagg->navMaxOffsetKind;
+ int64 maxOffset = wagg->navMaxOffset;
+ RPRNavOffsetKind firstKind = wagg->navFirstOffsetKind;
+ int64 firstOffset = wagg->navFirstOffset;
+
+ if (es->analyze)
+ {
+ maxKind = planstate->navMaxOffsetKind;
+ maxOffset = planstate->navMaxOffset;
+ firstKind = planstate->navFirstOffsetKind;
+ firstOffset = planstate->navFirstOffset;
+ }
+
+ switch (maxKind)
+ {
+ case RPR_NAV_OFFSET_NEEDS_EVAL:
+ ExplainPropertyText("Nav Mark Lookback", "runtime", es);
+ break;
+ 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;
+ }
+
+ if (wagg->hasFirstNav)
+ {
+ switch (firstKind)
+ {
+ case RPR_NAV_OFFSET_NEEDS_EVAL:
+ ExplainPropertyText("Nav Mark Lookahead", "runtime",
+ es);
+ break;
+ case RPR_NAV_OFFSET_RETAIN_ALL:
+ ExplainPropertyText("Nav Mark Lookahead", "retain all",
+ es);
+ break;
+ case RPR_NAV_OFFSET_FIXED:
+ ExplainPropertyInteger("Nav Mark Lookahead", NULL,
+ firstOffset, es);
+ break;
+ default:
+ elog(ERROR, "unrecognized RPR nav offset kind: %d",
+ firstKind);
+ break;
+ }
+ }
+ }
}
}
diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c
index dbed4f48a0f..6349a564a98 100644
--- a/src/backend/executor/execExpr.c
+++ b/src/backend/executor/execExpr.c
@@ -1225,12 +1225,16 @@ ExecInitExprRec(Expr *node, ExprState *state,
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.
+ * RPR navigation functions (PREV/NEXT/FIRST/LAST) 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.
+ *
+ * Default offset when offset_arg is NULL: PREV/NEXT: 1
+ * (physical offset from currentpos) FIRST/LAST: 0 (logical
+ * offset from match boundary)
*/
RPRNavExpr *nav = (RPRNavExpr *) node;
WindowAggState *winstate;
@@ -1243,25 +1247,52 @@ ExecInitExprRec(Expr *node, ExprState *state,
scratch.d.rpr_nav.winstate = winstate;
scratch.d.rpr_nav.kind = nav->kind;
- if (nav->offset_arg != NULL)
+ if (nav->kind >= RPR_NAV_PREV_FIRST)
{
/*
- * Allocate storage for the runtime offset value. The
- * offset expression is compiled below so it runs before
- * EEOP_RPR_NAV_SET.
+ * 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;
+ }
+
+ /* 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
+ {
+ offset_values[1] = Int64GetDatum(1);
+ offset_isnulls[1] = false;
+ }
+
+ 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);
- /* 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
{
+ /* Simple navigation with default offset */
scratch.d.rpr_nav.offset_value = NULL;
scratch.d.rpr_nav.offset_isnull = NULL;
}
diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c
index e41faa95be3..2ec579732cc 100644
--- a/src/backend/executor/execExprInterp.c
+++ b/src/backend/executor/execExprInterp.c
@@ -5942,7 +5942,35 @@ ExecAggPlainTransByRef(AggState *aggstate, AggStatePerTrans pertrans,
}
/*
- * Evaluate RPR PREV/NEXT navigation: swap slot to target row.
+ * 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.
*
* Saves the current outertuple into winstate for later restore, computes
* the target row position, fetches the corresponding slot from the
@@ -5963,27 +5991,35 @@ ExecEvalRPRNavSet(ExprState *state, ExprEvalStep *op, ExprContext *econtext)
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).
+ * 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("PREV/NEXT offset must not be null")));
+ errmsg("row pattern navigation 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")));
+ errmsg("row pattern navigation offset must not be negative")));
}
else
- offset = 1;
+ {
+ /* 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;
+ }
/*
* Calculate target position based on navigation direction. On overflow,
@@ -5999,8 +6035,107 @@ ExecEvalRPRNavSet(ExprState *state, ExprEvalStep *op, ExprContext *econtext)
if (pg_add_s64_overflow(winstate->currentpos, offset, &target_pos))
target_pos = -1;
break;
+ case RPR_NAV_FIRST:
+ /* FIRST: offset from match_start, clamped to currentpos */
+ if (pg_add_s64_overflow(winstate->nav_match_start, offset, &target_pos))
+ target_pos = -1;
+ else if (target_pos > winstate->currentpos)
+ target_pos = -1; /* beyond current match range */
+ break;
+ case RPR_NAV_LAST:
+ /* LAST: offset backward from currentpos, clamped to match_start */
+ if (pg_sub_s64_overflow(winstate->currentpos, offset, &target_pos))
+ target_pos = -1;
+ else if (target_pos < winstate->nav_match_start)
+ target_pos = -1; /* before match_start */
+ break;
+
+ case RPR_NAV_PREV_FIRST:
+ case RPR_NAV_NEXT_FIRST:
+ {
+ int64 compound_offset;
+ int64 inner_pos;
+
+ /* Inner: match_start + offset */
+ if (pg_add_s64_overflow(winstate->nav_match_start, offset, &inner_pos))
+ {
+ target_pos = -1;
+ break;
+ }
+ if (inner_pos > winstate->currentpos || inner_pos < 0)
+ {
+ target_pos = -1;
+ 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 (pg_sub_s64_overflow(inner_pos, compound_offset, &target_pos))
+ target_pos = -1;
+ }
+ else
+ {
+ if (pg_add_s64_overflow(inner_pos, compound_offset, &target_pos))
+ target_pos = -1;
+ }
+ }
+ break;
+
+ case RPR_NAV_PREV_LAST:
+ case RPR_NAV_NEXT_LAST:
+ {
+ int64 compound_offset;
+ int64 inner_pos;
+
+ /* Inner: currentpos - offset */
+ if (pg_sub_s64_overflow(winstate->currentpos, offset, &inner_pos))
+ {
+ target_pos = -1;
+ break;
+ }
+ if (inner_pos < winstate->nav_match_start)
+ {
+ target_pos = -1;
+ 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 (pg_sub_s64_overflow(inner_pos, compound_offset, &target_pos))
+ target_pos = -1;
+ }
+ else
+ {
+ if (pg_add_s64_overflow(inner_pos, compound_offset, &target_pos))
+ target_pos = -1;
+ }
+ }
+ break;
+ default:
+ elog(ERROR, "unrecognized RPR navigation kind: %d",
+ op->d.rpr_nav.kind);
+ break;
}
+ /*
+ * Slot swap elision: if target_pos is the current row, skip the
+ * tuplestore fetch and slot swap entirely. This benefits LAST(expr),
+ * PREV(expr, 0), NEXT(expr, 0), and similar cases.
+ *
+ * We must still set nav_saved_outertuple (done above) so that
+ * EEOP_RPR_NAV_RESTORE is a harmless no-op.
+ */
+ if (target_pos == winstate->currentpos)
+ return;
+
/* Fetch target row slot (returns nav_null_slot if out of range) */
target_slot = ExecRPRNavGetSlot(winstate, target_pos);
@@ -6015,9 +6150,11 @@ ExecEvalRPRNavSet(ExprState *state, ExprEvalStep *op, ExprContext *econtext)
}
/*
- * Evaluate RPR PREV/NEXT navigation: restore slot to original row.
+ * 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.
*/
void
diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 995acdd7be5..60f0d8b2fa1 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -48,6 +48,7 @@
* - src/include/nodes/plannodes.h (plan node definitions)
* - src/include/nodes/execnodes.h (execution state definitions)
* - src/include/optimizer/rpr.h (types and constants)
+ * - src/backend/optimizer/plan/createplan.c (nav offset computation)
*
* ============================================================================
*
@@ -609,22 +610,69 @@
* result = ExecEvalExpr(defineClause[i])
* varMatched[i] = (not null and true)
*
- * To support row navigation operators such as PREV() and NEXT(),
+ * To support row navigation operators (PREV, NEXT, FIRST, LAST),
* a 1-slot model is used: only ecxt_outertuple is set to the current
- * row. PREV/NEXT navigation is handled by EEOP_RPR_NAV_SET/RESTORE
- * opcodes emitted during DEFINE expression compilation:
+ * row. Navigation is handled by EEOP_RPR_NAV_SET/RESTORE opcodes
+ * emitted during DEFINE expression compilation:
*
* NAV_SET: save ecxt_outertuple, swap in target row via nav_slot
* (evaluate): argument expression reads from swapped slot
* NAV_RESTORE: restore original ecxt_outertuple
*
+ * Compound navigation (PREV(FIRST()), NEXT(FIRST()), PREV(LAST()),
+ * NEXT(LAST())) is flattened by the parser into a single RPRNavExpr
+ * with a compound kind (RPR_NAV_PREV_FIRST, etc.). The executor
+ * computes the target position in two steps: first the inner reference
+ * point (match_start + N or currentpos - N) with match-range validation,
+ * then the outer adjustment (± M) with partition-range validation.
+ * If either step is out of range, the result is NULL.
+ *
* nav_slot caches the last fetched position (nav_slot_pos) to avoid
- * redundant tuplestore lookups when multiple PREV/NEXT calls target
+ * redundant tuplestore lookups when multiple navigation calls target
* the same row.
*
* The varMatched array is referenced later in Phase 1 (Match).
*
- * VI-4. ExecRPRProcessRow(): 3-Phase Processing
+ * VI-4. Per-Context Re-evaluation (match_start-dependent variables)
+ *
+ * DEFINE variables that depend on match_start (those containing FIRST,
+ * LAST-with-offset, or compound PREV_FIRST/NEXT_FIRST/PREV_LAST/NEXT_LAST)
+ * are identified at plan time via defineMatchStartDependent. The shared
+ * evaluation in nfa_evaluate_row() uses the head context's matchStartRow
+ * for FIRST/LAST base position.
+ *
+ * When processing a context whose matchStartRow differs from the shared
+ * value, nfa_reevaluate_dependent_vars() temporarily sets nav_match_start
+ * to that context's matchStartRow and re-evaluates only the dependent
+ * variables. No restore is needed because contexts are ordered by
+ * matchStartRow (ascending), so no later context shares the head's value.
+ *
+ * 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_nav_offsets):
+ *
+ * navMaxOffset (Nav Mark Lookback):
+ * Maximum backward reach from currentpos. Contributed by PREV,
+ * LAST-with-offset, and compound PREV_LAST/NEXT_LAST.
+ * Mark position: currentpos - navMaxOffset.
+ *
+ * navFirstOffset (Nav Mark Lookahead):
+ * Minimum forward offset from match_start. Contributed by FIRST
+ * and compound PREV_FIRST/NEXT_FIRST. Can be negative when
+ * compound PREV_FIRST looks before match_start.
+ * Mark position: oldest_context->matchStartRow + navFirstOffset.
+ *
+ * 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
+ * RPR_NAV_OFFSET_RETAIN_ALL, disabling trim for that dimension.
+ *
+ * VI-6. ExecRPRProcessRow(): 3-Phase Processing
*
* NFA processing for a single row is divided into three phases:
*
@@ -707,6 +755,21 @@
*
* VIII-3. Absorption Conditions
*
+ * Planner-time prerequisites (all must hold for absorption to be enabled):
+ *
+ * (a) SKIP PAST LAST ROW. SKIP TO NEXT ROW creates overlapping
+ * contexts that cannot be safely absorbed.
+ * (b) Unbounded frame (ROWS BETWEEN CURRENT ROW AND UNBOUNDED
+ * FOLLOWING). Limited frames apply differently to each context,
+ * breaking the monotonicity principle.
+ * (c) No match_start-dependent navigation in DEFINE. FIRST,
+ * LAST-with-offset, and compound navigation referencing match_start
+ * (PREV_FIRST, NEXT_FIRST, PREV_LAST/NEXT_LAST with offset)
+ * cause different contexts to evaluate to different values for the
+ * same row, breaking monotonicity.
+ *
+ * Runtime conditions (evaluated per context pair):
+ *
* (1) The pattern is marked as isAbsorbable (see IV-5)
* (2) allStatesAbsorbable of the target context is true
* (3) An earlier context "covers" all states of the target
@@ -979,6 +1042,19 @@
* Only INITIAL is supported, searching only for matches starting at each
* row position pos.
*
+ * X-4. Bounded Frame Handling
+ *
+ * When the frame is bounded (e.g., ROWS BETWEEN CURRENT ROW AND 5
+ * FOLLOWING), ExecRPRProcessRow receives hasLimitedFrame=true and
+ * frameOffset indicating the upper bound. After the advance phase,
+ * any context whose match has exceeded the frame boundary
+ * (currentPos - matchStartRow >= frameOffset + 1) is finalized early.
+ * This prevents matches from extending beyond the window frame.
+ *
+ * Note that bounded frames also disable context absorption at the
+ * planner level (see VIII-3(b)), since the frame boundary breaks the
+ * monotonicity assumption required for correct absorption.
+ *
* Chapter XI Worked Example: Full Execution Trace
* ============================================================================
*
@@ -1330,6 +1406,14 @@
* nfa_advance_var execRPR.c VAR handling
* nfa_add_state_unique execRPR.c Deduplication
* nfa_states_covered execRPR.c Absorption check
+ * nfa_reevaluate_dependent_vars execRPR.c Per-context re-eval
+ * ExecRPRGetHeadContext execRPR.c Context lookup
+ * ExecRPRFreeContext execRPR.c Context deallocation
+ * ExecRPRCleanupDeadContexts execRPR.c Dead context cleanup
+ * ExecRPRFinalizeAllContexts execRPR.c Partition-end finalize
+ * ExecRPRRecordContextSuccess execRPR.c Stats: match success
+ * ExecRPRRecordContextFailure execRPR.c Stats: match failure
+ * compute_nav_offsets createplan.c Trim offset computation
*
* Appendix B. Data Structure Relationship Diagram
* ============================================================================
@@ -2989,6 +3073,56 @@ ExecRPRRecordContextFailure(WindowAggState *winstate, int64 failedLen)
}
}
+/*
+ * nfa_reevaluate_dependent_vars
+ * Re-evaluate match_start-dependent DEFINE variables for a specific
+ * context whose matchStartRow differs from the shared evaluation's
+ * nav_match_start.
+ *
+ * Only variables in defineMatchStartDependent are re-evaluated. The
+ * current row's slot (ecxt_outertuple) must already be set up by
+ * nfa_evaluate_row().
+ */
+static void
+nfa_reevaluate_dependent_vars(WindowAggState *winstate, RPRNFAContext *ctx,
+ int64 currentPos)
+{
+ ExprContext *econtext = winstate->ss.ps.ps_ExprContext;
+ int64 saved_match_start = winstate->nav_match_start;
+ int64 saved_pos = winstate->currentpos;
+ int varIdx = 0;
+ ListCell *lc;
+
+ /* Temporarily set nav_match_start and currentpos for FIRST/LAST */
+ winstate->nav_match_start = ctx->matchStartRow;
+ winstate->currentpos = currentPos;
+
+ /* Invalidate nav_slot cache since match_start changed */
+ winstate->nav_slot_pos = -1;
+
+ foreach(lc, winstate->defineClauseList)
+ {
+ if (bms_is_member(varIdx, winstate->defineMatchStartDependent))
+ {
+ ExprState *exprState = (ExprState *) lfirst(lc);
+ Datum result;
+ bool isnull;
+
+ result = ExecEvalExpr(exprState, econtext, &isnull);
+ winstate->nfaVarMatched[varIdx] = (!isnull && DatumGetBool(result));
+ }
+
+ varIdx++;
+ if (varIdx >= list_length(winstate->defineVariableList))
+ break;
+ }
+
+ /* Restore original match_start, currentpos, and invalidate cache */
+ winstate->nav_match_start = saved_match_start;
+ winstate->currentpos = saved_pos;
+ winstate->nav_slot_pos = -1;
+}
+
/*
* ExecRPRProcessRow
*
@@ -3003,6 +3137,7 @@ ExecRPRProcessRow(WindowAggState *winstate, int64 currentPos,
{
RPRNFAContext *ctx;
bool *varMatched = winstate->nfaVarMatched;
+ bool hasDependent = !bms_is_empty(winstate->defineMatchStartDependent);
/* Allow query cancellation once per row for simple/low-state patterns */
CHECK_FOR_INTERRUPTS();
@@ -3029,6 +3164,13 @@ ExecRPRProcessRow(WindowAggState *winstate, int64 currentPos,
}
}
+ /*
+ * If this context has a different matchStartRow than the one used in
+ * the shared evaluation, re-evaluate match_start-dependent variables
+ * with this context's matchStartRow.
+ */
+ if (hasDependent && ctx->matchStartRow != winstate->nav_match_start)
+ nfa_reevaluate_dependent_vars(winstate, ctx, currentPos);
nfa_match(winstate, ctx, varMatched);
ctx->lastProcessedRow = currentPos;
}
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 9787ef7756f..cdbe356abd7 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -34,6 +34,7 @@
#include "postgres.h"
#include "access/htup_details.h"
+#include "common/int.h"
#include "catalog/objectaccess.h"
#include "catalog/pg_aggregate.h"
#include "catalog/pg_collation_d.h"
@@ -245,8 +246,8 @@ static void update_reduced_frame(WindowObject winobj, int64 pos);
static bool nfa_evaluate_row(WindowObject winobj, int64 pos, bool *varMatched);
/* Forward declarations - navigation offset evaluation */
-static bool collect_prev_offset_walker(Node *node, List **offsets);
static void eval_nav_max_offset(WindowAggState *winstate, List *defineClause);
+static void eval_nav_first_offset(WindowAggState *winstate, List *defineClause);
/*
* Not null info bit array consists of 2-bit items
@@ -932,28 +933,7 @@ eval_windowaggregates(WindowAggState *winstate)
* head, so that tuplestore can discard unnecessary rows.
*/
if (agg_winobj->markptr >= 0)
- {
- int64 markpos = winstate->frameheadpos;
-
- if (rpr_is_defined(winstate))
- {
- /*
- * If RPR is used, PREV may need to look at rows before the frame
- * head. Adjust mark by navMaxOffset if known, otherwise retain
- * from position 0.
- */
- if (winstate->navMaxOffset >= 0)
- {
- if (markpos > winstate->navMaxOffset)
- markpos -= winstate->navMaxOffset;
- else
- markpos = 0;
- }
- else
- markpos = 0;
- }
- WinSetMarkPosition(agg_winobj, markpos);
- }
+ WinSetMarkPosition(agg_winobj, winstate->frameheadpos);
/*
* Now restart the aggregates that require it.
@@ -1279,15 +1259,15 @@ prepare_tuplestore(WindowAggState *winstate)
if (winstate->nav_winobj)
{
/*
- * Allocate mark and read pointers for PREV/NEXT navigation.
+ * Allocate mark and read pointers for RPR navigation.
*
- * If navMaxOffset >= 0, we advance the mark to (currentpos -
- * navMaxOffset) as rows are processed, allowing tuplestore_trim() to
- * free rows that are no longer reachable.
+ * If navMaxOffsetKind == RPR_NAV_OFFSET_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.
*
- * If navMaxOffset < 0 (RPR_NAV_OFFSET_NEEDS_EVAL or
- * RPR_NAV_OFFSET_RETAIN_ALL), the mark stays at 0, retaining the
- * entire partition in the tuplestore.
+ * RPR_NAV_OFFSET_NEEDS_EVAL is resolved at executor init; by this
+ * point it is either FIXED or RETAIN_ALL.
*/
winstate->nav_winobj->markptr =
tuplestore_alloc_read_pointer(winstate->buffer, 0);
@@ -2527,18 +2507,40 @@ ExecWindowAgg(PlanState *pstate)
/*
* Advance RPR navigation mark pointer if possible, so that
- * tuplestore_trim() can free rows no longer reachable by PREV.
+ * tuplestore_trim() can free rows no longer reachable by navigation.
*/
if (winstate->nav_winobj &&
winstate->rpPattern != NULL &&
- winstate->navMaxOffset >= 0)
+ winstate->navMaxOffsetKind == RPR_NAV_OFFSET_FIXED)
{
int64 navmarkpos;
+ /* Backward reach from PREV/LAST/compound PREV_LAST/NEXT_LAST */
if (winstate->currentpos > winstate->navMaxOffset)
navmarkpos = winstate->currentpos - winstate->navMaxOffset;
else
navmarkpos = 0;
+
+ /*
+ * If FIRST is used, also consider match_start + navFirstOffset.
+ * The oldest active context (nfaContext) has the smallest
+ * matchStartRow.
+ */
+ if (winstate->hasFirstNav &&
+ winstate->navFirstOffsetKind == RPR_NAV_OFFSET_FIXED &&
+ winstate->nfaContext != NULL)
+ {
+ int64 firstreach;
+
+ if (winstate->navFirstOffset > -winstate->nfaContext->matchStartRow)
+ firstreach = winstate->nfaContext->matchStartRow
+ + winstate->navFirstOffset;
+ else
+ firstreach = 0;
+ if (firstreach < navmarkpos)
+ navmarkpos = firstreach;
+ }
+
if (navmarkpos > winstate->nav_winobj->markpos)
WinSetMarkPosition(winstate->nav_winobj, navmarkpos);
}
@@ -2779,6 +2781,7 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
winstate->nav_null_slot = ExecStoreAllNullTuple(winstate->nav_null_slot);
winstate->nav_saved_outertuple = NULL;
+ winstate->nav_match_start = 0;
}
/*
@@ -2988,10 +2991,20 @@ 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 max PREV offset for tuplestore trim */
+ /* Set up nav offsets for tuplestore trim */
+ winstate->navMaxOffsetKind = node->navMaxOffsetKind;
winstate->navMaxOffset = node->navMaxOffset;
- if (winstate->navMaxOffset == RPR_NAV_OFFSET_NEEDS_EVAL)
+ if (winstate->navMaxOffsetKind == RPR_NAV_OFFSET_NEEDS_EVAL)
eval_nav_max_offset(winstate, node->defineClause);
+ winstate->hasFirstNav = node->hasFirstNav;
+ winstate->navFirstOffsetKind = node->navFirstOffsetKind;
+ winstate->navFirstOffset = node->navFirstOffset;
+ if (winstate->hasFirstNav &&
+ winstate->navFirstOffsetKind == RPR_NAV_OFFSET_NEEDS_EVAL)
+ eval_nav_first_offset(winstate, node->defineClause);
+
+ /* Copy match_start dependency bitmapset for per-context evaluation */
+ winstate->defineMatchStartDependent = bms_copy(node->defineMatchStartDependent);
/* Calculate NFA state size and allocate cycle detection bitmap */
if (node->rpPattern != NULL)
@@ -3903,84 +3916,254 @@ put_notnull_info(WindowObject winobj, int64 pos, int argno, bool isnull)
}
/*
- * collect_prev_offset_walker
- * Walk expression tree to collect PREV offset_arg expressions.
+ * eval_nav_offset_helper
+ * Evaluate an offset expression at executor init time for trim
+ * optimization. Returns the offset value, or 0 for NULL/negative
+ * (these will cause a runtime error during actual navigation, so the
+ * trim value is irrelevant).
+ */
+static int64
+eval_nav_offset_helper(WindowAggState *winstate, Expr *offset_expr,
+ int64 defaultOffset)
+{
+ 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;
+
+ offset = DatumGetInt64(val);
+ if (offset < 0)
+ return 0;
+
+ return offset;
+}
+
+typedef struct
+{
+ WindowAggState *winstate;
+ int64 maxOffset;
+ bool overflow; /* true if overflow detected */
+} EvalNavMaxContext;
+
+/*
+ * eval_nav_max_offset_walker
+ * Walk expression tree evaluating backward-reach offsets at runtime.
+ *
+ * Handles simple PREV, LAST-with-offset, and compound PREV_LAST/NEXT_LAST.
*/
static bool
-collect_prev_offset_walker(Node *node, List **offsets)
+eval_nav_max_offset_walker(Node *node, void *ctx)
{
+ EvalNavMaxContext *context = (EvalNavMaxContext *) ctx;
+
if (node == NULL)
return false;
+ /* Short-circuit if overflow already detected */
+ if (context->overflow)
+ return false;
+
if (IsA(node, RPRNavExpr))
{
RPRNavExpr *nav = (RPRNavExpr *) node;
+ int64 reach = 0;
+
+ if (nav->kind == RPR_NAV_PREV)
+ {
+ reach = eval_nav_offset_helper(context->winstate,
+ nav->offset_arg, 1);
+ }
+ else if (nav->kind == RPR_NAV_LAST && nav->offset_arg != NULL)
+ {
+ reach = eval_nav_offset_helper(context->winstate,
+ nav->offset_arg, 0);
+ }
+ else if (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);
- if (nav->kind == RPR_NAV_PREV && nav->offset_arg != NULL)
- *offsets = lappend(*offsets, nav->offset_arg);
+ if (nav->kind == RPR_NAV_PREV_LAST)
+ {
+ if (pg_add_s64_overflow(inner, outer, &reach))
+ {
+ context->overflow = true;
+ return false;
+ }
+ }
+ else
+ reach = (inner > outer) ? inner - outer : 0;
+ }
- /* Don't walk into RPRNavExpr children */
- return false;
+ if (reach > context->maxOffset)
+ context->maxOffset = reach;
+
+ return false; /* don't walk into children */
}
- return expression_tree_walker(node, collect_prev_offset_walker, offsets);
+ return expression_tree_walker(node, eval_nav_max_offset_walker, ctx);
}
/*
* eval_nav_max_offset
- * Evaluate non-constant PREV offsets at executor init time.
+ * Evaluate non-constant backward-reach offsets at executor init time.
+ *
+ * Called when the planner set navMaxOffsetKind to RPR_NAV_OFFSET_NEEDS_EVAL
+ * because some offset in PREV, LAST-with-offset, or compound PREV_LAST/
+ * NEXT_LAST contains a parameter or non-foldable expression.
*
- * Called when the planner set navMaxOffset to RPR_NAV_OFFSET_NEEDS_EVAL
- * because some PREV offset contains a parameter or non-foldable expression.
- * Walks the original defineClause expression trees, compiles and evaluates
- * each PREV offset_arg, and stores the maximum in winstate->navMaxOffset.
+ * On overflow, sets navMaxOffsetKind to RPR_NAV_OFFSET_RETAIN_ALL so that
+ * tuplestore trim is disabled for backward navigation.
*/
static void
eval_nav_max_offset(WindowAggState *winstate, List *defineClause)
{
- ExprContext *econtext = winstate->ss.ps.ps_ExprContext;
- List *offsets = NIL;
+ EvalNavMaxContext ctx;
ListCell *lc;
- int64 maxOffset = 0;
- /* Collect all PREV offset expressions from DEFINE clause */
+ ctx.winstate = winstate;
+ ctx.maxOffset = 0;
+ ctx.overflow = false;
+
foreach(lc, defineClause)
{
TargetEntry *te = (TargetEntry *) lfirst(lc);
- collect_prev_offset_walker((Node *) te->expr, &offsets);
+ eval_nav_max_offset_walker((Node *) te->expr, &ctx);
}
- /* Evaluate each offset and find maximum */
- foreach(lc, offsets)
+ if (ctx.overflow)
{
- Expr *offset_expr = (Expr *) lfirst(lc);
- ExprState *estate;
- Datum val;
- bool isnull;
- int64 offset;
+ winstate->navMaxOffsetKind = RPR_NAV_OFFSET_RETAIN_ALL;
+ winstate->navMaxOffset = 0;
+ }
+ else
+ {
+ winstate->navMaxOffsetKind = RPR_NAV_OFFSET_FIXED;
+ winstate->navMaxOffset = ctx.maxOffset;
+ }
+}
- estate = ExecInitExpr(offset_expr, (PlanState *) winstate);
- val = ExecEvalExprSwitchContext(estate, econtext, &isnull);
+typedef struct
+{
+ WindowAggState *winstate;
+ int64 minOffset;
+ bool found;
+} EvalNavFirstContext;
- /*
- * NULL or negative offsets will cause a runtime error when PREV is
- * actually evaluated. For trim purposes, treat them as 0.
- */
- if (isnull)
- continue;
+/*
+ * eval_nav_first_offset_walker
+ * Walk expression tree evaluating forward-from-match_start offsets.
+ *
+ * Handles simple FIRST and compound PREV_FIRST/NEXT_FIRST.
+ */
+static bool
+eval_nav_first_offset_walker(Node *node, void *ctx)
+{
+ EvalNavFirstContext *context = (EvalNavFirstContext *) ctx;
- offset = DatumGetInt64(val);
- if (offset < 0)
- continue;
+ if (node == NULL)
+ return false;
+
+ if (IsA(node, RPRNavExpr))
+ {
+ RPRNavExpr *nav = (RPRNavExpr *) node;
+ int64 combined = INT64_MAX;
- if (offset > maxOffset)
- maxOffset = offset;
+ if (nav->kind == RPR_NAV_FIRST)
+ {
+ context->found = true;
+ combined = eval_nav_offset_helper(context->winstate,
+ nav->offset_arg, 0);
+ }
+ else if (nav->kind == RPR_NAV_PREV_FIRST ||
+ nav->kind == RPR_NAV_NEXT_FIRST)
+ {
+ int64 inner = eval_nav_offset_helper(context->winstate,
+ nav->offset_arg, 0);
+ int64 outer = eval_nav_offset_helper(context->winstate,
+ nav->compound_offset_arg, 1);
+
+ context->found = true;
+ if (nav->kind == RPR_NAV_PREV_FIRST)
+ {
+ /*
+ * combined = inner - outer. Both are non-negative, so the
+ * result >= -INT64_MAX, which cannot underflow int64.
+ */
+ combined = inner - outer;
+ }
+ else
+ {
+ /*
+ * NEXT_FIRST: combined = inner + outer. This can overflow,
+ * but the result is always >= 0, so it never updates
+ * minOffset (which tracks the minimum). Clamp to INT64_MAX
+ * on overflow.
+ */
+ if (pg_add_s64_overflow(inner, outer, &combined))
+ combined = INT64_MAX;
+ }
+ }
+
+ if (combined < context->minOffset)
+ context->minOffset = combined;
+
+ return false;
}
- winstate->navMaxOffset = maxOffset;
+ return expression_tree_walker(node, eval_nav_first_offset_walker, ctx);
+}
+
+/*
+ * eval_nav_first_offset
+ * Evaluate non-constant forward-from-match_start offsets at executor
+ * init time.
+ *
+ * Called when the planner set navFirstOffsetKind to RPR_NAV_OFFSET_NEEDS_EVAL
+ * because some offset in FIRST or compound PREV_FIRST/NEXT_FIRST contains
+ * a parameter or non-foldable expression.
+ */
+static void
+eval_nav_first_offset(WindowAggState *winstate, List *defineClause)
+{
+ EvalNavFirstContext ctx;
+ ListCell *lc;
+
+ ctx.winstate = winstate;
+ ctx.minOffset = INT64_MAX;
+ ctx.found = false;
+
+ foreach(lc, defineClause)
+ {
+ TargetEntry *te = (TargetEntry *) lfirst(lc);
- list_free(offsets);
+ eval_nav_first_offset_walker((Node *) te->expr, &ctx);
+ }
+
+ if (ctx.found && ctx.minOffset < INT64_MAX)
+ {
+ winstate->navFirstOffsetKind = RPR_NAV_OFFSET_FIXED;
+ winstate->navFirstOffset = ctx.minOffset;
+ }
+ else
+ {
+ winstate->navFirstOffsetKind = RPR_NAV_OFFSET_FIXED;
+ winstate->navFirstOffset = 0;
+ }
}
/*
@@ -4210,8 +4393,14 @@ update_reduced_frame(WindowObject winobj, int64 pos)
/*
* Evaluate variables for this row - done only once, shared by all
- * contexts
+ * contexts.
+ *
+ * Set nav_match_start to the head context's matchStartRow for
+ * FIRST/LAST navigation. Match_start-dependent variables (FIRST,
+ * LAST-with-offset) are re-evaluated per-context in ExecRPRProcessRow
+ * when matchStartRow differs.
*/
+ winstate->nav_match_start = targetCtx->matchStartRow;
rowExists = nfa_evaluate_row(winobj, currentPos, winstate->nfaVarMatched);
/* No more rows in partition? Finalize all contexts */
@@ -4299,8 +4488,8 @@ register_result:
* 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.
+ * PREV/NEXT/FIRST/LAST 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)
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index d2f19584070..66d37b78898 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -2207,6 +2207,8 @@ expression_tree_walker_impl(Node *node,
return true;
if (expr->offset_arg && WALK(expr->offset_arg))
return true;
+ if (expr->compound_offset_arg && WALK(expr->compound_offset_arg))
+ return true;
}
break;
case T_SubscriptingRef:
@@ -3146,6 +3148,7 @@ expression_tree_mutator_impl(Node *node,
FLATCOPY(newnode, nav, RPRNavExpr);
MUTATE(newnode->arg, nav->arg, Expr *);
MUTATE(newnode->offset_arg, nav->offset_arg, Expr *);
+ MUTATE(newnode->compound_offset_arg, nav->compound_offset_arg, Expr *);
return (Node *) newnode;
}
break;
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index 8ee3ccf6d0d..02d511269ab 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -16,6 +16,7 @@
*/
#include "postgres.h"
+#include "common/int.h"
#include "access/sysattr.h"
#include "access/transam.h"
#include "catalog/pg_class.h"
@@ -292,6 +293,7 @@ static WindowAgg *make_windowagg(List *tlist, WindowClause *wc,
List *runCondition, RPSkipTo rpSkipTo,
RPRPattern *compiledPattern,
List *defineClause,
+ Bitmapset *defineMatchStartDependent,
List *qual, bool topWindow,
Plan *lefttree);
static Group *make_group(List *tlist, List *qual, int numGroupCols,
@@ -2462,19 +2464,72 @@ create_minmaxagg_plan(PlannerInfo *root, MinMaxAggPath *best_path)
}
/*
- * nav_max_offset_walker
- * Walk expression tree to find the maximum PREV offset.
+ * NavOffsetContext - context for compute_nav_offsets walker.
*
- * Only PREV is relevant for tuplestore trim since it looks backward;
- * NEXT looks forward and never references already-trimmed rows.
+ * Collects both backward reach (PREV, LAST-with-offset, compound
+ * PREV_LAST/NEXT_LAST) and forward-from-match-start reach (FIRST,
+ * compound PREV_FIRST/NEXT_FIRST) in a single tree walk.
+ */
+typedef struct NavOffsetContext
+{
+ 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 (>= 0), or -1 if none */
+ bool hasFirst; /* any FIRST node found */
+ bool firstNeedsEval; /* non-constant FIRST offset found */
+} NavOffsetContext;
+
+/*
+ * 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 */
+}
+
+/*
+ * nav_offset_walker
+ * Expression tree walker for compute_nav_offsets.
+ *
+ * For each RPRNavExpr found, extract its constant offset(s) and update the
+ * NavOffsetContext with the maximum backward reach (maxOffset) and minimum
+ * forward reach (firstOffset). Handles simple navigation (PREV, NEXT,
+ * FIRST, LAST) and compound forms (PREV_FIRST, NEXT_FIRST, PREV_LAST,
+ * NEXT_LAST) by combining inner and outer offsets.
*
- * Returns true (to stop walking) if a non-constant PREV offset is found,
- * in which case *maxOffset is set to -1. Otherwise accumulates the
- * maximum constant offset value.
+ * Non-constant offsets set maxNeedsEval or firstNeedsEval. Overflow sets
+ * maxOverflow or firstOverflow for RETAIN_ALL fallback.
*/
static bool
-nav_max_offset_walker(Node *node, int64 *maxOffset)
+nav_offset_walker(Node *node, void *ctx)
{
+ NavOffsetContext *context = (NavOffsetContext *) ctx;
+
if (node == NULL)
return false;
@@ -2482,81 +2537,294 @@ nav_max_offset_walker(Node *node, int64 *maxOffset)
{
RPRNavExpr *nav = (RPRNavExpr *) node;
- /* Only PREV looks backward; NEXT is irrelevant for trim */
- if (nav->kind == RPR_NAV_PREV)
+ /*
+ * 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))
{
- int64 offset;
-
- if (nav->offset_arg == NULL)
+ if (!context->maxNeedsEval)
{
- /* 1-arg form: implicit offset of 1 */
- offset = 1;
+ int64 offset;
+
+ if (extract_const_offset(nav->offset_arg, 1, &offset))
+ {
+ if (offset > context->maxOffset)
+ context->maxOffset = offset;
+ }
+ else
+ context->maxNeedsEval = true;
}
- else if (IsA(nav->offset_arg, Const))
+ }
+
+ /*
+ * 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)
{
- Const *c = (Const *) nav->offset_arg;
+ int64 offset;
- if (c->constisnull)
+ if (extract_const_offset(nav->offset_arg, 0, &offset))
{
- /*
- * NULL offset causes a runtime error, so this path is
- * never actually reached during execution. Use 0 as a
- * safe placeholder for planning purposes.
- */
- offset = 0;
+ if (offset < context->firstOffset)
+ 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,
+ outer,
+ combined;
+
+ if (extract_const_offset(nav->offset_arg, 0, &inner) &&
+ extract_const_offset(nav->compound_offset_arg, 1, &outer))
{
- offset = DatumGetInt64(c->constvalue);
- if (offset < 0)
- offset = 0; /* negative offset causes runtime error */
+ if (nav->kind == RPR_NAV_PREV_LAST)
+ {
+ if (pg_add_s64_overflow(inner, outer, &combined))
+ {
+ context->maxOverflow = true;
+ return false;
+ }
+ }
+ else
+ combined = (inner > outer) ? inner - outer : 0;
+
+ if (combined > context->maxOffset)
+ context->maxOffset = combined;
}
+ else
+ context->maxNeedsEval = true;
}
- else
+ }
+
+ /*
+ * 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)
{
- /*
- * Non-constant offset (Param, stable function, etc.). The
- * parser guarantees offset is a runtime constant, so it can
- * be evaluated at executor init time.
- */
- *maxOffset = RPR_NAV_OFFSET_NEEDS_EVAL;
- return true; /* stop walking */
+ int64 inner,
+ outer,
+ combined;
+
+ if (extract_const_offset(nav->offset_arg, 0, &inner) &&
+ extract_const_offset(nav->compound_offset_arg, 1, &outer))
+ {
+ if (nav->kind == RPR_NAV_PREV_FIRST)
+ {
+ /*
+ * combined = inner - outer. Both are non-negative,
+ * so the result >= -INT64_MAX, which cannot underflow
+ * int64. No overflow check needed.
+ */
+ combined = inner - outer;
+ }
+ else
+ {
+ /*
+ * NEXT_FIRST: combined = inner + outer. This can
+ * overflow, but the result is always >= 0, so it
+ * never updates firstOffset (which tracks the
+ * minimum). Clamp to INT64_MAX on overflow.
+ */
+ if (pg_add_s64_overflow(inner, outer, &combined))
+ combined = INT64_MAX;
+ }
+
+ if (combined < context->firstOffset)
+ context->firstOffset = combined;
+ }
+ else
+ context->firstNeedsEval = true;
}
+ }
- if (offset > *maxOffset)
- *maxOffset = offset;
+ /* Don't walk into RPRNavExpr children */
+ return false;
+ }
+
+ return expression_tree_walker(node, nav_offset_walker, ctx);
+}
+
+/*
+ * compute_nav_offsets
+ * Compute navigation offsets for tuplestore trim in a single pass.
+ *
+ * Walks all DEFINE clause expressions 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
+ */
+static void
+compute_nav_offsets(List *defineClause,
+ RPRNavOffsetKind *maxKind, int64 *maxResult,
+ bool *hasFirst,
+ RPRNavOffsetKind *firstKind, int64 *firstResult)
+{
+ NavOffsetContext ctx;
+ ListCell *lc;
+
+ ctx.maxOffset = 0;
+ ctx.maxNeedsEval = false;
+ ctx.maxOverflow = false;
+ ctx.firstOffset = INT64_MAX; /* sentinel: no FIRST found yet */
+ ctx.hasFirst = false;
+ ctx.firstNeedsEval = false;
+
+ foreach(lc, defineClause)
+ {
+ TargetEntry *te = (TargetEntry *) lfirst(lc);
+
+ nav_offset_walker((Node *) te->expr, &ctx);
+ }
+
+ /* 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;
+ }
+
+ /* 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 if (ctx.firstOffset == INT64_MAX)
+ {
+ *firstKind = RPR_NAV_OFFSET_FIXED;
+ *firstResult = 0; /* only implicit FIRST(v) */
+ }
+ else
+ {
+ *firstKind = RPR_NAV_OFFSET_FIXED;
+ *firstResult = ctx.firstOffset; /* may be negative */
}
+ }
+ else
+ {
+ *firstKind = RPR_NAV_OFFSET_FIXED;
+ *firstResult = 0;
+ }
+}
- /* Don't walk into RPRNavExpr children - offset_arg already handled */
+/*
+ * has_match_start_dependency
+ * Check if an expression tree contains navigation that depends on
+ * match_start: FIRST, LAST-with-offset, or compound PREV_FIRST/
+ * NEXT_FIRST/PREV_LAST/NEXT_LAST with offset. Such expressions
+ * require per-context re-evaluation during NFA processing.
+ *
+ * LAST without offset always resolves to currentpos and is
+ * match_start-independent.
+ */
+static bool
+has_match_start_dependency(Node *node, void *context)
+{
+ if (node == NULL)
return false;
+
+ if (IsA(node, RPRNavExpr))
+ {
+ RPRNavExpr *nav = (RPRNavExpr *) node;
+
+ if (nav->kind == RPR_NAV_FIRST)
+ return true;
+ if (nav->kind == RPR_NAV_LAST && nav->offset_arg != NULL)
+ return true;
+
+ /* Compound kinds with FIRST base depend on match_start */
+ if (nav->kind == RPR_NAV_PREV_FIRST ||
+ nav->kind == RPR_NAV_NEXT_FIRST)
+ return true;
+
+ /*
+ * PREV_LAST/NEXT_LAST: inner is LAST, which uses currentpos.
+ * match_start-dependent only if inner has offset (clamped to
+ * match_start).
+ */
+ if ((nav->kind == RPR_NAV_PREV_LAST ||
+ nav->kind == RPR_NAV_NEXT_LAST) &&
+ nav->offset_arg != NULL)
+ return true;
+
+ /* Check children (arg may contain further nav expressions) */
+ return has_match_start_dependency((Node *) nav->arg, context);
}
- return expression_tree_walker(node, nav_max_offset_walker, maxOffset);
+ return expression_tree_walker(node, has_match_start_dependency, NULL);
}
/*
- * compute_nav_max_offset
- * Compute the maximum PREV offset from DEFINE clause expressions.
+ * compute_match_start_dependent
+ * Build a Bitmapset of DEFINE variable indices whose expressions
+ * depend on match_start (contain FIRST, LAST-with-offset, or
+ * compound PREV_FIRST/NEXT_FIRST/PREV_LAST/NEXT_LAST with offset).
*
- * Returns the maximum constant offset found, or -1 if any PREV offset
- * cannot be determined statically. NEXT offsets are ignored since they
- * look forward and don't affect tuplestore trim.
+ * Variables in this set require per-context re-evaluation during NFA
+ * processing, because different contexts may have different match_start
+ * values.
*/
-static int64
-compute_nav_max_offset(List *defineClause)
+static Bitmapset *
+compute_match_start_dependent(List *defineClause)
{
- int64 maxOffset = 0;
+ Bitmapset *result = NULL;
ListCell *lc;
+ int varIdx = 0;
foreach(lc, defineClause)
{
TargetEntry *te = (TargetEntry *) lfirst(lc);
- if (nav_max_offset_walker((Node *) te->expr, &maxOffset))
- return maxOffset; /* NEEDS_EVAL or RETAIN_ALL */
+ if (has_match_start_dependency((Node *) te->expr, NULL))
+ result = bms_add_member(result, varIdx);
+
+ varIdx++;
}
- return maxOffset;
+ return result;
}
/*
@@ -2586,6 +2854,7 @@ create_windowagg_plan(PlannerInfo *root, WindowAggPath *best_path)
List *defineVariableList = NIL;
List *filteredDefineClause = NIL;
RPRPattern *compiledPattern = NULL;
+ Bitmapset *matchStartDependent = NULL;
/*
@@ -2648,11 +2917,15 @@ create_windowagg_plan(PlannerInfo *root, WindowAggPath *best_path)
buildDefineVariableList(wc->defineClause, &defineVariableList);
filteredDefineClause = wc->defineClause;
+ /* Identify match_start-dependent DEFINE variables */
+ matchStartDependent = compute_match_start_dependent(wc->defineClause);
+
/* Compile and optimize RPR patterns */
compiledPattern = buildRPRPattern(wc->rpPattern,
defineVariableList,
wc->rpSkipTo,
- wc->frameOptions);
+ wc->frameOptions,
+ !bms_is_empty(matchStartDependent));
}
/* And finally we can make the WindowAgg node */
@@ -2670,6 +2943,7 @@ create_windowagg_plan(PlannerInfo *root, WindowAggPath *best_path)
wc->rpSkipTo,
compiledPattern,
filteredDefineClause,
+ matchStartDependent,
best_path->qual,
best_path->topwindow,
subplan);
@@ -6742,6 +7016,7 @@ make_windowagg(List *tlist, WindowClause *wc,
List *runCondition, RPSkipTo rpSkipTo,
RPRPattern *compiledPattern,
List *defineClause,
+ Bitmapset *defineMatchStartDependent,
List *qual, bool topWindow, Plan *lefttree)
{
WindowAgg *node = makeNode(WindowAgg);
@@ -6776,8 +7051,14 @@ make_windowagg(List *tlist, WindowClause *wc,
node->defineClause = defineClause;
- /* Compute max PREV offset for tuplestore trim optimization */
- node->navMaxOffset = compute_nav_max_offset(defineClause);
+ /* Store pre-computed match_start dependency bitmapset */
+ node->defineMatchStartDependent = defineMatchStartDependent;
+
+ /* Compute nav offsets for tuplestore trim optimization */
+ compute_nav_offsets(defineClause,
+ &node->navMaxOffsetKind, &node->navMaxOffset,
+ &node->hasFirstNav,
+ &node->navFirstOffsetKind, &node->navFirstOffset);
plan->targetlist = tlist;
plan->lefttree = lefttree;
diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index c0e9d134aa9..767a214016c 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -1893,7 +1893,8 @@ buildDefineVariableList(List *defineClause, List **defineVariableList)
*/
RPRPattern *
buildRPRPattern(RPRPatternNode *pattern, List *defineVariableList,
- RPSkipTo rpSkipTo, int frameOptions)
+ RPSkipTo rpSkipTo, int frameOptions,
+ bool hasMatchStartDependent)
{
RPRPattern *result;
RPRPatternNode *optimized;
@@ -1947,7 +1948,8 @@ buildRPRPattern(RPRPatternNode *pattern, List *defineVariableList,
hasLimitedFrame = (frameOptions & FRAMEOPTION_ROWS) &&
!(frameOptions & FRAMEOPTION_END_UNBOUNDED_FOLLOWING);
- if (rpSkipTo == ST_PAST_LAST_ROW && !hasLimitedFrame)
+ if (rpSkipTo == ST_PAST_LAST_ROW && !hasLimitedFrame &&
+ !hasMatchStartDependent)
{
/* Runtime conditions met - check structural absorbability */
computeAbsorbability(result);
diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c
index e14ff4dc494..aa45a98713f 100644
--- a/src/backend/parser/parse_func.c
+++ b/src/backend/parser/parse_func.c
@@ -757,35 +757,79 @@ ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs,
if (retset)
check_srf_call_placement(pstate, last_srf, location);
- /* next() and prev() are only allowed in a WINDOW DEFINE clause */
+ /*
+ * RPR navigation functions (PREV/NEXT/FIRST/LAST) are only meaningful
+ * inside a WINDOW DEFINE clause.
+ *
+ * Outside DEFINE, these polymorphic placeholders can shadow column access
+ * via functional notation (e.g., last(f) meaning f.last). For the 1-arg
+ * form, try column projection first; if that succeeds, use it instead.
+ * Otherwise, report a clear parser error.
+ */
if (fdresult == FUNCDETAIL_NORMAL &&
pstate->p_expr_kind != EXPR_KIND_RPR_DEFINE &&
(funcid == F_PREV_ANYELEMENT || funcid == F_NEXT_ANYELEMENT ||
- funcid == F_PREV_ANYELEMENT_INT8 || funcid == F_NEXT_ANYELEMENT_INT8))
+ funcid == F_PREV_ANYELEMENT_INT8 || funcid == F_NEXT_ANYELEMENT_INT8 ||
+ funcid == F_FIRST_ANYELEMENT || funcid == F_LAST_ANYELEMENT ||
+ funcid == F_FIRST_ANYELEMENT_INT8 || funcid == F_LAST_ANYELEMENT_INT8))
+ {
+ /* 1-arg form: try column projection before erroring out */
+ if (nargs == 1 && !agg_star && !agg_distinct && over == NULL &&
+ list_length(funcname) == 1)
+ {
+ Node *projection;
+
+ projection = ParseComplexProjection(pstate,
+ strVal(linitial(funcname)),
+ linitial(fargs),
+ location);
+ if (projection)
+ return projection;
+ }
+
+ /* Not a column projection — report error */
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("%s can only be used in a DEFINE clause",
NameListToString(funcname)),
parser_errposition(pstate, location)));
+ }
/* build the appropriate output structure */
if (fdresult == FUNCDETAIL_NORMAL &&
+ pstate->p_expr_kind == EXPR_KIND_RPR_DEFINE &&
(funcid == F_PREV_ANYELEMENT || funcid == F_NEXT_ANYELEMENT ||
- funcid == F_PREV_ANYELEMENT_INT8 || funcid == F_NEXT_ANYELEMENT_INT8))
+ funcid == F_PREV_ANYELEMENT_INT8 || funcid == F_NEXT_ANYELEMENT_INT8 ||
+ funcid == F_FIRST_ANYELEMENT || funcid == F_LAST_ANYELEMENT ||
+ funcid == F_FIRST_ANYELEMENT_INT8 || funcid == F_LAST_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.
+ * RPR navigation functions (PREV/NEXT/FIRST/LAST) 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);
+ RPRNavKind kind;
+ bool has_offset;
+ RPRNavExpr *navexpr;
+
+ if (funcid == F_PREV_ANYELEMENT || funcid == F_PREV_ANYELEMENT_INT8)
+ kind = RPR_NAV_PREV;
+ else if (funcid == F_NEXT_ANYELEMENT || funcid == F_NEXT_ANYELEMENT_INT8)
+ kind = RPR_NAV_NEXT;
+ else if (funcid == F_FIRST_ANYELEMENT || funcid == F_FIRST_ANYELEMENT_INT8)
+ kind = RPR_NAV_FIRST;
+ else
+ kind = RPR_NAV_LAST;
+
+ has_offset = (funcid == F_PREV_ANYELEMENT_INT8 ||
+ funcid == F_NEXT_ANYELEMENT_INT8 ||
+ funcid == F_FIRST_ANYELEMENT_INT8 ||
+ funcid == F_LAST_ANYELEMENT_INT8);
+
+ navexpr = makeNode(RPRNavExpr);
- navexpr->kind = is_next ? RPR_NAV_NEXT : RPR_NAV_PREV;
+ navexpr->kind = kind;
navexpr->arg = (Expr *) linitial(fargs);
navexpr->offset_arg = has_offset ? (Expr *) lsecond(fargs) : NULL;
navexpr->resulttype = rettype;
diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index d1e02e52e53..05070cb04bb 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -339,8 +339,8 @@ transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
/*
* Transform the DEFINE expression. We must NOT add the whole
* expression to the query targetlist, because it may contain
- * RPRNavExpr nodes (PREV/NEXT) that can only be evaluated inside the
- * owning WindowAgg.
+ * RPRNavExpr nodes (PREV/NEXT/FIRST/LAST) that can only be evaluated
+ * inside the owning WindowAgg.
*
* Instead, we transform the expression directly and only ensure that
* the individual Var nodes it references are present in the
@@ -429,14 +429,21 @@ transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
/*
* check_rpr_nav_expr
* Validate a single RPRNavExpr node by walking its arg and offset_arg
- * subtrees in a single pass each. Checks for nested PREV/NEXT, missing
+ * subtrees in a single pass each. Checks for illegal nesting, missing
* column references, and non-constant offset expressions.
+ *
+ * Nesting rules (SQL standard 5.6.4):
+ * - PREV/NEXT wrapping FIRST/LAST: allowed (compound navigation)
+ * - FIRST/LAST wrapping PREV/NEXT: prohibited
+ * - Same-category nesting (PREV inside PREV, FIRST inside FIRST, etc.):
+ * prohibited
*/
typedef struct
{
- bool has_nav; /* RPRNavExpr found (nesting) */
+ int nav_count; /* number of RPRNavExpr nodes found */
bool has_column_ref; /* Var found */
-} NavCheckResult;
+ RPRNavKind inner_kind; /* kind of first (outermost) nested RPRNavExpr */
+} NavCheckResult;
static bool
nav_check_walker(Node *node, void *context)
@@ -446,7 +453,11 @@ nav_check_walker(Node *node, void *context)
if (node == NULL)
return false;
if (IsA(node, RPRNavExpr))
- result->has_nav = true;
+ {
+ if (result->nav_count == 0)
+ result->inner_kind = ((RPRNavExpr *) node)->kind;
+ result->nav_count++;
+ }
if (IsA(node, Var))
result->has_column_ref = true;
@@ -457,16 +468,93 @@ static void
check_rpr_nav_expr(RPRNavExpr *nav, ParseState *pstate)
{
NavCheckResult result;
+ bool outer_is_physical = (nav->kind == RPR_NAV_PREV ||
+ nav->kind == RPR_NAV_NEXT);
/* 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.nav_count > 0)
+ {
+ bool inner_is_physical = (result.inner_kind == RPR_NAV_PREV ||
+ result.inner_kind == RPR_NAV_NEXT);
+
+ if (outer_is_physical && !inner_is_physical)
+ {
+ /*
+ * PREV/NEXT wrapping FIRST/LAST: compound navigation per SQL
+ * standard 5.6.4. Flatten the nested RPRNavExpr into a single
+ * compound node. The inner RPRNavExpr must be the direct arg of
+ * the outer; expressions like PREV(val + FIRST(v)) are not valid
+ * compound navigation.
+ */
+ RPRNavExpr *inner;
+
+ /* Reject triple-or-deeper nesting (e.g. PREV(FIRST(PREV(x)))) */
+ if (result.nav_count > 1)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("row pattern navigation cannot be nested more than two levels deep"),
+ errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
+ parser_errposition(pstate, nav->location)));
+
+ if (!IsA(nav->arg, RPRNavExpr))
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("row pattern navigation operation must be a direct argument of the outer navigation"),
+ errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
+ parser_errposition(pstate, nav->location)));
+ inner = (RPRNavExpr *) nav->arg;
+
+ /* Determine compound kind */
+ if (nav->kind == RPR_NAV_PREV && inner->kind == RPR_NAV_FIRST)
+ nav->kind = RPR_NAV_PREV_FIRST;
+ else if (nav->kind == RPR_NAV_PREV && inner->kind == RPR_NAV_LAST)
+ nav->kind = RPR_NAV_PREV_LAST;
+ else if (nav->kind == RPR_NAV_NEXT && inner->kind == RPR_NAV_FIRST)
+ nav->kind = RPR_NAV_NEXT_FIRST;
+ else if (nav->kind == RPR_NAV_NEXT && inner->kind == RPR_NAV_LAST)
+ nav->kind = RPR_NAV_NEXT_LAST;
+
+ /* Move outer offset to compound_offset_arg */
+ nav->compound_offset_arg = nav->offset_arg;
+
+ /* Move inner offset and arg up */
+ nav->offset_arg = inner->offset_arg;
+ nav->arg = inner->arg;
+
+ /* No further nesting check needed - already validated */
+ return;
+ }
+ else if (!outer_is_physical && inner_is_physical)
+ {
+ /* FIRST/LAST wrapping PREV/NEXT: prohibited by standard */
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("FIRST and LAST cannot contain PREV or NEXT"),
+ errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
+ parser_errposition(pstate, nav->location)));
+ }
+ else if (outer_is_physical && inner_is_physical)
+ {
+ /* PREV/NEXT wrapping PREV/NEXT: prohibited */
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("PREV and NEXT cannot contain PREV or NEXT"),
+ errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
+ parser_errposition(pstate, nav->location)));
+ }
+ else
+ {
+ /* FIRST/LAST wrapping FIRST/LAST: prohibited */
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("FIRST and LAST cannot contain FIRST or LAST"),
+ errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
+ parser_errposition(pstate, nav->location)));
+ }
+ }
if (!result.has_column_ref)
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
@@ -483,7 +571,7 @@ check_rpr_nav_expr(RPRNavExpr *nav, ParseState *pstate)
contain_volatile_functions((Node *) nav->offset_arg))
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("PREV/NEXT offset must be a run-time constant"),
+ errmsg("row pattern navigation offset must be a run-time constant"),
parser_errposition(pstate, nav->location)));
}
}
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index a4fe725646c..467736d9146 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -10106,16 +10106,83 @@ get_rule_expr(Node *node, deparse_context *context,
case T_RPRNavExpr:
{
RPRNavExpr *nav = (RPRNavExpr *) node;
+ const char *outer_func = NULL;
+ const char *inner_func;
- appendStringInfoString(buf,
- nav->kind == RPR_NAV_PREV ? "PREV(" : "NEXT(");
- get_rule_expr((Node *) nav->arg, context, showimplicit);
- if (nav->offset_arg != NULL)
+ switch (nav->kind)
{
- appendStringInfoString(buf, ", ");
- get_rule_expr((Node *) nav->offset_arg, context, showimplicit);
+ case RPR_NAV_PREV:
+ inner_func = "PREV(";
+ break;
+ case RPR_NAV_NEXT:
+ inner_func = "NEXT(";
+ break;
+ case RPR_NAV_FIRST:
+ inner_func = "FIRST(";
+ break;
+ case RPR_NAV_LAST:
+ inner_func = "LAST(";
+ break;
+ case RPR_NAV_PREV_FIRST:
+ outer_func = "PREV(";
+ inner_func = "FIRST(";
+ break;
+ case RPR_NAV_PREV_LAST:
+ outer_func = "PREV(";
+ inner_func = "LAST(";
+ break;
+ case RPR_NAV_NEXT_FIRST:
+ outer_func = "NEXT(";
+ inner_func = "FIRST(";
+ break;
+ case RPR_NAV_NEXT_LAST:
+ outer_func = "NEXT(";
+ inner_func = "LAST(";
+ break;
+ default:
+ elog(ERROR, "unrecognized RPR navigation kind: %d",
+ nav->kind);
+ inner_func = NULL; /* keep compiler quiet */
+ break;
+ }
+
+ if (outer_func != NULL)
+ {
+ /*
+ * Compound: PREV(FIRST(arg [, inner_offset]) [,
+ * outer_offset])
+ */
+ appendStringInfoString(buf, outer_func);
+ appendStringInfoString(buf, inner_func);
+ 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, ')');
+ if (nav->compound_offset_arg != NULL)
+ {
+ appendStringInfoString(buf, ", ");
+ get_rule_expr((Node *) nav->compound_offset_arg,
+ context, showimplicit);
+ }
+ appendStringInfoChar(buf, ')');
+ }
+ else
+ {
+ /* Simple: FUNC(arg [, offset]) */
+ appendStringInfoString(buf, inner_func);
+ 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, ')');
}
- appendStringInfoChar(buf, ')');
}
break;
diff --git a/src/backend/utils/adt/windowfuncs.c b/src/backend/utils/adt/windowfuncs.c
index 091260d2cce..420a4962395 100644
--- a/src/backend/utils/adt/windowfuncs.c
+++ b/src/backend/utils/adt/windowfuncs.c
@@ -785,3 +785,59 @@ window_next_offset(PG_FUNCTION_ARGS)
errmsg("next() can only be used in a DEFINE clause")));
PG_RETURN_NULL(); /* not reached */
}
+
+/*
+ * first
+ * Catalog placeholder for RPR's FIRST navigation operator.
+ * See window_prev() for details.
+ */
+Datum
+window_first(PG_FUNCTION_ARGS)
+{
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("first() can only be used in a DEFINE clause")));
+ PG_RETURN_NULL(); /* not reached */
+}
+
+/*
+ * last
+ * Catalog placeholder for RPR's LAST navigation operator.
+ * See window_prev() for details.
+ */
+Datum
+window_last(PG_FUNCTION_ARGS)
+{
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("last() can only be used in a DEFINE clause")));
+ PG_RETURN_NULL(); /* not reached */
+}
+
+/*
+ * first(value, offset)
+ * Catalog placeholder for RPR's FIRST navigation operator with offset.
+ * See window_prev() for details.
+ */
+Datum
+window_first_offset(PG_FUNCTION_ARGS)
+{
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("first() can only be used in a DEFINE clause")));
+ PG_RETURN_NULL(); /* not reached */
+}
+
+/*
+ * last(value, offset)
+ * Catalog placeholder for RPR's LAST navigation operator with offset.
+ * See window_prev() for details.
+ */
+Datum
+window_last_offset(PG_FUNCTION_ARGS)
+{
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("last() 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 8e95169b7b0..6d70ed23aeb 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -10984,6 +10984,18 @@
{ oid => '8129', descr => 'next value at offset',
proname => 'next', provolatile => 's', proisstrict => 'f', prorettype => 'anyelement',
proargtypes => 'anyelement int8', prosrc => 'window_next_offset' },
+{ oid => '8130', descr => 'first value in match',
+ proname => 'first', provolatile => 's', prorettype => 'anyelement',
+ proargtypes => 'anyelement', prosrc => 'window_first' },
+{ oid => '8132', descr => 'first value in match at offset',
+ proname => 'first', provolatile => 's', proisstrict => 'f', prorettype => 'anyelement',
+ proargtypes => 'anyelement int8', prosrc => 'window_first_offset' },
+{ oid => '8131', descr => 'last value in match',
+ proname => 'last', provolatile => 's', prorettype => 'anyelement',
+ proargtypes => 'anyelement', prosrc => 'window_last' },
+{ oid => '8133', descr => 'last value in match at offset',
+ proname => 'last', provolatile => 's', proisstrict => 'f', prorettype => 'anyelement',
+ proargtypes => 'anyelement int8', prosrc => 'window_last_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 fac37c96896..834800a4062 100644
--- a/src/include/executor/execExpr.h
+++ b/src/include/executor/execExpr.h
@@ -699,10 +699,10 @@ typedef struct ExprEvalStep
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 */
+ 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 */
} rpr_nav;
/* for EEOP_AGG_*DESERIALIZE */
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index ff6d7d70a60..602b72a6e0d 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -2638,6 +2638,9 @@ typedef struct WindowAggState
Size nfaStateSize; /* pre-calculated RPRNFAState size */
bool *nfaVarMatched; /* per-row cache: varMatched[varId] for varId
* < numDefines */
+ Bitmapset *defineMatchStartDependent; /* DEFINE vars needing per-context
+ * evaluation
+ * (match_start-dependent) */
bitmapword *nfaVisitedElems; /* elemIdx visited bitmap for cycle
* detection */
int nfaVisitedNWords; /* number of bitmapwords in
@@ -2692,12 +2695,17 @@ typedef struct WindowAggState
TupleTableSlot *temp_slot_2;
/* RPR navigation */
- int64 navMaxOffset; /* max PREV offset; see RPR_NAV_OFFSET_* */
+ RPRNavOffsetKind navMaxOffsetKind; /* status of navMaxOffset */
+ int64 navMaxOffset; /* max backward nav offset (when FIXED) */
+ bool hasFirstNav; /* FIRST() present in DEFINE */
+ RPRNavOffsetKind navFirstOffsetKind; /* status of navFirstOffset */
+ int64 navFirstOffset; /* min FIRST() offset (when FIXED) */
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_slot; /* slot for PREV/NEXT/FIRST/LAST target row */
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 */
/* RPR current match result */
bool rpr_match_valid; /* true if a match result is set */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index b92663687a6..1fdf62575cb 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -586,9 +586,24 @@ typedef enum RPSkipTo
ST_NONE, /* no AFTER MATCH clause; default for non-RPR
* windows */
ST_NEXT_ROW, /* SKIP TO NEXT ROW */
- ST_PAST_LAST_ROW, /* SKIP TO PAST LAST ROW */
+ 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
*/
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 27a2e7b48c7..93ce505f0d4 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -1387,14 +1387,34 @@ typedef struct WindowAgg
List *defineClause;
/*
- * Maximum PREV offset for tuplestore mark optimization. >= 0: statically
- * determined max offset (mark = currentpos - offset).
- * RPR_NAV_OFFSET_NEEDS_EVAL: has non-constant offset; evaluate at
- * executor init. RPR_NAV_OFFSET_RETAIN_ALL: must retain entire partition
- * (no trim possible).
+ * Bitmapset of DEFINE variable indices whose expressions depend on
+ * match_start (contain FIRST, LAST-with-offset, or compound
+ * PREV_FIRST/NEXT_FIRST/PREV_LAST/NEXT_LAST with offset). Variables in
+ * this set require per-context re-evaluation during NFA processing.
*/
+ 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 94723a3b909..0afcfacd5d2 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -651,27 +651,50 @@ typedef struct WindowFuncRunCondition
/*
* RPRNavExpr
*
- * Represents a PREV() or NEXT() navigation call in an RPR DEFINE clause.
+ * Represents a PREV/NEXT/FIRST/LAST 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
+ * Simple navigation (PREV/NEXT/FIRST/LAST):
+ * kind: RPR_NAV_PREV, RPR_NAV_NEXT, RPR_NAV_FIRST, or RPR_NAV_LAST
+ * arg: the expression to evaluate against the target row
+ * offset_arg: optional explicit offset expression (2-arg form); NULL for
+ * the 1-arg form (implicit offset: 1 for PREV/NEXT, 0 for
+ * FIRST/LAST)
+ *
+ * Compound navigation (PREV/NEXT wrapping FIRST/LAST):
+ * kind: RPR_NAV_PREV_FIRST, PREV_LAST, NEXT_FIRST, NEXT_LAST
+ * arg: the expression to evaluate against the final target row
+ * offset_arg: inner offset (FIRST/LAST), NULL = implicit default
+ * compound_offset_arg: outer offset (PREV/NEXT), NULL = implicit default
+ *
+ * Compound target computation:
+ * PREV_FIRST: (match_start + inner) - outer
+ * NEXT_FIRST: (match_start + inner) + outer
+ * PREV_LAST: (currentpos - inner) - outer
+ * NEXT_LAST: (currentpos - inner) + outer
*/
typedef enum RPRNavKind
{
RPR_NAV_PREV,
RPR_NAV_NEXT,
+ RPR_NAV_FIRST,
+ RPR_NAV_LAST,
+ /* compound: outer(inner(arg)) */
+ RPR_NAV_PREV_FIRST,
+ RPR_NAV_PREV_LAST,
+ RPR_NAV_NEXT_FIRST,
+ RPR_NAV_NEXT_LAST
} RPRNavKind;
typedef struct RPRNavExpr
{
Expr xpr;
- RPRNavKind kind; /* PREV or NEXT */
+ RPRNavKind kind; /* navigation kind */
Expr *arg; /* argument expression */
- Expr *offset_arg; /* offset expression, or NULL for 1-arg form */
+ Expr *offset_arg; /* offset expression, or NULL for default */
+ Expr *compound_offset_arg; /* outer offset for compound nav, or
+ * NULL if simple */
Oid resulttype; /* result type (same as arg's type) */
/* OID of collation of result */
Oid resultcollid pg_node_attr(query_jumble_ignore);
diff --git a/src/include/optimizer/rpr.h b/src/include/optimizer/rpr.h
index 00a28abe2b4..0a14cfad79b 100644
--- a/src/include/optimizer/rpr.h
+++ b/src/include/optimizer/rpr.h
@@ -55,19 +55,11 @@
#define RPRElemIsFin(e) ((e)->varId == RPR_VARID_FIN)
#define RPRElemCanSkip(e) ((e)->min == 0)
-/*
- * navMaxOffset sentinel values.
- * Non-negative values represent a statically determined maximum PREV offset.
- */
-#define RPR_NAV_OFFSET_NEEDS_EVAL (-1) /* has non-constant PREV offset;
- * evaluate at executor init */
-#define RPR_NAV_OFFSET_RETAIN_ALL (-2) /* must retain entire partition
- * (e.g., future FIRST/LAST) */
-
extern List *collectPatternVariables(RPRPatternNode *pattern);
extern void buildDefineVariableList(List *defineClause,
List **defineVariableList);
extern RPRPattern *buildRPRPattern(RPRPatternNode *pattern, List *defineVariableList,
- RPSkipTo rpSkipTo, int frameOptions);
+ RPSkipTo rpSkipTo, int frameOptions,
+ bool hasMatchStartDependent);
#endif /* OPTIMIZER_RPR_H */
diff --git a/src/test/regress/expected/rpr.out b/src/test/regress/expected/rpr.out
index c02dbd4c08d..04ec25d4cf5 100644
--- a/src/test/regress/expected/rpr.out
+++ b/src/test/regress/expected/rpr.out
@@ -1040,9 +1040,10 @@ WINDOW w AS (
PATTERN (A)
DEFINE A AS price > PREV(PREV(price))
);
-ERROR: PREV and NEXT cannot be nested
+ERROR: PREV and NEXT cannot contain PREV or NEXT
LINE 7: DEFINE A AS price > PREV(PREV(price))
^
+HINT: Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed.
-- Nested NEXT
SELECT price FROM stock
WINDOW w AS (
@@ -1052,9 +1053,10 @@ WINDOW w AS (
PATTERN (A)
DEFINE A AS price > NEXT(NEXT(price))
);
-ERROR: PREV and NEXT cannot be nested
+ERROR: PREV and NEXT cannot contain PREV or NEXT
LINE 7: DEFINE A AS price > NEXT(NEXT(price))
^
+HINT: Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed.
-- PREV nested inside NEXT
SELECT price FROM stock
WINDOW w AS (
@@ -1064,9 +1066,10 @@ WINDOW w AS (
PATTERN (A)
DEFINE A AS price > NEXT(PREV(price))
);
-ERROR: PREV and NEXT cannot be nested
+ERROR: PREV and NEXT cannot contain PREV or NEXT
LINE 7: DEFINE A AS price > NEXT(PREV(price))
^
+HINT: Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed.
-- PREV nested inside expression inside NEXT
SELECT price FROM stock
WINDOW w AS (
@@ -1076,9 +1079,10 @@ WINDOW w AS (
PATTERN (A)
DEFINE A AS price > NEXT(price * PREV(price))
);
-ERROR: PREV and NEXT cannot be nested
+ERROR: PREV and NEXT cannot contain PREV or NEXT
LINE 7: DEFINE A AS price > NEXT(price * PREV(price))
^
+HINT: Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed.
-- Triple nesting: error reported at outermost PREV
SELECT price FROM stock
WINDOW w AS (
@@ -1088,9 +1092,10 @@ WINDOW w AS (
PATTERN (A)
DEFINE A AS price > PREV(PREV(PREV(price)))
);
-ERROR: PREV and NEXT cannot be nested
+ERROR: PREV and NEXT cannot contain PREV or NEXT
LINE 7: DEFINE A AS price > PREV(PREV(PREV(price)))
^
+HINT: Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed.
-- No column reference in PREV/NEXT argument
-- PREV(1): constant only, no column reference
SELECT price FROM stock
@@ -1137,7 +1142,7 @@ WINDOW w AS (
PATTERN (A)
DEFINE A AS PREV(price, price) > 0
);
-ERROR: PREV/NEXT offset must be a run-time constant
+ERROR: row pattern navigation offset must be a run-time constant
LINE 7: DEFINE A AS PREV(price, price) > 0
^
-- Non-constant offset: volatile function as offset
@@ -1149,7 +1154,7 @@ WINDOW w AS (
PATTERN (A)
DEFINE A AS PREV(price, random()::int) > 0
);
-ERROR: PREV/NEXT offset must be a run-time constant
+ERROR: row pattern navigation offset must be a run-time constant
LINE 7: DEFINE A AS PREV(price, random()::int) > 0
^
-- Non-constant offset: subquery as offset
@@ -1442,7 +1447,7 @@ WINDOW w AS (
PATTERN (A+)
DEFINE A AS PREV(price, -1) IS NOT NULL
);
-ERROR: PREV/NEXT offset must not be negative
+ERROR: row pattern navigation offset must not be negative
-- 2-arg PREV/NEXT: NULL offset (typed)
SELECT company, tdate, price, first_value(price) OVER w
FROM stock
@@ -1452,7 +1457,7 @@ WINDOW w AS (
PATTERN (A+)
DEFINE A AS PREV(price, NULL::int8) IS NOT NULL
);
-ERROR: PREV/NEXT offset must not be null
+ERROR: row pattern navigation offset must not be null
-- 2-arg PREV/NEXT: NULL offset (untyped)
SELECT company, tdate, price, first_value(price) OVER w
FROM stock
@@ -1462,7 +1467,7 @@ WINDOW w AS (
PATTERN (A+)
DEFINE A AS PREV(price, NULL) IS NOT NULL
);
-ERROR: PREV/NEXT offset must not be null
+ERROR: row pattern navigation 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
@@ -1474,9 +1479,9 @@ WINDOW w AS (
DEFINE A AS price > PREV(price, $1)
);
EXECUTE test_prev_offset(-1);
-ERROR: PREV/NEXT offset must not be negative
+ERROR: row pattern navigation offset must not be negative
EXECUTE test_prev_offset(NULL);
-ERROR: PREV/NEXT offset must not be null
+ERROR: row pattern navigation 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
@@ -1489,9 +1494,9 @@ WINDOW w AS (
DEFINE A AS price > PREV(price, 0 + $1)
);
EXECUTE test_prev_offset(-1);
-ERROR: PREV/NEXT offset must not be negative
+ERROR: row pattern navigation offset must not be negative
EXECUTE test_prev_offset(NULL);
-ERROR: PREV/NEXT offset must not be null
+ERROR: row pattern navigation offset must not be null
DEALLOCATE test_prev_offset;
-- 2-arg PREV/NEXT: host variable with positive value
-- Exercises RPR_NAV_OFFSET_NEEDS_EVAL -> eval_nav_max_offset() path
@@ -1630,6 +1635,604 @@ WINDOW w AS (
company2 | 07-10-2023 | 1300 | | | 0
(20 rows)
+--
+-- FIRST/LAST navigation
+--
+-- Test data for FIRST/LAST: values cycle back so FIRST(val) = LAST(val)
+-- at specific positions.
+CREATE TEMP TABLE rpr_nav (id int, val int);
+INSERT INTO rpr_nav VALUES (1,10),(2,20),(3,30),(4,10),(5,50),(6,10);
+-- FIRST(val) = constant: B matches when match_start has val=10
+-- match_start=1(10): A=id1, B=id2, FIRST(val)=10 → match {1,2}
+-- match_start=3(30): A=id3, B=id4, FIRST(val)=30≠10 → no match
+-- match_start=4(10): A=id4, B=id5, FIRST(val)=10 → match {4,5}
+SELECT id, val, first_value(id) OVER w AS mf, last_value(id) OVER w AS ml
+FROM rpr_nav WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B)
+ DEFINE A AS TRUE, B AS FIRST(val) = 10
+);
+ id | val | mf | ml
+----+-----+----+----
+ 1 | 10 | 1 | 2
+ 2 | 20 | |
+ 3 | 30 | |
+ 4 | 10 | 4 | 5
+ 5 | 50 | |
+ 6 | 10 | |
+(6 rows)
+
+-- LAST(val): always equals current row's val (offset 0 default)
+-- Equivalent to: B AS val > 15
+SELECT id, val, first_value(id) OVER w AS mf, last_value(id) OVER w AS ml
+FROM rpr_nav WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B)
+ DEFINE A AS TRUE, B AS LAST(val) > 15
+);
+ id | val | mf | ml
+----+-----+----+----
+ 1 | 10 | 1 | 2
+ 2 | 20 | |
+ 3 | 30 | |
+ 4 | 10 | 4 | 5
+ 5 | 50 | |
+ 6 | 10 | |
+(6 rows)
+
+-- Reluctant A+? with FIRST(val) = LAST(val): find shortest match where
+-- first and last rows have the same val.
+-- match_start=1(10): reluctant tries B early:
+-- id2(20≠10), id3(30≠10), id4(10=10) → match {1,2,3,4}
+-- match_start=5(50): id6(10≠50) → no match
+SELECT id, val, first_value(id) OVER w AS mf, last_value(id) OVER w AS ml
+FROM rpr_nav WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+? B)
+ DEFINE A AS TRUE, B AS FIRST(val) = LAST(val)
+);
+ id | val | mf | ml
+----+-----+----+----
+ 1 | 10 | 1 | 4
+ 2 | 20 | |
+ 3 | 30 | |
+ 4 | 10 | |
+ 5 | 50 | |
+ 6 | 10 | |
+(6 rows)
+
+-- Greedy A+ with FIRST(val) = LAST(val): find longest match where
+-- first and last rows have the same val.
+-- match_start=1(10): greedy A eats all, B tries last:
+-- id6(10=10) → match {1,2,3,4,5,6}
+SELECT id, val, first_value(id) OVER w AS mf, last_value(id) OVER w AS ml
+FROM rpr_nav WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE A AS TRUE, B AS FIRST(val) = LAST(val)
+);
+ id | val | mf | ml
+----+-----+----+----
+ 1 | 10 | 1 | 6
+ 2 | 20 | |
+ 3 | 30 | |
+ 4 | 10 | |
+ 5 | 50 | |
+ 6 | 10 | |
+(6 rows)
+
+-- SKIP TO NEXT ROW with FIRST(val) = LAST(val): overlapping match attempts.
+-- With ONE ROW PER MATCH, each row shows only its first match result.
+SELECT id, val, first_value(id) OVER w AS mf, last_value(id) OVER w AS ml
+FROM rpr_nav WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A+? B)
+ DEFINE A AS TRUE, B AS FIRST(val) = LAST(val)
+);
+ id | val | mf | ml
+----+-----+----+----
+ 1 | 10 | 1 | 4
+ 2 | 20 | |
+ 3 | 30 | |
+ 4 | 10 | 4 | 6
+ 5 | 50 | |
+ 6 | 10 | |
+(6 rows)
+
+-- FIRST/LAST 2-arg offset form
+--
+-- FIRST(val, 0) = FIRST(val): match_start row
+SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt
+FROM rpr_nav WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B+)
+ DEFINE A AS TRUE, B AS FIRST(val, 0) = 10
+);
+ id | val | mf | cnt
+----+-----+----+-----
+ 1 | 10 | 1 | 6
+ 2 | 20 | | 0
+ 3 | 30 | | 0
+ 4 | 10 | | 0
+ 5 | 50 | | 0
+ 6 | 10 | | 0
+(6 rows)
+
+-- FIRST(val, 1): match_start + 1 row (second row of match)
+-- match_start=1(10): FIRST(val,1)=20, B needs val=20 → id2(20) match, id3(30) no
+-- match_start=3(30): FIRST(val,1)=10, B needs val=10 → id4(10) match
+SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt
+FROM rpr_nav WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B+)
+ DEFINE A AS TRUE, B AS val = FIRST(val, 1)
+);
+ id | val | mf | cnt
+----+-----+----+-----
+ 1 | 10 | 1 | 2
+ 2 | 20 | | 0
+ 3 | 30 | 3 | 2
+ 4 | 10 | | 0
+ 5 | 50 | 5 | 2
+ 6 | 10 | | 0
+(6 rows)
+
+-- FIRST(val, 99): offset beyond match range → NULL, no match
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_nav WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B+)
+ DEFINE A AS TRUE, B AS FIRST(val, 99) IS NOT NULL
+);
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 0
+ 2 | 20 | 0
+ 3 | 30 | 0
+ 4 | 10 | 0
+ 5 | 50 | 0
+ 6 | 10 | 0
+(6 rows)
+
+-- LAST(val, 0) = LAST(val): current row
+SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt
+FROM rpr_nav WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B+)
+ DEFINE A AS TRUE, B AS LAST(val, 0) > 15
+);
+ id | val | mf | cnt
+----+-----+----+-----
+ 1 | 10 | 1 | 3
+ 2 | 20 | | 0
+ 3 | 30 | | 0
+ 4 | 10 | 4 | 2
+ 5 | 50 | | 0
+ 6 | 10 | | 0
+(6 rows)
+
+-- LAST(val, 1): one row back from current (previous match row)
+-- At B evaluation on id2: LAST(val,1) = val at id1 = 10
+-- B matches when previous row val < 30
+SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt
+FROM rpr_nav WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B+)
+ DEFINE A AS TRUE, B AS LAST(val, 1) < 30
+);
+ id | val | mf | cnt
+----+-----+----+-----
+ 1 | 10 | 1 | 3
+ 2 | 20 | | 0
+ 3 | 30 | | 0
+ 4 | 10 | 4 | 2
+ 5 | 50 | | 0
+ 6 | 10 | | 0
+(6 rows)
+
+-- LAST(val, 99): offset before match_start → NULL
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_nav WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B+)
+ DEFINE A AS TRUE, B AS LAST(val, 99) IS NOT NULL
+);
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 0
+ 2 | 20 | 0
+ 3 | 30 | 0
+ 4 | 10 | 0
+ 5 | 50 | 0
+ 6 | 10 | 0
+(6 rows)
+
+-- Error: NULL offset
+SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS FIRST(val, NULL::int8) IS NULL
+);
+ERROR: row pattern navigation offset must not be null
+-- Error: negative offset
+SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS LAST(val, -1) IS NULL
+);
+ERROR: row pattern navigation offset must not be negative
+-- FIRST/LAST outside DEFINE clause (error cases)
+SELECT first(val) FROM rpr_nav;
+ERROR: first can only be used in a DEFINE clause
+LINE 1: SELECT first(val) FROM rpr_nav;
+ ^
+SELECT last(val) FROM rpr_nav;
+ERROR: last can only be used in a DEFINE clause
+LINE 1: SELECT last(val) FROM rpr_nav;
+ ^
+SELECT first(val, 1) FROM rpr_nav;
+ERROR: first can only be used in a DEFINE clause
+LINE 1: SELECT first(val, 1) FROM rpr_nav;
+ ^
+-- Functional notation: should access column, not RPR navigation
+CREATE TEMP TABLE rpr_names (prev int, next int, first text, last text);
+INSERT INTO rpr_names VALUES (1, 2, 'Joe', 'Blow');
+SELECT prev(f), next(f), first(f), last(f) FROM rpr_names f;
+ prev | next | first | last
+------+------+-------+------
+ 1 | 2 | Joe | Blow
+(1 row)
+
+DROP TABLE rpr_names;
+-- Compound navigation: PREV(FIRST(val), M)
+-- rpr_nav: (1,10),(2,20),(3,30),(4,10),(5,50),(6,10)
+-- PREV(FIRST(val), 1): target = match_start + 0 - 1 = match_start - 1
+-- At match_start=1: target=0 → out of range → NULL
+-- At match_start=3: target=2(val=20) → 20 > 0 → true
+SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt
+FROM rpr_nav WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B+)
+ DEFINE A AS TRUE, B AS PREV(FIRST(val), 1) > 0
+);
+ id | val | mf | cnt
+----+-----+----+-----
+ 1 | 10 | | 0
+ 2 | 20 | 2 | 5
+ 3 | 30 | | 0
+ 4 | 10 | | 0
+ 5 | 50 | | 0
+ 6 | 10 | | 0
+(6 rows)
+
+-- NEXT(FIRST(val, 1), 1): target = match_start + 1 + 1 = match_start + 2
+-- At match_start=1, B on id2: target=1+1+1=3(val=30), 30>0 → true
+SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt
+FROM rpr_nav WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B+)
+ DEFINE A AS TRUE, B AS NEXT(FIRST(val, 1), 1) > 0
+);
+ id | val | mf | cnt
+----+-----+----+-----
+ 1 | 10 | 1 | 6
+ 2 | 20 | | 0
+ 3 | 30 | | 0
+ 4 | 10 | | 0
+ 5 | 50 | | 0
+ 6 | 10 | | 0
+(6 rows)
+
+-- PREV(LAST(val), 2): target = currentpos - 0 - 2 = currentpos - 2
+-- Same backward reach as PREV(val, 2)
+SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt
+FROM rpr_nav WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B+)
+ DEFINE A AS TRUE, B AS PREV(LAST(val), 2) IS NOT NULL
+);
+ id | val | mf | cnt
+----+-----+----+-----
+ 1 | 10 | | 0
+ 2 | 20 | 2 | 5
+ 3 | 30 | | 0
+ 4 | 10 | | 0
+ 5 | 50 | | 0
+ 6 | 10 | | 0
+(6 rows)
+
+-- NEXT(LAST(val, 1), 2): target = currentpos - 1 + 2 = currentpos + 1
+-- Looks 1 row ahead: same as NEXT(val, 1)
+SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt
+FROM rpr_nav WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B+)
+ DEFINE A AS TRUE, B AS NEXT(LAST(val, 1), 2) IS NOT NULL
+);
+ id | val | mf | cnt
+----+-----+----+-----
+ 1 | 10 | 1 | 5
+ 2 | 20 | | 0
+ 3 | 30 | | 0
+ 4 | 10 | | 0
+ 5 | 50 | | 0
+ 6 | 10 | | 0
+(6 rows)
+
+-- Compound: outer offset beyond partition (PREV far back)
+SELECT id, val, count(*) OVER w AS cnt
+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) IS NOT NULL
+);
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 0
+ 2 | 20 | 0
+ 3 | 30 | 0
+ 4 | 10 | 0
+ 5 | 50 | 0
+ 6 | 10 | 0
+(6 rows)
+
+-- Compound: outer offset beyond partition (NEXT far forward)
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_nav WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE A AS TRUE, B AS NEXT(FIRST(val), 99) IS NOT NULL
+);
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 0
+ 2 | 20 | 0
+ 3 | 30 | 0
+ 4 | 10 | 0
+ 5 | 50 | 0
+ 6 | 10 | 0
+(6 rows)
+
+-- Compound: inner offset beyond match range (FIRST offset too large)
+SELECT id, val, count(*) OVER w AS cnt
+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), 1) IS NOT NULL
+);
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 0
+ 2 | 20 | 0
+ 3 | 30 | 0
+ 4 | 10 | 0
+ 5 | 50 | 0
+ 6 | 10 | 0
+(6 rows)
+
+-- Compound: inner offset beyond match range (LAST offset too large)
+SELECT id, val, count(*) OVER w AS cnt
+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 NOT NULL
+);
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 0
+ 2 | 20 | 0
+ 3 | 30 | 0
+ 4 | 10 | 0
+ 5 | 50 | 0
+ 6 | 10 | 0
+(6 rows)
+
+-- Compound: NULL outer offset (runtime error)
+SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS PREV(FIRST(val), NULL::int8) IS NULL
+);
+ERROR: row pattern navigation offset must not be null
+-- Compound: negative outer offset (runtime error)
+SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS NEXT(LAST(val), -1) IS NULL
+);
+ERROR: row pattern navigation offset must not be negative
+-- Compound: default offsets on both sides
+-- PREV(FIRST(val)): inner=0 (match_start), outer=1 → target = match_start - 1
+SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt
+FROM rpr_nav WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B+)
+ DEFINE A AS TRUE, B AS PREV(FIRST(val)) IS NOT NULL
+);
+ id | val | mf | cnt
+----+-----+----+-----
+ 1 | 10 | | 0
+ 2 | 20 | 2 | 5
+ 3 | 30 | | 0
+ 4 | 10 | | 0
+ 5 | 50 | | 0
+ 6 | 10 | | 0
+(6 rows)
+
+-- NEXT(LAST(val)): inner=0 (currentpos), outer=1 → target = currentpos + 1
+SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt
+FROM rpr_nav WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B+)
+ DEFINE A AS TRUE, B AS NEXT(LAST(val)) IS NOT NULL
+);
+ id | val | mf | cnt
+----+-----+----+-----
+ 1 | 10 | 1 | 5
+ 2 | 20 | | 0
+ 3 | 30 | | 0
+ 4 | 10 | | 0
+ 5 | 50 | | 0
+ 6 | 10 | | 0
+(6 rows)
+
+-- Compound: inner NULL offset (runtime error)
+SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS PREV(FIRST(val, NULL::int8), 1) IS NULL
+);
+ERROR: row pattern navigation offset must not be null
+-- Compound: inner negative offset (runtime error)
+SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS NEXT(LAST(val, -1), 1) IS NULL
+);
+ERROR: row pattern navigation offset must not be negative
+-- Compound + host variable offsets
+PREPARE test_compound_offset(int8, int8) AS
+SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt
+FROM rpr_nav WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B+)
+ DEFINE A AS TRUE, B AS PREV(FIRST(val, $1), $2) IS NOT NULL
+);
+EXECUTE test_compound_offset(0, 1);
+ id | val | mf | cnt
+----+-----+----+-----
+ 1 | 10 | | 0
+ 2 | 20 | 2 | 5
+ 3 | 30 | | 0
+ 4 | 10 | | 0
+ 5 | 50 | | 0
+ 6 | 10 | | 0
+(6 rows)
+
+EXECUTE test_compound_offset(1, 1);
+ id | val | mf | cnt
+----+-----+----+-----
+ 1 | 10 | 1 | 6
+ 2 | 20 | | 0
+ 3 | 30 | | 0
+ 4 | 10 | | 0
+ 5 | 50 | | 0
+ 6 | 10 | | 0
+(6 rows)
+
+DEALLOCATE test_compound_offset;
+-- Compound + SKIP TO NEXT ROW: overlapping matches with PREV(FIRST())
+SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt
+FROM rpr_nav WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A B+)
+ DEFINE A AS TRUE, B AS PREV(FIRST(val), 1) > 0
+);
+ id | val | mf | cnt
+----+-----+----+-----
+ 1 | 10 | | 0
+ 2 | 20 | 2 | 5
+ 3 | 30 | 3 | 4
+ 4 | 10 | 4 | 3
+ 5 | 50 | 5 | 2
+ 6 | 10 | | 0
+(6 rows)
+
+-- Compound + multiple partitions
+CREATE TEMP TABLE rpr_nav_part (gid int, id int, val int);
+INSERT INTO rpr_nav_part VALUES
+ (1,1,10),(1,2,20),(1,3,30),
+ (2,1,40),(2,2,50),(2,3,60);
+SELECT gid, id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt
+FROM rpr_nav_part WINDOW w AS (
+ PARTITION BY gid ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B+)
+ DEFINE A AS TRUE, B AS NEXT(FIRST(val), 1) > 0
+);
+ gid | id | val | mf | cnt
+-----+----+-----+----+-----
+ 1 | 1 | 10 | 1 | 3
+ 1 | 2 | 20 | | 0
+ 1 | 3 | 30 | | 0
+ 2 | 1 | 40 | 1 | 3
+ 2 | 2 | 50 | | 0
+ 2 | 3 | 60 | | 0
+(6 rows)
+
+DROP TABLE rpr_nav_part;
+-- Reverse nesting: FIRST wrapping PREV is prohibited
+SELECT id, val 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 FIRST(PREV(val)) > 0
+);
+ERROR: FIRST and LAST cannot contain PREV or NEXT
+LINE 5: DEFINE A AS TRUE, B AS FIRST(PREV(val)) > 0
+ ^
+HINT: Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed.
+-- Reverse nesting: LAST wrapping NEXT is prohibited
+SELECT id, val 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 LAST(NEXT(val)) > 0
+);
+ERROR: FIRST and LAST cannot contain PREV or NEXT
+LINE 5: DEFINE A AS TRUE, B AS LAST(NEXT(val)) > 0
+ ^
+HINT: Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed.
+DROP TABLE rpr_nav;
--
-- SKIP TO / Backtracking / Frame boundary
--
@@ -2247,6 +2850,27 @@ FROM result WHERE match_len > 0;
1 | 99999
(1 row)
+RESET jit_above_cost;
+RESET jit;
+-- JIT compound navigation test
+SET jit = on;
+SET jit_above_cost = 0;
+SELECT count(*) AS matched_rows
+FROM (
+ SELECT v, count(*) OVER w AS match_len
+ FROM generate_series(1, 1000) AS t(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 PREV(FIRST(v), 1) > 0
+ )
+) sub WHERE match_len > 0;
+ matched_rows
+--------------
+ 1
+(1 row)
+
RESET jit_above_cost;
RESET jit;
--
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index 1e450a07ced..0845316965e 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -1629,7 +1629,7 @@ LINE 6: PATTERN (A{,2147483647})
-- Expected: ERROR: quantifier bound must be between 1 and 2147483646
DROP TABLE rpr_bounds;
-- ============================================================
--- Navigation Functions Tests (PREV / NEXT)
+-- Navigation Functions Tests (PREV / NEXT / FIRST / LAST)
-- ============================================================
CREATE TABLE rpr_nav (id INT, val INT);
INSERT INTO rpr_nav VALUES
@@ -1730,6 +1730,81 @@ ERROR: next can only be used in a DEFINE clause
LINE 1: SELECT NEXT(id), id, val, COUNT(*) OVER w as cnt
^
-- Expected: ERROR: next can only be used in a DEFINE clause
+-- FIRST function - reference match_start row
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_nav
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE
+ A AS val > 0,
+ B AS val > FIRST(val)
+)
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 5
+ 2 | 20 | 0
+ 3 | 15 | 0
+ 4 | 25 | 0
+ 5 | 30 | 0
+(5 rows)
+
+-- LAST function without offset - equivalent to current row's value
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_nav
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE
+ A AS val > 0,
+ B AS LAST(val) > PREV(val)
+)
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 2
+ 2 | 20 | 0
+ 3 | 15 | 3
+ 4 | 25 | 0
+ 5 | 30 | 0
+(5 rows)
+
+-- FIRST and LAST combined
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_nav
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE
+ A AS val > 0,
+ B AS val > FIRST(val) AND LAST(val) > PREV(val)
+)
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 2
+ 2 | 20 | 0
+ 3 | 15 | 3
+ 4 | 25 | 0
+ 5 | 30 | 0
+(5 rows)
+
+-- FIRST function cannot be used other than in DEFINE
+SELECT FIRST(id), id, val FROM rpr_nav;
+ERROR: first can only be used in a DEFINE clause
+LINE 1: SELECT FIRST(id), id, val FROM rpr_nav;
+ ^
+-- Expected: ERROR: first can only be used in a DEFINE clause
+-- LAST function cannot be used other than in DEFINE
+SELECT LAST(id), id, val FROM rpr_nav;
+ERROR: last can only be used in a DEFINE clause
+LINE 1: SELECT LAST(id), id, val FROM rpr_nav;
+ ^
+-- Expected: ERROR: last can only be used in a DEFINE clause
DROP TABLE rpr_nav;
-- ============================================================
-- SKIP TO / INITIAL Tests
@@ -2227,6 +2302,150 @@ SELECT pg_get_viewdef('rpr_serial_v8'::regclass);
d AS (val > 30) );
(1 row)
+-- Navigation function serialization: PREV with offset
+CREATE VIEW rpr_serial_nav1 AS
+SELECT id, val, count(*) OVER w
+FROM rpr_serial
+WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE A AS TRUE, B AS val > PREV(val, 2));
+SELECT pg_get_viewdef('rpr_serial_nav1'::regclass);
+ pg_get_viewdef
+------------------------------------------------------------------------------
+ SELECT id, +
+ val, +
+ count(*) OVER w AS count +
+ FROM rpr_serial +
+ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
+ AFTER MATCH SKIP PAST LAST ROW +
+ INITIAL +
+ PATTERN (a b+) +
+ DEFINE +
+ a AS true, +
+ b AS (val > PREV(val, (2)::bigint)) );
+(1 row)
+
+-- Navigation function serialization: FIRST and LAST
+CREATE VIEW rpr_serial_nav2 AS
+SELECT id, val, count(*) OVER w
+FROM rpr_serial
+WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE A AS TRUE, B AS FIRST(val) < LAST(val, 1));
+SELECT pg_get_viewdef('rpr_serial_nav2'::regclass);
+ pg_get_viewdef
+------------------------------------------------------------------------------
+ SELECT id, +
+ val, +
+ count(*) OVER w AS count +
+ FROM rpr_serial +
+ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
+ AFTER MATCH SKIP PAST LAST ROW +
+ INITIAL +
+ PATTERN (a b+) +
+ DEFINE +
+ a AS true, +
+ b AS (FIRST(val) < LAST(val, (1)::bigint)) );
+(1 row)
+
+-- Navigation function serialization: compound PREV(FIRST())
+CREATE VIEW rpr_serial_nav3 AS
+SELECT id, val, count(*) OVER w
+FROM rpr_serial
+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, 1), 2) > 0);
+SELECT pg_get_viewdef('rpr_serial_nav3'::regclass);
+ pg_get_viewdef
+------------------------------------------------------------------------------
+ SELECT id, +
+ val, +
+ count(*) OVER w AS count +
+ FROM rpr_serial +
+ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
+ AFTER MATCH SKIP PAST LAST ROW +
+ INITIAL +
+ PATTERN (a b+) +
+ DEFINE +
+ a AS true, +
+ b AS (PREV(FIRST(val, (1)::bigint), (2)::bigint) > 0) );
+(1 row)
+
+-- Navigation function serialization: compound NEXT(LAST())
+CREATE VIEW rpr_serial_nav4 AS
+SELECT id, val, count(*) OVER w
+FROM rpr_serial
+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), 2) IS NOT NULL);
+SELECT pg_get_viewdef('rpr_serial_nav4'::regclass);
+ pg_get_viewdef
+------------------------------------------------------------------------------
+ SELECT id, +
+ val, +
+ count(*) OVER w AS count +
+ FROM rpr_serial +
+ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
+ AFTER MATCH SKIP PAST LAST ROW +
+ INITIAL +
+ PATTERN (a b+) +
+ DEFINE +
+ a AS true, +
+ b AS (NEXT(LAST(val), (2)::bigint) IS NOT NULL) );
+(1 row)
+
+-- Navigation function serialization: compound PREV(LAST())
+CREATE VIEW rpr_serial_nav5 AS
+SELECT id, val, count(*) OVER w
+FROM rpr_serial
+WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE A AS TRUE, B AS PREV(LAST(val, 1), 2) > 0);
+SELECT pg_get_viewdef('rpr_serial_nav5'::regclass);
+ pg_get_viewdef
+------------------------------------------------------------------------------
+ SELECT id, +
+ val, +
+ count(*) OVER w AS count +
+ FROM rpr_serial +
+ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
+ AFTER MATCH SKIP PAST LAST ROW +
+ INITIAL +
+ PATTERN (a b+) +
+ DEFINE +
+ a AS true, +
+ b AS (PREV(LAST(val, (1)::bigint), (2)::bigint) > 0) );
+(1 row)
+
+-- Navigation function serialization: compound NEXT(FIRST())
+CREATE VIEW rpr_serial_nav6 AS
+SELECT id, val, count(*) OVER w
+FROM rpr_serial
+WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE A AS TRUE, B AS NEXT(FIRST(val), 3) > 0);
+SELECT pg_get_viewdef('rpr_serial_nav6'::regclass);
+ pg_get_viewdef
+------------------------------------------------------------------------------
+ SELECT id, +
+ val, +
+ count(*) OVER w AS count +
+ FROM rpr_serial +
+ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
+ AFTER MATCH SKIP PAST LAST ROW +
+ INITIAL +
+ PATTERN (a b+) +
+ DEFINE +
+ a AS true, +
+ b AS (NEXT(FIRST(val), (3)::bigint) > 0) );
+(1 row)
+
-- Reluctant {1}? quantifier deparse through ruleutils
CREATE VIEW rpr_quant_reluctant_v AS
SELECT id, val, count(*) OVER w
@@ -3021,6 +3240,42 @@ ORDER BY id;
(3 rows)
DROP TABLE rpr_null;
+-- Compound navigation: inner nav must be direct arg (not nested in expression)
+SELECT count(*) OVER w
+FROM generate_series(1,10) s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS PREV(v + FIRST(v)) > 0
+);
+ERROR: row pattern navigation operation must be a direct argument of the outer navigation
+LINE 6: DEFINE A AS PREV(v + FIRST(v)) > 0
+ ^
+HINT: Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed.
+-- FIRST/LAST wrapping FIRST/LAST: prohibited
+SELECT count(*) OVER w
+FROM generate_series(1,10) s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS FIRST(FIRST(v)) > 0
+);
+ERROR: FIRST and LAST cannot contain FIRST or LAST
+LINE 6: DEFINE A AS FIRST(FIRST(v)) > 0
+ ^
+HINT: Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed.
+-- Triple nesting: prohibited (3-level deep navigation)
+SELECT count(*) OVER w
+FROM generate_series(1,10) s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS PREV(FIRST(PREV(v))) > 0
+);
+ERROR: row pattern navigation cannot be nested more than two levels deep
+LINE 6: DEFINE A AS PREV(FIRST(PREV(v))) > 0
+ ^
+HINT: Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed.
-- ============================================================
-- Window Deduplication Tests
-- ============================================================
diff --git a/src/test/regress/expected/rpr_explain.out b/src/test/regress/expected/rpr_explain.out
index 77ab25a2289..dc3522f930f 100644
--- a/src/test/regress/expected/rpr_explain.out
+++ b/src/test/regress/expected/rpr_explain.out
@@ -36,7 +36,7 @@
-- Window Function Combinations
-- DEFINE Expression Variations
-- Large Scale Statistics Verification
--- Nav Mark Lookback (tuplestore trim)
+-- Nav Mark Lookback/Lookahead (tuplestore trim)
-- ============================================================
-- Filter function to normalize platform-dependent memory values (not NFA statistics).
-- NFA statistics should not change between platforms; if they do, it could
@@ -1131,6 +1131,127 @@ WINDOW w AS (
-> Function Scan on generate_series s (actual rows=42.00 loops=1)
(10 rows)
+-- No absorption when DEFINE uses FIRST (match_start-dependent)
+-- Same pattern as rpr_ev_ctx_absorb_unbounded but with FIRST in DEFINE.
+-- Compare: absorbed count should be 0 here vs >0 above.
+CREATE VIEW rpr_ev_ctx_no_absorb_first AS
+SELECT count(*) OVER w
+FROM generate_series(1, 50) 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 v % 5 <> 0, B AS v % 5 = 0 AND v > FIRST(v)
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_no_absorb_first'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+-------------------
+ PATTERN (a+ b)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 50) 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 v % 5 <> 0, B AS v % 5 = 0 AND v > FIRST(v)
+);');
+ rpr_explain_filter
+----------------------------------------------------------------------
+ WindowAgg (actual rows=50.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a+ b
+ Nav Mark Lookback: 0
+ Nav Mark Lookahead: 0
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 9 peak, 151 total, 0 merged
+ NFA Contexts: 5 peak, 51 total, 0 pruned
+ NFA: 10 matched (len 5/5/5.0), 0 mismatched
+ NFA: 0 absorbed, 40 skipped (len 1/4/2.5)
+ -> Function Scan on generate_series s (actual rows=50.00 loops=1)
+(11 rows)
+
+-- Absorption preserved when DEFINE uses only LAST without offset
+-- LAST(v) is match_start-independent (always currentpos), so absorption
+-- remains active. Compare: absorbed count should be >0, like the
+-- PREV-only case above.
+CREATE VIEW rpr_ev_ctx_absorb_last AS
+SELECT count(*) OVER w
+FROM generate_series(1, 50) 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 v % 5 <> 0, B AS LAST(v) % 5 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_absorb_last'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+-------------------
+ PATTERN (a+ b)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 50) 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 v % 5 <> 0, B AS LAST(v) % 5 = 0
+);');
+ rpr_explain_filter
+----------------------------------------------------------------------
+ WindowAgg (actual rows=50.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a+" b
+ Nav Mark Lookback: 0
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 3 peak, 91 total, 0 merged
+ NFA Contexts: 2 peak, 51 total, 0 pruned
+ NFA: 10 matched (len 5/5/5.0), 0 mismatched
+ NFA: 30 absorbed (len 1/1/1.0), 10 skipped (len 1/1/1.0)
+ -> Function Scan on generate_series s (actual rows=50.00 loops=1)
+(10 rows)
+
+-- No absorption with compound PREV(FIRST()) (match_start-dependent)
+CREATE VIEW rpr_ev_ctx_no_absorb_compound AS
+SELECT count(*) OVER w
+FROM generate_series(1, 50) 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 v % 5 <> 0, B AS v % 5 = 0 AND PREV(FIRST(v), 1) IS NOT NULL
+);
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 50) 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 v % 5 <> 0, B AS v % 5 = 0 AND PREV(FIRST(v), 1) IS NOT NULL
+);');
+ rpr_explain_filter
+----------------------------------------------------------------------
+ WindowAgg (actual rows=50.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a+ b
+ Nav Mark Lookback: 0
+ Nav Mark Lookahead: -1
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 9 peak, 151 total, 0 merged
+ NFA Contexts: 5 peak, 51 total, 0 pruned
+ NFA: 10 matched (len 4/5/4.9), 1 mismatched (len 5/5/5.0)
+ NFA: 0 absorbed, 39 skipped (len 1/4/2.5)
+ -> Function Scan on generate_series s (actual rows=50.00 loops=1)
+(11 rows)
+
-- ============================================================
-- Match Length Statistics Tests
-- ============================================================
@@ -4343,9 +4464,10 @@ SELECT * FROM (
(9 rows)
-- ============================================================
--- Nav Mark Lookback Tests
--- Verifies planner-computed navigation offset for tuplestore trim.
--- Lookback: how far back from currentpos (PREV/LAST).
+-- Nav Mark Lookback/Lookahead Tests
+-- Verifies planner-computed navigation offsets for tuplestore trim.
+-- Lookback: how far back from currentpos (PREV, LAST, compound PREV_LAST/NEXT_LAST).
+-- Lookahead: how far forward from match_start (FIRST, compound PREV_FIRST/NEXT_FIRST).
-- ============================================================
-- Prepare statement for host variable offset test below
PREPARE rpr_nav_offset_prep(int8) AS
@@ -4466,3 +4588,298 @@ EXPLAIN (COSTS OFF) EXECUTE rpr_nav_offset_prep(2);
RESET plan_cache_mode;
DEALLOCATE rpr_nav_offset_prep;
+-- FIRST(v): retain all (references match_start row)
+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)
+);
+ QUERY PLAN
+-------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a+
+ Nav Mark Lookback: 0
+ Nav Mark Lookahead: 0
+ -> 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)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS LAST(v, 1) > 0
+);
+ QUERY PLAN
+-------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a+
+ Nav Mark Lookback: 1
+ -> Function Scan on generate_series s
+(5 rows)
+
+-- LAST(v) without offset + PREV(v): no match_start dependency, offset 1
+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 LAST(v) > PREV(v)
+);
+ QUERY PLAN
+-------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a+"
+ Nav Mark Lookback: 1
+ -> Function Scan on generate_series s
+(5 rows)
+
+-- Compound PREV(FIRST(val, 1), 2): lookback from match_start, firstOffset = 1-2 = -1
+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 PREV(FIRST(v, 1), 2) > 0
+);
+ QUERY PLAN
+-------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a+
+ Nav Mark Lookback: 0
+ Nav Mark Lookahead: -1
+ -> Function Scan on generate_series s
+(6 rows)
+
+-- Compound NEXT(FIRST(val), 3): firstOffset = 0+3 = 3
+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 NEXT(FIRST(v), 3) > 0
+);
+ QUERY PLAN
+-------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a+
+ Nav Mark Lookback: 0
+ Nav Mark Lookahead: 3
+ -> Function Scan on generate_series s
+(6 rows)
+
+-- Compound PREV(LAST(val), 2): lookback = 0+2 = 2
+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 PREV(LAST(v), 2) > 0
+);
+ QUERY PLAN
+-------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a+"
+ Nav Mark Lookback: 2
+ -> Function Scan on generate_series s
+(5 rows)
+
+-- Compound NEXT(LAST(val, 1), 3): lookback = max(1-3, 0) = 0
+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 NEXT(LAST(v, 1), 3) > 0
+);
+ QUERY PLAN
+-------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a+
+ Nav Mark Lookback: 0
+ -> Function Scan on generate_series s
+(5 rows)
+
+-- Compound PREV(LAST(val, N), M): constant near-overflow (N+M just fits int64)
+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 PREV(LAST(v, 4611686018427387903), 4611686018427387903) IS NOT NULL
+);
+ QUERY PLAN
+-------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a+
+ Nav Mark Lookback: 9223372036854775806
+ -> Function Scan on generate_series s
+(5 rows)
+
+-- Compound PREV(LAST(val, N), M): constant overflow -> retain all
+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 PREV(LAST(v, 4611686018427387904), 4611686018427387904) IS NOT NULL
+);
+ QUERY PLAN
+-------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a+
+ Nav Mark Lookback: retain all
+ -> Function Scan on generate_series s
+(5 rows)
+
+-- Compound NEXT(FIRST(val, N), M): constant lookahead overflow -> no trim impact
+-- N + M overflows int64, but target is forward from match_start so it never
+-- constrains trim. Lookahead remains at default (0).
+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 NEXT(FIRST(v, 4611686018427387904), 4611686018427387904) IS NOT NULL
+);
+ QUERY PLAN
+-------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a+
+ Nav Mark Lookback: 0
+ Nav Mark Lookahead: 0
+ -> Function Scan on generate_series s
+(6 rows)
+
+-- Compound PREV(LAST(val, $1), $2): parameter lookback overflow -> retain all
+-- EXPLAIN shows "runtime" (plan-level); EXPLAIN ANALYZE shows "retain all"
+-- (executor-resolved).
+PREPARE test_overflow_lookback(int8, int8) AS
+SELECT count(*) OVER w
+FROM generate_series(1,10) s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS PREV(LAST(v, $1), $2) IS NOT NULL
+);
+SET plan_cache_mode = force_generic_plan;
+EXPLAIN (COSTS OFF) EXECUTE test_overflow_lookback(4611686018427387904, 4611686018427387904);
+ 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)
+
+EXPLAIN (COSTS OFF, ANALYZE, TIMING OFF, SUMMARY OFF)
+ EXECUTE test_overflow_lookback(4611686018427387904, 4611686018427387904);
+ QUERY PLAN
+----------------------------------------------------------------------
+ WindowAgg (actual rows=10.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a+
+ Nav Mark Lookback: retain all
+ Storage: Memory Maximum Storage: 17kB
+ NFA States: 1 peak, 11 total, 0 merged
+ NFA Contexts: 2 peak, 11 total, 10 pruned
+ NFA: 0 matched, 0 mismatched
+ -> Function Scan on generate_series s (actual rows=10.00 loops=1)
+(9 rows)
+
+RESET plan_cache_mode;
+DEALLOCATE test_overflow_lookback;
+-- Compound NEXT(FIRST(val, $1), $2): parameter lookahead overflow -> no trim impact
+PREPARE test_overflow_lookahead(int8, int8) AS
+SELECT count(*) OVER w
+FROM generate_series(1,10) s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS NEXT(FIRST(v, $1), $2) IS NOT NULL
+);
+SET plan_cache_mode = force_generic_plan;
+EXPLAIN (COSTS OFF, ANALYZE, TIMING OFF, SUMMARY OFF)
+ EXECUTE test_overflow_lookahead(4611686018427387904, 4611686018427387904);
+ QUERY PLAN
+----------------------------------------------------------------------
+ WindowAgg (actual rows=10.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a+
+ Nav Mark Lookback: 0
+ Nav Mark Lookahead: 0
+ Storage: Memory Maximum Storage: 17kB
+ NFA States: 1 peak, 11 total, 0 merged
+ NFA Contexts: 2 peak, 11 total, 10 pruned
+ NFA: 0 matched, 0 mismatched
+ -> Function Scan on generate_series s (actual rows=10.00 loops=1)
+(10 rows)
+
+RESET plan_cache_mode;
+DEALLOCATE test_overflow_lookahead;
+-- PREV(v) + PREV(v, $1): NEEDS_EVAL path must account for implicit lookback=1
+-- Previously, eval_nav_max_offset_walker skipped PREV(v) when offset_arg was
+-- NULL, causing maxOffset=0 when $1=0, which would trim the row needed by
+-- PREV(v). Verify this executes without "cannot fetch row before mark" error.
+PREPARE test_prev_implicit_offset(int8) AS
+SELECT count(*) OVER w
+FROM generate_series(1,10) s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS PREV(v) IS NOT NULL AND PREV(v, $1) IS NOT NULL
+);
+EXECUTE test_prev_implicit_offset(0);
+ count
+-------
+ 0
+ 9
+ 0
+ 0
+ 0
+ 0
+ 0
+ 0
+ 0
+ 0
+(10 rows)
+
+DEALLOCATE test_prev_implicit_offset;
+-- Runtime error: negative offset at execution time
+PREPARE test_runtime_neg_offset(int8) AS
+SELECT count(*) OVER w
+FROM generate_series(1,10) s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS PREV(v, $1) IS NOT NULL
+);
+EXECUTE test_runtime_neg_offset(-1);
+ERROR: row pattern navigation offset must not be negative
+DEALLOCATE test_runtime_neg_offset;
+-- Runtime error: null offset at execution time
+PREPARE test_runtime_null_offset(int8) AS
+SELECT count(*) OVER w
+FROM generate_series(1,10) s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS PREV(v, $1) IS NOT NULL
+);
+EXECUTE test_runtime_null_offset(NULL);
+ERROR: row pattern navigation offset must not be null
+DEALLOCATE test_runtime_null_offset;
diff --git a/src/test/regress/sql/rpr.sql b/src/test/regress/sql/rpr.sql
index 47f33904690..a05b429ce74 100644
--- a/src/test/regress/sql/rpr.sql
+++ b/src/test/regress/sql/rpr.sql
@@ -776,6 +776,363 @@ WINDOW w AS (
DEFINE A AS price > PREV(price, 1) AND price < NEXT(price, 1)
);
+--
+-- FIRST/LAST navigation
+--
+
+-- Test data for FIRST/LAST: values cycle back so FIRST(val) = LAST(val)
+-- at specific positions.
+CREATE TEMP TABLE rpr_nav (id int, val int);
+INSERT INTO rpr_nav VALUES (1,10),(2,20),(3,30),(4,10),(5,50),(6,10);
+
+-- FIRST(val) = constant: B matches when match_start has val=10
+-- match_start=1(10): A=id1, B=id2, FIRST(val)=10 → match {1,2}
+-- match_start=3(30): A=id3, B=id4, FIRST(val)=30≠10 → no match
+-- match_start=4(10): A=id4, B=id5, FIRST(val)=10 → match {4,5}
+SELECT id, val, first_value(id) OVER w AS mf, last_value(id) OVER w AS ml
+FROM rpr_nav WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B)
+ DEFINE A AS TRUE, B AS FIRST(val) = 10
+);
+
+-- LAST(val): always equals current row's val (offset 0 default)
+-- Equivalent to: B AS val > 15
+SELECT id, val, first_value(id) OVER w AS mf, last_value(id) OVER w AS ml
+FROM rpr_nav WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B)
+ DEFINE A AS TRUE, B AS LAST(val) > 15
+);
+
+-- Reluctant A+? with FIRST(val) = LAST(val): find shortest match where
+-- first and last rows have the same val.
+-- match_start=1(10): reluctant tries B early:
+-- id2(20≠10), id3(30≠10), id4(10=10) → match {1,2,3,4}
+-- match_start=5(50): id6(10≠50) → no match
+SELECT id, val, first_value(id) OVER w AS mf, last_value(id) OVER w AS ml
+FROM rpr_nav WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+? B)
+ DEFINE A AS TRUE, B AS FIRST(val) = LAST(val)
+);
+
+-- Greedy A+ with FIRST(val) = LAST(val): find longest match where
+-- first and last rows have the same val.
+-- match_start=1(10): greedy A eats all, B tries last:
+-- id6(10=10) → match {1,2,3,4,5,6}
+SELECT id, val, first_value(id) OVER w AS mf, last_value(id) OVER w AS ml
+FROM rpr_nav WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+ B)
+ DEFINE A AS TRUE, B AS FIRST(val) = LAST(val)
+);
+
+-- SKIP TO NEXT ROW with FIRST(val) = LAST(val): overlapping match attempts.
+-- With ONE ROW PER MATCH, each row shows only its first match result.
+SELECT id, val, first_value(id) OVER w AS mf, last_value(id) OVER w AS ml
+FROM rpr_nav WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A+? B)
+ DEFINE A AS TRUE, B AS FIRST(val) = LAST(val)
+);
+
+-- FIRST/LAST 2-arg offset form
+--
+-- FIRST(val, 0) = FIRST(val): match_start row
+SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt
+FROM rpr_nav WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B+)
+ DEFINE A AS TRUE, B AS FIRST(val, 0) = 10
+);
+
+-- FIRST(val, 1): match_start + 1 row (second row of match)
+-- match_start=1(10): FIRST(val,1)=20, B needs val=20 → id2(20) match, id3(30) no
+-- match_start=3(30): FIRST(val,1)=10, B needs val=10 → id4(10) match
+SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt
+FROM rpr_nav WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B+)
+ DEFINE A AS TRUE, B AS val = FIRST(val, 1)
+);
+
+-- FIRST(val, 99): offset beyond match range → NULL, no match
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_nav WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B+)
+ DEFINE A AS TRUE, B AS FIRST(val, 99) IS NOT NULL
+);
+
+-- LAST(val, 0) = LAST(val): current row
+SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt
+FROM rpr_nav WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B+)
+ DEFINE A AS TRUE, B AS LAST(val, 0) > 15
+);
+
+-- LAST(val, 1): one row back from current (previous match row)
+-- At B evaluation on id2: LAST(val,1) = val at id1 = 10
+-- B matches when previous row val < 30
+SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt
+FROM rpr_nav WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B+)
+ DEFINE A AS TRUE, B AS LAST(val, 1) < 30
+);
+
+-- LAST(val, 99): offset before match_start → NULL
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_nav WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B+)
+ DEFINE A AS TRUE, B AS LAST(val, 99) IS NOT NULL
+);
+
+-- Error: NULL offset
+SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS FIRST(val, NULL::int8) IS NULL
+);
+
+-- Error: negative offset
+SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS LAST(val, -1) IS NULL
+);
+
+-- FIRST/LAST outside DEFINE clause (error cases)
+SELECT first(val) FROM rpr_nav;
+SELECT last(val) FROM rpr_nav;
+SELECT first(val, 1) FROM rpr_nav;
+
+-- Functional notation: should access column, not RPR navigation
+CREATE TEMP TABLE rpr_names (prev int, next int, first text, last text);
+INSERT INTO rpr_names VALUES (1, 2, 'Joe', 'Blow');
+SELECT prev(f), next(f), first(f), last(f) FROM rpr_names f;
+DROP TABLE rpr_names;
+
+-- Compound navigation: PREV(FIRST(val), M)
+-- rpr_nav: (1,10),(2,20),(3,30),(4,10),(5,50),(6,10)
+-- PREV(FIRST(val), 1): target = match_start + 0 - 1 = match_start - 1
+-- At match_start=1: target=0 → out of range → NULL
+-- At match_start=3: target=2(val=20) → 20 > 0 → true
+SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt
+FROM rpr_nav WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B+)
+ DEFINE A AS TRUE, B AS PREV(FIRST(val), 1) > 0
+);
+
+-- NEXT(FIRST(val, 1), 1): target = match_start + 1 + 1 = match_start + 2
+-- At match_start=1, B on id2: target=1+1+1=3(val=30), 30>0 → true
+SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt
+FROM rpr_nav WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B+)
+ DEFINE A AS TRUE, B AS NEXT(FIRST(val, 1), 1) > 0
+);
+
+-- PREV(LAST(val), 2): target = currentpos - 0 - 2 = currentpos - 2
+-- Same backward reach as PREV(val, 2)
+SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt
+FROM rpr_nav WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B+)
+ DEFINE A AS TRUE, B AS PREV(LAST(val), 2) IS NOT NULL
+);
+
+-- NEXT(LAST(val, 1), 2): target = currentpos - 1 + 2 = currentpos + 1
+-- Looks 1 row ahead: same as NEXT(val, 1)
+SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt
+FROM rpr_nav WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B+)
+ DEFINE A AS TRUE, B AS NEXT(LAST(val, 1), 2) IS NOT NULL
+);
+
+-- Compound: outer offset beyond partition (PREV far back)
+SELECT id, val, count(*) OVER w AS cnt
+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) IS NOT NULL
+);
+
+-- Compound: outer offset beyond partition (NEXT far forward)
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_nav WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE A AS TRUE, B AS NEXT(FIRST(val), 99) IS NOT NULL
+);
+
+-- Compound: inner offset beyond match range (FIRST offset too large)
+SELECT id, val, count(*) OVER w AS cnt
+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), 1) IS NOT NULL
+);
+
+-- Compound: inner offset beyond match range (LAST offset too large)
+SELECT id, val, count(*) OVER w AS cnt
+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 NOT NULL
+);
+
+-- Compound: NULL outer offset (runtime error)
+SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS PREV(FIRST(val), NULL::int8) IS NULL
+);
+
+-- Compound: negative outer offset (runtime error)
+SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS NEXT(LAST(val), -1) IS NULL
+);
+
+-- Compound: default offsets on both sides
+-- PREV(FIRST(val)): inner=0 (match_start), outer=1 → target = match_start - 1
+SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt
+FROM rpr_nav WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B+)
+ DEFINE A AS TRUE, B AS PREV(FIRST(val)) IS NOT NULL
+);
+
+-- NEXT(LAST(val)): inner=0 (currentpos), outer=1 → target = currentpos + 1
+SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt
+FROM rpr_nav WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B+)
+ DEFINE A AS TRUE, B AS NEXT(LAST(val)) IS NOT NULL
+);
+
+-- Compound: inner NULL offset (runtime error)
+SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS PREV(FIRST(val, NULL::int8), 1) IS NULL
+);
+
+-- Compound: inner negative offset (runtime error)
+SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS NEXT(LAST(val, -1), 1) IS NULL
+);
+
+-- Compound + host variable offsets
+PREPARE test_compound_offset(int8, int8) AS
+SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt
+FROM rpr_nav WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B+)
+ DEFINE A AS TRUE, B AS PREV(FIRST(val, $1), $2) IS NOT NULL
+);
+EXECUTE test_compound_offset(0, 1);
+EXECUTE test_compound_offset(1, 1);
+DEALLOCATE test_compound_offset;
+
+-- Compound + SKIP TO NEXT ROW: overlapping matches with PREV(FIRST())
+SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt
+FROM rpr_nav WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO NEXT ROW
+ PATTERN (A B+)
+ DEFINE A AS TRUE, B AS PREV(FIRST(val), 1) > 0
+);
+
+-- Compound + multiple partitions
+CREATE TEMP TABLE rpr_nav_part (gid int, id int, val int);
+INSERT INTO rpr_nav_part VALUES
+ (1,1,10),(1,2,20),(1,3,30),
+ (2,1,40),(2,2,50),(2,3,60);
+SELECT gid, id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt
+FROM rpr_nav_part WINDOW w AS (
+ PARTITION BY gid ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A B+)
+ DEFINE A AS TRUE, B AS NEXT(FIRST(val), 1) > 0
+);
+DROP TABLE rpr_nav_part;
+
+-- Reverse nesting: FIRST wrapping PREV is prohibited
+SELECT id, val 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 FIRST(PREV(val)) > 0
+);
+
+-- Reverse nesting: LAST wrapping NEXT is prohibited
+SELECT id, val 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 LAST(NEXT(val)) > 0
+);
+
+DROP TABLE rpr_nav;
+
--
-- SKIP TO / Backtracking / Frame boundary
--
@@ -1129,6 +1486,23 @@ FROM result WHERE match_len > 0;
RESET jit_above_cost;
RESET jit;
+-- JIT compound navigation test
+SET jit = on;
+SET jit_above_cost = 0;
+SELECT count(*) AS matched_rows
+FROM (
+ SELECT v, count(*) OVER w AS match_len
+ FROM generate_series(1, 1000) AS t(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 PREV(FIRST(v), 1) > 0
+ )
+) sub WHERE match_len > 0;
+RESET jit_above_cost;
+RESET jit;
+
--
-- IGNORE NULLS
--
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index 3accecb73ba..f9d5aa89d7a 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -1202,7 +1202,7 @@ WINDOW w AS (
DROP TABLE rpr_bounds;
-- ============================================================
--- Navigation Functions Tests (PREV / NEXT)
+-- Navigation Functions Tests (PREV / NEXT / FIRST / LAST)
-- ============================================================
@@ -1278,6 +1278,53 @@ WINDOW w AS (
ORDER BY id;
-- Expected: ERROR: next can only be used in a DEFINE clause
+-- FIRST function - reference match_start row
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_nav
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE
+ A AS val > 0,
+ B AS val > FIRST(val)
+)
+ORDER BY id;
+
+-- LAST function without offset - equivalent to current row's value
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_nav
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE
+ A AS val > 0,
+ B AS LAST(val) > PREV(val)
+)
+ORDER BY id;
+
+-- FIRST and LAST combined
+SELECT id, val, COUNT(*) OVER w as cnt
+FROM rpr_nav
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE
+ A AS val > 0,
+ B AS val > FIRST(val) AND LAST(val) > PREV(val)
+)
+ORDER BY id;
+
+-- FIRST function cannot be used other than in DEFINE
+SELECT FIRST(id), id, val FROM rpr_nav;
+-- Expected: ERROR: first can only be used in a DEFINE clause
+
+-- LAST function cannot be used other than in DEFINE
+SELECT LAST(id), id, val FROM rpr_nav;
+-- Expected: ERROR: last can only be used in a DEFINE clause
+
DROP TABLE rpr_nav;
-- ============================================================
@@ -1548,6 +1595,66 @@ WINDOW w AS (
SELECT * FROM rpr_serial_v8 ORDER BY id;
SELECT pg_get_viewdef('rpr_serial_v8'::regclass);
+-- Navigation function serialization: PREV with offset
+CREATE VIEW rpr_serial_nav1 AS
+SELECT id, val, count(*) OVER w
+FROM rpr_serial
+WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE A AS TRUE, B AS val > PREV(val, 2));
+SELECT pg_get_viewdef('rpr_serial_nav1'::regclass);
+
+-- Navigation function serialization: FIRST and LAST
+CREATE VIEW rpr_serial_nav2 AS
+SELECT id, val, count(*) OVER w
+FROM rpr_serial
+WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE A AS TRUE, B AS FIRST(val) < LAST(val, 1));
+SELECT pg_get_viewdef('rpr_serial_nav2'::regclass);
+
+-- Navigation function serialization: compound PREV(FIRST())
+CREATE VIEW rpr_serial_nav3 AS
+SELECT id, val, count(*) OVER w
+FROM rpr_serial
+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, 1), 2) > 0);
+SELECT pg_get_viewdef('rpr_serial_nav3'::regclass);
+
+-- Navigation function serialization: compound NEXT(LAST())
+CREATE VIEW rpr_serial_nav4 AS
+SELECT id, val, count(*) OVER w
+FROM rpr_serial
+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), 2) IS NOT NULL);
+SELECT pg_get_viewdef('rpr_serial_nav4'::regclass);
+
+-- Navigation function serialization: compound PREV(LAST())
+CREATE VIEW rpr_serial_nav5 AS
+SELECT id, val, count(*) OVER w
+FROM rpr_serial
+WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE A AS TRUE, B AS PREV(LAST(val, 1), 2) > 0);
+SELECT pg_get_viewdef('rpr_serial_nav5'::regclass);
+
+-- Navigation function serialization: compound NEXT(FIRST())
+CREATE VIEW rpr_serial_nav6 AS
+SELECT id, val, count(*) OVER w
+FROM rpr_serial
+WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE A AS TRUE, B AS NEXT(FIRST(val), 3) > 0);
+SELECT pg_get_viewdef('rpr_serial_nav6'::regclass);
+
-- Reluctant {1}? quantifier deparse through ruleutils
CREATE VIEW rpr_quant_reluctant_v AS
SELECT id, val, count(*) OVER w
@@ -2121,6 +2228,33 @@ ORDER BY id;
DROP TABLE rpr_null;
+-- Compound navigation: inner nav must be direct arg (not nested in expression)
+SELECT count(*) OVER w
+FROM generate_series(1,10) s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS PREV(v + FIRST(v)) > 0
+);
+
+-- FIRST/LAST wrapping FIRST/LAST: prohibited
+SELECT count(*) OVER w
+FROM generate_series(1,10) s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS FIRST(FIRST(v)) > 0
+);
+
+-- Triple nesting: prohibited (3-level deep navigation)
+SELECT count(*) OVER w
+FROM generate_series(1,10) s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS PREV(FIRST(PREV(v))) > 0
+);
+
-- ============================================================
-- Window Deduplication Tests
-- ============================================================
diff --git a/src/test/regress/sql/rpr_explain.sql b/src/test/regress/sql/rpr_explain.sql
index 5082cc2b5de..a3789e92631 100644
--- a/src/test/regress/sql/rpr_explain.sql
+++ b/src/test/regress/sql/rpr_explain.sql
@@ -36,7 +36,7 @@
-- Window Function Combinations
-- DEFINE Expression Variations
-- Large Scale Statistics Verification
--- Nav Mark Lookback (tuplestore trim)
+-- Nav Mark Lookback/Lookahead (tuplestore trim)
-- ============================================================
-- Filter function to normalize platform-dependent memory values (not NFA statistics).
@@ -705,6 +705,76 @@ WINDOW w AS (
C AS v % 21 IN (3, 5, 8, 10, 13, 15, 18, 20)
);');
+-- No absorption when DEFINE uses FIRST (match_start-dependent)
+-- Same pattern as rpr_ev_ctx_absorb_unbounded but with FIRST in DEFINE.
+-- Compare: absorbed count should be 0 here vs >0 above.
+CREATE VIEW rpr_ev_ctx_no_absorb_first AS
+SELECT count(*) OVER w
+FROM generate_series(1, 50) 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 v % 5 <> 0, B AS v % 5 = 0 AND v > FIRST(v)
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_no_absorb_first'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 50) 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 v % 5 <> 0, B AS v % 5 = 0 AND v > FIRST(v)
+);');
+
+-- Absorption preserved when DEFINE uses only LAST without offset
+-- LAST(v) is match_start-independent (always currentpos), so absorption
+-- remains active. Compare: absorbed count should be >0, like the
+-- PREV-only case above.
+CREATE VIEW rpr_ev_ctx_absorb_last AS
+SELECT count(*) OVER w
+FROM generate_series(1, 50) 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 v % 5 <> 0, B AS LAST(v) % 5 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_absorb_last'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 50) 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 v % 5 <> 0, B AS LAST(v) % 5 = 0
+);');
+
+-- No absorption with compound PREV(FIRST()) (match_start-dependent)
+CREATE VIEW rpr_ev_ctx_no_absorb_compound AS
+SELECT count(*) OVER w
+FROM generate_series(1, 50) 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 v % 5 <> 0, B AS v % 5 = 0 AND PREV(FIRST(v), 1) IS NOT NULL
+);
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 50) 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 v % 5 <> 0, B AS v % 5 = 0 AND PREV(FIRST(v), 1) IS NOT NULL
+);');
+
-- ============================================================
-- Match Length Statistics Tests
-- ============================================================
@@ -2478,9 +2548,10 @@ SELECT * FROM (
) t WHERE cnt > 0;
-- ============================================================
--- Nav Mark Lookback Tests
--- Verifies planner-computed navigation offset for tuplestore trim.
--- Lookback: how far back from currentpos (PREV/LAST).
+-- Nav Mark Lookback/Lookahead Tests
+-- Verifies planner-computed navigation offsets for tuplestore trim.
+-- Lookback: how far back from currentpos (PREV, LAST, compound PREV_LAST/NEXT_LAST).
+-- Lookahead: how far forward from match_start (FIRST, compound PREV_FIRST/NEXT_FIRST).
-- ============================================================
-- Prepare statement for host variable offset test below
@@ -2547,3 +2618,166 @@ EXPLAIN (COSTS OFF) EXECUTE rpr_nav_offset_prep(2);
RESET plan_cache_mode;
DEALLOCATE rpr_nav_offset_prep;
+-- FIRST(v): retain all (references match_start row)
+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)
+);
+
+-- 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)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS LAST(v, 1) > 0
+);
+
+-- LAST(v) without offset + PREV(v): no match_start dependency, offset 1
+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 LAST(v) > PREV(v)
+);
+
+-- Compound PREV(FIRST(val, 1), 2): lookback from match_start, firstOffset = 1-2 = -1
+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 PREV(FIRST(v, 1), 2) > 0
+);
+
+-- Compound NEXT(FIRST(val), 3): firstOffset = 0+3 = 3
+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 NEXT(FIRST(v), 3) > 0
+);
+
+-- Compound PREV(LAST(val), 2): lookback = 0+2 = 2
+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 PREV(LAST(v), 2) > 0
+);
+
+-- Compound NEXT(LAST(val, 1), 3): lookback = max(1-3, 0) = 0
+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 NEXT(LAST(v, 1), 3) > 0
+);
+
+-- Compound PREV(LAST(val, N), M): constant near-overflow (N+M just fits int64)
+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 PREV(LAST(v, 4611686018427387903), 4611686018427387903) IS NOT NULL
+);
+
+-- Compound PREV(LAST(val, N), M): constant overflow -> retain all
+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 PREV(LAST(v, 4611686018427387904), 4611686018427387904) IS NOT NULL
+);
+
+-- Compound NEXT(FIRST(val, N), M): constant lookahead overflow -> no trim impact
+-- N + M overflows int64, but target is forward from match_start so it never
+-- constrains trim. Lookahead remains at default (0).
+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 NEXT(FIRST(v, 4611686018427387904), 4611686018427387904) IS NOT NULL
+);
+
+-- Compound PREV(LAST(val, $1), $2): parameter lookback overflow -> retain all
+-- EXPLAIN shows "runtime" (plan-level); EXPLAIN ANALYZE shows "retain all"
+-- (executor-resolved).
+PREPARE test_overflow_lookback(int8, int8) AS
+SELECT count(*) OVER w
+FROM generate_series(1,10) s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS PREV(LAST(v, $1), $2) IS NOT NULL
+);
+SET plan_cache_mode = force_generic_plan;
+EXPLAIN (COSTS OFF) EXECUTE test_overflow_lookback(4611686018427387904, 4611686018427387904);
+EXPLAIN (COSTS OFF, ANALYZE, TIMING OFF, SUMMARY OFF)
+ EXECUTE test_overflow_lookback(4611686018427387904, 4611686018427387904);
+RESET plan_cache_mode;
+DEALLOCATE test_overflow_lookback;
+
+-- Compound NEXT(FIRST(val, $1), $2): parameter lookahead overflow -> no trim impact
+PREPARE test_overflow_lookahead(int8, int8) AS
+SELECT count(*) OVER w
+FROM generate_series(1,10) s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS NEXT(FIRST(v, $1), $2) IS NOT NULL
+);
+SET plan_cache_mode = force_generic_plan;
+EXPLAIN (COSTS OFF, ANALYZE, TIMING OFF, SUMMARY OFF)
+ EXECUTE test_overflow_lookahead(4611686018427387904, 4611686018427387904);
+RESET plan_cache_mode;
+DEALLOCATE test_overflow_lookahead;
+
+-- PREV(v) + PREV(v, $1): NEEDS_EVAL path must account for implicit lookback=1
+-- Previously, eval_nav_max_offset_walker skipped PREV(v) when offset_arg was
+-- NULL, causing maxOffset=0 when $1=0, which would trim the row needed by
+-- PREV(v). Verify this executes without "cannot fetch row before mark" error.
+PREPARE test_prev_implicit_offset(int8) AS
+SELECT count(*) OVER w
+FROM generate_series(1,10) s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS PREV(v) IS NOT NULL AND PREV(v, $1) IS NOT NULL
+);
+EXECUTE test_prev_implicit_offset(0);
+DEALLOCATE test_prev_implicit_offset;
+
+-- Runtime error: negative offset at execution time
+PREPARE test_runtime_neg_offset(int8) AS
+SELECT count(*) OVER w
+FROM generate_series(1,10) s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS PREV(v, $1) IS NOT NULL
+);
+EXECUTE test_runtime_neg_offset(-1);
+DEALLOCATE test_runtime_neg_offset;
+
+-- Runtime error: null offset at execution time
+PREPARE test_runtime_null_offset(int8) AS
+SELECT count(*) OVER w
+FROM generate_series(1,10) s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS PREV(v, $1) IS NOT NULL
+);
+EXECUTE test_runtime_null_offset(NULL);
+DEALLOCATE test_runtime_null_offset;
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 16de1421302..a3b1a855cdc 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -747,6 +747,8 @@ ErrorData
ErrorSaveContext
EstimateDSMForeignScan_function
EstimationInfo
+EvalNavFirstContext
+EvalNavMaxContext
EventTriggerCacheEntry
EventTriggerCacheItem
EventTriggerCacheStateType
@@ -1801,6 +1803,8 @@ NamedLWLockTrancheRequest
NamedTuplestoreScan
NamedTuplestoreScanState
NamespaceInfo
+NavCheckResult
+NavOffsetContext
NestLoop
NestLoopParam
NestLoopState
@@ -2479,6 +2483,7 @@ QueuePosition
QuitSignalReason
RPRNavExpr
RPRNavKind
+RPRNavOffsetKind
RBTNode
RBTOrderControl
RBTree
--
2.50.1 (Apple Git-155)
From d5c5eb7b1795eecdae73dbb23df7c12bbddc3a36 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Tue, 7 Apr 2026 13:31:44 +0900
Subject: [PATCH] Guard against int64 overflow in RPR bounded frame end
computation
---
src/backend/executor/execRPR.c | 16 ++++++++++++----
1 file changed, 12 insertions(+), 4 deletions(-)
diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 60f0d8b2fa1..ee4c67c9597 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -22,6 +22,7 @@
*/
#include "postgres.h"
+#include "common/int.h"
#include "executor/execRPR.h"
#include "executor/executor.h"
#include "miscadmin.h"
@@ -1046,10 +1047,11 @@
*
* When the frame is bounded (e.g., ROWS BETWEEN CURRENT ROW AND 5
* FOLLOWING), ExecRPRProcessRow receives hasLimitedFrame=true and
- * frameOffset indicating the upper bound. After the advance phase,
+ * frameOffset indicating the upper bound. Before the match phase,
* any context whose match has exceeded the frame boundary
- * (currentPos - matchStartRow >= frameOffset + 1) is finalized early.
- * This prevents matches from extending beyond the window frame.
+ * (currentPos >= matchStartRow + frameOffset + 1) is finalized early
+ * by forcing a mismatch. This prevents matches from extending beyond
+ * the window frame. The sum is clamped to PG_INT64_MAX on overflow.
*
* Note that bounded frames also disable context absorption at the
* planner level (see VIII-3(b)), since the frame boundary breaks the
@@ -3154,7 +3156,12 @@ ExecRPRProcessRow(WindowAggState *winstate, int64 currentPos,
/* Check frame boundary - finalize if exceeded */
if (hasLimitedFrame)
{
- int64 ctxFrameEnd = ctx->matchStartRow + frameOffset + 1;
+ int64 ctxFrameEnd;
+
+ /* Clamp to INT64_MAX on overflow */
+ if (pg_add_s64_overflow(ctx->matchStartRow, frameOffset + 1,
+ &ctxFrameEnd))
+ ctxFrameEnd = PG_INT64_MAX;
if (currentPos >= ctxFrameEnd)
{
@@ -3204,6 +3211,7 @@ ExecRPRProcessRow(WindowAggState *winstate, int64 currentPos,
* context here must be within its frame boundary.
*/
Assert(!hasLimitedFrame ||
+ ctx->matchStartRow > PG_INT64_MAX - frameOffset - 1 ||
currentPos < ctx->matchStartRow + frameOffset + 1);
nfa_advance(winstate, ctx, currentPos);
--
2.50.1 (Apple Git-155)
From c3dc38316266bddc6d0d17aff47235bcfc79b303 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Tue, 7 Apr 2026 11:15:12 +0900
Subject: [PATCH] Fix RPR error message style: hint format, terminology,
capitalization
Remove colon in errhint "Use: ROWS instead" -> "Use ROWS instead."
and add missing trailing period. Shorten "row pattern definition
variable name" to "DEFINE variable" for consistency with other
error messages. Capitalize navigation function names in stub
error messages (prev -> PREV, etc.) to match SQL standard keyword
style used elsewhere in the parser.
---
src/backend/parser/parse_rpr.c | 6 +++---
src/backend/utils/adt/windowfuncs.c | 16 ++++++++--------
src/test/regress/expected/rpr_base.out | 12 ++++++------
3 files changed, 17 insertions(+), 17 deletions(-)
diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index 05070cb04bb..8fbe12e1518 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -78,7 +78,7 @@ transformRPR(ParseState *pstate, WindowClause *wc, WindowDef *windef,
ereport(ERROR,
(errcode(ERRCODE_WINDOWING_ERROR),
errmsg("FRAME option GROUPS is not permitted when row pattern recognition is used"),
- errhint("Use: ROWS instead"),
+ errhint("Use ROWS instead."),
parser_errposition(pstate,
windef->frameLocation >= 0 ?
windef->frameLocation : windef->location)));
@@ -86,7 +86,7 @@ transformRPR(ParseState *pstate, WindowClause *wc, WindowDef *windef,
ereport(ERROR,
(errcode(ERRCODE_WINDOWING_ERROR),
errmsg("FRAME option RANGE is not permitted when row pattern recognition is used"),
- errhint("Use: ROWS instead"),
+ errhint("Use ROWS instead."),
parser_errposition(pstate,
windef->frameLocation >= 0 ?
windef->frameLocation : windef->location)));
@@ -329,7 +329,7 @@ transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
if (!strcmp(n, name))
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
- errmsg("row pattern definition variable name \"%s\" appears more than once in DEFINE clause",
+ errmsg("DEFINE variable \"%s\" appears more than once",
name),
parser_errposition(pstate, exprLocation((Node *) r))));
}
diff --git a/src/backend/utils/adt/windowfuncs.c b/src/backend/utils/adt/windowfuncs.c
index 420a4962395..fb966cae43c 100644
--- a/src/backend/utils/adt/windowfuncs.c
+++ b/src/backend/utils/adt/windowfuncs.c
@@ -740,7 +740,7 @@ window_prev(PG_FUNCTION_ARGS)
{
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("prev() can only be used in a DEFINE clause")));
+ errmsg("PREV() can only be used in a DEFINE clause")));
PG_RETURN_NULL(); /* not reached */
}
@@ -754,7 +754,7 @@ window_next(PG_FUNCTION_ARGS)
{
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("next() can only be used in a DEFINE clause")));
+ errmsg("NEXT() can only be used in a DEFINE clause")));
PG_RETURN_NULL(); /* not reached */
}
@@ -768,7 +768,7 @@ window_prev_offset(PG_FUNCTION_ARGS)
{
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("prev() can only be used in a DEFINE clause")));
+ errmsg("PREV() can only be used in a DEFINE clause")));
PG_RETURN_NULL(); /* not reached */
}
@@ -782,7 +782,7 @@ window_next_offset(PG_FUNCTION_ARGS)
{
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("next() can only be used in a DEFINE clause")));
+ errmsg("NEXT() can only be used in a DEFINE clause")));
PG_RETURN_NULL(); /* not reached */
}
@@ -796,7 +796,7 @@ window_first(PG_FUNCTION_ARGS)
{
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("first() can only be used in a DEFINE clause")));
+ errmsg("FIRST() can only be used in a DEFINE clause")));
PG_RETURN_NULL(); /* not reached */
}
@@ -810,7 +810,7 @@ window_last(PG_FUNCTION_ARGS)
{
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("last() can only be used in a DEFINE clause")));
+ errmsg("LAST() can only be used in a DEFINE clause")));
PG_RETURN_NULL(); /* not reached */
}
@@ -824,7 +824,7 @@ window_first_offset(PG_FUNCTION_ARGS)
{
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("first() can only be used in a DEFINE clause")));
+ errmsg("FIRST() can only be used in a DEFINE clause")));
PG_RETURN_NULL(); /* not reached */
}
@@ -838,6 +838,6 @@ window_last_offset(PG_FUNCTION_ARGS)
{
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("last() can only be used in a DEFINE clause")));
+ errmsg("LAST() can only be used in a DEFINE clause")));
PG_RETURN_NULL(); /* not reached */
}
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index 0845316965e..912bd7b7c77 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -232,7 +232,7 @@ WINDOW w AS (
PATTERN (A+)
DEFINE A AS id > 0, A AS id < 10
);
-ERROR: row pattern definition variable name "a" appears more than once in DEFINE clause
+ERROR: DEFINE variable "a" appears more than once
LINE 7: DEFINE A AS id > 0, A AS id < 10
^
-- Expected: ERROR: row pattern definition variable name "a" appears more than once in DEFINE clause
@@ -469,7 +469,7 @@ WINDOW w AS (
ERROR: FRAME option RANGE is not permitted when row pattern recognition is used
LINE 5: RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWIN...
^
-HINT: Use: ROWS instead
+HINT: Use ROWS instead.
-- Expected: ERROR: FRAME option RANGE is not permitted when row pattern recognition is used
-- GROUPS frame not starting at CURRENT ROW
SELECT COUNT(*) OVER w
@@ -483,7 +483,7 @@ WINDOW w AS (
ERROR: FRAME option GROUPS is not permitted when row pattern recognition is used
LINE 5: GROUPS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWI...
^
-HINT: Use: ROWS instead
+HINT: Use ROWS instead.
-- Expected: ERROR: FRAME option GROUPS is not permitted when row pattern recognition is used
-- Starting with N PRECEDING
SELECT COUNT(*) OVER w
@@ -640,7 +640,7 @@ ORDER BY id;
ERROR: FRAME option RANGE is not permitted when row pattern recognition is used
LINE 5: RANGE BETWEEN CURRENT ROW AND 10 FOLLOWING
^
-HINT: Use: ROWS instead
+HINT: Use ROWS instead.
-- Expected: ERROR: FRAME option RANGE is not permitted when row pattern recognition is used
-- GROUPS frame with RPR (not permitted)
SELECT id, val, COUNT(*) OVER w as cnt
@@ -656,7 +656,7 @@ ORDER BY id;
ERROR: FRAME option GROUPS is not permitted when row pattern recognition is used
LINE 5: GROUPS BETWEEN CURRENT ROW AND 1 FOLLOWING
^
-HINT: Use: ROWS instead
+HINT: Use ROWS instead.
-- Expected: ERROR: FRAME option GROUPS is not permitted when row pattern recognition is used
DROP TABLE rpr_frame;
-- ============================================================
@@ -705,7 +705,7 @@ ORDER BY id;
ERROR: FRAME option RANGE is not permitted when row pattern recognition is used
LINE 6: RANGE BETWEEN CURRENT ROW AND 10 FOLLOWING
^
-HINT: Use: ROWS instead
+HINT: Use ROWS instead.
-- Expected: ERROR: FRAME option RANGE is not permitted when row pattern recognition is used
DROP TABLE rpr_partition;
-- ============================================================
--
2.50.1 (Apple Git-155)
From c48c70f494fb656f45e2923bc4260ad8c82f5787 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Tue, 7 Apr 2026 10:09:41 +0900
Subject: [PATCH] Fix comment typos, grammar, and inaccuracies in RPR code
---
src/backend/executor/execRPR.c | 7 +++----
src/backend/executor/nodeWindowAgg.c | 20 ++++++++++----------
src/backend/optimizer/plan/createplan.c | 3 ++-
3 files changed, 15 insertions(+), 15 deletions(-)
diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index ee4c67c9597..dede2dfab0d 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -416,7 +416,6 @@
* the normal loop-back (which cycle detection will eventually kill) and
* a fast-forward exit clone that bypasses the loop entirely.
* (See IX-4(c) for detailed runtime behavior.)
- * - Empty match is impossible since body is not nullable
*
* IV-5. Absorbability Analysis (RPR_ELEM_ABSORBABLE)
*
@@ -645,8 +644,8 @@
* When processing a context whose matchStartRow differs from the shared
* value, nfa_reevaluate_dependent_vars() temporarily sets nav_match_start
* to that context's matchStartRow and re-evaluates only the dependent
- * variables. No restore is needed because contexts are ordered by
- * matchStartRow (ascending), so no later context shares the head's value.
+ * variables. The original nav_match_start and currentpos are saved and
+ * restored after re-evaluation.
*
* VI-5. Tuplestore Mark and Trim (nodeWindowAgg.c)
*
@@ -2715,7 +2714,7 @@ nfa_advance_var(WindowAggState *winstate, RPRNFAContext *ctx,
bool reluctant = RPRElemIsReluctant(elem);
/*
- * Clone state for the second-priority path. For greedy, clone is the
+ * Clone state for the first-priority path. For greedy, clone is the
* loop state; for reluctant, clone is the exit state.
*/
if (reluctant)
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index cdbe356abd7..849ebf8abb0 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -3705,8 +3705,8 @@ ignorenulls_getfuncarginframe(WindowObject winobj, int argno,
{
/*
* Early check if row could be out of reduced frame. When RPR is
- * enabled, EXCUDE clause cannot be specified and the frame is always
- * contiguous. So we can do the check followings safely. Note,
+ * enabled, EXCLUDE clause cannot be specified and the frame is always
+ * contiguous. So we can safely perform the following checks. Note,
* however, it is possible that a row is out of reduced frame if
* there's a NULL in the middle. So we need to check it in the
* following do loop.
@@ -4168,7 +4168,7 @@ eval_nav_first_offset(WindowAggState *winstate, List *defineClause)
/*
* rpr_is_defined
- * return true if Row pattern recognition is defined.
+ * Return true if row pattern recognition is defined.
*/
static bool
rpr_is_defined(WindowAggState *winstate)
@@ -4182,14 +4182,14 @@ rpr_is_defined(WindowAggState *winstate)
* Determine whether a row is in the current row's reduced window frame
* according to row pattern matching
*
- * The row must has been already determined that it is in a full window frame
- * and fetched it into slot.
+ * The row must have already been determined to be in a full window frame
+ * and fetched into the slot.
*
* Returns:
* = 0, RPR is not defined.
* >0, if the row is the first in the reduced frame. Return the number of rows
* in the reduced frame.
- * -1, if the row is unmatched row
+ * -1, if the row is an unmatched row
* -2, if the row is in the reduced frame but needed to be skipped because of
* AFTER MATCH SKIP PAST LAST ROW
* -----------------
@@ -4204,8 +4204,8 @@ row_is_in_reduced_frame(WindowObject winobj, int64 pos)
if (!rpr_is_defined(winstate))
{
/*
- * RPR is not defined. Assume that we are always in the the reduced
- * window frame.
+ * RPR is not defined. Assume that we are always in the reduced window
+ * frame.
*/
rtn = 0;
return rtn;
@@ -4938,8 +4938,8 @@ WinGetFuncArgInFrame(WindowObject winobj, int argno,
* isout: output argument, set to indicate whether target row position
* is out of frame (can pass NULL if caller doesn't care about this)
*
- * Returns 0 if we successfully got the slot. false if out of frame.
- * (also isout is set)
+ * Returns 0 if we successfully got the slot, or nonzero if out of frame.
+ * (isout is also set in the latter case.)
*/
static int
WinGetSlotInFrame(WindowObject winobj, TupleTableSlot *slot,
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index 02d511269ab..50668f3b7ab 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -2475,7 +2475,8 @@ typedef struct NavOffsetContext
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 (>= 0), or -1 if none */
+ int64 firstOffset; /* min FIRST offset (may be negative for
+ * PREV_FIRST) */
bool hasFirst; /* any FIRST node found */
bool firstNeedsEval; /* non-constant FIRST offset found */
} NavOffsetContext;
--
2.50.1 (Apple Git-155)
From d3590794004c6200dee5178795a8d983f02d4375 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Tue, 7 Apr 2026 10:50:36 +0900
Subject: [PATCH] Fix RPR documentation: synopsis, grammar, and terminology
Remove erroneous comma in PATTERN synopsis. Fix typos in
advanced.sgml (">=;" stray semicolon, "with the a row",
"For example following"). Correct PREV/NEXT description
from "within the window frame" to "within the partition"
and add missing "DEFINE clause only" note. Capitalize
"Row Pattern Recognition" consistently across SGML files.
Fix numerous missing articles and grammar errors in
select.sgml: "after a match found" -> "after a match is
found", "do not necessarily" -> "does not necessarily",
add missing "the" before clause references.
---
doc/src/sgml/advanced.sgml | 14 +++++++-------
doc/src/sgml/func/func-window.sgml | 14 ++++++++------
doc/src/sgml/ref/select.sgml | 28 ++++++++++++++--------------
3 files changed, 29 insertions(+), 27 deletions(-)
diff --git a/doc/src/sgml/advanced.sgml b/doc/src/sgml/advanced.sgml
index 0caf9fdaff6..11c2416df51 100644
--- a/doc/src/sgml/advanced.sgml
+++ b/doc/src/sgml/advanced.sgml
@@ -553,8 +553,8 @@ WHERE pos < 3;
</para>
<para>
- Row pattern common syntax can be used to perform row pattern recognition
- in a query. The row pattern common syntax includes two sub
+ Row Pattern Common Syntax can be used to perform Row Pattern Recognition
+ in a query. The Row Pattern Common Syntax includes two sub
clauses: <literal>DEFINE</literal>
and <literal>PATTERN</literal>. <literal>DEFINE</literal> defines
row pattern variables along with an expression. The expression must be a
@@ -584,12 +584,12 @@ DEFINE
Once <literal>DEFINE</literal> exists, <literal>PATTERN</literal> can be
used. <literal>PATTERN</literal> defines a sequence of rows that satisfies
conditions defined in the <literal>DEFINE</literal> clause. For example
- following <literal>PATTERN</literal> defines a sequence of rows starting
- with the a row satisfying "LOWPRICE", then one or more rows satisfying
+ the following <literal>PATTERN</literal> defines a sequence of rows starting
+ with a row satisfying "LOWPRICE", then one or more rows satisfying
"UP" and finally one or more rows satisfying "DOWN". Pattern variables can
be followed by quantifiers: "+" means one or more matches, "*" means zero
or more matches, "?" means zero or one match, "{n}" (n > 0) means exactly
- n matches, "{n,}" (n >=; 0) means at least n matches, "{,m}" (m > 0) means
+ n matches, "{n,}" (n >= 0) means at least n matches, "{,m}" (m > 0) means
at most m matches, and "{n,m}" (0 <= n <= m, 0 < m) means between n and m
matches. Patterns can be grouped using parentheses and combined using
alternation (the vertical bar "|" for OR). For example, "(UP DOWN)+"
@@ -642,7 +642,7 @@ FROM stock
</para>
<para>
- Row pattern recognition internally uses a nondeterministic finite
+ Row Pattern Recognition internally uses a nondeterministic finite
automaton (NFA) to match patterns. For patterns with unbounded
quantifiers (e.g., <literal>A+</literal> or <literal>(A B)+</literal>),
the NFA may need to track many active matching contexts simultaneously,
@@ -676,7 +676,7 @@ FROM stock
</para>
<para>
- When examining query plans for row pattern recognition with
+ When examining query plans for Row Pattern Recognition with
<command>EXPLAIN</command>, the pattern output may include special
markers that indicate optimization opportunities. A double quote
<literal>"</literal> marks where pattern absorption can occur,
diff --git a/doc/src/sgml/func/func-window.sgml b/doc/src/sgml/func/func-window.sgml
index ab80690f7be..d109a2d22bc 100644
--- a/doc/src/sgml/func/func-window.sgml
+++ b/doc/src/sgml/func/func-window.sgml
@@ -279,9 +279,9 @@
</para>
<para>
- Row pattern recognition navigation functions are listed in
+ Row Pattern Recognition navigation functions are listed in
<xref linkend="functions-rpr-navigation-table"/>. These functions
- can be used to describe DEFINE clause of Row pattern recognition.
+ can be used to describe the DEFINE clause of Row Pattern Recognition.
</para>
<table id="functions-rpr-navigation-table">
@@ -309,12 +309,13 @@
</para>
<para>
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.
+ rows before the current row within the partition;
+ returns NULL if the target row is outside the partition.
<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.
+ Can only be used in a <literal>DEFINE</literal> clause.
</para></entry>
</row>
@@ -328,12 +329,13 @@
</para>
<para>
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.
+ rows after the current row within the partition;
+ returns NULL if the target row is outside the partition.
<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.
+ Can only be used in a <literal>DEFINE</literal> clause.
</para></entry>
</row>
diff --git a/doc/src/sgml/ref/select.sgml b/doc/src/sgml/ref/select.sgml
index 5e4ba9d3cc6..5272d6c0bfa 100644
--- a/doc/src/sgml/ref/select.sgml
+++ b/doc/src/sgml/ref/select.sgml
@@ -1133,34 +1133,34 @@ EXCLUDE NO OTHERS
<para>
The
optional <replaceable class="parameter">row_pattern_common_syntax</replaceable>
- defines the <firstterm>row pattern recognition condition</firstterm> for
+ defines the <firstterm>Row Pattern Recognition condition</firstterm> for
this
window. <replaceable class="parameter">row_pattern_common_syntax</replaceable>
- includes following subclauses.
+ includes the following subclauses.
<synopsis>
[ { AFTER MATCH SKIP PAST LAST ROW | AFTER MATCH SKIP TO NEXT ROW } ]
[ INITIAL | SEEK ]
-PATTERN ( <replaceable class="parameter">pattern_variable_name</replaceable> [ <replaceable>quantifier</replaceable> ] [, ...] )
+PATTERN ( <replaceable class="parameter">pattern_variable_name</replaceable> [ <replaceable>quantifier</replaceable> ] [ ... ] )
DEFINE <replaceable class="parameter">definition_variable_name</replaceable> AS <replaceable class="parameter">expression</replaceable> [, ...]
</synopsis>
<literal>AFTER MATCH SKIP PAST LAST ROW</literal> or <literal>AFTER MATCH
- SKIP TO NEXT ROW</literal> controls how to proceed to next row position
- after a match found. With <literal>AFTER MATCH SKIP PAST LAST
- ROW</literal> (the default) next row position is next to the last row of
- previous match. On the other hand, with <literal>AFTER MATCH SKIP TO NEXT
- ROW</literal> next row position is next to the first row of previous
- match. <literal>INITIAL</literal> or <literal>SEEK</literal> defines how a
- successful pattern matching starts from which row in a
- frame. If <literal>INITIAL</literal> is specified, the match must start
+ SKIP TO NEXT ROW</literal> controls how to proceed to the next row position
+ after a match is found. With <literal>AFTER MATCH SKIP PAST LAST
+ ROW</literal> (the default) the next row position is next to the last row of
+ the previous match. On the other hand, with <literal>AFTER MATCH SKIP TO NEXT
+ ROW</literal> the next row position is next to the first row of the previous
+ match. <literal>INITIAL</literal> or <literal>SEEK</literal> specifies from
+ which row in the frame pattern matching begins.
+ If <literal>INITIAL</literal> is specified, the match must start
from the first row in the frame. If <literal>SEEK</literal> is specified,
- the set of matching rows do not necessarily start from the first row. The
+ the set of matching rows does not necessarily start from the first row. The
default is <literal>INITIAL</literal>. Currently
only <literal>INITIAL</literal> is supported. <literal>DEFINE</literal>
defines definition variables along with a boolean
expression. <literal>PATTERN</literal> defines a sequence of rows that
satisfies certain conditions using variables defined
- in <literal>DEFINE</literal> clause (an empty <literal>PATTERN()</literal>
+ in the <literal>DEFINE</literal> clause (an empty <literal>PATTERN()</literal>
is not supported). Each pattern variable can be followed by a quantifier
to specify how many times it should match:
<literal>*</literal> (zero or more),
@@ -1198,7 +1198,7 @@ DEFINE <replaceable class="parameter">definition_variable_name</replaceable> AS
<para>
Note that the maximum number of unique pattern variables
- used in <literal>PATTERN</literal> clause is 251.
+ used in the <literal>PATTERN</literal> clause is 251.
If this limit is exceeded, an error will be raised.
Additionally, the maximum nesting depth of pattern groups
(parentheses) is 253 levels.
--
2.50.1 (Apple Git-155)
From 6a716cd3dc1f808438dba393603ec955cae63e65 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Tue, 7 Apr 2026 13:59:29 +0900
Subject: [PATCH] Fix nav_slot pass-by-ref dangling pointer in RPR navigation
When a DEFINE expression contains multiple navigation calls targeting
different positions (e.g., PREV(x,1) > PREV(x,2)), the second call
re-fetches nav_slot, freeing the previous tuple via pfree. Any
pass-by-ref datum extracted from the first navigation becomes a
dangling pointer. Fix by copying pass-by-ref results into per-tuple
memory in the RESTORE step.
---
src/backend/executor/execExpr.c | 5 ++
src/backend/executor/execExprInterp.c | 20 +++++++
src/include/executor/execExpr.h | 2 +
src/test/regress/expected/rpr.out | 80 +++++++++++++++++++++++++++
src/test/regress/sql/rpr.sql | 34 ++++++++++++
5 files changed, 141 insertions(+)
diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c
index 6349a564a98..8d8da67e79f 100644
--- a/src/backend/executor/execExpr.c
+++ b/src/backend/executor/execExpr.c
@@ -1304,7 +1304,12 @@ ExecInitExprRec(Expr *node, ExprState *state,
/* Emit RESTORE opcode: restore original slot */
scratch.opcode = EEOP_RPR_NAV_RESTORE;
+ scratch.resvalue = resv;
+ scratch.resnull = resnull;
scratch.d.rpr_nav.winstate = winstate;
+ get_typlenbyval(nav->resulttype,
+ &scratch.d.rpr_nav.resulttyplen,
+ &scratch.d.rpr_nav.resulttypbyval);
ExprEvalPushStep(state, &scratch);
break;
}
diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c
index 2ec579732cc..e2d41c3098f 100644
--- a/src/backend/executor/execExprInterp.c
+++ b/src/backend/executor/execExprInterp.c
@@ -6156,6 +6156,13 @@ ExecEvalRPRNavSet(ExprState *state, ExprEvalStep *op, ExprContext *econtext)
* 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
+ * nav_slot's tuple memory. If a subsequent navigation in the same
+ * expression re-fetches nav_slot for a different position, the old
+ * tuple is freed, leaving a dangling pointer. We prevent this by
+ * copying pass-by-ref results into per-tuple memory, which survives
+ * until the next ResetExprContext.
*/
void
ExecEvalRPRNavRestore(ExprState *state, ExprEvalStep *op,
@@ -6164,4 +6171,17 @@ ExecEvalRPRNavRestore(ExprState *state, ExprEvalStep *op,
WindowAggState *winstate = op->d.rpr_nav.winstate;
econtext->ecxt_outertuple = winstate->nav_saved_outertuple;
+
+ /* Stabilize pass-by-ref result against nav_slot re-fetch */
+ if (!op->d.rpr_nav.resulttypbyval &&
+ !*op->resnull)
+ {
+ MemoryContext oldContext;
+
+ oldContext = MemoryContextSwitchTo(econtext->ecxt_per_tuple_memory);
+ *op->resvalue = datumCopy(*op->resvalue,
+ false,
+ op->d.rpr_nav.resulttyplen);
+ MemoryContextSwitchTo(oldContext);
+ }
}
diff --git a/src/include/executor/execExpr.h b/src/include/executor/execExpr.h
index 834800a4062..e6b2ab30406 100644
--- a/src/include/executor/execExpr.h
+++ b/src/include/executor/execExpr.h
@@ -703,6 +703,8 @@ typedef struct ExprEvalStep
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? */
} rpr_nav;
/* for EEOP_AGG_*DESERIALIZE */
diff --git a/src/test/regress/expected/rpr.out b/src/test/regress/expected/rpr.out
index 04ec25d4cf5..32aa8bc3722 100644
--- a/src/test/regress/expected/rpr.out
+++ b/src/test/regress/expected/rpr.out
@@ -1635,6 +1635,86 @@ WINDOW w AS (
company2 | 07-10-2023 | 1300 | | | 0
(20 rows)
+-- Pass-by-ref types: two PREV calls targeting different positions.
+-- Verifies that datumCopy in RESTORE prevents dangling pointers when
+-- nav_slot is re-fetched for the second navigation.
+-- tdate::text gives distinct text values per row (e.g. '07-01-2023').
+-- B matches when 1-back date text > 2-back date text (always true for
+-- ascending dates), so B+ extends the full partition after A.
+SELECT company, tdate, tdate::text AS tdate_text,
+ first_value(tdate::text) OVER w, last_value(tdate::text) 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 PREV(tdate::text, 1) > PREV(tdate::text, 2)
+);
+ company | tdate | tdate_text | first_value | last_value | count
+----------+------------+------------+-------------+------------+-------
+ company1 | 07-01-2023 | 07-01-2023 | | | 0
+ company1 | 07-02-2023 | 07-02-2023 | 07-02-2023 | 07-10-2023 | 9
+ company1 | 07-03-2023 | 07-03-2023 | | | 0
+ company1 | 07-04-2023 | 07-04-2023 | | | 0
+ company1 | 07-05-2023 | 07-05-2023 | | | 0
+ company1 | 07-06-2023 | 07-06-2023 | | | 0
+ company1 | 07-07-2023 | 07-07-2023 | | | 0
+ company1 | 07-08-2023 | 07-08-2023 | | | 0
+ company1 | 07-09-2023 | 07-09-2023 | | | 0
+ company1 | 07-10-2023 | 07-10-2023 | | | 0
+ company2 | 07-01-2023 | 07-01-2023 | | | 0
+ company2 | 07-02-2023 | 07-02-2023 | 07-02-2023 | 07-10-2023 | 9
+ company2 | 07-03-2023 | 07-03-2023 | | | 0
+ company2 | 07-04-2023 | 07-04-2023 | | | 0
+ company2 | 07-05-2023 | 07-05-2023 | | | 0
+ company2 | 07-06-2023 | 07-06-2023 | | | 0
+ company2 | 07-07-2023 | 07-07-2023 | | | 0
+ company2 | 07-08-2023 | 07-08-2023 | | | 0
+ company2 | 07-09-2023 | 07-09-2023 | | | 0
+ company2 | 07-10-2023 | 07-10-2023 | | | 0
+(20 rows)
+
+-- numeric: PREV(price::numeric, 1) > PREV(price::numeric, 2)
+-- B matches when price 1-back > price 2-back (ascending pair).
+SELECT company, tdate, price::numeric AS nprice,
+ first_value(price::numeric) OVER w, last_value(price::numeric) 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 PREV(price::numeric, 1) > PREV(price::numeric, 2)
+);
+ company | tdate | nprice | 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 | 150 | 90 | 2
+ 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 | 1500 | 60 | 2
+ 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)
+
--
-- FIRST/LAST navigation
--
diff --git a/src/test/regress/sql/rpr.sql b/src/test/regress/sql/rpr.sql
index a05b429ce74..724d460b2da 100644
--- a/src/test/regress/sql/rpr.sql
+++ b/src/test/regress/sql/rpr.sql
@@ -776,6 +776,40 @@ WINDOW w AS (
DEFINE A AS price > PREV(price, 1) AND price < NEXT(price, 1)
);
+-- Pass-by-ref types: two PREV calls targeting different positions.
+-- Verifies that datumCopy in RESTORE prevents dangling pointers when
+-- nav_slot is re-fetched for the second navigation.
+-- tdate::text gives distinct text values per row (e.g. '07-01-2023').
+-- B matches when 1-back date text > 2-back date text (always true for
+-- ascending dates), so B+ extends the full partition after A.
+SELECT company, tdate, tdate::text AS tdate_text,
+ first_value(tdate::text) OVER w, last_value(tdate::text) 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 PREV(tdate::text, 1) > PREV(tdate::text, 2)
+);
+
+-- numeric: PREV(price::numeric, 1) > PREV(price::numeric, 2)
+-- B matches when price 1-back > price 2-back (ascending pair).
+SELECT company, tdate, price::numeric AS nprice,
+ first_value(price::numeric) OVER w, last_value(price::numeric) 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 PREV(price::numeric, 1) > PREV(price::numeric, 2)
+);
+
--
-- FIRST/LAST navigation
--
--
2.50.1 (Apple Git-155)
From d09908f16025319da7771c40892a3a66df5fdb7e Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Tue, 7 Apr 2026 20:49:27 +0900
Subject: [PATCH] Add inline comments for complex RPR algorithms and design
notes
Document END chain traversal in nfa_match(), fast-forward paths
in nfa_advance_end(), absorption safety rules with navigation
lookup table, per-context evaluation strategy table, fixed-length
group unrolling rationale, and BEGIN/END pointer layout diagram.
---
src/backend/executor/execRPR.c | 97 ++++++++++++++++++++++++++------
src/backend/optimizer/plan/rpr.c | 41 ++++++++++++--
2 files changed, 118 insertions(+), 20 deletions(-)
diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index dede2dfab0d..037d3b2e232 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -440,7 +440,14 @@
* Case 2: GROUP+ with fixed-length children (min == max, recursively)
* e.g., (A B)+, (A B{2})+, ((A (B C){2}){2})+
* -> ABSORBABLE_BRANCH on all elements within the group,
- * ABSORBABLE | ABSORBABLE_BRANCH on END
+ * ABSORBABLE | ABSORBABLE_BRANCH on END
+ *
+ * Why this is safe: when every child has min == max, the group
+ * is semantically equivalent to unrolling its body into {1,1}
+ * elements. E.g., (A B{2})+ behaves like (A B B)+. Each
+ * iteration consumes a fixed number of rows, so an earlier
+ * context's count always dominates a later one's (monotonicity).
+ *
* Case 3: GROUP+ whose body starts with VAR+ (e.g., (A+ B)+)
* -> Recurses from BEGIN into the body, applying Case 1.
* ABSORBABLE | ABSORBABLE_BRANCH set on A.
@@ -647,6 +654,19 @@
* variables. The original nav_match_start and currentpos are saved and
* restored after re-evaluation.
*
+ * Summary of evaluation strategy by navigation content:
+ *
+ * Navigation content evaluation
+ * -------------------------------------------------------
+ * No navigation shared (once per row)
+ * PREV/NEXT only shared (once per row)
+ * LAST (no offset) shared (once per row)
+ * LAST (with offset) per-context
+ * FIRST (any) per-context
+ * Compound (inner FIRST) per-context
+ * Compound (inner LAST, no off.) shared (once per row)
+ * Compound (inner LAST, w/off.) per-context
+ *
* VI-5. Tuplestore Mark and Trim (nodeWindowAgg.c)
*
* Navigation functions require access to past rows via the tuplestore.
@@ -762,11 +782,26 @@
* (b) Unbounded frame (ROWS BETWEEN CURRENT ROW AND UNBOUNDED
* FOLLOWING). Limited frames apply differently to each context,
* breaking the monotonicity principle.
- * (c) No match_start-dependent navigation in DEFINE. FIRST,
- * LAST-with-offset, and compound navigation referencing match_start
- * (PREV_FIRST, NEXT_FIRST, PREV_LAST/NEXT_LAST with offset)
- * cause different contexts to evaluate to different values for the
- * same row, breaking monotonicity.
+ * (c) No match_start-dependent navigation in DEFINE.
+ *
+ * Mechanism: each context has a different matchStartRow, so FIRST
+ * resolves to a different row for each context at the same
+ * currentpos. An earlier context's DEFINE result no longer
+ * subsumes a later one's, making count-dominance comparison
+ * invalid. Rather than comparing matchStartRow at runtime
+ * (which would complicate the absorb path), any match_start
+ * dependency disables absorption entirely.
+ *
+ * Navigation content match_start dep. absorption
+ * ------------------------------------------------------------
+ * No navigation none safe
+ * PREV/NEXT only none safe
+ * LAST (no offset) none safe
+ * LAST (with offset) boundary check unsafe
+ * FIRST (any) direct unsafe
+ * Compound (inner FIRST) direct unsafe
+ * Compound (inner LAST, no off.) none safe
+ * Compound (inner LAST, w/off.) boundary chk unsafe
*
* Runtime conditions (evaluated per context pair):
*
@@ -2260,7 +2295,13 @@ nfa_absorb_contexts(WindowAggState *winstate)
* nfa_eval_var_match
*
* Evaluate if a VAR element matches the current row.
- * Undefined variables (varId >= defineVariableList length) default to TRUE.
+ *
+ * varMatched is a pre-evaluated boolean array indexed by varId, computed
+ * once per row by evaluating all DEFINE expressions. NULL means no DEFINE
+ * clauses exist (only possible during early development/testing).
+ *
+ * Per SQL:2016 R020, pattern variables not listed in DEFINE are implicitly
+ * TRUE -- they match every row. This is checked via varId >= list_length.
*/
static bool
nfa_eval_var_match(WindowAggState *winstate, RPRPatternElement *elem,
@@ -2337,9 +2378,20 @@ nfa_match(WindowAggState *winstate, RPRNFAContext *ctx, bool *varMatched)
/*
* For VAR at max count with END next, advance through END
- * chain to reach the absorption judgment point. Only
+ * chain to reach the absorption judgment point. Only
* deterministic exits (count >= max, max finite) are handled;
* unbounded VARs stay for advance phase.
+ *
+ * In nested patterns like ((A B){2}){3}, a VAR reaching its
+ * max triggers an exit cascade: inner END increments inner
+ * group count, which may itself reach max, requiring an exit
+ * to the next outer END. The loop below walks this chain.
+ *
+ * ABSORBABLE_BRANCH marks elements inside the absorbable
+ * region; ABSORBABLE marks the outermost judgment point
+ * where count-dominance is evaluated. We chain through
+ * BRANCH elements until reaching the ABSORBABLE point or
+ * an element that can still loop (count < max).
*/
if (RPRElemIsAbsorbableBranch(elem) &&
!RPRElemIsAbsorbable(elem) &&
@@ -2561,12 +2613,25 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
RPRPatternElement *jumpElem;
RPRNFAState *ffState = NULL;
- /* Snapshot state for ff path before modifying for loop-back */
+ /*
+ * Two paths are explored in parallel when the group body is
+ * nullable (RPR_ELEM_EMPTY_LOOP):
+ *
+ * 1. Primary path: loop back and attempt real matches in the
+ * next iteration (state, modified below).
+ *
+ * 2. Fast-forward path: skip directly to after the group,
+ * treating all remaining required iterations as empty
+ * matches (ffState, handled after the primary path).
+ *
+ * The snapshot must be taken BEFORE modifying state for the
+ * loop-back, since both paths diverge from the same point.
+ */
if (RPRElemCanEmptyLoop(elem))
ffState = nfa_state_create(winstate, state->elemIdx,
state->counts, state->isAbsorbable);
- /* Loop back for real matches (primary path) */
+ /* Primary path: loop back for real matches */
for (int d = depth + 1; d < pattern->maxDepth; d++)
state->counts[d] = 0;
state->elemIdx = elem->jump;
@@ -2575,12 +2640,12 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
currentPos);
/*
- * Fast-forward fallback for nullable bodies. E.g. (A?){2,3} when A
- * doesn't match: the loop-back produces empty iterations that cycle
- * detection would kill. Instead, exit directly treating all
- * remaining required iterations as empty. Route to elem->next (not
- * nfa_advance_end) to avoid creating competing greedy/reluctant loop
- * states.
+ * Fast-forward path for nullable bodies. E.g. (A?){2,3} when
+ * A doesn't match: the primary loop-back produces empty
+ * iterations that cycle detection would kill. Instead, exit
+ * directly with count satisfied. Route to elem->next (not
+ * nfa_advance_end) to avoid creating competing greedy/reluctant
+ * loop states.
*/
if (ffState != NULL)
{
diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index 767a214016c..754fcd53099 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -478,6 +478,19 @@ mergeConsecutiveAlts(List *children)
* mergeGroupPrefixSuffix
* Merge sequence prefix/suffix into GROUP with matching children.
*
+ * When a GROUP's children appear as a prefix before and/or suffix after
+ * the GROUP in a SEQ, absorb them by incrementing the GROUP's quantifier.
+ * This runs iteratively: A B A B (A B)+ A B -> (A B){5,}.
+ *
+ * Algorithm:
+ * For each GROUP encountered in the sequence:
+ * 1. PREFIX phase: compare the last N elements already in the result
+ * list against the GROUP's children. On match, remove them from
+ * result and increment the GROUP's min/max. Repeat until no match.
+ * 2. SUFFIX phase: compare the next N elements in the input against
+ * the GROUP's children. On match, skip them (via skipUntil) and
+ * increment min/max. Repeat until no match.
+ *
* Examples:
* A B (A B)+ -> (A B){2,}
* (A B)+ A B -> (A B){2,}
@@ -813,8 +826,16 @@ tryMultiplyQuantifiers(RPRPatternNode *pattern)
}
/*
- * Case 2/3: Safe when child is finite AND (outer is exact OR child is
- * {1,1})
+ * Case 2: Outer exact (min == max): (A{2,3}){4} -> A{8,12}.
+ * Safe because every iteration produces the same range.
+ *
+ * Case 3: Child {1,1}: (A){2,5} -> A{2,5}.
+ * Safe because the child contributes exactly one per
+ * iteration, so the outer range maps directly.
+ *
+ * Unsafe example: (A{2}){2,3} produces counts 4 or 6 only, not
+ * the full range 4..6, so we cannot flatten when child has a
+ * non-trivial range AND outer is also a range.
*/
if (child->max != RPR_QUANTITY_INF &&
(pattern->min == pattern->max ||
@@ -824,6 +845,7 @@ tryMultiplyQuantifiers(RPRPatternNode *pattern)
if (new_min_64 >= RPR_QUANTITY_INF)
return pattern;
+ /* Outer unbounded: result is unbounded regardless of child */
if (pattern->max == RPR_QUANTITY_INF)
new_max_64 = RPR_QUANTITY_INF;
else
@@ -1186,8 +1208,19 @@ fillRPRPatternVar(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth dept
* fillRPRPatternGroup
* Fill a GROUP pattern and its children.
*
- * Creates elements for group content at increased depth, plus an END marker
- * if the group has a non-trivial quantifier.
+ * Creates elements for group content at increased depth, plus BEGIN/END
+ * marker pair if the group has a non-trivial quantifier (not {1,1}).
+ *
+ * Element layout for (A B){2,3}:
+ *
+ * [BEGIN] [A] [B] [END] [next element...]
+ * | | ^
+ * | +-- jump --+ (loop back to first child)
+ * +---- jump -------------------+ (skip to after END)
+ *
+ * BEGIN.jump points past END (skip path when count >= max or min == 0).
+ * END.jump points to the first child (loop-back path).
+ * BEGIN.next and END.next are set later by finalizeRPRPattern().
*
* Returns true if this group is nullable. A group is nullable when its
* min is 0 (can be skipped entirely) or its body is nullable (every path
--
2.50.1 (Apple Git-155)
From 29e8aaa01bb321da9eb2228832cc259725ac7841 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Tue, 7 Apr 2026 20:52:13 +0900
Subject: [PATCH] Remove unused include and fix header ordering in RPR files
---
src/backend/executor/execExprInterp.c | 2 +-
src/backend/executor/nodeWindowAgg.c | 3 +--
src/backend/parser/parse_rpr.c | 3 +--
3 files changed, 3 insertions(+), 5 deletions(-)
diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c
index e2d41c3098f..58b6693ed75 100644
--- a/src/backend/executor/execExprInterp.c
+++ b/src/backend/executor/execExprInterp.c
@@ -56,8 +56,8 @@
*/
#include "postgres.h"
-#include "common/int.h"
#include "access/heaptoast.h"
+#include "common/int.h"
#include "access/tupconvert.h"
#include "catalog/pg_type.h"
#include "commands/sequence.h"
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 849ebf8abb0..02f17e5472c 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -34,10 +34,9 @@
#include "postgres.h"
#include "access/htup_details.h"
-#include "common/int.h"
#include "catalog/objectaccess.h"
#include "catalog/pg_aggregate.h"
-#include "catalog/pg_collation_d.h"
+#include "common/int.h"
#include "catalog/pg_proc.h"
#include "executor/executor.h"
#include "executor/execRPR.h"
diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index 8fbe12e1518..8864b20e6cf 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -30,9 +30,8 @@
#include "nodes/nodeFuncs.h"
#include "optimizer/optimizer.h"
#include "optimizer/rpr.h"
-#include "parser/parse_clause.h"
-#include "parser/parse_collate.h"
#include "parser/parse_coerce.h"
+#include "parser/parse_collate.h"
#include "parser/parse_expr.h"
#include "parser/parse_rpr.h"
#include "parser/parse_target.h"
--
2.50.1 (Apple Git-155)
Attachments:
[text/plain] nocfbot-0001-Remove-unused-regex-include.txt (760B, 3-nocfbot-0001-Remove-unused-regex-include.txt)
download | inline diff:
From f79d4358bc0033bc4fe8b4f8c9e32904d3df6a93 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Tue, 24 Mar 2026 19:04:19 +0900
Subject: [PATCH] Remove unused regex/regex.h include from nodeWindowAgg.c
---
src/backend/executor/nodeWindowAgg.c | 1 -
1 file changed, 1 deletion(-)
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 4f882b877b1..185d7a0d5ae 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -50,7 +50,6 @@
#include "optimizer/rpr.h"
#include "parser/parse_agg.h"
#include "parser/parse_coerce.h"
-#include "regex/regex.h"
#include "utils/acl.h"
#include "utils/builtins.h"
#include "utils/datum.h"
--
2.50.1 (Apple Git-155)
[text/plain] nocfbot-0002-CHECK_FOR_INTERRUPTS-nfa_add_state_unique.txt (826B, 4-nocfbot-0002-CHECK_FOR_INTERRUPTS-nfa_add_state_unique.txt)
download | inline diff:
From 31e07dcbd5391b7ff9ef8293fcb090cf8f845c71 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Wed, 25 Mar 2026 00:25:40 +0900
Subject: [PATCH] Add CHECK_FOR_INTERRUPTS() to nfa_add_state_unique() for
state explosion patterns
---
src/backend/executor/execRPR.c | 2 ++
1 file changed, 2 insertions(+)
diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index bab5257f68f..cf54e0c76c3 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -1763,6 +1763,8 @@ nfa_add_state_unique(WindowAggState *winstate, RPRNFAContext *ctx, RPRNFAState *
/* Check for duplicate and find tail */
for (s = ctx->states; s != NULL; s = s->next)
{
+ CHECK_FOR_INTERRUPTS();
+
if (nfa_states_equal(winstate, s, state))
{
/*
--
2.50.1 (Apple Git-155)
[text/plain] nocfbot-0003-CHECK_FOR_INTERRUPTS-nfa_try_absorb_context.txt (859B, 5-nocfbot-0003-CHECK_FOR_INTERRUPTS-nfa_try_absorb_context.txt)
download | inline diff:
From 0f15fdabc01fc1503f2a13253df65844ece4c86d Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Wed, 25 Mar 2026 11:03:39 +0900
Subject: [PATCH] Add CHECK_FOR_INTERRUPTS() to nfa_try_absorb_context() loop
---
src/backend/executor/execRPR.c | 2 ++
1 file changed, 2 insertions(+)
diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index cf54e0c76c3..58f9da0b814 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -2084,6 +2084,8 @@ nfa_try_absorb_context(WindowAggState *winstate, RPRNFAContext *ctx)
for (older = ctx->prev; older != NULL; older = older->prev)
{
+ CHECK_FOR_INTERRUPTS();
+
/*
* By invariant: ctx->prev chain is in creation order (oldest first),
* and each row creates at most one context. So all contexts in this
--
2.50.1 (Apple Git-155)
[text/plain] nocfbot-0004-Fix-defineClause-TargetEntry-copy.txt (1.4K, 6-nocfbot-0004-Fix-defineClause-TargetEntry-copy.txt)
download | inline diff:
From 6601dda3d297ee8928bbe1c035102d683c78251f Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Wed, 25 Mar 2026 00:20:05 +0900
Subject: [PATCH] Fix in-place modification of defineClause TargetEntry in
setrefs.c
---
src/backend/optimizer/plan/setrefs.c | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c
index 69cd1861e9b..813a326bd78 100644
--- a/src/backend/optimizer/plan/setrefs.c
+++ b/src/backend/optimizer/plan/setrefs.c
@@ -2633,7 +2633,7 @@ set_upper_references(PlannerInfo *root, Plan *plan, int rtoffset)
NUM_EXEC_QUAL(plan));
/*
- * Modifies an expression tree in each DEFINE clause so that all Var
+ * Replace an expression tree in each DEFINE clause so that all Var
* nodes's varno refers to OUTER_VAR.
*/
if (IsA(plan, WindowAgg))
@@ -2646,6 +2646,7 @@ set_upper_references(PlannerInfo *root, Plan *plan, int rtoffset)
{
TargetEntry *tle = (TargetEntry *) lfirst(l);
+ tle = flatCopyTargetEntry(tle);
tle->expr = (Expr *)
fix_upper_expr(root,
(Node *) tle->expr,
@@ -2654,6 +2655,7 @@ set_upper_references(PlannerInfo *root, Plan *plan, int rtoffset)
rtoffset,
NRM_EQUAL,
NUM_EXEC_QUAL(plan));
+ lfirst(l) = tle;
}
}
}
--
2.50.1 (Apple Git-155)
[text/plain] nocfbot-0005-Fix-mark-handling-last_value-RPR.txt (1.9K, 7-nocfbot-0005-Fix-mark-handling-last_value-RPR.txt)
download | inline diff:
From 31e7dacb8b0fa6ead63ff92c19aa5dfb0cde76a1 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Wed, 25 Mar 2026 00:37:50 +0900
Subject: [PATCH] Fix mark handling for last_value() under RPR
Enable mark advancement in window_last_value() for
better tuplestore memory usage in non-RPR cases, while
adding a guard in WinGetFuncArgInFrame to suppress it
for RPR SEEK_TAIL to prevent position invalidation
from reduced frame shifts.
---
src/backend/executor/nodeWindowAgg.c | 10 ++++++++++
src/backend/utils/adt/windowfuncs.c | 2 +-
2 files changed, 11 insertions(+), 1 deletion(-)
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 185d7a0d5ae..aed7cbef99a 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -4932,7 +4932,17 @@ WinGetSlotInFrame(WindowObject winobj, TupleTableSlot *slot,
if (isout)
*isout = false;
if (set_mark)
+ {
+ /*
+ * If RPR is enabled and seek type is WINDOW_SEEK_TAIL, we set the
+ * mark position unconditionally to frameheadpos. In this case the
+ * frame always starts at CURRENT_ROW and never goes back, thus
+ * setting the mark at the position is safe.
+ */
+ if (winstate->rpPattern != NULL && seektype == WINDOW_SEEK_TAIL)
+ mark_pos = winstate->frameheadpos;
WinSetMarkPosition(winobj, mark_pos);
+ }
return 0;
out_of_frame:
diff --git a/src/backend/utils/adt/windowfuncs.c b/src/backend/utils/adt/windowfuncs.c
index efb60c99052..74ef109f72e 100644
--- a/src/backend/utils/adt/windowfuncs.c
+++ b/src/backend/utils/adt/windowfuncs.c
@@ -682,7 +682,7 @@ window_last_value(PG_FUNCTION_ARGS)
WinCheckAndInitializeNullTreatment(winobj, true, fcinfo);
result = WinGetFuncArgInFrame(winobj, 0,
- 0, WINDOW_SEEK_TAIL, false,
+ 0, WINDOW_SEEK_TAIL, true,
&isnull, NULL);
if (isnull)
PG_RETURN_NULL();
--
2.50.1 (Apple Git-155)
[text/plain] nocfbot-0006-Fix-DEFINE-expression-handling-RPR-window-planning.txt (8.2K, 8-nocfbot-0006-Fix-DEFINE-expression-handling-RPR-window-planning.txt)
download | inline diff:
From 7dd896a8b279a1f02e8a5d9f241d9a59a32ee588 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Thu, 2 Apr 2026 11:55:02 +0900
Subject: [PATCH] Fix DEFINE expression handling in RPR window planning
transformDefineClause() added full DEFINE expressions including
RPRNavExpr (PREV/NEXT) nodes to the query targetlist. These
propagated to upper WindowAgg nodes that lack RPR navigation state,
causing a SIGSEGV when RPR and non-RPR windows coexist in the same
query.
Add only the Var nodes referenced by DEFINE expressions to the
targetlist, and protect those Vars from removal by
remove_unused_subquery_outputs() so they remain available in the
tuplestore slot for pattern matching evaluation.
Move the subquery wrapping tests from rpr.sql to rpr_integration.sql.
---
src/backend/optimizer/path/allpaths.c | 68 +++++++++++++++++
src/backend/parser/parse_rpr.c | 106 ++++++++++++++------------
2 files changed, 126 insertions(+), 48 deletions(-)
diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c
index f42a2bae14a..f0ef4e289a6 100644
--- a/src/backend/optimizer/path/allpaths.c
+++ b/src/backend/optimizer/path/allpaths.c
@@ -4750,6 +4750,74 @@ remove_unused_subquery_outputs(Query *subquery, RelOptInfo *rel,
if (contain_volatile_functions(texpr))
continue;
+ /*
+ * If any RPR window clause references this column in its DEFINE
+ * clause, don't remove it. The DEFINE expression needs these columns
+ * in the tuplestore slot for pattern matching evaluation, even if the
+ * outer query doesn't reference them.
+ */
+ if (IsA(texpr, Var))
+ {
+ Var *var = (Var *) texpr;
+ ListCell *wlc;
+ bool needed_by_define = false;
+
+ foreach(wlc, subquery->windowClause)
+ {
+ WindowClause *wc = lfirst_node(WindowClause, wlc);
+
+ if (wc->defineClause != NIL)
+ {
+ List *vars = pull_var_clause((Node *) wc->defineClause, 0);
+ ListCell *vlc;
+
+ foreach(vlc, vars)
+ {
+ Var *dvar = (Var *) lfirst(vlc);
+
+ if (dvar->varattno == var->varattno)
+ {
+ needed_by_define = true;
+ break;
+ }
+ }
+ list_free(vars);
+ if (needed_by_define)
+ break;
+ }
+ }
+ if (needed_by_define)
+ continue;
+ }
+
+ /*
+ * If it's a window function referencing a window clause with RPR (Row
+ * Pattern Recognition), don't remove it. Even when the window
+ * function result is unused by the outer query, the RPR pattern
+ * matching (frame reduction via DEFINE/PATTERN) must still execute.
+ * Replacing this with NULL would leave no active window functions for
+ * the WindowClause, causing the planner to omit the WindowAgg node
+ * entirely.
+ */
+ if (IsA(texpr, WindowFunc))
+ {
+ WindowFunc *wfunc = (WindowFunc *) texpr;
+ ListCell *wlc;
+
+ foreach(wlc, subquery->windowClause)
+ {
+ WindowClause *wc = lfirst_node(WindowClause, wlc);
+
+ if (wc->winref == wfunc->winref &&
+ wc->defineClause != NIL)
+ {
+ break;
+ }
+ }
+ if (wlc != NULL)
+ continue;
+ }
+
/*
* OK, we don't need it. Replace the expression with a NULL constant.
* Preserve the exposed type of the expression, in case something
diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index 55283ab4bbe..db1309ca311 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -28,6 +28,7 @@
#include "miscadmin.h"
#include "nodes/makefuncs.h"
#include "nodes/nodeFuncs.h"
+#include "optimizer/optimizer.h"
#include "optimizer/rpr.h"
#include "parser/parse_clause.h"
#include "parser/parse_collate.h"
@@ -310,9 +311,7 @@ transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
restargets = NIL;
foreach(lc, windef->rpCommonSyntax->rpDefs)
{
- TargetEntry *te,
- *teDefine;
- int defineExprLocation;
+ TargetEntry *teDefine;
restarget = (ResTarget *) lfirst(lc);
name = restarget->name;
@@ -335,57 +334,68 @@ transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
restargets = lappend(restargets, restarget);
/*
- * Transform the DEFINE expression (restarget->val) and add it to the
- * targetlist as a TargetEntry if not already present, so the planner
- * can propagate the referenced columns to the outer plan's
- * targetlist.
+ * Transform the DEFINE expression. We must NOT add the whole
+ * expression to the query targetlist, because it may contain
+ * RPRNavExpr nodes (PREV/NEXT) that can only be evaluated inside the
+ * owning WindowAgg.
*
- * Note: findTargetlistEntrySQL99 transforms and clobbers
- * restarget->val.
+ * Instead, we transform the expression directly and only ensure that
+ * the individual Var nodes it references are present in the
+ * targetlist, so the planner can propagate the referenced columns.
*/
+ {
+ Node *expr;
+ List *vars;
+ ListCell *lc2;
- /*
- * Save the original expression location before transformation.
- * findTargetlistEntrySQL99 may return an existing TargetEntry whose
- * location points to where it was originally created (e.g., ORDER
- * BY), not the DEFINE clause. We need to preserve the DEFINE location
- * for accurate error reporting.
- */
- defineExprLocation = exprLocation(restarget->val);
+ expr = transformExpr(pstate, restarget->val,
+ EXPR_KIND_RPR_DEFINE);
+
+ /*
+ * Pull out Var nodes from the transformed expression and ensure
+ * each one is present in the targetlist. This is needed so the
+ * planner propagates the referenced columns through the plan
+ * tree, making them available to the WindowAgg's DEFINE
+ * evaluation.
+ */
+ vars = pull_var_clause(expr, 0);
+ foreach(lc2, vars)
+ {
+ Var *var = (Var *) lfirst(lc2);
+ bool found = false;
+ ListCell *tl;
- te = findTargetlistEntrySQL99(pstate, restarget->val,
- targetlist, EXPR_KIND_RPR_DEFINE);
+ foreach(tl, *targetlist)
+ {
+ TargetEntry *tle = (TargetEntry *) lfirst(tl);
- /* -------------------
- * Copy the TargetEntry for defineClause and always set the pattern
- * variable name. We use copyObject so the original targetlist entry
- * is not modified.
- *
- * Note: We must always set resname to the pattern variable name.
- * findTargetlistEntrySQL99 creates new TEs with resname = NULL
- * (resjunk entries), but returns existing TEs unchanged when the
- * expression already exists in targetlist.
- *
- * Example: "SELECT id, flag, ... WINDOW w AS (... DEFINE T AS flag)"
- *
- * 1. SELECT list processing creates: TE{resname="flag", expr=flag}
- * 2. DEFINE T AS flag: findTargetlistEntrySQL99 finds existing TE
- * 3. te->resname is "flag" (from SELECT), not NULL
- * 4. Without unconditionally setting resname, teDefine->resname
- * would remain "flag" instead of pattern variable name "T"
- * 5. buildRPRPattern builds defineVariableList from resname, so
- * it would contain ["flag"] instead of ["T"]
- * 6. Pattern variable "T" not found -> Assert failure crash
- */
- teDefine = copyObject(te);
- teDefine->resname = pstrdup(name);
+ if (IsA(tle->expr, Var) &&
+ ((Var *) tle->expr)->varno == var->varno &&
+ ((Var *) tle->expr)->varattno == var->varattno)
+ {
+ found = true;
+ break;
+ }
+ }
+ if (!found)
+ {
+ TargetEntry *newtle;
- /*
- * Update the expression location to point to the DEFINE clause. This
- * ensures error messages reference the correct source location.
- */
- if (defineExprLocation >= 0 && IsA(teDefine->expr, Var))
- ((Var *) teDefine->expr)->location = defineExprLocation;
+ newtle = makeTargetEntry((Expr *) copyObject(var),
+ list_length(*targetlist) + 1,
+ NULL,
+ true);
+ *targetlist = lappend(*targetlist, newtle);
+ }
+ }
+ list_free(vars);
+
+ /* Build the defineClause entry directly from the transformed expr */
+ teDefine = makeTargetEntry((Expr *) expr,
+ list_length(defineClause) + 1,
+ pstrdup(name),
+ true);
+ }
/* build transformed DEFINE clause (list of TargetEntry) */
defineClause = lappend(defineClause, teDefine);
--
2.50.1 (Apple Git-155)
[text/plain] nocfbot-0007-Add-RPR-planner-integration-tests.txt (46.7K, 9-nocfbot-0007-Add-RPR-planner-integration-tests.txt)
download | inline diff:
From 4141670cfe47e9aa2ceac717dba0d09015fa89c1 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Thu, 2 Apr 2026 14:09:53 +0900
Subject: [PATCH] Add RPR planner integration tests
Add rpr_integration.sql covering planner optimization interactions
with RPR windows: frame optimization, run condition pushdown, window
deduplication, unused window removal, inverse transition, cost
estimation, subquery flattening, DEFINE expression propagation, and
LIMIT. Also covers integration scenarios with partitioned tables,
LATERAL, recursive CTEs, incremental sort, volatile functions, and
correlated subqueries.
---
src/test/regress/expected/rpr_integration.out | 885 ++++++++++++++++++
src/test/regress/parallel_schedule | 2 +-
src/test/regress/sql/rpr_integration.sql | 537 +++++++++++
3 files changed, 1423 insertions(+), 1 deletion(-)
create mode 100644 src/test/regress/expected/rpr_integration.out
create mode 100644 src/test/regress/sql/rpr_integration.sql
diff --git a/src/test/regress/expected/rpr_integration.out b/src/test/regress/expected/rpr_integration.out
new file mode 100644
index 00000000000..a21ac5a8588
--- /dev/null
+++ b/src/test/regress/expected/rpr_integration.out
@@ -0,0 +1,885 @@
+-- ============================================================
+-- RPR Integration Tests
+-- Planner optimization interaction tests for Row Pattern Recognition
+-- ============================================================
+--
+-- Verifies that each planner optimization correctly handles RPR windows.
+-- Even if individual optimizations are tested elsewhere, this file
+-- provides a single checkpoint for all planner/RPR interactions.
+--
+-- A. Planner Optimization Protection Tests
+-- A1. Frame optimization bypass
+-- A2. Run condition pushdown bypass
+-- A3. Window dedup prevention (planner)
+-- A4. Window dedup prevention (parser)
+-- A5. Unused window removal prevention
+-- A6. Inverse transition bypass
+-- A7. Cost estimation RPR awareness
+-- A8. Subquery flattening prevention
+-- A9. DEFINE expression non-propagation
+-- A10. RPR + LIMIT
+--
+-- B. Integration Scenario Tests
+-- B1. RPR + CTE
+-- B2. RPR + JOIN
+-- B3. RPR + Set operations
+-- B4. RPR + Prepared statements
+-- B5. RPR + Partitioned table
+-- B6. RPR + LATERAL
+-- B7. RPR + Recursive CTE
+-- B8. RPR + Incremental sort
+-- B9. RPR + Volatile function in DEFINE
+-- B10. RPR + Correlated subquery
+--
+CREATE TABLE rpr_integ (id INT, val INT);
+INSERT INTO rpr_integ VALUES
+ (1, 10), (2, 20), (3, 15), (4, 25), (5, 5),
+ (6, 30), (7, 35), (8, 20), (9, 40), (10, 45);
+-- ============================================================
+-- A1. Frame optimization bypass
+-- ============================================================
+-- optimize_window_clauses() must not apply frame optimization to RPR windows.
+-- Non-RPR: frame is optimized (RANGE -> ROWS conversion, etc.)
+-- RPR: frame must stay as specified (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+-- Non-RPR window with default frame -> frame optimization applied
+EXPLAIN (COSTS OFF)
+SELECT count(*) OVER w FROM rpr_integ
+WINDOW w AS (ORDER BY id);
+ QUERY PLAN
+-----------------------------------
+ WindowAgg
+ Window: w AS (ORDER BY id)
+ -> Sort
+ Sort Key: id
+ -> Seq Scan on rpr_integ
+(5 rows)
+
+-- RPR window -> frame optimization must NOT change the frame
+EXPLAIN (COSTS OFF)
+SELECT count(*) OVER w FROM rpr_integ
+WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val));
+ 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)
+
+-- ============================================================
+-- A2. Run condition pushdown bypass
+-- ============================================================
+-- find_window_run_conditions() must not push monotonic filters as Run Conditions
+-- for RPR windows, because RPR results are match-dependent, not monotonic.
+-- Non-RPR: count(*) > 0 pushed down as Run Condition
+EXPLAIN (COSTS OFF)
+SELECT * FROM (
+ SELECT count(*) OVER w AS cnt
+ FROM rpr_integ
+ WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+) t WHERE cnt > 0;
+ QUERY PLAN
+-----------------------------------------------------------------------------------------------
+ Subquery Scan on t
+ -> WindowAgg
+ Window: w AS (ORDER BY rpr_integ.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Run Condition: (count(*) OVER w > 0)
+ -> Sort
+ Sort Key: rpr_integ.id
+ -> Seq Scan on rpr_integ
+(7 rows)
+
+-- RPR: count(*) > 0 must NOT be pushed down (appears as Filter, not Run Condition)
+EXPLAIN (COSTS OFF)
+SELECT * FROM (
+ SELECT count(*) OVER w AS cnt
+ FROM rpr_integ
+ WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val))
+) t WHERE cnt > 0;
+ QUERY PLAN
+-----------------------------------------------------------------------------------------------
+ Subquery Scan on t
+ Filter: (t.cnt > 0)
+ -> WindowAgg
+ Window: w AS (ORDER BY rpr_integ.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a b+
+ -> Sort
+ Sort Key: rpr_integ.id
+ -> Seq Scan on rpr_integ
+(8 rows)
+
+-- Verify results are correct
+SELECT * FROM (
+ SELECT id, val, count(*) OVER w AS cnt
+ FROM rpr_integ
+ WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val))
+) t WHERE cnt > 0
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 2
+ 3 | 15 | 2
+ 5 | 5 | 3
+ 8 | 20 | 3
+(4 rows)
+
+-- ============================================================
+-- A3. Window dedup prevention (planner)
+-- ============================================================
+-- optimize_window_clauses() must not merge RPR and non-RPR windows
+-- even if they share the same base frame.
+-- Two windows with same ORDER BY: one RPR, one non-RPR -> separate WindowAgg nodes
+EXPLAIN (COSTS OFF)
+SELECT
+ count(*) OVER w_rpr AS rpr_cnt,
+ count(*) OVER w_normal AS normal_cnt
+FROM rpr_integ
+WINDOW
+ w_rpr AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val)),
+ w_normal AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING);
+ QUERY PLAN
+-----------------------------------------------------------------------------------------
+ WindowAgg
+ Window: w_normal AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ -> WindowAgg
+ Window: w_rpr AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a b+
+ -> Sort
+ Sort Key: id
+ -> Seq Scan on rpr_integ
+(8 rows)
+
+-- Verify both produce different results
+SELECT
+ id, val,
+ count(*) OVER w_rpr AS rpr_cnt,
+ count(*) OVER w_normal AS normal_cnt
+FROM rpr_integ
+WINDOW
+ w_rpr AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val)),
+ w_normal AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ORDER BY id;
+ id | val | rpr_cnt | normal_cnt
+----+-----+---------+------------
+ 1 | 10 | 2 | 10
+ 2 | 20 | 0 | 9
+ 3 | 15 | 2 | 8
+ 4 | 25 | 0 | 7
+ 5 | 5 | 3 | 6
+ 6 | 30 | 0 | 5
+ 7 | 35 | 0 | 4
+ 8 | 20 | 3 | 3
+ 9 | 40 | 0 | 2
+ 10 | 45 | 0 | 1
+(10 rows)
+
+-- ============================================================
+-- A4. Window dedup prevention (parser)
+-- ============================================================
+-- transformWindowFuncCall() must not merge two RPR windows with different DEFINE.
+-- Two RPR windows: same PATTERN structure, different DEFINE conditions.
+-- EXPLAIN: must show two separate WindowAgg nodes
+EXPLAIN (COSTS OFF)
+SELECT
+ count(*) OVER w1 AS cnt_up,
+ count(*) OVER w2 AS cnt_down
+FROM rpr_integ
+WINDOW
+ w1 AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val)),
+ w2 AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val < PREV(val));
+ QUERY PLAN
+--------------------------------------------------------------------------------------
+ WindowAgg
+ Window: w2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a b+
+ -> WindowAgg
+ Window: w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a b+
+ -> Sort
+ Sort Key: id
+ -> Seq Scan on rpr_integ
+(9 rows)
+
+-- Verify results differ
+SELECT
+ id, val,
+ count(*) OVER w1 AS cnt_up,
+ count(*) OVER w2 AS cnt_down
+FROM rpr_integ
+WINDOW
+ w1 AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val)),
+ w2 AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val < PREV(val))
+ORDER BY id;
+ id | val | cnt_up | cnt_down
+----+-----+--------+----------
+ 1 | 10 | 2 | 0
+ 2 | 20 | 0 | 2
+ 3 | 15 | 2 | 0
+ 4 | 25 | 0 | 2
+ 5 | 5 | 3 | 0
+ 6 | 30 | 0 | 0
+ 7 | 35 | 0 | 2
+ 8 | 20 | 3 | 0
+ 9 | 40 | 0 | 0
+ 10 | 45 | 0 | 0
+(10 rows)
+
+-- ============================================================
+-- A5. Unused window removal prevention
+-- ============================================================
+-- remove_unused_subquery_outputs() must not remove RPR window functions
+-- even when their output columns are not referenced by the outer query.
+-- RPR window output not directly referenced, but pattern matching must still execute
+SELECT count(*) FROM (
+ SELECT count(*) OVER w FROM rpr_integ
+ WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > PREV(val))
+) t;
+ count
+-------
+ 10
+(1 row)
+
+-- With PREV in DEFINE: window must be preserved
+SELECT count(*), sum(c) FROM (
+ SELECT count(*) OVER w AS c FROM rpr_integ
+ WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > PREV(val))
+) t;
+ count | sum
+-------+-----
+ 10 | 6
+(1 row)
+
+-- Without PREV but with DEFINE: window must still be preserved
+SELECT count(*), sum(c) FROM (
+ SELECT count(*) OVER w AS c FROM rpr_integ
+ WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS TRUE)
+) t;
+ count | sum
+-------+-----
+ 10 | 10
+(1 row)
+
+-- ============================================================
+-- A6. Inverse transition bypass
+-- ============================================================
+-- RPR windows must not use moving aggregate (inverse transition) optimization.
+-- Moving aggregates recalculate by subtracting leaving rows, but RPR's
+-- reduced frames make this invalid.
+-- sum() supports inverse transition. With RPR, it must not be used.
+-- Verify correctness: sum with RPR pattern matching
+SELECT id, val,
+ sum(val) OVER w AS pattern_sum
+FROM rpr_integ
+WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val))
+ORDER BY id;
+ id | val | pattern_sum
+----+-----+-------------
+ 1 | 10 | 30
+ 2 | 20 |
+ 3 | 15 | 40
+ 4 | 25 |
+ 5 | 5 | 70
+ 6 | 30 |
+ 7 | 35 |
+ 8 | 20 | 105
+ 9 | 40 |
+ 10 | 45 |
+(10 rows)
+
+-- ============================================================
+-- A7. Cost estimation RPR awareness
+-- ============================================================
+-- cost_windowagg() must account for DEFINE expression evaluation cost.
+-- Verify RPR WindowAgg cost > non-RPR WindowAgg cost.
+CREATE FUNCTION get_windowagg_cost(query text) RETURNS numeric AS $$
+DECLARE
+ plan json;
+ cost numeric;
+BEGIN
+ EXECUTE 'EXPLAIN (FORMAT JSON) ' || query INTO plan;
+ cost := (plan->0->'Plan'->>'Total Cost')::numeric;
+ RETURN cost;
+END;
+$$ LANGUAGE plpgsql;
+SELECT get_windowagg_cost(
+ 'SELECT count(*) OVER w FROM rpr_integ
+ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+ C+) DEFINE B AS val > PREV(val), C AS val < PREV(val))')
+ >
+ get_windowagg_cost(
+ 'SELECT count(*) OVER w FROM rpr_integ
+ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)')
+ AS rpr_cost_is_higher;
+ rpr_cost_is_higher
+--------------------
+ t
+(1 row)
+
+DROP FUNCTION get_windowagg_cost(text);
+-- ============================================================
+-- A8. Subquery flattening prevention
+-- ============================================================
+-- is_simple_subquery() prevents pullup of subqueries with window functions.
+-- RPR subquery must not be flattened into the outer query.
+-- EXPLAIN must show Subquery Scan (not flattened).
+EXPLAIN (COSTS OFF)
+SELECT * FROM (
+ SELECT id, val, count(*) OVER w AS cnt
+ FROM rpr_integ
+ WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val))
+) sub
+WHERE cnt > 0;
+ QUERY PLAN
+-----------------------------------------------------------------------------------------------
+ Subquery Scan on sub
+ Filter: (sub.cnt > 0)
+ -> WindowAgg
+ Window: w AS (ORDER BY rpr_integ.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a b+
+ -> Sort
+ Sort Key: rpr_integ.id
+ -> Seq Scan on rpr_integ
+(8 rows)
+
+-- ============================================================
+-- A9. DEFINE expression non-propagation
+-- ============================================================
+-- DEFINE expressions must not appear in upper WindowAgg's targetlist.
+-- Only Var nodes should be propagated, not the full DEFINE expression.
+-- EXPLAIN VERBOSE: outer WindowAgg output must NOT contain DEFINE expression.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT
+ count(*) OVER w_rpr AS rpr_cnt,
+ count(*) OVER w_normal AS normal_cnt
+FROM rpr_integ
+WINDOW
+ w_rpr AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val)),
+ w_normal AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING);
+ QUERY PLAN
+---------------------------------------------------------------------------------------------------
+ WindowAgg
+ Output: (count(*) OVER w_rpr), count(*) OVER w_normal, id, val
+ Window: w_normal AS (ORDER BY rpr_integ.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ -> WindowAgg
+ Output: id, val, count(*) OVER w_rpr
+ Window: w_rpr AS (ORDER BY rpr_integ.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a b+
+ -> Sort
+ Output: id, val
+ Sort Key: rpr_integ.id
+ -> Seq Scan on public.rpr_integ
+ Output: id, val
+(12 rows)
+
+-- ============================================================
+-- A10. RPR + LIMIT
+-- ============================================================
+-- LIMIT must not interfere with RPR pattern matching.
+-- The result should be a prefix of the unlimited query.
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_integ
+WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val))
+ORDER BY id
+LIMIT 5;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 2
+ 2 | 20 | 0
+ 3 | 15 | 2
+ 4 | 25 | 0
+ 5 | 5 | 3
+(5 rows)
+
+-- ============================================================
+-- B1. RPR + CTE
+-- ============================================================
+-- CTE with RPR, outer query aggregates
+WITH rpr_result AS (
+ SELECT id, val, count(*) OVER w AS cnt
+ FROM rpr_integ
+ WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val))
+)
+SELECT count(*) AS total_rows,
+ count(*) FILTER (WHERE cnt > 0) AS matched_rows,
+ max(cnt) AS max_match_len
+FROM rpr_result;
+ total_rows | matched_rows | max_match_len
+------------+--------------+---------------
+ 10 | 4 | 3
+(1 row)
+
+-- Multiple CTE references
+WITH rpr_result AS (
+ SELECT id, val, count(*) OVER w AS cnt
+ FROM rpr_integ
+ WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val))
+)
+SELECT r1.id, r1.cnt
+FROM rpr_result r1
+JOIN rpr_result r2 ON r1.id = r2.id AND r1.cnt = r2.cnt
+WHERE r1.cnt > 0
+ORDER BY r1.id;
+ id | cnt
+----+-----
+ 1 | 2
+ 3 | 2
+ 5 | 3
+ 8 | 3
+(4 rows)
+
+-- ============================================================
+-- B2. RPR + JOIN
+-- ============================================================
+CREATE TABLE rpr_integ2 (id INT, label TEXT);
+INSERT INTO rpr_integ2 VALUES
+ (1, 'a'), (2, 'b'), (3, 'c'), (4, 'd'), (5, 'e'),
+ (6, 'f'), (7, 'g'), (8, 'h'), (9, 'i'), (10, 'j');
+-- RPR subquery joined with another table
+SELECT r.id, r.cnt, j.label
+FROM (
+ SELECT id, count(*) OVER w AS cnt
+ FROM rpr_integ
+ WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val))
+) r
+JOIN rpr_integ2 j ON r.id = j.id
+WHERE r.cnt > 0
+ORDER BY r.id;
+ id | cnt | label
+----+-----+-------
+ 1 | 2 | a
+ 3 | 2 | c
+ 5 | 3 | e
+ 8 | 3 | h
+(4 rows)
+
+-- ============================================================
+-- B3. RPR + Set operations
+-- ============================================================
+-- UNION of RPR and non-RPR results
+SELECT id, cnt, 'rpr' AS source FROM (
+ SELECT id, count(*) OVER w AS cnt
+ FROM rpr_integ
+ WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val))
+) t WHERE cnt > 0
+UNION ALL
+SELECT id, count(*) OVER (ORDER BY id) AS cnt, 'normal' AS source
+FROM rpr_integ
+ORDER BY source, id;
+ id | cnt | source
+----+-----+--------
+ 1 | 1 | normal
+ 2 | 2 | normal
+ 3 | 3 | normal
+ 4 | 4 | normal
+ 5 | 5 | normal
+ 6 | 6 | normal
+ 7 | 7 | normal
+ 8 | 8 | normal
+ 9 | 9 | normal
+ 10 | 10 | normal
+ 1 | 2 | rpr
+ 3 | 2 | rpr
+ 5 | 3 | rpr
+ 8 | 3 | rpr
+(14 rows)
+
+-- ============================================================
+-- B4. RPR + Prepared statements
+-- ============================================================
+-- Plan caching must correctly serialize/deserialize RPR fields
+PREPARE rpr_prep AS
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_integ
+WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val))
+ORDER BY id;
+-- First execution (custom plan)
+EXECUTE rpr_prep;
+ 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)
+
+-- Subsequent executions (may use cached plan)
+EXECUTE rpr_prep;
+ 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)
+
+EXECUTE rpr_prep;
+ 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)
+
+EXECUTE rpr_prep;
+ 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)
+
+EXECUTE rpr_prep;
+ 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)
+
+EXECUTE rpr_prep;
+ 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)
+
+-- Verify cached plan produces same result
+EXECUTE rpr_prep;
+ 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)
+
+DEALLOCATE rpr_prep;
+-- ============================================================
+-- B5. RPR + Partitioned table
+-- ============================================================
+-- RPR pattern matching must work correctly across partitions.
+CREATE TABLE rpr_part (id INT, val INT) PARTITION BY RANGE (id);
+CREATE TABLE rpr_part_1 PARTITION OF rpr_part FOR VALUES FROM (1) TO (6);
+CREATE TABLE rpr_part_2 PARTITION OF rpr_part FOR VALUES FROM (6) TO (11);
+INSERT INTO rpr_part SELECT id, val FROM rpr_integ;
+-- Same query on partitioned vs non-partitioned should produce identical results
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_part
+WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val))
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 2
+ 2 | 20 | 0
+ 3 | 15 | 2
+ 4 | 25 | 0
+ 5 | 5 | 3
+ 6 | 30 | 0
+ 7 | 35 | 0
+ 8 | 20 | 3
+ 9 | 40 | 0
+ 10 | 45 | 0
+(10 rows)
+
+-- EXPLAIN: Append under Sort under WindowAgg
+EXPLAIN (COSTS OFF)
+SELECT count(*) OVER w FROM rpr_part
+WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val));
+ QUERY PLAN
+----------------------------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ORDER BY rpr_part.id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a b+
+ -> Sort
+ Sort Key: rpr_part.id
+ -> Append
+ -> Seq Scan on rpr_part_1
+ -> Seq Scan on rpr_part_2
+(8 rows)
+
+DROP TABLE rpr_part;
+-- ============================================================
+-- B6. RPR + LATERAL
+-- ============================================================
+-- RPR inside a LATERAL subquery. Qualified column references from the
+-- outer query are not yet supported in DEFINE, so this tests the basic
+-- case where LATERAL provides filtering but DEFINE uses only local columns.
+SELECT o.id AS outer_id, r.id, r.cnt
+FROM rpr_integ o,
+LATERAL (
+ SELECT id, count(*) OVER w AS cnt
+ FROM rpr_integ
+ WHERE id <= o.id
+ WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val))
+) r
+WHERE r.cnt > 0 AND o.id IN (5, 10)
+ORDER BY o.id, r.id;
+ outer_id | id | cnt
+----------+----+-----
+ 5 | 1 | 2
+ 5 | 3 | 2
+ 10 | 1 | 2
+ 10 | 3 | 2
+ 10 | 5 | 3
+ 10 | 8 | 3
+(6 rows)
+
+-- ============================================================
+-- B7. RPR + Recursive CTE
+-- ============================================================
+-- RPR applied to results of a recursive CTE.
+WITH RECURSIVE seq AS (
+ SELECT 1 AS id, 10 AS val
+ UNION ALL
+ SELECT id + 1, val + (CASE WHEN id % 3 = 0 THEN -15 ELSE 10 END)
+ FROM seq WHERE id < 10
+)
+SELECT id, val, count(*) OVER w AS cnt
+FROM seq
+WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val))
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 3
+ 2 | 20 | 0
+ 3 | 30 | 0
+ 4 | 15 | 3
+ 5 | 25 | 0
+ 6 | 35 | 0
+ 7 | 20 | 3
+ 8 | 30 | 0
+ 9 | 40 | 0
+ 10 | 25 | 0
+(10 rows)
+
+-- ============================================================
+-- B8. RPR + Incremental sort
+-- ============================================================
+-- Incremental sort may be used when data is partially sorted.
+CREATE INDEX rpr_integ_id_idx ON rpr_integ (id);
+SET enable_seqscan = off;
+EXPLAIN (COSTS OFF)
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_integ
+WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val));
+ QUERY PLAN
+-------------------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: a b+
+ -> Index Scan using rpr_integ_id_idx on rpr_integ
+(4 rows)
+
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_integ
+WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val))
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 2
+ 2 | 20 | 0
+ 3 | 15 | 2
+ 4 | 25 | 0
+ 5 | 5 | 3
+ 6 | 30 | 0
+ 7 | 35 | 0
+ 8 | 20 | 3
+ 9 | 40 | 0
+ 10 | 45 | 0
+(10 rows)
+
+RESET enable_seqscan;
+DROP INDEX rpr_integ_id_idx;
+-- ============================================================
+-- B9. RPR + Volatile function in DEFINE
+-- ============================================================
+-- Volatile functions must be evaluated per-row, not optimized away.
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_integ
+WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val) AND random() >= 0.0)
+ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 10 | 2
+ 2 | 20 | 0
+ 3 | 15 | 2
+ 4 | 25 | 0
+ 5 | 5 | 3
+ 6 | 30 | 0
+ 7 | 35 | 0
+ 8 | 20 | 3
+ 9 | 40 | 0
+ 10 | 45 | 0
+(10 rows)
+
+-- ============================================================
+-- B10. RPR + Correlated subquery in WHERE
+-- ============================================================
+-- RPR window in a correlated subquery. DEFINE references only local
+-- columns (no qualified refs), while correlation is in WHERE.
+SELECT o.id, o.val,
+ (SELECT count(*) OVER w
+ FROM rpr_integ i
+ WHERE i.id <= o.id
+ WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val))
+ ORDER BY id
+ LIMIT 1) AS first_cnt
+FROM rpr_integ o
+ORDER BY o.id;
+ id | val | first_cnt
+----+-----+-----------
+ 1 | 10 | 0
+ 2 | 20 | 2
+ 3 | 15 | 2
+ 4 | 25 | 2
+ 5 | 5 | 2
+ 6 | 30 | 2
+ 7 | 35 | 2
+ 8 | 20 | 2
+ 9 | 40 | 2
+ 10 | 45 | 2
+(10 rows)
+
+-- Cleanup
+DROP TABLE rpr_integ;
+DROP TABLE rpr_integ2;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 6a2d19d4410..dbfa3e8f2cc 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -107,7 +107,7 @@ test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combo
# ----------
# Row Pattern Recognition tests
# ----------
-test: rpr rpr_base rpr_explain rpr_nfa
+test: rpr rpr_base rpr_explain rpr_nfa rpr_integration
# ----------
# Another group of parallel tests (JSON related)
diff --git a/src/test/regress/sql/rpr_integration.sql b/src/test/regress/sql/rpr_integration.sql
new file mode 100644
index 00000000000..2ae42c92a27
--- /dev/null
+++ b/src/test/regress/sql/rpr_integration.sql
@@ -0,0 +1,537 @@
+-- ============================================================
+-- RPR Integration Tests
+-- Planner optimization interaction tests for Row Pattern Recognition
+-- ============================================================
+--
+-- Verifies that each planner optimization correctly handles RPR windows.
+-- Even if individual optimizations are tested elsewhere, this file
+-- provides a single checkpoint for all planner/RPR interactions.
+--
+-- A. Planner Optimization Protection Tests
+-- A1. Frame optimization bypass
+-- A2. Run condition pushdown bypass
+-- A3. Window dedup prevention (planner)
+-- A4. Window dedup prevention (parser)
+-- A5. Unused window removal prevention
+-- A6. Inverse transition bypass
+-- A7. Cost estimation RPR awareness
+-- A8. Subquery flattening prevention
+-- A9. DEFINE expression non-propagation
+-- A10. RPR + LIMIT
+--
+-- B. Integration Scenario Tests
+-- B1. RPR + CTE
+-- B2. RPR + JOIN
+-- B3. RPR + Set operations
+-- B4. RPR + Prepared statements
+-- B5. RPR + Partitioned table
+-- B6. RPR + LATERAL
+-- B7. RPR + Recursive CTE
+-- B8. RPR + Incremental sort
+-- B9. RPR + Volatile function in DEFINE
+-- B10. RPR + Correlated subquery
+--
+
+CREATE TABLE rpr_integ (id INT, val INT);
+INSERT INTO rpr_integ VALUES
+ (1, 10), (2, 20), (3, 15), (4, 25), (5, 5),
+ (6, 30), (7, 35), (8, 20), (9, 40), (10, 45);
+
+-- ============================================================
+-- A1. Frame optimization bypass
+-- ============================================================
+-- optimize_window_clauses() must not apply frame optimization to RPR windows.
+-- Non-RPR: frame is optimized (RANGE -> ROWS conversion, etc.)
+-- RPR: frame must stay as specified (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+
+-- Non-RPR window with default frame -> frame optimization applied
+EXPLAIN (COSTS OFF)
+SELECT count(*) OVER w FROM rpr_integ
+WINDOW w AS (ORDER BY id);
+
+-- RPR window -> frame optimization must NOT change the frame
+EXPLAIN (COSTS OFF)
+SELECT count(*) OVER w FROM rpr_integ
+WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val));
+
+-- ============================================================
+-- A2. Run condition pushdown bypass
+-- ============================================================
+-- find_window_run_conditions() must not push monotonic filters as Run Conditions
+-- for RPR windows, because RPR results are match-dependent, not monotonic.
+
+-- Non-RPR: count(*) > 0 pushed down as Run Condition
+EXPLAIN (COSTS OFF)
+SELECT * FROM (
+ SELECT count(*) OVER w AS cnt
+ FROM rpr_integ
+ WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+) t WHERE cnt > 0;
+
+-- RPR: count(*) > 0 must NOT be pushed down (appears as Filter, not Run Condition)
+EXPLAIN (COSTS OFF)
+SELECT * FROM (
+ SELECT count(*) OVER w AS cnt
+ FROM rpr_integ
+ WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val))
+) t WHERE cnt > 0;
+
+-- Verify results are correct
+SELECT * FROM (
+ SELECT id, val, count(*) OVER w AS cnt
+ FROM rpr_integ
+ WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val))
+) t WHERE cnt > 0
+ORDER BY id;
+
+-- ============================================================
+-- A3. Window dedup prevention (planner)
+-- ============================================================
+-- optimize_window_clauses() must not merge RPR and non-RPR windows
+-- even if they share the same base frame.
+
+-- Two windows with same ORDER BY: one RPR, one non-RPR -> separate WindowAgg nodes
+EXPLAIN (COSTS OFF)
+SELECT
+ count(*) OVER w_rpr AS rpr_cnt,
+ count(*) OVER w_normal AS normal_cnt
+FROM rpr_integ
+WINDOW
+ w_rpr AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val)),
+ w_normal AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING);
+
+-- Verify both produce different results
+SELECT
+ id, val,
+ count(*) OVER w_rpr AS rpr_cnt,
+ count(*) OVER w_normal AS normal_cnt
+FROM rpr_integ
+WINDOW
+ w_rpr AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val)),
+ w_normal AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ORDER BY id;
+
+-- ============================================================
+-- A4. Window dedup prevention (parser)
+-- ============================================================
+-- transformWindowFuncCall() must not merge two RPR windows with different DEFINE.
+-- Two RPR windows: same PATTERN structure, different DEFINE conditions.
+
+-- EXPLAIN: must show two separate WindowAgg nodes
+EXPLAIN (COSTS OFF)
+SELECT
+ count(*) OVER w1 AS cnt_up,
+ count(*) OVER w2 AS cnt_down
+FROM rpr_integ
+WINDOW
+ w1 AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val)),
+ w2 AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val < PREV(val));
+
+-- Verify results differ
+SELECT
+ id, val,
+ count(*) OVER w1 AS cnt_up,
+ count(*) OVER w2 AS cnt_down
+FROM rpr_integ
+WINDOW
+ w1 AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val)),
+ w2 AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val < PREV(val))
+ORDER BY id;
+
+-- ============================================================
+-- A5. Unused window removal prevention
+-- ============================================================
+-- remove_unused_subquery_outputs() must not remove RPR window functions
+-- even when their output columns are not referenced by the outer query.
+
+-- RPR window output not directly referenced, but pattern matching must still execute
+SELECT count(*) FROM (
+ SELECT count(*) OVER w FROM rpr_integ
+ WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > PREV(val))
+) t;
+
+-- With PREV in DEFINE: window must be preserved
+SELECT count(*), sum(c) FROM (
+ SELECT count(*) OVER w AS c FROM rpr_integ
+ WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS val > PREV(val))
+) t;
+
+-- Without PREV but with DEFINE: window must still be preserved
+SELECT count(*), sum(c) FROM (
+ SELECT count(*) OVER w AS c FROM rpr_integ
+ WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS TRUE)
+) t;
+
+-- ============================================================
+-- A6. Inverse transition bypass
+-- ============================================================
+-- RPR windows must not use moving aggregate (inverse transition) optimization.
+-- Moving aggregates recalculate by subtracting leaving rows, but RPR's
+-- reduced frames make this invalid.
+
+-- sum() supports inverse transition. With RPR, it must not be used.
+-- Verify correctness: sum with RPR pattern matching
+SELECT id, val,
+ sum(val) OVER w AS pattern_sum
+FROM rpr_integ
+WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val))
+ORDER BY id;
+
+-- ============================================================
+-- A7. Cost estimation RPR awareness
+-- ============================================================
+-- cost_windowagg() must account for DEFINE expression evaluation cost.
+-- Verify RPR WindowAgg cost > non-RPR WindowAgg cost.
+
+CREATE FUNCTION get_windowagg_cost(query text) RETURNS numeric AS $$
+DECLARE
+ plan json;
+ cost numeric;
+BEGIN
+ EXECUTE 'EXPLAIN (FORMAT JSON) ' || query INTO plan;
+ cost := (plan->0->'Plan'->>'Total Cost')::numeric;
+ RETURN cost;
+END;
+$$ LANGUAGE plpgsql;
+
+SELECT get_windowagg_cost(
+ 'SELECT count(*) OVER w FROM rpr_integ
+ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+ C+) DEFINE B AS val > PREV(val), C AS val < PREV(val))')
+ >
+ get_windowagg_cost(
+ 'SELECT count(*) OVER w FROM rpr_integ
+ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)')
+ AS rpr_cost_is_higher;
+
+DROP FUNCTION get_windowagg_cost(text);
+
+-- ============================================================
+-- A8. Subquery flattening prevention
+-- ============================================================
+-- is_simple_subquery() prevents pullup of subqueries with window functions.
+-- RPR subquery must not be flattened into the outer query.
+-- EXPLAIN must show Subquery Scan (not flattened).
+
+EXPLAIN (COSTS OFF)
+SELECT * FROM (
+ SELECT id, val, count(*) OVER w AS cnt
+ FROM rpr_integ
+ WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val))
+) sub
+WHERE cnt > 0;
+
+-- ============================================================
+-- A9. DEFINE expression non-propagation
+-- ============================================================
+-- DEFINE expressions must not appear in upper WindowAgg's targetlist.
+-- Only Var nodes should be propagated, not the full DEFINE expression.
+-- EXPLAIN VERBOSE: outer WindowAgg output must NOT contain DEFINE expression.
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT
+ count(*) OVER w_rpr AS rpr_cnt,
+ count(*) OVER w_normal AS normal_cnt
+FROM rpr_integ
+WINDOW
+ w_rpr AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val)),
+ w_normal AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING);
+
+-- ============================================================
+-- A10. RPR + LIMIT
+-- ============================================================
+-- LIMIT must not interfere with RPR pattern matching.
+-- The result should be a prefix of the unlimited query.
+
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_integ
+WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val))
+ORDER BY id
+LIMIT 5;
+
+-- ============================================================
+-- B1. RPR + CTE
+-- ============================================================
+
+-- CTE with RPR, outer query aggregates
+WITH rpr_result AS (
+ SELECT id, val, count(*) OVER w AS cnt
+ FROM rpr_integ
+ WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val))
+)
+SELECT count(*) AS total_rows,
+ count(*) FILTER (WHERE cnt > 0) AS matched_rows,
+ max(cnt) AS max_match_len
+FROM rpr_result;
+
+-- Multiple CTE references
+WITH rpr_result AS (
+ SELECT id, val, count(*) OVER w AS cnt
+ FROM rpr_integ
+ WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val))
+)
+SELECT r1.id, r1.cnt
+FROM rpr_result r1
+JOIN rpr_result r2 ON r1.id = r2.id AND r1.cnt = r2.cnt
+WHERE r1.cnt > 0
+ORDER BY r1.id;
+
+-- ============================================================
+-- B2. RPR + JOIN
+-- ============================================================
+
+CREATE TABLE rpr_integ2 (id INT, label TEXT);
+INSERT INTO rpr_integ2 VALUES
+ (1, 'a'), (2, 'b'), (3, 'c'), (4, 'd'), (5, 'e'),
+ (6, 'f'), (7, 'g'), (8, 'h'), (9, 'i'), (10, 'j');
+
+-- RPR subquery joined with another table
+SELECT r.id, r.cnt, j.label
+FROM (
+ SELECT id, count(*) OVER w AS cnt
+ FROM rpr_integ
+ WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val))
+) r
+JOIN rpr_integ2 j ON r.id = j.id
+WHERE r.cnt > 0
+ORDER BY r.id;
+
+-- ============================================================
+-- B3. RPR + Set operations
+-- ============================================================
+
+-- UNION of RPR and non-RPR results
+SELECT id, cnt, 'rpr' AS source FROM (
+ SELECT id, count(*) OVER w AS cnt
+ FROM rpr_integ
+ WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val))
+) t WHERE cnt > 0
+UNION ALL
+SELECT id, count(*) OVER (ORDER BY id) AS cnt, 'normal' AS source
+FROM rpr_integ
+ORDER BY source, id;
+
+-- ============================================================
+-- B4. RPR + Prepared statements
+-- ============================================================
+
+-- Plan caching must correctly serialize/deserialize RPR fields
+PREPARE rpr_prep AS
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_integ
+WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val))
+ORDER BY id;
+
+-- First execution (custom plan)
+EXECUTE rpr_prep;
+
+-- Subsequent executions (may use cached plan)
+EXECUTE rpr_prep;
+EXECUTE rpr_prep;
+EXECUTE rpr_prep;
+EXECUTE rpr_prep;
+EXECUTE rpr_prep;
+
+-- Verify cached plan produces same result
+EXECUTE rpr_prep;
+
+DEALLOCATE rpr_prep;
+
+-- ============================================================
+-- B5. RPR + Partitioned table
+-- ============================================================
+-- RPR pattern matching must work correctly across partitions.
+
+CREATE TABLE rpr_part (id INT, val INT) PARTITION BY RANGE (id);
+CREATE TABLE rpr_part_1 PARTITION OF rpr_part FOR VALUES FROM (1) TO (6);
+CREATE TABLE rpr_part_2 PARTITION OF rpr_part FOR VALUES FROM (6) TO (11);
+INSERT INTO rpr_part SELECT id, val FROM rpr_integ;
+
+-- Same query on partitioned vs non-partitioned should produce identical results
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_part
+WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val))
+ORDER BY id;
+
+-- EXPLAIN: Append under Sort under WindowAgg
+EXPLAIN (COSTS OFF)
+SELECT count(*) OVER w FROM rpr_part
+WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val));
+
+DROP TABLE rpr_part;
+
+-- ============================================================
+-- B6. RPR + LATERAL
+-- ============================================================
+-- RPR inside a LATERAL subquery. Qualified column references from the
+-- outer query are not yet supported in DEFINE, so this tests the basic
+-- case where LATERAL provides filtering but DEFINE uses only local columns.
+
+SELECT o.id AS outer_id, r.id, r.cnt
+FROM rpr_integ o,
+LATERAL (
+ SELECT id, count(*) OVER w AS cnt
+ FROM rpr_integ
+ WHERE id <= o.id
+ WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val))
+) r
+WHERE r.cnt > 0 AND o.id IN (5, 10)
+ORDER BY o.id, r.id;
+
+-- ============================================================
+-- B7. RPR + Recursive CTE
+-- ============================================================
+-- RPR applied to results of a recursive CTE.
+
+WITH RECURSIVE seq AS (
+ SELECT 1 AS id, 10 AS val
+ UNION ALL
+ SELECT id + 1, val + (CASE WHEN id % 3 = 0 THEN -15 ELSE 10 END)
+ FROM seq WHERE id < 10
+)
+SELECT id, val, count(*) OVER w AS cnt
+FROM seq
+WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val))
+ORDER BY id;
+
+-- ============================================================
+-- B8. RPR + Incremental sort
+-- ============================================================
+-- Incremental sort may be used when data is partially sorted.
+
+CREATE INDEX rpr_integ_id_idx ON rpr_integ (id);
+SET enable_seqscan = off;
+
+EXPLAIN (COSTS OFF)
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_integ
+WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val));
+
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_integ
+WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val))
+ORDER BY id;
+
+RESET enable_seqscan;
+DROP INDEX rpr_integ_id_idx;
+
+-- ============================================================
+-- B9. RPR + Volatile function in DEFINE
+-- ============================================================
+-- Volatile functions must be evaluated per-row, not optimized away.
+
+SELECT id, val, count(*) OVER w AS cnt
+FROM rpr_integ
+WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val) AND random() >= 0.0)
+ORDER BY id;
+
+-- ============================================================
+-- B10. RPR + Correlated subquery in WHERE
+-- ============================================================
+-- RPR window in a correlated subquery. DEFINE references only local
+-- columns (no qualified refs), while correlation is in WHERE.
+
+SELECT o.id, o.val,
+ (SELECT count(*) OVER w
+ FROM rpr_integ i
+ WHERE i.id <= o.id
+ WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE B AS val > PREV(val))
+ ORDER BY id
+ LIMIT 1) AS first_cnt
+FROM rpr_integ o
+ORDER BY o.id;
+
+-- Cleanup
+DROP TABLE rpr_integ;
+DROP TABLE rpr_integ2;
--
2.50.1 (Apple Git-155)
[text/plain] nocfbot-0008-Replace-reduced-frame-map-with-single-match-result.txt (21.0K, 10-nocfbot-0008-Replace-reduced-frame-map-with-single-match-result.txt)
download | inline diff:
From 59e99c6b9322f402df560bc693863491487e12db Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Thu, 2 Apr 2026 14:09:12 +0900
Subject: [PATCH] Replace reduced frame map with single match result
The reduced frame map was a per-row byte array tracking match status.
Since rows are processed sequentially and only one match is active
at a time, replace it with four scalar fields: valid, matched,
start, and length.
Also distinguish empty matches (FIN reached with zero rows consumed)
from unmatched rows via RF_EMPTY_MATCH, counted as matched in NFA
statistics.
Widen row_is_in_reduced_frame() return type from int to int64,
since it returns rpr_match_length which is int64.
---
src/backend/executor/execRPR.c | 56 +++---
src/backend/executor/nodeWindowAgg.c | 233 +++++++++-------------
src/include/nodes/execnodes.h | 21 +-
src/test/regress/expected/rpr_explain.out | 8 +-
4 files changed, 132 insertions(+), 186 deletions(-)
diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 58f9da0b814..1cbe8e14780 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -549,7 +549,7 @@
*
* (1) Find or create a context for the target row
* (2) Enter the row processing loop
- * (3) After the loop ends, record the result in reduced_frame_map
+ * (3) After the loop ends, record the match result
*
* Pseudocode of the row processing loop:
*
@@ -923,18 +923,19 @@
* Chapter X Match Result Processing
* ============================================================================
*
- * X-1. Reduced Frame Map
+ * X-1. Match Result
*
- * RPR match results are recorded in a byte array called reduced_frame_map.
- * One byte is allocated per row, and the value is one of the following:
+ * RPR tracks the current match result as a single entry in WindowAggState
+ * with four fields: rpr_match_valid, rpr_match_matched, rpr_match_start,
+ * and rpr_match_length. When valid is true, the entry describes the
+ * match result for the position at rpr_match_start: matched indicates
+ * success or failure, and length gives the number of rows consumed.
+ * A match with length 0 represents an empty match (pattern matched but
+ * consumed no rows). When valid is false, the position has not been
+ * evaluated yet (RF_NOT_DETERMINED).
*
- * RF_NOT_DETERMINED (0) Not yet processed
- * RF_FRAME_HEAD (1) Start row of the match
- * RF_SKIPPED (2) Interior row of the match (skipped in frame)
- * RF_UNMATCHED (3) Match failure
- *
- * The window function references this map to determine frame inclusion for
- * each row.
+ * The window function calls get_reduced_frame_status() to look up a
+ * row's status against the current match result.
*
* X-2. AFTER MATCH SKIP
*
@@ -1028,8 +1029,7 @@
* Phase 3 (Advance): skipped (no states)
*
* C0.states is empty, so the loop terminates.
- * matchEndRow < matchStartRow -> RF_UNMATCHED.
- * register_reduced_frame_map(0, RF_UNMATCHED).
+ * matchEndRow < matchStartRow -> unmatched.
*
* --- Row 1 (price=110) ---
*
@@ -1113,9 +1113,7 @@
*
* C1.states is empty and matchEndRow=3 >= matchStartRow=1 -> match succeeds.
*
- * register_reduced_frame_map(1, RF_FRAME_HEAD)
- * register_reduced_frame_map(2, RF_SKIPPED)
- * register_reduced_frame_map(3, RF_SKIPPED)
+ * rpr_match_start = 1, rpr_match_length = 3
*
* --- Row 4 (price=130) ---
*
@@ -1128,15 +1126,15 @@
* B: 130 < PREV(115) -> false
*
* ... No subsequent rows, so ExecRPRFinalizeAllContexts() is called.
- * Match incomplete -> RF_UNMATCHED.
+ * Match incomplete -> unmatched.
*
* XI-5. Final Result
*
- * Row 0: RF_UNMATCHED -> frame = the row itself
- * Row 1: RF_FRAME_HEAD -> frame = rows 1 through 3
- * Row 2: RF_SKIPPED -> inside row 1's match
- * Row 3: RF_SKIPPED -> inside row 1's match
- * Row 4: RF_UNMATCHED -> frame = the row itself
+ * Row 0: unmatched -> frame = the row itself
+ * Row 1: match head -> frame = rows 1 through 3
+ * Row 2: inside match -> skipped
+ * Row 3: inside match -> skipped
+ * Row 4: unmatched -> frame = the row itself
*
* Chapter XII Summary of Key Design Decisions
* ============================================================================
@@ -1579,12 +1577,14 @@ static void nfa_advance(WindowAggState *winstate, RPRNFAContext *ctx,
*
* - Empty match handling: The initial advance uses currentPos =
* startPos - 1 (before any row is consumed). If FIN is reached via
- * epsilon transitions alone, matchEndRow = startPos - 1 < matchStartRow,
- * resulting in UNMATCHED. For reluctant min=0 patterns (A*?, A??),
- * the skip path reaches FIN first and early termination prunes enter
- * paths, yielding an immediate empty (unmatched) result. For
- * greedy patterns (A*), the enter path adds VAR states first, then
- * the skip FIN is recorded but VAR states survive for later matching.
+ * epsilon transitions alone, matchEndRow = startPos - 1 < matchStartRow.
+ * If matchedState is set (FIN was reached), this is an empty match
+ * (RF_EMPTY_MATCH); otherwise it is unmatched (RF_UNMATCHED).
+ * For reluctant min=0 patterns (A*?, A??), the skip path reaches
+ * FIN first and early termination prunes enter paths, yielding an
+ * immediate empty match result. For greedy patterns (A*), the enter
+ * path adds VAR states first, then the skip FIN is recorded but VAR
+ * states survive for later matching.
*
* Context Absorption Runtime:
* ---------------------------
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index aed7cbef99a..dca2de570e8 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -247,13 +247,10 @@ static void attno_map(Node *node);
static bool attno_map_walker(Node *node, void *context);
static bool rpr_is_defined(WindowAggState *winstate);
-static int row_is_in_reduced_frame(WindowObject winobj, int64 pos);
+static int64 row_is_in_reduced_frame(WindowObject winobj, int64 pos);
-static void create_reduced_frame_map(WindowAggState *winstate);
-static void clear_reduced_frame_map(WindowAggState *winstate);
-static int get_reduced_frame_map(WindowAggState *winstate, int64 pos);
-static void register_reduced_frame_map(WindowAggState *winstate, int64 pos,
- int val);
+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);
@@ -1035,13 +1032,7 @@ eval_windowaggregates(WindowAggState *winstate)
*/
for (;;)
{
- int ret;
-
-#ifdef RPR_DEBUG
- printf("===== loop in frame starts: aggregatedupto: " INT64_FORMAT " aggregatedbase: " INT64_FORMAT "\n",
- winstate->aggregatedupto,
- winstate->aggregatedbase);
-#endif
+ int64 ret;
/* Fetch next row if we didn't already */
if (TupIsNull(agg_row_slot))
@@ -1065,27 +1056,18 @@ eval_windowaggregates(WindowAggState *winstate)
if (rpr_is_defined(winstate))
{
-#ifdef RPR_DEBUG
- printf("reduced_frame_map: %d aggregatedupto: " INT64_FORMAT " aggregatedbase: " INT64_FORMAT "\n",
- get_reduced_frame_map(winstate,
- winstate->aggregatedupto),
- winstate->aggregatedupto,
- winstate->aggregatedbase);
-#endif
-
/*
- * If the row status at currentpos is already decided and current
- * row status is not decided yet, it means we passed the last
- * reduced frame. Time to break the loop.
+ * If currentpos is already decided but aggregatedupto is not yet
+ * determined, we've passed the last reduced frame.
*/
- if (get_reduced_frame_map(winstate, winstate->currentpos)
+ if (get_reduced_frame_status(winstate, winstate->currentpos)
!= RF_NOT_DETERMINED &&
- get_reduced_frame_map(winstate, winstate->aggregatedupto)
+ get_reduced_frame_status(winstate, winstate->aggregatedupto)
== RF_NOT_DETERMINED)
break;
/*
- * Otherwise we need to calculate the reduced frame.
+ * Calculate the reduced frame for aggregatedupto.
*/
ret = row_is_in_reduced_frame(winstate->agg_winobj,
winstate->aggregatedupto);
@@ -1093,17 +1075,13 @@ eval_windowaggregates(WindowAggState *winstate)
break;
/*
- * Check if current row needs to be skipped due to no match.
+ * Check if current row is inside a match but not the head
+ * (skipped), and it's the base row for aggregation.
*/
- if (get_reduced_frame_map(winstate,
- winstate->aggregatedupto) == RF_SKIPPED &&
+ if (get_reduced_frame_status(winstate,
+ winstate->aggregatedupto) == RF_SKIPPED &&
winstate->aggregatedupto == winstate->aggregatedbase)
- {
-#ifdef RPR_DEBUG
- printf("skip current row for aggregation\n");
-#endif
break;
- }
}
/* Set tuple context for evaluation of aggregate arguments */
@@ -1358,7 +1336,8 @@ begin_partition(WindowAggState *winstate)
winstate->framehead_valid = false;
winstate->frametail_valid = false;
winstate->grouptail_valid = false;
- create_reduced_frame_map(winstate);
+ if (rpr_is_defined(winstate))
+ clear_reduced_frame(winstate);
winstate->spooled_rows = 0;
winstate->currentpos = 0;
winstate->frameheadpos = 0;
@@ -1581,9 +1560,8 @@ release_partition(WindowAggState *winstate)
winstate->partition_spooled = false;
winstate->next_partition = true;
- /* Reset RPR reduced frame map */
- winstate->reduced_frame_map = NULL;
- winstate->alloc_sz = 0;
+ /* Reset RPR match results */
+ clear_reduced_frame(winstate);
/* Reset NFA state for new partition */
winstate->nfaContext = NULL;
@@ -2366,11 +2344,6 @@ ExecWindowAgg(PlanState *pstate)
CHECK_FOR_INTERRUPTS();
-#ifdef RPR_DEBUG
- printf("ExecWindowAgg called. pos: " INT64_FORMAT "\n",
- winstate->currentpos);
-#endif
-
if (winstate->status == WINDOWAGG_DONE)
return NULL;
@@ -2480,14 +2453,13 @@ ExecWindowAgg(PlanState *pstate)
if (winstate->status == WINDOWAGG_RUN)
{
/*
- * If RPR is defined and skip mode is next row, we need to clear
- * existing reduced frame info so that we newly calculate the info
- * starting from current row.
+ * If RPR is defined and skip mode is next row, clear the current
+ * match so the next row triggers re-evaluation.
*/
if (rpr_is_defined(winstate))
{
if (winstate->rpSkipTo == ST_NEXT_ROW)
- clear_reduced_frame_map(winstate);
+ clear_reduced_frame(winstate);
}
/*
@@ -2986,9 +2958,6 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
name = te->resname;
expr = te->expr;
-#ifdef RPR_DEBUG
- printf("defineVariable name: %s\n", name);
-#endif
winstate->defineVariableList =
lappend(winstate->defineVariableList,
makeString(pstrdup(name)));
@@ -3668,7 +3637,7 @@ ignorenulls_getfuncarginframe(WindowObject winobj, int argno,
int notnull_offset;
int notnull_relpos;
int forward;
- int num_reduced_frame;
+ int64 num_reduced_frame;
Assert(WindowObjectIsValid(winobj));
winstate = winobj->winstate;
@@ -3968,14 +3937,12 @@ rpr_is_defined(WindowAggState *winstate)
* AFTER MATCH SKIP PAST LAST ROW
* -----------------
*/
-static int
+static int64
row_is_in_reduced_frame(WindowObject winobj, int64 pos)
{
WindowAggState *winstate = winobj->winstate;
int state;
- int rtn;
- int64 i;
- int num_reduced_rows;
+ int64 rtn;
if (!rpr_is_defined(winstate))
{
@@ -3984,14 +3951,10 @@ row_is_in_reduced_frame(WindowObject winobj, int64 pos)
* window frame.
*/
rtn = 0;
-#ifdef RPR_DEBUG
- printf("row_is_in_reduced_frame returns %d: pos: " INT64_FORMAT "\n",
- rtn, pos);
-#endif
return rtn;
}
- state = get_reduced_frame_map(winstate, pos);
+ state = get_reduced_frame_status(winstate, pos);
if (state == RF_NOT_DETERMINED)
{
@@ -3999,16 +3962,12 @@ row_is_in_reduced_frame(WindowObject winobj, int64 pos)
update_reduced_frame(winobj, pos);
}
- state = get_reduced_frame_map(winstate, pos);
+ state = get_reduced_frame_status(winstate, pos);
switch (state)
{
case RF_FRAME_HEAD:
- num_reduced_rows = 1;
- for (i = pos + 1;
- get_reduced_frame_map(winstate, i) == RF_SKIPPED; i++)
- num_reduced_rows++;
- rtn = num_reduced_rows;
+ rtn = winstate->rpr_match_length;
break;
case RF_SKIPPED:
@@ -4016,6 +3975,7 @@ row_is_in_reduced_frame(WindowObject winobj, int64 pos)
break;
case RF_UNMATCHED:
+ case RF_EMPTY_MATCH:
rtn = -1;
break;
@@ -4025,91 +3985,56 @@ row_is_in_reduced_frame(WindowObject winobj, int64 pos)
break;
}
-#ifdef RPR_DEBUG
- printf("row_is_in_reduced_frame returns %d: pos: " INT64_FORMAT "\n",
- rtn, pos);
-#endif
return rtn;
}
-#define REDUCED_FRAME_MAP_INIT_SIZE 1024L
-
/*
- * create_reduced_frame_map
- * Create reduced frame map
+ * clear_reduced_frame
+ * Clear reduced frame status
*/
static void
-create_reduced_frame_map(WindowAggState *winstate)
+clear_reduced_frame(WindowAggState *winstate)
{
- winstate->reduced_frame_map =
- MemoryContextAlloc(winstate->partcontext,
- REDUCED_FRAME_MAP_INIT_SIZE);
- winstate->alloc_sz = REDUCED_FRAME_MAP_INIT_SIZE;
- clear_reduced_frame_map(winstate);
+ winstate->rpr_match_valid = false;
+ winstate->rpr_match_matched = false;
+ winstate->rpr_match_start = -1;
+ winstate->rpr_match_length = 0;
}
/*
- * clear_reduced_frame_map
- * Clear reduced frame map
- */
-static void
-clear_reduced_frame_map(WindowAggState *winstate)
-{
- Assert(winstate->reduced_frame_map != NULL);
- MemSet(winstate->reduced_frame_map, RF_NOT_DETERMINED,
- winstate->alloc_sz);
-}
-
-/*
- * get_reduced_frame_map
- * Get reduced frame map specified by pos
+ * get_reduced_frame_status
+ * Look up a position against the current match.
+ *
+ * Returns one of the RF_* constants:
+ * RF_NOT_DETERMINED pos has not been processed yet
+ * RF_FRAME_HEAD pos is the start of the current match
+ * RF_SKIPPED pos is inside the current match but not the start
+ * RF_UNMATCHED pos is processed but not part of any match
*/
static int
-get_reduced_frame_map(WindowAggState *winstate, int64 pos)
+get_reduced_frame_status(WindowAggState *winstate, int64 pos)
{
- Assert(winstate->reduced_frame_map != NULL);
- Assert(pos >= 0);
+ int64 start = winstate->rpr_match_start;
+ int64 length = winstate->rpr_match_length;
- /*
- * If pos is not in the reduced frame map, it means that any info
- * regarding the pos has not been registered yet. So we return
- * RF_NOT_DETERMINED.
- */
- if (pos >= winstate->alloc_sz)
+ if (!winstate->rpr_match_valid)
return RF_NOT_DETERMINED;
- return winstate->reduced_frame_map[pos];
-}
+ /* Empty match: covers only the start position */
+ if (pos == start && winstate->rpr_match_matched && length == 0)
+ return RF_EMPTY_MATCH;
-/*
- * register_reduced_frame_map
- * Add/replace reduced frame map member at pos.
- * If there's no enough space, expand the map.
- */
-static void
-register_reduced_frame_map(WindowAggState *winstate, int64 pos, int val)
-{
- int64 realloc_sz;
-
- Assert(winstate->reduced_frame_map != NULL);
-
- if (pos < 0)
- elog(ERROR, "wrong pos: " INT64_FORMAT, pos);
-
- while (pos > winstate->alloc_sz - 1)
- {
- realloc_sz = winstate->alloc_sz * 2;
-
- winstate->reduced_frame_map =
- repalloc(winstate->reduced_frame_map, realloc_sz);
+ /* Outside the result range */
+ if (pos < start || pos >= start + length)
+ return RF_NOT_DETERMINED;
- MemSet(winstate->reduced_frame_map + winstate->alloc_sz,
- RF_NOT_DETERMINED, realloc_sz - winstate->alloc_sz);
+ if (!winstate->rpr_match_matched)
+ return RF_UNMATCHED;
- winstate->alloc_sz = realloc_sz;
- }
+ if (pos == start)
+ return RF_FRAME_HEAD;
- winstate->reduced_frame_map[pos] = val;
+ return RF_SKIPPED;
}
/*
@@ -4156,7 +4081,11 @@ update_reduced_frame(WindowObject winobj, int64 pos)
if (winstate->nfaContext != NULL &&
pos < winstate->nfaContext->matchStartRow)
{
- register_reduced_frame_map(winstate, pos, RF_UNMATCHED);
+ /* already processed, unmatched */
+ winstate->rpr_match_valid = true;
+ winstate->rpr_match_matched = false;
+ winstate->rpr_match_start = pos;
+ winstate->rpr_match_length = 1;
return;
}
@@ -4173,7 +4102,11 @@ update_reduced_frame(WindowObject winobj, int64 pos)
*/
if (pos <= winstate->nfaLastProcessedRow)
{
- register_reduced_frame_map(winstate, pos, RF_UNMATCHED);
+ /* already processed, unmatched */
+ winstate->rpr_match_valid = true;
+ winstate->rpr_match_matched = false;
+ winstate->rpr_match_start = pos;
+ winstate->rpr_match_length = 1;
return;
}
/* Not yet processed - create new context and start fresh */
@@ -4245,26 +4178,38 @@ register_result:
Assert(pos == targetCtx->matchStartRow);
/*
- * Register reduced frame map based on match result.
+ * Record match result.
*/
+ winstate->rpr_match_valid = true;
+ winstate->rpr_match_start = targetCtx->matchStartRow;
+
if (targetCtx->matchEndRow < targetCtx->matchStartRow)
{
matchLen = targetCtx->lastProcessedRow - targetCtx->matchStartRow + 1;
- register_reduced_frame_map(winstate, targetCtx->matchStartRow, RF_UNMATCHED);
- ExecRPRRecordContextFailure(winstate, matchLen);
+ if (targetCtx->matchedState != NULL)
+ {
+ /* Empty match: FIN reached but 0 rows consumed */
+ winstate->rpr_match_matched = true;
+ winstate->rpr_match_length = 0;
+ ExecRPRRecordContextSuccess(winstate, 0);
+ }
+ else
+ {
+ /* No match */
+ winstate->rpr_match_matched = false;
+ winstate->rpr_match_length = 1;
+ ExecRPRRecordContextFailure(winstate, matchLen);
+ }
ExecRPRFreeContext(winstate, targetCtx);
return;
}
- /* Match succeeded - register frame map and record statistics */
+ /* Match succeeded */
matchLen = targetCtx->matchEndRow - targetCtx->matchStartRow + 1;
- register_reduced_frame_map(winstate, targetCtx->matchStartRow, RF_FRAME_HEAD);
- for (int64 i = targetCtx->matchStartRow + 1; i <= targetCtx->matchEndRow; i++)
- {
- register_reduced_frame_map(winstate, i, RF_SKIPPED);
- }
+ winstate->rpr_match_matched = true;
+ winstate->rpr_match_length = matchLen;
ExecRPRRecordContextSuccess(winstate, matchLen);
/* Remove the matched context */
@@ -4747,7 +4692,7 @@ WinGetSlotInFrame(WindowObject winobj, TupleTableSlot *slot,
WindowAggState *winstate;
int64 abs_pos;
int64 mark_pos;
- int num_reduced_frame;
+ int64 num_reduced_frame;
Assert(WindowObjectIsValid(winobj));
winstate = winobj->winstate;
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 33028c3f10b..c672d29f35b 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -2499,10 +2499,12 @@ typedef enum WindowAggStatus
* tuples during spool */
} WindowAggStatus;
-#define RF_NOT_DETERMINED 0
-#define RF_FRAME_HEAD 1
-#define RF_SKIPPED 2
-#define RF_UNMATCHED 3
+/* RPR reduced frame states returned by get_reduced_frame_status() */
+#define RF_NOT_DETERMINED 0 /* not yet processed */
+#define RF_FRAME_HEAD 1 /* start row of a match */
+#define RF_SKIPPED 2 /* interior row of a match */
+#define RF_UNMATCHED 3 /* no match at this row */
+#define RF_EMPTY_MATCH 4 /* empty match (0 rows); treated as unmatched */
/*
* RPRNFAState - single NFA state for pattern matching
@@ -2694,12 +2696,11 @@ typedef struct WindowAggState
TupleTableSlot *next_slot; /* NEXT row navigation operator */
TupleTableSlot *null_slot; /* all NULL slot */
- /*
- * Each byte corresponds to a row positioned at absolute its pos in
- * partition. See above definition for RF_*. Used for RPR.
- */
- char *reduced_frame_map;
- int64 alloc_sz; /* size of the map */
+ /* RPR current match result */
+ bool rpr_match_valid; /* true if a match result is set */
+ bool rpr_match_matched; /* true if the result was a match */
+ int64 rpr_match_start; /* start position of the match result */
+ int64 rpr_match_length; /* number of rows matched (0 = empty) */
} WindowAggState;
/* ----------------
diff --git a/src/test/regress/expected/rpr_explain.out b/src/test/regress/expected/rpr_explain.out
index bd345906133..79cbc246039 100644
--- a/src/test/regress/expected/rpr_explain.out
+++ b/src/test/regress/expected/rpr_explain.out
@@ -3348,8 +3348,8 @@ WINDOW w AS (
Pattern: ((a' b')+" c)*
Storage: Memory Maximum Storage: NkB
NFA States: 9 peak, 178 total, 0 merged
- NFA Contexts: 4 peak, 61 total, 22 pruned
- NFA: 1 matched (len 57/57/57.0), 0 mismatched
+ NFA Contexts: 4 peak, 61 total, 20 pruned
+ NFA: 3 matched (len 0/57/19.0), 0 mismatched
NFA: 0 absorbed, 37 skipped (len 1/3/2.0)
-> Function Scan on generate_series s (actual rows=60.00 loops=1)
(9 rows)
@@ -3385,8 +3385,8 @@ WINDOW w AS (
Pattern: (a (b c)+)*
Storage: Memory Maximum Storage: NkB
NFA States: 7 peak, 160 total, 0 merged
- NFA Contexts: 4 peak, 61 total, 22 pruned
- NFA: 1 matched (len 57/57/57.0), 0 mismatched
+ NFA Contexts: 4 peak, 61 total, 20 pruned
+ NFA: 3 matched (len 0/57/19.0), 0 mismatched
NFA: 0 absorbed, 37 skipped (len 1/3/2.0)
-> Function Scan on generate_series s (actual rows=60.00 loops=1)
(9 rows)
--
2.50.1 (Apple Git-155)
[text/plain] nocfbot-0009-Add-fixed-length-group-absorption-for-RPR.txt (55.5K, 11-nocfbot-0009-Add-fixed-length-group-absorption-for-RPR.txt)
download | inline diff:
From 7d9c68fffcf6eeebea395033da550c966456def2 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Sat, 4 Apr 2026 21:47:01 +0900
Subject: [PATCH] Add fixed-length group absorption for RPR
Extend context absorption to unbounded groups with fixed-length
children (min == max, recursively). Patterns like (A B{2})+ or
((A (B C){2}){2})+ are now absorbable, equivalent to unrolling to
{1,1} VARs at compile time without actually unrolling.
isFixedLengthChildren() recursively verifies min == max for all
children including nested subgroups, extending the existing Case 2
in isUnboundedStart().
Absorption comparison in nfa_states_covered requires states to be at
an ABSORBABLE judgment point, where count-dominance is guaranteed.
The inline advance in nfa_match is generalized to advance bounded
VARs within the absorbable region through END chains to reach the
judgment point.
Fix isAbsorbable propagation in nfa_advance_var and nfa_advance_end
exit paths, where reusing a state object skipped recomputation.
Mark VAR elements in the DFS visited bitmap at nfa_add_state_unique
instead of at nfa_advance_state entry, so that loop-back through ALT
to the same VAR is not incorrectly blocked by cycle detection.
---
src/backend/executor/execRPR.c | 110 ++++++--
src/backend/optimizer/plan/rpr.c | 136 +++++++--
src/test/regress/expected/rpr_base.out | 83 +++++-
src/test/regress/expected/rpr_explain.out | 206 +++++++++++++-
src/test/regress/expected/rpr_nfa.out | 321 ++++++++++++++++++++++
src/test/regress/sql/rpr_base.sql | 36 +++
src/test/regress/sql/rpr_explain.sql | 114 ++++++++
src/test/regress/sql/rpr_nfa.sql | 168 +++++++++++
8 files changed, 1111 insertions(+), 63 deletions(-)
diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 1cbe8e14780..8f0457e2b3c 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -1760,6 +1760,10 @@ nfa_add_state_unique(WindowAggState *winstate, RPRNFAContext *ctx, RPRNFAState *
RPRNFAState *s;
RPRNFAState *tail = NULL;
+ /* Mark VAR in visited before duplicate check to prevent DFS loops */
+ winstate->nfaVisitedElems[WORDNUM(state->elemIdx)] |=
+ ((bitmapword) 1 << BITNUM(state->elemIdx));
+
/* Check for duplicate and find tail */
for (s = ctx->states; s != NULL; s = s->next)
{
@@ -2033,6 +2037,14 @@ nfa_states_covered(RPRPattern *pattern, RPRNFAContext *older, RPRNFAContext *new
elem = &pattern->elements[newerState->elemIdx];
depth = elem->depth;
+ /*
+ * Only compare at absorption judgment points (RPR_ELEM_ABSORBABLE).
+ * Judgment points are where count-dominance guarantees the newer
+ * context's future matches are a subset of the older's.
+ */
+ if (!RPRElemIsAbsorbable(elem))
+ return false;
+
for (olderState = older->states; olderState != NULL; olderState = olderState->next)
{
CHECK_FOR_INTERRUPTS();
@@ -2175,9 +2187,10 @@ nfa_eval_var_match(WindowAggState *winstate, RPRPatternElement *elem,
* - not matched: remove state (exit alternatives already exist from
* previous advance when count >= min was satisfied)
*
- * For simple VARs (min=max=1) followed by END:
- * - Advance to END and update group count before absorb phase
- * - This ensures absorption can compare states by group completion
+ * For VARs that reached max count followed by END:
+ * - Advance through END chain to reach absorption judgment point
+ * - Only deterministic exits (count >= max, max != INF) are handled
+ * - Chains through END elements while count >= max (must-exit path)
*
* Non-VAR elements (ALT, END, FIN) are kept as-is for advance phase.
*/
@@ -2191,9 +2204,9 @@ nfa_match(WindowAggState *winstate, RPRNFAContext *ctx, bool *varMatched)
RPRNFAState *nextState;
/*
- * Evaluate VAR elements against current row. For simple VARs with END
- * next, advance to END and update group count inline so absorb phase can
- * compare states properly.
+ * Evaluate VAR elements against current row. For VARs that reach max
+ * count with END next, advance through END chain inline so absorb phase
+ * can compare states at judgment points.
*/
for (state = ctx->states; state != NULL; state = nextState)
{
@@ -2223,34 +2236,61 @@ nfa_match(WindowAggState *winstate, RPRNFAContext *ctx, bool *varMatched)
state->counts[depth] = count;
/*
- * For simple VAR (min=max=1) with END next, advance to END
- * and update group count inline. This keeps state in place,
- * preserving lexical order.
+ * For VAR at max count with END next, advance through END
+ * chain to reach the absorption judgment point. Only
+ * deterministic exits (count >= max, max finite) are handled;
+ * unbounded VARs stay for advance phase.
*/
- if (elem->min == 1 && elem->max == 1 &&
+ if (RPRElemIsAbsorbableBranch(elem) &&
+ !RPRElemIsAbsorbable(elem) &&
+ count >= elem->max &&
RPRElemIsEnd(&elements[elem->next]))
{
RPRPatternElement *endElem = &elements[elem->next];
int endDepth = endElem->depth;
int32 endCount = state->counts[endDepth];
- Assert(count == 1);
-
- /* Increment group count with overflow protection */
+ /* Increment group count */
if (endCount < RPR_COUNT_MAX)
endCount++;
-
- /*
- * END's max can never be exceeded here because
- * nfa_advance_end only loops when count < max, so
- * endCount entering inline advance is at most max-1, and
- * incrementing yields at most max.
- */
Assert(endElem->max == RPR_QUANTITY_INF ||
endCount <= endElem->max);
state->elemIdx = elem->next;
state->counts[endDepth] = endCount;
+
+ /*
+ * Chain through END elements within the absorbable region
+ * (ABSORBABLE_BRANCH) until reaching the judgment point
+ * (ABSORBABLE). Continue only on must-exit path (count
+ * >= max) with END next.
+ */
+ while (RPRElemIsAbsorbableBranch(endElem) &&
+ !RPRElemIsAbsorbable(endElem) &&
+ endCount >= endElem->max &&
+ RPRElemIsEnd(&elements[endElem->next]))
+ {
+ RPRPatternElement *outerEnd = &elements[endElem->next];
+ int outerDepth = outerEnd->depth;
+ int32 outerCount = state->counts[outerDepth];
+
+ /* Reset exited group's count */
+ state->counts[endDepth] = 0;
+
+ /* Increment outer group count */
+ if (outerCount < RPR_COUNT_MAX)
+ outerCount++;
+ Assert(outerEnd->max == RPR_QUANTITY_INF ||
+ outerCount <= outerEnd->max);
+
+ state->elemIdx = endElem->next;
+ state->counts[outerDepth] = outerCount;
+
+ /* Advance to next END in chain */
+ endElem = outerEnd;
+ endDepth = outerDepth;
+ endCount = outerCount;
+ }
}
/* else: stay at VAR for advance phase */
}
@@ -2468,6 +2508,10 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
state->elemIdx = elem->next;
nextElem = &elements[state->elemIdx];
+ /* Update isAbsorbable for target element (monotonic) */
+ state->isAbsorbable = state->isAbsorbable &&
+ RPRElemIsAbsorbableBranch(nextElem);
+
/* END->END: increment outer END's count */
if (RPRElemIsEnd(nextElem) && state->counts[nextElem->depth] < RPR_COUNT_MAX)
state->counts[nextElem->depth]++;
@@ -2621,6 +2665,13 @@ nfa_advance_var(WindowAggState *winstate, RPRNFAContext *ctx,
state->elemIdx = elem->next;
nextElem = &elements[state->elemIdx];
+ /*
+ * Update isAbsorbable for target element (monotonic: AND
+ * preserves false)
+ */
+ state->isAbsorbable = state->isAbsorbable &&
+ RPRElemIsAbsorbableBranch(nextElem);
+
/*
* When exiting directly to an outer END, increment its iteration
* count. Simple VARs (min=max=1) handle this via inline advance
@@ -2650,6 +2701,13 @@ nfa_advance_var(WindowAggState *winstate, RPRNFAContext *ctx,
state->elemIdx = elem->next;
nextElem = &elements[state->elemIdx];
+ /*
+ * Update isAbsorbable for target element (monotonic: AND preserves
+ * false)
+ */
+ state->isAbsorbable = state->isAbsorbable &&
+ RPRElemIsAbsorbableBranch(nextElem);
+
/* See comment above: increment outer END count for quantified VARs */
if (RPRElemIsEnd(nextElem))
{
@@ -2686,11 +2744,19 @@ nfa_advance_state(WindowAggState *winstate, RPRNFAContext *ctx,
nfa_state_free(winstate, state);
return;
}
- winstate->nfaVisitedElems[WORDNUM(state->elemIdx)] |=
- ((bitmapword) 1 << BITNUM(state->elemIdx));
elem = &pattern->elements[state->elemIdx];
+ /*
+ * Mark epsilon elements (END, ALT, BEGIN, FIN) in visited to prevent
+ * infinite epsilon cycles. VAR elements are marked later when added to
+ * the state list (nfa_add_state_unique), allowing legitimate loop-back to
+ * the same VAR in a new iteration.
+ */
+ if (!RPRElemIsVar(elem))
+ winstate->nfaVisitedElems[WORDNUM(state->elemIdx)] |=
+ ((bitmapword) 1 << BITNUM(state->elemIdx));
+
switch (elem->varId)
{
case RPR_VARID_FIN:
diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index 2728a0b9fca..c0e9d134aa9 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -92,6 +92,8 @@ static bool fillRPRPattern(RPRPatternNode *node, RPRPattern *pat,
static void finalizeRPRPattern(RPRPattern *result);
/* Forward declarations - context absorption */
+static bool isFixedLengthChildren(RPRPattern *pattern, RPRElemIdx idx,
+ RPRDepth scopeDepth);
static bool isUnboundedStart(RPRPattern *pattern, RPRElemIdx idx);
static void computeAbsorbabilityRecursive(RPRPattern *pattern,
RPRElemIdx startIdx,
@@ -1524,6 +1526,70 @@ finalizeRPRPattern(RPRPattern *result)
*-------------------------------------------------------------------------
*/
+/*
+ * isFixedLengthChildren
+ * Check if all children at scopeDepth have fixed-length quantifiers
+ * (min == max), recursively for nested subgroups.
+ *
+ * A fixed-length group is semantically equivalent to unrolling each child
+ * to {1,1} copies, which is the existing Case 2 already proven correct
+ * for absorption. This check recognizes fixed-length groups at compile
+ * time without actually unrolling them.
+ *
+ * Traverses the flat element array starting at idx. For VAR elements,
+ * checks min == max. For BEGIN elements (nested subgroups), recurses
+ * into the subgroup and also checks the subgroup's END quantifier.
+ * ALT elements are rejected (alternation inside absorbable group is
+ * not supported).
+ *
+ * Returns true if all children are fixed-length, stopping at the END
+ * element at scopeDepth - 1.
+ */
+static bool
+isFixedLengthChildren(RPRPattern *pattern, RPRElemIdx idx, RPRDepth scopeDepth)
+{
+ RPRPatternElement *e = &pattern->elements[idx];
+
+ check_stack_depth();
+
+ while (e->depth == scopeDepth)
+ {
+ if (RPRElemIsVar(e))
+ {
+ if (e->min != e->max)
+ return false;
+ }
+ else if (RPRElemIsBegin(e))
+ {
+ RPRElemIdx childIdx = e->next;
+
+ /* Recurse into subgroup children at scopeDepth + 1 */
+ if (!isFixedLengthChildren(pattern, childIdx, scopeDepth + 1))
+ return false;
+
+ /* Advance past the subgroup to its END element */
+ e = &pattern->elements[e->next];
+ while (e->depth > scopeDepth)
+ e = &pattern->elements[e->next];
+
+ /* e is now the END at scopeDepth; check its quantifier */
+ Assert(RPRElemIsEnd(e) && e->depth == scopeDepth);
+ if (e->min != e->max)
+ return false;
+ }
+ else
+ {
+ /* ALT inside group: not supported for absorption */
+ return false;
+ }
+
+ Assert(e->next != RPR_ELEMIDX_INVALID);
+ e = &pattern->elements[e->next];
+ }
+
+ return true;
+}
+
/*
* isUnboundedStart
* Check if the element at idx starts an unbounded greedy sequence.
@@ -1533,29 +1599,31 @@ finalizeRPRPattern(RPRPattern *result)
* - Greedy (not reluctant)
* - At the start of current scope
*
- * Algorithm:
- * - Traverse elements within current scope (parentDepth to startDepth)
- * - For GROUP: must be unbounded greedy AND contain only simple {1,1} VARs
- * - Sets ABSORBABLE and ABSORBABLE_BRANCH flags on matching elements
- *
* Two cases are handled:
* 1. Simple VAR: A+ B C - A has max=INF, gets both flags
- * 2. Group: (A B)+ C - END has max=INF, all children are {1,1} VARs
- * A,B,END get ABSORBABLE_BRANCH, only END gets ABSORBABLE
+ * 2. Unbounded GROUP with fixed-length children: (A B{2})+ C
+ * All children must have min == max (recursively for nested subgroups).
+ * This is equivalent to unrolling to {1,1} VARs, e.g., (A B B)+ C.
+ * All elements within the group get ABSORBABLE_BRANCH.
+ * Only the unbounded END gets ABSORBABLE (judgment point).
+ * Examples:
+ * (A B{2})+ C - B{2} has min==max, step=3
+ * (A (B C){2} D)+ E - nested {2} subgroup, step=6
+ * ((A (B C){2}){2})+ - doubly nested {2}, step=10
+ * (A ((B C{3}){2} D){2} E)+ F - deep nesting, step=20
*
* Returns false for patterns where absorption cannot work:
* - A B+ (unbounded not at start)
* - A+? B (reluctant quantifier)
* - (A | B)+ (ALT inside group)
- * - (A B+)+ (unbounded element inside group)
- * - ((A B)+ C)+ (nested unbounded groups)
+ * - (A B+)+ (variable-length element inside group)
+ * - (A B{2,5})+ (min != max inside group)
*/
static bool
isUnboundedStart(RPRPattern *pattern, RPRElemIdx idx)
{
RPRPatternElement *elem = &pattern->elements[idx];
RPRDepth startDepth = elem->depth;
- RPRPatternElement *nextElem;
RPRPatternElement *e;
/* Case 1: Simple unbounded VAR at start (greedy only) */
@@ -1568,21 +1636,19 @@ isUnboundedStart(RPRPattern *pattern, RPRElemIdx idx)
}
/*
- * Case 2: Unbounded GROUP - traverse siblings at startDepth and check if
- * they're all simple {1,1} VARs, then check if END at startDepth - 1 is
- * unbounded greedy.
+ * Case 2: Unbounded GROUP with fixed-length children. Each child must
+ * have min == max (recursively for nested subgroups), ensuring a fixed
+ * step size per iteration so that count-dominance holds.
*/
- for (e = elem; e->depth == startDepth; e = nextElem)
- {
- /* Must be simple {1,1} VAR */
- if (!RPRElemIsVar(e) || e->min != 1 || e->max != 1)
- return false;
+ if (!isFixedLengthChildren(pattern, idx, startDepth))
+ return false;
- Assert(e->next != RPR_ELEMIDX_INVALID);
- nextElem = &pattern->elements[e->next];
- }
+ /* Find the END element at startDepth - 1 */
+ e = &pattern->elements[idx];
+ while (e->depth >= startDepth)
+ e = &pattern->elements[e->next];
- /* Now e should be END at startDepth - 1 */
+ /* END must be unbounded greedy */
if (e->depth == startDepth - 1 &&
RPRElemIsEnd(e) && e->max == RPR_QUANTITY_INF &&
!RPRElemIsReluctant(e))
@@ -1590,7 +1656,8 @@ isUnboundedStart(RPRPattern *pattern, RPRElemIdx idx)
Assert(e->jump == idx); /* END points back to first child */
/* Set ABSORBABLE_BRANCH on all children, ABSORBABLE on END only */
- for (e = elem; !RPRElemIsEnd(e); e = &pattern->elements[e->next])
+ for (e = elem; !RPRElemIsEnd(e) || e->depth >= startDepth;
+ e = &pattern->elements[e->next])
e->flags |= RPR_ELEM_ABSORBABLE_BRANCH;
e->flags |= RPR_ELEM_ABSORBABLE_BRANCH | RPR_ELEM_ABSORBABLE;
return true;
@@ -1654,12 +1721,25 @@ computeAbsorbabilityRecursive(RPRPattern *pattern, RPRElemIdx startIdx,
}
else if (RPRElemIsBegin(elem))
{
- /* BEGIN: skip to first child and check that */
- computeAbsorbabilityRecursive(pattern, elem->next, hasAbsorbable);
-
- /* Mark BEGIN element if contents are absorbable */
- if (*hasAbsorbable)
+ /*
+ * BEGIN: first try to treat this BEGIN's children as an unbounded
+ * group directly (handles nested fixed-length groups like ((A{2}
+ * B{3}){2})+). If that fails, skip to first child and recurse as
+ * before.
+ */
+ if (isUnboundedStart(pattern, elem->next))
+ {
+ *hasAbsorbable = true;
elem->flags |= RPR_ELEM_ABSORBABLE_BRANCH;
+ }
+ else
+ {
+ computeAbsorbabilityRecursive(pattern, elem->next, hasAbsorbable);
+
+ /* Mark BEGIN element if contents are absorbable */
+ if (*hasAbsorbable)
+ elem->flags |= RPR_ELEM_ABSORBABLE_BRANCH;
+ }
}
else
{
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index 3168468d0ae..7452cf1b3ab 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -3312,7 +3312,7 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-------------------------------------------------------------------------------
WindowAgg
Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
- Pattern: (a{2}){2,}
+ Pattern: (a{2}'){2,}"
-> Sort
Sort Key: id
-> Seq Scan on rpr_plan
@@ -4095,6 +4095,87 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-> Seq Scan on rpr_plan
(6 rows)
+-- Fixed-length group absorbable: (A{2} B{3})+ -> (a{2}' b{3}'){2,}"
+-- All children have min == max, equivalent to unrolling to {1,1}
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW PATTERN ((A{2} B{3})+)
+ DEFINE A AS val <= 50, B AS val > 50);
+ QUERY PLAN
+-------------------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a{2}' b{3}')+"
+ -> Sort
+ Sort Key: id
+ -> Seq Scan on rpr_plan
+(6 rows)
+
+-- Nested fixed-length group: (A (B C){2} D)+ -> absorbable
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW PATTERN ((A (B C){2} D)+)
+ DEFINE A AS val <= 20, B AS val <= 40, C AS val <= 60, D AS val > 60);
+ QUERY PLAN
+-------------------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a' (b' c'){2}' d')+"
+ -> Sort
+ Sort Key: id
+ -> Seq Scan on rpr_plan
+(6 rows)
+
+-- Nested fixed-length with inner quantifier: ((A{2} B{3}){2})+ -> absorbable
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW PATTERN (((A{2} B{3}){2})+)
+ DEFINE A AS val <= 50, B AS val > 50);
+ QUERY PLAN
+-------------------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: ((a{2}' b{3}'){2}')+"
+ -> Sort
+ Sort Key: id
+ -> Seq Scan on rpr_plan
+(6 rows)
+
+-- Non-absorbable fixed-length: (A B{2,5})+ -> no markers (min != max)
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW PATTERN ((A B{2,5})+)
+ DEFINE A AS val <= 50, B AS val > 50);
+ QUERY PLAN
+-------------------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a b{2,5})+
+ -> Sort
+ Sort Key: id
+ -> Seq Scan on rpr_plan
+(6 rows)
+
+-- Non-absorbable fixed-length: (A B?)+ -> no markers (min != max)
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW PATTERN ((A B?)+)
+ DEFINE A AS val <= 50, B AS val > 50);
+ 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_plan
+(6 rows)
+
-- Non-absorbable (unbounded not at start): A B+ -> a b+ (no markers)
EXPLAIN (COSTS OFF)
SELECT COUNT(*) OVER w FROM rpr_plan
diff --git a/src/test/regress/expected/rpr_explain.out b/src/test/regress/expected/rpr_explain.out
index 79cbc246039..560f21f44c2 100644
--- a/src/test/regress/expected/rpr_explain.out
+++ b/src/test/regress/expected/rpr_explain.out
@@ -462,10 +462,10 @@ WINDOW w AS (
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a' b')+"
Storage: Memory Maximum Storage: NkB
- NFA States: 3 peak, 91 total, 0 merged
- NFA Contexts: 2 peak, 61 total, 0 pruned
+ NFA States: 4 peak, 91 total, 0 merged
+ NFA Contexts: 3 peak, 61 total, 0 pruned
NFA: 1 matched (len 60/60/60.0), 0 mismatched
- NFA: 29 absorbed (len 1/1/1.0), 30 skipped (len 1/1/1.0)
+ NFA: 29 absorbed (len 2/2/2.0), 30 skipped (len 1/1/1.0)
-> Function Scan on generate_series s (actual rows=60.00 loops=1)
(9 rows)
@@ -904,6 +904,188 @@ WINDOW w AS (
-> Function Scan on generate_series s (actual rows=100.00 loops=1)
(9 rows)
+-- Fixed-length group absorption: (A B B)+ C
+-- B B merged to B{2}; absorbable with fixed-length check
+-- step_size=3 (A + B + B); v % 7 cycle gives 2 iterations per match
+CREATE VIEW rpr_ev20b AS
+SELECT count(*) OVER w
+FROM generate_series(1, 70) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A B B)+ C)
+ DEFINE A AS v % 7 IN (1, 4), B AS v % 7 IN (2, 3, 5, 6), C AS v % 7 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev20b'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+-------------------------
+ PATTERN ((a b b)+ c)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 70) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A B B)+ C)
+ DEFINE A AS v % 7 IN (1, 4), B AS v % 7 IN (2, 3, 5, 6), C AS v % 7 = 0
+);');
+ rpr_explain_filter
+----------------------------------------------------------------------
+ WindowAgg (actual rows=70.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a' b{2}')+" c
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 3 peak, 91 total, 0 merged
+ NFA Contexts: 4 peak, 71 total, 40 pruned
+ NFA: 10 matched (len 7/7/7.0), 0 mismatched
+ NFA: 10 absorbed (len 3/3/3.0), 10 skipped (len 1/1/1.0)
+ -> Function Scan on generate_series s (actual rows=70.00 loops=1)
+(9 rows)
+
+-- Nested fixed-length group absorption: (A (B C){2} D)+ E
+-- step_size = 1 + (1+1)*2 + 1 = 6; v % 13 cycle gives 2 iterations + E
+CREATE VIEW rpr_ev20c AS
+SELECT count(*) OVER w
+FROM generate_series(1, 65) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A (B C){2} D)+ E)
+ DEFINE A AS v % 13 IN (1, 7), B AS v % 13 IN (2, 4, 8, 10),
+ C AS v % 13 IN (3, 5, 9, 11), D AS v % 13 IN (6, 12),
+ E AS v % 13 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev20c'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+--------------------------------
+ PATTERN ((a (b c){2} d)+ e)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 65) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A (B C){2} D)+ E)
+ DEFINE A AS v % 13 IN (1, 7), B AS v % 13 IN (2, 4, 8, 10),
+ C AS v % 13 IN (3, 5, 9, 11), D AS v % 13 IN (6, 12),
+ E AS v % 13 = 0
+);');
+ rpr_explain_filter
+----------------------------------------------------------------------
+ WindowAgg (actual rows=65.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a' (b' c'){2}' d')+" e
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 3 peak, 76 total, 0 merged
+ NFA Contexts: 4 peak, 66 total, 50 pruned
+ NFA: 5 matched (len 13/13/13.0), 0 mismatched
+ NFA: 5 absorbed (len 6/6/6.0), 5 skipped (len 1/1/1.0)
+ -> Function Scan on generate_series s (actual rows=65.00 loops=1)
+(9 rows)
+
+-- Doubly nested fixed-length group absorption: (A ((B C{3}){2} D){2} E)+ F
+-- step_size = 1 + ((1+3)*2+1)*2 + 1 = 20; v % 41 cycle gives 2 iterations + F
+CREATE VIEW rpr_ev20d AS
+SELECT count(*) OVER w
+FROM generate_series(1, 82) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A ((B C C C){2} D){2} E)+ F)
+ DEFINE A AS v % 41 IN (1, 21),
+ B AS v % 41 IN (2, 6, 11, 15, 22, 26, 31, 35),
+ C AS v % 41 IN (3,4,5, 7,8,9, 12,13,14, 16,17,18,
+ 23,24,25, 27,28,29, 32,33,34, 36,37,38),
+ D AS v % 41 IN (10, 19, 30, 39),
+ E AS v % 41 IN (20, 40),
+ F AS v % 41 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev20d'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+-------------------------------------------
+ PATTERN ((a ((b c c c){2} d){2} e)+ f)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 82) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A ((B C C C){2} D){2} E)+ F)
+ DEFINE A AS v % 41 IN (1, 21),
+ B AS v % 41 IN (2, 6, 11, 15, 22, 26, 31, 35),
+ C AS v % 41 IN (3,4,5, 7,8,9, 12,13,14, 16,17,18,
+ 23,24,25, 27,28,29, 32,33,34, 36,37,38),
+ D AS v % 41 IN (10, 19, 30, 39),
+ E AS v % 41 IN (20, 40),
+ F AS v % 41 = 0
+);');
+ rpr_explain_filter
+----------------------------------------------------------------------
+ WindowAgg (actual rows=82.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: (a' ((b' c{3}'){2}' d'){2}' e')+" f
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 3 peak, 87 total, 0 merged
+ NFA Contexts: 4 peak, 83 total, 76 pruned
+ NFA: 2 matched (len 41/41/41.0), 0 mismatched
+ NFA: 2 absorbed (len 20/20/20.0), 2 skipped (len 1/1/1.0)
+ -> Function Scan on generate_series s (actual rows=82.00 loops=1)
+(9 rows)
+
+-- 3-level END chain absorption: ((A (B C){2}){2})+
+-- step_size = (1 + (1+1)*2) * 2 = 10; v % 21 cycle gives 2 iterations
+-- END chain: END(BC{2}) -> END(A..{2}) -> END(+, ABSORBABLE)
+CREATE VIEW rpr_ev20e AS
+SELECT count(*) OVER w
+FROM generate_series(1, 42) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (((A (B C){2}){2})+)
+ DEFINE A AS v % 21 IN (1, 6, 11, 16),
+ B AS v % 21 IN (2, 4, 7, 9, 12, 14, 17, 19),
+ C AS v % 21 IN (3, 5, 8, 10, 13, 15, 18, 20)
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev20e'), E'\n')) AS line WHERE line ~ 'PATTERN';
+ line
+---------------------------------
+ PATTERN (((a (b c){2}){2})+)
+(1 row)
+
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 42) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (((A (B C){2}){2})+)
+ DEFINE A AS v % 21 IN (1, 6, 11, 16),
+ B AS v % 21 IN (2, 4, 7, 9, 12, 14, 17, 19),
+ C AS v % 21 IN (3, 5, 8, 10, 13, 15, 18, 20)
+);');
+ rpr_explain_filter
+----------------------------------------------------------------------
+ WindowAgg (actual rows=42.00 loops=1)
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: ((a' (b' c'){2}'){2}')+"
+ Storage: Memory Maximum Storage: NkB
+ NFA States: 5 peak, 47 total, 0 merged
+ NFA Contexts: 5 peak, 43 total, 30 pruned
+ NFA: 2 matched (len 20/20/20.0), 0 mismatched
+ NFA: 2 absorbed (len 10/10/10.0), 8 skipped (len 1/5/3.0)
+ -> Function Scan on generate_series s (actual rows=42.00 loops=1)
+(9 rows)
+
-- ============================================================
-- Match Length Statistics Tests
-- ============================================================
@@ -1894,10 +2076,10 @@ WINDOW w AS (
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a' b' c')+"
Storage: Memory Maximum Storage: NkB
- NFA States: 3 peak, 81 total, 0 merged
- NFA Contexts: 3 peak, 61 total, 20 pruned
+ NFA States: 4 peak, 81 total, 0 merged
+ NFA Contexts: 4 peak, 61 total, 20 pruned
NFA: 1 matched (len 60/60/60.0), 0 mismatched
- NFA: 19 absorbed (len 1/1/1.0), 20 skipped (len 1/1/1.0)
+ NFA: 19 absorbed (len 3/3/3.0), 20 skipped (len 1/1/1.0)
-> Function Scan on generate_series s (actual rows=60.00 loops=1)
(9 rows)
@@ -2461,7 +2643,7 @@ WINDOW w AS (
NFA States: 4 peak, 102 total, 0 merged
NFA Contexts: 2 peak, 41 total, 10 pruned
NFA: 10 matched (len 3/3/3.0), 0 mismatched
- NFA: 20 absorbed (len 1/1/1.0), 0 skipped
+ NFA: 10 absorbed (len 1/1/1.0), 10 skipped (len 1/1/1.0)
-> Function Scan on generate_series s (actual rows=40.00 loops=1)
(9 rows)
@@ -3158,10 +3340,10 @@ WINDOW w AS (
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
Pattern: (a' b')+"
Storage: Memory Maximum Storage: NkB
- NFA States: 3 peak, 61 total, 0 merged
- NFA Contexts: 2 peak, 41 total, 0 pruned
+ NFA States: 4 peak, 61 total, 0 merged
+ NFA Contexts: 3 peak, 41 total, 0 pruned
NFA: 1 matched (len 40/40/40.0), 0 mismatched
- NFA: 19 absorbed (len 1/1/1.0), 20 skipped (len 1/1/1.0)
+ NFA: 19 absorbed (len 2/2/2.0), 20 skipped (len 1/1/1.0)
-> Function Scan on generate_series s (actual rows=40.00 loops=1)
(9 rows)
@@ -3234,12 +3416,12 @@ WINDOW w AS (
----------------------------------------------------------------------
WindowAgg (actual rows=60.00 loops=1)
Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
- Pattern: ((a b){2})+
+ Pattern: ((a' b'){2}')+"
Storage: Memory Maximum Storage: NkB
NFA States: 5 peak, 76 total, 0 merged
NFA Contexts: 4 peak, 61 total, 15 pruned
NFA: 1 matched (len 60/60/60.0), 0 mismatched
- NFA: 0 absorbed, 44 skipped (len 1/4/2.3)
+ NFA: 14 absorbed (len 4/4/4.0), 30 skipped (len 1/2/1.5)
-> Function Scan on generate_series s (actual rows=60.00 loops=1)
(9 rows)
diff --git a/src/test/regress/expected/rpr_nfa.out b/src/test/regress/expected/rpr_nfa.out
index 7b5a17fb671..250f7f131b1 100644
--- a/src/test/regress/expected/rpr_nfa.out
+++ b/src/test/regress/expected/rpr_nfa.out
@@ -447,6 +447,327 @@ WINDOW w AS (
7 | {X} | |
(7 rows)
+-- Fixed-length group absorption: (A B{2})+ C
+-- B{2} has min == max, equivalent to unrolling to (A B B)+ C
+WITH test_absorb_fixedlen AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['B']),
+ (4, ARRAY['A']),
+ (5, ARRAY['B']),
+ (6, ARRAY['B']),
+ (7, ARRAY['A']),
+ (8, ARRAY['B']),
+ (9, ARRAY['B']),
+ (10, ARRAY['C']),
+ (11, ARRAY['X'])
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_absorb_fixedlen
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A B{2})+ C)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 10
+ 2 | {B} | |
+ 3 | {B} | |
+ 4 | {A} | |
+ 5 | {B} | |
+ 6 | {B} | |
+ 7 | {A} | |
+ 8 | {B} | |
+ 9 | {B} | |
+ 10 | {C} | |
+ 11 | {X} | |
+(11 rows)
+
+-- Consecutive vars merged to fixed-length: (A B B)+ -> (A B{2})+
+-- mergeConsecutiveVars produces B{2}; now absorbable with fixed-length check
+WITH test_absorb_consecutive AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['B']),
+ (4, ARRAY['A']),
+ (5, ARRAY['B']),
+ (6, ARRAY['B']),
+ (7, ARRAY['A']),
+ (8, ARRAY['B']),
+ (9, ARRAY['B']),
+ (10, ARRAY['X'])
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_absorb_consecutive
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A B B)+)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 9
+ 2 | {B} | |
+ 3 | {B} | |
+ 4 | {A} | |
+ 5 | {B} | |
+ 6 | {B} | |
+ 7 | {A} | |
+ 8 | {B} | |
+ 9 | {B} | |
+ 10 | {X} | |
+(10 rows)
+
+-- Nested fixed-length group absorption: (A (B C){2} D)+ E
+-- Inner group {2} has min == max; absorbable via recursive check
+-- step_size = 1 + (1+1)*2 + 1 = 6
+WITH test_absorb_nested_fixedlen AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['C']),
+ (4, ARRAY['B']),
+ (5, ARRAY['C']),
+ (6, ARRAY['D']),
+ (7, ARRAY['A']),
+ (8, ARRAY['B']),
+ (9, ARRAY['C']),
+ (10, ARRAY['B']),
+ (11, ARRAY['C']),
+ (12, ARRAY['D']),
+ (13, ARRAY['E']),
+ (14, ARRAY['X'])
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_absorb_nested_fixedlen
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A (B C){2} D)+ E)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags),
+ D AS 'D' = ANY(flags),
+ E AS 'E' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 13
+ 2 | {B} | |
+ 3 | {C} | |
+ 4 | {B} | |
+ 5 | {C} | |
+ 6 | {D} | |
+ 7 | {A} | |
+ 8 | {B} | |
+ 9 | {C} | |
+ 10 | {B} | |
+ 11 | {C} | |
+ 12 | {D} | |
+ 13 | {E} | |
+ 14 | {X} | |
+(14 rows)
+
+-- Doubly nested fixed-length group absorption: (A ((B C{3}){2} D){2} E)+ F
+-- step_size = 1 + ((1+3)*2+1)*2 + 1 = 20; 2 iterations + F = 41 rows
+WITH test_absorb_doubly_nested AS (
+ SELECT v AS id, ARRAY[
+ CASE
+ WHEN v % 41 IN (1, 21) THEN 'A'
+ WHEN v % 41 IN (2, 6, 11, 15, 22, 26, 31, 35) THEN 'B'
+ WHEN v % 41 IN (3,4,5, 7,8,9, 12,13,14, 16,17,18,
+ 23,24,25, 27,28,29, 32,33,34, 36,37,38) THEN 'C'
+ WHEN v % 41 IN (10, 19, 30, 39) THEN 'D'
+ WHEN v % 41 IN (20, 40) THEN 'E'
+ WHEN v % 41 = 0 THEN 'F'
+ ELSE 'X'
+ END
+ ] AS flags
+ FROM generate_series(1, 82) AS s(v)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_absorb_doubly_nested
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A ((B C C C){2} D){2} E)+ F)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags),
+ D AS 'D' = ANY(flags),
+ E AS 'E' = ANY(flags),
+ F AS 'F' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 41
+ 2 | {B} | |
+ 3 | {C} | |
+ 4 | {C} | |
+ 5 | {C} | |
+ 6 | {B} | |
+ 7 | {C} | |
+ 8 | {C} | |
+ 9 | {C} | |
+ 10 | {D} | |
+ 11 | {B} | |
+ 12 | {C} | |
+ 13 | {C} | |
+ 14 | {C} | |
+ 15 | {B} | |
+ 16 | {C} | |
+ 17 | {C} | |
+ 18 | {C} | |
+ 19 | {D} | |
+ 20 | {E} | |
+ 21 | {A} | |
+ 22 | {B} | |
+ 23 | {C} | |
+ 24 | {C} | |
+ 25 | {C} | |
+ 26 | {B} | |
+ 27 | {C} | |
+ 28 | {C} | |
+ 29 | {C} | |
+ 30 | {D} | |
+ 31 | {B} | |
+ 32 | {C} | |
+ 33 | {C} | |
+ 34 | {C} | |
+ 35 | {B} | |
+ 36 | {C} | |
+ 37 | {C} | |
+ 38 | {C} | |
+ 39 | {D} | |
+ 40 | {E} | |
+ 41 | {F} | |
+ 42 | {A} | 42 | 82
+ 43 | {B} | |
+ 44 | {C} | |
+ 45 | {C} | |
+ 46 | {C} | |
+ 47 | {B} | |
+ 48 | {C} | |
+ 49 | {C} | |
+ 50 | {C} | |
+ 51 | {D} | |
+ 52 | {B} | |
+ 53 | {C} | |
+ 54 | {C} | |
+ 55 | {C} | |
+ 56 | {B} | |
+ 57 | {C} | |
+ 58 | {C} | |
+ 59 | {C} | |
+ 60 | {D} | |
+ 61 | {E} | |
+ 62 | {A} | |
+ 63 | {B} | |
+ 64 | {C} | |
+ 65 | {C} | |
+ 66 | {C} | |
+ 67 | {B} | |
+ 68 | {C} | |
+ 69 | {C} | |
+ 70 | {C} | |
+ 71 | {D} | |
+ 72 | {B} | |
+ 73 | {C} | |
+ 74 | {C} | |
+ 75 | {C} | |
+ 76 | {B} | |
+ 77 | {C} | |
+ 78 | {C} | |
+ 79 | {C} | |
+ 80 | {D} | |
+ 81 | {E} | |
+ 82 | {F} | |
+(82 rows)
+
+-- 3-level END chain: ((A (B C){2}){2})+
+-- Tests END(BC{2}) -> END(A..{2}) -> END(+) chaining
+-- 2 iterations of +, each 10 rows: (A B C B C)(A B C B C)
+WITH test_absorb_3level_end AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']), -- 1st + iter, 1st {2}, A
+ (2, ARRAY['B']),
+ (3, ARRAY['C']),
+ (4, ARRAY['B']),
+ (5, ARRAY['C']), -- 1st (BC){2} done
+ (6, ARRAY['A']), -- 1st + iter, 2nd {2}, A
+ (7, ARRAY['B']),
+ (8, ARRAY['C']),
+ (9, ARRAY['B']),
+ (10, ARRAY['C']), -- 2nd (BC){2} done, 1st {2} done, 1st + iter done
+ (11, ARRAY['A']), -- 2nd + iter, 1st {2}, A
+ (12, ARRAY['B']),
+ (13, ARRAY['C']),
+ (14, ARRAY['B']),
+ (15, ARRAY['C']),
+ (16, ARRAY['A']), -- 2nd + iter, 2nd {2}, A
+ (17, ARRAY['B']),
+ (18, ARRAY['C']),
+ (19, ARRAY['B']),
+ (20, ARRAY['C']), -- 2nd + iter done
+ (21, ARRAY['X']) -- no match, + ends
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_absorb_3level_end
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (((A (B C){2}){2})+)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags)
+);
+ id | flags | match_start | match_end
+----+-------+-------------+-----------
+ 1 | {A} | 1 | 20
+ 2 | {B} | |
+ 3 | {C} | |
+ 4 | {B} | |
+ 5 | {C} | |
+ 6 | {A} | |
+ 7 | {B} | |
+ 8 | {C} | |
+ 9 | {B} | |
+ 10 | {C} | |
+ 11 | {A} | |
+ 12 | {B} | |
+ 13 | {C} | |
+ 14 | {B} | |
+ 15 | {C} | |
+ 16 | {A} | |
+ 17 | {B} | |
+ 18 | {C} | |
+ 19 | {B} | |
+ 20 | {C} | |
+ 21 | {X} | |
+(21 rows)
+
-- Multiple unbounded: A+ B+ (first element unbounded enables absorption)
WITH test_multi_unbounded AS (
SELECT * FROM (VALUES
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index cf6c062ae85..8c23c7598a3 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -2600,6 +2600,42 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
AFTER MATCH SKIP PAST LAST ROW PATTERN ((A+ B | A B)*)
DEFINE A AS val <= 50, B AS val > 50);
+-- Fixed-length group absorbable: (A{2} B{3})+ -> (a{2}' b{3}'){2,}"
+-- All children have min == max, equivalent to unrolling to {1,1}
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW PATTERN ((A{2} B{3})+)
+ DEFINE A AS val <= 50, B AS val > 50);
+
+-- Nested fixed-length group: (A (B C){2} D)+ -> absorbable
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW PATTERN ((A (B C){2} D)+)
+ DEFINE A AS val <= 20, B AS val <= 40, C AS val <= 60, D AS val > 60);
+
+-- Nested fixed-length with inner quantifier: ((A{2} B{3}){2})+ -> absorbable
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW PATTERN (((A{2} B{3}){2})+)
+ DEFINE A AS val <= 50, B AS val > 50);
+
+-- Non-absorbable fixed-length: (A B{2,5})+ -> no markers (min != max)
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW PATTERN ((A B{2,5})+)
+ DEFINE A AS val <= 50, B AS val > 50);
+
+-- Non-absorbable fixed-length: (A B?)+ -> no markers (min != max)
+EXPLAIN (COSTS OFF)
+SELECT COUNT(*) OVER w FROM rpr_plan
+WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW PATTERN ((A B?)+)
+ DEFINE A AS val <= 50, B AS val > 50);
+
-- Non-absorbable (unbounded not at start): A B+ -> a b+ (no markers)
EXPLAIN (COSTS OFF)
SELECT COUNT(*) OVER w FROM rpr_plan
diff --git a/src/test/regress/sql/rpr_explain.sql b/src/test/regress/sql/rpr_explain.sql
index 93e06b0cbdf..237f0366631 100644
--- a/src/test/regress/sql/rpr_explain.sql
+++ b/src/test/regress/sql/rpr_explain.sql
@@ -578,6 +578,120 @@ WINDOW w AS (
DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0
);');
+-- Fixed-length group absorption: (A B B)+ C
+-- B B merged to B{2}; absorbable with fixed-length check
+-- step_size=3 (A + B + B); v % 7 cycle gives 2 iterations per match
+CREATE VIEW rpr_ev20b AS
+SELECT count(*) OVER w
+FROM generate_series(1, 70) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A B B)+ C)
+ DEFINE A AS v % 7 IN (1, 4), B AS v % 7 IN (2, 3, 5, 6), C AS v % 7 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev20b'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 70) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A B B)+ C)
+ DEFINE A AS v % 7 IN (1, 4), B AS v % 7 IN (2, 3, 5, 6), C AS v % 7 = 0
+);');
+
+-- Nested fixed-length group absorption: (A (B C){2} D)+ E
+-- step_size = 1 + (1+1)*2 + 1 = 6; v % 13 cycle gives 2 iterations + E
+CREATE VIEW rpr_ev20c AS
+SELECT count(*) OVER w
+FROM generate_series(1, 65) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A (B C){2} D)+ E)
+ DEFINE A AS v % 13 IN (1, 7), B AS v % 13 IN (2, 4, 8, 10),
+ C AS v % 13 IN (3, 5, 9, 11), D AS v % 13 IN (6, 12),
+ E AS v % 13 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev20c'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 65) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A (B C){2} D)+ E)
+ DEFINE A AS v % 13 IN (1, 7), B AS v % 13 IN (2, 4, 8, 10),
+ C AS v % 13 IN (3, 5, 9, 11), D AS v % 13 IN (6, 12),
+ E AS v % 13 = 0
+);');
+
+-- Doubly nested fixed-length group absorption: (A ((B C{3}){2} D){2} E)+ F
+-- step_size = 1 + ((1+3)*2+1)*2 + 1 = 20; v % 41 cycle gives 2 iterations + F
+CREATE VIEW rpr_ev20d AS
+SELECT count(*) OVER w
+FROM generate_series(1, 82) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A ((B C C C){2} D){2} E)+ F)
+ DEFINE A AS v % 41 IN (1, 21),
+ B AS v % 41 IN (2, 6, 11, 15, 22, 26, 31, 35),
+ C AS v % 41 IN (3,4,5, 7,8,9, 12,13,14, 16,17,18,
+ 23,24,25, 27,28,29, 32,33,34, 36,37,38),
+ D AS v % 41 IN (10, 19, 30, 39),
+ E AS v % 41 IN (20, 40),
+ F AS v % 41 = 0
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev20d'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 82) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A ((B C C C){2} D){2} E)+ F)
+ DEFINE A AS v % 41 IN (1, 21),
+ B AS v % 41 IN (2, 6, 11, 15, 22, 26, 31, 35),
+ C AS v % 41 IN (3,4,5, 7,8,9, 12,13,14, 16,17,18,
+ 23,24,25, 27,28,29, 32,33,34, 36,37,38),
+ D AS v % 41 IN (10, 19, 30, 39),
+ E AS v % 41 IN (20, 40),
+ F AS v % 41 = 0
+);');
+
+-- 3-level END chain absorption: ((A (B C){2}){2})+
+-- step_size = (1 + (1+1)*2) * 2 = 10; v % 21 cycle gives 2 iterations
+-- END chain: END(BC{2}) -> END(A..{2}) -> END(+, ABSORBABLE)
+CREATE VIEW rpr_ev20e AS
+SELECT count(*) OVER w
+FROM generate_series(1, 42) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (((A (B C){2}){2})+)
+ DEFINE A AS v % 21 IN (1, 6, 11, 16),
+ B AS v % 21 IN (2, 4, 7, 9, 12, 14, 17, 19),
+ C AS v % 21 IN (3, 5, 8, 10, 13, 15, 18, 20)
+);
+SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev20e'), E'\n')) AS line WHERE line ~ 'PATTERN';
+SELECT rpr_explain_filter('
+EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 42) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (((A (B C){2}){2})+)
+ DEFINE A AS v % 21 IN (1, 6, 11, 16),
+ B AS v % 21 IN (2, 4, 7, 9, 12, 14, 17, 19),
+ C AS v % 21 IN (3, 5, 8, 10, 13, 15, 18, 20)
+);');
+
-- ============================================================
-- Match Length Statistics Tests
-- ============================================================
diff --git a/src/test/regress/sql/rpr_nfa.sql b/src/test/regress/sql/rpr_nfa.sql
index 5edcb3357e6..aaa7b44f789 100644
--- a/src/test/regress/sql/rpr_nfa.sql
+++ b/src/test/regress/sql/rpr_nfa.sql
@@ -346,6 +346,174 @@ WINDOW w AS (
B AS 'B' = ANY(flags)
);
+-- Fixed-length group absorption: (A B{2})+ C
+-- B{2} has min == max, equivalent to unrolling to (A B B)+ C
+WITH test_absorb_fixedlen AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['B']),
+ (4, ARRAY['A']),
+ (5, ARRAY['B']),
+ (6, ARRAY['B']),
+ (7, ARRAY['A']),
+ (8, ARRAY['B']),
+ (9, ARRAY['B']),
+ (10, ARRAY['C']),
+ (11, ARRAY['X'])
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_absorb_fixedlen
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A B{2})+ C)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags)
+);
+
+-- Consecutive vars merged to fixed-length: (A B B)+ -> (A B{2})+
+-- mergeConsecutiveVars produces B{2}; now absorbable with fixed-length check
+WITH test_absorb_consecutive AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['B']),
+ (4, ARRAY['A']),
+ (5, ARRAY['B']),
+ (6, ARRAY['B']),
+ (7, ARRAY['A']),
+ (8, ARRAY['B']),
+ (9, ARRAY['B']),
+ (10, ARRAY['X'])
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_absorb_consecutive
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A B B)+)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags)
+);
+
+-- Nested fixed-length group absorption: (A (B C){2} D)+ E
+-- Inner group {2} has min == max; absorbable via recursive check
+-- step_size = 1 + (1+1)*2 + 1 = 6
+WITH test_absorb_nested_fixedlen AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']),
+ (2, ARRAY['B']),
+ (3, ARRAY['C']),
+ (4, ARRAY['B']),
+ (5, ARRAY['C']),
+ (6, ARRAY['D']),
+ (7, ARRAY['A']),
+ (8, ARRAY['B']),
+ (9, ARRAY['C']),
+ (10, ARRAY['B']),
+ (11, ARRAY['C']),
+ (12, ARRAY['D']),
+ (13, ARRAY['E']),
+ (14, ARRAY['X'])
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_absorb_nested_fixedlen
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A (B C){2} D)+ E)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags),
+ D AS 'D' = ANY(flags),
+ E AS 'E' = ANY(flags)
+);
+
+-- Doubly nested fixed-length group absorption: (A ((B C{3}){2} D){2} E)+ F
+-- step_size = 1 + ((1+3)*2+1)*2 + 1 = 20; 2 iterations + F = 41 rows
+WITH test_absorb_doubly_nested AS (
+ SELECT v AS id, ARRAY[
+ CASE
+ WHEN v % 41 IN (1, 21) THEN 'A'
+ WHEN v % 41 IN (2, 6, 11, 15, 22, 26, 31, 35) THEN 'B'
+ WHEN v % 41 IN (3,4,5, 7,8,9, 12,13,14, 16,17,18,
+ 23,24,25, 27,28,29, 32,33,34, 36,37,38) THEN 'C'
+ WHEN v % 41 IN (10, 19, 30, 39) THEN 'D'
+ WHEN v % 41 IN (20, 40) THEN 'E'
+ WHEN v % 41 = 0 THEN 'F'
+ ELSE 'X'
+ END
+ ] AS flags
+ FROM generate_series(1, 82) AS s(v)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_absorb_doubly_nested
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN ((A ((B C C C){2} D){2} E)+ F)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags),
+ D AS 'D' = ANY(flags),
+ E AS 'E' = ANY(flags),
+ F AS 'F' = ANY(flags)
+);
+
+-- 3-level END chain: ((A (B C){2}){2})+
+-- Tests END(BC{2}) -> END(A..{2}) -> END(+) chaining
+-- 2 iterations of +, each 10 rows: (A B C B C)(A B C B C)
+WITH test_absorb_3level_end AS (
+ SELECT * FROM (VALUES
+ (1, ARRAY['A']), -- 1st + iter, 1st {2}, A
+ (2, ARRAY['B']),
+ (3, ARRAY['C']),
+ (4, ARRAY['B']),
+ (5, ARRAY['C']), -- 1st (BC){2} done
+ (6, ARRAY['A']), -- 1st + iter, 2nd {2}, A
+ (7, ARRAY['B']),
+ (8, ARRAY['C']),
+ (9, ARRAY['B']),
+ (10, ARRAY['C']), -- 2nd (BC){2} done, 1st {2} done, 1st + iter done
+ (11, ARRAY['A']), -- 2nd + iter, 1st {2}, A
+ (12, ARRAY['B']),
+ (13, ARRAY['C']),
+ (14, ARRAY['B']),
+ (15, ARRAY['C']),
+ (16, ARRAY['A']), -- 2nd + iter, 2nd {2}, A
+ (17, ARRAY['B']),
+ (18, ARRAY['C']),
+ (19, ARRAY['B']),
+ (20, ARRAY['C']), -- 2nd + iter done
+ (21, ARRAY['X']) -- no match, + ends
+ ) AS t(id, flags)
+)
+SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end
+FROM test_absorb_3level_end
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (((A (B C){2}){2})+)
+ DEFINE
+ A AS 'A' = ANY(flags),
+ B AS 'B' = ANY(flags),
+ C AS 'C' = ANY(flags)
+);
+
-- Multiple unbounded: A+ B+ (first element unbounded enables absorption)
WITH test_multi_unbounded AS (
SELECT * FROM (VALUES
--
2.50.1 (Apple Git-155)
[text/plain] nocfbot-0010-Rename-rpr_explain-test-views-to-descriptive-names.txt (127.8K, 12-nocfbot-0010-Rename-rpr_explain-test-views-to-descriptive-names.txt)
download
[text/plain] nocfbot-0011-Fix-quote_identifier-deparse.txt (7.3K, 13-nocfbot-0011-Fix-quote_identifier-deparse.txt)
download | inline diff:
From 115425a057767ebf2b6e4c877641ed156b0ed07e Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Tue, 7 Apr 2026 08:53:46 +0900
Subject: [PATCH] Fix quote_identifier() for RPR pattern variable name deparse
Add quote_identifier() to PATTERN and DEFINE variable name output
in ruleutils.c and explain.c. Without quoting, mixed-case or
reserved-word variable names (e.g., "Start", "Up") lose their
case or conflict with keywords in pg_get_viewdef() output,
breaking pg_dump/pg_restore round-trips.
Add regression test with quoted identifiers ("Start", "Up") to
verify correct deparse in both pg_get_viewdef and EXPLAIN output.
---
src/backend/commands/explain.c | 2 +-
src/backend/utils/adt/ruleutils.c | 4 ++--
src/test/regress/expected/rpr_base.out | 24 +++++++++++++++++++++++
src/test/regress/expected/rpr_explain.out | 19 ++++++++++++++++++
src/test/regress/sql/rpr_base.sql | 10 ++++++++++
src/test/regress/sql/rpr_explain.sql | 12 ++++++++++++
6 files changed, 68 insertions(+), 3 deletions(-)
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 7f0367ce546..933eadab71e 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -3176,7 +3176,7 @@ deparse_rpr_var(RPRPattern *pattern, int *idx, StringInfoData *buf,
appendStringInfoChar(buf, ' ');
Assert(elem->varId < pattern->numVars);
- appendStringInfoString(buf, pattern->varNames[elem->varId]);
+ appendStringInfoString(buf, quote_identifier(pattern->varNames[elem->varId]));
append_rpr_quantifier(buf, elem);
*needSpace = true;
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index cfe24de43cf..c755a42efd6 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -7160,7 +7160,7 @@ get_rule_pattern_node(RPRPatternNode *node, deparse_context *context)
switch (node->nodeType)
{
case RPR_PATTERN_VAR:
- appendStringInfoString(buf, node->varName);
+ appendStringInfoString(buf, quote_identifier(node->varName));
append_pattern_quantifier(buf, node);
break;
@@ -7229,7 +7229,7 @@ get_rule_define(List *defineClause, bool force_colno, deparse_context *context)
{
TargetEntry *te = (TargetEntry *) lfirst(lc_def);
- appendStringInfo(buf, "%s%s AS ", sep, te->resname);
+ appendStringInfo(buf, "%s%s AS ", sep, quote_identifier(te->resname));
get_rule_expr((Node *) te->expr, context, false);
sep = ",\n ";
}
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index 7452cf1b3ab..6526365dd6a 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -2252,6 +2252,30 @@ SELECT pg_get_viewdef('rpr_quant_reluctant_v'::regclass);
b AS (val > 0) );
(1 row)
+-- Quoted identifier round-trip: mixed case and reserved words need quoting
+CREATE VIEW rpr_serial_quoted AS
+SELECT id, val, count(*) OVER w
+FROM rpr_serial
+WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ("Start" "Up"+)
+ DEFINE "Start" AS TRUE, "Up" AS val > PREV(val));
+SELECT pg_get_viewdef('rpr_serial_quoted'::regclass);
+ pg_get_viewdef
+------------------------------------------------------------------------------
+ SELECT id, +
+ val, +
+ count(*) OVER w AS count +
+ FROM rpr_serial +
+ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
+ AFTER MATCH SKIP PAST LAST ROW +
+ INITIAL +
+ PATTERN ("Start" "Up"+) +
+ DEFINE +
+ "Start" AS true, +
+ "Up" AS (val > prev(val)) );
+(1 row)
+
-- Materialized view (if supported)
CREATE TABLE rpr_mview (id INT, val INT);
INSERT INTO rpr_mview VALUES (1, 10), (2, 20), (3, 30);
diff --git a/src/test/regress/expected/rpr_explain.out b/src/test/regress/expected/rpr_explain.out
index f66caf8908e..a68ec61e10f 100644
--- a/src/test/regress/expected/rpr_explain.out
+++ b/src/test/regress/expected/rpr_explain.out
@@ -301,6 +301,25 @@ WINDOW w AS (
-> Function Scan on generate_series s (actual rows=30.00 loops=1)
(8 rows)
+-- Regression test: Quoted identifiers in EXPLAIN pattern deparse
+-- Mixed case names must be quoted to preserve round-trip safety
+SELECT rpr_explain_filter('
+EXPLAIN (COSTS OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 10) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ("Start" "Up"+)
+ DEFINE "Start" AS TRUE, "Up" AS v > PREV(v)
+);');
+ rpr_explain_filter
+-------------------------------------------------------------------
+ WindowAgg
+ Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+ Pattern: "Start" "Up"+
+ -> Function Scan on generate_series s
+(4 rows)
+
-- ============================================================
-- State Statistics Tests (peak, total, merged)
-- ============================================================
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index 8c23c7598a3..3accecb73ba 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -1559,6 +1559,16 @@ WINDOW w AS (ORDER BY id
DEFINE A AS val > 0, B AS val > 0);
SELECT pg_get_viewdef('rpr_quant_reluctant_v'::regclass);
+-- Quoted identifier round-trip: mixed case and reserved words need quoting
+CREATE VIEW rpr_serial_quoted AS
+SELECT id, val, count(*) OVER w
+FROM rpr_serial
+WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ("Start" "Up"+)
+ DEFINE "Start" AS TRUE, "Up" AS val > PREV(val));
+SELECT pg_get_viewdef('rpr_serial_quoted'::regclass);
+
-- Materialized view (if supported)
CREATE TABLE rpr_mview (id INT, val INT);
diff --git a/src/test/regress/sql/rpr_explain.sql b/src/test/regress/sql/rpr_explain.sql
index 65a775fdad9..703ecd3b23b 100644
--- a/src/test/regress/sql/rpr_explain.sql
+++ b/src/test/regress/sql/rpr_explain.sql
@@ -226,6 +226,18 @@ WINDOW w AS (
DEFINE A AS v % 5 = 1, B AS v % 5 = 2, C AS v % 5 = 3, D AS v % 5 = 4, E AS v % 5 = 0
);');
+-- Regression test: Quoted identifiers in EXPLAIN pattern deparse
+-- Mixed case names must be quoted to preserve round-trip safety
+SELECT rpr_explain_filter('
+EXPLAIN (COSTS OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 10) AS s(v)
+WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN ("Start" "Up"+)
+ DEFINE "Start" AS TRUE, "Up" AS v > PREV(v)
+);');
+
-- ============================================================
-- State Statistics Tests (peak, total, merged)
-- ============================================================
--
2.50.1 (Apple Git-155)
[text/plain] nocfbot-0012-Fix-execRPR-Makefile-ordering.txt (723B, 14-nocfbot-0012-Fix-execRPR-Makefile-ordering.txt)
download | inline diff:
From b46e0efc2dac822d7b68734345cd9e051e8aa235 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Tue, 7 Apr 2026 09:14:59 +0900
Subject: [PATCH] Fix execRPR.o ordering in executor Makefile to match
meson.build
---
src/backend/executor/Makefile | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/backend/executor/Makefile b/src/backend/executor/Makefile
index eeed9a904e5..2b257427795 100644
--- a/src/backend/executor/Makefile
+++ b/src/backend/executor/Makefile
@@ -25,8 +25,8 @@ OBJS = \
execParallel.o \
execPartition.o \
execProcnode.o \
- execReplication.o \
execRPR.o \
+ execReplication.o \
execSRF.o \
execScan.o \
execTuples.o \
--
2.50.1 (Apple Git-155)
[text/plain] nocfbot-0013-Remove-unused-force_colno.txt (2.7K, 15-nocfbot-0013-Remove-unused-force_colno.txt)
download | inline diff:
From 4d64cb3907e95d3ffe61d206040b65207b31950d Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Tue, 7 Apr 2026 09:40:53 +0900
Subject: [PATCH] Remove unused force_colno parameter from RPR deparse
functions
---
src/backend/utils/adt/ruleutils.c | 15 ++++++---------
1 file changed, 6 insertions(+), 9 deletions(-)
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index c755a42efd6..e93c03a351c 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -449,10 +449,8 @@ static void get_rule_orderby(List *orderList, List *targetList,
bool force_colno, deparse_context *context);
static void append_pattern_quantifier(StringInfo buf, RPRPatternNode *node);
static void get_rule_pattern_node(RPRPatternNode *node, deparse_context *context);
-static void get_rule_pattern(RPRPatternNode *rpPattern, bool force_colno,
- deparse_context *context);
-static void get_rule_define(List *defineClause, bool force_colno,
- deparse_context *context);
+static void get_rule_pattern(RPRPatternNode *rpPattern, deparse_context *context);
+static void get_rule_define(List *defineClause, deparse_context *context);
static void get_rule_windowclause(Query *query, deparse_context *context);
static void get_rule_windowspec(WindowClause *wc, List *targetList,
deparse_context *context);
@@ -7203,8 +7201,7 @@ get_rule_pattern_node(RPRPatternNode *node, deparse_context *context)
* Display a PATTERN clause.
*/
static void
-get_rule_pattern(RPRPatternNode *rpPattern, bool force_colno,
- deparse_context *context)
+get_rule_pattern(RPRPatternNode *rpPattern, deparse_context *context)
{
StringInfo buf = context->buf;
@@ -7217,7 +7214,7 @@ get_rule_pattern(RPRPatternNode *rpPattern, bool force_colno,
* Display a DEFINE clause.
*/
static void
-get_rule_define(List *defineClause, bool force_colno, deparse_context *context)
+get_rule_define(List *defineClause, deparse_context *context)
{
StringInfo buf = context->buf;
const char *sep;
@@ -7356,7 +7353,7 @@ get_rule_windowspec(WindowClause *wc, List *targetList,
if (needspace)
appendStringInfoChar(buf, ' ');
appendStringInfoString(buf, "\n PATTERN ");
- get_rule_pattern(wc->rpPattern, false, context);
+ get_rule_pattern(wc->rpPattern, context);
needspace = true;
}
@@ -7365,7 +7362,7 @@ get_rule_windowspec(WindowClause *wc, List *targetList,
if (needspace)
appendStringInfoChar(buf, ' ');
appendStringInfoString(buf, "\n DEFINE\n");
- get_rule_define(wc->defineClause, false, context);
+ get_rule_define(wc->defineClause, context);
appendStringInfoChar(buf, ' ');
}
--
2.50.1 (Apple Git-155)
[text/plain] nocfbot-0014-CHECK_FOR_INTERRUPTS-cleanup-finalize.txt (1.0K, 16-nocfbot-0014-CHECK_FOR_INTERRUPTS-cleanup-finalize.txt)
download | inline diff:
From 4f779ed3658d7b4d97819b5cca39d6a1d555d67b Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Tue, 7 Apr 2026 09:43:07 +0900
Subject: [PATCH] Add CHECK_FOR_INTERRUPTS to RPR context cleanup and finalize
loops
---
src/backend/executor/execRPR.c | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 8f0457e2b3c..97efa9a4924 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -3068,6 +3068,8 @@ ExecRPRCleanupDeadContexts(WindowAggState *winstate, RPRNFAContext *excludeCtx)
for (ctx = winstate->nfaContext; ctx != NULL; ctx = next)
{
+ CHECK_FOR_INTERRUPTS();
+
next = ctx->next;
/* Skip the target context and contexts still processing */
@@ -3108,6 +3110,8 @@ ExecRPRFinalizeAllContexts(WindowAggState *winstate, int64 lastPos)
for (ctx = winstate->nfaContext; ctx != NULL; ctx = ctx->next)
{
+ CHECK_FOR_INTERRUPTS();
+
if (ctx->states != NULL)
{
nfa_match(winstate, ctx, NULL);
--
2.50.1 (Apple Git-155)
[text/plain] nocfbot-0015-Narrow-variable-scope-DEFINE-loop.txt (1.3K, 17-nocfbot-0015-Narrow-variable-scope-DEFINE-loop.txt)
download | inline diff:
From 366068f89fd0385c3231b2f1bd7d7e75f9641718 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Tue, 7 Apr 2026 09:48:55 +0900
Subject: [PATCH] Narrow variable scope in ExecInitWindowAgg DEFINE clause loop
---
src/backend/executor/nodeWindowAgg.c | 11 +++--------
1 file changed, 3 insertions(+), 8 deletions(-)
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index dca2de570e8..0202c508323 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -2628,9 +2628,6 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
TupleDesc scanDesc;
ListCell *l;
- TargetEntry *te;
- Expr *expr;
-
/* check for unsupported flags */
Assert(!(eflags & (EXEC_FLAG_BACKWARD | EXEC_FLAG_MARK)));
@@ -2951,13 +2948,11 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
*/
foreach(l, node->defineClause)
{
- char *name;
+ TargetEntry *te = lfirst(l);
+ char *name = te->resname;
+ Expr *expr = te->expr;
ExprState *exps;
- te = lfirst(l);
- name = te->resname;
- expr = te->expr;
-
winstate->defineVariableList =
lappend(winstate->defineVariableList,
makeString(pstrdup(name)));
--
2.50.1 (Apple Git-155)
[text/plain] nocfbot-0016-Normalize-flag-macros-to-bool.txt (1.4K, 18-nocfbot-0016-Normalize-flag-macros-to-bool.txt)
download | inline diff:
From d4454b9a6b576a79adca7f2ff96abd0b28db3a86 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Tue, 7 Apr 2026 11:32:56 +0900
Subject: [PATCH] Normalize RPR element flag macros to return bool
---
src/include/optimizer/rpr.h | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/include/optimizer/rpr.h b/src/include/optimizer/rpr.h
index e78092678bb..360e1bb777f 100644
--- a/src/include/optimizer/rpr.h
+++ b/src/include/optimizer/rpr.h
@@ -44,10 +44,10 @@
#define RPR_ELEM_ABSORBABLE 0x08 /* absorption judgment point */
/* Accessor macros for RPRPatternElement */
-#define RPRElemIsReluctant(e) ((e)->flags & RPR_ELEM_RELUCTANT)
-#define RPRElemCanEmptyLoop(e) ((e)->flags & RPR_ELEM_EMPTY_LOOP)
-#define RPRElemIsAbsorbableBranch(e) ((e)->flags & RPR_ELEM_ABSORBABLE_BRANCH)
-#define RPRElemIsAbsorbable(e) ((e)->flags & RPR_ELEM_ABSORBABLE)
+#define RPRElemIsReluctant(e) (((e)->flags & RPR_ELEM_RELUCTANT) != 0)
+#define RPRElemCanEmptyLoop(e) (((e)->flags & RPR_ELEM_EMPTY_LOOP) != 0)
+#define RPRElemIsAbsorbableBranch(e) (((e)->flags & RPR_ELEM_ABSORBABLE_BRANCH) != 0)
+#define RPRElemIsAbsorbable(e) (((e)->flags & RPR_ELEM_ABSORBABLE) != 0)
#define RPRElemIsVar(e) ((e)->varId <= RPR_VARID_MAX)
#define RPRElemIsBegin(e) ((e)->varId == RPR_VARID_BEGIN)
#define RPRElemIsEnd(e) ((e)->varId == RPR_VARID_END)
--
2.50.1 (Apple Git-155)
[text/plain] nocfbot-0017-Implement-1-slot-PREV-NEXT-navigation-for-RPR.txt (82.7K, 19-nocfbot-0017-Implement-1-slot-PREV-NEXT-navigation-for-RPR.txt)
download
[text/plain] nocfbot-0018-JIT-support-for-PREV-NEXT.txt (7.0K, 20-nocfbot-0018-JIT-support-for-PREV-NEXT.txt)
download | inline diff:
From 49555669b00a11ec6b106026f075b03825f6bc3f Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Thu, 2 Apr 2026 10:54:30 +0900
Subject: [PATCH] Add JIT compilation support for RPR PREV/NEXT navigation
---
src/backend/jit/llvm/llvmjit_expr.c | 72 +++++++++++++++++++++--------
src/test/regress/expected/rpr.out | 31 +++++++++++++
src/test/regress/sql/rpr.sql | 27 +++++++++++
3 files changed, 111 insertions(+), 19 deletions(-)
diff --git a/src/backend/jit/llvm/llvmjit_expr.c b/src/backend/jit/llvm/llvmjit_expr.c
index d158e37e7b5..4901b2a7ff4 100644
--- a/src/backend/jit/llvm/llvmjit_expr.c
+++ b/src/backend/jit/llvm/llvmjit_expr.c
@@ -127,6 +127,9 @@ llvm_compile_expr(ExprState *state)
LLVMValueRef v_aggvalues;
LLVMValueRef v_aggnulls;
+ /* RPR navigation: when true, EEOP_OUTER_VAR reloads from econtext */
+ bool has_rpr_nav;
+
instr_time starttime;
instr_time deform_starttime;
instr_time endtime;
@@ -300,19 +303,16 @@ llvm_compile_expr(ExprState *state)
* 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.
+ * After a slot swap, these cached pointers become stale because the new
+ * slot has its own tts_values/tts_isnull arrays.
*
- * 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.
+ * When RPR navigation opcodes are present, EEOP_OUTER_VAR reloads the
+ * slot pointer from econtext->ecxt_outertuple on every access instead of
+ * using the cached entry-block values. This avoids the SSA/PHI
+ * complexity while keeping the rest of the expression JIT-compiled.
+ * Expressions without RPR navigation use the cached values as before.
*/
+ has_rpr_nav = false;
if (parent && IsA(parent, WindowAggState) &&
((WindowAgg *) parent->plan)->rpPattern != NULL)
{
@@ -323,9 +323,8 @@ llvm_compile_expr(ExprState *state)
if (opcode == EEOP_RPR_NAV_SET ||
opcode == EEOP_RPR_NAV_RESTORE)
{
- LLVMDeleteFunction(eval_fn);
- LLVMDisposeBuilder(b);
- return false;
+ has_rpr_nav = true;
+ break;
}
}
}
@@ -492,8 +491,37 @@ llvm_compile_expr(ExprState *state)
}
else if (opcode == EEOP_OUTER_VAR)
{
- v_values = v_outervalues;
- v_nulls = v_outernulls;
+ if (has_rpr_nav)
+ {
+ /*
+ * RPR navigation swaps ecxt_outertuple
+ * mid-expression. Reload slot pointer from
+ * econtext on every access so we read from the
+ * current (possibly swapped) slot.
+ */
+ LLVMValueRef v_tmpslot;
+
+ v_tmpslot = l_load_struct_gep(b,
+ StructExprContext,
+ v_econtext,
+ FIELDNO_EXPRCONTEXT_OUTERTUPLE,
+ "v_outerslot_reload");
+ v_values = l_load_struct_gep(b,
+ StructTupleTableSlot,
+ v_tmpslot,
+ FIELDNO_TUPLETABLESLOT_VALUES,
+ "v_outervalues_reload");
+ v_nulls = l_load_struct_gep(b,
+ StructTupleTableSlot,
+ v_tmpslot,
+ FIELDNO_TUPLETABLESLOT_ISNULL,
+ "v_outernulls_reload");
+ }
+ else
+ {
+ v_values = v_outervalues;
+ v_nulls = v_outernulls;
+ }
}
else if (opcode == EEOP_SCAN_VAR)
{
@@ -2467,10 +2495,16 @@ llvm_compile_expr(ExprState *state)
break;
case EEOP_RPR_NAV_SET:
+ build_EvalXFunc(b, mod, "ExecEvalRPRNavSet",
+ v_state, op, v_econtext);
+ LLVMBuildBr(b, opblocks[opno + 1]);
+ break;
+
case EEOP_RPR_NAV_RESTORE:
- /* unreachable: filtered out by the pre-scan above */
- Assert(false);
- return false;
+ build_EvalXFunc(b, mod, "ExecEvalRPRNavRestore",
+ v_state, op, v_econtext);
+ LLVMBuildBr(b, opblocks[opno + 1]);
+ break;
case EEOP_AGG_STRICT_DESERIALIZE:
case EEOP_AGG_DESERIALIZE:
diff --git a/src/test/regress/expected/rpr.out b/src/test/regress/expected/rpr.out
index d586e17e0a1..de6ce4fba8a 100644
--- a/src/test/regress/expected/rpr.out
+++ b/src/test/regress/expected/rpr.out
@@ -2153,6 +2153,37 @@ SELECT match_first, match_last, match_len FROM result WHERE match_len > 0;
0 | 99998 | 99999
(1 row)
+-- JIT PREV/NEXT navigation test: 100K rows with PREV in DEFINE.
+-- Exercises EEOP_RPR_NAV_SET/RESTORE JIT code paths (has_rpr_nav reload)
+-- at scale. V-shape: price rises then falls, repeated across partition.
+SET jit_above_cost = 0;
+WITH data AS (
+ SELECT i, abs(50000 - i) AS price
+ FROM generate_series(1, 100000) i
+),
+result AS (
+ SELECT i, price,
+ count(*) OVER w AS match_len,
+ first_value(price) OVER w AS match_first
+ FROM data
+ WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ INITIAL
+ PATTERN (DOWN+ UP+)
+ DEFINE
+ DOWN AS price < PREV(price),
+ UP AS price > PREV(price)
+ )
+)
+SELECT count(*) AS matched_rows, max(match_len) AS longest_match
+FROM result WHERE match_len > 0;
+ matched_rows | longest_match
+--------------+---------------
+ 1 | 99999
+(1 row)
+
+RESET jit_above_cost;
--
-- IGNORE NULLS
--
diff --git a/src/test/regress/sql/rpr.sql b/src/test/regress/sql/rpr.sql
index 504476a2b02..b3bbc8254c4 100644
--- a/src/test/regress/sql/rpr.sql
+++ b/src/test/regress/sql/rpr.sql
@@ -1084,6 +1084,33 @@ result AS (
-- Should match: A (33333 rows) + B (33333 rows) + C (33333 rows) = 99999 rows
SELECT match_first, match_last, match_len FROM result WHERE match_len > 0;
+-- JIT PREV/NEXT navigation test: 100K rows with PREV in DEFINE.
+-- Exercises EEOP_RPR_NAV_SET/RESTORE JIT code paths (has_rpr_nav reload)
+-- at scale. V-shape: price rises then falls, repeated across partition.
+SET jit_above_cost = 0;
+WITH data AS (
+ SELECT i, abs(50000 - i) AS price
+ FROM generate_series(1, 100000) i
+),
+result AS (
+ SELECT i, price,
+ count(*) OVER w AS match_len,
+ first_value(price) OVER w AS match_first
+ FROM data
+ WINDOW w AS (
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ INITIAL
+ PATTERN (DOWN+ UP+)
+ DEFINE
+ DOWN AS price < PREV(price),
+ UP AS price > PREV(price)
+ )
+)
+SELECT count(*) AS matched_rows, max(match_len) AS longest_match
+FROM result WHERE match_len > 0;
+RESET jit_above_cost;
+
--
-- IGNORE NULLS
--
--
2.50.1 (Apple Git-155)
[text/plain] nocfbot-0019-Add-tuplestore-trim-optimization-for-RPR-PREV.txt (12.3K, 21-nocfbot-0019-Add-tuplestore-trim-optimization-for-RPR-PREV.txt)
download | inline diff:
From 0b71c6b947e6b279bff33d34c2e263833f9f5408 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Sat, 4 Apr 2026 11:49:32 +0900
Subject: [PATCH] Add tuplestore trim optimization for RPR PREV navigation
Advance the tuplestore mark pointer based on the maximum PREV offset
found in DEFINE clause expressions, allowing tuplestore_trim() to
free rows that PREV can no longer reach.
The planner walks DEFINE expressions to find the maximum PREV offset.
If all offsets are constants, navMaxOffset is set directly. If any
offset is non-constant (parameter or expression), the planner sets
RPR_NAV_OFFSET_NEEDS_EVAL and the executor evaluates all PREV offsets
at init time. The executor then advances the mark to
(currentpos - navMaxOffset) each row.
NEXT offsets are ignored since they look forward and do not affect
trim. RPR_NAV_OFFSET_RETAIN_ALL is reserved for future navigation
functions (FIRST/LAST) that require the entire partition.
---
src/backend/executor/nodeWindowAgg.c | 136 ++++++++++++++++++++++--
src/backend/optimizer/plan/createplan.c | 101 ++++++++++++++++++
src/include/nodes/execnodes.h | 1 +
src/include/nodes/plannodes.h | 9 ++
src/include/optimizer/rpr.h | 9 ++
5 files changed, 246 insertions(+), 10 deletions(-)
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 4e643df94cf..9787ef7756f 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -244,6 +244,10 @@ static void update_reduced_frame(WindowObject winobj, int64 pos);
/* Forward declarations - NFA row evaluation */
static bool nfa_evaluate_row(WindowObject winobj, int64 pos, bool *varMatched);
+/* Forward declarations - navigation offset evaluation */
+static bool collect_prev_offset_walker(Node *node, List **offsets);
+static void eval_nav_max_offset(WindowAggState *winstate, List *defineClause);
+
/*
* Not null info bit array consists of 2-bit items
*/
@@ -934,12 +938,18 @@ eval_windowaggregates(WindowAggState *winstate)
if (rpr_is_defined(winstate))
{
/*
- * If RPR is used, it is possible PREV wants to look at the
- * previous row. So the mark pos should be frameheadpos - 1
- * unless it is below 0.
+ * If RPR is used, PREV may need to look at rows before the frame
+ * head. Adjust mark by navMaxOffset if known, otherwise retain
+ * from position 0.
*/
- markpos -= 1;
- if (markpos < 0)
+ if (winstate->navMaxOffset >= 0)
+ {
+ if (markpos > winstate->navMaxOffset)
+ markpos -= winstate->navMaxOffset;
+ else
+ markpos = 0;
+ }
+ else
markpos = 0;
}
WinSetMarkPosition(agg_winobj, markpos);
@@ -1269,12 +1279,15 @@ prepare_tuplestore(WindowAggState *winstate)
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.
+ * Allocate mark and read pointers for PREV/NEXT navigation.
+ *
+ * If navMaxOffset >= 0, we advance the mark to (currentpos -
+ * navMaxOffset) as rows are processed, allowing tuplestore_trim() to
+ * free rows that are no longer reachable.
*
- * 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.
+ * If navMaxOffset < 0 (RPR_NAV_OFFSET_NEEDS_EVAL or
+ * RPR_NAV_OFFSET_RETAIN_ALL), the mark stays at 0, retaining the
+ * entire partition in the tuplestore.
*/
winstate->nav_winobj->markptr =
tuplestore_alloc_read_pointer(winstate->buffer, 0);
@@ -2512,6 +2525,24 @@ ExecWindowAgg(PlanState *pstate)
if (winstate->grouptail_ptr >= 0)
update_grouptailpos(winstate);
+ /*
+ * Advance RPR navigation mark pointer if possible, so that
+ * tuplestore_trim() can free rows no longer reachable by PREV.
+ */
+ if (winstate->nav_winobj &&
+ winstate->rpPattern != NULL &&
+ winstate->navMaxOffset >= 0)
+ {
+ int64 navmarkpos;
+
+ if (winstate->currentpos > winstate->navMaxOffset)
+ navmarkpos = winstate->currentpos - winstate->navMaxOffset;
+ else
+ navmarkpos = 0;
+ if (navmarkpos > winstate->nav_winobj->markpos)
+ WinSetMarkPosition(winstate->nav_winobj, navmarkpos);
+ }
+
/*
* Truncate any no-longer-needed rows from the tuplestore.
*/
@@ -2957,6 +2988,10 @@ 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 max PREV offset for tuplestore trim */
+ winstate->navMaxOffset = node->navMaxOffset;
+ if (winstate->navMaxOffset == RPR_NAV_OFFSET_NEEDS_EVAL)
+ eval_nav_max_offset(winstate, node->defineClause);
/* Calculate NFA state size and allocate cycle detection bitmap */
if (node->rpPattern != NULL)
@@ -3867,6 +3902,87 @@ put_notnull_info(WindowObject winobj, int64 pos, int argno, bool isnull)
mbp[bpos] = mb;
}
+/*
+ * collect_prev_offset_walker
+ * Walk expression tree to collect PREV offset_arg expressions.
+ */
+static bool
+collect_prev_offset_walker(Node *node, List **offsets)
+{
+ if (node == NULL)
+ return false;
+
+ if (IsA(node, RPRNavExpr))
+ {
+ RPRNavExpr *nav = (RPRNavExpr *) node;
+
+ if (nav->kind == RPR_NAV_PREV && nav->offset_arg != NULL)
+ *offsets = lappend(*offsets, nav->offset_arg);
+
+ /* Don't walk into RPRNavExpr children */
+ return false;
+ }
+
+ return expression_tree_walker(node, collect_prev_offset_walker, offsets);
+}
+
+/*
+ * eval_nav_max_offset
+ * Evaluate non-constant PREV offsets at executor init time.
+ *
+ * Called when the planner set navMaxOffset to RPR_NAV_OFFSET_NEEDS_EVAL
+ * because some PREV offset contains a parameter or non-foldable expression.
+ * Walks the original defineClause expression trees, compiles and evaluates
+ * each PREV offset_arg, and stores the maximum in winstate->navMaxOffset.
+ */
+static void
+eval_nav_max_offset(WindowAggState *winstate, List *defineClause)
+{
+ ExprContext *econtext = winstate->ss.ps.ps_ExprContext;
+ List *offsets = NIL;
+ ListCell *lc;
+ int64 maxOffset = 0;
+
+ /* Collect all PREV offset expressions from DEFINE clause */
+ foreach(lc, defineClause)
+ {
+ TargetEntry *te = (TargetEntry *) lfirst(lc);
+
+ collect_prev_offset_walker((Node *) te->expr, &offsets);
+ }
+
+ /* Evaluate each offset and find maximum */
+ foreach(lc, offsets)
+ {
+ Expr *offset_expr = (Expr *) lfirst(lc);
+ ExprState *estate;
+ Datum val;
+ bool isnull;
+ int64 offset;
+
+ estate = ExecInitExpr(offset_expr, (PlanState *) winstate);
+ val = ExecEvalExprSwitchContext(estate, econtext, &isnull);
+
+ /*
+ * NULL or negative offsets will cause a runtime error when PREV is
+ * actually evaluated. For trim purposes, treat them as 0.
+ */
+ if (isnull)
+ continue;
+
+ offset = DatumGetInt64(val);
+ if (offset < 0)
+ continue;
+
+ if (offset > maxOffset)
+ maxOffset = offset;
+ }
+
+ winstate->navMaxOffset = maxOffset;
+
+ list_free(offsets);
+}
+
/*
* rpr_is_defined
* return true if Row pattern recognition is defined.
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index 9ac24cc222d..ee2d53b5924 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -2461,6 +2461,104 @@ create_minmaxagg_plan(PlannerInfo *root, MinMaxAggPath *best_path)
return plan;
}
+/*
+ * nav_max_offset_walker
+ * Walk expression tree to find the maximum PREV offset.
+ *
+ * Only PREV is relevant for tuplestore trim since it looks backward;
+ * NEXT looks forward and never references already-trimmed rows.
+ *
+ * Returns true (to stop walking) if a non-constant PREV offset is found,
+ * in which case *maxOffset is set to -1. Otherwise accumulates the
+ * maximum constant offset value.
+ */
+static bool
+nav_max_offset_walker(Node *node, int64 *maxOffset)
+{
+ if (node == NULL)
+ return false;
+
+ if (IsA(node, RPRNavExpr))
+ {
+ RPRNavExpr *nav = (RPRNavExpr *) node;
+
+ /* Only PREV looks backward; NEXT is irrelevant for trim */
+ if (nav->kind == RPR_NAV_PREV)
+ {
+ int64 offset;
+
+ if (nav->offset_arg == NULL)
+ {
+ /* 1-arg form: implicit offset of 1 */
+ offset = 1;
+ }
+ else if (IsA(nav->offset_arg, Const))
+ {
+ Const *c = (Const *) nav->offset_arg;
+
+ if (c->constisnull)
+ {
+ /*
+ * NULL offset causes a runtime error, so this path is
+ * never actually reached during execution. Use 0 as a
+ * safe placeholder for planning purposes.
+ */
+ offset = 0;
+ }
+ else
+ {
+ offset = DatumGetInt64(c->constvalue);
+ if (offset < 0)
+ offset = 0; /* negative offset causes runtime error */
+ }
+ }
+ else
+ {
+ /*
+ * Non-constant offset (Param, stable function, etc.). The
+ * parser guarantees offset is a runtime constant, so it can
+ * be evaluated at executor init time.
+ */
+ *maxOffset = RPR_NAV_OFFSET_NEEDS_EVAL;
+ return true; /* stop walking */
+ }
+
+ if (offset > *maxOffset)
+ *maxOffset = offset;
+ }
+
+ /* Don't walk into RPRNavExpr children - offset_arg already handled */
+ return false;
+ }
+
+ return expression_tree_walker(node, nav_max_offset_walker, maxOffset);
+}
+
+/*
+ * compute_nav_max_offset
+ * Compute the maximum PREV offset from DEFINE clause expressions.
+ *
+ * Returns the maximum constant offset found, or -1 if any PREV offset
+ * cannot be determined statically. NEXT offsets are ignored since they
+ * look forward and don't affect tuplestore trim.
+ */
+static int64
+compute_nav_max_offset(List *defineClause)
+{
+ int64 maxOffset = 0;
+ ListCell *lc;
+
+ foreach(lc, defineClause)
+ {
+ TargetEntry *te = (TargetEntry *) lfirst(lc);
+
+ if (nav_max_offset_walker((Node *) te->expr, &maxOffset))
+ return RPR_NAV_OFFSET_NEEDS_EVAL;
+ }
+
+ return maxOffset;
+}
+
/*
* create_windowagg_plan
*
@@ -6678,6 +6776,9 @@ make_windowagg(List *tlist, WindowClause *wc,
node->defineClause = defineClause;
+ /* Compute max PREV offset for tuplestore trim optimization */
+ node->navMaxOffset = compute_nav_max_offset(defineClause);
+
plan->targetlist = tlist;
plan->lefttree = lefttree;
plan->righttree = NULL;
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 74a6b682132..ff6d7d70a60 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -2692,6 +2692,7 @@ typedef struct WindowAggState
TupleTableSlot *temp_slot_2;
/* RPR navigation */
+ int64 navMaxOffset; /* max PREV offset; see RPR_NAV_OFFSET_* */
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 */
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index ceaab4d97b0..27a2e7b48c7 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -1386,6 +1386,15 @@ typedef struct WindowAgg
/* Row Pattern DEFINE clause (list of TargetEntry) */
List *defineClause;
+ /*
+ * Maximum PREV offset for tuplestore mark optimization. >= 0: statically
+ * determined max offset (mark = currentpos - offset).
+ * RPR_NAV_OFFSET_NEEDS_EVAL: has non-constant offset; evaluate at
+ * executor init. RPR_NAV_OFFSET_RETAIN_ALL: must retain entire partition
+ * (no trim possible).
+ */
+ int64 navMaxOffset;
+
/*
* false for all apart from the WindowAgg that's closest to the root of
* the plan
diff --git a/src/include/optimizer/rpr.h b/src/include/optimizer/rpr.h
index 360e1bb777f..00a28abe2b4 100644
--- a/src/include/optimizer/rpr.h
+++ b/src/include/optimizer/rpr.h
@@ -55,6 +55,15 @@
#define RPRElemIsFin(e) ((e)->varId == RPR_VARID_FIN)
#define RPRElemCanSkip(e) ((e)->min == 0)
+/*
+ * navMaxOffset sentinel values.
+ * Non-negative values represent a statically determined maximum PREV offset.
+ */
+#define RPR_NAV_OFFSET_NEEDS_EVAL (-1) /* has non-constant PREV offset;
+ * evaluate at executor init */
+#define RPR_NAV_OFFSET_RETAIN_ALL (-2) /* must retain entire partition
+ * (e.g., future FIRST/LAST) */
+
extern List *collectPatternVariables(RPRPatternNode *pattern);
extern void buildDefineVariableList(List *defineClause,
List **defineVariableList);
--
2.50.1 (Apple Git-155)
[text/plain] nocfbot-0020-Update-RPR-code-comments-for-1-slot-navigation.txt (5.5K, 22-nocfbot-0020-Update-RPR-code-comments-for-1-slot-navigation.txt)
download | inline diff:
From 08c98ae40028559e6047cdbd4f20f2388ccab86c Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Thu, 2 Apr 2026 16:06:06 +0900
Subject: [PATCH] Update RPR code comments to reflect 1-slot navigation model
---
src/backend/executor/execRPR.c | 45 ++++++++++++++++++++++------------
src/backend/parser/parse_rpr.c | 3 ++-
2 files changed, 32 insertions(+), 16 deletions(-)
diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 97efa9a4924..995acdd7be5 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -191,10 +191,15 @@
* transformDefineClause() processes each DEFINE variable as follows:
*
* (1) Checks for duplicate variable names
- * (2) Transforms the expression into a standard SQL expression
- * (3) Coerces to Boolean type (coerce_to_boolean)
+ * (2) Transforms the expression via transformExpr()
+ * (3) Extracts Var nodes via pull_var_clause() and ensures each is
+ * present in the query targetlist, so the planner propagates the
+ * referenced columns through the plan tree
* (4) Wraps in a TargetEntry with the variable name set in resname
*
+ * After all variables are processed:
+ * (5) Coerces each expression to Boolean type (coerce_to_boolean)
+ *
* Variables that are used in PATTERN but not defined in DEFINE are implicitly
* evaluated as TRUE (matching all rows).
*
@@ -431,8 +436,9 @@
*
* Case 1: Simple VAR+ (e.g., A+)
* -> ABSORBABLE | ABSORBABLE_BRANCH set on the VAR
- * Case 2: GROUP+ whose body consists only of {1,1} VARs (e.g., (A B)+)
- * -> ABSORBABLE_BRANCH on children,
+ * Case 2: GROUP+ with fixed-length children (min == max, recursively)
+ * e.g., (A B)+, (A B{2})+, ((A (B C){2}){2})+
+ * -> ABSORBABLE_BRANCH on all elements within the group,
* ABSORBABLE | ABSORBABLE_BRANCH on END
* Case 3: GROUP+ whose body starts with VAR+ (e.g., (A+ B)+)
* -> Recurses from BEGIN into the body, applying Case 1.
@@ -604,11 +610,17 @@
* varMatched[i] = (not null and true)
*
* To support row navigation operators such as PREV() and NEXT(),
- * the previous row, current row, and next row are set in separate slots:
+ * a 1-slot model is used: only ecxt_outertuple is set to the current
+ * row. PREV/NEXT navigation is handled by EEOP_RPR_NAV_SET/RESTORE
+ * opcodes emitted during DEFINE expression compilation:
+ *
+ * NAV_SET: save ecxt_outertuple, swap in target row via nav_slot
+ * (evaluate): argument expression reads from swapped slot
+ * NAV_RESTORE: restore original ecxt_outertuple
*
- * ecxt_scantuple = previous row (for PREV reference)
- * ecxt_outertuple = current row (default reference)
- * ecxt_innertuple = next row (for NEXT reference)
+ * nav_slot caches the last fetched position (nav_slot_pos) to avoid
+ * redundant tuplestore lookups when multiple PREV/NEXT calls target
+ * the same row.
*
* The varMatched array is referenced later in Phase 1 (Match).
*
@@ -908,8 +920,11 @@
*
* (2) At runtime: initialize the nfaVisitedElems bitmap immediately before
* DFS expansion of each state within advance (once per state).
- * During DFS, set the corresponding elemIdx bit when visiting each
- * element.
+ * During DFS, epsilon elements (END, ALT, BEGIN) are marked in the
+ * bitmap at nfa_advance_state entry. VAR elements are marked later
+ * when added to the state list (nfa_add_state_unique), so that
+ * legitimate loop-back to the same VAR in a new group iteration
+ * (e.g., END -> ALT -> same VAR) is not blocked.
* If a previously visited elemIdx is revisited, that path is terminated.
*
* Note: the bitmap tracks only elemIdx and does not consider counts.
@@ -1216,11 +1231,11 @@
* (3) State Deduplication (IX-5)
*
* During advance, DFS may generate states with the same (elemIdx,
- * counts) combination through multiple paths. Additionally, unlike
- * VAR repetition, group repetition cannot perform absorption
- * comparison using VAR states, so inline advance is performed from
- * after Phase 1 match through to END; this process can also produce
- * duplicate states reaching the same END.
+ * counts) combination through multiple paths. Additionally, for
+ * group absorption, nfa_match performs inline advance from bounded
+ * VARs (count >= max) within the absorbable region (ABSORBABLE_BRANCH)
+ * through END chains to reach the judgment point (ABSORBABLE END).
+ * This process can also produce duplicate states reaching the same END.
* nfa_add_state_unique() blocks duplicate addition of identical states
* in both cases.
*
diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index 3fb5d94abe9..d1e02e52e53 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -265,7 +265,8 @@ validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
*
* Then for each DEFINE variable:
* 2. Checks for duplicate variable names in DEFINE clause
- * 3. Transforms expressions and adds to targetlist via findTargetlistEntrySQL99
+ * 3. Transforms expression via transformExpr() and ensures referenced
+ * Var nodes are present in the query targetlist (via pull_var_clause)
* 4. Creates defineClause entry with proper resname (pattern variable name)
* 5. Coerces expressions to boolean type
* 6. Marks column origins and assigns collation information
--
2.50.1 (Apple Git-155)
[text/plain] nocfbot-0021-Enable-JIT-PREV-NEXT-tests.txt (1.8K, 23-nocfbot-0021-Enable-JIT-PREV-NEXT-tests.txt)
download | inline diff:
From 74883a5f7ec23d20f0352d59595839099b141f89 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Mon, 6 Apr 2026 09:48:54 +0900
Subject: [PATCH] Enable JIT compilation for PREV/NEXT navigation tests in RPR
---
src/test/regress/expected/rpr.out | 2 ++
src/test/regress/sql/rpr.sql | 2 ++
2 files changed, 4 insertions(+)
diff --git a/src/test/regress/expected/rpr.out b/src/test/regress/expected/rpr.out
index de6ce4fba8a..5a460e9bd52 100644
--- a/src/test/regress/expected/rpr.out
+++ b/src/test/regress/expected/rpr.out
@@ -2156,6 +2156,7 @@ SELECT match_first, match_last, match_len FROM result WHERE match_len > 0;
-- JIT PREV/NEXT navigation test: 100K rows with PREV in DEFINE.
-- Exercises EEOP_RPR_NAV_SET/RESTORE JIT code paths (has_rpr_nav reload)
-- at scale. V-shape: price rises then falls, repeated across partition.
+SET jit = on;
SET jit_above_cost = 0;
WITH data AS (
SELECT i, abs(50000 - i) AS price
@@ -2184,6 +2185,7 @@ FROM result WHERE match_len > 0;
(1 row)
RESET jit_above_cost;
+RESET jit;
--
-- IGNORE NULLS
--
diff --git a/src/test/regress/sql/rpr.sql b/src/test/regress/sql/rpr.sql
index b3bbc8254c4..e417789eb2b 100644
--- a/src/test/regress/sql/rpr.sql
+++ b/src/test/regress/sql/rpr.sql
@@ -1087,6 +1087,7 @@ SELECT match_first, match_last, match_len FROM result WHERE match_len > 0;
-- JIT PREV/NEXT navigation test: 100K rows with PREV in DEFINE.
-- Exercises EEOP_RPR_NAV_SET/RESTORE JIT code paths (has_rpr_nav reload)
-- at scale. V-shape: price rises then falls, repeated across partition.
+SET jit = on;
SET jit_above_cost = 0;
WITH data AS (
SELECT i, abs(50000 - i) AS price
@@ -1110,6 +1111,7 @@ result AS (
SELECT count(*) AS matched_rows, max(match_len) AS longest_match
FROM result WHERE match_len > 0;
RESET jit_above_cost;
+RESET jit;
--
-- IGNORE NULLS
--
2.50.1 (Apple Git-155)
[text/plain] nocfbot-0022-Add-2-arg-PREV-NEXT-host-variable-test.txt (4.8K, 24-nocfbot-0022-Add-2-arg-PREV-NEXT-host-variable-test.txt)
download | inline diff:
From c29f3eb34d13f2c0211d3070abaf1062da0090ae Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Mon, 6 Apr 2026 09:53:14 +0900
Subject: [PATCH] Add 2-arg PREV/NEXT test for row pattern navigation with host
variable
---
src/test/regress/expected/rpr.out | 63 +++++++++++++++++++++++++++++++
src/test/regress/sql/rpr.sql | 16 ++++++++
2 files changed, 79 insertions(+)
diff --git a/src/test/regress/expected/rpr.out b/src/test/regress/expected/rpr.out
index 5a460e9bd52..c02dbd4c08d 100644
--- a/src/test/regress/expected/rpr.out
+++ b/src/test/regress/expected/rpr.out
@@ -1492,6 +1492,69 @@ 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 positive value
+-- Exercises RPR_NAV_OFFSET_NEEDS_EVAL -> eval_nav_max_offset() path
+PREPARE test_prev_offset(int8) AS
+SELECT company, tdate, price, first_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)
+);
+EXECUTE test_prev_offset(1);
+ company | tdate | price | first_value | count
+----------+------------+-------+-------------+-------
+ company1 | 07-01-2023 | 100 | 100 | 2
+ company1 | 07-02-2023 | 200 | | 0
+ company1 | 07-03-2023 | 150 | | 0
+ company1 | 07-04-2023 | 140 | 140 | 2
+ company1 | 07-05-2023 | 150 | | 0
+ company1 | 07-06-2023 | 90 | 90 | 3
+ company1 | 07-07-2023 | 110 | | 0
+ company1 | 07-08-2023 | 130 | | 0
+ company1 | 07-09-2023 | 120 | 120 | 2
+ company1 | 07-10-2023 | 130 | | 0
+ company2 | 07-01-2023 | 50 | 50 | 2
+ company2 | 07-02-2023 | 2000 | | 0
+ company2 | 07-03-2023 | 1500 | | 0
+ company2 | 07-04-2023 | 1400 | 1400 | 2
+ company2 | 07-05-2023 | 1500 | | 0
+ company2 | 07-06-2023 | 60 | 60 | 3
+ company2 | 07-07-2023 | 1100 | | 0
+ company2 | 07-08-2023 | 1300 | | 0
+ company2 | 07-09-2023 | 1200 | 1200 | 2
+ company2 | 07-10-2023 | 1300 | | 0
+(20 rows)
+
+EXECUTE test_prev_offset(2);
+ company | tdate | price | first_value | count
+----------+------------+-------+-------------+-------
+ company1 | 07-01-2023 | 100 | | 0
+ company1 | 07-02-2023 | 200 | 200 | 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 | 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 | 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 | 3
+ company2 | 07-08-2023 | 1300 | | 0
+ company2 | 07-09-2023 | 1200 | | 0
+ company2 | 07-10-2023 | 1300 | | 0
+(20 rows)
+
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
diff --git a/src/test/regress/sql/rpr.sql b/src/test/regress/sql/rpr.sql
index e417789eb2b..47f33904690 100644
--- a/src/test/regress/sql/rpr.sql
+++ b/src/test/regress/sql/rpr.sql
@@ -732,6 +732,22 @@ EXECUTE test_prev_offset(-1);
EXECUTE test_prev_offset(NULL);
DEALLOCATE test_prev_offset;
+-- 2-arg PREV/NEXT: host variable with positive value
+-- Exercises RPR_NAV_OFFSET_NEEDS_EVAL -> eval_nav_max_offset() path
+PREPARE test_prev_offset(int8) AS
+SELECT company, tdate, price, first_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)
+);
+EXECUTE test_prev_offset(1);
+EXECUTE test_prev_offset(2);
+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,
--
2.50.1 (Apple Git-155)
[text/plain] nocfbot-0023-Nav-Mark-Lookback-EXPLAIN.txt (100.4K, 25-nocfbot-0023-Nav-Mark-Lookback-EXPLAIN.txt)
download
[text/plain] nocfbot-0024-Implement-FIRST-LAST-navigation-for-RPR.txt (165.2K, 26-nocfbot-0024-Implement-FIRST-LAST-navigation-for-RPR.txt)
download
[text/plain] nocfbot-0025-Guard-int64-overflow-bounded-frame.txt (2.3K, 27-nocfbot-0025-Guard-int64-overflow-bounded-frame.txt)
download | inline diff:
From d5c5eb7b1795eecdae73dbb23df7c12bbddc3a36 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Tue, 7 Apr 2026 13:31:44 +0900
Subject: [PATCH] Guard against int64 overflow in RPR bounded frame end
computation
---
src/backend/executor/execRPR.c | 16 ++++++++++++----
1 file changed, 12 insertions(+), 4 deletions(-)
diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 60f0d8b2fa1..ee4c67c9597 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -22,6 +22,7 @@
*/
#include "postgres.h"
+#include "common/int.h"
#include "executor/execRPR.h"
#include "executor/executor.h"
#include "miscadmin.h"
@@ -1046,10 +1047,11 @@
*
* When the frame is bounded (e.g., ROWS BETWEEN CURRENT ROW AND 5
* FOLLOWING), ExecRPRProcessRow receives hasLimitedFrame=true and
- * frameOffset indicating the upper bound. After the advance phase,
+ * frameOffset indicating the upper bound. Before the match phase,
* any context whose match has exceeded the frame boundary
- * (currentPos - matchStartRow >= frameOffset + 1) is finalized early.
- * This prevents matches from extending beyond the window frame.
+ * (currentPos >= matchStartRow + frameOffset + 1) is finalized early
+ * by forcing a mismatch. This prevents matches from extending beyond
+ * the window frame. The sum is clamped to PG_INT64_MAX on overflow.
*
* Note that bounded frames also disable context absorption at the
* planner level (see VIII-3(b)), since the frame boundary breaks the
@@ -3154,7 +3156,12 @@ ExecRPRProcessRow(WindowAggState *winstate, int64 currentPos,
/* Check frame boundary - finalize if exceeded */
if (hasLimitedFrame)
{
- int64 ctxFrameEnd = ctx->matchStartRow + frameOffset + 1;
+ int64 ctxFrameEnd;
+
+ /* Clamp to INT64_MAX on overflow */
+ if (pg_add_s64_overflow(ctx->matchStartRow, frameOffset + 1,
+ &ctxFrameEnd))
+ ctxFrameEnd = PG_INT64_MAX;
if (currentPos >= ctxFrameEnd)
{
@@ -3204,6 +3211,7 @@ ExecRPRProcessRow(WindowAggState *winstate, int64 currentPos,
* context here must be within its frame boundary.
*/
Assert(!hasLimitedFrame ||
+ ctx->matchStartRow > PG_INT64_MAX - frameOffset - 1 ||
currentPos < ctx->matchStartRow + frameOffset + 1);
nfa_advance(winstate, ctx, currentPos);
--
2.50.1 (Apple Git-155)
[text/plain] nocfbot-0026-Fix-error-message-style.txt (7.4K, 28-nocfbot-0026-Fix-error-message-style.txt)
download | inline diff:
From c3dc38316266bddc6d0d17aff47235bcfc79b303 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Tue, 7 Apr 2026 11:15:12 +0900
Subject: [PATCH] Fix RPR error message style: hint format, terminology,
capitalization
Remove colon in errhint "Use: ROWS instead" -> "Use ROWS instead."
and add missing trailing period. Shorten "row pattern definition
variable name" to "DEFINE variable" for consistency with other
error messages. Capitalize navigation function names in stub
error messages (prev -> PREV, etc.) to match SQL standard keyword
style used elsewhere in the parser.
---
src/backend/parser/parse_rpr.c | 6 +++---
src/backend/utils/adt/windowfuncs.c | 16 ++++++++--------
src/test/regress/expected/rpr_base.out | 12 ++++++------
3 files changed, 17 insertions(+), 17 deletions(-)
diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index 05070cb04bb..8fbe12e1518 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -78,7 +78,7 @@ transformRPR(ParseState *pstate, WindowClause *wc, WindowDef *windef,
ereport(ERROR,
(errcode(ERRCODE_WINDOWING_ERROR),
errmsg("FRAME option GROUPS is not permitted when row pattern recognition is used"),
- errhint("Use: ROWS instead"),
+ errhint("Use ROWS instead."),
parser_errposition(pstate,
windef->frameLocation >= 0 ?
windef->frameLocation : windef->location)));
@@ -86,7 +86,7 @@ transformRPR(ParseState *pstate, WindowClause *wc, WindowDef *windef,
ereport(ERROR,
(errcode(ERRCODE_WINDOWING_ERROR),
errmsg("FRAME option RANGE is not permitted when row pattern recognition is used"),
- errhint("Use: ROWS instead"),
+ errhint("Use ROWS instead."),
parser_errposition(pstate,
windef->frameLocation >= 0 ?
windef->frameLocation : windef->location)));
@@ -329,7 +329,7 @@ transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
if (!strcmp(n, name))
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
- errmsg("row pattern definition variable name \"%s\" appears more than once in DEFINE clause",
+ errmsg("DEFINE variable \"%s\" appears more than once",
name),
parser_errposition(pstate, exprLocation((Node *) r))));
}
diff --git a/src/backend/utils/adt/windowfuncs.c b/src/backend/utils/adt/windowfuncs.c
index 420a4962395..fb966cae43c 100644
--- a/src/backend/utils/adt/windowfuncs.c
+++ b/src/backend/utils/adt/windowfuncs.c
@@ -740,7 +740,7 @@ window_prev(PG_FUNCTION_ARGS)
{
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("prev() can only be used in a DEFINE clause")));
+ errmsg("PREV() can only be used in a DEFINE clause")));
PG_RETURN_NULL(); /* not reached */
}
@@ -754,7 +754,7 @@ window_next(PG_FUNCTION_ARGS)
{
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("next() can only be used in a DEFINE clause")));
+ errmsg("NEXT() can only be used in a DEFINE clause")));
PG_RETURN_NULL(); /* not reached */
}
@@ -768,7 +768,7 @@ window_prev_offset(PG_FUNCTION_ARGS)
{
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("prev() can only be used in a DEFINE clause")));
+ errmsg("PREV() can only be used in a DEFINE clause")));
PG_RETURN_NULL(); /* not reached */
}
@@ -782,7 +782,7 @@ window_next_offset(PG_FUNCTION_ARGS)
{
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("next() can only be used in a DEFINE clause")));
+ errmsg("NEXT() can only be used in a DEFINE clause")));
PG_RETURN_NULL(); /* not reached */
}
@@ -796,7 +796,7 @@ window_first(PG_FUNCTION_ARGS)
{
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("first() can only be used in a DEFINE clause")));
+ errmsg("FIRST() can only be used in a DEFINE clause")));
PG_RETURN_NULL(); /* not reached */
}
@@ -810,7 +810,7 @@ window_last(PG_FUNCTION_ARGS)
{
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("last() can only be used in a DEFINE clause")));
+ errmsg("LAST() can only be used in a DEFINE clause")));
PG_RETURN_NULL(); /* not reached */
}
@@ -824,7 +824,7 @@ window_first_offset(PG_FUNCTION_ARGS)
{
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("first() can only be used in a DEFINE clause")));
+ errmsg("FIRST() can only be used in a DEFINE clause")));
PG_RETURN_NULL(); /* not reached */
}
@@ -838,6 +838,6 @@ window_last_offset(PG_FUNCTION_ARGS)
{
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("last() can only be used in a DEFINE clause")));
+ errmsg("LAST() can only be used in a DEFINE clause")));
PG_RETURN_NULL(); /* not reached */
}
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index 0845316965e..912bd7b7c77 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -232,7 +232,7 @@ WINDOW w AS (
PATTERN (A+)
DEFINE A AS id > 0, A AS id < 10
);
-ERROR: row pattern definition variable name "a" appears more than once in DEFINE clause
+ERROR: DEFINE variable "a" appears more than once
LINE 7: DEFINE A AS id > 0, A AS id < 10
^
-- Expected: ERROR: row pattern definition variable name "a" appears more than once in DEFINE clause
@@ -469,7 +469,7 @@ WINDOW w AS (
ERROR: FRAME option RANGE is not permitted when row pattern recognition is used
LINE 5: RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWIN...
^
-HINT: Use: ROWS instead
+HINT: Use ROWS instead.
-- Expected: ERROR: FRAME option RANGE is not permitted when row pattern recognition is used
-- GROUPS frame not starting at CURRENT ROW
SELECT COUNT(*) OVER w
@@ -483,7 +483,7 @@ WINDOW w AS (
ERROR: FRAME option GROUPS is not permitted when row pattern recognition is used
LINE 5: GROUPS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWI...
^
-HINT: Use: ROWS instead
+HINT: Use ROWS instead.
-- Expected: ERROR: FRAME option GROUPS is not permitted when row pattern recognition is used
-- Starting with N PRECEDING
SELECT COUNT(*) OVER w
@@ -640,7 +640,7 @@ ORDER BY id;
ERROR: FRAME option RANGE is not permitted when row pattern recognition is used
LINE 5: RANGE BETWEEN CURRENT ROW AND 10 FOLLOWING
^
-HINT: Use: ROWS instead
+HINT: Use ROWS instead.
-- Expected: ERROR: FRAME option RANGE is not permitted when row pattern recognition is used
-- GROUPS frame with RPR (not permitted)
SELECT id, val, COUNT(*) OVER w as cnt
@@ -656,7 +656,7 @@ ORDER BY id;
ERROR: FRAME option GROUPS is not permitted when row pattern recognition is used
LINE 5: GROUPS BETWEEN CURRENT ROW AND 1 FOLLOWING
^
-HINT: Use: ROWS instead
+HINT: Use ROWS instead.
-- Expected: ERROR: FRAME option GROUPS is not permitted when row pattern recognition is used
DROP TABLE rpr_frame;
-- ============================================================
@@ -705,7 +705,7 @@ ORDER BY id;
ERROR: FRAME option RANGE is not permitted when row pattern recognition is used
LINE 6: RANGE BETWEEN CURRENT ROW AND 10 FOLLOWING
^
-HINT: Use: ROWS instead
+HINT: Use ROWS instead.
-- Expected: ERROR: FRAME option RANGE is not permitted when row pattern recognition is used
DROP TABLE rpr_partition;
-- ============================================================
--
2.50.1 (Apple Git-155)
[text/plain] nocfbot-0027-Fix-comment-typos-grammar.txt (5.3K, 29-nocfbot-0027-Fix-comment-typos-grammar.txt)
download | inline diff:
From c48c70f494fb656f45e2923bc4260ad8c82f5787 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Tue, 7 Apr 2026 10:09:41 +0900
Subject: [PATCH] Fix comment typos, grammar, and inaccuracies in RPR code
---
src/backend/executor/execRPR.c | 7 +++----
src/backend/executor/nodeWindowAgg.c | 20 ++++++++++----------
src/backend/optimizer/plan/createplan.c | 3 ++-
3 files changed, 15 insertions(+), 15 deletions(-)
diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index ee4c67c9597..dede2dfab0d 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -416,7 +416,6 @@
* the normal loop-back (which cycle detection will eventually kill) and
* a fast-forward exit clone that bypasses the loop entirely.
* (See IX-4(c) for detailed runtime behavior.)
- * - Empty match is impossible since body is not nullable
*
* IV-5. Absorbability Analysis (RPR_ELEM_ABSORBABLE)
*
@@ -645,8 +644,8 @@
* When processing a context whose matchStartRow differs from the shared
* value, nfa_reevaluate_dependent_vars() temporarily sets nav_match_start
* to that context's matchStartRow and re-evaluates only the dependent
- * variables. No restore is needed because contexts are ordered by
- * matchStartRow (ascending), so no later context shares the head's value.
+ * variables. The original nav_match_start and currentpos are saved and
+ * restored after re-evaluation.
*
* VI-5. Tuplestore Mark and Trim (nodeWindowAgg.c)
*
@@ -2715,7 +2714,7 @@ nfa_advance_var(WindowAggState *winstate, RPRNFAContext *ctx,
bool reluctant = RPRElemIsReluctant(elem);
/*
- * Clone state for the second-priority path. For greedy, clone is the
+ * Clone state for the first-priority path. For greedy, clone is the
* loop state; for reluctant, clone is the exit state.
*/
if (reluctant)
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index cdbe356abd7..849ebf8abb0 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -3705,8 +3705,8 @@ ignorenulls_getfuncarginframe(WindowObject winobj, int argno,
{
/*
* Early check if row could be out of reduced frame. When RPR is
- * enabled, EXCUDE clause cannot be specified and the frame is always
- * contiguous. So we can do the check followings safely. Note,
+ * enabled, EXCLUDE clause cannot be specified and the frame is always
+ * contiguous. So we can safely perform the following checks. Note,
* however, it is possible that a row is out of reduced frame if
* there's a NULL in the middle. So we need to check it in the
* following do loop.
@@ -4168,7 +4168,7 @@ eval_nav_first_offset(WindowAggState *winstate, List *defineClause)
/*
* rpr_is_defined
- * return true if Row pattern recognition is defined.
+ * Return true if row pattern recognition is defined.
*/
static bool
rpr_is_defined(WindowAggState *winstate)
@@ -4182,14 +4182,14 @@ rpr_is_defined(WindowAggState *winstate)
* Determine whether a row is in the current row's reduced window frame
* according to row pattern matching
*
- * The row must has been already determined that it is in a full window frame
- * and fetched it into slot.
+ * The row must have already been determined to be in a full window frame
+ * and fetched into the slot.
*
* Returns:
* = 0, RPR is not defined.
* >0, if the row is the first in the reduced frame. Return the number of rows
* in the reduced frame.
- * -1, if the row is unmatched row
+ * -1, if the row is an unmatched row
* -2, if the row is in the reduced frame but needed to be skipped because of
* AFTER MATCH SKIP PAST LAST ROW
* -----------------
@@ -4204,8 +4204,8 @@ row_is_in_reduced_frame(WindowObject winobj, int64 pos)
if (!rpr_is_defined(winstate))
{
/*
- * RPR is not defined. Assume that we are always in the the reduced
- * window frame.
+ * RPR is not defined. Assume that we are always in the reduced window
+ * frame.
*/
rtn = 0;
return rtn;
@@ -4938,8 +4938,8 @@ WinGetFuncArgInFrame(WindowObject winobj, int argno,
* isout: output argument, set to indicate whether target row position
* is out of frame (can pass NULL if caller doesn't care about this)
*
- * Returns 0 if we successfully got the slot. false if out of frame.
- * (also isout is set)
+ * Returns 0 if we successfully got the slot, or nonzero if out of frame.
+ * (isout is also set in the latter case.)
*/
static int
WinGetSlotInFrame(WindowObject winobj, TupleTableSlot *slot,
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index 02d511269ab..50668f3b7ab 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -2475,7 +2475,8 @@ typedef struct NavOffsetContext
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 (>= 0), or -1 if none */
+ int64 firstOffset; /* min FIRST offset (may be negative for
+ * PREV_FIRST) */
bool hasFirst; /* any FIRST node found */
bool firstNeedsEval; /* non-constant FIRST offset found */
} NavOffsetContext;
--
2.50.1 (Apple Git-155)
[text/plain] nocfbot-0028-Fix-documentation-synopsis-grammar.txt (9.5K, 30-nocfbot-0028-Fix-documentation-synopsis-grammar.txt)
download | inline diff:
From d3590794004c6200dee5178795a8d983f02d4375 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Tue, 7 Apr 2026 10:50:36 +0900
Subject: [PATCH] Fix RPR documentation: synopsis, grammar, and terminology
Remove erroneous comma in PATTERN synopsis. Fix typos in
advanced.sgml (">=;" stray semicolon, "with the a row",
"For example following"). Correct PREV/NEXT description
from "within the window frame" to "within the partition"
and add missing "DEFINE clause only" note. Capitalize
"Row Pattern Recognition" consistently across SGML files.
Fix numerous missing articles and grammar errors in
select.sgml: "after a match found" -> "after a match is
found", "do not necessarily" -> "does not necessarily",
add missing "the" before clause references.
---
doc/src/sgml/advanced.sgml | 14 +++++++-------
doc/src/sgml/func/func-window.sgml | 14 ++++++++------
doc/src/sgml/ref/select.sgml | 28 ++++++++++++++--------------
3 files changed, 29 insertions(+), 27 deletions(-)
diff --git a/doc/src/sgml/advanced.sgml b/doc/src/sgml/advanced.sgml
index 0caf9fdaff6..11c2416df51 100644
--- a/doc/src/sgml/advanced.sgml
+++ b/doc/src/sgml/advanced.sgml
@@ -553,8 +553,8 @@ WHERE pos < 3;
</para>
<para>
- Row pattern common syntax can be used to perform row pattern recognition
- in a query. The row pattern common syntax includes two sub
+ Row Pattern Common Syntax can be used to perform Row Pattern Recognition
+ in a query. The Row Pattern Common Syntax includes two sub
clauses: <literal>DEFINE</literal>
and <literal>PATTERN</literal>. <literal>DEFINE</literal> defines
row pattern variables along with an expression. The expression must be a
@@ -584,12 +584,12 @@ DEFINE
Once <literal>DEFINE</literal> exists, <literal>PATTERN</literal> can be
used. <literal>PATTERN</literal> defines a sequence of rows that satisfies
conditions defined in the <literal>DEFINE</literal> clause. For example
- following <literal>PATTERN</literal> defines a sequence of rows starting
- with the a row satisfying "LOWPRICE", then one or more rows satisfying
+ the following <literal>PATTERN</literal> defines a sequence of rows starting
+ with a row satisfying "LOWPRICE", then one or more rows satisfying
"UP" and finally one or more rows satisfying "DOWN". Pattern variables can
be followed by quantifiers: "+" means one or more matches, "*" means zero
or more matches, "?" means zero or one match, "{n}" (n > 0) means exactly
- n matches, "{n,}" (n >=; 0) means at least n matches, "{,m}" (m > 0) means
+ n matches, "{n,}" (n >= 0) means at least n matches, "{,m}" (m > 0) means
at most m matches, and "{n,m}" (0 <= n <= m, 0 < m) means between n and m
matches. Patterns can be grouped using parentheses and combined using
alternation (the vertical bar "|" for OR). For example, "(UP DOWN)+"
@@ -642,7 +642,7 @@ FROM stock
</para>
<para>
- Row pattern recognition internally uses a nondeterministic finite
+ Row Pattern Recognition internally uses a nondeterministic finite
automaton (NFA) to match patterns. For patterns with unbounded
quantifiers (e.g., <literal>A+</literal> or <literal>(A B)+</literal>),
the NFA may need to track many active matching contexts simultaneously,
@@ -676,7 +676,7 @@ FROM stock
</para>
<para>
- When examining query plans for row pattern recognition with
+ When examining query plans for Row Pattern Recognition with
<command>EXPLAIN</command>, the pattern output may include special
markers that indicate optimization opportunities. A double quote
<literal>"</literal> marks where pattern absorption can occur,
diff --git a/doc/src/sgml/func/func-window.sgml b/doc/src/sgml/func/func-window.sgml
index ab80690f7be..d109a2d22bc 100644
--- a/doc/src/sgml/func/func-window.sgml
+++ b/doc/src/sgml/func/func-window.sgml
@@ -279,9 +279,9 @@
</para>
<para>
- Row pattern recognition navigation functions are listed in
+ Row Pattern Recognition navigation functions are listed in
<xref linkend="functions-rpr-navigation-table"/>. These functions
- can be used to describe DEFINE clause of Row pattern recognition.
+ can be used to describe the DEFINE clause of Row Pattern Recognition.
</para>
<table id="functions-rpr-navigation-table">
@@ -309,12 +309,13 @@
</para>
<para>
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.
+ rows before the current row within the partition;
+ returns NULL if the target row is outside the partition.
<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.
+ Can only be used in a <literal>DEFINE</literal> clause.
</para></entry>
</row>
@@ -328,12 +329,13 @@
</para>
<para>
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.
+ rows after the current row within the partition;
+ returns NULL if the target row is outside the partition.
<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.
+ Can only be used in a <literal>DEFINE</literal> clause.
</para></entry>
</row>
diff --git a/doc/src/sgml/ref/select.sgml b/doc/src/sgml/ref/select.sgml
index 5e4ba9d3cc6..5272d6c0bfa 100644
--- a/doc/src/sgml/ref/select.sgml
+++ b/doc/src/sgml/ref/select.sgml
@@ -1133,34 +1133,34 @@ EXCLUDE NO OTHERS
<para>
The
optional <replaceable class="parameter">row_pattern_common_syntax</replaceable>
- defines the <firstterm>row pattern recognition condition</firstterm> for
+ defines the <firstterm>Row Pattern Recognition condition</firstterm> for
this
window. <replaceable class="parameter">row_pattern_common_syntax</replaceable>
- includes following subclauses.
+ includes the following subclauses.
<synopsis>
[ { AFTER MATCH SKIP PAST LAST ROW | AFTER MATCH SKIP TO NEXT ROW } ]
[ INITIAL | SEEK ]
-PATTERN ( <replaceable class="parameter">pattern_variable_name</replaceable> [ <replaceable>quantifier</replaceable> ] [, ...] )
+PATTERN ( <replaceable class="parameter">pattern_variable_name</replaceable> [ <replaceable>quantifier</replaceable> ] [ ... ] )
DEFINE <replaceable class="parameter">definition_variable_name</replaceable> AS <replaceable class="parameter">expression</replaceable> [, ...]
</synopsis>
<literal>AFTER MATCH SKIP PAST LAST ROW</literal> or <literal>AFTER MATCH
- SKIP TO NEXT ROW</literal> controls how to proceed to next row position
- after a match found. With <literal>AFTER MATCH SKIP PAST LAST
- ROW</literal> (the default) next row position is next to the last row of
- previous match. On the other hand, with <literal>AFTER MATCH SKIP TO NEXT
- ROW</literal> next row position is next to the first row of previous
- match. <literal>INITIAL</literal> or <literal>SEEK</literal> defines how a
- successful pattern matching starts from which row in a
- frame. If <literal>INITIAL</literal> is specified, the match must start
+ SKIP TO NEXT ROW</literal> controls how to proceed to the next row position
+ after a match is found. With <literal>AFTER MATCH SKIP PAST LAST
+ ROW</literal> (the default) the next row position is next to the last row of
+ the previous match. On the other hand, with <literal>AFTER MATCH SKIP TO NEXT
+ ROW</literal> the next row position is next to the first row of the previous
+ match. <literal>INITIAL</literal> or <literal>SEEK</literal> specifies from
+ which row in the frame pattern matching begins.
+ If <literal>INITIAL</literal> is specified, the match must start
from the first row in the frame. If <literal>SEEK</literal> is specified,
- the set of matching rows do not necessarily start from the first row. The
+ the set of matching rows does not necessarily start from the first row. The
default is <literal>INITIAL</literal>. Currently
only <literal>INITIAL</literal> is supported. <literal>DEFINE</literal>
defines definition variables along with a boolean
expression. <literal>PATTERN</literal> defines a sequence of rows that
satisfies certain conditions using variables defined
- in <literal>DEFINE</literal> clause (an empty <literal>PATTERN()</literal>
+ in the <literal>DEFINE</literal> clause (an empty <literal>PATTERN()</literal>
is not supported). Each pattern variable can be followed by a quantifier
to specify how many times it should match:
<literal>*</literal> (zero or more),
@@ -1198,7 +1198,7 @@ DEFINE <replaceable class="parameter">definition_variable_name</replaceable> AS
<para>
Note that the maximum number of unique pattern variables
- used in <literal>PATTERN</literal> clause is 251.
+ used in the <literal>PATTERN</literal> clause is 251.
If this limit is exceeded, an error will be raised.
Additionally, the maximum nesting depth of pattern groups
(parentheses) is 253 levels.
--
2.50.1 (Apple Git-155)
[text/plain] nocfbot-0029-Fix-nav_slot-pass-by-ref-dangling-pointer.txt (10.0K, 31-nocfbot-0029-Fix-nav_slot-pass-by-ref-dangling-pointer.txt)
download | inline diff:
From 6a716cd3dc1f808438dba393603ec955cae63e65 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Tue, 7 Apr 2026 13:59:29 +0900
Subject: [PATCH] Fix nav_slot pass-by-ref dangling pointer in RPR navigation
When a DEFINE expression contains multiple navigation calls targeting
different positions (e.g., PREV(x,1) > PREV(x,2)), the second call
re-fetches nav_slot, freeing the previous tuple via pfree. Any
pass-by-ref datum extracted from the first navigation becomes a
dangling pointer. Fix by copying pass-by-ref results into per-tuple
memory in the RESTORE step.
---
src/backend/executor/execExpr.c | 5 ++
src/backend/executor/execExprInterp.c | 20 +++++++
src/include/executor/execExpr.h | 2 +
src/test/regress/expected/rpr.out | 80 +++++++++++++++++++++++++++
src/test/regress/sql/rpr.sql | 34 ++++++++++++
5 files changed, 141 insertions(+)
diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c
index 6349a564a98..8d8da67e79f 100644
--- a/src/backend/executor/execExpr.c
+++ b/src/backend/executor/execExpr.c
@@ -1304,7 +1304,12 @@ ExecInitExprRec(Expr *node, ExprState *state,
/* Emit RESTORE opcode: restore original slot */
scratch.opcode = EEOP_RPR_NAV_RESTORE;
+ scratch.resvalue = resv;
+ scratch.resnull = resnull;
scratch.d.rpr_nav.winstate = winstate;
+ get_typlenbyval(nav->resulttype,
+ &scratch.d.rpr_nav.resulttyplen,
+ &scratch.d.rpr_nav.resulttypbyval);
ExprEvalPushStep(state, &scratch);
break;
}
diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c
index 2ec579732cc..e2d41c3098f 100644
--- a/src/backend/executor/execExprInterp.c
+++ b/src/backend/executor/execExprInterp.c
@@ -6156,6 +6156,13 @@ ExecEvalRPRNavSet(ExprState *state, ExprEvalStep *op, ExprContext *econtext)
* 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
+ * nav_slot's tuple memory. If a subsequent navigation in the same
+ * expression re-fetches nav_slot for a different position, the old
+ * tuple is freed, leaving a dangling pointer. We prevent this by
+ * copying pass-by-ref results into per-tuple memory, which survives
+ * until the next ResetExprContext.
*/
void
ExecEvalRPRNavRestore(ExprState *state, ExprEvalStep *op,
@@ -6164,4 +6171,17 @@ ExecEvalRPRNavRestore(ExprState *state, ExprEvalStep *op,
WindowAggState *winstate = op->d.rpr_nav.winstate;
econtext->ecxt_outertuple = winstate->nav_saved_outertuple;
+
+ /* Stabilize pass-by-ref result against nav_slot re-fetch */
+ if (!op->d.rpr_nav.resulttypbyval &&
+ !*op->resnull)
+ {
+ MemoryContext oldContext;
+
+ oldContext = MemoryContextSwitchTo(econtext->ecxt_per_tuple_memory);
+ *op->resvalue = datumCopy(*op->resvalue,
+ false,
+ op->d.rpr_nav.resulttyplen);
+ MemoryContextSwitchTo(oldContext);
+ }
}
diff --git a/src/include/executor/execExpr.h b/src/include/executor/execExpr.h
index 834800a4062..e6b2ab30406 100644
--- a/src/include/executor/execExpr.h
+++ b/src/include/executor/execExpr.h
@@ -703,6 +703,8 @@ typedef struct ExprEvalStep
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? */
} rpr_nav;
/* for EEOP_AGG_*DESERIALIZE */
diff --git a/src/test/regress/expected/rpr.out b/src/test/regress/expected/rpr.out
index 04ec25d4cf5..32aa8bc3722 100644
--- a/src/test/regress/expected/rpr.out
+++ b/src/test/regress/expected/rpr.out
@@ -1635,6 +1635,86 @@ WINDOW w AS (
company2 | 07-10-2023 | 1300 | | | 0
(20 rows)
+-- Pass-by-ref types: two PREV calls targeting different positions.
+-- Verifies that datumCopy in RESTORE prevents dangling pointers when
+-- nav_slot is re-fetched for the second navigation.
+-- tdate::text gives distinct text values per row (e.g. '07-01-2023').
+-- B matches when 1-back date text > 2-back date text (always true for
+-- ascending dates), so B+ extends the full partition after A.
+SELECT company, tdate, tdate::text AS tdate_text,
+ first_value(tdate::text) OVER w, last_value(tdate::text) 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 PREV(tdate::text, 1) > PREV(tdate::text, 2)
+);
+ company | tdate | tdate_text | first_value | last_value | count
+----------+------------+------------+-------------+------------+-------
+ company1 | 07-01-2023 | 07-01-2023 | | | 0
+ company1 | 07-02-2023 | 07-02-2023 | 07-02-2023 | 07-10-2023 | 9
+ company1 | 07-03-2023 | 07-03-2023 | | | 0
+ company1 | 07-04-2023 | 07-04-2023 | | | 0
+ company1 | 07-05-2023 | 07-05-2023 | | | 0
+ company1 | 07-06-2023 | 07-06-2023 | | | 0
+ company1 | 07-07-2023 | 07-07-2023 | | | 0
+ company1 | 07-08-2023 | 07-08-2023 | | | 0
+ company1 | 07-09-2023 | 07-09-2023 | | | 0
+ company1 | 07-10-2023 | 07-10-2023 | | | 0
+ company2 | 07-01-2023 | 07-01-2023 | | | 0
+ company2 | 07-02-2023 | 07-02-2023 | 07-02-2023 | 07-10-2023 | 9
+ company2 | 07-03-2023 | 07-03-2023 | | | 0
+ company2 | 07-04-2023 | 07-04-2023 | | | 0
+ company2 | 07-05-2023 | 07-05-2023 | | | 0
+ company2 | 07-06-2023 | 07-06-2023 | | | 0
+ company2 | 07-07-2023 | 07-07-2023 | | | 0
+ company2 | 07-08-2023 | 07-08-2023 | | | 0
+ company2 | 07-09-2023 | 07-09-2023 | | | 0
+ company2 | 07-10-2023 | 07-10-2023 | | | 0
+(20 rows)
+
+-- numeric: PREV(price::numeric, 1) > PREV(price::numeric, 2)
+-- B matches when price 1-back > price 2-back (ascending pair).
+SELECT company, tdate, price::numeric AS nprice,
+ first_value(price::numeric) OVER w, last_value(price::numeric) 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 PREV(price::numeric, 1) > PREV(price::numeric, 2)
+);
+ company | tdate | nprice | 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 | 150 | 90 | 2
+ 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 | 1500 | 60 | 2
+ 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)
+
--
-- FIRST/LAST navigation
--
diff --git a/src/test/regress/sql/rpr.sql b/src/test/regress/sql/rpr.sql
index a05b429ce74..724d460b2da 100644
--- a/src/test/regress/sql/rpr.sql
+++ b/src/test/regress/sql/rpr.sql
@@ -776,6 +776,40 @@ WINDOW w AS (
DEFINE A AS price > PREV(price, 1) AND price < NEXT(price, 1)
);
+-- Pass-by-ref types: two PREV calls targeting different positions.
+-- Verifies that datumCopy in RESTORE prevents dangling pointers when
+-- nav_slot is re-fetched for the second navigation.
+-- tdate::text gives distinct text values per row (e.g. '07-01-2023').
+-- B matches when 1-back date text > 2-back date text (always true for
+-- ascending dates), so B+ extends the full partition after A.
+SELECT company, tdate, tdate::text AS tdate_text,
+ first_value(tdate::text) OVER w, last_value(tdate::text) 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 PREV(tdate::text, 1) > PREV(tdate::text, 2)
+);
+
+-- numeric: PREV(price::numeric, 1) > PREV(price::numeric, 2)
+-- B matches when price 1-back > price 2-back (ascending pair).
+SELECT company, tdate, price::numeric AS nprice,
+ first_value(price::numeric) OVER w, last_value(price::numeric) 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 PREV(price::numeric, 1) > PREV(price::numeric, 2)
+);
+
--
-- FIRST/LAST navigation
--
--
2.50.1 (Apple Git-155)
[text/plain] nocfbot-0030-Add-inline-comments-design-notes.txt (11.2K, 32-nocfbot-0030-Add-inline-comments-design-notes.txt)
download | inline diff:
From d09908f16025319da7771c40892a3a66df5fdb7e Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Tue, 7 Apr 2026 20:49:27 +0900
Subject: [PATCH] Add inline comments for complex RPR algorithms and design
notes
Document END chain traversal in nfa_match(), fast-forward paths
in nfa_advance_end(), absorption safety rules with navigation
lookup table, per-context evaluation strategy table, fixed-length
group unrolling rationale, and BEGIN/END pointer layout diagram.
---
src/backend/executor/execRPR.c | 97 ++++++++++++++++++++++++++------
src/backend/optimizer/plan/rpr.c | 41 ++++++++++++--
2 files changed, 118 insertions(+), 20 deletions(-)
diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index dede2dfab0d..037d3b2e232 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -440,7 +440,14 @@
* Case 2: GROUP+ with fixed-length children (min == max, recursively)
* e.g., (A B)+, (A B{2})+, ((A (B C){2}){2})+
* -> ABSORBABLE_BRANCH on all elements within the group,
- * ABSORBABLE | ABSORBABLE_BRANCH on END
+ * ABSORBABLE | ABSORBABLE_BRANCH on END
+ *
+ * Why this is safe: when every child has min == max, the group
+ * is semantically equivalent to unrolling its body into {1,1}
+ * elements. E.g., (A B{2})+ behaves like (A B B)+. Each
+ * iteration consumes a fixed number of rows, so an earlier
+ * context's count always dominates a later one's (monotonicity).
+ *
* Case 3: GROUP+ whose body starts with VAR+ (e.g., (A+ B)+)
* -> Recurses from BEGIN into the body, applying Case 1.
* ABSORBABLE | ABSORBABLE_BRANCH set on A.
@@ -647,6 +654,19 @@
* variables. The original nav_match_start and currentpos are saved and
* restored after re-evaluation.
*
+ * Summary of evaluation strategy by navigation content:
+ *
+ * Navigation content evaluation
+ * -------------------------------------------------------
+ * No navigation shared (once per row)
+ * PREV/NEXT only shared (once per row)
+ * LAST (no offset) shared (once per row)
+ * LAST (with offset) per-context
+ * FIRST (any) per-context
+ * Compound (inner FIRST) per-context
+ * Compound (inner LAST, no off.) shared (once per row)
+ * Compound (inner LAST, w/off.) per-context
+ *
* VI-5. Tuplestore Mark and Trim (nodeWindowAgg.c)
*
* Navigation functions require access to past rows via the tuplestore.
@@ -762,11 +782,26 @@
* (b) Unbounded frame (ROWS BETWEEN CURRENT ROW AND UNBOUNDED
* FOLLOWING). Limited frames apply differently to each context,
* breaking the monotonicity principle.
- * (c) No match_start-dependent navigation in DEFINE. FIRST,
- * LAST-with-offset, and compound navigation referencing match_start
- * (PREV_FIRST, NEXT_FIRST, PREV_LAST/NEXT_LAST with offset)
- * cause different contexts to evaluate to different values for the
- * same row, breaking monotonicity.
+ * (c) No match_start-dependent navigation in DEFINE.
+ *
+ * Mechanism: each context has a different matchStartRow, so FIRST
+ * resolves to a different row for each context at the same
+ * currentpos. An earlier context's DEFINE result no longer
+ * subsumes a later one's, making count-dominance comparison
+ * invalid. Rather than comparing matchStartRow at runtime
+ * (which would complicate the absorb path), any match_start
+ * dependency disables absorption entirely.
+ *
+ * Navigation content match_start dep. absorption
+ * ------------------------------------------------------------
+ * No navigation none safe
+ * PREV/NEXT only none safe
+ * LAST (no offset) none safe
+ * LAST (with offset) boundary check unsafe
+ * FIRST (any) direct unsafe
+ * Compound (inner FIRST) direct unsafe
+ * Compound (inner LAST, no off.) none safe
+ * Compound (inner LAST, w/off.) boundary chk unsafe
*
* Runtime conditions (evaluated per context pair):
*
@@ -2260,7 +2295,13 @@ nfa_absorb_contexts(WindowAggState *winstate)
* nfa_eval_var_match
*
* Evaluate if a VAR element matches the current row.
- * Undefined variables (varId >= defineVariableList length) default to TRUE.
+ *
+ * varMatched is a pre-evaluated boolean array indexed by varId, computed
+ * once per row by evaluating all DEFINE expressions. NULL means no DEFINE
+ * clauses exist (only possible during early development/testing).
+ *
+ * Per SQL:2016 R020, pattern variables not listed in DEFINE are implicitly
+ * TRUE -- they match every row. This is checked via varId >= list_length.
*/
static bool
nfa_eval_var_match(WindowAggState *winstate, RPRPatternElement *elem,
@@ -2337,9 +2378,20 @@ nfa_match(WindowAggState *winstate, RPRNFAContext *ctx, bool *varMatched)
/*
* For VAR at max count with END next, advance through END
- * chain to reach the absorption judgment point. Only
+ * chain to reach the absorption judgment point. Only
* deterministic exits (count >= max, max finite) are handled;
* unbounded VARs stay for advance phase.
+ *
+ * In nested patterns like ((A B){2}){3}, a VAR reaching its
+ * max triggers an exit cascade: inner END increments inner
+ * group count, which may itself reach max, requiring an exit
+ * to the next outer END. The loop below walks this chain.
+ *
+ * ABSORBABLE_BRANCH marks elements inside the absorbable
+ * region; ABSORBABLE marks the outermost judgment point
+ * where count-dominance is evaluated. We chain through
+ * BRANCH elements until reaching the ABSORBABLE point or
+ * an element that can still loop (count < max).
*/
if (RPRElemIsAbsorbableBranch(elem) &&
!RPRElemIsAbsorbable(elem) &&
@@ -2561,12 +2613,25 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
RPRPatternElement *jumpElem;
RPRNFAState *ffState = NULL;
- /* Snapshot state for ff path before modifying for loop-back */
+ /*
+ * Two paths are explored in parallel when the group body is
+ * nullable (RPR_ELEM_EMPTY_LOOP):
+ *
+ * 1. Primary path: loop back and attempt real matches in the
+ * next iteration (state, modified below).
+ *
+ * 2. Fast-forward path: skip directly to after the group,
+ * treating all remaining required iterations as empty
+ * matches (ffState, handled after the primary path).
+ *
+ * The snapshot must be taken BEFORE modifying state for the
+ * loop-back, since both paths diverge from the same point.
+ */
if (RPRElemCanEmptyLoop(elem))
ffState = nfa_state_create(winstate, state->elemIdx,
state->counts, state->isAbsorbable);
- /* Loop back for real matches (primary path) */
+ /* Primary path: loop back for real matches */
for (int d = depth + 1; d < pattern->maxDepth; d++)
state->counts[d] = 0;
state->elemIdx = elem->jump;
@@ -2575,12 +2640,12 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
currentPos);
/*
- * Fast-forward fallback for nullable bodies. E.g. (A?){2,3} when A
- * doesn't match: the loop-back produces empty iterations that cycle
- * detection would kill. Instead, exit directly treating all
- * remaining required iterations as empty. Route to elem->next (not
- * nfa_advance_end) to avoid creating competing greedy/reluctant loop
- * states.
+ * Fast-forward path for nullable bodies. E.g. (A?){2,3} when
+ * A doesn't match: the primary loop-back produces empty
+ * iterations that cycle detection would kill. Instead, exit
+ * directly with count satisfied. Route to elem->next (not
+ * nfa_advance_end) to avoid creating competing greedy/reluctant
+ * loop states.
*/
if (ffState != NULL)
{
diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index 767a214016c..754fcd53099 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -478,6 +478,19 @@ mergeConsecutiveAlts(List *children)
* mergeGroupPrefixSuffix
* Merge sequence prefix/suffix into GROUP with matching children.
*
+ * When a GROUP's children appear as a prefix before and/or suffix after
+ * the GROUP in a SEQ, absorb them by incrementing the GROUP's quantifier.
+ * This runs iteratively: A B A B (A B)+ A B -> (A B){5,}.
+ *
+ * Algorithm:
+ * For each GROUP encountered in the sequence:
+ * 1. PREFIX phase: compare the last N elements already in the result
+ * list against the GROUP's children. On match, remove them from
+ * result and increment the GROUP's min/max. Repeat until no match.
+ * 2. SUFFIX phase: compare the next N elements in the input against
+ * the GROUP's children. On match, skip them (via skipUntil) and
+ * increment min/max. Repeat until no match.
+ *
* Examples:
* A B (A B)+ -> (A B){2,}
* (A B)+ A B -> (A B){2,}
@@ -813,8 +826,16 @@ tryMultiplyQuantifiers(RPRPatternNode *pattern)
}
/*
- * Case 2/3: Safe when child is finite AND (outer is exact OR child is
- * {1,1})
+ * Case 2: Outer exact (min == max): (A{2,3}){4} -> A{8,12}.
+ * Safe because every iteration produces the same range.
+ *
+ * Case 3: Child {1,1}: (A){2,5} -> A{2,5}.
+ * Safe because the child contributes exactly one per
+ * iteration, so the outer range maps directly.
+ *
+ * Unsafe example: (A{2}){2,3} produces counts 4 or 6 only, not
+ * the full range 4..6, so we cannot flatten when child has a
+ * non-trivial range AND outer is also a range.
*/
if (child->max != RPR_QUANTITY_INF &&
(pattern->min == pattern->max ||
@@ -824,6 +845,7 @@ tryMultiplyQuantifiers(RPRPatternNode *pattern)
if (new_min_64 >= RPR_QUANTITY_INF)
return pattern;
+ /* Outer unbounded: result is unbounded regardless of child */
if (pattern->max == RPR_QUANTITY_INF)
new_max_64 = RPR_QUANTITY_INF;
else
@@ -1186,8 +1208,19 @@ fillRPRPatternVar(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth dept
* fillRPRPatternGroup
* Fill a GROUP pattern and its children.
*
- * Creates elements for group content at increased depth, plus an END marker
- * if the group has a non-trivial quantifier.
+ * Creates elements for group content at increased depth, plus BEGIN/END
+ * marker pair if the group has a non-trivial quantifier (not {1,1}).
+ *
+ * Element layout for (A B){2,3}:
+ *
+ * [BEGIN] [A] [B] [END] [next element...]
+ * | | ^
+ * | +-- jump --+ (loop back to first child)
+ * +---- jump -------------------+ (skip to after END)
+ *
+ * BEGIN.jump points past END (skip path when count >= max or min == 0).
+ * END.jump points to the first child (loop-back path).
+ * BEGIN.next and END.next are set later by finalizeRPRPattern().
*
* Returns true if this group is nullable. A group is nullable when its
* min is 0 (can be skipped entirely) or its body is nullable (every path
--
2.50.1 (Apple Git-155)
[text/plain] nocfbot-0031-Remove-unused-include-fix-header-ordering.txt (1.9K, 33-nocfbot-0031-Remove-unused-include-fix-header-ordering.txt)
download | inline diff:
From 29e8aaa01bb321da9eb2228832cc259725ac7841 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Tue, 7 Apr 2026 20:52:13 +0900
Subject: [PATCH] Remove unused include and fix header ordering in RPR files
---
src/backend/executor/execExprInterp.c | 2 +-
src/backend/executor/nodeWindowAgg.c | 3 +--
src/backend/parser/parse_rpr.c | 3 +--
3 files changed, 3 insertions(+), 5 deletions(-)
diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c
index e2d41c3098f..58b6693ed75 100644
--- a/src/backend/executor/execExprInterp.c
+++ b/src/backend/executor/execExprInterp.c
@@ -56,8 +56,8 @@
*/
#include "postgres.h"
-#include "common/int.h"
#include "access/heaptoast.h"
+#include "common/int.h"
#include "access/tupconvert.h"
#include "catalog/pg_type.h"
#include "commands/sequence.h"
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 849ebf8abb0..02f17e5472c 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -34,10 +34,9 @@
#include "postgres.h"
#include "access/htup_details.h"
-#include "common/int.h"
#include "catalog/objectaccess.h"
#include "catalog/pg_aggregate.h"
-#include "catalog/pg_collation_d.h"
+#include "common/int.h"
#include "catalog/pg_proc.h"
#include "executor/executor.h"
#include "executor/execRPR.h"
diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index 8fbe12e1518..8864b20e6cf 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -30,9 +30,8 @@
#include "nodes/nodeFuncs.h"
#include "optimizer/optimizer.h"
#include "optimizer/rpr.h"
-#include "parser/parse_clause.h"
-#include "parser/parse_collate.h"
#include "parser/parse_coerce.h"
+#include "parser/parse_collate.h"
#include "parser/parse_expr.h"
#include "parser/parse_rpr.h"
#include "parser/parse_target.h"
--
2.50.1 (Apple Git-155)
reply
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Reply to all the recipients using the --to and --cc options:
reply via email
To: [email protected]
Cc: [email protected], [email protected], [email protected], [email protected], [email protected], [email protected], [email protected], [email protected], [email protected]
Subject: Re: Row pattern recognition
In-Reply-To: <CAAAe_zB7rAEJtT6hXgF85=_Tj8Nti45ZHbQw26gxTF2DBs3hJw@mail.gmail.com>
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox