From 20e594baaaed6960cccc352e649af0c74576f84d Mon Sep 17 00:00:00 2001 From: Tatsuo Ishii Date: Sat, 13 Jun 2026 16:46:30 +0900 Subject: [PATCH v48 2/9] 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 in a WITH RECURSIVE clause is "potentially recursive" and + * shall not contain a . (PostgreSQL does + * not implement , 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 -- 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 2e4cc1de50d..9b9329598bc 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" @@ -756,8 +757,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); @@ -2790,6 +2871,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 7977ee24783..d7138f5141b 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -648,6 +648,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.43.0