v49-0002-Row-pattern-recognition-patch-parse-analysis.patch
application/octet-stream
Filename: v49-0002-Row-pattern-recognition-patch-parse-analysis.patch
Type: application/octet-stream
Part: 1
Message:
Re: Row pattern recognition
Patch
Same data as JSON:
GET /api/v1/attachments/:id/patch
the parsed metadata as JSON — format, series position, per-file stats; never the diff bytes.
API reference →
Format: format-patch
Series: patch v49-0002
Subject: Row pattern recognition patch (parse/analysis).
| File | + | − |
|---|---|---|
| src/backend/nodes/copyfuncs.c | 27 | 0 |
| src/backend/nodes/equalfuncs.c | 35 | 0 |
| src/backend/nodes/outfuncs.c | 51 | 0 |
| src/backend/nodes/readfuncs.c | 85 | 0 |
| src/backend/parser/Makefile | 1 | 0 |
| src/backend/parser/meson.build | 1 | 0 |
| src/backend/parser/parse_agg.c | 8 | 1 |
| src/backend/parser/parse_clause.c | 7 | 1 |
| src/backend/parser/parse_cte.c | 56 | 0 |
| src/backend/parser/parse_expr.c | 103 | 0 |
| src/backend/parser/parse_func.c | 85 | 1 |
| src/backend/parser/parse_rpr.c | 681 | 0 |
| src/backend/parser/README | 1 | 0 |
| src/include/nodes/primnodes.h | 54 | 0 |
| src/include/parser/parse_rpr.h | 22 | 0 |
From db195b451efd243ff6c6ef6632dc983f78891c29 Mon Sep 17 00:00:00 2001
From: Tatsuo Ishii <ishii@postgresql.org>
Date: Fri, 19 Jun 2026 20:18:45 +0900
Subject: [PATCH v50 02/29] Row pattern recognition patch (parse/analysis).
---
src/backend/nodes/copyfuncs.c | 27 ++
src/backend/nodes/equalfuncs.c | 35 ++
src/backend/nodes/outfuncs.c | 51 +++
src/backend/nodes/readfuncs.c | 85 ++++
src/backend/parser/Makefile | 1 +
src/backend/parser/README | 1 +
src/backend/parser/meson.build | 1 +
src/backend/parser/parse_agg.c | 9 +-
src/backend/parser/parse_clause.c | 8 +-
src/backend/parser/parse_cte.c | 56 +++
src/backend/parser/parse_expr.c | 103 +++++
src/backend/parser/parse_func.c | 86 +++-
src/backend/parser/parse_rpr.c | 681 ++++++++++++++++++++++++++++++
src/include/nodes/primnodes.h | 54 +++
src/include/parser/parse_rpr.h | 22 +
15 files changed, 1217 insertions(+), 3 deletions(-)
create mode 100644 src/backend/parser/parse_rpr.c
create mode 100644 src/include/parser/parse_rpr.h
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index ff22a04abe5..17d45930d7b 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -16,6 +16,7 @@
#include "postgres.h"
#include "miscadmin.h"
+#include "nodes/plannodes.h"
#include "utils/datum.h"
@@ -166,6 +167,32 @@ _copyBitmapset(const Bitmapset *from)
return bms_copy(from);
}
+static RPRPattern *
+_copyRPRPattern(const RPRPattern *from)
+{
+ RPRPattern *newnode = makeNode(RPRPattern);
+
+ COPY_SCALAR_FIELD(numVars);
+ COPY_SCALAR_FIELD(maxDepth);
+ COPY_SCALAR_FIELD(numElements);
+
+ /* Deep copy the varNames array (DEFINE clause is required) */
+ Assert(from->numVars > 0);
+ newnode->varNames = palloc0_array(char *, from->numVars);
+ for (int i = 0; i < from->numVars; i++)
+ newnode->varNames[i] = pstrdup(from->varNames[i]);
+
+ /* Deep copy the elements array (always has at least one element + FIN) */
+ Assert(from->numElements >= 2);
+ newnode->elements = palloc_array(RPRPatternElement, from->numElements);
+ memcpy(newnode->elements, from->elements,
+ from->numElements * sizeof(RPRPatternElement));
+
+ COPY_SCALAR_FIELD(isAbsorbable);
+
+ return newnode;
+}
+
/*
* copyObjectImpl -- implementation of copyObject(); see nodes/nodes.h
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 3d1a1adf86e..328199918b8 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -20,6 +20,7 @@
#include "postgres.h"
#include "miscadmin.h"
+#include "nodes/plannodes.h"
#include "utils/datum.h"
@@ -149,6 +150,40 @@ _equalBitmapset(const Bitmapset *a, const Bitmapset *b)
return bms_equal(a, b);
}
+static bool
+_equalRPRPattern(const RPRPattern *a, const RPRPattern *b)
+{
+ COMPARE_SCALAR_FIELD(numVars);
+ COMPARE_SCALAR_FIELD(maxDepth);
+ COMPARE_SCALAR_FIELD(numElements);
+
+ /* Compare varNames array */
+ if (a->numVars > 0)
+ {
+ if (a->varNames == NULL || b->varNames == NULL)
+ return false;
+ for (int i = 0; i < a->numVars; i++)
+ {
+ if (strcmp(a->varNames[i], b->varNames[i]) != 0)
+ return false;
+ }
+ }
+
+ /* Compare elements array */
+ if (a->numElements > 0)
+ {
+ if (a->elements == NULL || b->elements == NULL)
+ return false;
+ if (memcmp(a->elements, b->elements,
+ a->numElements * sizeof(RPRPatternElement)) != 0)
+ return false;
+ }
+
+ COMPARE_SCALAR_FIELD(isAbsorbable);
+
+ return true;
+}
+
/*
* Lists are handled specially
*/
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 953c5797c5d..e6ea9ce22d9 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -23,6 +23,7 @@
#include "nodes/bitmapset.h"
#include "nodes/nodes.h"
#include "nodes/pg_list.h"
+#include "nodes/plannodes.h"
#include "utils/datum.h"
/* State flag that determines how nodeToStringInternal() should treat location fields */
@@ -727,6 +728,56 @@ _outA_Const(StringInfo str, const A_Const *node)
WRITE_LOCATION_FIELD(location);
}
+static void
+_outRPRPattern(StringInfo str, const RPRPattern *node)
+{
+ WRITE_NODE_TYPE("RPRPATTERN");
+
+ WRITE_INT_FIELD(numVars);
+ WRITE_INT_FIELD(maxDepth);
+ WRITE_INT_FIELD(numElements);
+
+ /* Write varNames array as list of strings */
+ appendStringInfoString(str, " :varNames");
+ if (node->numVars > 0 && node->varNames != NULL)
+ {
+ appendStringInfoString(str, " (");
+ for (int i = 0; i < node->numVars; i++)
+ {
+ if (i > 0)
+ appendStringInfoChar(str, ' ');
+ outToken(str, node->varNames[i]);
+ }
+ appendStringInfoChar(str, ')');
+ }
+ else
+ appendStringInfoString(str, " <>");
+
+ /* Write elements array */
+ appendStringInfoString(str, " :elements");
+ if (node->numElements > 0 && node->elements != NULL)
+ {
+ appendStringInfoChar(str, ' ');
+ for (int i = 0; i < node->numElements; i++)
+ {
+ const RPRPatternElement *elem = &node->elements[i];
+
+ appendStringInfo(str, "(%d %d %u %d %d %d %d)",
+ (int) elem->varId,
+ (int) elem->depth,
+ (unsigned) elem->flags,
+ (int) elem->min,
+ (int) elem->max,
+ (int) elem->next,
+ (int) elem->jump);
+ }
+ }
+ else
+ appendStringInfoString(str, " <>");
+
+ WRITE_BOOL_FIELD(isAbsorbable);
+}
+
/*
* outNode -
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
index b6b2ce6c792..6c39c6fe06d 100644
--- a/src/backend/nodes/readfuncs.c
+++ b/src/backend/nodes/readfuncs.c
@@ -28,6 +28,7 @@
#include "miscadmin.h"
#include "nodes/bitmapset.h"
+#include "nodes/plannodes.h"
#include "nodes/readfuncs.h"
@@ -567,6 +568,90 @@ _readExtensibleNode(void)
READ_DONE();
}
+static RPRPattern *
+_readRPRPattern(void)
+{
+ READ_LOCALS(RPRPattern);
+
+ READ_INT_FIELD(numVars);
+ READ_INT_FIELD(maxDepth);
+ READ_INT_FIELD(numElements);
+
+ /* Read varNames array */
+ token = pg_strtok(&length); /* skip :varNames */
+ token = pg_strtok(&length); /* get '(' or '<>' */
+ if (local_node->numVars > 0 && token[0] == '(')
+ {
+ local_node->varNames = palloc_array(char *, local_node->numVars);
+ for (int i = 0; i < local_node->numVars; i++)
+ {
+ token = pg_strtok(&length);
+ local_node->varNames[i] = debackslash(token, length);
+ }
+ token = pg_strtok(&length); /* skip ')' */
+ }
+ else
+ {
+ local_node->varNames = NULL;
+ }
+
+ /* Read elements array */
+ token = pg_strtok(&length); /* skip :elements */
+ token = pg_strtok(&length); /* get '(' or '<>' */
+ if (local_node->numElements > 0 && token[0] == '(')
+ {
+ local_node->elements = palloc0_array(RPRPatternElement, local_node->numElements);
+ for (int i = 0; i < local_node->numElements; i++)
+ {
+ RPRPatternElement *elem = &local_node->elements[i];
+ int varId,
+ flags,
+ depth,
+ min,
+ max,
+ next,
+ jump;
+
+ /* Parse "(varId depth flags min max next jump)" */
+ token = pg_strtok(&length);
+ varId = atoi(token);
+ token = pg_strtok(&length);
+ depth = atoi(token);
+ token = pg_strtok(&length);
+ flags = atoi(token);
+ token = pg_strtok(&length);
+ min = atoi(token);
+ token = pg_strtok(&length);
+ max = atoi(token);
+ token = pg_strtok(&length);
+ next = atoi(token);
+ token = pg_strtok(&length);
+ jump = atoi(token);
+ token = pg_strtok(&length); /* skip ')' */
+
+ elem->varId = (RPRVarId) varId;
+ elem->flags = (RPRElemFlags) flags;
+ elem->depth = (RPRDepth) depth;
+ elem->min = (RPRQuantity) min;
+ elem->max = (RPRQuantity) max;
+ elem->next = (RPRElemIdx) next;
+ elem->jump = (RPRElemIdx) jump;
+
+ /* Read next element's '(' or end */
+ if (i < local_node->numElements - 1)
+ token = pg_strtok(&length); /* get '(' */
+ }
+ }
+ else
+ {
+ local_node->elements = NULL;
+ }
+
+ READ_BOOL_FIELD(isAbsorbable);
+
+ READ_DONE();
+}
+
/*
* parseNodeString
diff --git a/src/backend/parser/Makefile b/src/backend/parser/Makefile
index 8b5a4af6bf2..51e6b1adfb8 100644
--- a/src/backend/parser/Makefile
+++ b/src/backend/parser/Makefile
@@ -30,6 +30,7 @@ OBJS = \
parse_oper.o \
parse_param.o \
parse_relation.o \
+ parse_rpr.o \
parse_target.o \
parse_type.o \
parse_utilcmd.o \
diff --git a/src/backend/parser/README b/src/backend/parser/README
index e26eb437a9f..22a5e91c8cf 100644
--- a/src/backend/parser/README
+++ b/src/backend/parser/README
@@ -26,6 +26,7 @@ parse_node.c create nodes for various structures
parse_oper.c handle operators in expressions
parse_param.c handle Params (for the cases used in the core backend)
parse_relation.c support routines for tables and column handling
+parse_rpr.c handle Row Pattern Recognition
parse_target.c handle the result list of the query
parse_type.c support routines for data type handling
parse_utilcmd.c parse analysis for utility commands (done at execution time)
diff --git a/src/backend/parser/meson.build b/src/backend/parser/meson.build
index 86c09b29ec2..82fe86e10db 100644
--- a/src/backend/parser/meson.build
+++ b/src/backend/parser/meson.build
@@ -17,6 +17,7 @@ backend_sources += files(
'parse_oper.c',
'parse_param.c',
'parse_relation.c',
+ 'parse_rpr.c',
'parse_target.c',
'parse_type.c',
'parse_utilcmd.c',
diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c
index acb933392de..b16e54d6e31 100644
--- a/src/backend/parser/parse_agg.c
+++ b/src/backend/parser/parse_agg.c
@@ -597,7 +597,10 @@ check_agglevels_and_constraints(ParseState *pstate, Node *expr)
err = _("aggregate functions are not allowed in property definition expressions");
else
err = _("grouping operations are not allowed in property definition expressions");
+ break;
+ case EXPR_KIND_RPR_DEFINE:
+ errkind = true;
break;
/*
@@ -1045,6 +1048,9 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc,
case EXPR_KIND_FOR_PORTION:
err = _("window functions are not allowed in FOR PORTION OF expressions");
break;
+ case EXPR_KIND_RPR_DEFINE:
+ errkind = true;
+ break;
/*
* There is intentionally no default: case here, so that the
@@ -1125,7 +1131,8 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc,
equal(refwin->orderClause, windef->orderClause) &&
refwin->frameOptions == windef->frameOptions &&
equal(refwin->startOffset, windef->startOffset) &&
- equal(refwin->endOffset, windef->endOffset))
+ equal(refwin->endOffset, windef->endOffset) &&
+ equal(refwin->rpCommonSyntax, windef->rpCommonSyntax))
{
/* found a duplicate window specification */
wfunc->winref = winref;
diff --git a/src/backend/parser/parse_clause.c b/src/backend/parser/parse_clause.c
index 5fe5257b019..550ea4eb9c0 100644
--- a/src/backend/parser/parse_clause.c
+++ b/src/backend/parser/parse_clause.c
@@ -39,6 +39,7 @@
#include "parser/parse_graphtable.h"
#include "parser/parse_oper.h"
#include "parser/parse_relation.h"
+#include "parser/parse_rpr.h"
#include "parser/parse_target.h"
#include "parser/parse_type.h"
#include "parser/parser.h"
@@ -101,7 +102,6 @@ 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,
@@ -3035,6 +3035,8 @@ transformWindowDefinitions(ParseState *pstate,
* And prepare the new WindowClause.
*/
wc = makeNode(WindowClause);
+ wc->rpSkipTo = ST_NONE; /* ST_NONE marks this as a non-RPR window;
+ * overridden by transformRPR() if RPR is used */
wc->name = windef->name;
wc->refname = windef->refname;
@@ -3163,6 +3165,10 @@ transformWindowDefinitions(ParseState *pstate,
rangeopfamily, rangeopcintype,
&wc->endInRangeFunc,
windef->endOffset);
+
+ /* Process Row Pattern Recognition related clauses */
+ transformRPR(pstate, wc, windef, targetlist);
+
wc->winref = winref;
result = lappend(result, wc);
diff --git a/src/backend/parser/parse_cte.c b/src/backend/parser/parse_cte.c
index ccde199319a..3e493beba0b 100644
--- a/src/backend/parser/parse_cte.c
+++ b/src/backend/parser/parse_cte.c
@@ -96,6 +96,14 @@ static void checkWellFormedRecursion(CteState *cstate);
static bool checkWellFormedRecursionWalker(Node *node, CteState *cstate);
static void checkWellFormedSelectStmt(SelectStmt *stmt, CteState *cstate);
+/* Recursive-WITH RPR rejection */
+typedef struct
+{
+ ParseLoc location; /* location of first RPR window, or -1 */
+} ContainRPRContext;
+
+static bool contain_rpr_walker(Node *node, void *context);
+
/*
* transformWithClause -
@@ -164,6 +172,28 @@ transformWithClause(ParseState *pstate, WithClause *withClause)
CteState cstate;
int i;
+ /*
+ * Per ISO/IEC 9075-2:2016 7.17 Syntax Rule 3)e)f), every <with list
+ * element> in a WITH RECURSIVE clause is "potentially recursive" and
+ * shall not contain a <row pattern common syntax>. (PostgreSQL does
+ * not implement <row pattern measures>, so only the common syntax
+ * needs to be checked.) ISO/IEC 19075-5 6.17.5 (R020) and 4.18.5
+ * (R010) restate the prohibition for CREATE RECURSIVE VIEW, which is
+ * rewritten to WITH RECURSIVE by makeRecursiveViewSelect() and so
+ * flows through here as well.
+ */
+ foreach_node(CommonTableExpr, cte, withClause->ctes)
+ {
+ ContainRPRContext ctx;
+
+ ctx.location = -1;
+ if (contain_rpr_walker(cte->ctequery, &ctx))
+ ereport(ERROR,
+ errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("cannot use row pattern recognition in a recursive query"),
+ parser_errposition(pstate, ctx.location));
+ }
+
cstate.pstate = pstate;
cstate.numitems = list_length(withClause->ctes);
cstate.items = (CteItem *) palloc0(cstate.numitems * sizeof(CteItem));
@@ -1268,3 +1298,29 @@ 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.
+ */
+static bool
+contain_rpr_walker(Node *node, void *context)
+{
+ if (node == NULL)
+ return false;
+ if (IsA(node, WindowDef))
+ {
+ WindowDef *wd = (WindowDef *) node;
+
+ if (wd->rpCommonSyntax != NULL)
+ {
+ ((ContainRPRContext *) context)->location = wd->rpCommonSyntax->location;
+ return true;
+ }
+ }
+ return raw_expression_tree_walker(node, contain_rpr_walker, context);
+}
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 9adc9d4c0f6..6e5992642ab 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -579,6 +579,7 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
case EXPR_KIND_GENERATED_COLUMN:
case EXPR_KIND_CYCLE_MARK:
case EXPR_KIND_PROPGRAPH_PROPERTY:
+ case EXPR_KIND_RPR_DEFINE:
/* okay */
break;
@@ -627,6 +628,57 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
if (node != NULL)
return node;
+ /*----------
+ * Qualified references in DEFINE need a tri-classification:
+ *
+ * pattern variable qualifier (e.g. UP.price): valid per
+ * ISO/IEC 19075-5 6.15 / 4.16 but not yet implemented --
+ * raise FEATURE_NOT_SUPPORTED.
+ *
+ * FROM-clause range variable qualifier: prohibited by
+ * ISO/IEC 19075-5 6.5 -- raise SYNTAX_ERROR.
+ *
+ * any other qualifier (typo, undefined name): fall through and let
+ * normal column resolution produce a sensible error.
+ *
+ * The quoted text reflects only the ColumnRef portion; a trailing field
+ * selection on a composite type (e.g. ".amount" in "(A.items).amount")
+ * lives in the surrounding A_Indirection node and is not included here.
+ * That can be revisited when MEASURES support adds indirection-aware
+ * traversal.
+ *----------
+ */
+ if (pstate->p_expr_kind == EXPR_KIND_RPR_DEFINE &&
+ list_length(cref->fields) != 1)
+ {
+ char *qualifier = strVal(linitial(cref->fields));
+ bool is_pattern_var = false;
+
+ foreach_node(String, pv, pstate->p_rpr_pattern_vars)
+ {
+ if (strcmp(strVal(pv), qualifier) == 0)
+ {
+ is_pattern_var = true;
+ break;
+ }
+ }
+
+ if (is_pattern_var)
+ ereport(ERROR,
+ errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("pattern variable qualified expression \"%s\" is not supported in DEFINE clause",
+ NameListToString(cref->fields)),
+ parser_errposition(pstate, cref->location));
+ else if (refnameNamespaceItem(pstate, NULL, qualifier,
+ cref->location, NULL) != NULL)
+ ereport(ERROR,
+ errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("range variable qualified expression \"%s\" is not allowed in DEFINE clause",
+ NameListToString(cref->fields)),
+ parser_errposition(pstate, cref->location));
+ /* else: unknown qualifier -- fall through to normal resolution */
+ }
+
/*----------
* The allowed syntaxes are:
*
@@ -894,6 +946,30 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
}
}
+ /*
+ * Restrict column references in a row pattern DEFINE clause. node is now
+ * a successfully resolved reference, so reject the two forms RPR does not
+ * allow: a correlated reference to an outer query's column, and a
+ * schema/catalog-qualified reference (three or more name parts). Simple
+ * two-part qualifiers (pattern or range variable) are handled earlier,
+ * before resolution.
+ */
+ if (pstate->p_expr_kind == EXPR_KIND_RPR_DEFINE)
+ {
+ if (IsA(node, Var) && ((Var *) node)->varlevelsup > 0)
+ ereport(ERROR,
+ errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot use outer query column in DEFINE clause"),
+ parser_errposition(pstate, cref->location));
+
+ if (list_length(cref->fields) >= 3)
+ ereport(ERROR,
+ errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("qualified expression \"%s\" is not allowed in DEFINE clause",
+ NameListToString(cref->fields)),
+ parser_errposition(pstate, cref->location));
+ }
+
return node;
}
@@ -1893,6 +1969,31 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
err = _("cannot use subquery in FOR PORTION OF expression");
break;
+ /*----------
+ * XXX SQL/RPR (ISO/IEC 19075-5 6.17.4 / 4.18.4; R020 / R010)
+ * permits a subquery nested in a DEFINE expression provided
+ * that:
+ * (a) the subquery does not itself perform row pattern
+ * recognition, and
+ * (b) the subquery does not reference a row pattern variable
+ * of the outer query.
+ *
+ * We reject all subqueries here for now. Implementing the
+ * case distinction would mean walking the analyzed subquery
+ * Query tree for nested RPR window clauses to enforce (a),
+ * and walking it for ColumnRef qualifiers matching any
+ * ancestor's p_rpr_pattern_vars to enforce (b). Both checks
+ * are doable with the existing infrastructure -- they are
+ * left as future work, not blocked on any other feature.
+ * Until then this blanket rejection is intentional
+ * over-rejection, not a standard fit; it subsumes both (a)
+ * and (b) by making the subquery itself unreachable.
+ *----------
+ */
+ case EXPR_KIND_RPR_DEFINE:
+ err = _("cannot use subquery in DEFINE expression");
+ break;
+
/*
* There is intentionally no default: case here, so that the
* compiler will warn if we add a new ParseExprKind without
@@ -3255,6 +3356,8 @@ ParseExprKindName(ParseExprKind exprKind)
return "property definition expression";
case EXPR_KIND_FOR_PORTION:
return "FOR PORTION OF";
+ case EXPR_KIND_RPR_DEFINE:
+ return "DEFINE";
/*
* There is intentionally no default: case here, so that the
diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c
index 860767a52ee..1f6c8fa4fb2 100644
--- a/src/backend/parser/parse_func.c
+++ b/src/backend/parser/parse_func.c
@@ -31,6 +31,7 @@
#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"
@@ -758,8 +759,88 @@ 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 || fdresult == FUNCDETAIL_PROCEDURE)
+ 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)
{
FuncExpr *funcexpr = makeNode(FuncExpr);
@@ -2792,6 +2873,9 @@ check_srf_call_placement(ParseState *pstate, Node *last_srf, int location)
case EXPR_KIND_FOR_PORTION:
err = _("set-returning functions are not allowed in FOR PORTION OF expressions");
break;
+ case EXPR_KIND_RPR_DEFINE:
+ errkind = true;
+ break;
/*
* There is intentionally no default: case here, so that the
diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
new file mode 100644
index 00000000000..3eaea2be750
--- /dev/null
+++ b/src/backend/parser/parse_rpr.c
@@ -0,0 +1,681 @@
+/*-------------------------------------------------------------------------
+ *
+ * parse_rpr.c
+ * Handle Row Pattern Recognition clauses in parser.
+ *
+ * This file transforms RPR-related clauses from raw parse tree to planner
+ * structures during query analysis:
+ * - 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
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ * src/backend/parser/parse_rpr.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "miscadmin.h"
+#include "nodes/makefuncs.h"
+#include "nodes/nodeFuncs.h"
+#include "optimizer/optimizer.h"
+#include "optimizer/rpr.h"
+#include "parser/parse_coerce.h"
+#include "parser/parse_collate.h"
+#include "parser/parse_expr.h"
+#include "parser/parse_rpr.h"
+#include "parser/parse_target.h"
+
+/* DEFINE clause walker context -- see define_walker for usage. */
+typedef enum
+{
+ DEFINE_PHASE_BODY, /* top-level DEFINE expression */
+ DEFINE_PHASE_NAV_ARG, /* inside an outer nav's arg subtree */
+ DEFINE_PHASE_NAV_OFFSET, /* inside an outer nav's offset_arg /
+ * compound_offset_arg */
+} DefinePhase;
+
+typedef struct
+{
+ ParseState *pstate;
+ DefinePhase phase;
+ int nav_count; /* RPRNavExpr nodes seen in current nav.arg */
+ bool has_column_ref; /* Var seen in current nav scope */
+ RPRNavKind inner_kind; /* kind of first nested nav in current arg */
+} DefineWalkCtx;
+
+/* 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 bool define_walker(Node *node, void *context);
+
+/*
+ * transformRPR
+ * Process Row Pattern Recognition related clauses.
+ *
+ * Validates and transforms RPR clauses from parse tree to planner structures:
+ * - Validates frame options (must start at CURRENT ROW, no EXCLUDE)
+ * - 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)
+ *
+ * Returns early if windef has no rpCommonSyntax (non-RPR window).
+ */
+void
+transformRPR(ParseState *pstate, WindowClause *wc, WindowDef *windef,
+ List **targetlist)
+{
+ /* Window definition must exist when called */
+ Assert(windef != NULL);
+
+ /*
+ * Row Pattern Common Syntax clause exists?
+ */
+ if (windef->rpCommonSyntax == NULL)
+ return;
+
+ /* Check Frame options */
+
+ /* Frame type must be "ROW" */
+ if (wc->frameOptions & FRAMEOPTION_GROUPS)
+ ereport(ERROR,
+ errcode(ERRCODE_WINDOWING_ERROR),
+ errmsg("cannot use FRAME option GROUPS with row pattern recognition"),
+ errhint("Use ROWS instead."),
+ parser_errposition(pstate,
+ windef->frameLocation >= 0 ?
+ windef->frameLocation : windef->location));
+ if (wc->frameOptions & FRAMEOPTION_RANGE)
+ ereport(ERROR,
+ errcode(ERRCODE_WINDOWING_ERROR),
+ errmsg("cannot use FRAME option RANGE with row pattern recognition"),
+ errhint("Use ROWS instead."),
+ parser_errposition(pstate,
+ windef->frameLocation >= 0 ?
+ windef->frameLocation : windef->location));
+
+ /* Frame must start at current row */
+ if ((wc->frameOptions & FRAMEOPTION_START_CURRENT_ROW) == 0)
+ {
+ const char *frameType = "ROWS";
+ const char *startBound = "unknown";
+
+ /* Determine current start bound */
+ if (wc->frameOptions & FRAMEOPTION_START_UNBOUNDED_PRECEDING)
+ startBound = "UNBOUNDED PRECEDING";
+ else if (wc->frameOptions & FRAMEOPTION_START_OFFSET_PRECEDING)
+ startBound = "offset PRECEDING";
+ else if (wc->frameOptions & FRAMEOPTION_START_OFFSET_FOLLOWING)
+ startBound = "offset FOLLOWING";
+
+ /* At least one valid frame start option should be set */
+ Assert((wc->frameOptions & FRAMEOPTION_START_UNBOUNDED_PRECEDING) ||
+ (wc->frameOptions & FRAMEOPTION_START_OFFSET_PRECEDING) ||
+ (wc->frameOptions & FRAMEOPTION_START_OFFSET_FOLLOWING));
+
+ ereport(ERROR,
+ errcode(ERRCODE_WINDOWING_ERROR),
+ errmsg("FRAME must start at CURRENT ROW when using row pattern recognition"),
+ errdetail("Current frame starts with %s.", startBound),
+ errhint("Use: %s BETWEEN CURRENT ROW AND ...", frameType),
+ parser_errposition(pstate, windef->frameLocation >= 0 ? windef->frameLocation : windef->location));
+ }
+
+ /* EXCLUDE options are not permitted */
+ if ((wc->frameOptions & FRAMEOPTION_EXCLUSION) != 0)
+ {
+ const char *excludeType = "EXCLUDE";
+
+ /* Determine which EXCLUDE option was used */
+ if (wc->frameOptions & FRAMEOPTION_EXCLUDE_CURRENT_ROW)
+ excludeType = "EXCLUDE CURRENT ROW";
+ else if (wc->frameOptions & FRAMEOPTION_EXCLUDE_GROUP)
+ excludeType = "EXCLUDE GROUP";
+ else if (wc->frameOptions & FRAMEOPTION_EXCLUDE_TIES)
+ excludeType = "EXCLUDE TIES";
+
+ /* At least one valid exclude option should be set */
+ Assert((wc->frameOptions & FRAMEOPTION_EXCLUDE_CURRENT_ROW) ||
+ (wc->frameOptions & FRAMEOPTION_EXCLUDE_GROUP) ||
+ (wc->frameOptions & FRAMEOPTION_EXCLUDE_TIES));
+
+ ereport(ERROR,
+ errcode(ERRCODE_WINDOWING_ERROR),
+ errmsg("cannot use EXCLUDE options with row pattern recognition"),
+ errdetail("Frame definition includes %s.", excludeType),
+ errhint("Remove the EXCLUDE clause from the window definition."),
+ parser_errposition(pstate, windef->excludeLocation >= 0 ? windef->excludeLocation : windef->location));
+ }
+
+ /*
+ * The standard allows only UNBOUNDED FOLLOWING or a positive offset
+ * FOLLOWING as the frame end. The equivalent 0 FOLLOWING spelling is
+ * caught at runtime in calculate_frame_offsets().
+ */
+ if (wc->frameOptions & FRAMEOPTION_END_CURRENT_ROW)
+ ereport(ERROR,
+ errcode(ERRCODE_WINDOWING_ERROR),
+ errmsg("cannot use CURRENT ROW as frame end with row pattern recognition"),
+ errhint("Use UNBOUNDED FOLLOWING or a positive offset FOLLOWING."),
+ parser_errposition(pstate,
+ windef->frameLocation >= 0 ?
+ windef->frameLocation : windef->location));
+
+ /* Assign AFTER MATCH SKIP TO flag */
+ wc->rpSkipTo = windef->rpCommonSyntax->rpSkipTo;
+
+ /* Assign INITIAL flag */
+ wc->initial = windef->rpCommonSyntax->initial;
+
+ /* Transform DEFINE clause into list of TargetEntry's */
+ wc->defineClause = transformDefineClause(pstate, wc, windef, targetlist);
+
+ /* Store PATTERN AST for deparsing */
+ wc->rpPattern = windef->rpCommonSyntax->rpPattern;
+}
+
+/*
+ * validateRPRPatternVarCount
+ * Validate that PATTERN variable count fits the varId range.
+ *
+ * Recursively traverses the pattern tree, collecting unique variable names.
+ * 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.
+ */
+static void
+validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node,
+ List *rpDefs, List **varNames)
+{
+ /* Pattern node must exist - parser always provides non-NULL root */
+ Assert(node != NULL);
+
+ /*
+ * trailing_alt is a transient grammar flag; splitRPRTrailingAlt must have
+ * cleared it on every node before the pattern reaches parse analysis.
+ */
+ Assert(!node->trailing_alt);
+
+ check_stack_depth();
+
+ switch (node->nodeType)
+ {
+ case RPR_PATTERN_VAR:
+ /* Add variable name if not already in list */
+ {
+ bool found = false;
+
+ foreach_node(String, varname, *varNames)
+ {
+ if (strcmp(strVal(varname), node->varName) == 0)
+ {
+ found = true;
+ break;
+ }
+ }
+ if (!found)
+ {
+ /*
+ * Check against RPR_VARID_MAX before adding. varId
+ * values run 0 to RPR_VARID_MAX inclusive, so the next
+ * varId to be assigned (the current list length) must not
+ * exceed it.
+ */
+ if (list_length(*varNames) > RPR_VARID_MAX)
+ ereport(ERROR,
+ errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+ errmsg("too many pattern variables"),
+ errdetail("Maximum is %d.", RPR_VARID_MAX + 1),
+ parser_errposition(pstate,
+ exprLocation((Node *) node)));
+
+ *varNames = lappend(*varNames, makeString(pstrdup(node->varName)));
+ }
+ }
+ break;
+
+ case RPR_PATTERN_SEQ:
+ case RPR_PATTERN_ALT:
+ case RPR_PATTERN_GROUP:
+ /* Recurse into children */
+ foreach_node(RPRPatternNode, child, node->children)
+ {
+ validateRPRPatternVarCount(pstate, child, NULL, 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));
+ }
+ }
+}
+
+/*
+ * transformDefineClause
+ * Process DEFINE clause and transform ResTarget into list of TargetEntry.
+ *
+ * First:
+ * 1. Validates PATTERN variable count and collects RPR variable names
+ *
+ * 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
+ *
+ * Note: Variables not in DEFINE are evaluated as TRUE by the executor.
+ * Variables in DEFINE but not in PATTERN are rejected as an error.
+ *
+ * XXX Pattern variable qualified expressions in DEFINE (e.g. "A.price")
+ * are not yet supported. Currently rejected by transformColumnRef in
+ * parse_expr.c via the p_rpr_pattern_vars check.
+ */
+static List *
+transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef,
+ List **targetlist)
+{
+ List *restargets;
+ List *defineClause = NIL;
+ char *name;
+ List *patternVarNames = NIL;
+
+ /*
+ * If Row Definition Common Syntax exists, DEFINE clause must exist. (the
+ * raw parser should have already checked it.)
+ */
+ Assert(windef->rpCommonSyntax->rpDefs != NULL);
+
+ /*
+ * Validate PATTERN variable count, reject DEFINE variables not used in
+ * PATTERN, and collect PATTERN variable names for transformColumnRef.
+ */
+ validateRPRPatternVarCount(pstate, windef->rpCommonSyntax->rpPattern,
+ windef->rpCommonSyntax->rpDefs,
+ &patternVarNames);
+ pstate->p_rpr_pattern_vars = patternVarNames;
+
+ /*
+ * Check for duplicate row pattern definition variables. The standard
+ * requires that no two row pattern definition variable names shall be
+ * equivalent.
+ */
+ restargets = NIL;
+ foreach_node(ResTarget, restarget, windef->rpCommonSyntax->rpDefs)
+ {
+ TargetEntry *teDefine;
+
+ name = restarget->name;
+
+ foreach_node(ResTarget, r, restargets)
+ {
+ char *n;
+
+ n = r->name;
+
+ if (!strcmp(n, name))
+ ereport(ERROR,
+ errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("DEFINE variable \"%s\" appears more than once",
+ name),
+ parser_errposition(pstate, exprLocation((Node *) r)));
+ }
+
+ restargets = lappend(restargets, restarget);
+
+ /*
+ * 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.
+ */
+ {
+ Node *expr;
+ List *vars;
+
+ 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;
+
+ foreach_node(TargetEntry, tle, *targetlist)
+ {
+ 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);
+ }
+ }
+ 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
+ * variable.
+ */
+ foreach_ptr(TargetEntry, te, defineClause)
+ {
+ DefineWalkCtx ctx;
+
+ ctx.pstate = pstate;
+ ctx.phase = DEFINE_PHASE_BODY;
+ ctx.nav_count = 0;
+ ctx.has_column_ref = false;
+ ctx.inner_kind = 0;
+ (void) define_walker((Node *) te->expr, &ctx);
+ }
+
+ /* mark column origins */
+ markTargetListOrigins(pstate, defineClause);
+
+ /* mark all nodes in the DEFINE clause tree with collation information */
+ assign_expr_collations(pstate, (Node *) defineClause);
+
+ return defineClause;
+}
+
+/*
+ * Single-pass DEFINE clause validator.
+ *
+ * One walker function (define_walker) visits every node in a DEFINE
+ * expression exactly once and enforces every rule:
+ * - For each outer RPRNavExpr (per ISO/IEC 19075-5 5.6.4 nesting rules):
+ * * arg must contain at least one column reference
+ * * 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
+ *
+ * Volatile callees (and sequence operations) are rejected later in the
+ * planner via validate_rpr_define_volatility(); see optimizer/plan/rpr.c.
+ *
+ * The walker uses a phase tag to know which subtree it is in: DEFINE
+ * body (top-level), inside a nav.arg, or inside a nav.offset_arg /
+ * compound_offset_arg. When entering an outer nav (PHASE_BODY), it
+ * 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.
+ */
+
+/*
+ * define_walker
+ * Single-pass DEFINE clause validator. At each node, enforces:
+ *
+ * [1] for each outer RPRNavExpr (PHASE_BODY -> PHASE_NAV_ARG):
+ * - nav.arg must contain at least one column reference
+ * - PREV/NEXT wrapping FIRST/LAST is flattened in place
+ * to a compound kind (PREV_FIRST, PREV_LAST, NEXT_FIRST,
+ * NEXT_LAST)
+ * - any other nesting is rejected (FIRST(PREV()),
+ * 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)
+ *
+ * Var sightings feed the column-ref rule for the enclosing nav scope;
+ * RPRNavExpr sightings inside PHASE_NAV_ARG feed the nesting decision.
+ * See the comment block above DefinePhase for the overall design and
+ * how each subtree is walked exactly once.
+ */
+static bool
+define_walker(Node *node, void *context)
+{
+ DefineWalkCtx *ctx = (DefineWalkCtx *) context;
+
+ if (node == NULL)
+ return false;
+
+ /* Var sighting feeds the column-ref rule for the enclosing nav scope. */
+ if (IsA(node, Var) &&
+ (ctx->phase == DEFINE_PHASE_NAV_ARG ||
+ ctx->phase == DEFINE_PHASE_NAV_OFFSET))
+ ctx->has_column_ref = true;
+
+ if (IsA(node, RPRNavExpr))
+ {
+ RPRNavExpr *nav = (RPRNavExpr *) node;
+
+ if (ctx->phase == DEFINE_PHASE_NAV_ARG)
+ {
+ /*
+ * Nested nav inside an outer nav.arg: record for the outer's
+ * compound / nesting decision, then keep recursing so deeper Vars
+ * and volatile callees are still observed.
+ */
+ if (ctx->nav_count == 0)
+ ctx->inner_kind = nav->kind;
+ ctx->nav_count++;
+ return expression_tree_walker(node, define_walker, ctx);
+ }
+
+ 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.
+ */
+ return expression_tree_walker(node, define_walker, ctx);
+ }
+
+ /*
+ * 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);
+ bool flattened = false;
+
+ ctx->phase = DEFINE_PHASE_NAV_ARG;
+ ctx->nav_count = 0;
+ ctx->has_column_ref = false;
+ ctx->inner_kind = 0;
+ (void) define_walker((Node *) nav->arg, ctx);
+
+ if (ctx->nav_count > 0)
+ {
+ bool inner_phys = (ctx->inner_kind == RPR_NAV_PREV ||
+ ctx->inner_kind == RPR_NAV_NEXT);
+
+ if (outer_phys && !inner_phys)
+ {
+ RPRNavExpr *inner;
+
+ /* Reject triple-or-deeper nesting */
+ if (ctx->nav_count > 1)
+ ereport(ERROR,
+ errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("cannot nest row pattern navigation more than two levels deep"),
+ errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
+ parser_errposition(ctx->pstate, nav->location));
+
+ if (!IsA(nav->arg, RPRNavExpr))
+ ereport(ERROR,
+ errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("row pattern navigation operation must be a direct argument of the outer navigation"),
+ errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
+ parser_errposition(ctx->pstate, nav->location));
+
+ inner = (RPRNavExpr *) nav->arg;
+
+ if (nav->kind == RPR_NAV_PREV && inner->kind == RPR_NAV_FIRST)
+ nav->kind = RPR_NAV_PREV_FIRST;
+ else if (nav->kind == RPR_NAV_PREV && inner->kind == RPR_NAV_LAST)
+ nav->kind = RPR_NAV_PREV_LAST;
+ else if (nav->kind == RPR_NAV_NEXT && inner->kind == RPR_NAV_FIRST)
+ nav->kind = RPR_NAV_NEXT_FIRST;
+ else if (nav->kind == RPR_NAV_NEXT && inner->kind == RPR_NAV_LAST)
+ nav->kind = RPR_NAV_NEXT_LAST;
+
+ nav->compound_offset_arg = nav->offset_arg;
+ nav->offset_arg = inner->offset_arg;
+ nav->arg = inner->arg;
+ flattened = true;
+
+ /*
+ * The flattened argument must include a column reference,
+ * just like the simple-nav case below.
+ */
+ if (!ctx->has_column_ref)
+ ereport(ERROR,
+ errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("argument of row pattern navigation operation must include at least one column reference"),
+ parser_errposition(ctx->pstate, nav->location));
+ }
+ else if (!outer_phys && inner_phys)
+ ereport(ERROR,
+ errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("FIRST and LAST cannot contain PREV or NEXT"),
+ errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
+ parser_errposition(ctx->pstate, nav->location));
+ else if (outer_phys && inner_phys)
+ ereport(ERROR,
+ errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("PREV and NEXT cannot contain PREV or NEXT"),
+ errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
+ parser_errposition(ctx->pstate, nav->location));
+ else
+ ereport(ERROR,
+ errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("FIRST and LAST cannot contain FIRST or LAST"),
+ errhint("Only PREV(FIRST()), PREV(LAST()), NEXT(FIRST()), and NEXT(LAST()) compound forms are allowed."),
+ parser_errposition(ctx->pstate, nav->location));
+ }
+ else if (!ctx->has_column_ref)
+ {
+ ereport(ERROR,
+ errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("argument of row pattern navigation operation must include at least one column reference"),
+ parser_errposition(ctx->pstate, nav->location));
+ }
+
+ /*
+ * Walk offset arg(s) in PHASE_NAV_OFFSET to enforce the
+ * constant-offset rule. For compound forms, both the inner
+ * (post-flatten nav->offset_arg) and outer (compound_offset_arg)
+ * offsets must be constants; the inner's column-ref status was
+ * not separately tracked during the PHASE_NAV_ARG walk (which
+ * only checks that nav.arg as a whole has at least one Var), so
+ * it is re-walked here to catch column references the inner
+ * offset would have leaked.
+ */
+ ctx->phase = DEFINE_PHASE_NAV_OFFSET;
+
+ if (nav->offset_arg != NULL)
+ {
+ ctx->has_column_ref = false;
+ (void) define_walker((Node *) nav->offset_arg, ctx);
+ if (ctx->has_column_ref)
+ ereport(ERROR,
+ errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("row pattern navigation offset must be a run-time constant"),
+ parser_errposition(ctx->pstate, exprLocation((Node *) nav->offset_arg)));
+ }
+ if (flattened && nav->compound_offset_arg != NULL)
+ {
+ ctx->has_column_ref = false;
+ (void) define_walker((Node *) nav->compound_offset_arg, ctx);
+ if (ctx->has_column_ref)
+ ereport(ERROR,
+ errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("row pattern navigation offset must be a run-time constant"),
+ parser_errposition(ctx->pstate, exprLocation((Node *) nav->compound_offset_arg)));
+ }
+
+ *ctx = saved;
+ return false;
+ }
+ }
+
+ return expression_tree_walker(node, define_walker, ctx);
+}
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index bb05aeebee4..7c1b3d1bb07 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -640,6 +640,60 @@ typedef struct WindowFuncRunCondition
Expr *arg;
} WindowFuncRunCondition;
+/*
+ * RPRNavExpr
+ *
+ * Represents a PREV/NEXT/FIRST/LAST navigation call in an RPR DEFINE clause.
+ * At expression compile time this is translated into EEOP_RPR_NAV_SET /
+ * EEOP_RPR_NAV_RESTORE opcodes rather than a normal function call.
+ *
+ * Simple navigation (PREV/NEXT/FIRST/LAST):
+ * kind: RPR_NAV_PREV, RPR_NAV_NEXT, RPR_NAV_FIRST, or RPR_NAV_LAST
+ * arg: the expression to evaluate against the target row
+ * offset_arg: optional explicit offset expression (2-arg form); NULL for
+ * the 1-arg form (implicit offset: 1 for PREV/NEXT, 0 for
+ * FIRST/LAST)
+ *
+ * Compound navigation (PREV/NEXT wrapping FIRST/LAST):
+ * kind: RPR_NAV_PREV_FIRST, PREV_LAST, NEXT_FIRST, NEXT_LAST
+ * arg: the expression to evaluate against the final target row
+ * offset_arg: inner offset (FIRST/LAST), NULL = implicit default
+ * compound_offset_arg: outer offset (PREV/NEXT), NULL = implicit default
+ *
+ * Compound target computation:
+ * PREV_FIRST: (match_start + inner) - outer
+ * NEXT_FIRST: (match_start + inner) + outer
+ * PREV_LAST: (currentpos - inner) - outer
+ * NEXT_LAST: (currentpos - inner) + outer
+ */
+typedef enum RPRNavKind
+{
+ RPR_NAV_PREV,
+ RPR_NAV_NEXT,
+ RPR_NAV_FIRST,
+ RPR_NAV_LAST,
+ /* compound: outer(inner(arg)) */
+ RPR_NAV_PREV_FIRST,
+ RPR_NAV_PREV_LAST,
+ RPR_NAV_NEXT_FIRST,
+ RPR_NAV_NEXT_LAST,
+} RPRNavKind;
+
+typedef struct RPRNavExpr
+{
+ Expr xpr;
+ RPRNavKind kind; /* navigation kind */
+ Expr *arg; /* argument expression */
+ Expr *offset_arg; /* offset expression, or NULL for default */
+ Expr *compound_offset_arg; /* outer offset for compound nav, or
+ * NULL if simple */
+ Oid resulttype; /* result type (same as arg's type) */
+ /* OID of collation of result */
+ Oid resultcollid pg_node_attr(query_jumble_ignore);
+ /* token location, or -1 if unknown */
+ ParseLoc location;
+} RPRNavExpr;
+
/*
* MergeSupportFunc
*
diff --git a/src/include/parser/parse_rpr.h b/src/include/parser/parse_rpr.h
new file mode 100644
index 00000000000..7fab6f292aa
--- /dev/null
+++ b/src/include/parser/parse_rpr.h
@@ -0,0 +1,22 @@
+/*-------------------------------------------------------------------------
+ *
+ * parse_rpr.h
+ * handle Row Pattern Recognition in parser
+ *
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/parser/parse_rpr.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PARSE_RPR_H
+#define PARSE_RPR_H
+
+#include "parser/parse_node.h"
+
+extern void transformRPR(ParseState *pstate, WindowClause *wc,
+ WindowDef *windef, List **targetlist);
+
+#endif /* PARSE_RPR_H */
--
2.50.1 (Apple Git-155)