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, 5 Apr 2026 21:50:53 +0900
Message-ID: <CAAAe_zDEDtJEf6aO=x3qhPOs8kXmsha7Q8ROJXE8VZNp+Dxe2A@mail.gmail.com> (raw)
In-Reply-To: <CAAAe_zCB31g2bkRAWhDZVaegx+Z2JnF-zBxfXD7nunWttYi6Gg@mail.gmail.com>
References: <CAAAe_zDneX+WWEgoM+rXLg+H2OK_VuXoiipzJu2Q0rCSSCzY8g@mail.gmail.com>
<[email protected]>
<CAAAe_zBCF3dwSjStmG0kJqw_y1z8QD73Rf1G58QTKEvd9tScwA@mail.gmail.com>
<[email protected]>
<CAAAe_zCB31g2bkRAWhDZVaegx+Z2JnF-zBxfXD7nunWttYi6Gg@mail.gmail.com>
Hi Tatsuo,
Attached are 15 incremental patches on top of v46 (replacing
the previous 8-patch set). Here is a summary of the changes
from the previous version:
0001-0005: Unchanged from the previous set.
0006: Fix DEFINE expression handling in RPR window
planning (revised)
Integration tests in 0007 revealed two crashes:
(1) subquery wrapping with outer aggregate causes
WindowAgg removal when RPR window function output
is unused, (2) RPR and non-RPR windows coexisting
causes SIGSEGV from RPRNavExpr propagating to the
wrong WindowAgg. This version extends the fix to
extract only Var nodes via pull_var_clause()
instead of adding the whole DEFINE expression to
the targetlist. The allpaths.c guard is extended
to also preserve columns referenced by DEFINE
clauses.
0007: Add RPR planner integration tests (new)
Planner optimization interactions with RPR windows
have been revealing crashes, so this is a dedicated
test file: window dedup, subquery flattening,
DEFINE propagation, LATERAL, recursive CTE,
etc. Will be expanded further.
0008: Replace reduced frame map with single match result
(new)
RPR processes rows sequentially and maintains at
most one active match per start position. This
replaces the per-row boolean frame map array with
four scalar fields (valid, matched, start, length),
simplifying nodeWindowAgg.c.
0009: Add fixed-length group absorption for RPR (new)
Extends context absorption to accept fixed-length
quantifiers (min == max) inside unbounded groups.
The NFA compiler optimizes (A B B)+ into
(A B{2})+, which previously lost absorption
eligibility because only {1,1} children
qualified. Now (A B{2})+ and ((A{2} B{3}){2})+
also qualify with no runtime changes. Also fixes
two NFA bugs: (1) incorrect absorption at
non-judgment points -- added ABSORBABLE check
in nfa_states_covered and fixed isAbsorbable
propagation in advance exit paths, (2) VAR
visited bitmap moved to nfa_add_state_unique
to prevent false cycle detection on new-iteration
loop-back caused by generalized END chain
traversal. Design note posted separately [1].
0010: Rename rpr_explain test views to descriptive
names (new)
Replaces sequential view names (rpr_ev83, rpr_ev84)
with descriptive names (rpr_ev_nav_prev1, etc.) for
maintainability when adding new tests.
0011: Implement 1-slot PREV/NEXT navigation for RPR
(was 0007, rebased)
Context changes only: optimizer.h include moved to
0006, test view names updated by 0010.
0012: Add JIT compilation support for RPR PREV/NEXT
(was 0008, unchanged)
Note: master recently disabled JIT by default
[3]. A quick benchmark with RPR did not show
measurable improvement from JIT. This patch is
retained for correctness (the new opcodes must
be handled when JIT is enabled), but the
practical benefit may be limited.
0013: Add tuplestore trim optimization for RPR PREV
navigation (new)
The planner extracts max_offset from all PREV
calls in DEFINE clauses and sets mark = currentpos
- max_offset at runtime, so only max_offset rows
are retained. Constant expressions are folded by
the planner; non-constant offsets (parameters,
etc.) are evaluated by the executor at init time
via RPR_NAV_OFFSET_NEEDS_EVAL.
0014: Update RPR code comments to reflect 1-slot
navigation model (new)
Updates stale comments in execRPR.c and
parse_rpr.c that still referenced the old 3-slot
design.
0015: Implement FIRST/LAST navigation for RPR
(work in progress)
Adds FIRST(column) and LAST(column) navigation
functions per SQL standard. Parser, planner,
executor, and tests are included. See the design
note [2] for the interaction with context
absorption.
Not yet done: compound navigation (e.g.
PREV(FIRST(col))), tests for FIRST/LAST offset
variants, tuplestore mark handling for FIRST,
and additional edge-case tests.
In the FIRST/LAST design note [2], I listed remaining
items that can be done within the current architecture.
With this patch set, all items except PREFIX pattern
absorption are now covered: fixed-length group absorption
(0009), JIT proper support (0012), and FIRST/LAST
navigation (0015). PREFIX absorption is planned
separately, but stabilizing the base patch takes
priority. If it is ready in time, would it make sense
to decide then whether to include it in this round?
Apart from bug fixes, are there any features you
consider essential before the patch can be committed?
[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] jit: Change the default to off
https://github.com/postgres/postgres/commit/7f8c88c2b872
Best regards,
Henson
2026년 4월 2일 (목) 오후 12:51, Henson Choi <[email protected]>님이 작성:
> Hi Tatsuo,
>
> Thank you for the review and the attached patch for 0005.
> I appreciate you taking the time to look at each patch
> carefully.
>
> Attached are 8 incremental patches on top of v46 (replacing
> the previous 6-patch set). 0001-0003 are unchanged. 0004 and
> 0005 incorporate your feedback below. 0006 is a new planner
> fix. Previous 0006 becomes 0007. 0008 is new.
>
> > 0004: Fix in-place modification of defineClause TargetEntry
>>
>> Probably we want to modify the comment above since it implies an
>> in-place modification? What about something like this? (Modifies ->
>> Replace)
>>
>> /*
>> * Replace an expression tree in each DEFINE clause so that all
>> Var
>> * nodes's varno refers to OUTER_VAR.
>> */
>>
>
> Good point, thank you. Applied in the updated 0004.
>
>
>> > 0005: Fix mark handling for last_value() under RPR
>>
>> I think instead we can set a mark at frameheadpos when seek type is
>> SEEK_TAIL and RPR is enabled. See attached patch.
>>
>
> Thank you for the patch. Your approach is cleaner -- setting
> the mark at frameheadpos is more direct than suppressing
> advancement. I've adopted it in the updated 0005.
>
>
>> > 0006: Implement 1-slot PREV/NEXT navigation for RPR
>>
>> Excellent! I will take a look at it. (it will take for a while).
>>
>
> No rush at all. In the meantime, while testing the PREV/NEXT
> patch in various query patterns, I found a planner issue.
> I've also added JIT support. Here is a summary of the new
> patches:
>
>
> 0006: Prevent removal of RPR window functions in unused
> subquery outputs
>
> Wrapping an RPR window query in a subquery with an
> outer aggregate causes a crash:
>
> SELECT count(*) FROM (
> SELECT count(*) OVER w FROM ...
> WINDOW w AS (... DEFINE A AS i > PREV(i))
> ) t;
>
> remove_unused_subquery_outputs() replaces unused subquery
> target entries with NULL constants. When an RPR window
> function's result is not referenced by the outer query,
> this replacement eliminates all active window functions
> for the WindowClause, causing the planner to omit the
> WindowAgg node. DEFINE clause expressions containing
> RPRNavExpr (PREV/NEXT) then lose their execution context,
> leading to a crash.
>
> The fix skips the NULL replacement for window functions
> whose WindowClause has a defineClause. Even when the
> window function result is unused, RPR pattern matching
> (frame reduction) must still execute -- the WindowAgg
> node must be preserved.
>
> An alternative would be to let the planner remove the
> window function but teach it to still generate the
> WindowAgg node when defineClause is present, even with
> no active window functions. That would be a more
> targeted optimization but requires deeper planner
> changes. Do you think the current approach is
> sufficient, or would you prefer a different strategy?
>
>
> 0007: Implement 1-slot PREV/NEXT navigation for RPR
> (unchanged from previous 0006)
>
>
>> > 2. LLVM JIT fallback (in 0006): The mid-expression slot swap
>>
>> I am not an expert of JIT, but for me, it sounds reasonable. We can
>> enhance it later on.
>>
>
> 0008: Add JIT compilation support for RPR PREV/NEXT navigation
>
> EEOP_OUTER_VAR normally uses slot pointers cached in the
> entry block, but the mid-expression slot swap in
> NAV_SET/RESTORE invalidates them. To avoid penalizing
> existing expressions, the reload path is only generated
> when RPR navigation opcodes are present in the
> expression (compile-time decision). Expressions without
> PREV/NEXT produce identical machine code as before.
> NAV_SET/RESTORE use the standard build_EvalXFunc pattern.
>
> A 100K-row PREV/NEXT test case that runs under
> jit_above_cost=0 is included in rpr.sql.
>
> I would appreciate your thoughts on these when you have time.
>
>
> 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 24a19226ef1fe100fc1680b178103e997c0c48bb 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.
---
src/backend/executor/execRPR.c | 56 +++---
src/backend/executor/nodeWindowAgg.c | 221 ++++++++--------------
src/include/nodes/execnodes.h | 21 +-
src/test/regress/expected/rpr_explain.out | 8 +-
4 files changed, 126 insertions(+), 180 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..1c925e0221d 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -249,11 +249,8 @@ 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 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);
@@ -1037,12 +1034,6 @@ eval_windowaggregates(WindowAggState *winstate)
{
int ret;
-#ifdef RPR_DEBUG
- printf("===== loop in frame starts: aggregatedupto: " INT64_FORMAT " aggregatedbase: " INT64_FORMAT "\n",
- winstate->aggregatedupto,
- winstate->aggregatedbase);
-#endif
-
/* 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)));
@@ -3974,8 +3943,6 @@ row_is_in_reduced_frame(WindowObject winobj, int64 pos)
WindowAggState *winstate = winobj->winstate;
int state;
int rtn;
- int64 i;
- int num_reduced_rows;
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
- */
-static void
-create_reduced_frame_map(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);
-}
-
/*
- * clear_reduced_frame_map
- * Clear reduced frame map
+ * clear_reduced_frame
+ * Clear reduced frame status
*/
static void
-clear_reduced_frame_map(WindowAggState *winstate)
+clear_reduced_frame(WindowAggState *winstate)
{
- Assert(winstate->reduced_frame_map != NULL);
- MemSet(winstate->reduced_frame_map, RF_NOT_DETERMINED,
- winstate->alloc_sz);
+ winstate->rpr_match_valid = false;
+ winstate->rpr_match_matched = false;
+ winstate->rpr_match_start = -1;
+ winstate->rpr_match_length = 0;
}
/*
- * 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];
-}
-
-/*
- * 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);
+ /* Empty match: covers only the start position */
+ if (pos == start && winstate->rpr_match_matched && length == 0)
+ return RF_EMPTY_MATCH;
- 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 */
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 87986f98416ac0d145d654c65c5c8530ada78f3f 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 aaff42b0eac54654d9cfd42ae4d5db4c1129764b 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 1b45b59329f920174c90ee181c506505eb9357d7 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_explain.out | 80 ++++
src/test/regress/sql/rpr.sql | 322 ++++++++++++-
src/test/regress/sql/rpr_explain.sql | 56 +++
src/tools/pgindent/typedefs.list | 3 +-
21 files changed, 1606 insertions(+), 164 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 1c925e0221d..d2fffe0fce6 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 int 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++)
{
@@ -2729,15 +2740,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
@@ -2907,6 +2921,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;
@@ -2947,7 +2978,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)
{
@@ -2961,7 +2994,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);
@@ -2996,107 +3028,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;
}
@@ -3157,8 +3120,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)
@@ -4223,6 +4186,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)
@@ -4233,37 +4200,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)
{
@@ -4281,6 +4236,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 cfe24de43cf..742d889154b 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -10106,6 +10106,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_explain.out b/src/test/regress/expected/rpr_explain.out
index f66caf8908e..4a646d1e1d8 100644
--- a/src/test/regress/expected/rpr_explain.out
+++ b/src/test/regress/expected/rpr_explain.out
@@ -3828,6 +3828,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 65a775fdad9..aed2f69bc4f 100644
--- a/src/test/regress/sql/rpr_explain.sql
+++ b/src/test/regress/sql/rpr_explain.sql
@@ -2202,6 +2202,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 56ec798cb75eaee3665c0e03a81ee1673e3f8a96 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 a8481774babb51b4d5527fab6c134987f9bb49c9 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 d2fffe0fce6..186029df1d9 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.
*/
@@ -2960,6 +2991,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)
@@ -3872,6 +3907,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 e78092678bb..91148f6c574 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 cddb155a9d06f3813f874105c7f66b802652586b 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 8f0457e2b3c..329cbfbaaaa 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 33a8de8aeb8c022cac0204f0b1fc84bcdc63570e 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 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.
---
src/backend/executor/execExpr.c | 16 +-
src/backend/executor/execExprInterp.c | 54 +++++--
src/backend/executor/execRPR.c | 78 ++++++++-
src/backend/executor/nodeWindowAgg.c | 28 ++--
src/backend/optimizer/plan/createplan.c | 95 ++++++++++-
src/backend/optimizer/plan/rpr.c | 6 +-
src/backend/parser/parse_func.c | 70 ++++++--
src/backend/parser/parse_rpr.c | 56 ++++++-
src/backend/utils/adt/ruleutils.c | 19 ++-
src/backend/utils/adt/windowfuncs.c | 56 +++++++
src/include/catalog/pg_proc.dat | 12 ++
src/include/executor/execExpr.h | 2 +-
src/include/nodes/execnodes.h | 9 +-
src/include/nodes/plannodes.h | 13 +-
src/include/nodes/primnodes.h | 11 +-
src/include/optimizer/rpr.h | 10 +-
src/test/regress/expected/rpr.out | 186 ++++++++++++++++++++--
src/test/regress/expected/rpr_base.out | 77 ++++++++-
src/test/regress/expected/rpr_explain.out | 83 ++++++++++
src/test/regress/sql/rpr.sql | 100 ++++++++++++
src/test/regress/sql/rpr_base.sql | 49 +++++-
src/test/regress/sql/rpr_explain.sql | 49 ++++++
22 files changed, 990 insertions(+), 89 deletions(-)
diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c
index dbed4f48a0f..af2e915701e 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;
diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c
index e41faa95be3..88c5e3b7635 100644
--- a/src/backend/executor/execExprInterp.c
+++ b/src/backend/executor/execExprInterp.c
@@ -5942,7 +5942,7 @@ ExecAggPlainTransByRef(AggState *aggstate, AggStatePerTrans pertrans,
}
/*
- * Evaluate RPR PREV/NEXT navigation: swap slot to target row.
+ * 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 +5963,36 @@ 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 unsigned offset. For 2-arg forms the offset expression
+ * has already been evaluated into offset_value. 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: 0 (standard 5.6.3)
*/
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 PREV/NEXT, 0 for FIRST/LAST */
+ if (op->d.rpr_nav.kind == RPR_NAV_FIRST ||
+ op->d.rpr_nav.kind == RPR_NAV_LAST)
+ offset = 0;
+ else
+ offset = 1;
+ }
/*
* Calculate target position based on navigation direction. On overflow,
@@ -5999,8 +6008,33 @@ 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;
}
+ /*
+ * 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 +6049,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 329cbfbaaaa..10e06a30f8e 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -609,17 +609,17 @@
* 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
*
* 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).
@@ -2997,12 +2997,63 @@ ExecRPRRecordContextFailure(WindowAggState *winstate, int64 failedLen)
* 2. Absorb redundant contexts - ideal timing after convergence
* 3. Advance all contexts (divergence) - create new states for next row
*/
+/*
+ * 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;
+}
+
void
ExecRPRProcessRow(WindowAggState *winstate, int64 currentPos,
bool hasLimitedFrame, int64 frameOffset)
{
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,7 +3080,24 @@ ExecRPRProcessRow(WindowAggState *winstate, int64 currentPos,
}
}
- nfa_match(winstate, ctx, varMatched);
+ /*
+ * 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);
+
+ /* Restore shared varMatched values for dependent variables */
+ nfa_reevaluate_dependent_vars(winstate, winstate->nfaContext,
+ currentPos);
+ }
+ else
+ {
+ nfa_match(winstate, ctx, varMatched);
+ }
ctx->lastProcessedRow = currentPos;
}
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 186029df1d9..867ff5068e3 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -938,9 +938,9 @@ eval_windowaggregates(WindowAggState *winstate)
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 RPR is used, navigation 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)
{
@@ -1279,7 +1279,7 @@ 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
@@ -2527,7 +2527,7 @@ 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 &&
@@ -2782,6 +2782,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;
}
/*
@@ -2991,11 +2992,14 @@ 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 max backward nav offset for tuplestore trim */
winstate->navMaxOffset = node->navMaxOffset;
if (winstate->navMaxOffset == RPR_NAV_OFFSET_NEEDS_EVAL)
eval_nav_max_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)
{
@@ -4215,8 +4219,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 */
@@ -4304,8 +4314,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/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index ee2d53b5924..c4c40d24cf2 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -292,6 +292,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,
@@ -2482,6 +2483,20 @@ nav_max_offset_walker(Node *node, int64 *maxOffset)
{
RPRNavExpr *nav = (RPRNavExpr *) node;
+ /*
+ * FIRST always references match_start, so we must retain the entire
+ * partition. LAST with an explicit offset can look back to
+ * match_start for the boundary check, so same treatment. LAST without
+ * offset always resolves to currentpos and never looks backward —
+ * no impact on trim.
+ */
+ if (nav->kind == RPR_NAV_FIRST ||
+ (nav->kind == RPR_NAV_LAST && nav->offset_arg != NULL))
+ {
+ *maxOffset = RPR_NAV_OFFSET_RETAIN_ALL;
+ return true; /* stop walking */
+ }
+
/* Only PREV looks backward; NEXT is irrelevant for trim */
if (nav->kind == RPR_NAV_PREV)
{
@@ -2538,9 +2553,10 @@ nav_max_offset_walker(Node *node, int64 *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.
+ * Returns the maximum constant offset found, RPR_NAV_OFFSET_NEEDS_EVAL if
+ * any PREV offset cannot be determined statically, or
+ * RPR_NAV_OFFSET_RETAIN_ALL if FIRST/LAST is present. NEXT offsets are
+ * ignored since they look forward and don't affect tuplestore trim.
*/
static int64
compute_nav_max_offset(List *defineClause)
@@ -2559,6 +2575,65 @@ compute_nav_max_offset(List *defineClause)
return maxOffset;
}
+/*
+ * has_match_start_dependency
+ * Check if an expression tree contains FIRST or LAST-with-offset,
+ * which depend on match_start and require per-context evaluation.
+ *
+ * LAST without offset always resolves to currentpos and is
+ * match_start-independent.
+ */
+static bool
+has_match_start_dependency(Node *node)
+{
+ 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;
+
+ /* Check children (arg may contain further nav expressions) */
+ return has_match_start_dependency((Node *) nav->arg);
+ }
+
+ return expression_tree_walker(node, has_match_start_dependency, NULL);
+}
+
+/*
+ * compute_match_start_dependent
+ * Build a Bitmapset of DEFINE variable indices whose expressions
+ * depend on match_start (contain FIRST or LAST-with-offset).
+ *
+ * Variables in this set require per-context re-evaluation during NFA
+ * processing, because different contexts may have different match_start
+ * values.
+ */
+static Bitmapset *
+compute_match_start_dependent(List *defineClause)
+{
+ Bitmapset *result = NULL;
+ ListCell *lc;
+ int varIdx = 0;
+
+ foreach(lc, defineClause)
+ {
+ TargetEntry *te = (TargetEntry *) lfirst(lc);
+
+ if (has_match_start_dependency((Node *) te->expr))
+ result = bms_add_member(result, varIdx);
+
+ varIdx++;
+ }
+
+ return result;
+}
+
/*
* create_windowagg_plan
*
@@ -2586,6 +2661,7 @@ create_windowagg_plan(PlannerInfo *root, WindowAggPath *best_path)
List *defineVariableList = NIL;
List *filteredDefineClause = NIL;
RPRPattern *compiledPattern = NULL;
+ Bitmapset *matchStartDependent = NULL;
/*
@@ -2648,11 +2724,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 +2750,7 @@ create_windowagg_plan(PlannerInfo *root, WindowAggPath *best_path)
wc->rpSkipTo,
compiledPattern,
filteredDefineClause,
+ matchStartDependent,
best_path->qual,
best_path->topwindow,
subplan);
@@ -6742,6 +6823,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,7 +6858,10 @@ make_windowagg(List *tlist, WindowClause *wc,
node->defineClause = defineClause;
- /* Compute max PREV offset for tuplestore trim optimization */
+ /* Store pre-computed match_start dependency bitmapset */
+ node->defineMatchStartDependent = defineMatchStartDependent;
+
+ /* Compute max backward nav offset for tuplestore trim optimization */
node->navMaxOffset = compute_nav_max_offset(defineClause);
plan->targetlist = tlist;
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..dab02fff4f2 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,13 +429,20 @@ 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) */
bool has_column_ref; /* Var found */
+ RPRNavKind inner_kind; /* kind of nested RPRNavExpr, if any */
} NavCheckResult;
static bool
@@ -446,7 +453,10 @@ nav_check_walker(Node *node, void *context)
if (node == NULL)
return false;
if (IsA(node, RPRNavExpr))
+ {
result->has_nav = true;
+ result->inner_kind = ((RPRNavExpr *) node)->kind;
+ }
if (IsA(node, Var))
result->has_column_ref = true;
@@ -457,16 +467,46 @@ 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)));
+ {
+ 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: valid compound navigation per
+ * SQL standard 5.6.4, but not yet implemented.
+ */
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("compound row pattern navigation is not yet supported"),
+ parser_errposition(pstate, nav->location)));
+ }
+ 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"),
+ parser_errposition(pstate, nav->location)));
+ }
+ else
+ {
+ /* Same-category nesting: prohibited */
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("row pattern navigation operations cannot be nested"),
+ parser_errposition(pstate, nav->location)));
+ }
+ }
if (!result.has_column_ref)
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
@@ -483,7 +523,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 742d889154b..e9cf49ca210 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -10109,9 +10109,24 @@ get_rule_expr(Node *node, deparse_context *context,
case T_RPRNavExpr:
{
RPRNavExpr *nav = (RPRNavExpr *) node;
+ const char *funcname;
- appendStringInfoString(buf,
- nav->kind == RPR_NAV_PREV ? "PREV(" : "NEXT(");
+ switch (nav->kind)
+ {
+ case RPR_NAV_PREV:
+ funcname = "PREV(";
+ break;
+ case RPR_NAV_NEXT:
+ funcname = "NEXT(";
+ break;
+ case RPR_NAV_FIRST:
+ funcname = "FIRST(";
+ break;
+ case RPR_NAV_LAST:
+ funcname = "LAST(";
+ break;
+ }
+ appendStringInfoString(buf, funcname);
get_rule_expr((Node *) nav->arg, context, showimplicit);
if (nav->offset_arg != NULL)
{
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..a34e1a8dfe6 100644
--- a/src/include/executor/execExpr.h
+++ b/src/include/executor/execExpr.h
@@ -699,7 +699,7 @@ typedef struct ExprEvalStep
struct
{
WindowAggState *winstate;
- RPRNavKind kind; /* PREV or NEXT */
+ RPRNavKind kind; /* PREV, NEXT, FIRST, or LAST */
Datum *offset_value; /* 2-arg: runtime offset value, or
* NULL */
bool *offset_isnull; /* 2-arg: runtime offset null flag */
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index ff6d7d70a60..d65cee1aefd 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,14 @@ typedef struct WindowAggState
TupleTableSlot *temp_slot_2;
/* RPR navigation */
- int64 navMaxOffset; /* max PREV offset; see RPR_NAV_OFFSET_* */
+ int64 navMaxOffset; /* max backward nav 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 */
+ 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/plannodes.h b/src/include/nodes/plannodes.h
index 27a2e7b48c7..a4b1e880fe4 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -1387,11 +1387,18 @@ typedef struct WindowAgg
List *defineClause;
/*
- * Maximum PREV offset for tuplestore mark optimization. >= 0: statically
- * determined max offset (mark = currentpos - offset).
+ * Bitmapset of DEFINE variable indices whose expressions depend on
+ * match_start (contain FIRST or LAST-with-offset). Variables in this set
+ * require per-context re-evaluation during NFA processing.
+ */
+ Bitmapset *defineMatchStartDependent;
+
+ /*
+ * Maximum backward navigation 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).
+ * (e.g., FIRST/LAST navigation).
*/
int64 navMaxOffset;
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index 94723a3b909..0e40fad6291 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -651,25 +651,28 @@ 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
+ * 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 which uses an implicit offset of 1
+ * the 1-arg form (implicit offset: 1 for PREV/NEXT, 0 for
+ * FIRST/LAST)
*/
typedef enum RPRNavKind
{
RPR_NAV_PREV,
RPR_NAV_NEXT,
+ RPR_NAV_FIRST,
+ RPR_NAV_LAST,
} RPRNavKind;
typedef struct RPRNavExpr
{
Expr xpr;
- RPRNavKind kind; /* PREV or NEXT */
+ RPRNavKind kind; /* PREV, NEXT, FIRST, or LAST */
Expr *arg; /* argument expression */
Expr *offset_arg; /* offset expression, or NULL for 1-arg form */
Oid resulttype; /* result type (same as arg's type) */
diff --git a/src/include/optimizer/rpr.h b/src/include/optimizer/rpr.h
index 91148f6c574..90e13ed7249 100644
--- a/src/include/optimizer/rpr.h
+++ b/src/include/optimizer/rpr.h
@@ -57,17 +57,19 @@
/*
* navMaxOffset sentinel values.
- * Non-negative values represent a statically determined maximum PREV offset.
+ * Non-negative values represent a statically determined maximum backward
+ * navigation offset (PREV).
*/
-#define RPR_NAV_OFFSET_NEEDS_EVAL (-1) /* has non-constant PREV offset;
+#define RPR_NAV_OFFSET_NEEDS_EVAL (-1) /* has non-constant offset;
* evaluate at executor init */
#define RPR_NAV_OFFSET_RETAIN_ALL (-2) /* must retain entire partition
- * (e.g., future FIRST/LAST) */
+ * (e.g., FIRST/LAST navigation) */
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 de6ce4fba8a..fb623ea152b 100644
--- a/src/test/regress/expected/rpr.out
+++ b/src/test/regress/expected/rpr.out
@@ -1040,7 +1040,7 @@ WINDOW w AS (
PATTERN (A)
DEFINE A AS price > PREV(PREV(price))
);
-ERROR: PREV and NEXT cannot be nested
+ERROR: row pattern navigation operations cannot be nested
LINE 7: DEFINE A AS price > PREV(PREV(price))
^
-- Nested NEXT
@@ -1052,7 +1052,7 @@ WINDOW w AS (
PATTERN (A)
DEFINE A AS price > NEXT(NEXT(price))
);
-ERROR: PREV and NEXT cannot be nested
+ERROR: row pattern navigation operations cannot be nested
LINE 7: DEFINE A AS price > NEXT(NEXT(price))
^
-- PREV nested inside NEXT
@@ -1064,7 +1064,7 @@ WINDOW w AS (
PATTERN (A)
DEFINE A AS price > NEXT(PREV(price))
);
-ERROR: PREV and NEXT cannot be nested
+ERROR: row pattern navigation operations cannot be nested
LINE 7: DEFINE A AS price > NEXT(PREV(price))
^
-- PREV nested inside expression inside NEXT
@@ -1076,7 +1076,7 @@ WINDOW w AS (
PATTERN (A)
DEFINE A AS price > NEXT(price * PREV(price))
);
-ERROR: PREV and NEXT cannot be nested
+ERROR: row pattern navigation operations cannot be nested
LINE 7: DEFINE A AS price > NEXT(price * PREV(price))
^
-- Triple nesting: error reported at outermost PREV
@@ -1088,7 +1088,7 @@ WINDOW w AS (
PATTERN (A)
DEFINE A AS price > PREV(PREV(PREV(price)))
);
-ERROR: PREV and NEXT cannot be nested
+ERROR: row pattern navigation operations cannot be nested
LINE 7: DEFINE A AS price > PREV(PREV(PREV(price)))
^
-- No column reference in PREV/NEXT argument
@@ -1137,7 +1137,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 +1149,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 +1442,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 +1452,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 +1462,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 +1474,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 +1489,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: two PREV with different offsets in same DEFINE clause
-- B: price exceeds both 1-back and 2-back values
@@ -1567,6 +1567,164 @@ 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 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: not yet supported
+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 PREV(FIRST(val), 1) > 0
+);
+ERROR: compound row pattern navigation is not yet supported
+LINE 5: DEFINE A AS TRUE, B AS PREV(FIRST(val), 1) > 0
+ ^
+-- 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
+ ^
+DROP TABLE rpr_nav;
--
-- SKIP TO / Backtracking / Frame boundary
--
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index 7452cf1b3ab..709d8f5d8e6 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
diff --git a/src/test/regress/expected/rpr_explain.out b/src/test/regress/expected/rpr_explain.out
index 4a646d1e1d8..83a035023ba 100644
--- a/src/test/regress/expected/rpr_explain.out
+++ b/src/test/regress/expected/rpr_explain.out
@@ -1086,6 +1086,89 @@ WINDOW w AS (
-> Function Scan on generate_series s (actual rows=42.00 loops=1)
(9 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
+ 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)
+(9 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
+ 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)
+
-- ============================================================
-- Match Length Statistics Tests
-- ============================================================
diff --git a/src/test/regress/sql/rpr.sql b/src/test/regress/sql/rpr.sql
index b3bbc8254c4..27929c47d7b 100644
--- a/src/test/regress/sql/rpr.sql
+++ b/src/test/regress/sql/rpr.sql
@@ -760,6 +760,106 @@ 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 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: not yet supported
+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 PREV(FIRST(val), 1) > 0
+);
+
+-- 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
+);
+
+DROP TABLE rpr_nav;
+
--
-- SKIP TO / Backtracking / Frame boundary
--
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index 8c23c7598a3..6ac248b3101 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;
-- ============================================================
diff --git a/src/test/regress/sql/rpr_explain.sql b/src/test/regress/sql/rpr_explain.sql
index aed2f69bc4f..9aed4f72070 100644
--- a/src/test/regress/sql/rpr_explain.sql
+++ b/src/test/regress/sql/rpr_explain.sql
@@ -692,6 +692,55 @@ 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
+);');
+
-- ============================================================
-- Match Length Statistics Tests
-- ============================================================
--
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 (20.1K, 10-nocfbot-0008-Replace-reduced-frame-map-with-single-match-result.txt)
download | inline diff:
From 24a19226ef1fe100fc1680b178103e997c0c48bb 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.
---
src/backend/executor/execRPR.c | 56 +++---
src/backend/executor/nodeWindowAgg.c | 221 ++++++++--------------
src/include/nodes/execnodes.h | 21 +-
src/test/regress/expected/rpr_explain.out | 8 +-
4 files changed, 126 insertions(+), 180 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..1c925e0221d 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -249,11 +249,8 @@ 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 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);
@@ -1037,12 +1034,6 @@ eval_windowaggregates(WindowAggState *winstate)
{
int ret;
-#ifdef RPR_DEBUG
- printf("===== loop in frame starts: aggregatedupto: " INT64_FORMAT " aggregatedbase: " INT64_FORMAT "\n",
- winstate->aggregatedupto,
- winstate->aggregatedbase);
-#endif
-
/* 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)));
@@ -3974,8 +3943,6 @@ row_is_in_reduced_frame(WindowObject winobj, int64 pos)
WindowAggState *winstate = winobj->winstate;
int state;
int rtn;
- int64 i;
- int num_reduced_rows;
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
- */
-static void
-create_reduced_frame_map(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);
-}
-
/*
- * clear_reduced_frame_map
- * Clear reduced frame map
+ * clear_reduced_frame
+ * Clear reduced frame status
*/
static void
-clear_reduced_frame_map(WindowAggState *winstate)
+clear_reduced_frame(WindowAggState *winstate)
{
- Assert(winstate->reduced_frame_map != NULL);
- MemSet(winstate->reduced_frame_map, RF_NOT_DETERMINED,
- winstate->alloc_sz);
+ winstate->rpr_match_valid = false;
+ winstate->rpr_match_matched = false;
+ winstate->rpr_match_start = -1;
+ winstate->rpr_match_length = 0;
}
/*
- * 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];
-}
-
-/*
- * 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);
+ /* Empty match: covers only the start position */
+ if (pos == start && winstate->rpr_match_matched && length == 0)
+ return RF_EMPTY_MATCH;
- 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 */
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 87986f98416ac0d145d654c65c5c8530ada78f3f 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-Implement-1-slot-PREV-NEXT-navigation-for-RPR.txt (82.0K, 13-nocfbot-0011-Implement-1-slot-PREV-NEXT-navigation-for-RPR.txt)
download
[text/plain] nocfbot-0012-JIT-support-for-PREV-NEXT.txt (7.0K, 14-nocfbot-0012-JIT-support-for-PREV-NEXT.txt)
download | inline diff:
From 56ec798cb75eaee3665c0e03a81ee1673e3f8a96 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-0013-Add-tuplestore-trim-optimization-for-RPR-PREV.txt (12.3K, 15-nocfbot-0013-Add-tuplestore-trim-optimization-for-RPR-PREV.txt)
download | inline diff:
From a8481774babb51b4d5527fab6c134987f9bb49c9 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 d2fffe0fce6..186029df1d9 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.
*/
@@ -2960,6 +2991,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)
@@ -3872,6 +3907,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 e78092678bb..91148f6c574 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-0014-Update-RPR-code-comments-for-1-slot-navigation.txt (5.5K, 16-nocfbot-0014-Update-RPR-code-comments-for-1-slot-navigation.txt)
download | inline diff:
From cddb155a9d06f3813f874105c7f66b802652586b 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 8f0457e2b3c..329cbfbaaaa 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] wip-0015-Implement-FIRST-LAST-navigation-for-RPR.txt (62.8K, 17-wip-0015-Implement-FIRST-LAST-navigation-for-RPR.txt)
download | inline diff:
From 33a8de8aeb8c022cac0204f0b1fc84bcdc63570e 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 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.
---
src/backend/executor/execExpr.c | 16 +-
src/backend/executor/execExprInterp.c | 54 +++++--
src/backend/executor/execRPR.c | 78 ++++++++-
src/backend/executor/nodeWindowAgg.c | 28 ++--
src/backend/optimizer/plan/createplan.c | 95 ++++++++++-
src/backend/optimizer/plan/rpr.c | 6 +-
src/backend/parser/parse_func.c | 70 ++++++--
src/backend/parser/parse_rpr.c | 56 ++++++-
src/backend/utils/adt/ruleutils.c | 19 ++-
src/backend/utils/adt/windowfuncs.c | 56 +++++++
src/include/catalog/pg_proc.dat | 12 ++
src/include/executor/execExpr.h | 2 +-
src/include/nodes/execnodes.h | 9 +-
src/include/nodes/plannodes.h | 13 +-
src/include/nodes/primnodes.h | 11 +-
src/include/optimizer/rpr.h | 10 +-
src/test/regress/expected/rpr.out | 186 ++++++++++++++++++++--
src/test/regress/expected/rpr_base.out | 77 ++++++++-
src/test/regress/expected/rpr_explain.out | 83 ++++++++++
src/test/regress/sql/rpr.sql | 100 ++++++++++++
src/test/regress/sql/rpr_base.sql | 49 +++++-
src/test/regress/sql/rpr_explain.sql | 49 ++++++
22 files changed, 990 insertions(+), 89 deletions(-)
diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c
index dbed4f48a0f..af2e915701e 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;
diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c
index e41faa95be3..88c5e3b7635 100644
--- a/src/backend/executor/execExprInterp.c
+++ b/src/backend/executor/execExprInterp.c
@@ -5942,7 +5942,7 @@ ExecAggPlainTransByRef(AggState *aggstate, AggStatePerTrans pertrans,
}
/*
- * Evaluate RPR PREV/NEXT navigation: swap slot to target row.
+ * 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 +5963,36 @@ 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 unsigned offset. For 2-arg forms the offset expression
+ * has already been evaluated into offset_value. 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: 0 (standard 5.6.3)
*/
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 PREV/NEXT, 0 for FIRST/LAST */
+ if (op->d.rpr_nav.kind == RPR_NAV_FIRST ||
+ op->d.rpr_nav.kind == RPR_NAV_LAST)
+ offset = 0;
+ else
+ offset = 1;
+ }
/*
* Calculate target position based on navigation direction. On overflow,
@@ -5999,8 +6008,33 @@ 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;
}
+ /*
+ * 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 +6049,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 329cbfbaaaa..10e06a30f8e 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -609,17 +609,17 @@
* 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
*
* 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).
@@ -2997,12 +2997,63 @@ ExecRPRRecordContextFailure(WindowAggState *winstate, int64 failedLen)
* 2. Absorb redundant contexts - ideal timing after convergence
* 3. Advance all contexts (divergence) - create new states for next row
*/
+/*
+ * 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;
+}
+
void
ExecRPRProcessRow(WindowAggState *winstate, int64 currentPos,
bool hasLimitedFrame, int64 frameOffset)
{
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,7 +3080,24 @@ ExecRPRProcessRow(WindowAggState *winstate, int64 currentPos,
}
}
- nfa_match(winstate, ctx, varMatched);
+ /*
+ * 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);
+
+ /* Restore shared varMatched values for dependent variables */
+ nfa_reevaluate_dependent_vars(winstate, winstate->nfaContext,
+ currentPos);
+ }
+ else
+ {
+ nfa_match(winstate, ctx, varMatched);
+ }
ctx->lastProcessedRow = currentPos;
}
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 186029df1d9..867ff5068e3 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -938,9 +938,9 @@ eval_windowaggregates(WindowAggState *winstate)
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 RPR is used, navigation 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)
{
@@ -1279,7 +1279,7 @@ 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
@@ -2527,7 +2527,7 @@ 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 &&
@@ -2782,6 +2782,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;
}
/*
@@ -2991,11 +2992,14 @@ 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 max backward nav offset for tuplestore trim */
winstate->navMaxOffset = node->navMaxOffset;
if (winstate->navMaxOffset == RPR_NAV_OFFSET_NEEDS_EVAL)
eval_nav_max_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)
{
@@ -4215,8 +4219,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 */
@@ -4304,8 +4314,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/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index ee2d53b5924..c4c40d24cf2 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -292,6 +292,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,
@@ -2482,6 +2483,20 @@ nav_max_offset_walker(Node *node, int64 *maxOffset)
{
RPRNavExpr *nav = (RPRNavExpr *) node;
+ /*
+ * FIRST always references match_start, so we must retain the entire
+ * partition. LAST with an explicit offset can look back to
+ * match_start for the boundary check, so same treatment. LAST without
+ * offset always resolves to currentpos and never looks backward —
+ * no impact on trim.
+ */
+ if (nav->kind == RPR_NAV_FIRST ||
+ (nav->kind == RPR_NAV_LAST && nav->offset_arg != NULL))
+ {
+ *maxOffset = RPR_NAV_OFFSET_RETAIN_ALL;
+ return true; /* stop walking */
+ }
+
/* Only PREV looks backward; NEXT is irrelevant for trim */
if (nav->kind == RPR_NAV_PREV)
{
@@ -2538,9 +2553,10 @@ nav_max_offset_walker(Node *node, int64 *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.
+ * Returns the maximum constant offset found, RPR_NAV_OFFSET_NEEDS_EVAL if
+ * any PREV offset cannot be determined statically, or
+ * RPR_NAV_OFFSET_RETAIN_ALL if FIRST/LAST is present. NEXT offsets are
+ * ignored since they look forward and don't affect tuplestore trim.
*/
static int64
compute_nav_max_offset(List *defineClause)
@@ -2559,6 +2575,65 @@ compute_nav_max_offset(List *defineClause)
return maxOffset;
}
+/*
+ * has_match_start_dependency
+ * Check if an expression tree contains FIRST or LAST-with-offset,
+ * which depend on match_start and require per-context evaluation.
+ *
+ * LAST without offset always resolves to currentpos and is
+ * match_start-independent.
+ */
+static bool
+has_match_start_dependency(Node *node)
+{
+ 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;
+
+ /* Check children (arg may contain further nav expressions) */
+ return has_match_start_dependency((Node *) nav->arg);
+ }
+
+ return expression_tree_walker(node, has_match_start_dependency, NULL);
+}
+
+/*
+ * compute_match_start_dependent
+ * Build a Bitmapset of DEFINE variable indices whose expressions
+ * depend on match_start (contain FIRST or LAST-with-offset).
+ *
+ * Variables in this set require per-context re-evaluation during NFA
+ * processing, because different contexts may have different match_start
+ * values.
+ */
+static Bitmapset *
+compute_match_start_dependent(List *defineClause)
+{
+ Bitmapset *result = NULL;
+ ListCell *lc;
+ int varIdx = 0;
+
+ foreach(lc, defineClause)
+ {
+ TargetEntry *te = (TargetEntry *) lfirst(lc);
+
+ if (has_match_start_dependency((Node *) te->expr))
+ result = bms_add_member(result, varIdx);
+
+ varIdx++;
+ }
+
+ return result;
+}
+
/*
* create_windowagg_plan
*
@@ -2586,6 +2661,7 @@ create_windowagg_plan(PlannerInfo *root, WindowAggPath *best_path)
List *defineVariableList = NIL;
List *filteredDefineClause = NIL;
RPRPattern *compiledPattern = NULL;
+ Bitmapset *matchStartDependent = NULL;
/*
@@ -2648,11 +2724,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 +2750,7 @@ create_windowagg_plan(PlannerInfo *root, WindowAggPath *best_path)
wc->rpSkipTo,
compiledPattern,
filteredDefineClause,
+ matchStartDependent,
best_path->qual,
best_path->topwindow,
subplan);
@@ -6742,6 +6823,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,7 +6858,10 @@ make_windowagg(List *tlist, WindowClause *wc,
node->defineClause = defineClause;
- /* Compute max PREV offset for tuplestore trim optimization */
+ /* Store pre-computed match_start dependency bitmapset */
+ node->defineMatchStartDependent = defineMatchStartDependent;
+
+ /* Compute max backward nav offset for tuplestore trim optimization */
node->navMaxOffset = compute_nav_max_offset(defineClause);
plan->targetlist = tlist;
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..dab02fff4f2 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,13 +429,20 @@ 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) */
bool has_column_ref; /* Var found */
+ RPRNavKind inner_kind; /* kind of nested RPRNavExpr, if any */
} NavCheckResult;
static bool
@@ -446,7 +453,10 @@ nav_check_walker(Node *node, void *context)
if (node == NULL)
return false;
if (IsA(node, RPRNavExpr))
+ {
result->has_nav = true;
+ result->inner_kind = ((RPRNavExpr *) node)->kind;
+ }
if (IsA(node, Var))
result->has_column_ref = true;
@@ -457,16 +467,46 @@ 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)));
+ {
+ 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: valid compound navigation per
+ * SQL standard 5.6.4, but not yet implemented.
+ */
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("compound row pattern navigation is not yet supported"),
+ parser_errposition(pstate, nav->location)));
+ }
+ 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"),
+ parser_errposition(pstate, nav->location)));
+ }
+ else
+ {
+ /* Same-category nesting: prohibited */
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("row pattern navigation operations cannot be nested"),
+ parser_errposition(pstate, nav->location)));
+ }
+ }
if (!result.has_column_ref)
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
@@ -483,7 +523,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 742d889154b..e9cf49ca210 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -10109,9 +10109,24 @@ get_rule_expr(Node *node, deparse_context *context,
case T_RPRNavExpr:
{
RPRNavExpr *nav = (RPRNavExpr *) node;
+ const char *funcname;
- appendStringInfoString(buf,
- nav->kind == RPR_NAV_PREV ? "PREV(" : "NEXT(");
+ switch (nav->kind)
+ {
+ case RPR_NAV_PREV:
+ funcname = "PREV(";
+ break;
+ case RPR_NAV_NEXT:
+ funcname = "NEXT(";
+ break;
+ case RPR_NAV_FIRST:
+ funcname = "FIRST(";
+ break;
+ case RPR_NAV_LAST:
+ funcname = "LAST(";
+ break;
+ }
+ appendStringInfoString(buf, funcname);
get_rule_expr((Node *) nav->arg, context, showimplicit);
if (nav->offset_arg != NULL)
{
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..a34e1a8dfe6 100644
--- a/src/include/executor/execExpr.h
+++ b/src/include/executor/execExpr.h
@@ -699,7 +699,7 @@ typedef struct ExprEvalStep
struct
{
WindowAggState *winstate;
- RPRNavKind kind; /* PREV or NEXT */
+ RPRNavKind kind; /* PREV, NEXT, FIRST, or LAST */
Datum *offset_value; /* 2-arg: runtime offset value, or
* NULL */
bool *offset_isnull; /* 2-arg: runtime offset null flag */
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index ff6d7d70a60..d65cee1aefd 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,14 @@ typedef struct WindowAggState
TupleTableSlot *temp_slot_2;
/* RPR navigation */
- int64 navMaxOffset; /* max PREV offset; see RPR_NAV_OFFSET_* */
+ int64 navMaxOffset; /* max backward nav 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 */
+ 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/plannodes.h b/src/include/nodes/plannodes.h
index 27a2e7b48c7..a4b1e880fe4 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -1387,11 +1387,18 @@ typedef struct WindowAgg
List *defineClause;
/*
- * Maximum PREV offset for tuplestore mark optimization. >= 0: statically
- * determined max offset (mark = currentpos - offset).
+ * Bitmapset of DEFINE variable indices whose expressions depend on
+ * match_start (contain FIRST or LAST-with-offset). Variables in this set
+ * require per-context re-evaluation during NFA processing.
+ */
+ Bitmapset *defineMatchStartDependent;
+
+ /*
+ * Maximum backward navigation 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).
+ * (e.g., FIRST/LAST navigation).
*/
int64 navMaxOffset;
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index 94723a3b909..0e40fad6291 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -651,25 +651,28 @@ 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
+ * 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 which uses an implicit offset of 1
+ * the 1-arg form (implicit offset: 1 for PREV/NEXT, 0 for
+ * FIRST/LAST)
*/
typedef enum RPRNavKind
{
RPR_NAV_PREV,
RPR_NAV_NEXT,
+ RPR_NAV_FIRST,
+ RPR_NAV_LAST,
} RPRNavKind;
typedef struct RPRNavExpr
{
Expr xpr;
- RPRNavKind kind; /* PREV or NEXT */
+ RPRNavKind kind; /* PREV, NEXT, FIRST, or LAST */
Expr *arg; /* argument expression */
Expr *offset_arg; /* offset expression, or NULL for 1-arg form */
Oid resulttype; /* result type (same as arg's type) */
diff --git a/src/include/optimizer/rpr.h b/src/include/optimizer/rpr.h
index 91148f6c574..90e13ed7249 100644
--- a/src/include/optimizer/rpr.h
+++ b/src/include/optimizer/rpr.h
@@ -57,17 +57,19 @@
/*
* navMaxOffset sentinel values.
- * Non-negative values represent a statically determined maximum PREV offset.
+ * Non-negative values represent a statically determined maximum backward
+ * navigation offset (PREV).
*/
-#define RPR_NAV_OFFSET_NEEDS_EVAL (-1) /* has non-constant PREV offset;
+#define RPR_NAV_OFFSET_NEEDS_EVAL (-1) /* has non-constant offset;
* evaluate at executor init */
#define RPR_NAV_OFFSET_RETAIN_ALL (-2) /* must retain entire partition
- * (e.g., future FIRST/LAST) */
+ * (e.g., FIRST/LAST navigation) */
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 de6ce4fba8a..fb623ea152b 100644
--- a/src/test/regress/expected/rpr.out
+++ b/src/test/regress/expected/rpr.out
@@ -1040,7 +1040,7 @@ WINDOW w AS (
PATTERN (A)
DEFINE A AS price > PREV(PREV(price))
);
-ERROR: PREV and NEXT cannot be nested
+ERROR: row pattern navigation operations cannot be nested
LINE 7: DEFINE A AS price > PREV(PREV(price))
^
-- Nested NEXT
@@ -1052,7 +1052,7 @@ WINDOW w AS (
PATTERN (A)
DEFINE A AS price > NEXT(NEXT(price))
);
-ERROR: PREV and NEXT cannot be nested
+ERROR: row pattern navigation operations cannot be nested
LINE 7: DEFINE A AS price > NEXT(NEXT(price))
^
-- PREV nested inside NEXT
@@ -1064,7 +1064,7 @@ WINDOW w AS (
PATTERN (A)
DEFINE A AS price > NEXT(PREV(price))
);
-ERROR: PREV and NEXT cannot be nested
+ERROR: row pattern navigation operations cannot be nested
LINE 7: DEFINE A AS price > NEXT(PREV(price))
^
-- PREV nested inside expression inside NEXT
@@ -1076,7 +1076,7 @@ WINDOW w AS (
PATTERN (A)
DEFINE A AS price > NEXT(price * PREV(price))
);
-ERROR: PREV and NEXT cannot be nested
+ERROR: row pattern navigation operations cannot be nested
LINE 7: DEFINE A AS price > NEXT(price * PREV(price))
^
-- Triple nesting: error reported at outermost PREV
@@ -1088,7 +1088,7 @@ WINDOW w AS (
PATTERN (A)
DEFINE A AS price > PREV(PREV(PREV(price)))
);
-ERROR: PREV and NEXT cannot be nested
+ERROR: row pattern navigation operations cannot be nested
LINE 7: DEFINE A AS price > PREV(PREV(PREV(price)))
^
-- No column reference in PREV/NEXT argument
@@ -1137,7 +1137,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 +1149,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 +1442,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 +1452,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 +1462,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 +1474,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 +1489,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: two PREV with different offsets in same DEFINE clause
-- B: price exceeds both 1-back and 2-back values
@@ -1567,6 +1567,164 @@ 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 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: not yet supported
+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 PREV(FIRST(val), 1) > 0
+);
+ERROR: compound row pattern navigation is not yet supported
+LINE 5: DEFINE A AS TRUE, B AS PREV(FIRST(val), 1) > 0
+ ^
+-- 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
+ ^
+DROP TABLE rpr_nav;
--
-- SKIP TO / Backtracking / Frame boundary
--
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index 7452cf1b3ab..709d8f5d8e6 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
diff --git a/src/test/regress/expected/rpr_explain.out b/src/test/regress/expected/rpr_explain.out
index 4a646d1e1d8..83a035023ba 100644
--- a/src/test/regress/expected/rpr_explain.out
+++ b/src/test/regress/expected/rpr_explain.out
@@ -1086,6 +1086,89 @@ WINDOW w AS (
-> Function Scan on generate_series s (actual rows=42.00 loops=1)
(9 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
+ 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)
+(9 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
+ 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)
+
-- ============================================================
-- Match Length Statistics Tests
-- ============================================================
diff --git a/src/test/regress/sql/rpr.sql b/src/test/regress/sql/rpr.sql
index b3bbc8254c4..27929c47d7b 100644
--- a/src/test/regress/sql/rpr.sql
+++ b/src/test/regress/sql/rpr.sql
@@ -760,6 +760,106 @@ 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 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: not yet supported
+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 PREV(FIRST(val), 1) > 0
+);
+
+-- 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
+);
+
+DROP TABLE rpr_nav;
+
--
-- SKIP TO / Backtracking / Frame boundary
--
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index 8c23c7598a3..6ac248b3101 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;
-- ============================================================
diff --git a/src/test/regress/sql/rpr_explain.sql b/src/test/regress/sql/rpr_explain.sql
index aed2f69bc4f..9aed4f72070 100644
--- a/src/test/regress/sql/rpr_explain.sql
+++ b/src/test/regress/sql/rpr_explain.sql
@@ -692,6 +692,55 @@ 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
+);');
+
-- ============================================================
-- Match Length Statistics Tests
-- ============================================================
--
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_zDEDtJEf6aO=x3qhPOs8kXmsha7Q8ROJXE8VZNp+Dxe2A@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