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]
Cc: [email protected]
Cc: [email protected]
Subject: Re: Row pattern recognition
Date: Sun, 21 Jun 2026 15:39:11 +0900
Message-ID: <CAAAe_zCsaf=WedELLjqLe3BV_8dWiO1DPDGA9sXj4qhe+=-XXw@mail.gmail.com> (raw)
In-Reply-To: <[email protected]>
References: <CAAAe_zBY0rrgf+tKXMUc-Y3nDDD69hddRBKopEKAZobhY=Cy-Q@mail.gmail.com>
<CAAAe_zDYxq0d3exCDwvKncD0kaL2uehDir6HXo4r5DXMitKrSg@mail.gmail.com>
<[email protected]>
<[email protected]>
Hi hackers,
This is an increment on top of v49: it lands the two fixes I left as
still-to-come there -- the DEFINE-evaluation use-after-free, now a dedicated
ExprContext, and the PREV/NEXT/FIRST/LAST namespace collision -- adds a
correctness fix found while reviewing the tuplestore spool (dormant
matches), and applies Jian He's and Tatsuo Ishii's review of the v48 series
as a set of mostly behavior-neutral commits.
Before the patch list, a note on CI: cfbot has been red here, but the
failure is not RPR -- it is the libLLVM 19 + ASAN JIT crash (CF 6870), which
reproduces on plain master. The build-system fix (exclude sanitizer flags
from JIT bitcode generation) is Matheus Alcantara's; the meson half is
v3-0001-Exclude-sanitizer-flags-from-LLVM-JIT-bitcode-gen.patch:
https://postgr.es/m/CAAAe_zBX5uV9K0ikuROLgdNvDCgGqHRskT-73L+oX9=3aXR2AQ@mail.gmail.com
For v50, what would you think about folding that meson patch in as a
temporary prerequisite at the front of the series, so cfbot's ASAN build
gets past the JIT crash and actually exercises RPR?
First, two cleanups splitting changes unrelated to RPR out of the feature
patch:
nocfbot-0001 Remove blank-line changes unrelated to row pattern
recognition
nocfbot-0002 Remove unnecessary includes from the row pattern
recognition patch
The fixes -- the two I left as still-to-come in v48, plus the dormant-match
fix found since (nocfbot-0003..0005, all behavior-changing):
nocfbot-0003 Recognize row pattern navigation operations by name in
DEFINE
The placeholder PREV/NEXT/FIRST/LAST pg_proc functions polluted the
ordinary function namespace and could be silently misbound to
same-named user functions. They are dropped; an unqualified
PREV/NEXT/FIRST/LAST inside a DEFINE clause is recognized as
navigation by name, while a schema-qualified call still reaches an
ordinary function. A navigation operation inside a navigation offset
-- which must be a run-time constant -- is now rejected instead of
crashing the planner. (f).prev follows attribute notation as an
ordinary function, the same inside and outside DEFINE.
nocfbot-0004 Use a dedicated ExprContext for RPR DEFINE clause
evaluation
nocfbot-0039's interim leak fix had turned into a use-after-free and
was voided in v48. DEFINE evaluation now has its own ExprContext,
reset once per row and separate from both the per-output-tuple context
and tmpcontext, resolving the leak and the use-after-free together.
nocfbot-0005 Drive RPR row pattern matching once per row
Matching only advanced when a window function read the frame, so a row
whose only window function skips the frame (e.g. nth_value() with a
NULL offset) left the match behind the current row -- silently wrong
results and a spurious "cannot fetch row ... before WindowObject's
mark
position" error.
The match is now driven once per row, before the window functions run;
the navigation mark advances from the frontier the match reached, so
the tuplestore is trimmed sooner.
Review of v48 (Jian He, and Tatsuo Ishii), as nocfbot-0006..0013 --
behavior-neutral except where tagged [behavior change]:
nocfbot-0006 Tidy up row pattern recognition plumbing
Remove the dead collectPatternVariables()/buildDefineVariableList()
helpers; drop the redundant rpSkipTo/defineClause arguments of
make_windowagg(); mark RPRNavExpr.resulttype query_jumble_ignore;
rename WindowAggState.defineClauseList to defineClauseExprs; assorted
block flattening. No change to planner or executor output.
nocfbot-0007 Further tidy up row pattern recognition plumbing
Drop the now-unused WindowClause argument of transformDefineClause();
use foreach_node()/foreach_current_index() in the DEFINE walkers and
drop their redundant end-of-list break tests; minor include and
comment fixups. No output change.
nocfbot-0008 Refactor transformDefineClause in row pattern recognition
[behavior change]
Hoist the "DEFINE variable not used in PATTERN" cross-check out of the
recursive walker into its caller, and reorder per-variable processing
to transformExpr -> coerce_to_boolean -> pull_var_clause, dropping the
separate second coercion pass. The only observable change is one
error-cursor position: the duplicate-variable error now points at the
later definition. New regression coverage for DEFINE coercion and Var
propagation is added.
nocfbot-0009 Replace a bare block with an else in the RPR DEFINE clause
walker
Cosmetic flattening of define_walker()'s phase dispatch into an
if / else if / else chain.
nocfbot-0010 Rename loop index variables in row pattern deparse helpers
Tatsuo's suggestion; descriptive names for the deparse index/loop
variables, no change to deparsed output.
nocfbot-0011 Rename absorption "judgment point" to "comparison point" in
comments
Comment and executor-README wording only; identifiers unchanged.
nocfbot-0012 Improve comments, documentation, and naming for row pattern
recognition
A batch of comment/doc clarity fixes -- the AST-vs-parse-tree wording,
the Run Condition EXPLAIN test, the contain_rpr_walker comment, the
ALT-marker and quantifier comments, the RPCommonSyntax.location "or
-1"
convention, and the transformDefineClause header -- plus renaming the
saturated-count sentinel RPR_COUNT_MAX to RPR_COUNT_INF for
consistency with RPR_QUANTITY_INF.
nocfbot-0013 Document eval_nav_offset_helper's NULL/negative offset
handling
Comment only. The NEEDS_EVAL offset branch is reachable (a Param
offset can be NULL or negative at run time), so it stays a graceful
return rather than an assertion.
For traceability, where each patch came from:
patch proposer proposal
------------- --------------
-------------------------------------------------
nocfbot-0001 Henson drop blank-line churn unrelated to RPR
nocfbot-0002 Henson drop unnecessary includes
nocfbot-0003 Henson nav namespace collision; (f).prev as
ordinary function
nocfbot-0004 Henson dedicated ExprContext for DEFINE evaluation
nocfbot-0005 Henson drive row pattern matching once per row
nocfbot-0006 Jian He tidy RPR plumbing
nocfbot-0007 Henson further plumbing tidy
nocfbot-0008 Jian He refactor transformDefineClause
nocfbot-0009 Jian He bare block -> else in the DEFINE walker
nocfbot-0010 Tatsuo Ishii rename deparse loop/index variables
nocfbot-0011 Jian He "judgment point" -> "comparison point"
wording
nocfbot-0012 Jian He comment/doc clarity batch + RPR_COUNT_INF
rename
nocfbot-0013 Jian He document eval_nav_offset NULL/negative
offset handling
Still to come -- Jian He's follow-ups on this thread (2026-06-19), to fold
once each approach is agreed (all under review):
topic proposed change
----------------------------- -------------------------------------------
DEFINE qualified column-ref make the differing pg_temp.t.c /
error messages public.t.c / t.c messages consistent, on
the standard "invalid reference to
FROM-clause entry for table"
validateRPRPatternVarCount rename to preprocessRPRPattern
quantifier INF bound abstract behind an RPR_QUANTITY_INF macro
"nullable" variables rename to match_empty
splitRPRTrailingAlt use foreach_node / foreach_current_index
buildRPRPattern signature pass the WindowClause directly
collectDefineVariables / inline and drop the helpers
tryUnwrapSingleChild
variable-limit error msg fold the maximum into the primary message
Quality work to run alongside, on Linux: Valgrind (leak and use-after-free,
to exercise the DEFINE ExprContext fix, whose reproducer is cassert-only)
and standing gcov coverage to catch untested planner paths.
Longer term, and out of scope for this CF entry:
- Smaller documentation and error-message follow-ups (glossary, the
bounded-quantifier message, README wording).
- Short-circuit / tri-state DEFINE evaluation, as a separate series.
- SEEK clause support (SQL:2016).
- Empty pattern PATTERN () -- correctly rejected today; deferred because
its empty-match semantics (SHOW/OMIT EMPTY MATCHES) are tied to the
still-out-of-scope MEASURES clause.
- Relaxing the DEFINE-subquery over-rejection where the standard permits
it (the subquery does no RPR of its own and makes no outer reference).
- Prefix-pattern absorption (an optimization; designed and intentionally
split out as its own series).
- R010 (MATCH_RECOGNIZE in the FROM clause) and the shared RPRContext it
would back.
Please let me know if any of the slicing or grouping looks off.
On a personal note, some fatigue has built up, so I'll be easing off the
pace a little for a while and may be slower to follow up on this thread
than I have been. The work continues -- just at a gentler pace.
Thanks again to Jian and Tatsuo for the careful review.
Best regards,
Henson
From 9d58cdf40d78efdb35131279a006b618213843db Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Tue, 16 Jun 2026 23:26:16 +0900
Subject: [PATCH 01/13] Remove blank-line changes unrelated to row pattern
recognition
The RPR patch added or dropped blank lines in five existing files with no
related code change. Revert them so the files differ from the base only
where RPR actually touches them.
---
src/backend/executor/nodeWindowAgg.c | 2 --
src/backend/optimizer/plan/setrefs.c | 1 +
src/backend/parser/parse_clause.c | 1 +
src/backend/utils/adt/ruleutils.c | 1 -
src/backend/utils/adt/windowfuncs.c | 1 +
5 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index cb6a484b7de..5b2385f1d8d 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -177,7 +177,6 @@ typedef struct WindowStatePerAggData
bool restart; /* need to restart this agg in this cycle? */
} WindowStatePerAggData;
-
static void initialize_windowaggregate(WindowAggState *winstate,
WindowStatePerFunc perfuncstate,
WindowStatePerAgg peraggstate);
@@ -1086,7 +1085,6 @@ next_tuple:
ExecClearTuple(agg_row_slot);
}
-
/* The frame's end is not supposed to move backwards, ever */
Assert(aggregatedupto_nonrestarted <= winstate->aggregatedupto);
diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c
index 813a326bd78..6e4f3fd61e2 100644
--- a/src/backend/optimizer/plan/setrefs.c
+++ b/src/backend/optimizer/plan/setrefs.c
@@ -214,6 +214,7 @@ static List *set_windowagg_runcondition_references(PlannerInfo *root,
static void record_elided_node(PlannerGlobal *glob, int plan_node_id,
NodeTag elided_type, Bitmapset *relids);
+
/*****************************************************************************
*
* SUBPLAN REFERENCES
diff --git a/src/backend/parser/parse_clause.c b/src/backend/parser/parse_clause.c
index 550ea4eb9c0..8eb367aa579 100644
--- a/src/backend/parser/parse_clause.c
+++ b/src/backend/parser/parse_clause.c
@@ -102,6 +102,7 @@ static Node *transformFrameOffset(ParseState *pstate, int frameOptions,
Oid rangeopfamily, Oid rangeopcintype, Oid *inRangeFunc,
Node *clause);
+
/*
* transformFromClause -
* Process the FROM clause and add items to the query's range table,
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 415da6417d4..4eb7e35bee4 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -7311,7 +7311,6 @@ get_rule_windowspec(WindowClause *wc, List *targetList,
get_rule_orderby(wc->orderClause, targetList, false, context);
needspace = true;
}
-
/* framing clause is never inherited, so print unless it's default */
if (wc->frameOptions & FRAMEOPTION_NONDEFAULT)
{
diff --git a/src/backend/utils/adt/windowfuncs.c b/src/backend/utils/adt/windowfuncs.c
index 3869f6c8994..d15aa0c75db 100644
--- a/src/backend/utils/adt/windowfuncs.c
+++ b/src/backend/utils/adt/windowfuncs.c
@@ -41,6 +41,7 @@ static bool rank_up(WindowObject winobj);
static Datum leadlag_common(FunctionCallInfo fcinfo,
bool forward, bool withoffset, bool withdefault);
+
/*
* utility routine for *_rank functions.
*/
--
2.50.1 (Apple Git-155)
From 7f135d9dee30c6c4c86f470a97996880b0f6ece2 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Mon, 15 Jun 2026 15:29:59 +0900
Subject: [PATCH 03/13] Recognize row pattern navigation operations by name in
DEFINE
PREV, NEXT, FIRST, and LAST were placeholder functions in pg_proc that
polluted the ordinary function namespace and could be silently misbound to
same-named user functions. Recognize them by name inside a DEFINE clause
and drop the placeholders; an unqualified call is always navigation, while
a schema-qualified call still reaches an ordinary function. Also reject a
navigation operation inside a navigation offset, which must be a run-time
constant and previously could crash the planner.
Recognition happens in two steps in ParseFuncOrColumn: note the matched
name up front, skip the catalog lookup by treating it as FUNCDETAIL_NORMAL,
let the common decoration and wrong-kind-of-routine checks run, and only
then route to ParseRPRNavCall to build the RPRNavExpr. ParseRPRNavCall
therefore does not duplicate the aggregate/window decoration checks
(agg_star, DISTINCT, WITHIN GROUP, ORDER BY, FILTER, OVER); the common path
performs them with identical messages.
Document this in func-window.sgml.
---
doc/src/sgml/func/func-window.sgml | 6 +
src/backend/parser/parse_func.c | 296 ++++++++++-----
src/backend/parser/parse_rpr.c | 15 +-
src/backend/utils/adt/ruleutils.c | 55 ++-
src/backend/utils/adt/windowfuncs.c | 118 ------
src/include/catalog/pg_proc.dat | 24 --
src/test/regress/expected/rpr.out | 32 --
src/test/regress/expected/rpr_base.out | 492 ++++++++++++++++++++++++-
src/test/regress/sql/rpr.sql | 15 -
src/test/regress/sql/rpr_base.sql | 251 +++++++++++++
10 files changed, 1007 insertions(+), 297 deletions(-)
diff --git a/doc/src/sgml/func/func-window.sgml b/doc/src/sgml/func/func-window.sgml
index ab469b56fd7..1079b6abb6e 100644
--- a/doc/src/sgml/func/func-window.sgml
+++ b/doc/src/sgml/func/func-window.sgml
@@ -282,6 +282,10 @@ IGNORE NULLS
Row Pattern Recognition navigation functions are listed in
<xref linkend="functions-rpr-navigation-table"/>. These functions
can be used to describe the DEFINE clause of Row Pattern Recognition.
+ The names <function>PREV</function>, <function>NEXT</function>,
+ <function>FIRST</function>, and <function>LAST</function> are
+ recognized as navigation functions only in an unqualified call; a
+ schema-qualified call is resolved as an ordinary function instead.
</para>
<table id="functions-rpr-navigation-table">
@@ -397,6 +401,8 @@ IGNORE NULLS
permitted. Same-category nesting (e.g.,
<function>PREV</function> inside <function>PREV</function>) is also
prohibited.
+ The <parameter>offset</parameter> argument must be a run-time constant:
+ it cannot reference columns or contain a navigation operation.
</para>
<note>
diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c
index 1f6c8fa4fb2..f3b37aa992c 100644
--- a/src/backend/parser/parse_func.c
+++ b/src/backend/parser/parse_func.c
@@ -31,7 +31,6 @@
#include "parser/parse_target.h"
#include "parser/parse_type.h"
#include "utils/builtins.h"
-#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/syscache.h"
@@ -49,6 +48,9 @@ static void unify_hypothetical_args(ParseState *pstate,
List *fargs, int numAggregatedArgs,
Oid *actual_arg_types, Oid *declared_arg_types);
static Oid FuncNameAsType(List *funcname);
+static Node *ParseRPRNavCall(ParseState *pstate, List *funcname,
+ List *fargs, List *argnames, FuncCall *fn,
+ int location);
static Node *ParseComplexProjection(ParseState *pstate, const char *funcname,
Node *first_arg, int location);
static Oid LookupFuncNameInternal(ObjectType objtype, List *funcname,
@@ -122,6 +124,7 @@ ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs,
int fgc_flags;
char aggkind = 0;
ParseCallbackState pcbstate;
+ bool could_be_rpr_nav = false;
/*
* If there's an aggregate filter, transform it using transformWhereClause
@@ -218,6 +221,28 @@ ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs,
Assert(first_arg != NULL);
}
+ /*
+ * Inside an RPR DEFINE clause, an unqualified call to one of the row
+ * pattern navigation names PREV/NEXT/FIRST/LAST denotes the navigation
+ * operation, not an ordinary function. Just note that here; the catalog
+ * lookup is skipped and the RPRNavExpr is built at the end, after the
+ * common decoration checks have run (see the could_be_rpr_nav handling
+ * below). A schema-qualified call is the explicit way to reach an
+ * ordinary function of one of these names.
+ */
+ if (!is_column && !proc_call &&
+ pstate->p_expr_kind == EXPR_KIND_RPR_DEFINE &&
+ list_length(funcname) == 1)
+ {
+ const char *name = strVal(linitial(funcname));
+
+ if (strcmp(name, "prev") == 0 ||
+ strcmp(name, "next") == 0 ||
+ strcmp(name, "first") == 0 ||
+ strcmp(name, "last") == 0)
+ could_be_rpr_nav = true;
+ }
+
/*
* Decide whether it's legitimate to consider the construct to be a column
* projection. For that, there has to be a single argument of complex
@@ -266,17 +291,32 @@ ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs,
* with default arguments.
*/
- setup_parser_errposition_callback(&pcbstate, pstate, location);
+ if (!could_be_rpr_nav)
+ {
+ setup_parser_errposition_callback(&pcbstate, pstate, location);
+
+ fdresult = func_get_detail(funcname, fargs, argnames, nargs,
+ actual_arg_types,
+ !func_variadic, true, proc_call,
+ &fgc_flags,
+ &funcid, &rettype, &retset,
+ &nvargs, &vatype,
+ &declared_arg_types, &argdefaults);
- fdresult = func_get_detail(funcname, fargs, argnames, nargs,
- actual_arg_types,
- !func_variadic, true, proc_call,
- &fgc_flags,
- &funcid, &rettype, &retset,
- &nvargs, &vatype,
- &declared_arg_types, &argdefaults);
+ cancel_parser_errposition_callback(&pcbstate);
+ }
+ else
+ {
+ /*
+ * A recognized navigation name skips catalog lookup entirely. Treat
+ * it as an ordinary function so the common wrong-kind-of-routine and
+ * decoration checks below run with the existing messages, then route
+ * to ParseRPRNavCall to build the RPRNavExpr.
+ */
+ Assert(!proc_call);
- cancel_parser_errposition_callback(&pcbstate);
+ fdresult = FUNCDETAIL_NORMAL;
+ }
/*
* Check for various wrong-kind-of-routine cases.
@@ -653,6 +693,15 @@ ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs,
parser_errposition(pstate, location)));
}
+ /*
+ * A recognized navigation name has now passed the common decoration and
+ * wrong-kind checks above; build the RPRNavExpr. No fallback to function
+ * resolution ever happens here.
+ */
+ if (could_be_rpr_nav)
+ return ParseRPRNavCall(pstate, funcname, fargs, argnames, fn,
+ location);
+
/*
* If there are default arguments, we have to include their types in
* actual_arg_types for the purpose of checking generic type consistency.
@@ -759,88 +808,8 @@ ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs,
if (retset)
check_srf_call_placement(pstate, last_srf, location);
- /*
- * 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_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("cannot use %s outside 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_FIRST_ANYELEMENT || funcid == F_LAST_ANYELEMENT ||
- funcid == F_FIRST_ANYELEMENT_INT8 || funcid == F_LAST_ANYELEMENT_INT8))
- {
- /*
- * 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.
- */
- 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 = kind;
- 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)
+ if (fdresult == FUNCDETAIL_NORMAL || fdresult == FUNCDETAIL_PROCEDURE)
{
FuncExpr *funcexpr = makeNode(FuncExpr);
@@ -2111,6 +2080,151 @@ FuncNameAsType(List *funcname)
return result;
}
+/*
+ * ParseRPRNavCall
+ * Recognize a row pattern navigation operation in a DEFINE clause.
+ *
+ * Inside an EXPR_KIND_RPR_DEFINE clause an unqualified call to one of the
+ * names PREV/NEXT/FIRST/LAST denotes the corresponding row pattern navigation
+ * operation (ISO/IEC 19075-5 Subclause 5.6), not an ordinary function call.
+ * The name is matched here, before any catalog lookup, with no fallback to
+ * function resolution: once it matches, decoration and argument-count
+ * violations are dedicated errors rather than letting an ordinary function of
+ * the same name take over. A schema-qualified call (the caller restricts us
+ * to unqualified names) is the documented way to reach such a function
+ * instead.
+ *
+ * The caller routes here only after the name has matched one of the four
+ * navigation names and the common decoration/wrong-kind checks in
+ * ParseFuncOrColumn have run, so this always returns an RPRNavExpr.
+ */
+static Node *
+ParseRPRNavCall(ParseState *pstate, List *funcname, List *fargs,
+ List *argnames, FuncCall *fn, int location)
+{
+ const char *name = strVal(linitial(funcname));
+ RPRNavKind kind;
+ const char *navname;
+ int nargs = list_length(fargs);
+ Node *arg;
+ RPRNavExpr *navexpr;
+
+ /* match the parser-downcased identifier; otherwise not a navigation name */
+ if (strcmp(name, "prev") == 0)
+ {
+ kind = RPR_NAV_PREV;
+ navname = "PREV";
+ }
+ else if (strcmp(name, "next") == 0)
+ {
+ kind = RPR_NAV_NEXT;
+ navname = "NEXT";
+ }
+ else if (strcmp(name, "first") == 0)
+ {
+ kind = RPR_NAV_FIRST;
+ navname = "FIRST";
+ }
+ else if (strcmp(name, "last") == 0)
+ {
+ kind = RPR_NAV_LAST;
+ navname = "LAST";
+ }
+ else
+ {
+ /* the caller only routes here after matching one of the four names */
+ pg_unreachable();
+ return NULL;
+ }
+
+ /*
+ * Once the name matches we never fall back to function resolution, so any
+ * decoration that does not make sense for a navigation operation is a
+ * hard error. The aggregate/window decorations (agg_star, DISTINCT,
+ * WITHIN GROUP, ORDER BY, FILTER, OVER, RESPECT/IGNORE NULLS) are already
+ * rejected by the common path in ParseFuncOrColumn, which treated the
+ * recognized name as an ordinary function; what remains are the
+ * decorations that path accepts for a plain function but a navigation
+ * operation must still reject.
+ */
+ if (fn->func_variadic)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("cannot use VARIADIC with row pattern navigation function %s",
+ navname),
+ parser_errposition(pstate, location)));
+ if (argnames != NIL)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("row pattern navigation operations cannot use named arguments"),
+ parser_errposition(pstate, location)));
+ /* takes a value expression and an optional offset */
+ if (nargs == 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("too few arguments for row pattern navigation function %s",
+ navname),
+ errdetail("%s takes a value expression and an optional offset argument.",
+ navname),
+ parser_errposition(pstate, location)));
+ if (nargs > 2)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("too many arguments for row pattern navigation function %s",
+ navname),
+ errdetail("%s takes a value expression and an optional offset argument.",
+ navname),
+ parser_errposition(pstate, location)));
+
+ /*
+ * Resolve a still-unknown first argument to text, the same way the
+ * anycompatible family does. A navigation operation is not a polymorphic
+ * function, so the old "could not determine polymorphic type" error does
+ * not apply; an unknown literal cannot contain a column reference, so the
+ * walker still rejects it later.
+ */
+ arg = linitial(fargs);
+ if (exprType(arg) == UNKNOWNOID)
+ arg = coerce_to_common_type(pstate, arg, TEXTOID, navname);
+
+ navexpr = makeNode(RPRNavExpr);
+ navexpr->kind = kind;
+ navexpr->arg = (Expr *) arg;
+
+ /* an explicit offset is coerced to int8, which the executor reads */
+ if (nargs == 2)
+ {
+ Node *offset = lsecond(fargs);
+ Oid offtype = exprType(offset);
+
+ if (offtype != INT8OID)
+ {
+ Node *newoffset;
+
+ newoffset = coerce_to_target_type(pstate, offset, offtype,
+ INT8OID, -1, COERCION_IMPLICIT,
+ COERCE_IMPLICIT_CAST, -1);
+ if (newoffset == NULL)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATATYPE_MISMATCH),
+ errmsg("offset argument of %s must be type %s, not type %s",
+ navname, "bigint", format_type_be(offtype)),
+ parser_errposition(pstate, exprLocation(offset))));
+ offset = newoffset;
+ }
+ navexpr->offset_arg = (Expr *) offset;
+ }
+ else
+ navexpr->offset_arg = NULL;
+
+ /* compound_offset_arg stays NULL; define_walker flattening fills it in */
+ navexpr->resulttype = exprType(arg);
+ /* resultcollid will be set by parse_collate.c */
+ navexpr->location = location;
+
+ return (Node *) navexpr;
+}
+
/*
* ParseComplexProjection -
* handles function calls with a single argument that is of complex type.
diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index 3eaea2be750..8ed01bb8f28 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -472,6 +472,7 @@ transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
* * PREV/NEXT wrapping FIRST/LAST flattens to a compound kind
* * Other nestings are rejected (FIRST(PREV()), PREV(PREV()), ...)
* * offset_arg / compound_offset_arg must not contain column refs
+ * or nested navigation operations
*
* Volatile callees (and sequence operations) are rejected later in the
* planner via validate_rpr_define_volatility(); see optimizer/plan/rpr.c.
@@ -482,7 +483,7 @@ transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
* walks nav.arg in PHASE_NAV_ARG to collect nesting/column-ref state,
* applies compound flatten or raises a nesting error, then walks the
* (post-flatten) offset(s) in PHASE_NAV_OFFSET to enforce the
- * constant-offset rule. No subtree is walked twice.
+ * constant-offset and no-nested-nav rules. No subtree is walked twice.
*/
/*
@@ -498,6 +499,7 @@ transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
* PREV(PREV()), FIRST(FIRST()), three-or-more deep)
* [2] for each nav offset (PHASE_NAV_OFFSET):
* - must be a run-time constant (no column references)
+ * - must not contain a row pattern navigation operation
*
* Var sightings feed the column-ref rule for the enclosing nav scope;
* RPRNavExpr sightings inside PHASE_NAV_ARG feed the nesting decision.
@@ -538,11 +540,14 @@ define_walker(Node *node, void *context)
if (ctx->phase == DEFINE_PHASE_NAV_OFFSET)
{
/*
- * Navs inside offset_arg are unusual but not directly banned; the
- * constant-offset rule will catch any Var or volatile they
- * contain.
+ * A navigation offset must be a run-time constant, so it cannot
+ * contain a navigation operation.
*/
- return expression_tree_walker(node, define_walker, ctx);
+ ereport(ERROR,
+ errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("row pattern navigation offset cannot contain a row pattern navigation operation"),
+ errdetail("A navigation offset must be a run-time constant."),
+ parser_errposition(ctx->pstate, nav->location));
}
/*
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 4eb7e35bee4..2b7fd7367f3 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -127,6 +127,7 @@ typedef struct
bool varprefix; /* true to print prefixes on Vars */
bool colNamesVisible; /* do we care about output column names? */
bool inGroupBy; /* deparsing GROUP BY clause? */
+ bool inRPRDefine; /* deparsing an RPR DEFINE clause? */
bool varInOrderBy; /* deparsing simple Var in ORDER BY? */
Bitmapset *appendparents; /* if not null, map child Vars of these relids
* back to the parent rel */
@@ -545,7 +546,7 @@ static char *generate_qualified_relation_name(Oid relid);
static char *generate_function_name(Oid funcid, int nargs,
List *argnames, Oid *argtypes,
bool has_variadic, bool *use_variadic_p,
- bool inGroupBy);
+ bool inGroupBy, bool inRPRDefine);
static char *generate_operator_name(Oid operid, Oid arg1, Oid arg2);
static void add_cast_to(StringInfo buf, Oid typid);
static char *generate_qualified_type_name(Oid typid);
@@ -1131,6 +1132,7 @@ pg_get_triggerdef_worker(Oid trigid, bool pretty)
context.indentLevel = PRETTYINDENT_STD;
context.colNamesVisible = true;
context.inGroupBy = false;
+ context.inRPRDefine = false;
context.varInOrderBy = false;
context.appendparents = NULL;
@@ -1142,7 +1144,7 @@ pg_get_triggerdef_worker(Oid trigid, bool pretty)
appendStringInfo(&buf, "EXECUTE FUNCTION %s(",
generate_function_name(trigrec->tgfoid, 0,
NIL, NULL,
- false, NULL, false));
+ false, NULL, false, false));
if (trigrec->tgnargs > 0)
{
@@ -3401,7 +3403,7 @@ pg_get_functiondef(PG_FUNCTION_ARGS)
appendStringInfo(&buf, " SUPPORT %s",
generate_function_name(proc->prosupport, 1,
NIL, argtypes,
- false, NULL, false));
+ false, NULL, false, false));
}
if (oldlen != buf.len)
@@ -4054,6 +4056,7 @@ deparse_expression_pretty(Node *expr, List *dpcontext,
context.indentLevel = startIndent;
context.colNamesVisible = true;
context.inGroupBy = false;
+ context.inRPRDefine = false;
context.varInOrderBy = false;
context.appendparents = NULL;
@@ -5849,6 +5852,7 @@ make_ruledef(StringInfo buf, HeapTuple ruletup, TupleDesc rulettc,
context.indentLevel = PRETTYINDENT_STD;
context.colNamesVisible = true;
context.inGroupBy = false;
+ context.inRPRDefine = false;
context.varInOrderBy = false;
context.appendparents = NULL;
@@ -6041,6 +6045,7 @@ get_query_def(Query *query, StringInfo buf, List *parentnamespace,
context.indentLevel = startIndent;
context.colNamesVisible = colNamesVisible;
context.inGroupBy = false;
+ context.inRPRDefine = false;
context.varInOrderBy = false;
context.appendparents = NULL;
@@ -7220,15 +7225,25 @@ get_rule_define(List *defineClause, deparse_context *context)
{
StringInfo buf = context->buf;
const char *sep;
+ bool save_inrprdefine = context->inRPRDefine;
sep = " ";
+ /*
+ * Within the DEFINE clause an unqualified prev/next/first/last is a
+ * navigation operation, so a user function of one of those names must be
+ * schema-qualified to survive a reparse; see generate_function_name().
+ */
+ context->inRPRDefine = true;
+
foreach_node(TargetEntry, te, defineClause)
{
appendStringInfo(buf, "%s%s AS ", sep, quote_identifier(te->resname));
get_rule_expr((Node *) te->expr, context, false);
sep = ",\n ";
}
+
+ context->inRPRDefine = save_inrprdefine;
}
/*
@@ -7459,6 +7474,7 @@ get_window_frame_options_for_explain(int frameOptions,
context.indentLevel = 0;
context.colNamesVisible = true;
context.inGroupBy = false;
+ context.inRPRDefine = false;
context.varInOrderBy = false;
context.appendparents = NULL;
@@ -11691,7 +11707,8 @@ get_func_expr(FuncExpr *expr, deparse_context *context,
argnames, argtypes,
expr->funcvariadic,
&use_variadic,
- context->inGroupBy));
+ context->inGroupBy,
+ context->inRPRDefine));
nargs = 0;
foreach(l, expr->args)
{
@@ -11761,7 +11778,8 @@ get_agg_expr_helper(Aggref *aggref, deparse_context *context,
funcname = generate_function_name(aggref->aggfnoid, nargs, NIL,
argtypes, aggref->aggvariadic,
&use_variadic,
- context->inGroupBy);
+ context->inGroupBy,
+ context->inRPRDefine);
/* Print the aggregate name, schema-qualified if needed */
appendStringInfo(buf, "%s(%s", funcname,
@@ -11902,7 +11920,8 @@ get_windowfunc_expr_helper(WindowFunc *wfunc, deparse_context *context,
if (!funcname)
funcname = generate_function_name(wfunc->winfnoid, nargs, argnames,
argtypes, false, NULL,
- context->inGroupBy);
+ context->inGroupBy,
+ context->inRPRDefine);
appendStringInfo(buf, "%s(", funcname);
@@ -13743,7 +13762,7 @@ get_tablesample_def(TableSampleClause *tablesample, deparse_context *context)
appendStringInfo(buf, " TABLESAMPLE %s (",
generate_function_name(tablesample->tsmhandler, 1,
NIL, argtypes,
- false, NULL, false));
+ false, NULL, false, false));
nargs = 0;
foreach(l, tablesample->args)
@@ -14157,12 +14176,14 @@ generate_qualified_relation_name(Oid relid)
*
* inGroupBy must be true if we're deparsing a GROUP BY clause.
*
+ * inRPRDefine must be true if we're deparsing an RPR DEFINE clause.
+ *
* The result includes all necessary quoting and schema-prefixing.
*/
static char *
generate_function_name(Oid funcid, int nargs, List *argnames, Oid *argtypes,
bool has_variadic, bool *use_variadic_p,
- bool inGroupBy)
+ bool inGroupBy, bool inRPRDefine)
{
char *result;
HeapTuple proctup;
@@ -14196,6 +14217,24 @@ generate_function_name(Oid funcid, int nargs, List *argnames, Oid *argtypes,
force_qualify = true;
}
+ /*
+ * Inside a row pattern DEFINE clause, the parser binds an unqualified
+ * prev/next/first/last to a navigation operation before any catalog
+ * lookup, so an unqualified call to a user function of one of those names
+ * would change meaning across a deparse/reparse cycle. Force schema
+ * qualification; the qualified form is the documented escape hatch. Only
+ * the exact lower-case names are at risk: a mixed-case proname deparses
+ * quoted and cannot match the parser's downcased comparison.
+ */
+ if (inRPRDefine)
+ {
+ if (strcmp(proname, "prev") == 0 ||
+ strcmp(proname, "next") == 0 ||
+ strcmp(proname, "first") == 0 ||
+ strcmp(proname, "last") == 0)
+ force_qualify = true;
+ }
+
/*
* Determine whether VARIADIC should be printed. We must do this first
* since it affects the lookup rules in func_get_detail().
diff --git a/src/backend/utils/adt/windowfuncs.c b/src/backend/utils/adt/windowfuncs.c
index d15aa0c75db..78b7f05aba2 100644
--- a/src/backend/utils/adt/windowfuncs.c
+++ b/src/backend/utils/adt/windowfuncs.c
@@ -724,121 +724,3 @@ window_nth_value(PG_FUNCTION_ARGS)
PG_RETURN_DATUM(result);
}
-
-/*
- * prev
- * 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)
-{
- ereport(ERROR,
- errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("cannot use PREV() outside a DEFINE clause"));
- PG_RETURN_NULL(); /* not reached */
-}
-
-/*
- * next
- * Catalog placeholder for RPR's NEXT navigation operator.
- * See window_prev() for details.
- */
-Datum
-window_next(PG_FUNCTION_ARGS)
-{
- ereport(ERROR,
- errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("cannot use NEXT() outside 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("cannot use PREV() outside 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("cannot use NEXT() outside 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("cannot use FIRST() outside 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("cannot use LAST() outside 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("cannot use FIRST() outside 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("cannot use LAST() outside 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 b3aa42fc66e..be157a5fbe9 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -10967,30 +10967,6 @@
{ oid => '3114', descr => 'fetch the Nth row value',
proname => 'nth_value', prokind => 'w', prorettype => 'anyelement',
proargtypes => 'anyelement int4', prosrc => 'window_nth_value' },
-{ oid => '8126', descr => 'fetch the preceding row value',
- proname => 'prev', provolatile => 's', prorettype => 'anyelement',
- proargtypes => 'anyelement', prosrc => 'window_prev' },
-{ oid => '8128', descr => 'fetch the Nth preceding row value',
- proname => 'prev', provolatile => 's', proisstrict => 'f', prorettype => 'anyelement',
- proargtypes => 'anyelement int8', prosrc => 'window_prev_offset' },
-{ oid => '8127', descr => 'fetch the following row value',
- proname => 'next', provolatile => 's', prorettype => 'anyelement',
- proargtypes => 'anyelement', prosrc => 'window_next' },
-{ oid => '8129', descr => 'fetch the Nth following row value',
- proname => 'next', provolatile => 's', proisstrict => 'f', prorettype => 'anyelement',
- proargtypes => 'anyelement int8', prosrc => 'window_next_offset' },
-{ oid => '8130', descr => 'fetch the first row value within match',
- proname => 'first', provolatile => 's', prorettype => 'anyelement',
- proargtypes => 'anyelement', prosrc => 'window_first' },
-{ oid => '8132', descr => 'fetch the Nth row value within match',
- proname => 'first', provolatile => 's', proisstrict => 'f', prorettype => 'anyelement',
- proargtypes => 'anyelement int8', prosrc => 'window_first_offset' },
-{ oid => '8131', descr => 'fetch the last row value within match',
- proname => 'last', provolatile => 's', prorettype => 'anyelement',
- proargtypes => 'anyelement', prosrc => 'window_last' },
-{ oid => '8133', descr => 'fetch the Nth-from-last row value within match',
- 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/test/regress/expected/rpr.out b/src/test/regress/expected/rpr.out
index c02c9d75a9a..dc5140fecc9 100644
--- a/src/test/regress/expected/rpr.out
+++ b/src/test/regress/expected/rpr.out
@@ -1021,16 +1021,6 @@ WINDOW w AS (
--
-- Error cases: PREV/NEXT usage restrictions
--
--- PREV outside DEFINE clause
-SELECT prev(price) FROM stock;
-ERROR: cannot use prev outside a DEFINE clause
-LINE 1: SELECT prev(price) FROM stock;
- ^
--- NEXT outside DEFINE clause
-SELECT next(price) FROM stock;
-ERROR: cannot use next outside a DEFINE clause
-LINE 1: SELECT next(price) FROM stock;
- ^
-- Nested PREV
SELECT price FROM stock
WINDOW w AS (
@@ -1598,15 +1588,6 @@ WINDOW w AS (
company2 | 07-10-2023 | 1300 | | | 0
(20 rows)
--- 2-arg PREV/NEXT outside DEFINE clause
-SELECT prev(price, 2) FROM stock;
-ERROR: cannot use prev outside a DEFINE clause
-LINE 1: SELECT prev(price, 2) FROM stock;
- ^
-SELECT next(price, 2) FROM stock;
-ERROR: cannot use next outside 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
@@ -2134,19 +2115,6 @@ SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS (
DEFINE A AS LAST(val, -1) IS NULL
);
ERROR: row pattern navigation offset must not be negative
--- FIRST/LAST outside DEFINE clause (error cases)
-SELECT first(val) FROM rpr_nav;
-ERROR: cannot use first outside a DEFINE clause
-LINE 1: SELECT first(val) FROM rpr_nav;
- ^
-SELECT last(val) FROM rpr_nav;
-ERROR: cannot use last outside a DEFINE clause
-LINE 1: SELECT last(val) FROM rpr_nav;
- ^
-SELECT first(val, 1) FROM rpr_nav;
-ERROR: cannot use first outside 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');
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index 41541898f5a..cf158e1c043 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -1709,9 +1709,10 @@ WINDOW w AS (
B AS val > PREV(val)
)
ORDER BY id;
-ERROR: cannot use prev outside a DEFINE clause
+ERROR: function prev(integer) does not exist
LINE 1: SELECT PREV(id), id, val, COUNT(*) OVER w as cnt
^
+DETAIL: There is no function of that name.
-- NEXT function cannot be used other than in DEFINE
SELECT NEXT(id), id, val, COUNT(*) OVER w as cnt
FROM rpr_nav
@@ -1724,9 +1725,10 @@ WINDOW w AS (
B AS val > PREV(val)
)
ORDER BY id;
-ERROR: cannot use next outside a DEFINE clause
+ERROR: function next(integer) does not exist
LINE 1: SELECT NEXT(id), id, val, COUNT(*) OVER w as cnt
^
+DETAIL: There is no function of that name.
-- FIRST function - reference match_start row
SELECT id, val, COUNT(*) OVER w as cnt
FROM rpr_nav
@@ -1792,15 +1794,376 @@ ORDER BY id;
-- FIRST function cannot be used other than in DEFINE
SELECT FIRST(id), id, val FROM rpr_nav;
-ERROR: cannot use first outside a DEFINE clause
+ERROR: function first(integer) does not exist
LINE 1: SELECT FIRST(id), id, val FROM rpr_nav;
^
+DETAIL: There is no function of that name.
-- LAST function cannot be used other than in DEFINE
SELECT LAST(id), id, val FROM rpr_nav;
-ERROR: cannot use last outside a DEFINE clause
+ERROR: function last(integer) does not exist
LINE 1: SELECT LAST(id), id, val FROM rpr_nav;
^
+DETAIL: There is no function of that name.
DROP TABLE rpr_nav;
+-- Name-space: prev/next/first/last are navigation functions, not ordinary functions
+CREATE SCHEMA rpr_navns;
+SET search_path TO rpr_navns, public;
+CREATE TABLE nt (g text, id int, val int);
+INSERT INTO nt VALUES ('x', 1, 100), ('x', 2, 200), ('x', 3, 150),
+ ('x', 4, 140), ('x', 5, 150);
+-- Outside DEFINE these are ordinary identifiers and resolve to nothing
+SELECT prev(val) FROM nt;
+ERROR: function prev(integer) does not exist
+LINE 1: SELECT prev(val) FROM nt;
+ ^
+DETAIL: There is no function of that name.
+SELECT next(val) FROM nt;
+ERROR: function next(integer) does not exist
+LINE 1: SELECT next(val) FROM nt;
+ ^
+DETAIL: There is no function of that name.
+SELECT prev(val, 2) FROM nt;
+ERROR: function prev(integer, integer) does not exist
+LINE 1: SELECT prev(val, 2) FROM nt;
+ ^
+DETAIL: There is no function of that name.
+SELECT next(val, 2) FROM nt;
+ERROR: function next(integer, integer) does not exist
+LINE 1: SELECT next(val, 2) FROM nt;
+ ^
+DETAIL: There is no function of that name.
+SELECT first(val) FROM nt;
+ERROR: function first(integer) does not exist
+LINE 1: SELECT first(val) FROM nt;
+ ^
+DETAIL: There is no function of that name.
+SELECT last(val) FROM nt;
+ERROR: function last(integer) does not exist
+LINE 1: SELECT last(val) FROM nt;
+ ^
+DETAIL: There is no function of that name.
+SELECT first(val, 1) FROM nt;
+ERROR: function first(integer, integer) does not exist
+LINE 1: SELECT first(val, 1) FROM nt;
+ ^
+DETAIL: There is no function of that name.
+-- A schema-qualified call is also a plain (failing) function lookup
+SELECT pg_catalog.prev(val) FROM nt;
+ERROR: function pg_catalog.prev(integer) does not exist
+LINE 1: SELECT pg_catalog.prev(val) FROM nt;
+ ^
+-- Outside DEFINE, a user-defined function of that name is callable
+CREATE FUNCTION next(numeric) RETURNS numeric AS 'SELECT -999::numeric'
+ LANGUAGE sql IMMUTABLE;
+SELECT next(10);
+ next
+------
+ -999
+(1 row)
+
+-- Inside DEFINE, unqualified PREV is nav whether or not a user prev() exists
+SELECT id, val, count(*) OVER w AS cnt, last_value(id) OVER w AS last_id
+ FROM nt
+ WINDOW w AS (PARTITION BY g ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+ PATTERN (START UP+)
+ DEFINE START AS TRUE, UP AS val > PREV(val))
+ ORDER BY id;
+ id | val | cnt | last_id
+----+-----+-----+---------
+ 1 | 100 | 2 | 2
+ 2 | 200 | 0 |
+ 3 | 150 | 0 |
+ 4 | 140 | 2 | 5
+ 5 | 150 | 0 |
+(5 rows)
+
+-- A qualified call invokes the function, so its volatility still matters
+-- VOLATILE: unqualified is nav; qualified is rejected as a volatile function
+CREATE FUNCTION prev(integer) RETURNS integer AS 'SELECT -999'
+ LANGUAGE sql VOLATILE;
+SELECT id, val, count(*) OVER w AS cnt, last_value(id) OVER w AS last_id
+ FROM nt
+ WINDOW w AS (PARTITION BY g ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+ PATTERN (START UP+)
+ DEFINE START AS TRUE, UP AS val > PREV(val))
+ ORDER BY id;
+ id | val | cnt | last_id
+----+-----+-----+---------
+ 1 | 100 | 2 | 2
+ 2 | 200 | 0 |
+ 3 | 150 | 0 |
+ 4 | 140 | 2 | 5
+ 5 | 150 | 0 |
+(5 rows)
+
+SELECT id, val, count(*) OVER w AS cnt, last_value(id) OVER w AS last_id
+ FROM nt
+ WINDOW w AS (PARTITION BY g ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+ PATTERN (A+)
+ DEFINE A AS rpr_navns.prev(val) = -999)
+ ORDER BY id;
+ERROR: volatile functions are not allowed in DEFINE clause
+LINE 6: DEFINE A AS rpr_navns.prev(val) = -999)
+ ^
+DROP FUNCTION prev(integer);
+-- IMMUTABLE: unqualified is nav; qualified is the escape hatch and succeeds
+CREATE FUNCTION prev(integer) RETURNS integer AS 'SELECT -999'
+ LANGUAGE sql IMMUTABLE;
+SELECT id, val, count(*) OVER w AS cnt, last_value(id) OVER w AS last_id
+ FROM nt
+ WINDOW w AS (PARTITION BY g ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+ PATTERN (START UP+)
+ DEFINE START AS TRUE, UP AS val > PREV(val))
+ ORDER BY id;
+ id | val | cnt | last_id
+----+-----+-----+---------
+ 1 | 100 | 2 | 2
+ 2 | 200 | 0 |
+ 3 | 150 | 0 |
+ 4 | 140 | 2 | 5
+ 5 | 150 | 0 |
+(5 rows)
+
+-- (val).prev is attribute notation, so it calls the ordinary function prev(val)
+-- (the IMMUTABLE user prev here), the same as the schema-qualified call below
+SELECT id, val, count(*) OVER w AS cnt, last_value(id) OVER w AS last_id
+ FROM nt
+ WINDOW w AS (PARTITION BY g ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+ PATTERN (A+)
+ DEFINE A AS (val).prev = -999)
+ ORDER BY id;
+ id | val | cnt | last_id
+----+-----+-----+---------
+ 1 | 100 | 5 | 5
+ 2 | 200 | 0 |
+ 3 | 150 | 0 |
+ 4 | 140 | 0 |
+ 5 | 150 | 0 |
+(5 rows)
+
+SELECT id, val, count(*) OVER w AS cnt, last_value(id) OVER w AS last_id
+ FROM nt
+ WINDOW w AS (PARTITION BY g ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+ PATTERN (A+)
+ DEFINE A AS rpr_navns.prev(val) = -999)
+ ORDER BY id;
+ id | val | cnt | last_id
+----+-----+-----+---------
+ 1 | 100 | 5 | 5
+ 2 | 200 | 0 |
+ 3 | 150 | 0 |
+ 4 | 140 | 0 |
+ 5 | 150 | 0 |
+(5 rows)
+
+-- Zero or more than two arguments is an error, with no function fallback
+SELECT count(*) OVER w FROM nt
+ WINDOW w AS (PARTITION BY g ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+ PATTERN (A+) DEFINE A AS PREV() IS NULL);
+ERROR: too few arguments for row pattern navigation function PREV
+LINE 4: PATTERN (A+) DEFINE A AS PREV() IS NULL);
+ ^
+DETAIL: PREV takes a value expression and an optional offset argument.
+SELECT count(*) OVER w FROM nt
+ WINDOW w AS (PARTITION BY g ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+ PATTERN (A+) DEFINE A AS PREV(val, 1, 2) IS NULL);
+ERROR: too many arguments for row pattern navigation function PREV
+LINE 4: PATTERN (A+) DEFINE A AS PREV(val, 1, 2) IS NULL);
+ ^
+DETAIL: PREV takes a value expression and an optional offset argument.
+-- the error stands even when a user function of that exact arity exists
+CREATE FUNCTION prev(integer, integer, integer) RETURNS integer
+ AS 'SELECT -999' LANGUAGE sql IMMUTABLE;
+SELECT count(*) OVER w FROM nt
+ WINDOW w AS (PARTITION BY g ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+ PATTERN (A+) DEFINE A AS PREV(val, 1, 2) IS NULL);
+ERROR: too many arguments for row pattern navigation function PREV
+LINE 4: PATTERN (A+) DEFINE A AS PREV(val, 1, 2) IS NULL);
+ ^
+DETAIL: PREV takes a value expression and an optional offset argument.
+DROP FUNCTION prev(integer, integer, integer);
+-- Syntactic decoration is rejected
+SELECT count(*) OVER w FROM nt
+ WINDOW w AS (PARTITION BY g ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+ PATTERN (A+) DEFINE A AS PREV(*) IS NULL);
+ERROR: prev(*) specified, but prev is not an aggregate function
+LINE 4: PATTERN (A+) DEFINE A AS PREV(*) IS NULL);
+ ^
+SELECT count(*) OVER w FROM nt
+ WINDOW w AS (PARTITION BY g ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+ PATTERN (A+) DEFINE A AS PREV(DISTINCT val) IS NULL);
+ERROR: DISTINCT specified, but prev is not an aggregate function
+LINE 4: PATTERN (A+) DEFINE A AS PREV(DISTINCT val) IS NULL);
+ ^
+SELECT count(*) OVER w FROM nt
+ WINDOW w AS (PARTITION BY g ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+ PATTERN (A+) DEFINE A AS PREV(val ORDER BY val) IS NULL);
+ERROR: ORDER BY specified, but prev is not an aggregate function
+LINE 4: PATTERN (A+) DEFINE A AS PREV(val ORDER BY val) IS NULL)...
+ ^
+SELECT count(*) OVER w FROM nt
+ WINDOW w AS (PARTITION BY g ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+ PATTERN (A+) DEFINE A AS PREV(val) FILTER (WHERE true) IS NULL);
+ERROR: FILTER specified, but prev is not an aggregate function
+LINE 4: PATTERN (A+) DEFINE A AS PREV(val) FILTER (WHERE true) I...
+ ^
+SELECT count(*) OVER w FROM nt
+ WINDOW w AS (PARTITION BY g ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+ PATTERN (A+) DEFINE A AS PREV(val) WITHIN GROUP (ORDER BY val) IS NULL);
+ERROR: WITHIN GROUP specified, but prev is not an aggregate function
+LINE 4: PATTERN (A+) DEFINE A AS PREV(val) WITHIN GROUP (ORDER B...
+ ^
+SELECT count(*) OVER w FROM nt
+ WINDOW w AS (PARTITION BY g ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+ PATTERN (A+) DEFINE A AS PREV(val) OVER () IS NULL);
+ERROR: OVER specified, but prev is not a window function nor an aggregate function
+LINE 4: PATTERN (A+) DEFINE A AS PREV(val) OVER () IS NULL);
+ ^
+SELECT count(*) OVER w FROM nt
+ WINDOW w AS (PARTITION BY g ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+ PATTERN (A+) DEFINE A AS PREV(VARIADIC ARRAY[val]) IS NULL);
+ERROR: cannot use VARIADIC with row pattern navigation function PREV
+LINE 4: PATTERN (A+) DEFINE A AS PREV(VARIADIC ARRAY[val]) IS NU...
+ ^
+SELECT count(*) OVER w FROM nt
+ WINDOW w AS (PARTITION BY g ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+ PATTERN (A+) DEFINE A AS prev(x => val) IS NULL);
+ERROR: row pattern navigation operations cannot use named arguments
+LINE 4: PATTERN (A+) DEFINE A AS prev(x => val) IS NULL);
+ ^
+SELECT count(*) OVER w FROM nt
+ WINDOW w AS (PARTITION BY g ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+ PATTERN (A+) DEFINE A AS PREV(val) IGNORE NULLS IS NULL);
+ERROR: only window functions accept RESPECT/IGNORE NULLS
+LINE 4: PATTERN (A+) DEFINE A AS PREV(val) IGNORE NULLS IS NULL)...
+ ^
+-- Quoting does not escape: "prev" is nav, "PREV" is an ordinary name
+SELECT id, val, count(*) OVER w AS cnt
+ FROM nt
+ WINDOW w AS (PARTITION BY g ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+ PATTERN (START UP+)
+ DEFINE START AS TRUE, UP AS val > "prev"(val))
+ ORDER BY id;
+ id | val | cnt
+----+-----+-----
+ 1 | 100 | 2
+ 2 | 200 | 0
+ 3 | 150 | 0
+ 4 | 140 | 2
+ 5 | 150 | 0
+(5 rows)
+
+SELECT count(*) OVER w FROM nt
+ WINDOW w AS (PARTITION BY g ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+ PATTERN (A+) DEFINE A AS "PREV"(val) IS NULL);
+ERROR: function PREV(integer) does not exist
+LINE 4: PATTERN (A+) DEFINE A AS "PREV"(val) IS NULL);
+ ^
+DETAIL: There is no function of that name.
+-- A view round-trips: bare PREV stays a navigation function, and a qualified
+-- user prev() stays schema-qualified so it does not reparse as navigation
+CREATE VIEW navns_nav AS
+ SELECT id, count(*) OVER w AS cnt FROM nt
+ WINDOW w AS (PARTITION BY g ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+ PATTERN (START UP+) DEFINE START AS TRUE, UP AS val > PREV(val));
+CREATE VIEW navns_fn AS
+ SELECT id, count(*) OVER w AS cnt FROM nt
+ WINDOW w AS (PARTITION BY g ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+ PATTERN (A+) DEFINE A AS rpr_navns.prev(val) = -999);
+SELECT pg_get_viewdef('navns_nav');
+ pg_get_viewdef
+---------------------------------------------------------------------------------------------
+ SELECT id, +
+ count(*) OVER w AS cnt +
+ FROM nt +
+ WINDOW w AS (PARTITION BY g ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
+ AFTER MATCH SKIP PAST LAST ROW +
+ INITIAL +
+ PATTERN (start up+) +
+ DEFINE +
+ start AS true, +
+ up AS (val > PREV(val)) );
+(1 row)
+
+SELECT pg_get_viewdef('navns_fn');
+ pg_get_viewdef
+---------------------------------------------------------------------------------------------
+ SELECT id, +
+ count(*) OVER w AS cnt +
+ FROM nt +
+ WINDOW w AS (PARTITION BY g ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
+ AFTER MATCH SKIP PAST LAST ROW +
+ INITIAL +
+ PATTERN (a+) +
+ DEFINE +
+ a AS (rpr_navns.prev(val) = '-999'::integer) );
+(1 row)
+
+DROP VIEW navns_nav, navns_fn;
+-- Attribute notation is field selection only, never a function fallback
+CREATE TYPE rpr_navns_pair AS (first int, last int);
+CREATE TABLE ct (id int, p rpr_navns_pair);
+INSERT INTO ct VALUES (1, (10, 20)), (2, (30, 40));
+SELECT (p).last FROM ct ORDER BY id;
+ last
+------
+ 20
+ 40
+(2 rows)
+
+SELECT count(*) OVER w FROM ct
+ WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+ PATTERN (A+) DEFINE A AS (p).last > 0);
+ count
+-------
+ 2
+ 0
+(2 rows)
+
+SELECT count(*) OVER w FROM ct
+ WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+ PATTERN (A+) DEFINE A AS (p).prev > 0);
+ERROR: column "prev" not found in data type rpr_navns_pair
+LINE 4: PATTERN (A+) DEFINE A AS (p).prev > 0);
+ ^
+-- Navigation offset must not contain a navigation operation
+SELECT id, val
+ FROM nt
+ WINDOW w AS (PARTITION BY g ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+ PATTERN (A+)
+ DEFINE A AS PREV(val, FIRST(1)) > 0)
+ ORDER BY id;
+ERROR: row pattern navigation offset cannot contain a row pattern navigation operation
+LINE 6: DEFINE A AS PREV(val, FIRST(1)) > 0)
+ ^
+DETAIL: A navigation offset must be a run-time constant.
+DROP SCHEMA rpr_navns CASCADE;
+RESET search_path;
-- ============================================================
-- SKIP TO / INITIAL Tests
-- ============================================================
@@ -3655,6 +4018,127 @@ ERROR: cannot nest row pattern navigation more than two levels deep
LINE 6: DEFINE A AS PREV(FIRST(PREV(v))) > 0
^
HINT: Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed.
+-- A navigation offset must be a run-time constant, not a navigation operation
+SELECT count(*) OVER w
+FROM generate_series(1,10) s(v)
+WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+) DEFINE A AS PREV(v, FIRST(1)) > 0);
+ERROR: row pattern navigation offset cannot contain a row pattern navigation operation
+LINE 4: PATTERN (A+) DEFINE A AS PREV(v, FIRST(1)) > 0);
+ ^
+DETAIL: A navigation offset must be a run-time constant.
+SELECT count(*) OVER w
+FROM generate_series(1,10) s(v)
+WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+) DEFINE A AS PREV(v, FIRST(1) + 1) > 0);
+ERROR: row pattern navigation offset cannot contain a row pattern navigation operation
+LINE 4: PATTERN (A+) DEFINE A AS PREV(v, FIRST(1) + 1) > 0);
+ ^
+DETAIL: A navigation offset must be a run-time constant.
+SELECT count(*) OVER w
+FROM generate_series(1,10) s(v)
+WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+) DEFINE A AS PREV(v, NEXT(1, 0)) > 0);
+ERROR: row pattern navigation offset cannot contain a row pattern navigation operation
+LINE 4: PATTERN (A+) DEFINE A AS PREV(v, NEXT(1, 0)) > 0);
+ ^
+DETAIL: A navigation offset must be a run-time constant.
+SELECT count(*) OVER w
+FROM generate_series(1,10) s(v)
+WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+) DEFINE A AS PREV(FIRST(v), LAST(1)) > 0);
+ERROR: row pattern navigation offset cannot contain a row pattern navigation operation
+LINE 4: PATTERN (A+) DEFINE A AS PREV(FIRST(v), LAST(1)) > 0);
+ ^
+DETAIL: A navigation offset must be a run-time constant.
+SELECT count(*) OVER w
+FROM generate_series(1,10) s(v)
+WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+) DEFINE A AS PREV(v, FIRST(v)) > 0);
+ERROR: row pattern navigation offset cannot contain a row pattern navigation operation
+LINE 4: PATTERN (A+) DEFINE A AS PREV(v, FIRST(v)) > 0);
+ ^
+DETAIL: A navigation offset must be a run-time constant.
+SELECT count(*) OVER w
+FROM generate_series(1,10) s(v)
+WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+) DEFINE A AS NEXT(v, PREV(v, 1)) > 0);
+ERROR: row pattern navigation offset cannot contain a row pattern navigation operation
+LINE 4: PATTERN (A+) DEFINE A AS NEXT(v, PREV(v, 1)) > 0);
+ ^
+DETAIL: A navigation offset must be a run-time constant.
+SELECT count(*) OVER w
+FROM generate_series(1,10) s(v)
+WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+) DEFINE A AS PREV(FIRST(v, LAST(1)), 2) > 0);
+ERROR: cannot nest row pattern navigation more than two levels deep
+LINE 4: PATTERN (A+) DEFINE A AS PREV(FIRST(v, LAST(1)), 2) > 0)...
+ ^
+HINT: Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed.
+SELECT count(*) OVER w
+FROM generate_series(1,10) s(v)
+WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+) DEFINE A AS PREV(v, FIRST(1::bigint)) > 0);
+ERROR: row pattern navigation offset cannot contain a row pattern navigation operation
+LINE 4: PATTERN (A+) DEFINE A AS PREV(v, FIRST(1::bigint)) > 0);
+ ^
+DETAIL: A navigation offset must be a run-time constant.
+-- An unknown literal argument resolves to text; it must still reference a column
+SELECT count(*) OVER w
+FROM generate_series(1,5) s(v)
+WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+) DEFINE A AS PREV('foo') = 'bar');
+ERROR: argument of row pattern navigation operation must include at least one column reference
+LINE 4: PATTERN (A+) DEFINE A AS PREV('foo') = 'bar');
+ ^
+SELECT count(*) OVER w
+FROM generate_series(1,5) s(v)
+WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+) DEFINE A AS PREV('foo'));
+ERROR: argument of DEFINE must be type boolean, not type text
+LINE 4: PATTERN (A+) DEFINE A AS PREV('foo'));
+ ^
+SELECT count(*) OVER w
+FROM generate_series(1,5) s(v)
+WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+) DEFINE A AS PREV(NULL) IS NULL);
+ERROR: argument of row pattern navigation operation must include at least one column reference
+LINE 4: PATTERN (A+) DEFINE A AS PREV(NULL) IS NULL);
+ ^
+PREPARE rpr_navarg AS SELECT count(*) OVER w
+FROM generate_series(1,5) s(v)
+WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+) DEFINE A AS PREV($1) IS NULL);
+ERROR: argument of row pattern navigation operation must include at least one column reference
+LINE 4: PATTERN (A+) DEFINE A AS PREV($1) IS NULL);
+ ^
+-- An int2 offset is coerced to int8 like any implicit cast (same as plain 0)
+SELECT count(*) OVER w
+FROM generate_series(1,5) s(v)
+WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+) DEFINE A AS PREV(v, 0::smallint) = v);
+ count
+-------
+ 5
+ 0
+ 0
+ 0
+ 0
+(5 rows)
+
+SELECT count(*) OVER w
+FROM generate_series(1,5) s(v)
+WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+) DEFINE A AS PREV(v, 0) = v);
+ count
+-------
+ 5
+ 0
+ 0
+ 0
+ 0
+(5 rows)
+
-- ============================================================
-- Window Deduplication Tests
-- ============================================================
diff --git a/src/test/regress/sql/rpr.sql b/src/test/regress/sql/rpr.sql
index b15b30c85ac..e3e9de789db 100644
--- a/src/test/regress/sql/rpr.sql
+++ b/src/test/regress/sql/rpr.sql
@@ -444,12 +444,6 @@ WINDOW w AS (
-- 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 (
@@ -831,10 +825,6 @@ WINDOW w AS (
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
@@ -1124,11 +1114,6 @@ SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS (
DEFINE A AS LAST(val, -1) IS NULL
);
--- FIRST/LAST outside DEFINE clause (error cases)
-SELECT first(val) FROM rpr_nav;
-SELECT last(val) FROM rpr_nav;
-SELECT first(val, 1) FROM rpr_nav;
-
-- Functional notation: should access column, not RPR navigation
CREATE TEMP TABLE rpr_names (prev int, next int, first text, last text);
INSERT INTO rpr_names VALUES (1, 2, 'Joe', 'Blow');
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index fcefd59de4a..e71f0dd3680 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -1309,6 +1309,195 @@ SELECT LAST(id), id, val FROM rpr_nav;
DROP TABLE rpr_nav;
+-- Name-space: prev/next/first/last are navigation functions, not ordinary functions
+CREATE SCHEMA rpr_navns;
+SET search_path TO rpr_navns, public;
+CREATE TABLE nt (g text, id int, val int);
+INSERT INTO nt VALUES ('x', 1, 100), ('x', 2, 200), ('x', 3, 150),
+ ('x', 4, 140), ('x', 5, 150);
+
+-- Outside DEFINE these are ordinary identifiers and resolve to nothing
+SELECT prev(val) FROM nt;
+SELECT next(val) FROM nt;
+SELECT prev(val, 2) FROM nt;
+SELECT next(val, 2) FROM nt;
+SELECT first(val) FROM nt;
+SELECT last(val) FROM nt;
+SELECT first(val, 1) FROM nt;
+-- A schema-qualified call is also a plain (failing) function lookup
+SELECT pg_catalog.prev(val) FROM nt;
+
+-- Outside DEFINE, a user-defined function of that name is callable
+CREATE FUNCTION next(numeric) RETURNS numeric AS 'SELECT -999::numeric'
+ LANGUAGE sql IMMUTABLE;
+SELECT next(10);
+
+-- Inside DEFINE, unqualified PREV is nav whether or not a user prev() exists
+SELECT id, val, count(*) OVER w AS cnt, last_value(id) OVER w AS last_id
+ FROM nt
+ WINDOW w AS (PARTITION BY g ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+ PATTERN (START UP+)
+ DEFINE START AS TRUE, UP AS val > PREV(val))
+ ORDER BY id;
+
+-- A qualified call invokes the function, so its volatility still matters
+-- VOLATILE: unqualified is nav; qualified is rejected as a volatile function
+CREATE FUNCTION prev(integer) RETURNS integer AS 'SELECT -999'
+ LANGUAGE sql VOLATILE;
+SELECT id, val, count(*) OVER w AS cnt, last_value(id) OVER w AS last_id
+ FROM nt
+ WINDOW w AS (PARTITION BY g ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+ PATTERN (START UP+)
+ DEFINE START AS TRUE, UP AS val > PREV(val))
+ ORDER BY id;
+SELECT id, val, count(*) OVER w AS cnt, last_value(id) OVER w AS last_id
+ FROM nt
+ WINDOW w AS (PARTITION BY g ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+ PATTERN (A+)
+ DEFINE A AS rpr_navns.prev(val) = -999)
+ ORDER BY id;
+DROP FUNCTION prev(integer);
+-- IMMUTABLE: unqualified is nav; qualified is the escape hatch and succeeds
+CREATE FUNCTION prev(integer) RETURNS integer AS 'SELECT -999'
+ LANGUAGE sql IMMUTABLE;
+SELECT id, val, count(*) OVER w AS cnt, last_value(id) OVER w AS last_id
+ FROM nt
+ WINDOW w AS (PARTITION BY g ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+ PATTERN (START UP+)
+ DEFINE START AS TRUE, UP AS val > PREV(val))
+ ORDER BY id;
+-- (val).prev is attribute notation, so it calls the ordinary function prev(val)
+-- (the IMMUTABLE user prev here), the same as the schema-qualified call below
+SELECT id, val, count(*) OVER w AS cnt, last_value(id) OVER w AS last_id
+ FROM nt
+ WINDOW w AS (PARTITION BY g ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+ PATTERN (A+)
+ DEFINE A AS (val).prev = -999)
+ ORDER BY id;
+SELECT id, val, count(*) OVER w AS cnt, last_value(id) OVER w AS last_id
+ FROM nt
+ WINDOW w AS (PARTITION BY g ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+ PATTERN (A+)
+ DEFINE A AS rpr_navns.prev(val) = -999)
+ ORDER BY id;
+
+-- Zero or more than two arguments is an error, with no function fallback
+SELECT count(*) OVER w FROM nt
+ WINDOW w AS (PARTITION BY g ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+ PATTERN (A+) DEFINE A AS PREV() IS NULL);
+SELECT count(*) OVER w FROM nt
+ WINDOW w AS (PARTITION BY g ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+ PATTERN (A+) DEFINE A AS PREV(val, 1, 2) IS NULL);
+-- the error stands even when a user function of that exact arity exists
+CREATE FUNCTION prev(integer, integer, integer) RETURNS integer
+ AS 'SELECT -999' LANGUAGE sql IMMUTABLE;
+SELECT count(*) OVER w FROM nt
+ WINDOW w AS (PARTITION BY g ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+ PATTERN (A+) DEFINE A AS PREV(val, 1, 2) IS NULL);
+DROP FUNCTION prev(integer, integer, integer);
+
+-- Syntactic decoration is rejected
+SELECT count(*) OVER w FROM nt
+ WINDOW w AS (PARTITION BY g ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+ PATTERN (A+) DEFINE A AS PREV(*) IS NULL);
+SELECT count(*) OVER w FROM nt
+ WINDOW w AS (PARTITION BY g ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+ PATTERN (A+) DEFINE A AS PREV(DISTINCT val) IS NULL);
+SELECT count(*) OVER w FROM nt
+ WINDOW w AS (PARTITION BY g ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+ PATTERN (A+) DEFINE A AS PREV(val ORDER BY val) IS NULL);
+SELECT count(*) OVER w FROM nt
+ WINDOW w AS (PARTITION BY g ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+ PATTERN (A+) DEFINE A AS PREV(val) FILTER (WHERE true) IS NULL);
+SELECT count(*) OVER w FROM nt
+ WINDOW w AS (PARTITION BY g ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+ PATTERN (A+) DEFINE A AS PREV(val) WITHIN GROUP (ORDER BY val) IS NULL);
+SELECT count(*) OVER w FROM nt
+ WINDOW w AS (PARTITION BY g ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+ PATTERN (A+) DEFINE A AS PREV(val) OVER () IS NULL);
+SELECT count(*) OVER w FROM nt
+ WINDOW w AS (PARTITION BY g ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+ PATTERN (A+) DEFINE A AS PREV(VARIADIC ARRAY[val]) IS NULL);
+SELECT count(*) OVER w FROM nt
+ WINDOW w AS (PARTITION BY g ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+ PATTERN (A+) DEFINE A AS prev(x => val) IS NULL);
+SELECT count(*) OVER w FROM nt
+ WINDOW w AS (PARTITION BY g ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+ PATTERN (A+) DEFINE A AS PREV(val) IGNORE NULLS IS NULL);
+
+-- Quoting does not escape: "prev" is nav, "PREV" is an ordinary name
+SELECT id, val, count(*) OVER w AS cnt
+ FROM nt
+ WINDOW w AS (PARTITION BY g ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+ PATTERN (START UP+)
+ DEFINE START AS TRUE, UP AS val > "prev"(val))
+ ORDER BY id;
+SELECT count(*) OVER w FROM nt
+ WINDOW w AS (PARTITION BY g ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+ PATTERN (A+) DEFINE A AS "PREV"(val) IS NULL);
+
+-- A view round-trips: bare PREV stays a navigation function, and a qualified
+-- user prev() stays schema-qualified so it does not reparse as navigation
+CREATE VIEW navns_nav AS
+ SELECT id, count(*) OVER w AS cnt FROM nt
+ WINDOW w AS (PARTITION BY g ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+ PATTERN (START UP+) DEFINE START AS TRUE, UP AS val > PREV(val));
+CREATE VIEW navns_fn AS
+ SELECT id, count(*) OVER w AS cnt FROM nt
+ WINDOW w AS (PARTITION BY g ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+ PATTERN (A+) DEFINE A AS rpr_navns.prev(val) = -999);
+SELECT pg_get_viewdef('navns_nav');
+SELECT pg_get_viewdef('navns_fn');
+DROP VIEW navns_nav, navns_fn;
+
+-- Attribute notation is field selection only, never a function fallback
+CREATE TYPE rpr_navns_pair AS (first int, last int);
+CREATE TABLE ct (id int, p rpr_navns_pair);
+INSERT INTO ct VALUES (1, (10, 20)), (2, (30, 40));
+SELECT (p).last FROM ct ORDER BY id;
+SELECT count(*) OVER w FROM ct
+ WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+ PATTERN (A+) DEFINE A AS (p).last > 0);
+SELECT count(*) OVER w FROM ct
+ WINDOW w AS (ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+ PATTERN (A+) DEFINE A AS (p).prev > 0);
+
+-- Navigation offset must not contain a navigation operation
+SELECT id, val
+ FROM nt
+ WINDOW w AS (PARTITION BY g ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL
+ PATTERN (A+)
+ DEFINE A AS PREV(val, FIRST(1)) > 0)
+ ORDER BY id;
+
+DROP SCHEMA rpr_navns CASCADE;
+RESET search_path;
+
-- ============================================================
-- SKIP TO / INITIAL Tests
-- ============================================================
@@ -2420,6 +2609,68 @@ WINDOW w AS (
DEFINE A AS PREV(FIRST(PREV(v))) > 0
);
+-- A navigation offset must be a run-time constant, not a navigation operation
+SELECT count(*) OVER w
+FROM generate_series(1,10) s(v)
+WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+) DEFINE A AS PREV(v, FIRST(1)) > 0);
+SELECT count(*) OVER w
+FROM generate_series(1,10) s(v)
+WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+) DEFINE A AS PREV(v, FIRST(1) + 1) > 0);
+SELECT count(*) OVER w
+FROM generate_series(1,10) s(v)
+WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+) DEFINE A AS PREV(v, NEXT(1, 0)) > 0);
+SELECT count(*) OVER w
+FROM generate_series(1,10) s(v)
+WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+) DEFINE A AS PREV(FIRST(v), LAST(1)) > 0);
+SELECT count(*) OVER w
+FROM generate_series(1,10) s(v)
+WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+) DEFINE A AS PREV(v, FIRST(v)) > 0);
+SELECT count(*) OVER w
+FROM generate_series(1,10) s(v)
+WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+) DEFINE A AS NEXT(v, PREV(v, 1)) > 0);
+SELECT count(*) OVER w
+FROM generate_series(1,10) s(v)
+WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+) DEFINE A AS PREV(FIRST(v, LAST(1)), 2) > 0);
+SELECT count(*) OVER w
+FROM generate_series(1,10) s(v)
+WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+) DEFINE A AS PREV(v, FIRST(1::bigint)) > 0);
+
+-- An unknown literal argument resolves to text; it must still reference a column
+SELECT count(*) OVER w
+FROM generate_series(1,5) s(v)
+WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+) DEFINE A AS PREV('foo') = 'bar');
+SELECT count(*) OVER w
+FROM generate_series(1,5) s(v)
+WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+) DEFINE A AS PREV('foo'));
+SELECT count(*) OVER w
+FROM generate_series(1,5) s(v)
+WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+) DEFINE A AS PREV(NULL) IS NULL);
+PREPARE rpr_navarg AS SELECT count(*) OVER w
+FROM generate_series(1,5) s(v)
+WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+) DEFINE A AS PREV($1) IS NULL);
+
+-- An int2 offset is coerced to int8 like any implicit cast (same as plain 0)
+SELECT count(*) OVER w
+FROM generate_series(1,5) s(v)
+WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+) DEFINE A AS PREV(v, 0::smallint) = v);
+SELECT count(*) OVER w
+FROM generate_series(1,5) s(v)
+WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+) DEFINE A AS PREV(v, 0) = v);
+
-- ============================================================
-- Window Deduplication Tests
-- ============================================================
--
2.50.1 (Apple Git-155)
From 2025f549ef7c653b0e0e87f282ac8fa46feb2fc5 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Wed, 17 Jun 2026 08:16:57 +0900
Subject: [PATCH 02/13] Remove unnecessary includes from the row pattern
recognition patch
Drop includes that are unused or covered by existing forward typedefs:
condition_variable.h, hsearch.h, queryenvironment.h from execnodes.h;
<limits.h> from rpr.c; windowapi.h from execRPR.h. Switch rpr.h from
parsenodes.h to primnodes.h, where RPRNavExpr is actually defined.
---
src/backend/optimizer/plan/rpr.c | 2 --
src/include/executor/execRPR.h | 1 -
src/include/nodes/execnodes.h | 3 ---
src/include/optimizer/rpr.h | 7 ++++---
4 files changed, 4 insertions(+), 9 deletions(-)
diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index 175777a8ffc..50bca59451f 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -37,8 +37,6 @@
#include "postgres.h"
-#include <limits.h>
-
#include "catalog/pg_proc.h"
#include "mb/pg_wchar.h"
#include "miscadmin.h"
diff --git a/src/include/executor/execRPR.h b/src/include/executor/execRPR.h
index 7b2b0febb76..fb7dc63a4c6 100644
--- a/src/include/executor/execRPR.h
+++ b/src/include/executor/execRPR.h
@@ -15,7 +15,6 @@
#define EXECRPR_H
#include "nodes/execnodes.h"
-#include "windowapi.h"
/* NFA context management */
extern RPRNFAContext *ExecRPRStartContext(WindowAggState *winstate,
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 792aa3f0d05..8060b018ef8 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -38,9 +38,6 @@
#include "nodes/plannodes.h"
#include "partitioning/partdefs.h"
#include "storage/buf.h"
-#include "storage/condition_variable.h"
-#include "utils/hsearch.h"
-#include "utils/queryenvironment.h"
#include "utils/reltrigger.h"
#include "utils/typcache.h"
diff --git a/src/include/optimizer/rpr.h b/src/include/optimizer/rpr.h
index 802d2f1dd69..b4f87d8caa4 100644
--- a/src/include/optimizer/rpr.h
+++ b/src/include/optimizer/rpr.h
@@ -10,11 +10,12 @@
*
*-------------------------------------------------------------------------
*/
-#ifndef OPTIMIZER_RPR_H
-#define OPTIMIZER_RPR_H
+#ifndef RPR_H
+#define RPR_H
#include "nodes/parsenodes.h"
#include "nodes/plannodes.h"
+#include "nodes/primnodes.h"
/* Limits and special values */
/*
@@ -96,4 +97,4 @@ typedef struct NavTraversal
extern bool nav_traversal_walker(Node *node, void *ctx);
-#endif /* OPTIMIZER_RPR_H */
+#endif /* RPR_H */
--
2.50.1 (Apple Git-155)
From a70f1c1e753425adcec6021c07a0f5af6dc0968f Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Mon, 15 Jun 2026 16:54:13 +0900
Subject: [PATCH 04/13] Use a dedicated ExprContext for RPR DEFINE clause
evaluation
DEFINE clauses were evaluated in the per-output-tuple context. Because
EEOP_RPR_NAV_RESTORE copies pass-by-reference navigation results into that
context, resetting it per row freed window function results before
ExecProject (a use-after-free), while not resetting it leaked across the
partition. tmpcontext cannot be reused either, as ExecQualAndReset() resets
it mid-expression when NEXT re-enters spool_tuples. Use a third ExprContext,
reset once per row and separate from both.
---
src/backend/executor/execRPR.c | 5 ++++-
src/backend/executor/nodeWindowAgg.c | 25 ++++++++++++++++++++++---
src/include/nodes/execnodes.h | 1 +
3 files changed, 27 insertions(+), 4 deletions(-)
diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index b326a58bbf5..de78b06d277 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -1592,10 +1592,13 @@ static void
nfa_reevaluate_dependent_vars(WindowAggState *winstate, RPRNFAContext *ctx,
int64 currentPos)
{
- ExprContext *econtext = winstate->ss.ps.ps_ExprContext;
+ ExprContext *econtext = winstate->rprContext;
int64 saved_match_start = winstate->nav_match_start;
int64 saved_pos = winstate->currentpos;
+ /* Release the previous evaluation's DEFINE expression memory */
+ ResetExprContext(econtext);
+
/* Temporarily set nav_match_start and currentpos for FIRST/LAST */
winstate->nav_match_start = ctx->matchStartRow;
winstate->currentpos = currentPos;
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 5b2385f1d8d..90f33bdee40 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -2742,12 +2742,28 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
/*
* Create expression contexts. We need two, one for per-input-tuple
- * processing and one for per-output-tuple processing. We cheat a little
- * by using ExecAssignExprContext() to build both.
+ * processing and one for per-output-tuple processing, plus an optional
+ * third for row pattern recognition DEFINE evaluation (built just below
+ * when a DEFINE clause is present). We cheat a little by using
+ * ExecAssignExprContext() to build them all. Each call overwrites
+ * ps_ExprContext, so the last call must establish the output context.
*/
ExecAssignExprContext(estate, &winstate->ss.ps);
tmpcontext = winstate->ss.ps.ps_ExprContext;
winstate->tmpcontext = tmpcontext;
+
+ /*
+ * Row pattern recognition evaluates DEFINE clauses in a third context,
+ * reset before each DEFINE evaluation pass. It must be distinct from
+ * tmpcontext and ps_ExprContext so its reset frees neither input nor
+ * output tuple memory.
+ */
+ if (node->defineClause != NIL)
+ {
+ ExecAssignExprContext(estate, &winstate->ss.ps);
+ winstate->rprContext = winstate->ss.ps.ps_ExprContext;
+ }
+
ExecAssignExprContext(estate, &winstate->ss.ps);
/* Create long-lived context for storage of partition-local memory etc */
@@ -4572,12 +4588,15 @@ static bool
nfa_evaluate_row(WindowObject winobj, int64 pos, bool *varMatched)
{
WindowAggState *winstate = winobj->winstate;
- ExprContext *econtext = winstate->ss.ps.ps_ExprContext;
+ ExprContext *econtext = winstate->rprContext;
int numDefineVars = list_length(winstate->defineVariableList);
int varIdx = 0;
TupleTableSlot *slot;
int64 saved_pos;
+ /* Release the previous row's DEFINE evaluation memory */
+ ResetExprContext(econtext);
+
/* Fetch current row into temp_slot_1 */
slot = winstate->temp_slot_1;
if (!window_gettupleslot(winobj, pos, slot))
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 8060b018ef8..5b4ac8a6c33 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -2698,6 +2698,7 @@ typedef struct WindowAggState
MemoryContext aggcontext; /* shared context for aggregate working data */
MemoryContext curaggcontext; /* current aggregate's working data */
ExprContext *tmpcontext; /* short-term evaluation context */
+ ExprContext *rprContext; /* DEFINE clause evaluation context */
bool all_first; /* true if the scan is starting */
bool partition_spooled; /* true if all tuples in current partition
--
2.50.1 (Apple Git-155)
From 0ed285dc51016e273694e9724e588493ee243564 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Tue, 16 Jun 2026 13:02:05 +0900
Subject: [PATCH 05/13] Drive RPR row pattern matching once per row
Row pattern matching only advanced when a window function read the frame,
so a row whose only window function skips the frame (e.g. nth_value() with
a NULL offset) left the match state behind the current row, producing
silently wrong results and a spurious "cannot fetch row before mark
position" error.
Advance the match once per row, before the window functions run, so it
tracks the row scan rather than frame access. Extract the reduced-frame
loop into advance_reduced_frame_nfa() and the mark advance into
advance_nav_mark(), advancing the navigation mark from the frontier the
match reached rather than from the output row, so tuplestore_trim() frees
rows sooner.
Add regression coverage for the frame-skipping and PREV-only deferred-frame
cases, and assert the contexts' nondecreasing matchStartRow ordering.
---
src/backend/executor/execRPR.c | 8 +-
src/backend/executor/nodeWindowAgg.c | 254 ++++++++++++++++-----------
src/test/regress/expected/rpr.out | 149 ++++++++++++++++
src/test/regress/sql/rpr.sql | 75 ++++++++
4 files changed, 385 insertions(+), 101 deletions(-)
diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index de78b06d277..cea7e0b2973 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -1666,7 +1666,13 @@ ExecRPRStartContext(WindowAggState *winstate, int64 startPos)
ctx->states->isAbsorbable = false;
}
- /* Add to tail of active context list (doubly-linked, oldest-first) */
+ /*
+ * Add to tail of active context list (doubly-linked, oldest-first).
+ * matchStartRow is nondecreasing along the list, so the head holds the
+ * smallest -- an ordering other code relies on.
+ */
+ Assert(winstate->nfaContextTail == NULL ||
+ startPos >= winstate->nfaContextTail->matchStartRow);
ctx->prev = winstate->nfaContextTail;
ctx->next = NULL;
if (winstate->nfaContextTail != NULL)
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 90f33bdee40..2d97710da8a 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -239,6 +239,10 @@ static int64 row_is_in_reduced_frame(WindowObject winobj, int64 pos);
static void clear_reduced_frame(WindowAggState *winstate);
static int get_reduced_frame_status(WindowAggState *winstate, int64 pos);
+static void advance_nav_mark(WindowAggState *winstate, int64 currentPos);
+static void advance_reduced_frame_nfa(WindowObject winobj,
+ RPRNFAContext *targetCtx, int64 pos,
+ bool hasLimitedFrame, int64 frameOffset);
static void update_reduced_frame(WindowObject winobj, int64 pos);
/* Forward declarations - NFA row evaluation */
@@ -2521,6 +2525,16 @@ ExecWindowAgg(PlanState *pstate)
{
if (winstate->rpSkipTo == ST_NEXT_ROW)
clear_reduced_frame(winstate);
+
+ /*
+ * Drive the row pattern match every row, so it tracks the row
+ * scan rather than frame access: a window function that skips
+ * the frame (e.g. nth_value() with a NULL offset) must not
+ * leave the match state behind currentpos.
+ */
+ Assert(winstate->nav_winobj != NULL);
+ (void) row_is_in_reduced_frame(winstate->nav_winobj,
+ winstate->currentpos);
}
/*
@@ -2562,43 +2576,6 @@ 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 navigation.
- */
- if (winstate->nav_winobj &&
- winstate->rpPattern != NULL &&
- winstate->navMaxOffsetKind == RPR_NAV_OFFSET_FIXED)
- {
- int64 navmarkpos;
-
- /* Backward reach from PREV/LAST/compound PREV_LAST/NEXT_LAST */
- if (winstate->currentpos > winstate->navMaxOffset)
- navmarkpos = winstate->currentpos - winstate->navMaxOffset;
- else
- navmarkpos = 0;
-
- /*
- * If FIRST is used, also consider match_start + navFirstOffset.
- * The oldest active context (nfaContext) has the smallest
- * matchStartRow.
- */
- if (winstate->hasFirstNav &&
- winstate->navFirstOffsetKind == RPR_NAV_OFFSET_FIXED &&
- winstate->nfaContext != NULL)
- {
- int64 firstreach;
-
- if (!pg_add_s64_overflow(winstate->nfaContext->matchStartRow,
- winstate->navFirstOffset,
- &firstreach))
- navmarkpos = Min(navmarkpos, Max(firstreach, 0));
- }
-
- if (navmarkpos > winstate->nav_winobj->markpos)
- WinSetMarkPosition(winstate->nav_winobj, navmarkpos);
- }
-
/*
* Truncate any no-longer-needed rows from the tuplestore.
*/
@@ -4382,6 +4359,143 @@ get_reduced_frame_status(WindowAggState *winstate, int64 pos)
return RF_SKIPPED;
}
+/*
+ * advance_nav_mark
+ * Advance the RPR navigation mark, derived from the NFA frontier
+ * (currentPos) but held back by the navigation's backward reach, so
+ * tuplestore_trim() can free rows no longer reachable by navigation.
+ *
+ * The nav read pointer is independent of the aggregate and per-function read
+ * pointers, so moving its mark does not affect their fetches; it only bounds
+ * the DEFINE clause's own PREV/LAST/FIRST lookups. Backward reach (PREV/LAST)
+ * is measured from the frontier. FIRST reaches back from the head context's
+ * matchStartRow instead, so it is bounded separately; without FIRST the mark
+ * can follow the frontier freely.
+ */
+static void
+advance_nav_mark(WindowAggState *winstate, int64 currentPos)
+{
+ int64 navmarkpos;
+
+ /* No RPR navigation read pointer: nothing to advance */
+ if (winstate->nav_winobj == NULL)
+ return;
+
+ /* RETAIN_ALL disables trim for the backward (PREV/LAST) dimension */
+ if (winstate->navMaxOffsetKind == RPR_NAV_OFFSET_RETAIN_ALL)
+ return;
+
+ /* navMax is FIXED here: NEEDS_EVAL resolved, RETAIN_ALL returned */
+ Assert(winstate->navMaxOffsetKind == RPR_NAV_OFFSET_FIXED);
+
+ if (currentPos > winstate->navMaxOffset)
+ navmarkpos = currentPos - winstate->navMaxOffset;
+ else
+ navmarkpos = 0;
+
+ if (winstate->hasFirstNav && winstate->nfaContext != NULL)
+ {
+ int64 firstreach;
+
+ /* navFirst is always FIXED; it never takes RETAIN_ALL */
+ Assert(winstate->navFirstOffsetKind == RPR_NAV_OFFSET_FIXED);
+
+ /*
+ * Head context has the smallest matchStartRow (contexts appended in
+ * nondecreasing order), so bounding by it covers every FIRST reach.
+ */
+ if (!pg_add_s64_overflow(winstate->nfaContext->matchStartRow,
+ winstate->navFirstOffset,
+ &firstreach))
+ navmarkpos = Min(navmarkpos, Max(firstreach, 0));
+ }
+
+ if (navmarkpos > winstate->nav_winobj->markpos)
+ WinSetMarkPosition(winstate->nav_winobj, navmarkpos);
+}
+
+/*
+ * advance_reduced_frame_nfa
+ * Drive the NFA forward until targetCtx completes or the partition ends.
+ *
+ * This is the match driver, extracted from update_reduced_frame(), which calls
+ * it to advance the match and then records the resolved result. Row
+ * evaluations are shared across all active contexts.
+ */
+static void
+advance_reduced_frame_nfa(WindowObject winobj, RPRNFAContext *targetCtx,
+ int64 pos, bool hasLimitedFrame, int64 frameOffset)
+{
+ WindowAggState *winstate = winobj->winstate;
+ int64 currentPos;
+ int64 startPos;
+
+ /*
+ * Determine where to start processing. Usually nfaLastProcessedRow+1 >=
+ * pos since contexts are created at currentPos+1 during processing.
+ * However, pos can exceed this when rows are skipped (e.g., unmatched
+ * rows don't update nfaLastProcessedRow).
+ */
+ startPos = Max(pos, winstate->nfaLastProcessedRow + 1);
+
+ /*
+ * Process rows until target context completes or we hit boundaries. Each
+ * row evaluation is shared across all active contexts.
+ */
+ for (currentPos = startPos; targetCtx->states != NULL; currentPos++)
+ {
+ bool rowExists;
+
+ /*
+ * Evaluate variables for this row - done only once, shared by all
+ * 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 */
+ if (!rowExists)
+ {
+ ExecRPRFinalizeAllContexts(winstate, currentPos - 1);
+ /* Clean up dead contexts from finalization */
+ ExecRPRCleanupDeadContexts(winstate, targetCtx);
+ break;
+ }
+
+ /* Update last processed row */
+ winstate->nfaLastProcessedRow = currentPos;
+
+ /*--------------------------
+ * Process all contexts for this row:
+ * 1. Match all (convergence)
+ * 2. Absorb redundant
+ * 3. Advance all (divergence)
+ */
+ ExecRPRProcessRow(winstate, currentPos, hasLimitedFrame, frameOffset);
+
+ /*
+ * Create a new context for the next potential start position. This
+ * enables overlapping match detection for SKIP TO NEXT ROW.
+ */
+ ExecRPRStartContext(winstate, currentPos + 1);
+
+ /*
+ * Clean up dead contexts (failed with no active states and no match).
+ * This removes contexts that failed during processing and counts them
+ * appropriately as pruned or mismatched.
+ */
+ ExecRPRCleanupDeadContexts(winstate, targetCtx);
+
+ /* Advance the nav mark to the frontier so trim can free old rows. */
+ advance_nav_mark(winstate, currentPos);
+ }
+}
+
/*
* update_reduced_frame
* Update reduced frame info using multi-context NFA pattern matching.
@@ -4401,8 +4515,6 @@ update_reduced_frame(WindowObject winobj, int64 pos)
{
WindowAggState *winstate = winobj->winstate;
RPRNFAContext *targetCtx;
- int64 currentPos;
- int64 startPos;
int frameOptions = winstate->frameOptions;
bool hasLimitedFrame;
int64 frameOffset = 0;
@@ -4468,67 +4580,9 @@ update_reduced_frame(WindowObject winobj, int64 pos)
goto register_result;
}
- /*
- * Determine where to start processing. Usually nfaLastProcessedRow+1 >=
- * pos since contexts are created at currentPos+1 during processing.
- * However, pos can exceed this when rows are skipped (e.g., unmatched
- * rows don't update nfaLastProcessedRow).
- */
- startPos = Max(pos, winstate->nfaLastProcessedRow + 1);
-
- /*
- * Process rows until target context completes or we hit boundaries. Each
- * row evaluation is shared across all active contexts.
- */
- for (currentPos = startPos; targetCtx->states != NULL; currentPos++)
- {
- bool rowExists;
-
- /*
- * Evaluate variables for this row - done only once, shared by all
- * 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 */
- if (!rowExists)
- {
- ExecRPRFinalizeAllContexts(winstate, currentPos - 1);
- /* Clean up dead contexts from finalization */
- ExecRPRCleanupDeadContexts(winstate, targetCtx);
- break;
- }
-
- /* Update last processed row */
- winstate->nfaLastProcessedRow = currentPos;
-
- /*--------------------------
- * Process all contexts for this row:
- * 1. Match all (convergence)
- * 2. Absorb redundant
- * 3. Advance all (divergence)
- */
- ExecRPRProcessRow(winstate, currentPos, hasLimitedFrame, frameOffset);
-
- /*
- * Create a new context for the next potential start position. This
- * enables overlapping match detection for SKIP TO NEXT ROW.
- */
- ExecRPRStartContext(winstate, currentPos + 1);
-
- /*
- * Clean up dead contexts (failed with no active states and no match).
- * This removes contexts that failed during processing and counts them
- * appropriately as pruned or mismatched.
- */
- ExecRPRCleanupDeadContexts(winstate, targetCtx);
- }
+ /* Drive the NFA forward until pos's match is resolved. */
+ advance_reduced_frame_nfa(winobj, targetCtx, pos, hasLimitedFrame,
+ frameOffset);
register_result:
Assert(pos == targetCtx->matchStartRow);
diff --git a/src/test/regress/expected/rpr.out b/src/test/regress/expected/rpr.out
index dc5140fecc9..6ad830b9e36 100644
--- a/src/test/regress/expected/rpr.out
+++ b/src/test/regress/expected/rpr.out
@@ -3297,6 +3297,155 @@ WINDOW w AS (
4 | 20 | | 0
(4 rows)
+--
+-- nth_value with a NULL offset
+--
+CREATE TABLE rpr_dormant (id int, price int);
+INSERT INTO rpr_dormant SELECT g, g*10 FROM generate_series(1,60) g;
+-- reference: first_value(id) is the start row of the match beginning at the
+-- current row, count(*) is that match's length over the reduced frame
+SELECT * FROM (
+ SELECT id, first_value(id) OVER w AS match_start, count(*) OVER w AS match_len
+ FROM rpr_dormant
+ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+) DEFINE A AS price > PREV(FIRST(price), 50))
+) s WHERE id > 50 ORDER BY id;
+ id | match_start | match_len
+----+-------------+-----------
+ 51 | 51 | 10
+ 52 | | 0
+ 53 | | 0
+ 54 | | 0
+ 55 | | 0
+ 56 | | 0
+ 57 | | 0
+ 58 | | 0
+ 59 | | 0
+ 60 | | 0
+(10 rows)
+
+-- nth_value with a NULL offset; FIRST navigation in DEFINE, SKIP PAST LAST ROW
+SELECT * FROM (
+ SELECT id, nv FROM (
+ SELECT id, nth_value(price, CASE WHEN id < 50 THEN NULL ELSE 1 END) OVER w AS nv
+ FROM rpr_dormant
+ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+) DEFINE A AS price > PREV(FIRST(price), 50))
+ ) s
+) t WHERE id > 50 ORDER BY id;
+ id | nv
+----+-----
+ 51 | 510
+ 52 |
+ 53 |
+ 54 |
+ 55 |
+ 56 |
+ 57 |
+ 58 |
+ 59 |
+ 60 |
+(10 rows)
+
+-- the same window with first_value and count alongside nth_value
+SELECT * FROM (
+ SELECT id, nv, fv, cnt FROM (
+ SELECT id, nth_value(price, CASE WHEN id < 50 THEN NULL ELSE 1 END) OVER w AS nv,
+ first_value(id) OVER w AS fv, count(*) OVER w AS cnt
+ FROM rpr_dormant
+ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+) DEFINE A AS price > PREV(FIRST(price), 50))
+ ) s
+) t WHERE id > 50 ORDER BY id;
+ id | nv | fv | cnt
+----+-----+----+-----
+ 51 | 510 | 51 | 10
+ 52 | | | 0
+ 53 | | | 0
+ 54 | | | 0
+ 55 | | | 0
+ 56 | | | 0
+ 57 | | | 0
+ 58 | | | 0
+ 59 | | | 0
+ 60 | | | 0
+(10 rows)
+
+-- the same nth_value with a non-navigation DEFINE
+SELECT * FROM (
+ SELECT id, nv FROM (
+ SELECT id, nth_value(price, CASE WHEN id < 50 THEN NULL ELSE 1 END) OVER w AS nv
+ FROM rpr_dormant
+ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+) DEFINE A AS price > 0)
+ ) s
+) t WHERE id > 50 ORDER BY id;
+ id | nv
+----+----
+ 51 |
+ 52 |
+ 53 |
+ 54 |
+ 55 |
+ 56 |
+ 57 |
+ 58 |
+ 59 |
+ 60 |
+(10 rows)
+
+-- the same nth_value with a PREV-only DEFINE (no FIRST navigation)
+SELECT * FROM (
+ SELECT id, nv FROM (
+ SELECT id, nth_value(price, CASE WHEN id < 50 THEN NULL ELSE 1 END) OVER w AS nv
+ FROM rpr_dormant
+ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+) DEFINE A AS price > PREV(price, 50))
+ ) s
+) t WHERE id > 50 ORDER BY id;
+ id | nv
+----+-----
+ 51 | 510
+ 52 |
+ 53 |
+ 54 |
+ 55 |
+ 56 |
+ 57 |
+ 58 |
+ 59 |
+ 60 |
+(10 rows)
+
+-- nth_value with a NULL offset band in the middle of the partition
+SELECT * FROM (
+ SELECT id, nv FROM (
+ SELECT id, nth_value(price, CASE WHEN id BETWEEN 20 AND 40 THEN NULL ELSE 1 END) OVER w AS nv
+ FROM rpr_dormant
+ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+) DEFINE A AS price > PREV(FIRST(price), 50))
+ ) s
+) t WHERE id BETWEEN 38 AND 46 ORDER BY id;
+ id | nv
+----+----
+ 38 |
+ 39 |
+ 40 |
+ 41 |
+ 42 |
+ 43 |
+ 44 |
+ 45 |
+ 46 |
+(9 rows)
+
+DROP TABLE rpr_dormant;
--
-- NULL handling
--
diff --git a/src/test/regress/sql/rpr.sql b/src/test/regress/sql/rpr.sql
index e3e9de789db..3363691c041 100644
--- a/src/test/regress/sql/rpr.sql
+++ b/src/test/regress/sql/rpr.sql
@@ -1812,6 +1812,81 @@ WINDOW w AS (
B AS val IS NULL
);
+--
+-- nth_value with a NULL offset
+--
+
+CREATE TABLE rpr_dormant (id int, price int);
+INSERT INTO rpr_dormant SELECT g, g*10 FROM generate_series(1,60) g;
+
+-- reference: first_value(id) is the start row of the match beginning at the
+-- current row, count(*) is that match's length over the reduced frame
+SELECT * FROM (
+ SELECT id, first_value(id) OVER w AS match_start, count(*) OVER w AS match_len
+ FROM rpr_dormant
+ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+) DEFINE A AS price > PREV(FIRST(price), 50))
+) s WHERE id > 50 ORDER BY id;
+
+-- nth_value with a NULL offset; FIRST navigation in DEFINE, SKIP PAST LAST ROW
+SELECT * FROM (
+ SELECT id, nv FROM (
+ SELECT id, nth_value(price, CASE WHEN id < 50 THEN NULL ELSE 1 END) OVER w AS nv
+ FROM rpr_dormant
+ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+) DEFINE A AS price > PREV(FIRST(price), 50))
+ ) s
+) t WHERE id > 50 ORDER BY id;
+
+-- the same window with first_value and count alongside nth_value
+SELECT * FROM (
+ SELECT id, nv, fv, cnt FROM (
+ SELECT id, nth_value(price, CASE WHEN id < 50 THEN NULL ELSE 1 END) OVER w AS nv,
+ first_value(id) OVER w AS fv, count(*) OVER w AS cnt
+ FROM rpr_dormant
+ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+) DEFINE A AS price > PREV(FIRST(price), 50))
+ ) s
+) t WHERE id > 50 ORDER BY id;
+
+-- the same nth_value with a non-navigation DEFINE
+SELECT * FROM (
+ SELECT id, nv FROM (
+ SELECT id, nth_value(price, CASE WHEN id < 50 THEN NULL ELSE 1 END) OVER w AS nv
+ FROM rpr_dormant
+ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+) DEFINE A AS price > 0)
+ ) s
+) t WHERE id > 50 ORDER BY id;
+
+-- the same nth_value with a PREV-only DEFINE (no FIRST navigation)
+SELECT * FROM (
+ SELECT id, nv FROM (
+ SELECT id, nth_value(price, CASE WHEN id < 50 THEN NULL ELSE 1 END) OVER w AS nv
+ FROM rpr_dormant
+ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+) DEFINE A AS price > PREV(price, 50))
+ ) s
+) t WHERE id > 50 ORDER BY id;
+
+-- nth_value with a NULL offset band in the middle of the partition
+SELECT * FROM (
+ SELECT id, nv FROM (
+ SELECT id, nth_value(price, CASE WHEN id BETWEEN 20 AND 40 THEN NULL ELSE 1 END) OVER w AS nv
+ FROM rpr_dormant
+ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+) DEFINE A AS price > PREV(FIRST(price), 50))
+ ) s
+) t WHERE id BETWEEN 38 AND 46 ORDER BY id;
+
+DROP TABLE rpr_dormant;
+
--
-- NULL handling
--
--
2.50.1 (Apple Git-155)
From b8484088192a3ea8d24bdbff4b4596450e1383c3 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Wed, 17 Jun 2026 13:12:36 +0900
Subject: [PATCH 06/13] Tidy up row pattern recognition plumbing
Several behavior-neutral cleanups to the row pattern recognition code,
with no change to planner or executor output:
- Remove the dead collectPatternVariables() and buildDefineVariableList()
helpers. The parser already guarantees that every DEFINE variable
appears in PATTERN, so create_windowagg_plan() and cost_windowagg() walk
the DEFINE clause directly instead.
- Drop the redundant rpSkipTo and defineClause arguments of
make_windowagg(); both are read from the WindowClause.
- Mark RPRNavExpr.resulttype as query_jumble_ignore, matching the other
derived result-type fields.
- Rename WindowAggState.defineClauseList to defineClauseExprs to reflect
that it stores ExprState nodes.
- Replace a post-loop ListCell NULL test in remove_unused_subquery_outputs()
with a boolean flag, flatten the nested block in show_window_def(), and
flatten the DEFINE init loop in ExecInitWindowAgg().
- Minor regression-test comment cleanup.
---
src/backend/commands/explain.c | 94 +++++++++----------
src/backend/executor/README.rpr | 5 +-
src/backend/executor/execRPR.c | 2 +-
src/backend/executor/nodeWindowAgg.c | 41 ++++----
src/backend/optimizer/path/allpaths.c | 13 ++-
src/backend/optimizer/path/costsize.c | 22 +----
src/backend/optimizer/plan/createplan.c | 24 +++--
src/backend/optimizer/plan/rpr.c | 77 ---------------
src/include/nodes/execnodes.h | 2 +-
src/include/nodes/primnodes.h | 3 +-
src/include/optimizer/rpr.h | 3 -
src/test/regress/expected/rpr_integration.out | 29 ++----
src/test/regress/sql/rpr_integration.sql | 31 ++----
13 files changed, 110 insertions(+), 236 deletions(-)
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 70fd7f386a0..696bdb9c8b5 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -3212,77 +3212,73 @@ show_window_def(WindowAggState *planstate, List *ancestors, ExplainState *es)
/* Show Row Pattern Recognition pattern if present */
if (wagg->rpPattern != NULL)
{
+ RPRNavOffsetKind maxKind = wagg->navMaxOffsetKind;
+ int64 maxOffset = wagg->navMaxOffset;
+ RPRNavOffsetKind firstKind = wagg->navFirstOffsetKind;
+ int64 firstOffset = wagg->navFirstOffset;
+
char *patternStr = deparse_rpr_pattern(wagg->rpPattern);
- if (patternStr != NULL)
- {
- ExplainPropertyText("Pattern", patternStr, es);
- pfree(patternStr);
- }
+ ExplainPropertyText("Pattern", patternStr, es);
+
+ pfree(patternStr);
/*
* Show navigation offsets for tuplestore trim. For EXPLAIN ANALYZE,
* use the executor-resolved values (which may differ from the plan
* when NEEDS_EVAL was resolved to FIXED or RETAIN_ALL at init).
*/
+ if (es->analyze)
{
- RPRNavOffsetKind maxKind = wagg->navMaxOffsetKind;
- int64 maxOffset = wagg->navMaxOffset;
- RPRNavOffsetKind firstKind = wagg->navFirstOffsetKind;
- int64 firstOffset = wagg->navFirstOffset;
+ maxKind = planstate->navMaxOffsetKind;
+ maxOffset = planstate->navMaxOffset;
+ firstKind = planstate->navFirstOffsetKind;
+ firstOffset = planstate->navFirstOffset;
+ }
- if (es->analyze)
- {
- maxKind = planstate->navMaxOffsetKind;
- maxOffset = planstate->navMaxOffset;
- firstKind = planstate->navFirstOffsetKind;
- firstOffset = planstate->navFirstOffset;
- }
+ switch (maxKind)
+ {
+ case RPR_NAV_OFFSET_NEEDS_EVAL:
+ ExplainPropertyText("Nav Mark Lookback", "runtime", es);
+ break;
+ case RPR_NAV_OFFSET_RETAIN_ALL:
+ ExplainPropertyText("Nav Mark Lookback", "retain all", es);
+ break;
+ case RPR_NAV_OFFSET_FIXED:
+ ExplainPropertyInteger("Nav Mark Lookback", NULL,
+ maxOffset, es);
+ break;
+ default:
+ elog(ERROR, "unrecognized RPR nav offset kind: %d",
+ maxKind);
+ break;
+ }
- switch (maxKind)
+ if (wagg->hasFirstNav)
+ {
+ switch (firstKind)
{
case RPR_NAV_OFFSET_NEEDS_EVAL:
- ExplainPropertyText("Nav Mark Lookback", "runtime", es);
+ ExplainPropertyText("Nav Mark Lookahead", "runtime",
+ es);
break;
case RPR_NAV_OFFSET_RETAIN_ALL:
- ExplainPropertyText("Nav Mark Lookback", "retain all", es);
+ ExplainPropertyText("Nav Mark Lookahead", "retain all",
+ es);
break;
case RPR_NAV_OFFSET_FIXED:
- ExplainPropertyInteger("Nav Mark Lookback", NULL,
- maxOffset, es);
+ if (firstOffset == PG_INT64_MAX)
+ ExplainPropertyText("Nav Mark Lookahead", "infinite",
+ es);
+ else
+ ExplainPropertyInteger("Nav Mark Lookahead", NULL,
+ firstOffset, es);
break;
default:
elog(ERROR, "unrecognized RPR nav offset kind: %d",
- maxKind);
+ firstKind);
break;
}
-
- if (wagg->hasFirstNav)
- {
- switch (firstKind)
- {
- case RPR_NAV_OFFSET_NEEDS_EVAL:
- ExplainPropertyText("Nav Mark Lookahead", "runtime",
- es);
- break;
- case RPR_NAV_OFFSET_RETAIN_ALL:
- ExplainPropertyText("Nav Mark Lookahead", "retain all",
- es);
- break;
- case RPR_NAV_OFFSET_FIXED:
- if (firstOffset == PG_INT64_MAX)
- ExplainPropertyText("Nav Mark Lookahead", "infinite",
- es);
- else
- ExplainPropertyInteger("Nav Mark Lookahead", NULL,
- firstOffset, es);
- break;
- default:
- elog(ERROR, "unrecognized RPR nav offset kind: %d",
- firstKind);
- break;
- }
- }
}
}
}
diff --git a/src/backend/executor/README.rpr b/src/backend/executor/README.rpr
index 00af86681b8..713ad84e1d9 100644
--- a/src/backend/executor/README.rpr
+++ b/src/backend/executor/README.rpr
@@ -188,7 +188,6 @@ Chapter IV Compilation Phase
IV-1. Entry Point
create_windowagg_plan() (createplan.c)
- +-- buildDefineVariableList() Build variable name list from DEFINE
+-- buildRPRPattern() NFA compilation (6 phases)
IV-2. The 6 Phases of buildRPRPattern()
@@ -1468,8 +1467,6 @@ Appendix A. Key Function Index
--------------------------------------------------------------------------
transformRPR parse_rpr.c Parser entry point
transformDefineClause parse_rpr.c DEFINE transformation
- collectPatternVariables rpr.c Variable collection
- buildDefineVariableList rpr.c DEFINE variable list
buildRPRPattern rpr.c NFA compilation main
optimizeRPRPattern rpr.c AST optimization
fillRPRPattern rpr.c NFA element generation
@@ -1538,7 +1535,7 @@ Appendix B. Data Structure Relationship Diagram
|--- rpSkipTo: RPSkipTo (AFTER MATCH SKIP mode)
|--- rpPattern: RPRPattern* (copied from plan)
|--- defineVariableList: List<String> (variable names, DEFINE order)
- |--- defineClauseList: List<ExprState>
+ |--- defineClauseExprs: List<ExprState>
|--- nfaVarMatched: bool[] (per-row cache)
|--- defineMatchStartDependent: Bitmapset* (match_start_dependent
| DEFINE vars; see VI-4)
diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index cea7e0b2973..def0b8423b3 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -1606,7 +1606,7 @@ nfa_reevaluate_dependent_vars(WindowAggState *winstate, RPRNFAContext *ctx,
/* Invalidate nav_slot cache since match_start changed */
winstate->nav_slot_pos = -1;
- foreach_ptr(ExprState, exprState, winstate->defineClauseList)
+ foreach_ptr(ExprState, exprState, winstate->defineClauseExprs)
{
int varIdx = foreach_current_index(exprState);
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 2d97710da8a..5d4832d1db9 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -3067,27 +3067,26 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
/* Set up row pattern recognition DEFINE clause */
winstate->defineVariableList = NIL;
- winstate->defineClauseList = NIL;
- if (node->defineClause != NIL)
+ winstate->defineClauseExprs = NIL;
+
+ /*
+ * 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_node(TargetEntry, te, node->defineClause)
{
- /*
- * 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_node(TargetEntry, te, node->defineClause)
- {
- char *name = te->resname;
- Expr *expr = te->expr;
- ExprState *exps;
-
- winstate->defineVariableList =
- lappend(winstate->defineVariableList,
- makeString(pstrdup(name)));
- exps = ExecInitExpr(expr, (PlanState *) winstate);
- winstate->defineClauseList =
- lappend(winstate->defineClauseList, exps);
- }
+ char *name = te->resname;
+ ExprState *exprstate;
+
+ winstate->defineVariableList =
+ lappend(winstate->defineVariableList,
+ makeString(pstrdup(name)));
+
+ exprstate = ExecInitExpr(te->expr, (PlanState *) winstate);
+
+ winstate->defineClauseExprs =
+ lappend(winstate->defineClauseExprs, exprstate);
}
/* Initialize NFA free lists for row pattern matching */
@@ -4669,7 +4668,7 @@ nfa_evaluate_row(WindowObject winobj, int64 pos, bool *varMatched)
/* Invalidate nav_slot cache so PREV/NEXT re-fetch for new row */
winstate->nav_slot_pos = -1;
- foreach_ptr(ExprState, exprState, winstate->defineClauseList)
+ foreach_ptr(ExprState, exprState, winstate->defineClauseExprs)
{
Datum result;
bool isnull;
diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c
index f3c9f3c0bd6..44864b3635b 100644
--- a/src/backend/optimizer/path/allpaths.c
+++ b/src/backend/optimizer/path/allpaths.c
@@ -4807,20 +4807,19 @@ remove_unused_subquery_outputs(Query *subquery, RelOptInfo *rel,
*/
if (IsA(texpr, WindowFunc))
{
+ bool is_rpr = false;
WindowFunc *wfunc = (WindowFunc *) texpr;
- ListCell *wlc;
- foreach(wlc, subquery->windowClause)
+ foreach_node(WindowClause, wc, subquery->windowClause)
{
- WindowClause *wc = lfirst_node(WindowClause, wlc);
-
- if (wc->winref == wfunc->winref &&
- wc->defineClause != NIL)
+ if (wc->winref == wfunc->winref && wc->defineClause != NIL)
{
+ is_rpr = true;
break;
}
}
- if (wlc != NULL)
+
+ if (is_rpr)
continue;
}
diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c
index 82472c3fe96..32a4b172e3d 100644
--- a/src/backend/optimizer/path/costsize.c
+++ b/src/backend/optimizer/path/costsize.c
@@ -3231,30 +3231,18 @@ cost_windowagg(Path *path, PlannerInfo *root,
* so we'll just do this for now.
*
* Moreover, if row pattern recognition is used, we charge the DEFINE
- * expressions once per tuple for each variable that appears in PATTERN.
+ * expressions once per tuple for each DEFINE variable.
*/
if (winclause->rpPattern)
{
- List *pattern_vars;
QualCost defcosts;
- pattern_vars = collectPatternVariables(winclause->rpPattern);
-
- foreach_node(String, pv, pattern_vars)
+ foreach_node(TargetEntry, def, winclause->defineClause)
{
- char *ptname = strVal(pv);
-
- foreach_node(TargetEntry, def, winclause->defineClause)
- {
- if (!strcmp(ptname, def->resname))
- {
- cost_qual_eval_node(&defcosts, (Node *) def->expr, root);
- startup_cost += defcosts.startup;
- total_cost += defcosts.per_tuple * input_tuples;
- }
- }
+ cost_qual_eval_node(&defcosts, (Node *) def->expr, root);
+ startup_cost += defcosts.startup;
+ total_cost += defcosts.per_tuple * input_tuples;
}
- list_free_deep(pattern_vars);
}
foreach(lc, windowFuncs)
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index cca4126e511..d96e76b0221 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -290,9 +290,8 @@ static Memoize *make_memoize(Plan *lefttree, Oid *hashoperators,
static WindowAgg *make_windowagg(List *tlist, WindowClause *wc,
int partNumCols, AttrNumber *partColIdx, Oid *partOperators, Oid *partCollations,
int ordNumCols, AttrNumber *ordColIdx, Oid *ordOperators, Oid *ordCollations,
- List *runCondition, RPSkipTo rpSkipTo,
+ List *runCondition,
RPRPattern *compiledPattern,
- List *defineClause,
Bitmapset *defineMatchStartDependent,
RPRNavOffsetKind navMaxOffsetKind, int64 navMaxOffset,
bool hasFirstNav,
@@ -2822,7 +2821,6 @@ create_windowagg_plan(PlannerInfo *root, WindowAggPath *best_path)
Oid *ordCollations;
ListCell *lc;
List *defineVariableList = NIL;
- List *filteredDefineClause = NIL;
RPRPattern *compiledPattern = NULL;
Bitmapset *matchStartDependent = NULL;
RPRNavOffsetKind navMaxOffsetKind = RPR_NAV_OFFSET_FIXED;
@@ -2889,8 +2887,9 @@ create_windowagg_plan(PlannerInfo *root, WindowAggPath *best_path)
* rejects DEFINE variables not used in PATTERN, so no filtering is
* needed.
*/
- buildDefineVariableList(wc->defineClause, &defineVariableList);
- filteredDefineClause = wc->defineClause;
+ foreach_node(TargetEntry, te, wc->defineClause)
+ defineVariableList = lappend(defineVariableList,
+ makeString(pstrdup(te->resname)));
/*
* Walk DEFINE once: collect nav offsets (for tuplestore trim) and the
@@ -2923,13 +2922,13 @@ create_windowagg_plan(PlannerInfo *root, WindowAggPath *best_path)
ordOperators,
ordCollations,
best_path->runCondition,
- wc->rpSkipTo,
compiledPattern,
- filteredDefineClause,
matchStartDependent,
- navMaxOffsetKind, navMaxOffset,
+ navMaxOffsetKind,
+ navMaxOffset,
hasFirstNav,
- navFirstOffsetKind, navFirstOffset,
+ navFirstOffsetKind,
+ navFirstOffset,
best_path->qual,
best_path->topwindow,
subplan);
@@ -7000,9 +6999,8 @@ static WindowAgg *
make_windowagg(List *tlist, WindowClause *wc,
int partNumCols, AttrNumber *partColIdx, Oid *partOperators, Oid *partCollations,
int ordNumCols, AttrNumber *ordColIdx, Oid *ordOperators, Oid *ordCollations,
- List *runCondition, RPSkipTo rpSkipTo,
+ List *runCondition,
RPRPattern *compiledPattern,
- List *defineClause,
Bitmapset *defineMatchStartDependent,
RPRNavOffsetKind navMaxOffsetKind, int64 navMaxOffset,
bool hasFirstNav,
@@ -7034,12 +7032,12 @@ make_windowagg(List *tlist, WindowClause *wc,
node->inRangeAsc = wc->inRangeAsc;
node->inRangeNullsFirst = wc->inRangeNullsFirst;
node->topWindow = topWindow;
- node->rpSkipTo = rpSkipTo;
+ node->rpSkipTo = wc->rpSkipTo;
/* Store compiled pattern for NFA execution */
node->rpPattern = compiledPattern;
- node->defineClause = defineClause;
+ node->defineClause = wc->defineClause;
/* Store pre-computed match_start dependency bitmapset */
node->defineMatchStartDependent = defineMatchStartDependent;
diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index 50bca59451f..48b76a842ed 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -96,9 +96,6 @@ static void computeAbsorbabilityRecursive(RPRPattern *pattern,
bool *hasAbsorbable);
static void computeAbsorbability(RPRPattern *pattern);
-static void collectPatternVariablesRecursive(RPRPatternNode *node,
- List **varNames);
-
/*
* rprPatternEqual
* Compare two RPRPatternNode trees for equality.
@@ -1824,59 +1821,6 @@ computeAbsorbability(RPRPattern *pattern)
pattern->isAbsorbable = hasAbsorbable;
}
-/*
- * collectPatternVariablesRecursive
- * Recursively collect variable names from pattern AST.
- */
-static void
-collectPatternVariablesRecursive(RPRPatternNode *node, List **varNames)
-{
- Assert(node != NULL);
-
- check_stack_depth();
-
- switch (node->nodeType)
- {
- case RPR_PATTERN_VAR:
- /* Add variable if not already in list */
- foreach_node(String, varname, *varNames)
- {
- if (strcmp(strVal(varname), node->varName) == 0)
- return; /* Already collected */
- }
- *varNames = lappend(*varNames, makeString(pstrdup(node->varName)));
- break;
-
- case RPR_PATTERN_SEQ:
- case RPR_PATTERN_ALT:
- case RPR_PATTERN_GROUP:
- foreach_node(RPRPatternNode, child, node->children)
- {
- collectPatternVariablesRecursive(child, varNames);
- }
- break;
- }
-}
-
-/*
- * collectPatternVariables
- * Collect unique variable names used in PATTERN clause.
- *
- * Returns a list of String nodes containing variable names.
- * Used to filter defineClause before varId assignment.
- */
-List *
-collectPatternVariables(RPRPatternNode *pattern)
-{
- List *varNames = NIL;
-
- /* Caller ensures pattern is not NULL */
- Assert(pattern != NULL);
-
- collectPatternVariablesRecursive(pattern, &varNames);
- return varNames;
-}
-
/*
* rpr_volatile_func_checker
* check_functions_in_node callback: true if funcid is VOLATILE.
@@ -1953,27 +1897,6 @@ validate_rpr_define_volatility(List *defineClause)
}
}
-/*
- * buildDefineVariableList
- * Build defineVariableList from defineClause.
- *
- * The parser already ensures that all DEFINE variables appear in PATTERN,
- * so no filtering is needed here.
- *
- * Sets *defineVariableList to list of variable names (String nodes).
- */
-void
-buildDefineVariableList(List *defineClause, List **defineVariableList)
-{
- *defineVariableList = NIL;
-
- foreach_node(TargetEntry, te, defineClause)
- {
- *defineVariableList = lappend(*defineVariableList,
- makeString(pstrdup(te->resname)));
- }
-}
-
/*
* buildRPRPattern
* Compile pattern AST to flat bytecode array.
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 5b4ac8a6c33..59b8656c857 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -2654,7 +2654,7 @@ typedef struct WindowAggState
struct RPRPattern *rpPattern; /* compiled pattern for NFA execution */
List *defineVariableList; /* list of row pattern definition
* variables (list of String) */
- List *defineClauseList; /* expression for row pattern definition
+ List *defineClauseExprs; /* expression for row pattern definition
* search conditions ExprState list */
RPRNFAContext *nfaContext; /* active matching contexts (head) */
RPRNFAContext *nfaContextTail; /* tail of active contexts (for reverse
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index 7c1b3d1bb07..8576a3c9c5b 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -687,7 +687,8 @@ typedef struct RPRNavExpr
Expr *offset_arg; /* offset expression, or NULL for default */
Expr *compound_offset_arg; /* outer offset for compound nav, or
* NULL if simple */
- Oid resulttype; /* result type (same as arg's type) */
+ /* result type (same as arg's type) */
+ Oid resulttype pg_node_attr(query_jumble_ignore);
/* OID of collation of result */
Oid resultcollid pg_node_attr(query_jumble_ignore);
/* token location, or -1 if unknown */
diff --git a/src/include/optimizer/rpr.h b/src/include/optimizer/rpr.h
index b4f87d8caa4..23763442b65 100644
--- a/src/include/optimizer/rpr.h
+++ b/src/include/optimizer/rpr.h
@@ -67,10 +67,7 @@
#define RPRElemIsFin(e) ((e)->varId == RPR_VARID_FIN)
#define RPRElemCanSkip(e) ((e)->min == 0)
-extern List *collectPatternVariables(RPRPatternNode *pattern);
extern void validate_rpr_define_volatility(List *defineClause);
-extern void buildDefineVariableList(List *defineClause,
- List **defineVariableList);
extern RPRPattern *buildRPRPattern(RPRPatternNode *pattern, List *defineVariableList,
RPSkipTo rpSkipTo, int frameOptions,
bool hasMatchStartDependent);
diff --git a/src/test/regress/expected/rpr_integration.out b/src/test/regress/expected/rpr_integration.out
index 80a32cca9ab..652989927d7 100644
--- a/src/test/regress/expected/rpr_integration.out
+++ b/src/test/regress/expected/rpr_integration.out
@@ -240,9 +240,9 @@ ORDER BY id;
-- must therefore yield two separate WindowAgg nodes. Inline specs
-- are used here because dedup only applies to inline windows.
-- Baseline: two inline RPR windows that are structurally identical
--- (same ORDER BY, frame, PATTERN, and DEFINE) are deduped by the
--- parser into a single WindowAgg node, confirming that parser-level
--- dedup is active for RPR windows whose DEFINE matches.
+-- (same PARTITION BY, ORDER BY, frame, PATTERN, and DEFINE) are deduped by the
+-- parser into a single WindowAgg node, confirming that parser-level dedup is
+-- active for RPR windows whose DEFINE matches.
EXPLAIN (COSTS OFF)
SELECT
count(*) OVER (ORDER BY id
@@ -325,15 +325,10 @@ ORDER BY id;
-- A5. Unused window removal prevention
-- ============================================================
-- Verify that remove_unused_subquery_outputs() does not drop an RPR
--- window function even when the outer query does not reference its
--- result. The RPR WindowAgg node is responsible for performing pattern
--- matching, so removing the window function would silently skip the
--- pattern match even though the surrounding query still depends on
--- RPR semantics.
--- The outer query ignores the per-row window result, yet pattern
--- matching must still execute. The plan must still contain a
--- WindowAgg node below the outer Aggregate; if the window were
--- removed, only Aggregate + Seq Scan would appear.
+-- window function when the outer query does not reference its result.
+-- The WindowAgg node performs the pattern match itself; without it,
+-- the match would be silently skipped. The plan must contain a
+-- WindowAgg node beneath the outer Aggregate.
EXPLAIN (COSTS OFF)
SELECT count(*) FROM (
SELECT count(*) OVER w FROM rpr_integ
@@ -587,10 +582,8 @@ WHERE cnt > 0;
-- EXPLAIN VERBOSE is therefore expected to show a clean targetlist on
-- the outer WindowAgg, with no DEFINE-derived expression leaking in.
-- Note: columns referenced by DEFINE (e.g., "val") may appear as
--- resjunk entries in upper WindowAgg targetlists -- that is a
--- harmless byproduct of the column guard's broad scope and does not
--- affect client output. The claim here is limited to the full
--- DEFINE boolean expression.
+-- resjunk entries in upper WindowAgg targetlists -- but that is harmless.
+-- The claim here is limited to the full DEFINE boolean expression.
EXPLAIN (VERBOSE, COSTS OFF)
SELECT
count(*) OVER w_rpr AS rpr_cnt,
@@ -620,10 +613,6 @@ WINDOW
Output: id, val
(13 rows)
--- Executing the same query shows the client result is limited to
--- the two projected columns; "id" and "val" that appeared in the
--- upper WindowAgg Output line are resjunk-only and do not reach
--- the client.
SELECT
count(*) OVER w_rpr AS rpr_cnt,
count(*) OVER w_normal AS normal_cnt
diff --git a/src/test/regress/sql/rpr_integration.sql b/src/test/regress/sql/rpr_integration.sql
index 1d6075265bb..2b990b24704 100644
--- a/src/test/regress/sql/rpr_integration.sql
+++ b/src/test/regress/sql/rpr_integration.sql
@@ -166,9 +166,9 @@ ORDER BY id;
-- are used here because dedup only applies to inline windows.
-- Baseline: two inline RPR windows that are structurally identical
--- (same ORDER BY, frame, PATTERN, and DEFINE) are deduped by the
--- parser into a single WindowAgg node, confirming that parser-level
--- dedup is active for RPR windows whose DEFINE matches.
+-- (same PARTITION BY, ORDER BY, frame, PATTERN, and DEFINE) are deduped by the
+-- parser into a single WindowAgg node, confirming that parser-level dedup is
+-- active for RPR windows whose DEFINE matches.
EXPLAIN (COSTS OFF)
SELECT
count(*) OVER (ORDER BY id
@@ -214,16 +214,10 @@ ORDER BY id;
-- A5. Unused window removal prevention
-- ============================================================
-- Verify that remove_unused_subquery_outputs() does not drop an RPR
--- window function even when the outer query does not reference its
--- result. The RPR WindowAgg node is responsible for performing pattern
--- matching, so removing the window function would silently skip the
--- pattern match even though the surrounding query still depends on
--- RPR semantics.
-
--- The outer query ignores the per-row window result, yet pattern
--- matching must still execute. The plan must still contain a
--- WindowAgg node below the outer Aggregate; if the window were
--- removed, only Aggregate + Seq Scan would appear.
+-- window function when the outer query does not reference its result.
+-- The WindowAgg node performs the pattern match itself; without it,
+-- the match would be silently skipped. The plan must contain a
+-- WindowAgg node beneath the outer Aggregate.
EXPLAIN (COSTS OFF)
SELECT count(*) FROM (
SELECT count(*) OVER w FROM rpr_integ
@@ -399,11 +393,8 @@ WHERE cnt > 0;
-- EXPLAIN VERBOSE is therefore expected to show a clean targetlist on
-- the outer WindowAgg, with no DEFINE-derived expression leaking in.
-- Note: columns referenced by DEFINE (e.g., "val") may appear as
--- resjunk entries in upper WindowAgg targetlists -- that is a
--- harmless byproduct of the column guard's broad scope and does not
--- affect client output. The claim here is limited to the full
--- DEFINE boolean expression.
-
+-- resjunk entries in upper WindowAgg targetlists -- but that is harmless.
+-- The claim here is limited to the full DEFINE boolean expression.
EXPLAIN (VERBOSE, COSTS OFF)
SELECT
count(*) OVER w_rpr AS rpr_cnt,
@@ -417,10 +408,6 @@ WINDOW
w_normal AS (ORDER BY id
ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING);
--- Executing the same query shows the client result is limited to
--- the two projected columns; "id" and "val" that appeared in the
--- upper WindowAgg Output line are resjunk-only and do not reach
--- the client.
SELECT
count(*) OVER w_rpr AS rpr_cnt,
count(*) OVER w_normal AS normal_cnt
--
2.50.1 (Apple Git-155)
From 4cf9108ce7796a3821873f8377c95e34799760f8 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Wed, 17 Jun 2026 14:05:25 +0900
Subject: [PATCH 07/13] Further tidy up row pattern recognition plumbing
More behavior-neutral cleanups to the row pattern recognition code,
continuing the previous tidy-up and with no change to planner or
executor output:
- Drop the now-unused WindowClause argument of transformDefineClause()
and flatten the redundant nested block in its DEFINE-variable loop.
- Replace manual ListCell iteration and index bookkeeping with
foreach_node()/foreach_current_index() in
validate_rpr_define_volatility() and nfa_evaluate_row(), and drop the
redundant end-of-list break tests in nfa_evaluate_row() and
nfa_reevaluate_dependent_vars() now that the loops walk
defineClauseExprs directly.
- Test winstate->defineVariableList against NIL instead of
list_length() > 0 in ExecInitWindowAgg().
- Remove the now-unused makefuncs.h include from plan/rpr.c.
- Fix the stale struct name in the RPCommonSyntax header comment.
---
src/backend/executor/execRPR.c | 3 -
src/backend/executor/nodeWindowAgg.c | 9 +--
src/backend/optimizer/plan/rpr.c | 7 +--
src/backend/parser/parse_rpr.c | 84 +++++++++++++---------------
src/include/nodes/parsenodes.h | 2 +-
5 files changed, 44 insertions(+), 61 deletions(-)
diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index def0b8423b3..099b81aeb81 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -1618,9 +1618,6 @@ nfa_reevaluate_dependent_vars(WindowAggState *winstate, RPRNFAContext *ctx,
result = ExecEvalExpr(exprState, econtext, &isnull);
winstate->nfaVarMatched[varIdx] = (!isnull && DatumGetBool(result));
}
-
- if (varIdx + 1 >= list_length(winstate->defineVariableList))
- break;
}
/* Restore original match_start, currentpos, and invalidate cache */
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 5d4832d1db9..819ad814bf5 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -3103,7 +3103,7 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
* ordering (DEFINE order first), varId == defineIdx for all defined
* variables, so no mapping is needed.
*/
- if (list_length(winstate->defineVariableList) > 0)
+ if (winstate->defineVariableList != NIL)
winstate->nfaVarMatched = palloc0(sizeof(bool) *
list_length(winstate->defineVariableList));
else
@@ -4642,8 +4642,6 @@ nfa_evaluate_row(WindowObject winobj, int64 pos, bool *varMatched)
{
WindowAggState *winstate = winobj->winstate;
ExprContext *econtext = winstate->rprContext;
- int numDefineVars = list_length(winstate->defineVariableList);
- int varIdx = 0;
TupleTableSlot *slot;
int64 saved_pos;
@@ -4670,6 +4668,7 @@ nfa_evaluate_row(WindowObject winobj, int64 pos, bool *varMatched)
foreach_ptr(ExprState, exprState, winstate->defineClauseExprs)
{
+ int varIdx = foreach_current_index(exprState);
Datum result;
bool isnull;
@@ -4677,10 +4676,6 @@ nfa_evaluate_row(WindowObject winobj, int64 pos, bool *varMatched)
result = ExecEvalExpr(exprState, econtext, &isnull);
varMatched[varIdx] = (!isnull && DatumGetBool(result));
-
- varIdx++;
- if (varIdx >= numDefineVars)
- break;
}
winstate->currentpos = saved_pos;
diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index 48b76a842ed..597a966c7b1 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -40,7 +40,6 @@
#include "catalog/pg_proc.h"
#include "mb/pg_wchar.h"
#include "miscadmin.h"
-#include "nodes/makefuncs.h"
#include "nodes/nodeFuncs.h"
#include "optimizer/rpr.h"
#include "tcop/tcopprot.h"
@@ -1887,12 +1886,8 @@ reject_volatile_in_define_walker(Node *node, void *context)
void
validate_rpr_define_volatility(List *defineClause)
{
- ListCell *lc;
-
- foreach(lc, defineClause)
+ foreach_node(TargetEntry, te, defineClause)
{
- TargetEntry *te = lfirst_node(TargetEntry, lc);
-
(void) reject_volatile_in_define_walker((Node *) te->expr, NULL);
}
}
diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index 8ed01bb8f28..3e6b2e579a3 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -54,8 +54,8 @@ typedef struct
/* Forward declarations */
static void validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
List *rpDefs, List **varNames);
-static List *transformDefineClause(ParseState *pstate, WindowClause *wc,
- WindowDef *windef, List **targetlist);
+static List *transformDefineClause(ParseState *pstate, WindowDef *windef,
+ List **targetlist);
static bool define_walker(Node *node, void *context);
/*
@@ -178,7 +178,7 @@ transformRPR(ParseState *pstate, WindowClause *wc, WindowDef *windef,
wc->initial = windef->rpCommonSyntax->initial;
/* Transform DEFINE clause into list of TargetEntry's */
- wc->defineClause = transformDefineClause(pstate, wc, windef, targetlist);
+ wc->defineClause = transformDefineClause(pstate, windef, targetlist);
/* Store PATTERN AST for deparsing */
wc->rpPattern = windef->rpCommonSyntax->rpPattern;
@@ -313,7 +313,7 @@ validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
* parse_expr.c via the p_rpr_pattern_vars check.
*/
static List *
-transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
+transformDefineClause(ParseState *pstate, WindowDef *windef,
List **targetlist)
{
List *restargets;
@@ -345,6 +345,8 @@ transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
foreach_node(ResTarget, restarget, windef->rpCommonSyntax->rpDefs)
{
TargetEntry *teDefine;
+ Node *expr;
+ List *vars;
name = restarget->name;
@@ -374,54 +376,48 @@ transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
* the individual Var nodes it references are present in the
* targetlist, so the planner can propagate the referenced columns.
*/
- {
- Node *expr;
- List *vars;
+ expr = transformExpr(pstate, restarget->val,
+ EXPR_KIND_RPR_DEFINE);
- 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_node(Var, var, vars)
+ {
+ bool found = false;
- /*
- * 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_node(Var, var, vars)
+ foreach_node(TargetEntry, tle, *targetlist)
{
- bool found = false;
-
- foreach_node(TargetEntry, tle, *targetlist)
+ if (IsA(tle->expr, Var) &&
+ ((Var *) tle->expr)->varno == var->varno &&
+ ((Var *) tle->expr)->varattno == var->varattno)
{
- if (IsA(tle->expr, Var) &&
- ((Var *) tle->expr)->varno == var->varno &&
- ((Var *) tle->expr)->varattno == var->varattno)
- {
- found = true;
- break;
- }
- }
- if (!found)
- {
- TargetEntry *newtle;
-
- newtle = makeTargetEntry((Expr *) copyObject(var),
- list_length(*targetlist) + 1,
- NULL,
- true);
- *targetlist = lappend(*targetlist, newtle);
+ found = true;
+ break;
}
}
- list_free(vars);
+ if (!found)
+ {
+ TargetEntry *newtle;
- /* Build the defineClause entry directly from the transformed expr */
- teDefine = makeTargetEntry((Expr *) expr,
- list_length(defineClause) + 1,
- pstrdup(name),
- true);
+ 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);
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index e309dfbdb66..947e668020e 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -644,7 +644,7 @@ typedef struct RPRPatternNode
} RPRPatternNode;
/*
- * RowPatternCommonSyntax - raw representation of row pattern common syntax
+ * RPCommonSyntax - raw representation of row pattern common syntax
*/
typedef struct RPCommonSyntax
{
--
2.50.1 (Apple Git-155)
From 7a69cb818e4d1c37998266a8c1883d5454a8d9f5 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Wed, 17 Jun 2026 14:55:51 +0900
Subject: [PATCH 08/13] Refactor transformDefineClause in row pattern
recognition
Two related cleanups to DEFINE clause parse analysis, with no change to
planner or executor output beyond one error-cursor position:
- Hoist the "DEFINE variable not used in PATTERN" cross-check out of the
recursive validateRPRPatternVarCount() into its caller. The check only
needs to run once, so the rpDefs argument and its NULL-sentinel gating
are gone, and the recursive routine now only counts pattern variables.
- Reorder per-variable DEFINE processing to transformExpr ->
coerce_to_boolean -> pull_var_clause and drop the separate second
coercion pass, so pull_var_clause always operates on the final coerced
expression and a type mismatch is reported before the targetlist is
touched. The duplicate-variable check moves to its own leading loop
and now reports at the later (duplicate) definition.
Add regression coverage for DEFINE coercion and Var propagation: a
boolean-domain predicate (the one case where coerce_to_boolean is not a
no-op), a Var referenced only inside a navigation operation, and
rejection of a non-boolean DEFINE expression.
---
src/backend/parser/parse_rpr.c | 163 +++++++++++--------------
src/test/regress/expected/rpr_base.out | 78 +++++++++++-
src/test/regress/sql/rpr_base.sql | 59 +++++++++
3 files changed, 209 insertions(+), 91 deletions(-)
diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index 3e6b2e579a3..116cd206e39 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -53,7 +53,7 @@ typedef struct
/* Forward declarations */
static void validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
- List *rpDefs, List **varNames);
+ List **varNames);
static List *transformDefineClause(ParseState *pstate, WindowDef *windef,
List **targetlist);
static bool define_walker(Node *node, void *context);
@@ -192,15 +192,14 @@ transformRPR(ParseState *pstate, WindowClause *wc, WindowDef *windef,
* Throws an error if the number of unique variables would require a varId
* greater than RPR_VARID_MAX.
*
- * If rpDefs is non-NULL, each DEFINE variable name is also validated against
- * varNames; any DEFINE name not present in PATTERN is rejected with an error.
- * varNames itself is not extended by this step -- it carries only PATTERN
- * variable names, which is what transformColumnRef checks via
- * p_rpr_pattern_vars to identify pattern variable qualifiers.
+ * varNames collects the unique PATTERN variable names, which is what
+ * transformColumnRef checks via p_rpr_pattern_vars to identify pattern
+ * variable qualifiers. Cross-checking DEFINE variable names against this
+ * list is the caller's responsibility, since it only needs to run once.
*/
static void
validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
- List *rpDefs, List **varNames)
+ List **varNames)
{
/* Pattern node must exist - parser always provides non-NULL root */
Assert(node != NULL);
@@ -255,39 +254,10 @@ validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
/* Recurse into children */
foreach_node(RPRPatternNode, child, node->children)
{
- validateRPRPatternVarCount(pstate, child, NULL, varNames);
+ validateRPRPatternVarCount(pstate, child, varNames);
}
break;
}
-
- /*
- * After the top-level call, validate that every DEFINE variable name is
- * present in the PATTERN variable list; reject names not used in PATTERN.
- * This is only done once at the outermost recursion level, detected by
- * rpDefs being non-NULL (recursive calls pass NULL).
- */
- if (rpDefs)
- {
- foreach_node(ResTarget, rt, rpDefs)
- {
- bool found = false;
-
- foreach_node(String, varname, *varNames)
- {
- if (strcmp(strVal(varname), rt->name) == 0)
- {
- found = true;
- break;
- }
- }
- if (!found)
- ereport(ERROR,
- errcode(ERRCODE_SYNTAX_ERROR),
- errmsg("DEFINE variable \"%s\" is not used in PATTERN",
- rt->name),
- parser_errposition(pstate, rt->location));
- }
- }
}
/*
@@ -296,14 +266,16 @@ validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
*
* First:
* 1. Validates PATTERN variable count and collects RPR variable names
+ * 2. Rejects DEFINE variables not used in PATTERN
+ * 3. Checks for duplicate variable names in DEFINE clause
*
* Then for each DEFINE variable:
- * 2. Checks for duplicate variable names in DEFINE clause
- * 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
+ * 4. Transforms expression via transformExpr() and coerces it to boolean
+ * 5. Creates defineClause entry with proper resname (pattern variable name)
+ * 6. Ensures referenced Var nodes are present in the query targetlist (via
+ * pull_var_clause)
+ *
+ * Finally marks column origins and assigns collation information.
*
* Note: Variables not in DEFINE are evaluated as TRUE by the executor.
* Variables in DEFINE but not in PATTERN are rejected as an error.
@@ -316,9 +288,7 @@ static List *
transformDefineClause(ParseState *pstate, WindowDef *windef,
List **targetlist)
{
- List *restargets;
List *defineClause = NIL;
- char *name;
List *patternVarNames = NIL;
/*
@@ -328,56 +298,86 @@ transformDefineClause(ParseState *pstate, WindowDef *windef,
Assert(windef->rpCommonSyntax->rpDefs != NULL);
/*
- * Validate PATTERN variable count, reject DEFINE variables not used in
- * PATTERN, and collect PATTERN variable names for transformColumnRef.
+ * Validate PATTERN variable count and collect the PATTERN variable names
+ * for transformColumnRef.
*/
validateRPRPatternVarCount(pstate, windef->rpCommonSyntax->rpPattern,
- windef->rpCommonSyntax->rpDefs,
&patternVarNames);
pstate->p_rpr_pattern_vars = patternVarNames;
+ /*
+ * Reject any DEFINE variable whose name does not appear in PATTERN. This
+ * cross-check only needs to run once, so it lives here in the caller
+ * rather than in the recursive validateRPRPatternVarCount().
+ */
+ foreach_node(ResTarget, rt, windef->rpCommonSyntax->rpDefs)
+ {
+ bool found = false;
+
+ foreach_node(String, varname, patternVarNames)
+ {
+ if (strcmp(strVal(varname), rt->name) == 0)
+ {
+ found = true;
+ break;
+ }
+ }
+ if (!found)
+ ereport(ERROR,
+ errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("DEFINE variable \"%s\" is not used in PATTERN",
+ rt->name),
+ parser_errposition(pstate, rt->location));
+ }
+
/*
* Check for duplicate row pattern definition variables. The standard
* requires that no two row pattern definition variable names shall be
- * equivalent.
+ * equivalent. Report the error at the later (duplicate) definition.
*/
- restargets = NIL;
foreach_node(ResTarget, restarget, windef->rpCommonSyntax->rpDefs)
{
- TargetEntry *teDefine;
- Node *expr;
- List *vars;
-
- name = restarget->name;
-
- foreach_node(ResTarget, r, restargets)
+ foreach_node(ResTarget, prior, windef->rpCommonSyntax->rpDefs)
{
- char *n;
-
- n = r->name;
-
- if (!strcmp(n, name))
+ if (prior == restarget)
+ break;
+ if (strcmp(prior->name, restarget->name) == 0)
ereport(ERROR,
errcode(ERRCODE_SYNTAX_ERROR),
errmsg("DEFINE variable \"%s\" appears more than once",
- name),
- parser_errposition(pstate, exprLocation((Node *) r)));
+ restarget->name),
+ parser_errposition(pstate,
+ exprLocation((Node *) restarget)));
}
+ }
- restargets = lappend(restargets, restarget);
+ foreach_node(ResTarget, restarget, windef->rpCommonSyntax->rpDefs)
+ {
+ TargetEntry *teDefine;
+ Node *expr;
+ List *vars;
/*
- * Transform the DEFINE expression. We must NOT add the whole
- * expression to the query targetlist, because it may contain
- * 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
- * targetlist, so the planner can propagate the referenced columns.
+ * Transform the DEFINE expression and coerce it to boolean. We must
+ * NOT add the whole expression to the query targetlist, because it
+ * may contain RPRNavExpr nodes (PREV/NEXT/FIRST/LAST) that can only
+ * be evaluated inside the owning WindowAgg. Coercing here, before
+ * pull_var_clause, keeps pull_var_clause operating on the final
+ * expression form and surfaces a type mismatch before the targetlist
+ * is touched.
*/
expr = transformExpr(pstate, restarget->val,
EXPR_KIND_RPR_DEFINE);
+ expr = coerce_to_boolean(pstate, expr, "DEFINE");
+
+ /* Build the defineClause entry directly from the transformed expr */
+ teDefine = makeTargetEntry((Expr *) expr,
+ list_length(defineClause) + 1,
+ pstrdup(restarget->name),
+ true);
+
+ /* build transformed DEFINE clause (list of TargetEntry) */
+ defineClause = lappend(defineClause, teDefine);
/*
* Pull out Var nodes from the transformed expression and ensure each
@@ -412,26 +412,9 @@ transformDefineClause(ParseState *pstate, WindowDef *windef,
}
}
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);
}
- list_free(restargets);
pstate->p_rpr_pattern_vars = NIL;
- /*
- * Make sure that the row pattern definition search condition is a boolean
- * expression.
- */
- foreach_ptr(TargetEntry, te, defineClause)
- te->expr = (Expr *) coerce_to_boolean(pstate, (Node *) te->expr, "DEFINE");
-
/*
* Validate DEFINE expressions: nested PREV/NEXT, column references,
* compound flatten, volatile callees -- all in a single walk per
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index cf158e1c043..80fabde514c 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -236,7 +236,7 @@ WINDOW w AS (
);
ERROR: DEFINE variable "a" appears more than once
LINE 7: DEFINE A AS id > 0, A AS id < 10
- ^
+ ^
DROP TABLE rpr_dup;
-- Boolean coercion
CREATE TABLE rpr_bool (id INT, flag BOOLEAN);
@@ -319,6 +319,82 @@ DROP CAST (truthyint AS boolean);
DROP FUNCTION truthyint_to_bool(truthyint);
DROP TYPE truthyint;
DROP TABLE rpr_bool;
+-- Coercion over a boolean domain is not a no-op; the wrapped Var must still
+-- propagate when referenced only in DEFINE (flag is not in the select list)
+CREATE DOMAIN boolish AS boolean;
+CREATE TABLE rpr_domain (id int, flag boolish);
+INSERT INTO rpr_domain VALUES (1, true), (2, false), (3, true);
+SELECT id, COUNT(*) OVER w AS cnt
+FROM rpr_domain
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS flag
+)
+ORDER BY id;
+ id | cnt
+----+-----
+ 1 | 1
+ 2 | 0
+ 3 | 1
+(3 rows)
+
+DROP TABLE rpr_domain;
+DROP DOMAIN boolish;
+-- A Var referenced only inside a navigation operation must still propagate
+-- (val appears only inside PREV(), not as a bare operand or in the select list)
+CREATE TABLE rpr_nav (id int, val int);
+INSERT INTO rpr_nav VALUES (1, 0), (2, 1), (3, 0), (4, 2);
+SELECT id, COUNT(*) OVER w AS cnt
+FROM rpr_nav
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (UP+)
+ DEFINE UP AS id > PREV(val)
+)
+ORDER BY id;
+ id | cnt
+----+-----
+ 1 | 0
+ 2 | 3
+ 3 | 0
+ 4 | 0
+(4 rows)
+
+DROP TABLE rpr_nav;
+-- A non-boolean DEFINE expression is rejected
+CREATE TABLE rpr_noncoerce (id int, n int);
+INSERT INTO rpr_noncoerce VALUES (1, 1);
+SELECT id, COUNT(*) OVER w AS cnt
+FROM rpr_noncoerce
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS n
+);
+ERROR: argument of DEFINE must be type boolean, not type integer
+LINE 7: DEFINE A AS n
+ ^
+DROP TABLE rpr_noncoerce;
+-- A non-boolean later DEFINE is rejected at its own definition even when an
+-- earlier DEFINE variable is valid
+CREATE TABLE rpr_noncoerce2 (id int, n int);
+INSERT INTO rpr_noncoerce2 VALUES (1, 1);
+SELECT id, COUNT(*) OVER w AS cnt
+FROM rpr_noncoerce2
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE A AS id > 0, B AS n
+);
+ERROR: argument of DEFINE must be type boolean, not type integer
+LINE 7: DEFINE A AS id > 0, B AS n
+ ^
+DROP TABLE rpr_noncoerce2;
-- Complex expressions
CREATE TABLE rpr_complex (id INT, val1 INT, val2 INT);
INSERT INTO rpr_complex VALUES (1, 10, 20), (2, 15, 25), (3, 20, 30);
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index e71f0dd3680..21840aa77be 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -261,6 +261,65 @@ DROP TYPE truthyint;
DROP TABLE rpr_bool;
+-- Coercion over a boolean domain is not a no-op; the wrapped Var must still
+-- propagate when referenced only in DEFINE (flag is not in the select list)
+CREATE DOMAIN boolish AS boolean;
+CREATE TABLE rpr_domain (id int, flag boolish);
+INSERT INTO rpr_domain VALUES (1, true), (2, false), (3, true);
+SELECT id, COUNT(*) OVER w AS cnt
+FROM rpr_domain
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS flag
+)
+ORDER BY id;
+DROP TABLE rpr_domain;
+DROP DOMAIN boolish;
+
+-- A Var referenced only inside a navigation operation must still propagate
+-- (val appears only inside PREV(), not as a bare operand or in the select list)
+CREATE TABLE rpr_nav (id int, val int);
+INSERT INTO rpr_nav VALUES (1, 0), (2, 1), (3, 0), (4, 2);
+SELECT id, COUNT(*) OVER w AS cnt
+FROM rpr_nav
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (UP+)
+ DEFINE UP AS id > PREV(val)
+)
+ORDER BY id;
+DROP TABLE rpr_nav;
+
+-- A non-boolean DEFINE expression is rejected
+CREATE TABLE rpr_noncoerce (id int, n int);
+INSERT INTO rpr_noncoerce VALUES (1, 1);
+SELECT id, COUNT(*) OVER w AS cnt
+FROM rpr_noncoerce
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS n
+);
+DROP TABLE rpr_noncoerce;
+
+-- A non-boolean later DEFINE is rejected at its own definition even when an
+-- earlier DEFINE variable is valid
+CREATE TABLE rpr_noncoerce2 (id int, n int);
+INSERT INTO rpr_noncoerce2 VALUES (1, 1);
+SELECT id, COUNT(*) OVER w AS cnt
+FROM rpr_noncoerce2
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE A AS id > 0, B AS n
+);
+DROP TABLE rpr_noncoerce2;
+
-- Complex expressions
CREATE TABLE rpr_complex (id INT, val1 INT, val2 INT);
INSERT INTO rpr_complex VALUES (1, 10, 20), (2, 15, 25), (3, 20, 30);
--
2.50.1 (Apple Git-155)
From f3dda49e5a24347ff23da72f5fe80640291bc34c Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Wed, 17 Jun 2026 20:43:01 +0900
Subject: [PATCH 09/13] Replace a bare block with an else in the RPR DEFINE
clause walker
define_walker() ended its phase handling with a bare braced block for
the DEFINE-body case, following two if blocks that respectively return
and raise an error. Turn it into the else branch of an if / else if /
else chain. No behavior change.
---
src/backend/parser/parse_rpr.c | 15 +++++++--------
1 file changed, 7 insertions(+), 8 deletions(-)
diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index 116cd206e39..7c201d55164 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -515,8 +515,7 @@ define_walker(Node *node, void *context)
ctx->nav_count++;
return expression_tree_walker(node, define_walker, ctx);
}
-
- if (ctx->phase == DEFINE_PHASE_NAV_OFFSET)
+ else if (ctx->phase == DEFINE_PHASE_NAV_OFFSET)
{
/*
* A navigation offset must be a run-time constant, so it cannot
@@ -528,13 +527,13 @@ define_walker(Node *node, void *context)
errdetail("A navigation offset must be a run-time constant."),
parser_errposition(ctx->pstate, nav->location));
}
-
- /*
- * PHASE_BODY: this is an outer nav at top level. Walk arg first to
- * collect nesting / column-ref state, then validate and (for compound
- * forms) flatten, then walk offset(s).
- */
+ else
{
+ /*
+ * PHASE_BODY: this is an outer nav at top level. Walk arg first
+ * to collect nesting / column-ref state, then validate and (for
+ * compound forms) flatten, then walk offset(s).
+ */
DefineWalkCtx saved = *ctx;
bool outer_phys = (nav->kind == RPR_NAV_PREV ||
nav->kind == RPR_NAV_NEXT);
--
2.50.1 (Apple Git-155)
From 0e23c67f20f26c7f0e68f830ca5d8f8906a5e601 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Fri, 19 Jun 2026 11:30:40 +0900
Subject: [PATCH 10/13] Rename loop index variables in row pattern deparse
helpers
Give the index parameters and loop variables in the RPR pattern deparse
helpers more descriptive names: the construct-index parameter becomes
idx, the branch-number parameter becomes bno, and each helper's sole
loop variable becomes i. Adjust the accompanying comments to match.
This is a cosmetic change with no effect on the deparsed output.
---
src/backend/commands/explain.c | 68 +++++++++++++++++-----------------
1 file changed, 34 insertions(+), 34 deletions(-)
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 696bdb9c8b5..423cf352125 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -124,11 +124,11 @@ static void append_rpr_quantifier(StringInfo buf, RPRPatternElement *elem);
static char *deparse_rpr_pattern(RPRPattern *pattern);
static int deparse_rpr_seq(RPRPattern *pattern, int start, int limit,
StringInfo buf);
-static int deparse_rpr_node(RPRPattern *pattern, int i, int limit,
+static int deparse_rpr_node(RPRPattern *pattern, int idx, int limit,
StringInfo buf);
static int rpr_match_end(RPRPattern *pattern, int beginIdx);
-static int rpr_alt_scope_end(RPRPattern *pattern, int i);
-static int rpr_next_branch(RPRPattern *pattern, int b, int altEnd);
+static int rpr_alt_scope_end(RPRPattern *pattern, int idx);
+static int rpr_next_branch(RPRPattern *pattern, int bno, int altEnd);
static void show_storage_info(char *maxStorageType, int64 maxSpaceUsed,
ExplainState *es);
static void show_tablesample(TableSampleClause *tsc, PlanState *planstate,
@@ -3014,8 +3014,8 @@ deparse_rpr_seq(RPRPattern *pattern, int start, int limit, StringInfo buf)
}
/*
- * Deparse the single construct starting at index i, bounded by the inherited
- * limit. Returns the index just past the construct.
+ * Deparse the single construct starting at index idx, bounded by the
+ * inherited limit. Returns the index just past the construct.
*
* A VAR is its name plus quantifier. A BEGIN opens a group spanning to its
* matching END (rpr_match_end); when the group's sole child is an ALT that
@@ -3026,9 +3026,9 @@ deparse_rpr_seq(RPRPattern *pattern, int start, int limit, StringInfo buf)
* handed down by rpr_next_branch.
*/
static int
-deparse_rpr_node(RPRPattern *pattern, int i, int limit, StringInfo buf)
+deparse_rpr_node(RPRPattern *pattern, int idx, int limit, StringInfo buf)
{
- RPRPatternElement *elem = &pattern->elements[i];
+ RPRPatternElement *elem = &pattern->elements[idx];
if (RPRElemIsVar(elem))
{
@@ -3036,27 +3036,27 @@ deparse_rpr_node(RPRPattern *pattern, int i, int limit, StringInfo buf)
appendStringInfoString(buf,
quote_identifier(pattern->varNames[elem->varId]));
append_rpr_quantifier(buf, elem);
- return i + 1;
+ return idx + 1;
}
if (RPRElemIsBegin(elem))
{
- int end = rpr_match_end(pattern, i);
+ int end = rpr_match_end(pattern, idx);
bool loneAlt;
- loneAlt = (i + 1 < end &&
- RPRElemIsAlt(&pattern->elements[i + 1]) &&
- rpr_alt_scope_end(pattern, i + 1) == end);
+ loneAlt = (idx + 1 < end &&
+ RPRElemIsAlt(&pattern->elements[idx + 1]) &&
+ rpr_alt_scope_end(pattern, idx + 1) == end);
if (loneAlt)
{
/* The ALT child already parenthesizes the whole group body. */
- (void) deparse_rpr_node(pattern, i + 1, end, buf);
+ (void) deparse_rpr_node(pattern, idx + 1, end, buf);
}
else
{
appendStringInfoChar(buf, '(');
- (void) deparse_rpr_seq(pattern, i + 1, end, buf);
+ (void) deparse_rpr_seq(pattern, idx + 1, end, buf);
appendStringInfoChar(buf, ')');
}
append_rpr_quantifier(buf, &pattern->elements[end]);
@@ -3065,7 +3065,7 @@ deparse_rpr_node(RPRPattern *pattern, int i, int limit, StringInfo buf)
Assert(RPRElemIsAlt(elem));
{
- int altEnd = rpr_alt_scope_end(pattern, i);
+ int altEnd = rpr_alt_scope_end(pattern, idx);
int b;
bool first = true;
@@ -3073,7 +3073,7 @@ deparse_rpr_node(RPRPattern *pattern, int i, int limit, StringInfo buf)
altEnd = limit;
appendStringInfoChar(buf, '(');
- b = i + 1;
+ b = idx + 1;
while (b < altEnd)
{
int nb = rpr_next_branch(pattern, b, altEnd);
@@ -3097,41 +3097,41 @@ static int
rpr_match_end(RPRPattern *pattern, int beginIdx)
{
RPRDepth d = pattern->elements[beginIdx].depth;
- int j;
+ int i;
- for (j = beginIdx + 1; j < pattern->numElements; j++)
+ for (i = beginIdx + 1; i < pattern->numElements; i++)
{
- RPRPatternElement *e = &pattern->elements[j];
+ RPRPatternElement *e = &pattern->elements[i];
if (RPRElemIsEnd(e) && e->depth == d)
- return j;
+ return i;
}
pg_unreachable(); /* a BEGIN always has a matching END */
}
/*
- * Scope end of the construct at index i: the first following element whose
- * depth is no greater than i's own. For an ALT marker this is the index just
- * past its last branch, since depth stays constant across branch boundaries.
- * FIN sits at depth 0, so a top-level ALT stops there.
+ * Scope end of the construct at index idx: the first following element whose
+ * depth is no greater than idx's own. For an ALT marker this is the index
+ * just past its last branch, since depth stays constant across branch
+ * boundaries. FIN sits at depth 0, so a top-level ALT stops there.
*/
static int
-rpr_alt_scope_end(RPRPattern *pattern, int i)
+rpr_alt_scope_end(RPRPattern *pattern, int idx)
{
- RPRDepth d = pattern->elements[i].depth;
- int k;
+ RPRDepth d = pattern->elements[idx].depth;
+ int i;
- for (k = i + 1; k < pattern->numElements; k++)
+ for (i = idx + 1; i < pattern->numElements; i++)
{
- if (pattern->elements[k].depth <= d)
- return k;
+ if (pattern->elements[i].depth <= d)
+ return i;
}
return pattern->numElements;
}
/*
- * Boundary of the alternation branch starting at b (i.e. the start of the next
- * branch, or altEnd if b is the last branch).
+ * Boundary of the alternation branch starting at bno (i.e. the start of the
+ * next branch, or altEnd if bno is the last branch).
*
* The branch-start element's jump points at the next branch when this is not
* the last branch. jump is overloaded (a group BEGIN also uses it for its
@@ -3140,9 +3140,9 @@ rpr_alt_scope_end(RPRPattern *pattern, int i)
* next redirected past the alternation, so it does not point at j.
*/
static int
-rpr_next_branch(RPRPattern *pattern, int b, int altEnd)
+rpr_next_branch(RPRPattern *pattern, int bno, int altEnd)
{
- int j = pattern->elements[b].jump;
+ int j = pattern->elements[bno].jump;
if (j != RPR_ELEMIDX_INVALID && j < altEnd &&
pattern->elements[j - 1].next != j)
--
2.50.1 (Apple Git-155)
From 0bc60ef4dc9cba045f6ee82d8cfcb48e2f5cf712 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Fri, 19 Jun 2026 13:04:00 +0900
Subject: [PATCH 11/13] Rename absorption "judgment point" to "comparison
point" in comments
The absorption analysis comments described the ABSORBABLE element flag
as a "judgment point". Use "comparison point" instead, which says more
directly what happens there: where consecutive iterations are compared.
This touches comments and the executor README only, across the planner,
executor, and EXPLAIN deparse, plus the rpr_base test comments; the
"equivalence judgment" wording and all identifiers are unchanged.
No change to behavior or to query output.
---
src/backend/commands/explain.c | 2 +-
src/backend/executor/README.rpr | 4 ++--
src/backend/executor/execRPR.c | 29 +++++++++++++-------------
src/backend/optimizer/plan/rpr.c | 14 ++++++-------
src/include/optimizer/rpr.h | 2 +-
src/test/regress/expected/rpr_base.out | 6 +++---
src/test/regress/sql/rpr_base.sql | 6 +++---
7 files changed, 32 insertions(+), 31 deletions(-)
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 423cf352125..b3fc324718d 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -2937,7 +2937,7 @@ append_rpr_quantifier(StringInfo buf, RPRPatternElement *elem)
appendStringInfoChar(buf, '?');
}
- /* Append absorption markers: " for judgment point, ' for branch only */
+ /* Append absorption markers: " for comparison point, ' for branch only */
if (RPRElemIsAbsorbable(elem))
{
Assert(elem->max == RPR_QUANTITY_INF);
diff --git a/src/backend/executor/README.rpr b/src/backend/executor/README.rpr
index 713ad84e1d9..50d1ff87f7e 100644
--- a/src/backend/executor/README.rpr
+++ b/src/backend/executor/README.rpr
@@ -285,7 +285,7 @@ Element flags (1 byte, bitmask):
absorption.
0x08 RPR_ELEM_ABSORBABLE (VAR, END)
- Absorption judgment point. Where to compare consecutive
+ Absorption comparison point. Where to compare consecutive
iterations for absorption.
- Simple unbounded VAR (A+): set on the VAR itself
- Unbounded GROUP ((A B)+): set on the END element only
@@ -1393,7 +1393,7 @@ XII-5. Execution Optimization Summary
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).
+ through END chains to reach the comparison 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/executor/execRPR.c b/src/backend/executor/execRPR.c
index 099b81aeb81..90e3a068f04 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -578,7 +578,7 @@ nfa_update_absorption_flags(RPRNFAContext *ctx)
/*
* Iterate through all states to check absorption status. Uses
* state->isAbsorbable which tracks if state is in absorbable region. This
- * is different from RPRElemIsAbsorbable(elem) which checks judgment
+ * is different from RPRElemIsAbsorbable(elem) which checks comparison
* point.
*/
for (state = ctx->states; state != NULL; state = state->next)
@@ -625,8 +625,8 @@ nfa_states_covered(RPRPattern *pattern, RPRNFAContext *older, RPRNFAContext *new
depth = elem->depth;
/*
- * Only compare at absorption judgment points (RPR_ELEM_ABSORBABLE).
- * Judgment points are where count-dominance guarantees the newer
+ * Only compare at absorption comparison points (RPR_ELEM_ABSORBABLE).
+ * Comparison points are where count-dominance guarantees the newer
* context's future matches are a subset of the older's.
*/
if (!RPRElemIsAbsorbable(elem))
@@ -782,7 +782,8 @@ nfa_eval_var_match(WindowAggState *winstate, RPRPatternElement *elem,
* previous advance when count >= min was satisfied)
*
* For VARs that reached max count followed by END:
- * - Advance through the END-element chain to the absorption judgment point
+ * - Advance through the END-element chain to the absorption
+ * comparison point
* - Only deterministic exits (count >= max, max != INF) are handled
* - Chains through END elements while count >= max (must-exit path)
*
@@ -800,7 +801,7 @@ nfa_match(WindowAggState *winstate, RPRNFAContext *ctx, bool *varMatched)
/*
* Evaluate VAR elements against current row. For VARs that reach max
* count with END next, advance through the chain of END elements inline
- * so absorb phase can compare states at judgment points.
+ * so absorb phase can compare states at comparison points.
*/
for (state = ctx->states; state != NULL; state = nextState)
{
@@ -831,7 +832,7 @@ nfa_match(WindowAggState *winstate, RPRNFAContext *ctx, bool *varMatched)
/*
* For VAR at max count with END next, advance through END
- * chain to reach the absorption judgment point. Only
+ * chain to reach the absorption comparison point. Only
* deterministic exits (count >= max, max finite) are handled;
* unbounded VARs stay for advance phase.
*
@@ -841,10 +842,10 @@ nfa_match(WindowAggState *winstate, RPRNFAContext *ctx, bool *varMatched)
* to the next outer END. The loop below walks this chain.
*
* ABSORBABLE_BRANCH marks elements inside the absorbable
- * region; ABSORBABLE marks the outermost judgment point where
- * count-dominance is evaluated. We chain through BRANCH
- * elements until reaching the ABSORBABLE point or an element
- * that can still loop (count < max).
+ * region; ABSORBABLE marks the outermost comparison point
+ * where count-dominance is evaluated. We chain through
+ * BRANCH elements until reaching the ABSORBABLE point or an
+ * element that can still loop (count < max).
*/
if (RPRElemIsAbsorbableBranch(elem) &&
!RPRElemIsAbsorbable(elem) &&
@@ -876,7 +877,7 @@ nfa_match(WindowAggState *winstate, RPRNFAContext *ctx, bool *varMatched)
/*
* Chain through END elements within the absorbable region
- * (ABSORBABLE_BRANCH) until reaching the judgment point
+ * (ABSORBABLE_BRANCH) until reaching the comparison point
* (ABSORBABLE). Continue only on must-exit path (count
* >= max) with END next.
*/
@@ -892,9 +893,9 @@ nfa_match(WindowAggState *winstate, RPRNFAContext *ctx, bool *varMatched)
/*
* Exit this intermediate group: clear its own count
* (count-clear policy). It sits below the absorbable
- * judgment point, so it is excluded from the
- * dominance comparison; the judgment point where the
- * chain stops keeps its count.
+ * comparison point, so it is excluded from the
+ * dominance comparison; the comparison point where
+ * the chain stops keeps its count.
*/
state->counts[endDepth] = 0;
diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index 597a966c7b1..ebba8e50b1d 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -19,7 +19,7 @@
* complexity from O(n^2) to O(n) for patterns like A+ B.
*
* The absorption analysis uses two element flags:
- * - RPR_ELEM_ABSORBABLE: marks WHERE to compare (judgment point)
+ * - RPR_ELEM_ABSORBABLE: marks WHERE to compare (comparison point)
* - RPR_ELEM_ABSORBABLE_BRANCH: marks the absorbable region
*
* See computeAbsorbability() and the detailed comments before
@@ -1484,7 +1484,7 @@ finalizeRPRPattern(RPRPattern *result)
* than Ctx2's match (1 to current). So Ctx2 can be safely eliminated.
*
* Two Flags:
- * 1. RPR_ELEM_ABSORBABLE - "Absorption judgment point"
+ * 1. RPR_ELEM_ABSORBABLE - "Absorption comparison point"
* WHERE contexts can be compared for absorption.
* - Simple unbounded VAR (A+): the VAR element itself
* - Unbounded GROUP ((A B)+): the END element only
@@ -1506,20 +1506,20 @@ finalizeRPRPattern(RPRPattern *result)
* -> Both at END, comparable! Ctx1 absorbs Ctx2.
*
* Contexts synchronize at END every group-length rows. Therefore:
- * - ABSORBABLE marks END as judgment point (where to compare)
+ * - ABSORBABLE marks END as comparison point (where to compare)
* - ABSORBABLE_BRANCH keeps state.isAbsorbable=true through A->B->END
*
* Pattern Examples:
*
* Pattern: A+ B
- * Element 0 (A): ABSORBABLE | ABSORBABLE_BRANCH <- judgment point
+ * Element 0 (A): ABSORBABLE | ABSORBABLE_BRANCH <- comparison point
* Element 1 (B): (none)
* -> Compare at A every row. When contexts move to B, absorption stops.
*
* Pattern: (A B)+ C
* Element 0 (A): ABSORBABLE_BRANCH
* Element 1 (B): ABSORBABLE_BRANCH
- * Element 2 (END): ABSORBABLE | ABSORBABLE_BRANCH <- judgment point
+ * Element 2 (END): ABSORBABLE | ABSORBABLE_BRANCH <- comparison point
* Element 3 (C): (none)
* -> Compare at END every 2 rows. When contexts move to C, absorption stops.
*
@@ -1629,7 +1629,7 @@ isFixedLengthChildren(RPRPattern *pattern, RPRElemIdx idx, RPRDepth scopeDepth)
* 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).
+ * Only the unbounded END gets ABSORBABLE (comparison point).
* Examples:
* (A B{2})+ C - B{2} has min==max, step=3
* (A (B C){2} D)+ E - nested {2} subgroup, step=6
@@ -1791,7 +1791,7 @@ computeAbsorbabilityRecursive(RPRPattern *pattern, RPRElemIdx startIdx,
* decrease property required for safe absorption.
*
* This function sets two flags:
- * RPR_ELEM_ABSORBABLE: Absorption judgment point
+ * RPR_ELEM_ABSORBABLE: Absorption comparison point
* - Simple unbounded VAR: the VAR itself (e.g., A in A+)
* - Unbounded GROUP: the END element (e.g., END in (A B)+)
* RPR_ELEM_ABSORBABLE_BRANCH: All elements in absorbable region
diff --git a/src/include/optimizer/rpr.h b/src/include/optimizer/rpr.h
index 23763442b65..83d3cd29b07 100644
--- a/src/include/optimizer/rpr.h
+++ b/src/include/optimizer/rpr.h
@@ -53,7 +53,7 @@
* optimizer/plan/rpr.c.
*/
#define RPR_ELEM_ABSORBABLE_BRANCH 0x04 /* element in absorbable region */
-#define RPR_ELEM_ABSORBABLE 0x08 /* absorption judgment point */
+#define RPR_ELEM_ABSORBABLE 0x08 /* absorption comparison point */
/* Accessor macros for RPRPatternElement */
#define RPRElemIsReluctant(e) (((e)->flags & RPR_ELEM_RELUCTANT) != 0)
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index 80fabde514c..b385e972e7f 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -5472,9 +5472,9 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-- Absorption Flag Display Tests
-- ============================================================
-- Tests absorption marker display in EXPLAIN output
--- Markers: ' = branch element, " = judgment point
+-- Markers: ' = branch element, " = comparison point
-- Files: explain.c (append_rpr_quantifier, deparse_rpr_pattern)
--- Simple VAR: A+ -> a+" (judgment point)
+-- Simple VAR: A+ -> a+" (comparison point)
EXPLAIN (COSTS OFF)
SELECT COUNT(*) OVER w FROM rpr_plan
WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
@@ -5490,7 +5490,7 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-> Seq Scan on rpr_plan
(7 rows)
--- GROUP unbounded: (A B)+ -> (a' b')+" (branch + judgment)
+-- GROUP unbounded: (A B)+ -> (a' b')+" (branch + comparison)
EXPLAIN (COSTS OFF)
SELECT COUNT(*) OVER w FROM rpr_plan
WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index 21840aa77be..af498fffb66 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -3254,16 +3254,16 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-- Absorption Flag Display Tests
-- ============================================================
-- Tests absorption marker display in EXPLAIN output
--- Markers: ' = branch element, " = judgment point
+-- Markers: ' = branch element, " = comparison point
-- Files: explain.c (append_rpr_quantifier, deparse_rpr_pattern)
--- Simple VAR: A+ -> a+" (judgment point)
+-- Simple VAR: A+ -> a+" (comparison point)
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+) DEFINE A AS val > 0);
--- GROUP unbounded: (A B)+ -> (a' b')+" (branch + judgment)
+-- GROUP unbounded: (A B)+ -> (a' b')+" (branch + comparison)
EXPLAIN (COSTS OFF)
SELECT COUNT(*) OVER w FROM rpr_plan
WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
--
2.50.1 (Apple Git-155)
From 3cccad2ecc34be6d0b1e96afdca7468e4e9ab21f Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Fri, 19 Jun 2026 14:17:19 +0900
Subject: [PATCH 13/13] Document eval_nav_offset_helper's NULL/negative offset
handling
eval_nav_offset_helper pre-evaluates a navigation offset at executor init
to size the frame trim, returning 0 for a NULL or negative offset rather
than rejecting it. The comment did not say why, leaving the purpose of the
function and of those branches unclear.
Explain that a NULL or negative offset is caught per row on the navigation
path that consumes it, which errors out before navigation produces any
result, so the trim value computed here is never used. The branches are
reachable -- a navigation offset can be a run-time constant such as a Param
-- and are already covered by the PREV(price, $1) tests in rpr.sql, so they
need neither a new test nor an assertion.
---
src/backend/executor/nodeWindowAgg.c | 13 +++++++++----
1 file changed, 9 insertions(+), 4 deletions(-)
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 819ad814bf5..eb1d616b49a 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -3985,10 +3985,15 @@ put_notnull_info(WindowObject winobj, int64 pos, int argno, bool isnull)
/*
* eval_nav_offset_helper
- * Evaluate an offset expression at executor init time for trim
- * optimization. Returns the offset value, or 0 for NULL/negative
- * (these will cause a runtime error during actual navigation, so the
- * trim value is irrelevant).
+ * Pre-evaluate a navigation offset expression at executor init time, to
+ * bound how far navigation can reach (which sizes the frame trim).
+ * Returns the offset value, or 0 for a NULL or negative offset.
+ *
+ * The offset is not validated here. A NULL or negative value is caught later,
+ * per row, on the navigation path that consumes it (see EEOP_RPR_NAV_SET in
+ * execExprInterp.c), which errors out before navigation produces any result;
+ * the trim sizing computed from such an offset is therefore never used, and 0
+ * is returned as a harmless placeholder.
*/
static int64
eval_nav_offset_helper(WindowAggState *winstate, Expr *offset_expr,
--
2.50.1 (Apple Git-155)
From 9c7e5bc9dde1adafcff995370340975661890d3c Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Fri, 19 Jun 2026 13:43:58 +0900
Subject: [PATCH 12/13] Improve comments, documentation, and naming for row
pattern recognition
A batch of clarity cleanups for row pattern recognition, none of which
change behavior or query output:
- Call the parser-built PATTERN structure a "parse tree" rather than an
"AST", which is not PostgreSQL's usual term (parsenodes.h, parse_rpr.c,
plan/rpr.c, and the executor README).
- Trim the transformDefineClause header comment, whose step list merely
duplicated the inline comments.
- Drop a duplicated standard-citation sentence from the contain_rpr_walker
header; transformWithClause already carries it.
- Fix a stale "ALT_START" reference to name the ALT marker, and normalize
an "or -1" ParseLoc comment to the usual "or -1 if unknown".
- Move the EMPTY-alternative explanation in row_pattern_quantifier_opt
into the action block, keeping the /*EMPTY*/ marker.
- Reword the Run Condition pushdown test comment to explain in SQL terms
why count(*) is DECREASING over the required frame.
- Rename RPR_COUNT_MAX to RPR_COUNT_INF, defined from RPR_QUANTITY_INF.
The old "MAX" name suggested a configurable repetition limit -- that is
elem->max, a different thing -- which led to questions about erroring
when a count reaches it. The value is the int32 saturation ceiling, and
a saturated count reads as "unbounded".
---
src/backend/executor/README.rpr | 44 +++++++++++------------
src/backend/executor/execRPR.c | 23 ++++++------
src/backend/optimizer/plan/rpr.c | 23 ++++++------
src/backend/parser/gram.y | 7 ++--
src/backend/parser/parse_cte.c | 4 +--
src/backend/parser/parse_rpr.c | 19 ++--------
src/include/nodes/parsenodes.h | 12 +++----
src/include/optimizer/rpr.h | 9 ++++-
src/test/regress/expected/rpr_explain.out | 12 ++++---
src/test/regress/sql/rpr_explain.sql | 12 ++++---
10 files changed, 84 insertions(+), 81 deletions(-)
diff --git a/src/backend/executor/README.rpr b/src/backend/executor/README.rpr
index 50d1ff87f7e..9275e265d4b 100644
--- a/src/backend/executor/README.rpr
+++ b/src/backend/executor/README.rpr
@@ -96,16 +96,16 @@ Chapter II Overall Processing Pipeline
RPR processing is divided into three phases:
- +------------------------------------------------------------+
- | 1. Parsing (Parser) |
- | SQL text -> PATTERN AST + DEFINE expression tree |
- | |
- | 2. Compilation (Optimizer/Planner) |
- | PATTERN AST -> optimization -> flat NFA element array |
- | |
- | 3. Execution (Executor) |
- | Row-by-row matching via NFA simulation |
- +------------------------------------------------------------+
+ +--------------------------------------------------------------+
+ | 1. Parsing (Parser) |
+ | SQL text -> PATTERN parse tree + DEFINE expression tree |
+ | |
+ | 2. Compilation (Optimizer/Planner) |
+ | PATTERN parse tree -> optimization -> flat NFA elements |
+ | |
+ | 3. Execution (Executor) |
+ | Row-by-row matching via NFA simulation |
+ +--------------------------------------------------------------+
Each phase uses independent data structures, and the interfaces between
phases are well-defined:
@@ -137,7 +137,7 @@ following:
(3) DEFINE clause transformation (transformDefineClause)
-III-2. PATTERN AST (Abstract Syntax Tree)
+III-2. PATTERN parse tree
The parser transforms the PATTERN clause into an RPRPatternNode tree.
Each node has one of the following four types:
@@ -192,16 +192,16 @@ IV-1. Entry Point
IV-2. The 6 Phases of buildRPRPattern()
- Phase 1: AST optimization (optimizeRPRPattern)
+ Phase 1: parse tree optimization (optimizeRPRPattern)
Phase 2: Statistics collection (scanRPRPattern)
Phase 3: Memory allocation (makeRPRPattern)
Phase 4: NFA element fill (fillRPRPattern)
Phase 5: Finalization (finalizeRPRPattern)
Phase 6: Absorbability analysis (computeAbsorbability)
-IV-3. Phase 1: AST Optimization
+IV-3. Phase 1: Parse Tree Optimization
-After copying the parser-generated AST, the following optimizations are
+After copying the parser-generated parse tree, the following optimizations are
applied:
(a) SEQ flattening: Unwrap nested SEQ nodes
@@ -236,7 +236,7 @@ applied:
IV-4. Phase 4: NFA Element Array Generation
-Transforms the optimized AST into a flat array of RPRPatternElement.
+Transforms the optimized parse tree into a flat array of RPRPatternElement.
This is the core data structure used for NFA simulation at runtime.
RPRPatternElement struct (16 bytes):
@@ -298,7 +298,7 @@ Element flags (1 byte, bitmask):
Example: PATTERN (A+ B | C)
- AST: ALT(SEQ(VAR(A,1,INF), VAR(B,1,1)), VAR(C,1,1))
+ Parse tree: ALT(SEQ(VAR(A,1,INF), VAR(B,1,1)), VAR(C,1,1))
Compilation result:
@@ -345,9 +345,9 @@ Example: PATTERN ((A B)+)
IV-4a. Reluctant Flag (RPR_ELEM_RELUCTANT)
-The reluctant flag is set during Phase 4 (fillRPRPattern) when the AST node
-has reluctant == true. It reverses the priority of quantifier expansion at
-runtime:
+The reluctant flag is set during Phase 4 (fillRPRPattern) when the parse
+tree node has reluctant == true. It reverses the priority of quantifier
+expansion at runtime:
Greedy (default): try loop-back first, then exit (prefer longer match)
Reluctant: try exit first, then loop-back (prefer shorter match)
@@ -1363,9 +1363,9 @@ XII-5. Execution Optimization Summary
-- Compile-time --
- (1) AST Optimization (IV-3)
+ (1) Parse Tree Optimization (IV-3)
- Simplifies the AST before converting the pattern to an NFA.
+ Simplifies the parse tree before converting the pattern to an NFA.
Reduces the number of NFA elements through consecutive variable
merging (A A -> A{2}), SEQ flattening, quantifier multiplication,
and other transformations.
@@ -1468,7 +1468,7 @@ Appendix A. Key Function Index
transformRPR parse_rpr.c Parser entry point
transformDefineClause parse_rpr.c DEFINE transformation
buildRPRPattern rpr.c NFA compilation main
- optimizeRPRPattern rpr.c AST optimization
+ optimizeRPRPattern rpr.c parse tree optimization
fillRPRPattern rpr.c NFA element generation
finalizeRPRPattern rpr.c Finalization
computeAbsorbability rpr.c Absorption analysis
diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 90e3a068f04..e5e30d79f01 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -821,8 +821,11 @@ nfa_match(WindowAggState *winstate, RPRNFAContext *ctx, bool *varMatched)
if (matched)
{
- /* Increment count */
- if (count < RPR_COUNT_MAX)
+ /*
+ * Increment count, saturating at RPR_COUNT_INF to avoid int32
+ * overflow; a saturated count then compares as "unbounded".
+ */
+ if (count < RPR_COUNT_INF)
count++;
/* Max constraint should not be exceeded */
@@ -857,7 +860,7 @@ nfa_match(WindowAggState *winstate, RPRNFAContext *ctx, bool *varMatched)
int32 endCount = state->counts[endDepth];
/* Increment group count */
- if (endCount < RPR_COUNT_MAX)
+ if (endCount < RPR_COUNT_INF)
endCount++;
Assert(endElem->max == RPR_QUANTITY_INF ||
endCount <= endElem->max);
@@ -900,7 +903,7 @@ nfa_match(WindowAggState *winstate, RPRNFAContext *ctx, bool *varMatched)
state->counts[endDepth] = 0;
/* Increment outer group count */
- if (outerCount < RPR_COUNT_MAX)
+ if (outerCount < RPR_COUNT_INF)
outerCount++;
Assert(outerEnd->max == RPR_QUANTITY_INF ||
outerCount <= outerEnd->max);
@@ -1180,7 +1183,7 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
/* END->END: increment outer END's count */
if (RPRElemIsEnd(nextElem) &&
- ffState->counts[nextElem->depth] < RPR_COUNT_MAX)
+ ffState->counts[nextElem->depth] < RPR_COUNT_INF)
ffState->counts[nextElem->depth]++;
}
@@ -1235,7 +1238,7 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
RPRElemIsAbsorbableBranch(nextElem);
/* END->END: increment outer END's count */
- if (RPRElemIsEnd(nextElem) && state->counts[nextElem->depth] < RPR_COUNT_MAX)
+ if (RPRElemIsEnd(nextElem) && state->counts[nextElem->depth] < RPR_COUNT_INF)
state->counts[nextElem->depth]++;
nfa_route_to_elem(winstate, ctx, state, nextElem, currentPos);
@@ -1262,7 +1265,7 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
nextElem = &elements[exitState->elemIdx];
/* END->END: increment outer END's count */
- if (RPRElemIsEnd(nextElem) && exitState->counts[nextElem->depth] < RPR_COUNT_MAX)
+ if (RPRElemIsEnd(nextElem) && exitState->counts[nextElem->depth] < RPR_COUNT_INF)
exitState->counts[nextElem->depth]++;
/* Prepare loop state */
@@ -1355,7 +1358,7 @@ nfa_advance_var(WindowAggState *winstate, RPRNFAContext *ctx,
/* When exiting directly to an outer END, increment its count */
if (RPRElemIsEnd(nextElem))
{
- if (cloneState->counts[nextElem->depth] < RPR_COUNT_MAX)
+ if (cloneState->counts[nextElem->depth] < RPR_COUNT_INF)
cloneState->counts[nextElem->depth]++;
}
@@ -1404,7 +1407,7 @@ nfa_advance_var(WindowAggState *winstate, RPRNFAContext *ctx,
*/
if (RPRElemIsEnd(nextElem))
{
- if (state->counts[nextElem->depth] < RPR_COUNT_MAX)
+ if (state->counts[nextElem->depth] < RPR_COUNT_INF)
state->counts[nextElem->depth]++;
}
@@ -1438,7 +1441,7 @@ nfa_advance_var(WindowAggState *winstate, RPRNFAContext *ctx,
/* See comment above: increment outer END count for quantified VARs */
if (RPRElemIsEnd(nextElem))
{
- if (state->counts[nextElem->depth] < RPR_COUNT_MAX)
+ if (state->counts[nextElem->depth] < RPR_COUNT_INF)
state->counts[nextElem->depth]++;
}
diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index ebba8e50b1d..62292508aad 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -3,13 +3,13 @@
* rpr.c
* Row Pattern Recognition pattern compilation for planner
*
- * This file contains functions for optimizing RPR pattern AST and
+ * This file contains functions for optimizing the RPR pattern parse tree and
* compiling it to a flat element array for NFA execution by WindowAgg.
*
* Key components:
* 1. Pattern Optimization: Simplifies patterns before compilation
* (e.g., flatten nested SEQ/ALT, merge consecutive vars)
- * 2. Pattern Compilation: Converts AST to flat element array for NFA
+ * 2. Pattern Compilation: Converts parse tree to flat element array for NFA
* 3. Absorption Analysis: Computes flags for O(n^2)->O(n) optimization
*
* Context Absorption Optimization:
@@ -995,7 +995,7 @@ collectDefineVariables(List *defineVariableList, char **varNames)
/*
* scanRPRPatternRecursive
- * Recursively scan pattern AST (pass 1 internal).
+ * Recursively scan pattern parse tree (pass 1 internal).
*
* Collects unique variable names and counts elements while tracking depth.
* Variables from DEFINE clause are already in varNames; this adds any
@@ -1092,7 +1092,7 @@ scanRPRPatternRecursive(RPRPatternNode *node, char **varNames, int *numVars,
/*
* scanRPRPattern
- * Scan pattern AST (pass 1 entry point).
+ * Scan pattern parse tree (pass 1 entry point).
*
* Collects unique variable names (appending to those from DEFINE clause),
* counts total elements (including FIN marker), and tracks maximum depth.
@@ -1297,7 +1297,7 @@ fillRPRPatternGroup(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth de
* fillRPRPatternAlt
* Fill an ALT pattern and its alternatives.
*
- * Creates ALT_START marker, fills each alternative at increased depth,
+ * Creates the ALT marker, fills each alternative at increased depth,
* sets jump pointers for backtracking, and next pointers for successful paths.
*
* Returns true if any branch is nullable (OR semantics: one nullable
@@ -1383,9 +1383,10 @@ fillRPRPatternAlt(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth dept
/*
* fillRPRPattern
- * Fill pattern elements array from AST (pass 2).
+ * Fill pattern elements array from parse tree (pass 2).
*
- * Recursively traverses AST and populates pre-allocated elements array.
+ * Recursively traverses the parse tree and populates pre-allocated elements
+ * array.
* Dispatches to type-specific fill functions.
*
* Returns true if the pattern is nullable (can match zero rows).
@@ -1767,7 +1768,7 @@ computeAbsorbabilityRecursive(RPRPattern *pattern, RPRElemIdx startIdx,
}
else
{
- /* Should never reach END - structural invariant of pattern AST */
+ /* Should never reach END - structural invariant of pattern parse tree */
Assert(!RPRElemIsEnd(elem));
/* Non-ALT, non-BEGIN: check if unbounded start */
@@ -1894,13 +1895,13 @@ validate_rpr_define_volatility(List *defineClause)
/*
* buildRPRPattern
- * Compile pattern AST to flat bytecode array.
+ * Compile pattern parse tree to flat bytecode array.
*
* Compilation phases:
- * 1. Optimize AST (flatten, merge, deduplicate)
+ * 1. Optimize parse tree (flatten, merge, deduplicate)
* 2. Scan: collect variables, count elements (pass 1)
* 3. Allocate result structure
- * 4. Fill elements from AST (pass 2)
+ * 4. Fill elements from parse tree (pass 2)
* 5. Finalize pattern structure
* 6. Compute context absorption eligibility
*
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 4ae951aaeba..6eb01ea7f0b 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -17734,9 +17734,12 @@ row_pattern_primary:
;
row_pattern_quantifier_opt:
- /* EMPTY - no quantifier means exactly once; @$ is unused since
- * min=max=1 never produces an error */
+ /*EMPTY*/
{
+ /*
+ * no quantifier means exactly once; @$ is unused since
+ * min=max=1 never produces an error
+ */
$$ = (Node *) makeRPRQuantifier(1, 1, false, @$);
}
| '*'
diff --git a/src/backend/parser/parse_cte.c b/src/backend/parser/parse_cte.c
index 3e493beba0b..c35feeae6fd 100644
--- a/src/backend/parser/parse_cte.c
+++ b/src/backend/parser/parse_cte.c
@@ -1303,9 +1303,7 @@ checkWellFormedSelectStmt(SelectStmt *stmt, CteState *cstate)
/*
* contain_rpr_walker
* Returns true if the raw parse tree contains any <row pattern common
- * syntax> -- i.e., any WindowDef with PATTERN/DEFINE attached. Used
- * by transformWithClause() to enforce ISO/IEC 9075-2:2016 7.17 SR 3)f)
- * on WITH RECURSIVE elements.
+ * syntax> -- i.e., any WindowDef with PATTERN/DEFINE attached.
*/
static bool
contain_rpr_walker(Node *node, void *context)
diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index 7c201d55164..ed12190cb06 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -8,7 +8,7 @@
* - Validates frame options (must start at CURRENT ROW, no EXCLUDE)
* - Validates PATTERN variable count (max RPR_VARID_MAX + 1)
* - Transforms DEFINE clause
- * - Stores the PATTERN AST and the SKIP TO/INITIAL flags
+ * - Stores the PATTERN parse tree and the SKIP TO/INITIAL flags
*
* Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
* Portions Copyright (c) 1994, Regents of the University of California
@@ -67,7 +67,7 @@ static bool define_walker(Node *node, void *context);
* - Set AFTER MATCH SKIP TO flag
* - Set SEEK/INITIAL flag
* - Transforms DEFINE clause into TargetEntry list
- * - Stores PATTERN AST for deparsing (optimization happens in planner)
+ * - Stores PATTERN parse tree for deparsing (optimization happens in planner)
*
* Returns early if windef has no rpCommonSyntax (non-RPR window).
*/
@@ -180,7 +180,7 @@ transformRPR(ParseState *pstate, WindowClause *wc, WindowDef *windef,
/* Transform DEFINE clause into list of TargetEntry's */
wc->defineClause = transformDefineClause(pstate, windef, targetlist);
- /* Store PATTERN AST for deparsing */
+ /* Store PATTERN parse tree for deparsing */
wc->rpPattern = windef->rpCommonSyntax->rpPattern;
}
@@ -264,19 +264,6 @@ validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
* transformDefineClause
* Process DEFINE clause and transform ResTarget into list of TargetEntry.
*
- * First:
- * 1. Validates PATTERN variable count and collects RPR variable names
- * 2. Rejects DEFINE variables not used in PATTERN
- * 3. Checks for duplicate variable names in DEFINE clause
- *
- * Then for each DEFINE variable:
- * 4. Transforms expression via transformExpr() and coerces it to boolean
- * 5. Creates defineClause entry with proper resname (pattern variable name)
- * 6. Ensures referenced Var nodes are present in the query targetlist (via
- * pull_var_clause)
- *
- * Finally marks column origins and assigns collation information.
- *
* Note: Variables not in DEFINE are evaluated as TRUE by the executor.
* Variables in DEFINE but not in PATTERN are rejected as an error.
*
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 947e668020e..f28215b8e83 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -621,7 +621,7 @@ typedef enum RPRPatternNodeType
} RPRPatternNodeType;
/*
- * RPRPatternNode - Row Pattern Recognition pattern AST node
+ * RPRPatternNode - Row Pattern Recognition pattern parse tree node
*/
typedef struct RPRPatternNode
{
@@ -630,7 +630,7 @@ typedef struct RPRPatternNode
int32 min; /* minimum repetitions (0 for *, ?) */
int32 max; /* maximum repetitions (PG_INT32_MAX for *, +) */
bool reluctant; /* true for reluctant (non-greedy) */
- ParseLoc location; /* token location, or -1 */
+ ParseLoc location; /* token location, or -1 if unknown */
char *varName; /* VAR: variable name */
List *children; /* SEQ, ALT, GROUP: child nodes */
@@ -652,10 +652,10 @@ typedef struct RPCommonSyntax
RPSkipTo rpSkipTo; /* Row Pattern AFTER MATCH SKIP type */
bool initial; /* true if <row pattern initial or seek> is
* initial */
- RPRPatternNode *rpPattern; /* PATTERN clause AST */
+ RPRPatternNode *rpPattern; /* PATTERN parse tree */
List *rpDefs; /* row pattern definitions clause (list of
* ResTarget) */
- ParseLoc location; /* PATTERN keyword location, or -1 */
+ ParseLoc location; /* PATTERN keyword location, or -1 if unknown */
} RPCommonSyntax;
/*
@@ -1727,7 +1727,7 @@ typedef struct GroupingSet
* options are never copied, per spec.
* "defineClause" is Row Pattern Recognition DEFINE clause (list of
* TargetEntry). TargetEntry.resname represents row pattern definition
- * variable name. "rpPattern" represents PATTERN clause as an AST tree
+ * variable name. "rpPattern" represents the PATTERN clause as a parse tree
* (RPRPatternNode).
*
*/
@@ -1763,7 +1763,7 @@ typedef struct WindowClause
* initial */
/* Row Pattern DEFINE clause (list of TargetEntry) */
List *defineClause;
- /* Row Pattern PATTERN clause AST */
+ /* Row Pattern PATTERN parse tree */
RPRPatternNode *rpPattern;
} WindowClause;
diff --git a/src/include/optimizer/rpr.h b/src/include/optimizer/rpr.h
index 83d3cd29b07..f5462ab2a7c 100644
--- a/src/include/optimizer/rpr.h
+++ b/src/include/optimizer/rpr.h
@@ -27,8 +27,15 @@
* before release.
*/
#define RPR_VARID_MAX 0xEF /* pattern variables are 0 to 0xEF */
+
+/*
+ * RPR_COUNT_INF is the value a runtime repetition count saturates at to avoid
+ * int32 overflow (see the count++ guard in nfa_match). It is defined as
+ * RPR_QUANTITY_INF so that a saturated count compares as "unbounded", just
+ * like an unbounded quantifier's max.
+ */
#define RPR_QUANTITY_INF PG_INT32_MAX /* unbounded quantifier */
-#define RPR_COUNT_MAX PG_INT32_MAX /* max runtime count (NFA state) */
+#define RPR_COUNT_INF RPR_QUANTITY_INF
#define RPR_ELEMIDX_MAX PG_INT16_MAX /* max pattern elements */
#define RPR_ELEMIDX_INVALID ((RPRElemIdx) -1) /* invalid index */
#define RPR_DEPTH_MAX (PG_UINT8_MAX - 1) /* max pattern nesting depth:
diff --git a/src/test/regress/expected/rpr_explain.out b/src/test/regress/expected/rpr_explain.out
index cc86d0aae30..8672b4c3055 100644
--- a/src/test/regress/expected/rpr_explain.out
+++ b/src/test/regress/expected/rpr_explain.out
@@ -5598,13 +5598,15 @@ EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev_opt_mixed;
-- find_window_run_conditions() pushes WHERE filters on monotonic window
-- functions into WindowAgg as Run Conditions for early termination.
-- With RPR's required frame (ROWS BETWEEN CURRENT ROW AND UNBOUNDED
--- FOLLOWING), the monotonic direction determines which operators trigger
--- Run Condition pushdown:
+-- FOLLOWING), the monotonic direction of the window function determines
+-- which comparison operators allow pushdown:
-- INCREASING (<=): row_number, rank, dense_rank, percent_rank,
-- cume_dist, ntile
--- DECREASING (>): count(*) (via int8inc, END_UNBOUNDED_FOLLOWING)
--- RPR window function results are match-dependent, not monotonic.
--- Test with count(*) > 0 as representative case.
+-- DECREASING (>): count(*). As the current row advances, the frame
+-- (which ends at UNBOUNDED FOLLOWING) shrinks, so the
+-- count decreases.
+-- RPR window function results are match-dependent, not monotonic, so this
+-- pushdown does not apply. Test with count(*) > 0 as a representative case.
--
-- Without RPR: count(*) > 0 is pushed down as Run Condition
EXPLAIN (COSTS OFF)
diff --git a/src/test/regress/sql/rpr_explain.sql b/src/test/regress/sql/rpr_explain.sql
index 297ca78d54b..115402e304d 100644
--- a/src/test/regress/sql/rpr_explain.sql
+++ b/src/test/regress/sql/rpr_explain.sql
@@ -3174,13 +3174,15 @@ EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev_opt_mixed;
-- find_window_run_conditions() pushes WHERE filters on monotonic window
-- functions into WindowAgg as Run Conditions for early termination.
-- With RPR's required frame (ROWS BETWEEN CURRENT ROW AND UNBOUNDED
--- FOLLOWING), the monotonic direction determines which operators trigger
--- Run Condition pushdown:
+-- FOLLOWING), the monotonic direction of the window function determines
+-- which comparison operators allow pushdown:
-- INCREASING (<=): row_number, rank, dense_rank, percent_rank,
-- cume_dist, ntile
--- DECREASING (>): count(*) (via int8inc, END_UNBOUNDED_FOLLOWING)
--- RPR window function results are match-dependent, not monotonic.
--- Test with count(*) > 0 as representative case.
+-- DECREASING (>): count(*). As the current row advances, the frame
+-- (which ends at UNBOUNDED FOLLOWING) shrinks, so the
+-- count decreases.
+-- RPR window function results are match-dependent, not monotonic, so this
+-- pushdown does not apply. Test with count(*) > 0 as a representative case.
--
-- Without RPR: count(*) > 0 is pushed down as Run Condition
--
2.50.1 (Apple Git-155)
Attachments:
[text/plain] nocfbot-0001-drop-blank-line-churn.txt (3.3K, 3-nocfbot-0001-drop-blank-line-churn.txt)
download | inline diff:
From 9d58cdf40d78efdb35131279a006b618213843db Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Tue, 16 Jun 2026 23:26:16 +0900
Subject: [PATCH 01/13] Remove blank-line changes unrelated to row pattern
recognition
The RPR patch added or dropped blank lines in five existing files with no
related code change. Revert them so the files differ from the base only
where RPR actually touches them.
---
src/backend/executor/nodeWindowAgg.c | 2 --
src/backend/optimizer/plan/setrefs.c | 1 +
src/backend/parser/parse_clause.c | 1 +
src/backend/utils/adt/ruleutils.c | 1 -
src/backend/utils/adt/windowfuncs.c | 1 +
5 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index cb6a484b7de..5b2385f1d8d 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -177,7 +177,6 @@ typedef struct WindowStatePerAggData
bool restart; /* need to restart this agg in this cycle? */
} WindowStatePerAggData;
-
static void initialize_windowaggregate(WindowAggState *winstate,
WindowStatePerFunc perfuncstate,
WindowStatePerAgg peraggstate);
@@ -1086,7 +1085,6 @@ next_tuple:
ExecClearTuple(agg_row_slot);
}
-
/* The frame's end is not supposed to move backwards, ever */
Assert(aggregatedupto_nonrestarted <= winstate->aggregatedupto);
diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c
index 813a326bd78..6e4f3fd61e2 100644
--- a/src/backend/optimizer/plan/setrefs.c
+++ b/src/backend/optimizer/plan/setrefs.c
@@ -214,6 +214,7 @@ static List *set_windowagg_runcondition_references(PlannerInfo *root,
static void record_elided_node(PlannerGlobal *glob, int plan_node_id,
NodeTag elided_type, Bitmapset *relids);
+
/*****************************************************************************
*
* SUBPLAN REFERENCES
diff --git a/src/backend/parser/parse_clause.c b/src/backend/parser/parse_clause.c
index 550ea4eb9c0..8eb367aa579 100644
--- a/src/backend/parser/parse_clause.c
+++ b/src/backend/parser/parse_clause.c
@@ -102,6 +102,7 @@ static Node *transformFrameOffset(ParseState *pstate, int frameOptions,
Oid rangeopfamily, Oid rangeopcintype, Oid *inRangeFunc,
Node *clause);
+
/*
* transformFromClause -
* Process the FROM clause and add items to the query's range table,
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 415da6417d4..4eb7e35bee4 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -7311,7 +7311,6 @@ get_rule_windowspec(WindowClause *wc, List *targetList,
get_rule_orderby(wc->orderClause, targetList, false, context);
needspace = true;
}
-
/* framing clause is never inherited, so print unless it's default */
if (wc->frameOptions & FRAMEOPTION_NONDEFAULT)
{
diff --git a/src/backend/utils/adt/windowfuncs.c b/src/backend/utils/adt/windowfuncs.c
index 3869f6c8994..d15aa0c75db 100644
--- a/src/backend/utils/adt/windowfuncs.c
+++ b/src/backend/utils/adt/windowfuncs.c
@@ -41,6 +41,7 @@ static bool rank_up(WindowObject winobj);
static Datum leadlag_common(FunctionCallInfo fcinfo,
bool forward, bool withoffset, bool withdefault);
+
/*
* utility routine for *_rank functions.
*/
--
2.50.1 (Apple Git-155)
[text/plain] nocfbot-0003-nav-by-name.txt (67.6K, 4-nocfbot-0003-nav-by-name.txt)
download
[text/plain] nocfbot-0002-drop-unused-includes.txt (2.6K, 5-nocfbot-0002-drop-unused-includes.txt)
download | inline diff:
From 2025f549ef7c653b0e0e87f282ac8fa46feb2fc5 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Wed, 17 Jun 2026 08:16:57 +0900
Subject: [PATCH 02/13] Remove unnecessary includes from the row pattern
recognition patch
Drop includes that are unused or covered by existing forward typedefs:
condition_variable.h, hsearch.h, queryenvironment.h from execnodes.h;
<limits.h> from rpr.c; windowapi.h from execRPR.h. Switch rpr.h from
parsenodes.h to primnodes.h, where RPRNavExpr is actually defined.
---
src/backend/optimizer/plan/rpr.c | 2 --
src/include/executor/execRPR.h | 1 -
src/include/nodes/execnodes.h | 3 ---
src/include/optimizer/rpr.h | 7 ++++---
4 files changed, 4 insertions(+), 9 deletions(-)
diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index 175777a8ffc..50bca59451f 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -37,8 +37,6 @@
#include "postgres.h"
-#include <limits.h>
-
#include "catalog/pg_proc.h"
#include "mb/pg_wchar.h"
#include "miscadmin.h"
diff --git a/src/include/executor/execRPR.h b/src/include/executor/execRPR.h
index 7b2b0febb76..fb7dc63a4c6 100644
--- a/src/include/executor/execRPR.h
+++ b/src/include/executor/execRPR.h
@@ -15,7 +15,6 @@
#define EXECRPR_H
#include "nodes/execnodes.h"
-#include "windowapi.h"
/* NFA context management */
extern RPRNFAContext *ExecRPRStartContext(WindowAggState *winstate,
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 792aa3f0d05..8060b018ef8 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -38,9 +38,6 @@
#include "nodes/plannodes.h"
#include "partitioning/partdefs.h"
#include "storage/buf.h"
-#include "storage/condition_variable.h"
-#include "utils/hsearch.h"
-#include "utils/queryenvironment.h"
#include "utils/reltrigger.h"
#include "utils/typcache.h"
diff --git a/src/include/optimizer/rpr.h b/src/include/optimizer/rpr.h
index 802d2f1dd69..b4f87d8caa4 100644
--- a/src/include/optimizer/rpr.h
+++ b/src/include/optimizer/rpr.h
@@ -10,11 +10,12 @@
*
*-------------------------------------------------------------------------
*/
-#ifndef OPTIMIZER_RPR_H
-#define OPTIMIZER_RPR_H
+#ifndef RPR_H
+#define RPR_H
#include "nodes/parsenodes.h"
#include "nodes/plannodes.h"
+#include "nodes/primnodes.h"
/* Limits and special values */
/*
@@ -96,4 +97,4 @@ typedef struct NavTraversal
extern bool nav_traversal_walker(Node *node, void *ctx);
-#endif /* OPTIMIZER_RPR_H */
+#endif /* RPR_H */
--
2.50.1 (Apple Git-155)
[text/plain] nocfbot-0004-dedicated-define-exprcontext.txt (4.4K, 6-nocfbot-0004-dedicated-define-exprcontext.txt)
download | inline diff:
From a70f1c1e753425adcec6021c07a0f5af6dc0968f Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Mon, 15 Jun 2026 16:54:13 +0900
Subject: [PATCH 04/13] Use a dedicated ExprContext for RPR DEFINE clause
evaluation
DEFINE clauses were evaluated in the per-output-tuple context. Because
EEOP_RPR_NAV_RESTORE copies pass-by-reference navigation results into that
context, resetting it per row freed window function results before
ExecProject (a use-after-free), while not resetting it leaked across the
partition. tmpcontext cannot be reused either, as ExecQualAndReset() resets
it mid-expression when NEXT re-enters spool_tuples. Use a third ExprContext,
reset once per row and separate from both.
---
src/backend/executor/execRPR.c | 5 ++++-
src/backend/executor/nodeWindowAgg.c | 25 ++++++++++++++++++++++---
src/include/nodes/execnodes.h | 1 +
3 files changed, 27 insertions(+), 4 deletions(-)
diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index b326a58bbf5..de78b06d277 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -1592,10 +1592,13 @@ static void
nfa_reevaluate_dependent_vars(WindowAggState *winstate, RPRNFAContext *ctx,
int64 currentPos)
{
- ExprContext *econtext = winstate->ss.ps.ps_ExprContext;
+ ExprContext *econtext = winstate->rprContext;
int64 saved_match_start = winstate->nav_match_start;
int64 saved_pos = winstate->currentpos;
+ /* Release the previous evaluation's DEFINE expression memory */
+ ResetExprContext(econtext);
+
/* Temporarily set nav_match_start and currentpos for FIRST/LAST */
winstate->nav_match_start = ctx->matchStartRow;
winstate->currentpos = currentPos;
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 5b2385f1d8d..90f33bdee40 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -2742,12 +2742,28 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
/*
* Create expression contexts. We need two, one for per-input-tuple
- * processing and one for per-output-tuple processing. We cheat a little
- * by using ExecAssignExprContext() to build both.
+ * processing and one for per-output-tuple processing, plus an optional
+ * third for row pattern recognition DEFINE evaluation (built just below
+ * when a DEFINE clause is present). We cheat a little by using
+ * ExecAssignExprContext() to build them all. Each call overwrites
+ * ps_ExprContext, so the last call must establish the output context.
*/
ExecAssignExprContext(estate, &winstate->ss.ps);
tmpcontext = winstate->ss.ps.ps_ExprContext;
winstate->tmpcontext = tmpcontext;
+
+ /*
+ * Row pattern recognition evaluates DEFINE clauses in a third context,
+ * reset before each DEFINE evaluation pass. It must be distinct from
+ * tmpcontext and ps_ExprContext so its reset frees neither input nor
+ * output tuple memory.
+ */
+ if (node->defineClause != NIL)
+ {
+ ExecAssignExprContext(estate, &winstate->ss.ps);
+ winstate->rprContext = winstate->ss.ps.ps_ExprContext;
+ }
+
ExecAssignExprContext(estate, &winstate->ss.ps);
/* Create long-lived context for storage of partition-local memory etc */
@@ -4572,12 +4588,15 @@ static bool
nfa_evaluate_row(WindowObject winobj, int64 pos, bool *varMatched)
{
WindowAggState *winstate = winobj->winstate;
- ExprContext *econtext = winstate->ss.ps.ps_ExprContext;
+ ExprContext *econtext = winstate->rprContext;
int numDefineVars = list_length(winstate->defineVariableList);
int varIdx = 0;
TupleTableSlot *slot;
int64 saved_pos;
+ /* Release the previous row's DEFINE evaluation memory */
+ ResetExprContext(econtext);
+
/* Fetch current row into temp_slot_1 */
slot = winstate->temp_slot_1;
if (!window_gettupleslot(winobj, pos, slot))
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 8060b018ef8..5b4ac8a6c33 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -2698,6 +2698,7 @@ typedef struct WindowAggState
MemoryContext aggcontext; /* shared context for aggregate working data */
MemoryContext curaggcontext; /* current aggregate's working data */
ExprContext *tmpcontext; /* short-term evaluation context */
+ ExprContext *rprContext; /* DEFINE clause evaluation context */
bool all_first; /* true if the scan is starting */
bool partition_spooled; /* true if all tuples in current partition
--
2.50.1 (Apple Git-155)
[text/plain] nocfbot-0005-match-once-per-row.txt (19.8K, 7-nocfbot-0005-match-once-per-row.txt)
download | inline diff:
From 0ed285dc51016e273694e9724e588493ee243564 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Tue, 16 Jun 2026 13:02:05 +0900
Subject: [PATCH 05/13] Drive RPR row pattern matching once per row
Row pattern matching only advanced when a window function read the frame,
so a row whose only window function skips the frame (e.g. nth_value() with
a NULL offset) left the match state behind the current row, producing
silently wrong results and a spurious "cannot fetch row before mark
position" error.
Advance the match once per row, before the window functions run, so it
tracks the row scan rather than frame access. Extract the reduced-frame
loop into advance_reduced_frame_nfa() and the mark advance into
advance_nav_mark(), advancing the navigation mark from the frontier the
match reached rather than from the output row, so tuplestore_trim() frees
rows sooner.
Add regression coverage for the frame-skipping and PREV-only deferred-frame
cases, and assert the contexts' nondecreasing matchStartRow ordering.
---
src/backend/executor/execRPR.c | 8 +-
src/backend/executor/nodeWindowAgg.c | 254 ++++++++++++++++-----------
src/test/regress/expected/rpr.out | 149 ++++++++++++++++
src/test/regress/sql/rpr.sql | 75 ++++++++
4 files changed, 385 insertions(+), 101 deletions(-)
diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index de78b06d277..cea7e0b2973 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -1666,7 +1666,13 @@ ExecRPRStartContext(WindowAggState *winstate, int64 startPos)
ctx->states->isAbsorbable = false;
}
- /* Add to tail of active context list (doubly-linked, oldest-first) */
+ /*
+ * Add to tail of active context list (doubly-linked, oldest-first).
+ * matchStartRow is nondecreasing along the list, so the head holds the
+ * smallest -- an ordering other code relies on.
+ */
+ Assert(winstate->nfaContextTail == NULL ||
+ startPos >= winstate->nfaContextTail->matchStartRow);
ctx->prev = winstate->nfaContextTail;
ctx->next = NULL;
if (winstate->nfaContextTail != NULL)
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 90f33bdee40..2d97710da8a 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -239,6 +239,10 @@ static int64 row_is_in_reduced_frame(WindowObject winobj, int64 pos);
static void clear_reduced_frame(WindowAggState *winstate);
static int get_reduced_frame_status(WindowAggState *winstate, int64 pos);
+static void advance_nav_mark(WindowAggState *winstate, int64 currentPos);
+static void advance_reduced_frame_nfa(WindowObject winobj,
+ RPRNFAContext *targetCtx, int64 pos,
+ bool hasLimitedFrame, int64 frameOffset);
static void update_reduced_frame(WindowObject winobj, int64 pos);
/* Forward declarations - NFA row evaluation */
@@ -2521,6 +2525,16 @@ ExecWindowAgg(PlanState *pstate)
{
if (winstate->rpSkipTo == ST_NEXT_ROW)
clear_reduced_frame(winstate);
+
+ /*
+ * Drive the row pattern match every row, so it tracks the row
+ * scan rather than frame access: a window function that skips
+ * the frame (e.g. nth_value() with a NULL offset) must not
+ * leave the match state behind currentpos.
+ */
+ Assert(winstate->nav_winobj != NULL);
+ (void) row_is_in_reduced_frame(winstate->nav_winobj,
+ winstate->currentpos);
}
/*
@@ -2562,43 +2576,6 @@ 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 navigation.
- */
- if (winstate->nav_winobj &&
- winstate->rpPattern != NULL &&
- winstate->navMaxOffsetKind == RPR_NAV_OFFSET_FIXED)
- {
- int64 navmarkpos;
-
- /* Backward reach from PREV/LAST/compound PREV_LAST/NEXT_LAST */
- if (winstate->currentpos > winstate->navMaxOffset)
- navmarkpos = winstate->currentpos - winstate->navMaxOffset;
- else
- navmarkpos = 0;
-
- /*
- * If FIRST is used, also consider match_start + navFirstOffset.
- * The oldest active context (nfaContext) has the smallest
- * matchStartRow.
- */
- if (winstate->hasFirstNav &&
- winstate->navFirstOffsetKind == RPR_NAV_OFFSET_FIXED &&
- winstate->nfaContext != NULL)
- {
- int64 firstreach;
-
- if (!pg_add_s64_overflow(winstate->nfaContext->matchStartRow,
- winstate->navFirstOffset,
- &firstreach))
- navmarkpos = Min(navmarkpos, Max(firstreach, 0));
- }
-
- if (navmarkpos > winstate->nav_winobj->markpos)
- WinSetMarkPosition(winstate->nav_winobj, navmarkpos);
- }
-
/*
* Truncate any no-longer-needed rows from the tuplestore.
*/
@@ -4382,6 +4359,143 @@ get_reduced_frame_status(WindowAggState *winstate, int64 pos)
return RF_SKIPPED;
}
+/*
+ * advance_nav_mark
+ * Advance the RPR navigation mark, derived from the NFA frontier
+ * (currentPos) but held back by the navigation's backward reach, so
+ * tuplestore_trim() can free rows no longer reachable by navigation.
+ *
+ * The nav read pointer is independent of the aggregate and per-function read
+ * pointers, so moving its mark does not affect their fetches; it only bounds
+ * the DEFINE clause's own PREV/LAST/FIRST lookups. Backward reach (PREV/LAST)
+ * is measured from the frontier. FIRST reaches back from the head context's
+ * matchStartRow instead, so it is bounded separately; without FIRST the mark
+ * can follow the frontier freely.
+ */
+static void
+advance_nav_mark(WindowAggState *winstate, int64 currentPos)
+{
+ int64 navmarkpos;
+
+ /* No RPR navigation read pointer: nothing to advance */
+ if (winstate->nav_winobj == NULL)
+ return;
+
+ /* RETAIN_ALL disables trim for the backward (PREV/LAST) dimension */
+ if (winstate->navMaxOffsetKind == RPR_NAV_OFFSET_RETAIN_ALL)
+ return;
+
+ /* navMax is FIXED here: NEEDS_EVAL resolved, RETAIN_ALL returned */
+ Assert(winstate->navMaxOffsetKind == RPR_NAV_OFFSET_FIXED);
+
+ if (currentPos > winstate->navMaxOffset)
+ navmarkpos = currentPos - winstate->navMaxOffset;
+ else
+ navmarkpos = 0;
+
+ if (winstate->hasFirstNav && winstate->nfaContext != NULL)
+ {
+ int64 firstreach;
+
+ /* navFirst is always FIXED; it never takes RETAIN_ALL */
+ Assert(winstate->navFirstOffsetKind == RPR_NAV_OFFSET_FIXED);
+
+ /*
+ * Head context has the smallest matchStartRow (contexts appended in
+ * nondecreasing order), so bounding by it covers every FIRST reach.
+ */
+ if (!pg_add_s64_overflow(winstate->nfaContext->matchStartRow,
+ winstate->navFirstOffset,
+ &firstreach))
+ navmarkpos = Min(navmarkpos, Max(firstreach, 0));
+ }
+
+ if (navmarkpos > winstate->nav_winobj->markpos)
+ WinSetMarkPosition(winstate->nav_winobj, navmarkpos);
+}
+
+/*
+ * advance_reduced_frame_nfa
+ * Drive the NFA forward until targetCtx completes or the partition ends.
+ *
+ * This is the match driver, extracted from update_reduced_frame(), which calls
+ * it to advance the match and then records the resolved result. Row
+ * evaluations are shared across all active contexts.
+ */
+static void
+advance_reduced_frame_nfa(WindowObject winobj, RPRNFAContext *targetCtx,
+ int64 pos, bool hasLimitedFrame, int64 frameOffset)
+{
+ WindowAggState *winstate = winobj->winstate;
+ int64 currentPos;
+ int64 startPos;
+
+ /*
+ * Determine where to start processing. Usually nfaLastProcessedRow+1 >=
+ * pos since contexts are created at currentPos+1 during processing.
+ * However, pos can exceed this when rows are skipped (e.g., unmatched
+ * rows don't update nfaLastProcessedRow).
+ */
+ startPos = Max(pos, winstate->nfaLastProcessedRow + 1);
+
+ /*
+ * Process rows until target context completes or we hit boundaries. Each
+ * row evaluation is shared across all active contexts.
+ */
+ for (currentPos = startPos; targetCtx->states != NULL; currentPos++)
+ {
+ bool rowExists;
+
+ /*
+ * Evaluate variables for this row - done only once, shared by all
+ * 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 */
+ if (!rowExists)
+ {
+ ExecRPRFinalizeAllContexts(winstate, currentPos - 1);
+ /* Clean up dead contexts from finalization */
+ ExecRPRCleanupDeadContexts(winstate, targetCtx);
+ break;
+ }
+
+ /* Update last processed row */
+ winstate->nfaLastProcessedRow = currentPos;
+
+ /*--------------------------
+ * Process all contexts for this row:
+ * 1. Match all (convergence)
+ * 2. Absorb redundant
+ * 3. Advance all (divergence)
+ */
+ ExecRPRProcessRow(winstate, currentPos, hasLimitedFrame, frameOffset);
+
+ /*
+ * Create a new context for the next potential start position. This
+ * enables overlapping match detection for SKIP TO NEXT ROW.
+ */
+ ExecRPRStartContext(winstate, currentPos + 1);
+
+ /*
+ * Clean up dead contexts (failed with no active states and no match).
+ * This removes contexts that failed during processing and counts them
+ * appropriately as pruned or mismatched.
+ */
+ ExecRPRCleanupDeadContexts(winstate, targetCtx);
+
+ /* Advance the nav mark to the frontier so trim can free old rows. */
+ advance_nav_mark(winstate, currentPos);
+ }
+}
+
/*
* update_reduced_frame
* Update reduced frame info using multi-context NFA pattern matching.
@@ -4401,8 +4515,6 @@ update_reduced_frame(WindowObject winobj, int64 pos)
{
WindowAggState *winstate = winobj->winstate;
RPRNFAContext *targetCtx;
- int64 currentPos;
- int64 startPos;
int frameOptions = winstate->frameOptions;
bool hasLimitedFrame;
int64 frameOffset = 0;
@@ -4468,67 +4580,9 @@ update_reduced_frame(WindowObject winobj, int64 pos)
goto register_result;
}
- /*
- * Determine where to start processing. Usually nfaLastProcessedRow+1 >=
- * pos since contexts are created at currentPos+1 during processing.
- * However, pos can exceed this when rows are skipped (e.g., unmatched
- * rows don't update nfaLastProcessedRow).
- */
- startPos = Max(pos, winstate->nfaLastProcessedRow + 1);
-
- /*
- * Process rows until target context completes or we hit boundaries. Each
- * row evaluation is shared across all active contexts.
- */
- for (currentPos = startPos; targetCtx->states != NULL; currentPos++)
- {
- bool rowExists;
-
- /*
- * Evaluate variables for this row - done only once, shared by all
- * 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 */
- if (!rowExists)
- {
- ExecRPRFinalizeAllContexts(winstate, currentPos - 1);
- /* Clean up dead contexts from finalization */
- ExecRPRCleanupDeadContexts(winstate, targetCtx);
- break;
- }
-
- /* Update last processed row */
- winstate->nfaLastProcessedRow = currentPos;
-
- /*--------------------------
- * Process all contexts for this row:
- * 1. Match all (convergence)
- * 2. Absorb redundant
- * 3. Advance all (divergence)
- */
- ExecRPRProcessRow(winstate, currentPos, hasLimitedFrame, frameOffset);
-
- /*
- * Create a new context for the next potential start position. This
- * enables overlapping match detection for SKIP TO NEXT ROW.
- */
- ExecRPRStartContext(winstate, currentPos + 1);
-
- /*
- * Clean up dead contexts (failed with no active states and no match).
- * This removes contexts that failed during processing and counts them
- * appropriately as pruned or mismatched.
- */
- ExecRPRCleanupDeadContexts(winstate, targetCtx);
- }
+ /* Drive the NFA forward until pos's match is resolved. */
+ advance_reduced_frame_nfa(winobj, targetCtx, pos, hasLimitedFrame,
+ frameOffset);
register_result:
Assert(pos == targetCtx->matchStartRow);
diff --git a/src/test/regress/expected/rpr.out b/src/test/regress/expected/rpr.out
index dc5140fecc9..6ad830b9e36 100644
--- a/src/test/regress/expected/rpr.out
+++ b/src/test/regress/expected/rpr.out
@@ -3297,6 +3297,155 @@ WINDOW w AS (
4 | 20 | | 0
(4 rows)
+--
+-- nth_value with a NULL offset
+--
+CREATE TABLE rpr_dormant (id int, price int);
+INSERT INTO rpr_dormant SELECT g, g*10 FROM generate_series(1,60) g;
+-- reference: first_value(id) is the start row of the match beginning at the
+-- current row, count(*) is that match's length over the reduced frame
+SELECT * FROM (
+ SELECT id, first_value(id) OVER w AS match_start, count(*) OVER w AS match_len
+ FROM rpr_dormant
+ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+) DEFINE A AS price > PREV(FIRST(price), 50))
+) s WHERE id > 50 ORDER BY id;
+ id | match_start | match_len
+----+-------------+-----------
+ 51 | 51 | 10
+ 52 | | 0
+ 53 | | 0
+ 54 | | 0
+ 55 | | 0
+ 56 | | 0
+ 57 | | 0
+ 58 | | 0
+ 59 | | 0
+ 60 | | 0
+(10 rows)
+
+-- nth_value with a NULL offset; FIRST navigation in DEFINE, SKIP PAST LAST ROW
+SELECT * FROM (
+ SELECT id, nv FROM (
+ SELECT id, nth_value(price, CASE WHEN id < 50 THEN NULL ELSE 1 END) OVER w AS nv
+ FROM rpr_dormant
+ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+) DEFINE A AS price > PREV(FIRST(price), 50))
+ ) s
+) t WHERE id > 50 ORDER BY id;
+ id | nv
+----+-----
+ 51 | 510
+ 52 |
+ 53 |
+ 54 |
+ 55 |
+ 56 |
+ 57 |
+ 58 |
+ 59 |
+ 60 |
+(10 rows)
+
+-- the same window with first_value and count alongside nth_value
+SELECT * FROM (
+ SELECT id, nv, fv, cnt FROM (
+ SELECT id, nth_value(price, CASE WHEN id < 50 THEN NULL ELSE 1 END) OVER w AS nv,
+ first_value(id) OVER w AS fv, count(*) OVER w AS cnt
+ FROM rpr_dormant
+ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+) DEFINE A AS price > PREV(FIRST(price), 50))
+ ) s
+) t WHERE id > 50 ORDER BY id;
+ id | nv | fv | cnt
+----+-----+----+-----
+ 51 | 510 | 51 | 10
+ 52 | | | 0
+ 53 | | | 0
+ 54 | | | 0
+ 55 | | | 0
+ 56 | | | 0
+ 57 | | | 0
+ 58 | | | 0
+ 59 | | | 0
+ 60 | | | 0
+(10 rows)
+
+-- the same nth_value with a non-navigation DEFINE
+SELECT * FROM (
+ SELECT id, nv FROM (
+ SELECT id, nth_value(price, CASE WHEN id < 50 THEN NULL ELSE 1 END) OVER w AS nv
+ FROM rpr_dormant
+ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+) DEFINE A AS price > 0)
+ ) s
+) t WHERE id > 50 ORDER BY id;
+ id | nv
+----+----
+ 51 |
+ 52 |
+ 53 |
+ 54 |
+ 55 |
+ 56 |
+ 57 |
+ 58 |
+ 59 |
+ 60 |
+(10 rows)
+
+-- the same nth_value with a PREV-only DEFINE (no FIRST navigation)
+SELECT * FROM (
+ SELECT id, nv FROM (
+ SELECT id, nth_value(price, CASE WHEN id < 50 THEN NULL ELSE 1 END) OVER w AS nv
+ FROM rpr_dormant
+ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+) DEFINE A AS price > PREV(price, 50))
+ ) s
+) t WHERE id > 50 ORDER BY id;
+ id | nv
+----+-----
+ 51 | 510
+ 52 |
+ 53 |
+ 54 |
+ 55 |
+ 56 |
+ 57 |
+ 58 |
+ 59 |
+ 60 |
+(10 rows)
+
+-- nth_value with a NULL offset band in the middle of the partition
+SELECT * FROM (
+ SELECT id, nv FROM (
+ SELECT id, nth_value(price, CASE WHEN id BETWEEN 20 AND 40 THEN NULL ELSE 1 END) OVER w AS nv
+ FROM rpr_dormant
+ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+) DEFINE A AS price > PREV(FIRST(price), 50))
+ ) s
+) t WHERE id BETWEEN 38 AND 46 ORDER BY id;
+ id | nv
+----+----
+ 38 |
+ 39 |
+ 40 |
+ 41 |
+ 42 |
+ 43 |
+ 44 |
+ 45 |
+ 46 |
+(9 rows)
+
+DROP TABLE rpr_dormant;
--
-- NULL handling
--
diff --git a/src/test/regress/sql/rpr.sql b/src/test/regress/sql/rpr.sql
index e3e9de789db..3363691c041 100644
--- a/src/test/regress/sql/rpr.sql
+++ b/src/test/regress/sql/rpr.sql
@@ -1812,6 +1812,81 @@ WINDOW w AS (
B AS val IS NULL
);
+--
+-- nth_value with a NULL offset
+--
+
+CREATE TABLE rpr_dormant (id int, price int);
+INSERT INTO rpr_dormant SELECT g, g*10 FROM generate_series(1,60) g;
+
+-- reference: first_value(id) is the start row of the match beginning at the
+-- current row, count(*) is that match's length over the reduced frame
+SELECT * FROM (
+ SELECT id, first_value(id) OVER w AS match_start, count(*) OVER w AS match_len
+ FROM rpr_dormant
+ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+) DEFINE A AS price > PREV(FIRST(price), 50))
+) s WHERE id > 50 ORDER BY id;
+
+-- nth_value with a NULL offset; FIRST navigation in DEFINE, SKIP PAST LAST ROW
+SELECT * FROM (
+ SELECT id, nv FROM (
+ SELECT id, nth_value(price, CASE WHEN id < 50 THEN NULL ELSE 1 END) OVER w AS nv
+ FROM rpr_dormant
+ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+) DEFINE A AS price > PREV(FIRST(price), 50))
+ ) s
+) t WHERE id > 50 ORDER BY id;
+
+-- the same window with first_value and count alongside nth_value
+SELECT * FROM (
+ SELECT id, nv, fv, cnt FROM (
+ SELECT id, nth_value(price, CASE WHEN id < 50 THEN NULL ELSE 1 END) OVER w AS nv,
+ first_value(id) OVER w AS fv, count(*) OVER w AS cnt
+ FROM rpr_dormant
+ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+) DEFINE A AS price > PREV(FIRST(price), 50))
+ ) s
+) t WHERE id > 50 ORDER BY id;
+
+-- the same nth_value with a non-navigation DEFINE
+SELECT * FROM (
+ SELECT id, nv FROM (
+ SELECT id, nth_value(price, CASE WHEN id < 50 THEN NULL ELSE 1 END) OVER w AS nv
+ FROM rpr_dormant
+ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+) DEFINE A AS price > 0)
+ ) s
+) t WHERE id > 50 ORDER BY id;
+
+-- the same nth_value with a PREV-only DEFINE (no FIRST navigation)
+SELECT * FROM (
+ SELECT id, nv FROM (
+ SELECT id, nth_value(price, CASE WHEN id < 50 THEN NULL ELSE 1 END) OVER w AS nv
+ FROM rpr_dormant
+ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+) DEFINE A AS price > PREV(price, 50))
+ ) s
+) t WHERE id > 50 ORDER BY id;
+
+-- nth_value with a NULL offset band in the middle of the partition
+SELECT * FROM (
+ SELECT id, nv FROM (
+ SELECT id, nth_value(price, CASE WHEN id BETWEEN 20 AND 40 THEN NULL ELSE 1 END) OVER w AS nv
+ FROM rpr_dormant
+ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP PAST LAST ROW
+ PATTERN (A+) DEFINE A AS price > PREV(FIRST(price), 50))
+ ) s
+) t WHERE id BETWEEN 38 AND 46 ORDER BY id;
+
+DROP TABLE rpr_dormant;
+
--
-- NULL handling
--
--
2.50.1 (Apple Git-155)
[text/plain] nocfbot-0006-tidy-plumbing.txt (26.2K, 8-nocfbot-0006-tidy-plumbing.txt)
download | inline diff:
From b8484088192a3ea8d24bdbff4b4596450e1383c3 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Wed, 17 Jun 2026 13:12:36 +0900
Subject: [PATCH 06/13] Tidy up row pattern recognition plumbing
Several behavior-neutral cleanups to the row pattern recognition code,
with no change to planner or executor output:
- Remove the dead collectPatternVariables() and buildDefineVariableList()
helpers. The parser already guarantees that every DEFINE variable
appears in PATTERN, so create_windowagg_plan() and cost_windowagg() walk
the DEFINE clause directly instead.
- Drop the redundant rpSkipTo and defineClause arguments of
make_windowagg(); both are read from the WindowClause.
- Mark RPRNavExpr.resulttype as query_jumble_ignore, matching the other
derived result-type fields.
- Rename WindowAggState.defineClauseList to defineClauseExprs to reflect
that it stores ExprState nodes.
- Replace a post-loop ListCell NULL test in remove_unused_subquery_outputs()
with a boolean flag, flatten the nested block in show_window_def(), and
flatten the DEFINE init loop in ExecInitWindowAgg().
- Minor regression-test comment cleanup.
---
src/backend/commands/explain.c | 94 +++++++++----------
src/backend/executor/README.rpr | 5 +-
src/backend/executor/execRPR.c | 2 +-
src/backend/executor/nodeWindowAgg.c | 41 ++++----
src/backend/optimizer/path/allpaths.c | 13 ++-
src/backend/optimizer/path/costsize.c | 22 +----
src/backend/optimizer/plan/createplan.c | 24 +++--
src/backend/optimizer/plan/rpr.c | 77 ---------------
src/include/nodes/execnodes.h | 2 +-
src/include/nodes/primnodes.h | 3 +-
src/include/optimizer/rpr.h | 3 -
src/test/regress/expected/rpr_integration.out | 29 ++----
src/test/regress/sql/rpr_integration.sql | 31 ++----
13 files changed, 110 insertions(+), 236 deletions(-)
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 70fd7f386a0..696bdb9c8b5 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -3212,77 +3212,73 @@ show_window_def(WindowAggState *planstate, List *ancestors, ExplainState *es)
/* Show Row Pattern Recognition pattern if present */
if (wagg->rpPattern != NULL)
{
+ RPRNavOffsetKind maxKind = wagg->navMaxOffsetKind;
+ int64 maxOffset = wagg->navMaxOffset;
+ RPRNavOffsetKind firstKind = wagg->navFirstOffsetKind;
+ int64 firstOffset = wagg->navFirstOffset;
+
char *patternStr = deparse_rpr_pattern(wagg->rpPattern);
- if (patternStr != NULL)
- {
- ExplainPropertyText("Pattern", patternStr, es);
- pfree(patternStr);
- }
+ ExplainPropertyText("Pattern", patternStr, es);
+
+ pfree(patternStr);
/*
* Show navigation offsets for tuplestore trim. For EXPLAIN ANALYZE,
* use the executor-resolved values (which may differ from the plan
* when NEEDS_EVAL was resolved to FIXED or RETAIN_ALL at init).
*/
+ if (es->analyze)
{
- RPRNavOffsetKind maxKind = wagg->navMaxOffsetKind;
- int64 maxOffset = wagg->navMaxOffset;
- RPRNavOffsetKind firstKind = wagg->navFirstOffsetKind;
- int64 firstOffset = wagg->navFirstOffset;
+ maxKind = planstate->navMaxOffsetKind;
+ maxOffset = planstate->navMaxOffset;
+ firstKind = planstate->navFirstOffsetKind;
+ firstOffset = planstate->navFirstOffset;
+ }
- if (es->analyze)
- {
- maxKind = planstate->navMaxOffsetKind;
- maxOffset = planstate->navMaxOffset;
- firstKind = planstate->navFirstOffsetKind;
- firstOffset = planstate->navFirstOffset;
- }
+ switch (maxKind)
+ {
+ case RPR_NAV_OFFSET_NEEDS_EVAL:
+ ExplainPropertyText("Nav Mark Lookback", "runtime", es);
+ break;
+ case RPR_NAV_OFFSET_RETAIN_ALL:
+ ExplainPropertyText("Nav Mark Lookback", "retain all", es);
+ break;
+ case RPR_NAV_OFFSET_FIXED:
+ ExplainPropertyInteger("Nav Mark Lookback", NULL,
+ maxOffset, es);
+ break;
+ default:
+ elog(ERROR, "unrecognized RPR nav offset kind: %d",
+ maxKind);
+ break;
+ }
- switch (maxKind)
+ if (wagg->hasFirstNav)
+ {
+ switch (firstKind)
{
case RPR_NAV_OFFSET_NEEDS_EVAL:
- ExplainPropertyText("Nav Mark Lookback", "runtime", es);
+ ExplainPropertyText("Nav Mark Lookahead", "runtime",
+ es);
break;
case RPR_NAV_OFFSET_RETAIN_ALL:
- ExplainPropertyText("Nav Mark Lookback", "retain all", es);
+ ExplainPropertyText("Nav Mark Lookahead", "retain all",
+ es);
break;
case RPR_NAV_OFFSET_FIXED:
- ExplainPropertyInteger("Nav Mark Lookback", NULL,
- maxOffset, es);
+ if (firstOffset == PG_INT64_MAX)
+ ExplainPropertyText("Nav Mark Lookahead", "infinite",
+ es);
+ else
+ ExplainPropertyInteger("Nav Mark Lookahead", NULL,
+ firstOffset, es);
break;
default:
elog(ERROR, "unrecognized RPR nav offset kind: %d",
- maxKind);
+ firstKind);
break;
}
-
- if (wagg->hasFirstNav)
- {
- switch (firstKind)
- {
- case RPR_NAV_OFFSET_NEEDS_EVAL:
- ExplainPropertyText("Nav Mark Lookahead", "runtime",
- es);
- break;
- case RPR_NAV_OFFSET_RETAIN_ALL:
- ExplainPropertyText("Nav Mark Lookahead", "retain all",
- es);
- break;
- case RPR_NAV_OFFSET_FIXED:
- if (firstOffset == PG_INT64_MAX)
- ExplainPropertyText("Nav Mark Lookahead", "infinite",
- es);
- else
- ExplainPropertyInteger("Nav Mark Lookahead", NULL,
- firstOffset, es);
- break;
- default:
- elog(ERROR, "unrecognized RPR nav offset kind: %d",
- firstKind);
- break;
- }
- }
}
}
}
diff --git a/src/backend/executor/README.rpr b/src/backend/executor/README.rpr
index 00af86681b8..713ad84e1d9 100644
--- a/src/backend/executor/README.rpr
+++ b/src/backend/executor/README.rpr
@@ -188,7 +188,6 @@ Chapter IV Compilation Phase
IV-1. Entry Point
create_windowagg_plan() (createplan.c)
- +-- buildDefineVariableList() Build variable name list from DEFINE
+-- buildRPRPattern() NFA compilation (6 phases)
IV-2. The 6 Phases of buildRPRPattern()
@@ -1468,8 +1467,6 @@ Appendix A. Key Function Index
--------------------------------------------------------------------------
transformRPR parse_rpr.c Parser entry point
transformDefineClause parse_rpr.c DEFINE transformation
- collectPatternVariables rpr.c Variable collection
- buildDefineVariableList rpr.c DEFINE variable list
buildRPRPattern rpr.c NFA compilation main
optimizeRPRPattern rpr.c AST optimization
fillRPRPattern rpr.c NFA element generation
@@ -1538,7 +1535,7 @@ Appendix B. Data Structure Relationship Diagram
|--- rpSkipTo: RPSkipTo (AFTER MATCH SKIP mode)
|--- rpPattern: RPRPattern* (copied from plan)
|--- defineVariableList: List<String> (variable names, DEFINE order)
- |--- defineClauseList: List<ExprState>
+ |--- defineClauseExprs: List<ExprState>
|--- nfaVarMatched: bool[] (per-row cache)
|--- defineMatchStartDependent: Bitmapset* (match_start_dependent
| DEFINE vars; see VI-4)
diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index cea7e0b2973..def0b8423b3 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -1606,7 +1606,7 @@ nfa_reevaluate_dependent_vars(WindowAggState *winstate, RPRNFAContext *ctx,
/* Invalidate nav_slot cache since match_start changed */
winstate->nav_slot_pos = -1;
- foreach_ptr(ExprState, exprState, winstate->defineClauseList)
+ foreach_ptr(ExprState, exprState, winstate->defineClauseExprs)
{
int varIdx = foreach_current_index(exprState);
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 2d97710da8a..5d4832d1db9 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -3067,27 +3067,26 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
/* Set up row pattern recognition DEFINE clause */
winstate->defineVariableList = NIL;
- winstate->defineClauseList = NIL;
- if (node->defineClause != NIL)
+ winstate->defineClauseExprs = NIL;
+
+ /*
+ * 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_node(TargetEntry, te, node->defineClause)
{
- /*
- * 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_node(TargetEntry, te, node->defineClause)
- {
- char *name = te->resname;
- Expr *expr = te->expr;
- ExprState *exps;
-
- winstate->defineVariableList =
- lappend(winstate->defineVariableList,
- makeString(pstrdup(name)));
- exps = ExecInitExpr(expr, (PlanState *) winstate);
- winstate->defineClauseList =
- lappend(winstate->defineClauseList, exps);
- }
+ char *name = te->resname;
+ ExprState *exprstate;
+
+ winstate->defineVariableList =
+ lappend(winstate->defineVariableList,
+ makeString(pstrdup(name)));
+
+ exprstate = ExecInitExpr(te->expr, (PlanState *) winstate);
+
+ winstate->defineClauseExprs =
+ lappend(winstate->defineClauseExprs, exprstate);
}
/* Initialize NFA free lists for row pattern matching */
@@ -4669,7 +4668,7 @@ nfa_evaluate_row(WindowObject winobj, int64 pos, bool *varMatched)
/* Invalidate nav_slot cache so PREV/NEXT re-fetch for new row */
winstate->nav_slot_pos = -1;
- foreach_ptr(ExprState, exprState, winstate->defineClauseList)
+ foreach_ptr(ExprState, exprState, winstate->defineClauseExprs)
{
Datum result;
bool isnull;
diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c
index f3c9f3c0bd6..44864b3635b 100644
--- a/src/backend/optimizer/path/allpaths.c
+++ b/src/backend/optimizer/path/allpaths.c
@@ -4807,20 +4807,19 @@ remove_unused_subquery_outputs(Query *subquery, RelOptInfo *rel,
*/
if (IsA(texpr, WindowFunc))
{
+ bool is_rpr = false;
WindowFunc *wfunc = (WindowFunc *) texpr;
- ListCell *wlc;
- foreach(wlc, subquery->windowClause)
+ foreach_node(WindowClause, wc, subquery->windowClause)
{
- WindowClause *wc = lfirst_node(WindowClause, wlc);
-
- if (wc->winref == wfunc->winref &&
- wc->defineClause != NIL)
+ if (wc->winref == wfunc->winref && wc->defineClause != NIL)
{
+ is_rpr = true;
break;
}
}
- if (wlc != NULL)
+
+ if (is_rpr)
continue;
}
diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c
index 82472c3fe96..32a4b172e3d 100644
--- a/src/backend/optimizer/path/costsize.c
+++ b/src/backend/optimizer/path/costsize.c
@@ -3231,30 +3231,18 @@ cost_windowagg(Path *path, PlannerInfo *root,
* so we'll just do this for now.
*
* Moreover, if row pattern recognition is used, we charge the DEFINE
- * expressions once per tuple for each variable that appears in PATTERN.
+ * expressions once per tuple for each DEFINE variable.
*/
if (winclause->rpPattern)
{
- List *pattern_vars;
QualCost defcosts;
- pattern_vars = collectPatternVariables(winclause->rpPattern);
-
- foreach_node(String, pv, pattern_vars)
+ foreach_node(TargetEntry, def, winclause->defineClause)
{
- char *ptname = strVal(pv);
-
- foreach_node(TargetEntry, def, winclause->defineClause)
- {
- if (!strcmp(ptname, def->resname))
- {
- cost_qual_eval_node(&defcosts, (Node *) def->expr, root);
- startup_cost += defcosts.startup;
- total_cost += defcosts.per_tuple * input_tuples;
- }
- }
+ cost_qual_eval_node(&defcosts, (Node *) def->expr, root);
+ startup_cost += defcosts.startup;
+ total_cost += defcosts.per_tuple * input_tuples;
}
- list_free_deep(pattern_vars);
}
foreach(lc, windowFuncs)
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index cca4126e511..d96e76b0221 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -290,9 +290,8 @@ static Memoize *make_memoize(Plan *lefttree, Oid *hashoperators,
static WindowAgg *make_windowagg(List *tlist, WindowClause *wc,
int partNumCols, AttrNumber *partColIdx, Oid *partOperators, Oid *partCollations,
int ordNumCols, AttrNumber *ordColIdx, Oid *ordOperators, Oid *ordCollations,
- List *runCondition, RPSkipTo rpSkipTo,
+ List *runCondition,
RPRPattern *compiledPattern,
- List *defineClause,
Bitmapset *defineMatchStartDependent,
RPRNavOffsetKind navMaxOffsetKind, int64 navMaxOffset,
bool hasFirstNav,
@@ -2822,7 +2821,6 @@ create_windowagg_plan(PlannerInfo *root, WindowAggPath *best_path)
Oid *ordCollations;
ListCell *lc;
List *defineVariableList = NIL;
- List *filteredDefineClause = NIL;
RPRPattern *compiledPattern = NULL;
Bitmapset *matchStartDependent = NULL;
RPRNavOffsetKind navMaxOffsetKind = RPR_NAV_OFFSET_FIXED;
@@ -2889,8 +2887,9 @@ create_windowagg_plan(PlannerInfo *root, WindowAggPath *best_path)
* rejects DEFINE variables not used in PATTERN, so no filtering is
* needed.
*/
- buildDefineVariableList(wc->defineClause, &defineVariableList);
- filteredDefineClause = wc->defineClause;
+ foreach_node(TargetEntry, te, wc->defineClause)
+ defineVariableList = lappend(defineVariableList,
+ makeString(pstrdup(te->resname)));
/*
* Walk DEFINE once: collect nav offsets (for tuplestore trim) and the
@@ -2923,13 +2922,13 @@ create_windowagg_plan(PlannerInfo *root, WindowAggPath *best_path)
ordOperators,
ordCollations,
best_path->runCondition,
- wc->rpSkipTo,
compiledPattern,
- filteredDefineClause,
matchStartDependent,
- navMaxOffsetKind, navMaxOffset,
+ navMaxOffsetKind,
+ navMaxOffset,
hasFirstNav,
- navFirstOffsetKind, navFirstOffset,
+ navFirstOffsetKind,
+ navFirstOffset,
best_path->qual,
best_path->topwindow,
subplan);
@@ -7000,9 +6999,8 @@ static WindowAgg *
make_windowagg(List *tlist, WindowClause *wc,
int partNumCols, AttrNumber *partColIdx, Oid *partOperators, Oid *partCollations,
int ordNumCols, AttrNumber *ordColIdx, Oid *ordOperators, Oid *ordCollations,
- List *runCondition, RPSkipTo rpSkipTo,
+ List *runCondition,
RPRPattern *compiledPattern,
- List *defineClause,
Bitmapset *defineMatchStartDependent,
RPRNavOffsetKind navMaxOffsetKind, int64 navMaxOffset,
bool hasFirstNav,
@@ -7034,12 +7032,12 @@ make_windowagg(List *tlist, WindowClause *wc,
node->inRangeAsc = wc->inRangeAsc;
node->inRangeNullsFirst = wc->inRangeNullsFirst;
node->topWindow = topWindow;
- node->rpSkipTo = rpSkipTo;
+ node->rpSkipTo = wc->rpSkipTo;
/* Store compiled pattern for NFA execution */
node->rpPattern = compiledPattern;
- node->defineClause = defineClause;
+ node->defineClause = wc->defineClause;
/* Store pre-computed match_start dependency bitmapset */
node->defineMatchStartDependent = defineMatchStartDependent;
diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index 50bca59451f..48b76a842ed 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -96,9 +96,6 @@ static void computeAbsorbabilityRecursive(RPRPattern *pattern,
bool *hasAbsorbable);
static void computeAbsorbability(RPRPattern *pattern);
-static void collectPatternVariablesRecursive(RPRPatternNode *node,
- List **varNames);
-
/*
* rprPatternEqual
* Compare two RPRPatternNode trees for equality.
@@ -1824,59 +1821,6 @@ computeAbsorbability(RPRPattern *pattern)
pattern->isAbsorbable = hasAbsorbable;
}
-/*
- * collectPatternVariablesRecursive
- * Recursively collect variable names from pattern AST.
- */
-static void
-collectPatternVariablesRecursive(RPRPatternNode *node, List **varNames)
-{
- Assert(node != NULL);
-
- check_stack_depth();
-
- switch (node->nodeType)
- {
- case RPR_PATTERN_VAR:
- /* Add variable if not already in list */
- foreach_node(String, varname, *varNames)
- {
- if (strcmp(strVal(varname), node->varName) == 0)
- return; /* Already collected */
- }
- *varNames = lappend(*varNames, makeString(pstrdup(node->varName)));
- break;
-
- case RPR_PATTERN_SEQ:
- case RPR_PATTERN_ALT:
- case RPR_PATTERN_GROUP:
- foreach_node(RPRPatternNode, child, node->children)
- {
- collectPatternVariablesRecursive(child, varNames);
- }
- break;
- }
-}
-
-/*
- * collectPatternVariables
- * Collect unique variable names used in PATTERN clause.
- *
- * Returns a list of String nodes containing variable names.
- * Used to filter defineClause before varId assignment.
- */
-List *
-collectPatternVariables(RPRPatternNode *pattern)
-{
- List *varNames = NIL;
-
- /* Caller ensures pattern is not NULL */
- Assert(pattern != NULL);
-
- collectPatternVariablesRecursive(pattern, &varNames);
- return varNames;
-}
-
/*
* rpr_volatile_func_checker
* check_functions_in_node callback: true if funcid is VOLATILE.
@@ -1953,27 +1897,6 @@ validate_rpr_define_volatility(List *defineClause)
}
}
-/*
- * buildDefineVariableList
- * Build defineVariableList from defineClause.
- *
- * The parser already ensures that all DEFINE variables appear in PATTERN,
- * so no filtering is needed here.
- *
- * Sets *defineVariableList to list of variable names (String nodes).
- */
-void
-buildDefineVariableList(List *defineClause, List **defineVariableList)
-{
- *defineVariableList = NIL;
-
- foreach_node(TargetEntry, te, defineClause)
- {
- *defineVariableList = lappend(*defineVariableList,
- makeString(pstrdup(te->resname)));
- }
-}
-
/*
* buildRPRPattern
* Compile pattern AST to flat bytecode array.
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 5b4ac8a6c33..59b8656c857 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -2654,7 +2654,7 @@ typedef struct WindowAggState
struct RPRPattern *rpPattern; /* compiled pattern for NFA execution */
List *defineVariableList; /* list of row pattern definition
* variables (list of String) */
- List *defineClauseList; /* expression for row pattern definition
+ List *defineClauseExprs; /* expression for row pattern definition
* search conditions ExprState list */
RPRNFAContext *nfaContext; /* active matching contexts (head) */
RPRNFAContext *nfaContextTail; /* tail of active contexts (for reverse
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index 7c1b3d1bb07..8576a3c9c5b 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -687,7 +687,8 @@ typedef struct RPRNavExpr
Expr *offset_arg; /* offset expression, or NULL for default */
Expr *compound_offset_arg; /* outer offset for compound nav, or
* NULL if simple */
- Oid resulttype; /* result type (same as arg's type) */
+ /* result type (same as arg's type) */
+ Oid resulttype pg_node_attr(query_jumble_ignore);
/* OID of collation of result */
Oid resultcollid pg_node_attr(query_jumble_ignore);
/* token location, or -1 if unknown */
diff --git a/src/include/optimizer/rpr.h b/src/include/optimizer/rpr.h
index b4f87d8caa4..23763442b65 100644
--- a/src/include/optimizer/rpr.h
+++ b/src/include/optimizer/rpr.h
@@ -67,10 +67,7 @@
#define RPRElemIsFin(e) ((e)->varId == RPR_VARID_FIN)
#define RPRElemCanSkip(e) ((e)->min == 0)
-extern List *collectPatternVariables(RPRPatternNode *pattern);
extern void validate_rpr_define_volatility(List *defineClause);
-extern void buildDefineVariableList(List *defineClause,
- List **defineVariableList);
extern RPRPattern *buildRPRPattern(RPRPatternNode *pattern, List *defineVariableList,
RPSkipTo rpSkipTo, int frameOptions,
bool hasMatchStartDependent);
diff --git a/src/test/regress/expected/rpr_integration.out b/src/test/regress/expected/rpr_integration.out
index 80a32cca9ab..652989927d7 100644
--- a/src/test/regress/expected/rpr_integration.out
+++ b/src/test/regress/expected/rpr_integration.out
@@ -240,9 +240,9 @@ ORDER BY id;
-- must therefore yield two separate WindowAgg nodes. Inline specs
-- are used here because dedup only applies to inline windows.
-- Baseline: two inline RPR windows that are structurally identical
--- (same ORDER BY, frame, PATTERN, and DEFINE) are deduped by the
--- parser into a single WindowAgg node, confirming that parser-level
--- dedup is active for RPR windows whose DEFINE matches.
+-- (same PARTITION BY, ORDER BY, frame, PATTERN, and DEFINE) are deduped by the
+-- parser into a single WindowAgg node, confirming that parser-level dedup is
+-- active for RPR windows whose DEFINE matches.
EXPLAIN (COSTS OFF)
SELECT
count(*) OVER (ORDER BY id
@@ -325,15 +325,10 @@ ORDER BY id;
-- A5. Unused window removal prevention
-- ============================================================
-- Verify that remove_unused_subquery_outputs() does not drop an RPR
--- window function even when the outer query does not reference its
--- result. The RPR WindowAgg node is responsible for performing pattern
--- matching, so removing the window function would silently skip the
--- pattern match even though the surrounding query still depends on
--- RPR semantics.
--- The outer query ignores the per-row window result, yet pattern
--- matching must still execute. The plan must still contain a
--- WindowAgg node below the outer Aggregate; if the window were
--- removed, only Aggregate + Seq Scan would appear.
+-- window function when the outer query does not reference its result.
+-- The WindowAgg node performs the pattern match itself; without it,
+-- the match would be silently skipped. The plan must contain a
+-- WindowAgg node beneath the outer Aggregate.
EXPLAIN (COSTS OFF)
SELECT count(*) FROM (
SELECT count(*) OVER w FROM rpr_integ
@@ -587,10 +582,8 @@ WHERE cnt > 0;
-- EXPLAIN VERBOSE is therefore expected to show a clean targetlist on
-- the outer WindowAgg, with no DEFINE-derived expression leaking in.
-- Note: columns referenced by DEFINE (e.g., "val") may appear as
--- resjunk entries in upper WindowAgg targetlists -- that is a
--- harmless byproduct of the column guard's broad scope and does not
--- affect client output. The claim here is limited to the full
--- DEFINE boolean expression.
+-- resjunk entries in upper WindowAgg targetlists -- but that is harmless.
+-- The claim here is limited to the full DEFINE boolean expression.
EXPLAIN (VERBOSE, COSTS OFF)
SELECT
count(*) OVER w_rpr AS rpr_cnt,
@@ -620,10 +613,6 @@ WINDOW
Output: id, val
(13 rows)
--- Executing the same query shows the client result is limited to
--- the two projected columns; "id" and "val" that appeared in the
--- upper WindowAgg Output line are resjunk-only and do not reach
--- the client.
SELECT
count(*) OVER w_rpr AS rpr_cnt,
count(*) OVER w_normal AS normal_cnt
diff --git a/src/test/regress/sql/rpr_integration.sql b/src/test/regress/sql/rpr_integration.sql
index 1d6075265bb..2b990b24704 100644
--- a/src/test/regress/sql/rpr_integration.sql
+++ b/src/test/regress/sql/rpr_integration.sql
@@ -166,9 +166,9 @@ ORDER BY id;
-- are used here because dedup only applies to inline windows.
-- Baseline: two inline RPR windows that are structurally identical
--- (same ORDER BY, frame, PATTERN, and DEFINE) are deduped by the
--- parser into a single WindowAgg node, confirming that parser-level
--- dedup is active for RPR windows whose DEFINE matches.
+-- (same PARTITION BY, ORDER BY, frame, PATTERN, and DEFINE) are deduped by the
+-- parser into a single WindowAgg node, confirming that parser-level dedup is
+-- active for RPR windows whose DEFINE matches.
EXPLAIN (COSTS OFF)
SELECT
count(*) OVER (ORDER BY id
@@ -214,16 +214,10 @@ ORDER BY id;
-- A5. Unused window removal prevention
-- ============================================================
-- Verify that remove_unused_subquery_outputs() does not drop an RPR
--- window function even when the outer query does not reference its
--- result. The RPR WindowAgg node is responsible for performing pattern
--- matching, so removing the window function would silently skip the
--- pattern match even though the surrounding query still depends on
--- RPR semantics.
-
--- The outer query ignores the per-row window result, yet pattern
--- matching must still execute. The plan must still contain a
--- WindowAgg node below the outer Aggregate; if the window were
--- removed, only Aggregate + Seq Scan would appear.
+-- window function when the outer query does not reference its result.
+-- The WindowAgg node performs the pattern match itself; without it,
+-- the match would be silently skipped. The plan must contain a
+-- WindowAgg node beneath the outer Aggregate.
EXPLAIN (COSTS OFF)
SELECT count(*) FROM (
SELECT count(*) OVER w FROM rpr_integ
@@ -399,11 +393,8 @@ WHERE cnt > 0;
-- EXPLAIN VERBOSE is therefore expected to show a clean targetlist on
-- the outer WindowAgg, with no DEFINE-derived expression leaking in.
-- Note: columns referenced by DEFINE (e.g., "val") may appear as
--- resjunk entries in upper WindowAgg targetlists -- that is a
--- harmless byproduct of the column guard's broad scope and does not
--- affect client output. The claim here is limited to the full
--- DEFINE boolean expression.
-
+-- resjunk entries in upper WindowAgg targetlists -- but that is harmless.
+-- The claim here is limited to the full DEFINE boolean expression.
EXPLAIN (VERBOSE, COSTS OFF)
SELECT
count(*) OVER w_rpr AS rpr_cnt,
@@ -417,10 +408,6 @@ WINDOW
w_normal AS (ORDER BY id
ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING);
--- Executing the same query shows the client result is limited to
--- the two projected columns; "id" and "val" that appeared in the
--- upper WindowAgg Output line are resjunk-only and do not reach
--- the client.
SELECT
count(*) OVER w_rpr AS rpr_cnt,
count(*) OVER w_normal AS normal_cnt
--
2.50.1 (Apple Git-155)
[text/plain] nocfbot-0007-tidy-plumbing-more.txt (9.0K, 9-nocfbot-0007-tidy-plumbing-more.txt)
download | inline diff:
From 4cf9108ce7796a3821873f8377c95e34799760f8 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Wed, 17 Jun 2026 14:05:25 +0900
Subject: [PATCH 07/13] Further tidy up row pattern recognition plumbing
More behavior-neutral cleanups to the row pattern recognition code,
continuing the previous tidy-up and with no change to planner or
executor output:
- Drop the now-unused WindowClause argument of transformDefineClause()
and flatten the redundant nested block in its DEFINE-variable loop.
- Replace manual ListCell iteration and index bookkeeping with
foreach_node()/foreach_current_index() in
validate_rpr_define_volatility() and nfa_evaluate_row(), and drop the
redundant end-of-list break tests in nfa_evaluate_row() and
nfa_reevaluate_dependent_vars() now that the loops walk
defineClauseExprs directly.
- Test winstate->defineVariableList against NIL instead of
list_length() > 0 in ExecInitWindowAgg().
- Remove the now-unused makefuncs.h include from plan/rpr.c.
- Fix the stale struct name in the RPCommonSyntax header comment.
---
src/backend/executor/execRPR.c | 3 -
src/backend/executor/nodeWindowAgg.c | 9 +--
src/backend/optimizer/plan/rpr.c | 7 +--
src/backend/parser/parse_rpr.c | 84 +++++++++++++---------------
src/include/nodes/parsenodes.h | 2 +-
5 files changed, 44 insertions(+), 61 deletions(-)
diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index def0b8423b3..099b81aeb81 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -1618,9 +1618,6 @@ nfa_reevaluate_dependent_vars(WindowAggState *winstate, RPRNFAContext *ctx,
result = ExecEvalExpr(exprState, econtext, &isnull);
winstate->nfaVarMatched[varIdx] = (!isnull && DatumGetBool(result));
}
-
- if (varIdx + 1 >= list_length(winstate->defineVariableList))
- break;
}
/* Restore original match_start, currentpos, and invalidate cache */
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 5d4832d1db9..819ad814bf5 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -3103,7 +3103,7 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
* ordering (DEFINE order first), varId == defineIdx for all defined
* variables, so no mapping is needed.
*/
- if (list_length(winstate->defineVariableList) > 0)
+ if (winstate->defineVariableList != NIL)
winstate->nfaVarMatched = palloc0(sizeof(bool) *
list_length(winstate->defineVariableList));
else
@@ -4642,8 +4642,6 @@ nfa_evaluate_row(WindowObject winobj, int64 pos, bool *varMatched)
{
WindowAggState *winstate = winobj->winstate;
ExprContext *econtext = winstate->rprContext;
- int numDefineVars = list_length(winstate->defineVariableList);
- int varIdx = 0;
TupleTableSlot *slot;
int64 saved_pos;
@@ -4670,6 +4668,7 @@ nfa_evaluate_row(WindowObject winobj, int64 pos, bool *varMatched)
foreach_ptr(ExprState, exprState, winstate->defineClauseExprs)
{
+ int varIdx = foreach_current_index(exprState);
Datum result;
bool isnull;
@@ -4677,10 +4676,6 @@ nfa_evaluate_row(WindowObject winobj, int64 pos, bool *varMatched)
result = ExecEvalExpr(exprState, econtext, &isnull);
varMatched[varIdx] = (!isnull && DatumGetBool(result));
-
- varIdx++;
- if (varIdx >= numDefineVars)
- break;
}
winstate->currentpos = saved_pos;
diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index 48b76a842ed..597a966c7b1 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -40,7 +40,6 @@
#include "catalog/pg_proc.h"
#include "mb/pg_wchar.h"
#include "miscadmin.h"
-#include "nodes/makefuncs.h"
#include "nodes/nodeFuncs.h"
#include "optimizer/rpr.h"
#include "tcop/tcopprot.h"
@@ -1887,12 +1886,8 @@ reject_volatile_in_define_walker(Node *node, void *context)
void
validate_rpr_define_volatility(List *defineClause)
{
- ListCell *lc;
-
- foreach(lc, defineClause)
+ foreach_node(TargetEntry, te, defineClause)
{
- TargetEntry *te = lfirst_node(TargetEntry, lc);
-
(void) reject_volatile_in_define_walker((Node *) te->expr, NULL);
}
}
diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index 8ed01bb8f28..3e6b2e579a3 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -54,8 +54,8 @@ typedef struct
/* Forward declarations */
static void validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
List *rpDefs, List **varNames);
-static List *transformDefineClause(ParseState *pstate, WindowClause *wc,
- WindowDef *windef, List **targetlist);
+static List *transformDefineClause(ParseState *pstate, WindowDef *windef,
+ List **targetlist);
static bool define_walker(Node *node, void *context);
/*
@@ -178,7 +178,7 @@ transformRPR(ParseState *pstate, WindowClause *wc, WindowDef *windef,
wc->initial = windef->rpCommonSyntax->initial;
/* Transform DEFINE clause into list of TargetEntry's */
- wc->defineClause = transformDefineClause(pstate, wc, windef, targetlist);
+ wc->defineClause = transformDefineClause(pstate, windef, targetlist);
/* Store PATTERN AST for deparsing */
wc->rpPattern = windef->rpCommonSyntax->rpPattern;
@@ -313,7 +313,7 @@ validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
* parse_expr.c via the p_rpr_pattern_vars check.
*/
static List *
-transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
+transformDefineClause(ParseState *pstate, WindowDef *windef,
List **targetlist)
{
List *restargets;
@@ -345,6 +345,8 @@ transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
foreach_node(ResTarget, restarget, windef->rpCommonSyntax->rpDefs)
{
TargetEntry *teDefine;
+ Node *expr;
+ List *vars;
name = restarget->name;
@@ -374,54 +376,48 @@ transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
* the individual Var nodes it references are present in the
* targetlist, so the planner can propagate the referenced columns.
*/
- {
- Node *expr;
- List *vars;
+ expr = transformExpr(pstate, restarget->val,
+ EXPR_KIND_RPR_DEFINE);
- 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_node(Var, var, vars)
+ {
+ bool found = false;
- /*
- * 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_node(Var, var, vars)
+ foreach_node(TargetEntry, tle, *targetlist)
{
- bool found = false;
-
- foreach_node(TargetEntry, tle, *targetlist)
+ if (IsA(tle->expr, Var) &&
+ ((Var *) tle->expr)->varno == var->varno &&
+ ((Var *) tle->expr)->varattno == var->varattno)
{
- if (IsA(tle->expr, Var) &&
- ((Var *) tle->expr)->varno == var->varno &&
- ((Var *) tle->expr)->varattno == var->varattno)
- {
- found = true;
- break;
- }
- }
- if (!found)
- {
- TargetEntry *newtle;
-
- newtle = makeTargetEntry((Expr *) copyObject(var),
- list_length(*targetlist) + 1,
- NULL,
- true);
- *targetlist = lappend(*targetlist, newtle);
+ found = true;
+ break;
}
}
- list_free(vars);
+ if (!found)
+ {
+ TargetEntry *newtle;
- /* Build the defineClause entry directly from the transformed expr */
- teDefine = makeTargetEntry((Expr *) expr,
- list_length(defineClause) + 1,
- pstrdup(name),
- true);
+ 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);
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index e309dfbdb66..947e668020e 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -644,7 +644,7 @@ typedef struct RPRPatternNode
} RPRPatternNode;
/*
- * RowPatternCommonSyntax - raw representation of row pattern common syntax
+ * RPCommonSyntax - raw representation of row pattern common syntax
*/
typedef struct RPCommonSyntax
{
--
2.50.1 (Apple Git-155)
[text/plain] nocfbot-0008-refactor-define.txt (15.5K, 10-nocfbot-0008-refactor-define.txt)
download | inline diff:
From 7a69cb818e4d1c37998266a8c1883d5454a8d9f5 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Wed, 17 Jun 2026 14:55:51 +0900
Subject: [PATCH 08/13] Refactor transformDefineClause in row pattern
recognition
Two related cleanups to DEFINE clause parse analysis, with no change to
planner or executor output beyond one error-cursor position:
- Hoist the "DEFINE variable not used in PATTERN" cross-check out of the
recursive validateRPRPatternVarCount() into its caller. The check only
needs to run once, so the rpDefs argument and its NULL-sentinel gating
are gone, and the recursive routine now only counts pattern variables.
- Reorder per-variable DEFINE processing to transformExpr ->
coerce_to_boolean -> pull_var_clause and drop the separate second
coercion pass, so pull_var_clause always operates on the final coerced
expression and a type mismatch is reported before the targetlist is
touched. The duplicate-variable check moves to its own leading loop
and now reports at the later (duplicate) definition.
Add regression coverage for DEFINE coercion and Var propagation: a
boolean-domain predicate (the one case where coerce_to_boolean is not a
no-op), a Var referenced only inside a navigation operation, and
rejection of a non-boolean DEFINE expression.
---
src/backend/parser/parse_rpr.c | 163 +++++++++++--------------
src/test/regress/expected/rpr_base.out | 78 +++++++++++-
src/test/regress/sql/rpr_base.sql | 59 +++++++++
3 files changed, 209 insertions(+), 91 deletions(-)
diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index 3e6b2e579a3..116cd206e39 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -53,7 +53,7 @@ typedef struct
/* Forward declarations */
static void validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
- List *rpDefs, List **varNames);
+ List **varNames);
static List *transformDefineClause(ParseState *pstate, WindowDef *windef,
List **targetlist);
static bool define_walker(Node *node, void *context);
@@ -192,15 +192,14 @@ transformRPR(ParseState *pstate, WindowClause *wc, WindowDef *windef,
* Throws an error if the number of unique variables would require a varId
* greater than RPR_VARID_MAX.
*
- * If rpDefs is non-NULL, each DEFINE variable name is also validated against
- * varNames; any DEFINE name not present in PATTERN is rejected with an error.
- * varNames itself is not extended by this step -- it carries only PATTERN
- * variable names, which is what transformColumnRef checks via
- * p_rpr_pattern_vars to identify pattern variable qualifiers.
+ * varNames collects the unique PATTERN variable names, which is what
+ * transformColumnRef checks via p_rpr_pattern_vars to identify pattern
+ * variable qualifiers. Cross-checking DEFINE variable names against this
+ * list is the caller's responsibility, since it only needs to run once.
*/
static void
validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
- List *rpDefs, List **varNames)
+ List **varNames)
{
/* Pattern node must exist - parser always provides non-NULL root */
Assert(node != NULL);
@@ -255,39 +254,10 @@ validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
/* Recurse into children */
foreach_node(RPRPatternNode, child, node->children)
{
- validateRPRPatternVarCount(pstate, child, NULL, varNames);
+ validateRPRPatternVarCount(pstate, child, varNames);
}
break;
}
-
- /*
- * After the top-level call, validate that every DEFINE variable name is
- * present in the PATTERN variable list; reject names not used in PATTERN.
- * This is only done once at the outermost recursion level, detected by
- * rpDefs being non-NULL (recursive calls pass NULL).
- */
- if (rpDefs)
- {
- foreach_node(ResTarget, rt, rpDefs)
- {
- bool found = false;
-
- foreach_node(String, varname, *varNames)
- {
- if (strcmp(strVal(varname), rt->name) == 0)
- {
- found = true;
- break;
- }
- }
- if (!found)
- ereport(ERROR,
- errcode(ERRCODE_SYNTAX_ERROR),
- errmsg("DEFINE variable \"%s\" is not used in PATTERN",
- rt->name),
- parser_errposition(pstate, rt->location));
- }
- }
}
/*
@@ -296,14 +266,16 @@ validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
*
* First:
* 1. Validates PATTERN variable count and collects RPR variable names
+ * 2. Rejects DEFINE variables not used in PATTERN
+ * 3. Checks for duplicate variable names in DEFINE clause
*
* Then for each DEFINE variable:
- * 2. Checks for duplicate variable names in DEFINE clause
- * 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
+ * 4. Transforms expression via transformExpr() and coerces it to boolean
+ * 5. Creates defineClause entry with proper resname (pattern variable name)
+ * 6. Ensures referenced Var nodes are present in the query targetlist (via
+ * pull_var_clause)
+ *
+ * Finally marks column origins and assigns collation information.
*
* Note: Variables not in DEFINE are evaluated as TRUE by the executor.
* Variables in DEFINE but not in PATTERN are rejected as an error.
@@ -316,9 +288,7 @@ static List *
transformDefineClause(ParseState *pstate, WindowDef *windef,
List **targetlist)
{
- List *restargets;
List *defineClause = NIL;
- char *name;
List *patternVarNames = NIL;
/*
@@ -328,56 +298,86 @@ transformDefineClause(ParseState *pstate, WindowDef *windef,
Assert(windef->rpCommonSyntax->rpDefs != NULL);
/*
- * Validate PATTERN variable count, reject DEFINE variables not used in
- * PATTERN, and collect PATTERN variable names for transformColumnRef.
+ * Validate PATTERN variable count and collect the PATTERN variable names
+ * for transformColumnRef.
*/
validateRPRPatternVarCount(pstate, windef->rpCommonSyntax->rpPattern,
- windef->rpCommonSyntax->rpDefs,
&patternVarNames);
pstate->p_rpr_pattern_vars = patternVarNames;
+ /*
+ * Reject any DEFINE variable whose name does not appear in PATTERN. This
+ * cross-check only needs to run once, so it lives here in the caller
+ * rather than in the recursive validateRPRPatternVarCount().
+ */
+ foreach_node(ResTarget, rt, windef->rpCommonSyntax->rpDefs)
+ {
+ bool found = false;
+
+ foreach_node(String, varname, patternVarNames)
+ {
+ if (strcmp(strVal(varname), rt->name) == 0)
+ {
+ found = true;
+ break;
+ }
+ }
+ if (!found)
+ ereport(ERROR,
+ errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("DEFINE variable \"%s\" is not used in PATTERN",
+ rt->name),
+ parser_errposition(pstate, rt->location));
+ }
+
/*
* Check for duplicate row pattern definition variables. The standard
* requires that no two row pattern definition variable names shall be
- * equivalent.
+ * equivalent. Report the error at the later (duplicate) definition.
*/
- restargets = NIL;
foreach_node(ResTarget, restarget, windef->rpCommonSyntax->rpDefs)
{
- TargetEntry *teDefine;
- Node *expr;
- List *vars;
-
- name = restarget->name;
-
- foreach_node(ResTarget, r, restargets)
+ foreach_node(ResTarget, prior, windef->rpCommonSyntax->rpDefs)
{
- char *n;
-
- n = r->name;
-
- if (!strcmp(n, name))
+ if (prior == restarget)
+ break;
+ if (strcmp(prior->name, restarget->name) == 0)
ereport(ERROR,
errcode(ERRCODE_SYNTAX_ERROR),
errmsg("DEFINE variable \"%s\" appears more than once",
- name),
- parser_errposition(pstate, exprLocation((Node *) r)));
+ restarget->name),
+ parser_errposition(pstate,
+ exprLocation((Node *) restarget)));
}
+ }
- restargets = lappend(restargets, restarget);
+ foreach_node(ResTarget, restarget, windef->rpCommonSyntax->rpDefs)
+ {
+ TargetEntry *teDefine;
+ Node *expr;
+ List *vars;
/*
- * Transform the DEFINE expression. We must NOT add the whole
- * expression to the query targetlist, because it may contain
- * 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
- * targetlist, so the planner can propagate the referenced columns.
+ * Transform the DEFINE expression and coerce it to boolean. We must
+ * NOT add the whole expression to the query targetlist, because it
+ * may contain RPRNavExpr nodes (PREV/NEXT/FIRST/LAST) that can only
+ * be evaluated inside the owning WindowAgg. Coercing here, before
+ * pull_var_clause, keeps pull_var_clause operating on the final
+ * expression form and surfaces a type mismatch before the targetlist
+ * is touched.
*/
expr = transformExpr(pstate, restarget->val,
EXPR_KIND_RPR_DEFINE);
+ expr = coerce_to_boolean(pstate, expr, "DEFINE");
+
+ /* Build the defineClause entry directly from the transformed expr */
+ teDefine = makeTargetEntry((Expr *) expr,
+ list_length(defineClause) + 1,
+ pstrdup(restarget->name),
+ true);
+
+ /* build transformed DEFINE clause (list of TargetEntry) */
+ defineClause = lappend(defineClause, teDefine);
/*
* Pull out Var nodes from the transformed expression and ensure each
@@ -412,26 +412,9 @@ transformDefineClause(ParseState *pstate, WindowDef *windef,
}
}
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);
}
- list_free(restargets);
pstate->p_rpr_pattern_vars = NIL;
- /*
- * Make sure that the row pattern definition search condition is a boolean
- * expression.
- */
- foreach_ptr(TargetEntry, te, defineClause)
- te->expr = (Expr *) coerce_to_boolean(pstate, (Node *) te->expr, "DEFINE");
-
/*
* Validate DEFINE expressions: nested PREV/NEXT, column references,
* compound flatten, volatile callees -- all in a single walk per
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index cf158e1c043..80fabde514c 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -236,7 +236,7 @@ WINDOW w AS (
);
ERROR: DEFINE variable "a" appears more than once
LINE 7: DEFINE A AS id > 0, A AS id < 10
- ^
+ ^
DROP TABLE rpr_dup;
-- Boolean coercion
CREATE TABLE rpr_bool (id INT, flag BOOLEAN);
@@ -319,6 +319,82 @@ DROP CAST (truthyint AS boolean);
DROP FUNCTION truthyint_to_bool(truthyint);
DROP TYPE truthyint;
DROP TABLE rpr_bool;
+-- Coercion over a boolean domain is not a no-op; the wrapped Var must still
+-- propagate when referenced only in DEFINE (flag is not in the select list)
+CREATE DOMAIN boolish AS boolean;
+CREATE TABLE rpr_domain (id int, flag boolish);
+INSERT INTO rpr_domain VALUES (1, true), (2, false), (3, true);
+SELECT id, COUNT(*) OVER w AS cnt
+FROM rpr_domain
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS flag
+)
+ORDER BY id;
+ id | cnt
+----+-----
+ 1 | 1
+ 2 | 0
+ 3 | 1
+(3 rows)
+
+DROP TABLE rpr_domain;
+DROP DOMAIN boolish;
+-- A Var referenced only inside a navigation operation must still propagate
+-- (val appears only inside PREV(), not as a bare operand or in the select list)
+CREATE TABLE rpr_nav (id int, val int);
+INSERT INTO rpr_nav VALUES (1, 0), (2, 1), (3, 0), (4, 2);
+SELECT id, COUNT(*) OVER w AS cnt
+FROM rpr_nav
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (UP+)
+ DEFINE UP AS id > PREV(val)
+)
+ORDER BY id;
+ id | cnt
+----+-----
+ 1 | 0
+ 2 | 3
+ 3 | 0
+ 4 | 0
+(4 rows)
+
+DROP TABLE rpr_nav;
+-- A non-boolean DEFINE expression is rejected
+CREATE TABLE rpr_noncoerce (id int, n int);
+INSERT INTO rpr_noncoerce VALUES (1, 1);
+SELECT id, COUNT(*) OVER w AS cnt
+FROM rpr_noncoerce
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS n
+);
+ERROR: argument of DEFINE must be type boolean, not type integer
+LINE 7: DEFINE A AS n
+ ^
+DROP TABLE rpr_noncoerce;
+-- A non-boolean later DEFINE is rejected at its own definition even when an
+-- earlier DEFINE variable is valid
+CREATE TABLE rpr_noncoerce2 (id int, n int);
+INSERT INTO rpr_noncoerce2 VALUES (1, 1);
+SELECT id, COUNT(*) OVER w AS cnt
+FROM rpr_noncoerce2
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE A AS id > 0, B AS n
+);
+ERROR: argument of DEFINE must be type boolean, not type integer
+LINE 7: DEFINE A AS id > 0, B AS n
+ ^
+DROP TABLE rpr_noncoerce2;
-- Complex expressions
CREATE TABLE rpr_complex (id INT, val1 INT, val2 INT);
INSERT INTO rpr_complex VALUES (1, 10, 20), (2, 15, 25), (3, 20, 30);
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index e71f0dd3680..21840aa77be 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -261,6 +261,65 @@ DROP TYPE truthyint;
DROP TABLE rpr_bool;
+-- Coercion over a boolean domain is not a no-op; the wrapped Var must still
+-- propagate when referenced only in DEFINE (flag is not in the select list)
+CREATE DOMAIN boolish AS boolean;
+CREATE TABLE rpr_domain (id int, flag boolish);
+INSERT INTO rpr_domain VALUES (1, true), (2, false), (3, true);
+SELECT id, COUNT(*) OVER w AS cnt
+FROM rpr_domain
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS flag
+)
+ORDER BY id;
+DROP TABLE rpr_domain;
+DROP DOMAIN boolish;
+
+-- A Var referenced only inside a navigation operation must still propagate
+-- (val appears only inside PREV(), not as a bare operand or in the select list)
+CREATE TABLE rpr_nav (id int, val int);
+INSERT INTO rpr_nav VALUES (1, 0), (2, 1), (3, 0), (4, 2);
+SELECT id, COUNT(*) OVER w AS cnt
+FROM rpr_nav
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (UP+)
+ DEFINE UP AS id > PREV(val)
+)
+ORDER BY id;
+DROP TABLE rpr_nav;
+
+-- A non-boolean DEFINE expression is rejected
+CREATE TABLE rpr_noncoerce (id int, n int);
+INSERT INTO rpr_noncoerce VALUES (1, 1);
+SELECT id, COUNT(*) OVER w AS cnt
+FROM rpr_noncoerce
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A+)
+ DEFINE A AS n
+);
+DROP TABLE rpr_noncoerce;
+
+-- A non-boolean later DEFINE is rejected at its own definition even when an
+-- earlier DEFINE variable is valid
+CREATE TABLE rpr_noncoerce2 (id int, n int);
+INSERT INTO rpr_noncoerce2 VALUES (1, 1);
+SELECT id, COUNT(*) OVER w AS cnt
+FROM rpr_noncoerce2
+WINDOW w AS (
+ ORDER BY id
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ PATTERN (A B+)
+ DEFINE A AS id > 0, B AS n
+);
+DROP TABLE rpr_noncoerce2;
+
-- Complex expressions
CREATE TABLE rpr_complex (id INT, val1 INT, val2 INT);
INSERT INTO rpr_complex VALUES (1, 10, 20), (2, 15, 25), (3, 20, 30);
--
2.50.1 (Apple Git-155)
[text/plain] nocfbot-0009-define-walker-else.txt (1.8K, 11-nocfbot-0009-define-walker-else.txt)
download | inline diff:
From f3dda49e5a24347ff23da72f5fe80640291bc34c Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Wed, 17 Jun 2026 20:43:01 +0900
Subject: [PATCH 09/13] Replace a bare block with an else in the RPR DEFINE
clause walker
define_walker() ended its phase handling with a bare braced block for
the DEFINE-body case, following two if blocks that respectively return
and raise an error. Turn it into the else branch of an if / else if /
else chain. No behavior change.
---
src/backend/parser/parse_rpr.c | 15 +++++++--------
1 file changed, 7 insertions(+), 8 deletions(-)
diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index 116cd206e39..7c201d55164 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -515,8 +515,7 @@ define_walker(Node *node, void *context)
ctx->nav_count++;
return expression_tree_walker(node, define_walker, ctx);
}
-
- if (ctx->phase == DEFINE_PHASE_NAV_OFFSET)
+ else if (ctx->phase == DEFINE_PHASE_NAV_OFFSET)
{
/*
* A navigation offset must be a run-time constant, so it cannot
@@ -528,13 +527,13 @@ define_walker(Node *node, void *context)
errdetail("A navigation offset must be a run-time constant."),
parser_errposition(ctx->pstate, nav->location));
}
-
- /*
- * PHASE_BODY: this is an outer nav at top level. Walk arg first to
- * collect nesting / column-ref state, then validate and (for compound
- * forms) flatten, then walk offset(s).
- */
+ else
{
+ /*
+ * PHASE_BODY: this is an outer nav at top level. Walk arg first
+ * to collect nesting / column-ref state, then validate and (for
+ * compound forms) flatten, then walk offset(s).
+ */
DefineWalkCtx saved = *ctx;
bool outer_phys = (nav->kind == RPR_NAV_PREV ||
nav->kind == RPR_NAV_NEXT);
--
2.50.1 (Apple Git-155)
[text/plain] nocfbot-0010-rename-deparse-vars.txt (6.8K, 12-nocfbot-0010-rename-deparse-vars.txt)
download | inline diff:
From 0e23c67f20f26c7f0e68f830ca5d8f8906a5e601 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Fri, 19 Jun 2026 11:30:40 +0900
Subject: [PATCH 10/13] Rename loop index variables in row pattern deparse
helpers
Give the index parameters and loop variables in the RPR pattern deparse
helpers more descriptive names: the construct-index parameter becomes
idx, the branch-number parameter becomes bno, and each helper's sole
loop variable becomes i. Adjust the accompanying comments to match.
This is a cosmetic change with no effect on the deparsed output.
---
src/backend/commands/explain.c | 68 +++++++++++++++++-----------------
1 file changed, 34 insertions(+), 34 deletions(-)
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 696bdb9c8b5..423cf352125 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -124,11 +124,11 @@ static void append_rpr_quantifier(StringInfo buf, RPRPatternElement *elem);
static char *deparse_rpr_pattern(RPRPattern *pattern);
static int deparse_rpr_seq(RPRPattern *pattern, int start, int limit,
StringInfo buf);
-static int deparse_rpr_node(RPRPattern *pattern, int i, int limit,
+static int deparse_rpr_node(RPRPattern *pattern, int idx, int limit,
StringInfo buf);
static int rpr_match_end(RPRPattern *pattern, int beginIdx);
-static int rpr_alt_scope_end(RPRPattern *pattern, int i);
-static int rpr_next_branch(RPRPattern *pattern, int b, int altEnd);
+static int rpr_alt_scope_end(RPRPattern *pattern, int idx);
+static int rpr_next_branch(RPRPattern *pattern, int bno, int altEnd);
static void show_storage_info(char *maxStorageType, int64 maxSpaceUsed,
ExplainState *es);
static void show_tablesample(TableSampleClause *tsc, PlanState *planstate,
@@ -3014,8 +3014,8 @@ deparse_rpr_seq(RPRPattern *pattern, int start, int limit, StringInfo buf)
}
/*
- * Deparse the single construct starting at index i, bounded by the inherited
- * limit. Returns the index just past the construct.
+ * Deparse the single construct starting at index idx, bounded by the
+ * inherited limit. Returns the index just past the construct.
*
* A VAR is its name plus quantifier. A BEGIN opens a group spanning to its
* matching END (rpr_match_end); when the group's sole child is an ALT that
@@ -3026,9 +3026,9 @@ deparse_rpr_seq(RPRPattern *pattern, int start, int limit, StringInfo buf)
* handed down by rpr_next_branch.
*/
static int
-deparse_rpr_node(RPRPattern *pattern, int i, int limit, StringInfo buf)
+deparse_rpr_node(RPRPattern *pattern, int idx, int limit, StringInfo buf)
{
- RPRPatternElement *elem = &pattern->elements[i];
+ RPRPatternElement *elem = &pattern->elements[idx];
if (RPRElemIsVar(elem))
{
@@ -3036,27 +3036,27 @@ deparse_rpr_node(RPRPattern *pattern, int i, int limit, StringInfo buf)
appendStringInfoString(buf,
quote_identifier(pattern->varNames[elem->varId]));
append_rpr_quantifier(buf, elem);
- return i + 1;
+ return idx + 1;
}
if (RPRElemIsBegin(elem))
{
- int end = rpr_match_end(pattern, i);
+ int end = rpr_match_end(pattern, idx);
bool loneAlt;
- loneAlt = (i + 1 < end &&
- RPRElemIsAlt(&pattern->elements[i + 1]) &&
- rpr_alt_scope_end(pattern, i + 1) == end);
+ loneAlt = (idx + 1 < end &&
+ RPRElemIsAlt(&pattern->elements[idx + 1]) &&
+ rpr_alt_scope_end(pattern, idx + 1) == end);
if (loneAlt)
{
/* The ALT child already parenthesizes the whole group body. */
- (void) deparse_rpr_node(pattern, i + 1, end, buf);
+ (void) deparse_rpr_node(pattern, idx + 1, end, buf);
}
else
{
appendStringInfoChar(buf, '(');
- (void) deparse_rpr_seq(pattern, i + 1, end, buf);
+ (void) deparse_rpr_seq(pattern, idx + 1, end, buf);
appendStringInfoChar(buf, ')');
}
append_rpr_quantifier(buf, &pattern->elements[end]);
@@ -3065,7 +3065,7 @@ deparse_rpr_node(RPRPattern *pattern, int i, int limit, StringInfo buf)
Assert(RPRElemIsAlt(elem));
{
- int altEnd = rpr_alt_scope_end(pattern, i);
+ int altEnd = rpr_alt_scope_end(pattern, idx);
int b;
bool first = true;
@@ -3073,7 +3073,7 @@ deparse_rpr_node(RPRPattern *pattern, int i, int limit, StringInfo buf)
altEnd = limit;
appendStringInfoChar(buf, '(');
- b = i + 1;
+ b = idx + 1;
while (b < altEnd)
{
int nb = rpr_next_branch(pattern, b, altEnd);
@@ -3097,41 +3097,41 @@ static int
rpr_match_end(RPRPattern *pattern, int beginIdx)
{
RPRDepth d = pattern->elements[beginIdx].depth;
- int j;
+ int i;
- for (j = beginIdx + 1; j < pattern->numElements; j++)
+ for (i = beginIdx + 1; i < pattern->numElements; i++)
{
- RPRPatternElement *e = &pattern->elements[j];
+ RPRPatternElement *e = &pattern->elements[i];
if (RPRElemIsEnd(e) && e->depth == d)
- return j;
+ return i;
}
pg_unreachable(); /* a BEGIN always has a matching END */
}
/*
- * Scope end of the construct at index i: the first following element whose
- * depth is no greater than i's own. For an ALT marker this is the index just
- * past its last branch, since depth stays constant across branch boundaries.
- * FIN sits at depth 0, so a top-level ALT stops there.
+ * Scope end of the construct at index idx: the first following element whose
+ * depth is no greater than idx's own. For an ALT marker this is the index
+ * just past its last branch, since depth stays constant across branch
+ * boundaries. FIN sits at depth 0, so a top-level ALT stops there.
*/
static int
-rpr_alt_scope_end(RPRPattern *pattern, int i)
+rpr_alt_scope_end(RPRPattern *pattern, int idx)
{
- RPRDepth d = pattern->elements[i].depth;
- int k;
+ RPRDepth d = pattern->elements[idx].depth;
+ int i;
- for (k = i + 1; k < pattern->numElements; k++)
+ for (i = idx + 1; i < pattern->numElements; i++)
{
- if (pattern->elements[k].depth <= d)
- return k;
+ if (pattern->elements[i].depth <= d)
+ return i;
}
return pattern->numElements;
}
/*
- * Boundary of the alternation branch starting at b (i.e. the start of the next
- * branch, or altEnd if b is the last branch).
+ * Boundary of the alternation branch starting at bno (i.e. the start of the
+ * next branch, or altEnd if bno is the last branch).
*
* The branch-start element's jump points at the next branch when this is not
* the last branch. jump is overloaded (a group BEGIN also uses it for its
@@ -3140,9 +3140,9 @@ rpr_alt_scope_end(RPRPattern *pattern, int i)
* next redirected past the alternation, so it does not point at j.
*/
static int
-rpr_next_branch(RPRPattern *pattern, int b, int altEnd)
+rpr_next_branch(RPRPattern *pattern, int bno, int altEnd)
{
- int j = pattern->elements[b].jump;
+ int j = pattern->elements[bno].jump;
if (j != RPR_ELEMIDX_INVALID && j < altEnd &&
pattern->elements[j - 1].next != j)
--
2.50.1 (Apple Git-155)
[text/plain] nocfbot-0011-comparison-point-wording.txt (13.1K, 13-nocfbot-0011-comparison-point-wording.txt)
download | inline diff:
From 0bc60ef4dc9cba045f6ee82d8cfcb48e2f5cf712 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Fri, 19 Jun 2026 13:04:00 +0900
Subject: [PATCH 11/13] Rename absorption "judgment point" to "comparison
point" in comments
The absorption analysis comments described the ABSORBABLE element flag
as a "judgment point". Use "comparison point" instead, which says more
directly what happens there: where consecutive iterations are compared.
This touches comments and the executor README only, across the planner,
executor, and EXPLAIN deparse, plus the rpr_base test comments; the
"equivalence judgment" wording and all identifiers are unchanged.
No change to behavior or to query output.
---
src/backend/commands/explain.c | 2 +-
src/backend/executor/README.rpr | 4 ++--
src/backend/executor/execRPR.c | 29 +++++++++++++-------------
src/backend/optimizer/plan/rpr.c | 14 ++++++-------
src/include/optimizer/rpr.h | 2 +-
src/test/regress/expected/rpr_base.out | 6 +++---
src/test/regress/sql/rpr_base.sql | 6 +++---
7 files changed, 32 insertions(+), 31 deletions(-)
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 423cf352125..b3fc324718d 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -2937,7 +2937,7 @@ append_rpr_quantifier(StringInfo buf, RPRPatternElement *elem)
appendStringInfoChar(buf, '?');
}
- /* Append absorption markers: " for judgment point, ' for branch only */
+ /* Append absorption markers: " for comparison point, ' for branch only */
if (RPRElemIsAbsorbable(elem))
{
Assert(elem->max == RPR_QUANTITY_INF);
diff --git a/src/backend/executor/README.rpr b/src/backend/executor/README.rpr
index 713ad84e1d9..50d1ff87f7e 100644
--- a/src/backend/executor/README.rpr
+++ b/src/backend/executor/README.rpr
@@ -285,7 +285,7 @@ Element flags (1 byte, bitmask):
absorption.
0x08 RPR_ELEM_ABSORBABLE (VAR, END)
- Absorption judgment point. Where to compare consecutive
+ Absorption comparison point. Where to compare consecutive
iterations for absorption.
- Simple unbounded VAR (A+): set on the VAR itself
- Unbounded GROUP ((A B)+): set on the END element only
@@ -1393,7 +1393,7 @@ XII-5. Execution Optimization Summary
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).
+ through END chains to reach the comparison 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/executor/execRPR.c b/src/backend/executor/execRPR.c
index 099b81aeb81..90e3a068f04 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -578,7 +578,7 @@ nfa_update_absorption_flags(RPRNFAContext *ctx)
/*
* Iterate through all states to check absorption status. Uses
* state->isAbsorbable which tracks if state is in absorbable region. This
- * is different from RPRElemIsAbsorbable(elem) which checks judgment
+ * is different from RPRElemIsAbsorbable(elem) which checks comparison
* point.
*/
for (state = ctx->states; state != NULL; state = state->next)
@@ -625,8 +625,8 @@ nfa_states_covered(RPRPattern *pattern, RPRNFAContext *older, RPRNFAContext *new
depth = elem->depth;
/*
- * Only compare at absorption judgment points (RPR_ELEM_ABSORBABLE).
- * Judgment points are where count-dominance guarantees the newer
+ * Only compare at absorption comparison points (RPR_ELEM_ABSORBABLE).
+ * Comparison points are where count-dominance guarantees the newer
* context's future matches are a subset of the older's.
*/
if (!RPRElemIsAbsorbable(elem))
@@ -782,7 +782,8 @@ nfa_eval_var_match(WindowAggState *winstate, RPRPatternElement *elem,
* previous advance when count >= min was satisfied)
*
* For VARs that reached max count followed by END:
- * - Advance through the END-element chain to the absorption judgment point
+ * - Advance through the END-element chain to the absorption
+ * comparison point
* - Only deterministic exits (count >= max, max != INF) are handled
* - Chains through END elements while count >= max (must-exit path)
*
@@ -800,7 +801,7 @@ nfa_match(WindowAggState *winstate, RPRNFAContext *ctx, bool *varMatched)
/*
* Evaluate VAR elements against current row. For VARs that reach max
* count with END next, advance through the chain of END elements inline
- * so absorb phase can compare states at judgment points.
+ * so absorb phase can compare states at comparison points.
*/
for (state = ctx->states; state != NULL; state = nextState)
{
@@ -831,7 +832,7 @@ nfa_match(WindowAggState *winstate, RPRNFAContext *ctx, bool *varMatched)
/*
* For VAR at max count with END next, advance through END
- * chain to reach the absorption judgment point. Only
+ * chain to reach the absorption comparison point. Only
* deterministic exits (count >= max, max finite) are handled;
* unbounded VARs stay for advance phase.
*
@@ -841,10 +842,10 @@ nfa_match(WindowAggState *winstate, RPRNFAContext *ctx, bool *varMatched)
* to the next outer END. The loop below walks this chain.
*
* ABSORBABLE_BRANCH marks elements inside the absorbable
- * region; ABSORBABLE marks the outermost judgment point where
- * count-dominance is evaluated. We chain through BRANCH
- * elements until reaching the ABSORBABLE point or an element
- * that can still loop (count < max).
+ * region; ABSORBABLE marks the outermost comparison point
+ * where count-dominance is evaluated. We chain through
+ * BRANCH elements until reaching the ABSORBABLE point or an
+ * element that can still loop (count < max).
*/
if (RPRElemIsAbsorbableBranch(elem) &&
!RPRElemIsAbsorbable(elem) &&
@@ -876,7 +877,7 @@ nfa_match(WindowAggState *winstate, RPRNFAContext *ctx, bool *varMatched)
/*
* Chain through END elements within the absorbable region
- * (ABSORBABLE_BRANCH) until reaching the judgment point
+ * (ABSORBABLE_BRANCH) until reaching the comparison point
* (ABSORBABLE). Continue only on must-exit path (count
* >= max) with END next.
*/
@@ -892,9 +893,9 @@ nfa_match(WindowAggState *winstate, RPRNFAContext *ctx, bool *varMatched)
/*
* Exit this intermediate group: clear its own count
* (count-clear policy). It sits below the absorbable
- * judgment point, so it is excluded from the
- * dominance comparison; the judgment point where the
- * chain stops keeps its count.
+ * comparison point, so it is excluded from the
+ * dominance comparison; the comparison point where
+ * the chain stops keeps its count.
*/
state->counts[endDepth] = 0;
diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index 597a966c7b1..ebba8e50b1d 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -19,7 +19,7 @@
* complexity from O(n^2) to O(n) for patterns like A+ B.
*
* The absorption analysis uses two element flags:
- * - RPR_ELEM_ABSORBABLE: marks WHERE to compare (judgment point)
+ * - RPR_ELEM_ABSORBABLE: marks WHERE to compare (comparison point)
* - RPR_ELEM_ABSORBABLE_BRANCH: marks the absorbable region
*
* See computeAbsorbability() and the detailed comments before
@@ -1484,7 +1484,7 @@ finalizeRPRPattern(RPRPattern *result)
* than Ctx2's match (1 to current). So Ctx2 can be safely eliminated.
*
* Two Flags:
- * 1. RPR_ELEM_ABSORBABLE - "Absorption judgment point"
+ * 1. RPR_ELEM_ABSORBABLE - "Absorption comparison point"
* WHERE contexts can be compared for absorption.
* - Simple unbounded VAR (A+): the VAR element itself
* - Unbounded GROUP ((A B)+): the END element only
@@ -1506,20 +1506,20 @@ finalizeRPRPattern(RPRPattern *result)
* -> Both at END, comparable! Ctx1 absorbs Ctx2.
*
* Contexts synchronize at END every group-length rows. Therefore:
- * - ABSORBABLE marks END as judgment point (where to compare)
+ * - ABSORBABLE marks END as comparison point (where to compare)
* - ABSORBABLE_BRANCH keeps state.isAbsorbable=true through A->B->END
*
* Pattern Examples:
*
* Pattern: A+ B
- * Element 0 (A): ABSORBABLE | ABSORBABLE_BRANCH <- judgment point
+ * Element 0 (A): ABSORBABLE | ABSORBABLE_BRANCH <- comparison point
* Element 1 (B): (none)
* -> Compare at A every row. When contexts move to B, absorption stops.
*
* Pattern: (A B)+ C
* Element 0 (A): ABSORBABLE_BRANCH
* Element 1 (B): ABSORBABLE_BRANCH
- * Element 2 (END): ABSORBABLE | ABSORBABLE_BRANCH <- judgment point
+ * Element 2 (END): ABSORBABLE | ABSORBABLE_BRANCH <- comparison point
* Element 3 (C): (none)
* -> Compare at END every 2 rows. When contexts move to C, absorption stops.
*
@@ -1629,7 +1629,7 @@ isFixedLengthChildren(RPRPattern *pattern, RPRElemIdx idx, RPRDepth scopeDepth)
* 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).
+ * Only the unbounded END gets ABSORBABLE (comparison point).
* Examples:
* (A B{2})+ C - B{2} has min==max, step=3
* (A (B C){2} D)+ E - nested {2} subgroup, step=6
@@ -1791,7 +1791,7 @@ computeAbsorbabilityRecursive(RPRPattern *pattern, RPRElemIdx startIdx,
* decrease property required for safe absorption.
*
* This function sets two flags:
- * RPR_ELEM_ABSORBABLE: Absorption judgment point
+ * RPR_ELEM_ABSORBABLE: Absorption comparison point
* - Simple unbounded VAR: the VAR itself (e.g., A in A+)
* - Unbounded GROUP: the END element (e.g., END in (A B)+)
* RPR_ELEM_ABSORBABLE_BRANCH: All elements in absorbable region
diff --git a/src/include/optimizer/rpr.h b/src/include/optimizer/rpr.h
index 23763442b65..83d3cd29b07 100644
--- a/src/include/optimizer/rpr.h
+++ b/src/include/optimizer/rpr.h
@@ -53,7 +53,7 @@
* optimizer/plan/rpr.c.
*/
#define RPR_ELEM_ABSORBABLE_BRANCH 0x04 /* element in absorbable region */
-#define RPR_ELEM_ABSORBABLE 0x08 /* absorption judgment point */
+#define RPR_ELEM_ABSORBABLE 0x08 /* absorption comparison point */
/* Accessor macros for RPRPatternElement */
#define RPRElemIsReluctant(e) (((e)->flags & RPR_ELEM_RELUCTANT) != 0)
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index 80fabde514c..b385e972e7f 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -5472,9 +5472,9 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-- Absorption Flag Display Tests
-- ============================================================
-- Tests absorption marker display in EXPLAIN output
--- Markers: ' = branch element, " = judgment point
+-- Markers: ' = branch element, " = comparison point
-- Files: explain.c (append_rpr_quantifier, deparse_rpr_pattern)
--- Simple VAR: A+ -> a+" (judgment point)
+-- Simple VAR: A+ -> a+" (comparison point)
EXPLAIN (COSTS OFF)
SELECT COUNT(*) OVER w FROM rpr_plan
WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
@@ -5490,7 +5490,7 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-> Seq Scan on rpr_plan
(7 rows)
--- GROUP unbounded: (A B)+ -> (a' b')+" (branch + judgment)
+-- GROUP unbounded: (A B)+ -> (a' b')+" (branch + comparison)
EXPLAIN (COSTS OFF)
SELECT COUNT(*) OVER w FROM rpr_plan
WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index 21840aa77be..af498fffb66 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -3254,16 +3254,16 @@ WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
-- Absorption Flag Display Tests
-- ============================================================
-- Tests absorption marker display in EXPLAIN output
--- Markers: ' = branch element, " = judgment point
+-- Markers: ' = branch element, " = comparison point
-- Files: explain.c (append_rpr_quantifier, deparse_rpr_pattern)
--- Simple VAR: A+ -> a+" (judgment point)
+-- Simple VAR: A+ -> a+" (comparison point)
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+) DEFINE A AS val > 0);
--- GROUP unbounded: (A B)+ -> (a' b')+" (branch + judgment)
+-- GROUP unbounded: (A B)+ -> (a' b')+" (branch + comparison)
EXPLAIN (COSTS OFF)
SELECT COUNT(*) OVER w FROM rpr_plan
WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
--
2.50.1 (Apple Git-155)
[text/plain] nocfbot-0013-doc-nav-offset.txt (2.2K, 14-nocfbot-0013-doc-nav-offset.txt)
download | inline diff:
From 3cccad2ecc34be6d0b1e96afdca7468e4e9ab21f Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Fri, 19 Jun 2026 14:17:19 +0900
Subject: [PATCH 13/13] Document eval_nav_offset_helper's NULL/negative offset
handling
eval_nav_offset_helper pre-evaluates a navigation offset at executor init
to size the frame trim, returning 0 for a NULL or negative offset rather
than rejecting it. The comment did not say why, leaving the purpose of the
function and of those branches unclear.
Explain that a NULL or negative offset is caught per row on the navigation
path that consumes it, which errors out before navigation produces any
result, so the trim value computed here is never used. The branches are
reachable -- a navigation offset can be a run-time constant such as a Param
-- and are already covered by the PREV(price, $1) tests in rpr.sql, so they
need neither a new test nor an assertion.
---
src/backend/executor/nodeWindowAgg.c | 13 +++++++++----
1 file changed, 9 insertions(+), 4 deletions(-)
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 819ad814bf5..eb1d616b49a 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -3985,10 +3985,15 @@ put_notnull_info(WindowObject winobj, int64 pos, int argno, bool isnull)
/*
* eval_nav_offset_helper
- * Evaluate an offset expression at executor init time for trim
- * optimization. Returns the offset value, or 0 for NULL/negative
- * (these will cause a runtime error during actual navigation, so the
- * trim value is irrelevant).
+ * Pre-evaluate a navigation offset expression at executor init time, to
+ * bound how far navigation can reach (which sizes the frame trim).
+ * Returns the offset value, or 0 for a NULL or negative offset.
+ *
+ * The offset is not validated here. A NULL or negative value is caught later,
+ * per row, on the navigation path that consumes it (see EEOP_RPR_NAV_SET in
+ * execExprInterp.c), which errors out before navigation produces any result;
+ * the trim sizing computed from such an offset is therefore never used, and 0
+ * is returned as a harmless placeholder.
*/
static int64
eval_nav_offset_helper(WindowAggState *winstate, Expr *offset_expr,
--
2.50.1 (Apple Git-155)
[text/plain] nocfbot-0012-comment-doc-clarity.txt (23.5K, 15-nocfbot-0012-comment-doc-clarity.txt)
download | inline diff:
From 9c7e5bc9dde1adafcff995370340975661890d3c Mon Sep 17 00:00:00 2001
From: Henson Choi <[email protected]>
Date: Fri, 19 Jun 2026 13:43:58 +0900
Subject: [PATCH 12/13] Improve comments, documentation, and naming for row
pattern recognition
A batch of clarity cleanups for row pattern recognition, none of which
change behavior or query output:
- Call the parser-built PATTERN structure a "parse tree" rather than an
"AST", which is not PostgreSQL's usual term (parsenodes.h, parse_rpr.c,
plan/rpr.c, and the executor README).
- Trim the transformDefineClause header comment, whose step list merely
duplicated the inline comments.
- Drop a duplicated standard-citation sentence from the contain_rpr_walker
header; transformWithClause already carries it.
- Fix a stale "ALT_START" reference to name the ALT marker, and normalize
an "or -1" ParseLoc comment to the usual "or -1 if unknown".
- Move the EMPTY-alternative explanation in row_pattern_quantifier_opt
into the action block, keeping the /*EMPTY*/ marker.
- Reword the Run Condition pushdown test comment to explain in SQL terms
why count(*) is DECREASING over the required frame.
- Rename RPR_COUNT_MAX to RPR_COUNT_INF, defined from RPR_QUANTITY_INF.
The old "MAX" name suggested a configurable repetition limit -- that is
elem->max, a different thing -- which led to questions about erroring
when a count reaches it. The value is the int32 saturation ceiling, and
a saturated count reads as "unbounded".
---
src/backend/executor/README.rpr | 44 +++++++++++------------
src/backend/executor/execRPR.c | 23 ++++++------
src/backend/optimizer/plan/rpr.c | 23 ++++++------
src/backend/parser/gram.y | 7 ++--
src/backend/parser/parse_cte.c | 4 +--
src/backend/parser/parse_rpr.c | 19 ++--------
src/include/nodes/parsenodes.h | 12 +++----
src/include/optimizer/rpr.h | 9 ++++-
src/test/regress/expected/rpr_explain.out | 12 ++++---
src/test/regress/sql/rpr_explain.sql | 12 ++++---
10 files changed, 84 insertions(+), 81 deletions(-)
diff --git a/src/backend/executor/README.rpr b/src/backend/executor/README.rpr
index 50d1ff87f7e..9275e265d4b 100644
--- a/src/backend/executor/README.rpr
+++ b/src/backend/executor/README.rpr
@@ -96,16 +96,16 @@ Chapter II Overall Processing Pipeline
RPR processing is divided into three phases:
- +------------------------------------------------------------+
- | 1. Parsing (Parser) |
- | SQL text -> PATTERN AST + DEFINE expression tree |
- | |
- | 2. Compilation (Optimizer/Planner) |
- | PATTERN AST -> optimization -> flat NFA element array |
- | |
- | 3. Execution (Executor) |
- | Row-by-row matching via NFA simulation |
- +------------------------------------------------------------+
+ +--------------------------------------------------------------+
+ | 1. Parsing (Parser) |
+ | SQL text -> PATTERN parse tree + DEFINE expression tree |
+ | |
+ | 2. Compilation (Optimizer/Planner) |
+ | PATTERN parse tree -> optimization -> flat NFA elements |
+ | |
+ | 3. Execution (Executor) |
+ | Row-by-row matching via NFA simulation |
+ +--------------------------------------------------------------+
Each phase uses independent data structures, and the interfaces between
phases are well-defined:
@@ -137,7 +137,7 @@ following:
(3) DEFINE clause transformation (transformDefineClause)
-III-2. PATTERN AST (Abstract Syntax Tree)
+III-2. PATTERN parse tree
The parser transforms the PATTERN clause into an RPRPatternNode tree.
Each node has one of the following four types:
@@ -192,16 +192,16 @@ IV-1. Entry Point
IV-2. The 6 Phases of buildRPRPattern()
- Phase 1: AST optimization (optimizeRPRPattern)
+ Phase 1: parse tree optimization (optimizeRPRPattern)
Phase 2: Statistics collection (scanRPRPattern)
Phase 3: Memory allocation (makeRPRPattern)
Phase 4: NFA element fill (fillRPRPattern)
Phase 5: Finalization (finalizeRPRPattern)
Phase 6: Absorbability analysis (computeAbsorbability)
-IV-3. Phase 1: AST Optimization
+IV-3. Phase 1: Parse Tree Optimization
-After copying the parser-generated AST, the following optimizations are
+After copying the parser-generated parse tree, the following optimizations are
applied:
(a) SEQ flattening: Unwrap nested SEQ nodes
@@ -236,7 +236,7 @@ applied:
IV-4. Phase 4: NFA Element Array Generation
-Transforms the optimized AST into a flat array of RPRPatternElement.
+Transforms the optimized parse tree into a flat array of RPRPatternElement.
This is the core data structure used for NFA simulation at runtime.
RPRPatternElement struct (16 bytes):
@@ -298,7 +298,7 @@ Element flags (1 byte, bitmask):
Example: PATTERN (A+ B | C)
- AST: ALT(SEQ(VAR(A,1,INF), VAR(B,1,1)), VAR(C,1,1))
+ Parse tree: ALT(SEQ(VAR(A,1,INF), VAR(B,1,1)), VAR(C,1,1))
Compilation result:
@@ -345,9 +345,9 @@ Example: PATTERN ((A B)+)
IV-4a. Reluctant Flag (RPR_ELEM_RELUCTANT)
-The reluctant flag is set during Phase 4 (fillRPRPattern) when the AST node
-has reluctant == true. It reverses the priority of quantifier expansion at
-runtime:
+The reluctant flag is set during Phase 4 (fillRPRPattern) when the parse
+tree node has reluctant == true. It reverses the priority of quantifier
+expansion at runtime:
Greedy (default): try loop-back first, then exit (prefer longer match)
Reluctant: try exit first, then loop-back (prefer shorter match)
@@ -1363,9 +1363,9 @@ XII-5. Execution Optimization Summary
-- Compile-time --
- (1) AST Optimization (IV-3)
+ (1) Parse Tree Optimization (IV-3)
- Simplifies the AST before converting the pattern to an NFA.
+ Simplifies the parse tree before converting the pattern to an NFA.
Reduces the number of NFA elements through consecutive variable
merging (A A -> A{2}), SEQ flattening, quantifier multiplication,
and other transformations.
@@ -1468,7 +1468,7 @@ Appendix A. Key Function Index
transformRPR parse_rpr.c Parser entry point
transformDefineClause parse_rpr.c DEFINE transformation
buildRPRPattern rpr.c NFA compilation main
- optimizeRPRPattern rpr.c AST optimization
+ optimizeRPRPattern rpr.c parse tree optimization
fillRPRPattern rpr.c NFA element generation
finalizeRPRPattern rpr.c Finalization
computeAbsorbability rpr.c Absorption analysis
diff --git a/src/backend/executor/execRPR.c b/src/backend/executor/execRPR.c
index 90e3a068f04..e5e30d79f01 100644
--- a/src/backend/executor/execRPR.c
+++ b/src/backend/executor/execRPR.c
@@ -821,8 +821,11 @@ nfa_match(WindowAggState *winstate, RPRNFAContext *ctx, bool *varMatched)
if (matched)
{
- /* Increment count */
- if (count < RPR_COUNT_MAX)
+ /*
+ * Increment count, saturating at RPR_COUNT_INF to avoid int32
+ * overflow; a saturated count then compares as "unbounded".
+ */
+ if (count < RPR_COUNT_INF)
count++;
/* Max constraint should not be exceeded */
@@ -857,7 +860,7 @@ nfa_match(WindowAggState *winstate, RPRNFAContext *ctx, bool *varMatched)
int32 endCount = state->counts[endDepth];
/* Increment group count */
- if (endCount < RPR_COUNT_MAX)
+ if (endCount < RPR_COUNT_INF)
endCount++;
Assert(endElem->max == RPR_QUANTITY_INF ||
endCount <= endElem->max);
@@ -900,7 +903,7 @@ nfa_match(WindowAggState *winstate, RPRNFAContext *ctx, bool *varMatched)
state->counts[endDepth] = 0;
/* Increment outer group count */
- if (outerCount < RPR_COUNT_MAX)
+ if (outerCount < RPR_COUNT_INF)
outerCount++;
Assert(outerEnd->max == RPR_QUANTITY_INF ||
outerCount <= outerEnd->max);
@@ -1180,7 +1183,7 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
/* END->END: increment outer END's count */
if (RPRElemIsEnd(nextElem) &&
- ffState->counts[nextElem->depth] < RPR_COUNT_MAX)
+ ffState->counts[nextElem->depth] < RPR_COUNT_INF)
ffState->counts[nextElem->depth]++;
}
@@ -1235,7 +1238,7 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
RPRElemIsAbsorbableBranch(nextElem);
/* END->END: increment outer END's count */
- if (RPRElemIsEnd(nextElem) && state->counts[nextElem->depth] < RPR_COUNT_MAX)
+ if (RPRElemIsEnd(nextElem) && state->counts[nextElem->depth] < RPR_COUNT_INF)
state->counts[nextElem->depth]++;
nfa_route_to_elem(winstate, ctx, state, nextElem, currentPos);
@@ -1262,7 +1265,7 @@ nfa_advance_end(WindowAggState *winstate, RPRNFAContext *ctx,
nextElem = &elements[exitState->elemIdx];
/* END->END: increment outer END's count */
- if (RPRElemIsEnd(nextElem) && exitState->counts[nextElem->depth] < RPR_COUNT_MAX)
+ if (RPRElemIsEnd(nextElem) && exitState->counts[nextElem->depth] < RPR_COUNT_INF)
exitState->counts[nextElem->depth]++;
/* Prepare loop state */
@@ -1355,7 +1358,7 @@ nfa_advance_var(WindowAggState *winstate, RPRNFAContext *ctx,
/* When exiting directly to an outer END, increment its count */
if (RPRElemIsEnd(nextElem))
{
- if (cloneState->counts[nextElem->depth] < RPR_COUNT_MAX)
+ if (cloneState->counts[nextElem->depth] < RPR_COUNT_INF)
cloneState->counts[nextElem->depth]++;
}
@@ -1404,7 +1407,7 @@ nfa_advance_var(WindowAggState *winstate, RPRNFAContext *ctx,
*/
if (RPRElemIsEnd(nextElem))
{
- if (state->counts[nextElem->depth] < RPR_COUNT_MAX)
+ if (state->counts[nextElem->depth] < RPR_COUNT_INF)
state->counts[nextElem->depth]++;
}
@@ -1438,7 +1441,7 @@ nfa_advance_var(WindowAggState *winstate, RPRNFAContext *ctx,
/* See comment above: increment outer END count for quantified VARs */
if (RPRElemIsEnd(nextElem))
{
- if (state->counts[nextElem->depth] < RPR_COUNT_MAX)
+ if (state->counts[nextElem->depth] < RPR_COUNT_INF)
state->counts[nextElem->depth]++;
}
diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c
index ebba8e50b1d..62292508aad 100644
--- a/src/backend/optimizer/plan/rpr.c
+++ b/src/backend/optimizer/plan/rpr.c
@@ -3,13 +3,13 @@
* rpr.c
* Row Pattern Recognition pattern compilation for planner
*
- * This file contains functions for optimizing RPR pattern AST and
+ * This file contains functions for optimizing the RPR pattern parse tree and
* compiling it to a flat element array for NFA execution by WindowAgg.
*
* Key components:
* 1. Pattern Optimization: Simplifies patterns before compilation
* (e.g., flatten nested SEQ/ALT, merge consecutive vars)
- * 2. Pattern Compilation: Converts AST to flat element array for NFA
+ * 2. Pattern Compilation: Converts parse tree to flat element array for NFA
* 3. Absorption Analysis: Computes flags for O(n^2)->O(n) optimization
*
* Context Absorption Optimization:
@@ -995,7 +995,7 @@ collectDefineVariables(List *defineVariableList, char **varNames)
/*
* scanRPRPatternRecursive
- * Recursively scan pattern AST (pass 1 internal).
+ * Recursively scan pattern parse tree (pass 1 internal).
*
* Collects unique variable names and counts elements while tracking depth.
* Variables from DEFINE clause are already in varNames; this adds any
@@ -1092,7 +1092,7 @@ scanRPRPatternRecursive(RPRPatternNode *node, char **varNames, int *numVars,
/*
* scanRPRPattern
- * Scan pattern AST (pass 1 entry point).
+ * Scan pattern parse tree (pass 1 entry point).
*
* Collects unique variable names (appending to those from DEFINE clause),
* counts total elements (including FIN marker), and tracks maximum depth.
@@ -1297,7 +1297,7 @@ fillRPRPatternGroup(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth de
* fillRPRPatternAlt
* Fill an ALT pattern and its alternatives.
*
- * Creates ALT_START marker, fills each alternative at increased depth,
+ * Creates the ALT marker, fills each alternative at increased depth,
* sets jump pointers for backtracking, and next pointers for successful paths.
*
* Returns true if any branch is nullable (OR semantics: one nullable
@@ -1383,9 +1383,10 @@ fillRPRPatternAlt(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth dept
/*
* fillRPRPattern
- * Fill pattern elements array from AST (pass 2).
+ * Fill pattern elements array from parse tree (pass 2).
*
- * Recursively traverses AST and populates pre-allocated elements array.
+ * Recursively traverses the parse tree and populates pre-allocated elements
+ * array.
* Dispatches to type-specific fill functions.
*
* Returns true if the pattern is nullable (can match zero rows).
@@ -1767,7 +1768,7 @@ computeAbsorbabilityRecursive(RPRPattern *pattern, RPRElemIdx startIdx,
}
else
{
- /* Should never reach END - structural invariant of pattern AST */
+ /* Should never reach END - structural invariant of pattern parse tree */
Assert(!RPRElemIsEnd(elem));
/* Non-ALT, non-BEGIN: check if unbounded start */
@@ -1894,13 +1895,13 @@ validate_rpr_define_volatility(List *defineClause)
/*
* buildRPRPattern
- * Compile pattern AST to flat bytecode array.
+ * Compile pattern parse tree to flat bytecode array.
*
* Compilation phases:
- * 1. Optimize AST (flatten, merge, deduplicate)
+ * 1. Optimize parse tree (flatten, merge, deduplicate)
* 2. Scan: collect variables, count elements (pass 1)
* 3. Allocate result structure
- * 4. Fill elements from AST (pass 2)
+ * 4. Fill elements from parse tree (pass 2)
* 5. Finalize pattern structure
* 6. Compute context absorption eligibility
*
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 4ae951aaeba..6eb01ea7f0b 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -17734,9 +17734,12 @@ row_pattern_primary:
;
row_pattern_quantifier_opt:
- /* EMPTY - no quantifier means exactly once; @$ is unused since
- * min=max=1 never produces an error */
+ /*EMPTY*/
{
+ /*
+ * no quantifier means exactly once; @$ is unused since
+ * min=max=1 never produces an error
+ */
$$ = (Node *) makeRPRQuantifier(1, 1, false, @$);
}
| '*'
diff --git a/src/backend/parser/parse_cte.c b/src/backend/parser/parse_cte.c
index 3e493beba0b..c35feeae6fd 100644
--- a/src/backend/parser/parse_cte.c
+++ b/src/backend/parser/parse_cte.c
@@ -1303,9 +1303,7 @@ checkWellFormedSelectStmt(SelectStmt *stmt, CteState *cstate)
/*
* contain_rpr_walker
* Returns true if the raw parse tree contains any <row pattern common
- * syntax> -- i.e., any WindowDef with PATTERN/DEFINE attached. Used
- * by transformWithClause() to enforce ISO/IEC 9075-2:2016 7.17 SR 3)f)
- * on WITH RECURSIVE elements.
+ * syntax> -- i.e., any WindowDef with PATTERN/DEFINE attached.
*/
static bool
contain_rpr_walker(Node *node, void *context)
diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index 7c201d55164..ed12190cb06 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -8,7 +8,7 @@
* - Validates frame options (must start at CURRENT ROW, no EXCLUDE)
* - Validates PATTERN variable count (max RPR_VARID_MAX + 1)
* - Transforms DEFINE clause
- * - Stores the PATTERN AST and the SKIP TO/INITIAL flags
+ * - Stores the PATTERN parse tree and the SKIP TO/INITIAL flags
*
* Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
* Portions Copyright (c) 1994, Regents of the University of California
@@ -67,7 +67,7 @@ static bool define_walker(Node *node, void *context);
* - Set AFTER MATCH SKIP TO flag
* - Set SEEK/INITIAL flag
* - Transforms DEFINE clause into TargetEntry list
- * - Stores PATTERN AST for deparsing (optimization happens in planner)
+ * - Stores PATTERN parse tree for deparsing (optimization happens in planner)
*
* Returns early if windef has no rpCommonSyntax (non-RPR window).
*/
@@ -180,7 +180,7 @@ transformRPR(ParseState *pstate, WindowClause *wc, WindowDef *windef,
/* Transform DEFINE clause into list of TargetEntry's */
wc->defineClause = transformDefineClause(pstate, windef, targetlist);
- /* Store PATTERN AST for deparsing */
+ /* Store PATTERN parse tree for deparsing */
wc->rpPattern = windef->rpCommonSyntax->rpPattern;
}
@@ -264,19 +264,6 @@ validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
* transformDefineClause
* Process DEFINE clause and transform ResTarget into list of TargetEntry.
*
- * First:
- * 1. Validates PATTERN variable count and collects RPR variable names
- * 2. Rejects DEFINE variables not used in PATTERN
- * 3. Checks for duplicate variable names in DEFINE clause
- *
- * Then for each DEFINE variable:
- * 4. Transforms expression via transformExpr() and coerces it to boolean
- * 5. Creates defineClause entry with proper resname (pattern variable name)
- * 6. Ensures referenced Var nodes are present in the query targetlist (via
- * pull_var_clause)
- *
- * Finally marks column origins and assigns collation information.
- *
* Note: Variables not in DEFINE are evaluated as TRUE by the executor.
* Variables in DEFINE but not in PATTERN are rejected as an error.
*
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 947e668020e..f28215b8e83 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -621,7 +621,7 @@ typedef enum RPRPatternNodeType
} RPRPatternNodeType;
/*
- * RPRPatternNode - Row Pattern Recognition pattern AST node
+ * RPRPatternNode - Row Pattern Recognition pattern parse tree node
*/
typedef struct RPRPatternNode
{
@@ -630,7 +630,7 @@ typedef struct RPRPatternNode
int32 min; /* minimum repetitions (0 for *, ?) */
int32 max; /* maximum repetitions (PG_INT32_MAX for *, +) */
bool reluctant; /* true for reluctant (non-greedy) */
- ParseLoc location; /* token location, or -1 */
+ ParseLoc location; /* token location, or -1 if unknown */
char *varName; /* VAR: variable name */
List *children; /* SEQ, ALT, GROUP: child nodes */
@@ -652,10 +652,10 @@ typedef struct RPCommonSyntax
RPSkipTo rpSkipTo; /* Row Pattern AFTER MATCH SKIP type */
bool initial; /* true if <row pattern initial or seek> is
* initial */
- RPRPatternNode *rpPattern; /* PATTERN clause AST */
+ RPRPatternNode *rpPattern; /* PATTERN parse tree */
List *rpDefs; /* row pattern definitions clause (list of
* ResTarget) */
- ParseLoc location; /* PATTERN keyword location, or -1 */
+ ParseLoc location; /* PATTERN keyword location, or -1 if unknown */
} RPCommonSyntax;
/*
@@ -1727,7 +1727,7 @@ typedef struct GroupingSet
* options are never copied, per spec.
* "defineClause" is Row Pattern Recognition DEFINE clause (list of
* TargetEntry). TargetEntry.resname represents row pattern definition
- * variable name. "rpPattern" represents PATTERN clause as an AST tree
+ * variable name. "rpPattern" represents the PATTERN clause as a parse tree
* (RPRPatternNode).
*
*/
@@ -1763,7 +1763,7 @@ typedef struct WindowClause
* initial */
/* Row Pattern DEFINE clause (list of TargetEntry) */
List *defineClause;
- /* Row Pattern PATTERN clause AST */
+ /* Row Pattern PATTERN parse tree */
RPRPatternNode *rpPattern;
} WindowClause;
diff --git a/src/include/optimizer/rpr.h b/src/include/optimizer/rpr.h
index 83d3cd29b07..f5462ab2a7c 100644
--- a/src/include/optimizer/rpr.h
+++ b/src/include/optimizer/rpr.h
@@ -27,8 +27,15 @@
* before release.
*/
#define RPR_VARID_MAX 0xEF /* pattern variables are 0 to 0xEF */
+
+/*
+ * RPR_COUNT_INF is the value a runtime repetition count saturates at to avoid
+ * int32 overflow (see the count++ guard in nfa_match). It is defined as
+ * RPR_QUANTITY_INF so that a saturated count compares as "unbounded", just
+ * like an unbounded quantifier's max.
+ */
#define RPR_QUANTITY_INF PG_INT32_MAX /* unbounded quantifier */
-#define RPR_COUNT_MAX PG_INT32_MAX /* max runtime count (NFA state) */
+#define RPR_COUNT_INF RPR_QUANTITY_INF
#define RPR_ELEMIDX_MAX PG_INT16_MAX /* max pattern elements */
#define RPR_ELEMIDX_INVALID ((RPRElemIdx) -1) /* invalid index */
#define RPR_DEPTH_MAX (PG_UINT8_MAX - 1) /* max pattern nesting depth:
diff --git a/src/test/regress/expected/rpr_explain.out b/src/test/regress/expected/rpr_explain.out
index cc86d0aae30..8672b4c3055 100644
--- a/src/test/regress/expected/rpr_explain.out
+++ b/src/test/regress/expected/rpr_explain.out
@@ -5598,13 +5598,15 @@ EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev_opt_mixed;
-- find_window_run_conditions() pushes WHERE filters on monotonic window
-- functions into WindowAgg as Run Conditions for early termination.
-- With RPR's required frame (ROWS BETWEEN CURRENT ROW AND UNBOUNDED
--- FOLLOWING), the monotonic direction determines which operators trigger
--- Run Condition pushdown:
+-- FOLLOWING), the monotonic direction of the window function determines
+-- which comparison operators allow pushdown:
-- INCREASING (<=): row_number, rank, dense_rank, percent_rank,
-- cume_dist, ntile
--- DECREASING (>): count(*) (via int8inc, END_UNBOUNDED_FOLLOWING)
--- RPR window function results are match-dependent, not monotonic.
--- Test with count(*) > 0 as representative case.
+-- DECREASING (>): count(*). As the current row advances, the frame
+-- (which ends at UNBOUNDED FOLLOWING) shrinks, so the
+-- count decreases.
+-- RPR window function results are match-dependent, not monotonic, so this
+-- pushdown does not apply. Test with count(*) > 0 as a representative case.
--
-- Without RPR: count(*) > 0 is pushed down as Run Condition
EXPLAIN (COSTS OFF)
diff --git a/src/test/regress/sql/rpr_explain.sql b/src/test/regress/sql/rpr_explain.sql
index 297ca78d54b..115402e304d 100644
--- a/src/test/regress/sql/rpr_explain.sql
+++ b/src/test/regress/sql/rpr_explain.sql
@@ -3174,13 +3174,15 @@ EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev_opt_mixed;
-- find_window_run_conditions() pushes WHERE filters on monotonic window
-- functions into WindowAgg as Run Conditions for early termination.
-- With RPR's required frame (ROWS BETWEEN CURRENT ROW AND UNBOUNDED
--- FOLLOWING), the monotonic direction determines which operators trigger
--- Run Condition pushdown:
+-- FOLLOWING), the monotonic direction of the window function determines
+-- which comparison operators allow pushdown:
-- INCREASING (<=): row_number, rank, dense_rank, percent_rank,
-- cume_dist, ntile
--- DECREASING (>): count(*) (via int8inc, END_UNBOUNDED_FOLLOWING)
--- RPR window function results are match-dependent, not monotonic.
--- Test with count(*) > 0 as representative case.
+-- DECREASING (>): count(*). As the current row advances, the frame
+-- (which ends at UNBOUNDED FOLLOWING) shrinks, so the
+-- count decreases.
+-- RPR window function results are match-dependent, not monotonic, so this
+-- pushdown does not apply. Test with count(*) > 0 as a representative case.
--
-- Without RPR: count(*) > 0 is pushed down as Run Condition
--
2.50.1 (Apple Git-155)
view thread (141+ messages) latest in thread
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], [email protected], [email protected]
Subject: Re: Row pattern recognition
In-Reply-To: <CAAAe_zCsaf=WedELLjqLe3BV_8dWiO1DPDGA9sXj4qhe+=-XXw@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