From d5f63847585a9f0782c37ee9127bdb62ff163c50 Mon Sep 17 00:00:00 2001 From: Tatsuo Ishii Date: Tue, 20 Oct 2026 17:34:38 +0900 Subject: [PATCH v54 2/9] Row pattern recognition patch (parse/analysis). --- src/backend/nodes/copyfuncs.c | 30 ++ src/backend/nodes/outfuncs.c | 50 +++ src/backend/nodes/queryjumblefuncs.c | 30 ++ src/backend/nodes/readfuncs.c | 92 +++++ src/backend/parser/Makefile | 1 + src/backend/parser/README | 1 + src/backend/parser/meson.build | 1 + src/backend/parser/parse_agg.c | 42 +- src/backend/parser/parse_clause.c | 7 + src/backend/parser/parse_cte.c | 54 +++ src/backend/parser/parse_expr.c | 218 ++++++++++- src/backend/parser/parse_func.c | 233 ++++++++++- src/backend/parser/parse_rpr.c | 561 +++++++++++++++++++++++++++ src/backend/parser/parse_target.c | 84 +++- src/fe_utils/psqlscan.l | 2 +- src/include/nodes/primnodes.h | 59 +++ src/include/parser/parse_func.h | 2 + src/include/parser/parse_rpr.h | 22 ++ src/interfaces/ecpg/preproc/pgc.l | 4 +- 19 files changed, 1467 insertions(+), 26 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..abc6d693f64 100644 --- a/src/backend/nodes/copyfuncs.c +++ b/src/backend/nodes/copyfuncs.c @@ -166,6 +166,36 @@ _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 && from->varNames != NULL); + 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). + * This carries the whole struct, reserved byte included, where out/read + * carry seven fields and zero that byte -- see RPRPatternElement. + */ + 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/outfuncs.c b/src/backend/nodes/outfuncs.c index 40990143927..0a99e602942 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -718,6 +718,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. makeRPRPattern() guarantees + * the array, so the list has exactly one spelling and the read side has + * no second shape to interpret. + */ + appendStringInfoString(str, " :varNames"); + Assert(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, ')'); + + /* + * Write elements array (makeRPRPattern guarantees numElements >= 2). + * Seven fields go out; the reserved byte is padding and stays behind -- + * see RPRPatternElement in plannodes.h. + */ + appendStringInfoString(str, " :elements"); + Assert(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); + } + + WRITE_BOOL_FIELD(isAbsorbable); +} + /* * outNode - diff --git a/src/backend/nodes/queryjumblefuncs.c b/src/backend/nodes/queryjumblefuncs.c index e5f645dc29e..63bb8bdd710 100644 --- a/src/backend/nodes/queryjumblefuncs.c +++ b/src/backend/nodes/queryjumblefuncs.c @@ -79,6 +79,9 @@ static void _jumbleVariableSetStmt(JumbleState *jstate, Node *node); static void _jumbleRangeTblEntry_eref(JumbleState *jstate, RangeTblEntry *rte, Alias *expr); +static void _jumbleWindowClause_defineClause(JumbleState *jstate, + WindowClause *wc, + List *defineClause); /* * Given a possibly multi-statement source string, confine our attention to the @@ -774,6 +777,33 @@ _jumbleRangeTblEntry_eref(JumbleState *jstate, JUMBLE_STRING(aliasname); } +/* + * Custom query jumble function for WindowClause.defineClause. + * + * The DEFINE clause is stored as a list of TargetEntry, one per row pattern + * variable, whose resname is the variable being defined. TargetEntry.resname + * is marked as query_jumble_ignore, however in a DEFINE clause, the resname + * is semantically significant: it binds the condition to a PATTERN variable. + * "DEFINE A AS p > 50, B AS p < 50" and "DEFINE B AS p > 50, A AS p < 50" + * are different queries and must get different query ids. Therefore jumble + * TargetEntry.resname is necessary for WindowClause.defineClause. + */ +static void +_jumbleWindowClause_defineClause(JumbleState *jstate, + WindowClause *wc, + List *defineClause) +{ + /* + * Jumble the list as the generated code would, so that a window without a + * DEFINE clause keeps its query id. + */ + _jumbleNode(jstate, (Node *) defineClause); + + /* Then add the variable names, which TargetEntry.resname hides. */ + foreach_node(TargetEntry, expr, defineClause) + JUMBLE_STRING(resname); +} + /* * CompLocation: comparator for qsorting LocationLen structs by location */ diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index 2839a711f9e..b8f905aaa91 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -558,6 +558,98 @@ _readExtensibleNode(ReadNodeContext *ctx) READ_DONE(); } +static RPRPattern * +_readRPRPattern(ReadNodeContext *ctx) +{ + READ_LOCALS(RPRPattern); + + READ_INT_FIELD(numVars); + READ_INT_FIELD(maxDepth); + READ_INT_FIELD(numElements); + + /* + * Read varNames array. _outRPRPattern() always writes the list, so every + * token here has one spelling and any other input is malformed. The + * delimiters are checked rather than counted on, because a numVars that + * disagrees with the list would otherwise leave the token stream off by + * one for everything that follows. + */ + token = pg_strtok(ctx, &length); /* skip :varNames */ + token = pg_strtok(ctx, &length); /* get '(' */ + if (local_node->numVars <= 0 || token == NULL || token[0] != '(') + elog(ERROR, "unexpected varNames in RPRPattern"); + local_node->varNames = palloc_array(char *, local_node->numVars); + for (int i = 0; i < local_node->numVars; i++) + { + token = pg_strtok(ctx, &length); + if (token == NULL) + elog(ERROR, "unexpected end of RPRPattern varNames"); + local_node->varNames[i] = nullable_string(token, length); + } + token = pg_strtok(ctx, &length); /* get ')' */ + if (token == NULL || token[0] != ')') + elog(ERROR, "unterminated varNames in RPRPattern"); + + /* Read elements array */ + token = pg_strtok(ctx, &length); /* skip :elements */ + token = pg_strtok(ctx, &length); /* get '(' */ + /* out always emits the array (makeRPRPattern guarantees numElements >= 2) */ + if (local_node->numElements <= 0 || token == NULL || token[0] != '(') + elog(ERROR, "unexpected elements in RPRPattern"); + /* palloc0 also zeroes reserved, which the round trip drops */ + 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, + depth, + min, + max, + next, + jump; + unsigned int flags; /* written with %u, unlike the others */ + + /* Parse "(varId depth flags min max next jump)" */ + token = pg_strtok(ctx, &length); + varId = atoi(token); + token = pg_strtok(ctx, &length); + depth = atoi(token); + token = pg_strtok(ctx, &length); + flags = atoui(token); + token = pg_strtok(ctx, &length); + min = atoi(token); + token = pg_strtok(ctx, &length); + max = atoi(token); + token = pg_strtok(ctx, &length); + next = atoi(token); + token = pg_strtok(ctx, &length); + jump = atoi(token); + token = pg_strtok(ctx, &length); /* get ')' */ + if (token == NULL || token[0] != ')') + elog(ERROR, "unterminated element in RPRPattern"); + + 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(ctx, &length); /* get '(' */ + if (token == NULL || token[0] != '(') + elog(ERROR, "unexpected end of RPRPattern elements"); + } + } + + READ_BOOL_FIELD(isAbsorbable); + + READ_DONE(); +} + /* * parseNodeString diff --git a/src/backend/parser/Makefile b/src/backend/parser/Makefile index 8c0fe28d63f..f4c7d605fe3 100644 --- a/src/backend/parser/Makefile +++ b/src/backend/parser/Makefile @@ -29,6 +29,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 924ee87a453..a07102b37a0 100644 --- a/src/backend/parser/meson.build +++ b/src/backend/parser/meson.build @@ -16,6 +16,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 a5424c9d24c..bb380114ec3 100644 --- a/src/backend/parser/parse_agg.c +++ b/src/backend/parser/parse_agg.c @@ -585,6 +585,10 @@ check_agglevels_and_constraints(ParseState *pstate, Node *expr) errkind = true; break; + case EXPR_KIND_RPR_DEFINE: + errkind = true; + break; + /* * There is intentionally no default: case here, so that the * compiler will warn if we add a new ParseExprKind without @@ -1024,6 +1028,9 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc, case EXPR_KIND_CYCLE_MARK: errkind = true; break; + case EXPR_KIND_RPR_DEFINE: + errkind = true; + break; /* * There is intentionally no default: case here, so that the @@ -1104,7 +1111,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; @@ -1323,6 +1331,38 @@ parseCheckAggregates(ParseState *pstate, Query *qry) have_non_var_grouping, &func_grouped_rels); + /* + * A row pattern DEFINE clause is the one part of a WindowClause holding + * an expression tree of its own, so it needs the substitution too: + * partitionClause and orderClause carry just a sortgroupref into the + * target list, and the frame offsets are checked to be Var-free. Without + * this its Vars would stay plain relation Vars while the target list + * copies of the same columns become Vars of the RTE_GROUP RTE, and a + * grouping set that nulls one of those columns would make the two copies + * disagree in varnullingrels, which setrefs.c reports as an internal + * error. + * + * No finalize_grouping_exprs() goes with it. That call finalizes + * GROUPING expressions, and a DEFINE clause cannot hold one -- + * transformExpr() rejects a GroupingFunc under EXPR_KIND_RPR_DEFINE + * before we get here. + */ + foreach_node(WindowClause, wc, qry->windowClause) + { + if (wc->defineClause == NIL) + continue; + + clause = (Node *) wc->defineClause; + if (hasJoinRTEs) + clause = flatten_join_alias_for_parser(qry, clause, 0); + wc->defineClause = (List *) + substitute_grouped_columns(clause, pstate, qry, + groupClauses, groupClauseCommonVars, + gset_common, + have_non_var_grouping, + &func_grouped_rels); + } + /* * Per spec, aggregates can't appear in a recursive term. */ diff --git a/src/backend/parser/parse_clause.c b/src/backend/parser/parse_clause.c index 7a5179b9053..16217f0ac9f 100644 --- a/src/backend/parser/parse_clause.c +++ b/src/backend/parser/parse_clause.c @@ -37,6 +37,7 @@ #include "parser/parse_func.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" @@ -2829,6 +2830,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; @@ -2957,6 +2960,10 @@ transformWindowDefinitions(ParseState *pstate, rangeopfamily, rangeopcintype, &wc->endInRangeFunc, windef->endOffset); + + /* Process Row Pattern Recognition related clauses */ + transformRPR(pstate, wc, windef); + wc->winref = winref; result = lappend(result, wc); diff --git a/src/backend/parser/parse_cte.c b/src/backend/parser/parse_cte.c index 73e86b7422f..0d80b8b00c0 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 = palloc0_array(CteItem, cstate.numitems); @@ -1268,3 +1298,27 @@ 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. + */ +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 05a6b72d4c9..94193cfcdd3 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -577,6 +577,7 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref) case EXPR_KIND_COPY_WHERE: case EXPR_KIND_GENERATED_COLUMN: case EXPR_KIND_CYCLE_MARK: + case EXPR_KIND_RPR_DEFINE: /* okay */ break; @@ -612,6 +613,67 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref) return node; } + /*---------- + * A pattern variable qualifier (e.g. UP.price) is valid per ISO/IEC + * 19075-5 6.15 / 4.16 but not yet implemented, and has to be recognized + * here: a pattern variable names no range table entry, so leaving it to + * normal resolution would reject it as a qualified expression instead. + * + * Like every other rule below, this one only reaches names the ref hooks + * left for the query parser to resolve. A PL that answers a name first + * keeps it: under "#variable_conflict use_variable" PL/pgSQL claims any + * name one of its variables owns, so a PL/pgSQL variable sharing a name + * with a pattern variable takes A.price, exactly as it takes a name a + * table column would otherwise own. That is what asking for + * use_variable means, and the DEFINE rules do not override it. + * + * Only a two-part name has that shape. A longer name whose first part + * happens to spell a pattern variable is schema- or catalog-qualified, + * and goes the way of the other qualified forms. + * + * The other qualified forms DEFINE disallows are diagnosed after the + * reference resolves, below. Classifying them here on the qualifier + * alone would report a misspelled column as a problem with the qualifier + * and lose the "Perhaps you meant" hint normal resolution offers. + * + * 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_rpr_define && + list_length(cref->fields) != 1) + { + if (list_length(cref->fields) == 2) + { + char *qualifier = strVal(linitial(cref->fields)); + + foreach_node(String, pv, pstate->p_rpr_pattern_vars) + { + if (strcmp(strVal(pv), qualifier) == 0) + 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)); + } + } + + /* + * A whole-row reference is barred by its form alone: no qualifier + * makes one legal here, so resolving it first would only choose which + * rejection it gets. + */ + if (IsA(llast(cref->fields), A_Star)) + ereport(ERROR, + errcode(ERRCODE_SYNTAX_ERROR), + errmsg("whole-row reference is not allowed in DEFINE clause"), + errhint("A DEFINE condition may reference individual columns only."), + parser_errposition(pstate, cref->location)); + } + /*---------- * The allowed syntaxes are: * @@ -663,8 +725,22 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref) cref->location, &levels_up); if (nsitem) + { + /* + * A lone name that resolves as a range variable is a + * whole-row reference without the star, which the + * rule above has no A_Star to match on. + */ + if (pstate->p_rpr_define) + ereport(ERROR, + errcode(ERRCODE_SYNTAX_ERROR), + errmsg("whole-row reference is not allowed in DEFINE clause"), + errhint("A DEFINE condition may reference individual columns only."), + parser_errposition(pstate, cref->location)); + node = transformWholeRowRef(pstate, nsitem, levels_up, cref->location); + } } break; } @@ -859,6 +935,25 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref) errorMissingColumn(pstate, relname, colname, cref->location); break; case CRERR_NO_RTE: + + /* + * ISO/IEC 19075-5 6.5 reserves the qualifier slot in a DEFINE + * clause for a row pattern variable, so a qualifier naming + * nothing is rejected for occupying it, the same as one that + * names something. Reporting a missing FROM-clause entry + * would point at a repair that does not exist: adding the + * relation only moves the reference to one of the rejections + * below, the range variable one for a two-part name and the + * qualified expression one for a longer name, or the outer + * column one if the relation is added to an outer query. + */ + if (pstate->p_rpr_define) + ereport(ERROR, + errcode(ERRCODE_SYNTAX_ERROR), + errmsg("qualified expression \"%s\" is not allowed in DEFINE clause", + NameListToString(cref->fields)), + parser_errposition(pstate, cref->location)); + errorMissingRTE(pstate, makeRangeVar(nspname, relname, cref->location)); break; @@ -879,6 +974,88 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref) } } + /* + * Restrict column references in a row pattern DEFINE clause. node is now + * a successfully resolved reference, so the qualified forms RPR does not + * allow can be rejected without mistaking a name that does not resolve at + * all for one of them: a correlated reference to an outer query's column, + * a range variable qualifier, and a schema/catalog-qualified reference. + * + * The error class follows the division the neighbouring restrictions use: + * ERRCODE_FEATURE_NOT_SUPPORTED for what the standard allows and this + * implementation does not, ERRCODE_SYNTAX_ERROR for every other rejected + * spelling, whether the standard forbids it or never gave it at all. + */ + if (pstate->p_rpr_define) + { + ParseNamespaceItem *qual_nsitem = NULL; + int qual_levels_up = 0; + + if (list_length(cref->fields) == 2) + qual_nsitem = refnameNamespaceItem(pstate, NULL, + strVal(linitial(cref->fields)), + cref->location, + &qual_levels_up); + + /* + * Ask which level the qualifier resolved at, not merely whether it + * resolved. A two-part name whose second part is not a column is + * retried as a function call on the whole row, and that yields a + * FuncExpr rather than a Var, so the outer reference its argument + * carries is invisible to the varlevelsup test. + * + * The level counts levels searched, not levels found, so it means + * nothing unless the search succeeded. + */ + if ((IsA(node, Var) && ((Var *) node)->varlevelsup > 0) || + (qual_nsitem != NULL && qual_levels_up > 0)) + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot use outer query column in DEFINE clause"), + parser_errposition(pstate, cref->location)); + + if (qual_nsitem != 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)); + + /* + * ISO/IEC 19075-5 6.5 reserves the qualifier slot for a row pattern + * variable, so a name is rejected for occupying it whatever the + * qualifier turns out to name. What is left here resolved through + * p_post_columnref_hook, which reads a two-part name as a routine's + * parameter or variable, or as a field of a composite one, and the + * hook is public enough that an extension may add readings of its + * own; the message names none of them. Selecting a field from an + * unqualified value, written "(x).f", occupies no qualifier slot and + * remains the way to reach a composite. + * + * The pre hook's readings never arrive here, so this rule is not the + * last word on a qualified name: in a PL/pgSQL function written with + * "#variable_conflict use_variable", plpgsql_pre_column_ref() answers + * fn.var and rec.field itself and returns before any of this runs, + * leaving those spellings usable there. The pragma redirects name + * resolution wholesale -- it takes names a table column would + * otherwise own too -- and DEFINE does not carve itself out of it. + */ + if (list_length(cref->fields) == 2) + ereport(ERROR, + errcode(ERRCODE_SYNTAX_ERROR), + errmsg("qualified expression \"%s\" is not allowed in DEFINE clause", + NameListToString(cref->fields)), + errhint("Write the name without its qualifier, or write \"(x).field\" to select a field of a composite value."), + 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; } @@ -1790,9 +1967,13 @@ transformSubLink(ParseState *pstate, SubLink *sublink) * Check to see if the sublink is in an invalid place within the query. We * allow sublinks everywhere in SELECT/INSERT/UPDATE/DELETE/MERGE, but * generally not in utility statements. + * + * A row pattern DEFINE condition rejects a sublink wherever in the + * condition it stands, so its scope is asked about ahead of p_expr_kind, + * which by here may name a clause nested in the condition instead. */ err = NULL; - switch (pstate->p_expr_kind) + switch (pstate->p_rpr_define ? EXPR_KIND_RPR_DEFINE : pstate->p_expr_kind) { case EXPR_KIND_NONE: Assert(false); /* can't happen */ @@ -1872,6 +2053,39 @@ transformSubLink(ParseState *pstate, SubLink *sublink) err = _("cannot use subquery in column generation 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 rejects the SubLink, which is not the same as keeping + * the subquery unanalyzed: a construct that analyzes its + * query before building the SubLink, as + * transformJsonArrayQueryConstructor() does, has already + * resolved names and opened relations inside it by the time + * we get here, and reports its own errors first. Whoever + * implements (a) and (b) must not read this rejection as + * proof that nothing inside a DEFINE subquery runs. + *---------- + */ + 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 @@ -3230,6 +3444,8 @@ ParseExprKindName(ParseExprKind exprKind) return "GENERATED AS"; case EXPR_KIND_CYCLE_MARK: return "CYCLE"; + 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 410e54accd8..e688009e6bb 100644 --- a/src/backend/parser/parse_func.c +++ b/src/backend/parser/parse_func.c @@ -48,6 +48,9 @@ static void unify_hypothetical_args(ParseState *pstate, List *fargs, int numAggregatedArgs, Oid *actual_arg_types, Oid *declared_arg_types); static Oid FuncNameAsType(List *funcname); +static Node *ParseRPRNavCall(ParseState *pstate, List *funcname, + List *fargs, List *argnames, FuncCall *fn, + int location); static Node *ParseComplexProjection(ParseState *pstate, const char *funcname, Node *first_arg, int location); static Oid LookupFuncNameInternal(ObjectType objtype, List *funcname, @@ -121,6 +124,7 @@ ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs, int fgc_flags; char aggkind = 0; ParseCallbackState pcbstate; + bool could_be_rpr_nav = false; /* * If there's an aggregate filter, transform it using transformWhereClause @@ -217,6 +221,23 @@ ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs, Assert(first_arg != NULL); } + /* + * Inside an RPR DEFINE clause, an unqualified call to one of the row + * pattern navigation names PREV/NEXT/FIRST/LAST denotes the navigation + * operation, not an ordinary function. Just note that here; the catalog + * lookup is skipped and the RPRNavExpr is built at the end, after the + * common decoration checks have run (see the could_be_rpr_nav handling + * below). A schema-qualified call is the explicit way to reach an + * ordinary function of one of these names. + */ + if (!is_column && !proc_call && + pstate->p_rpr_define && + list_length(funcname) == 1) + { + if (is_rpr_navigation_name(strVal(linitial(funcname)))) + could_be_rpr_nav = true; + } + /* * Decide whether it's legitimate to consider the construct to be a column * projection. For that, there has to be a single argument of complex @@ -265,17 +286,32 @@ ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs, * with default arguments. */ - setup_parser_errposition_callback(&pcbstate, pstate, location); + if (!could_be_rpr_nav) + { + setup_parser_errposition_callback(&pcbstate, pstate, location); + + fdresult = func_get_detail(funcname, fargs, argnames, nargs, + actual_arg_types, + !func_variadic, true, proc_call, + &fgc_flags, + &funcid, &rettype, &retset, + &nvargs, &vatype, + &declared_arg_types, &argdefaults); - fdresult = func_get_detail(funcname, fargs, argnames, nargs, - actual_arg_types, - !func_variadic, true, proc_call, - &fgc_flags, - &funcid, &rettype, &retset, - &nvargs, &vatype, - &declared_arg_types, &argdefaults); + cancel_parser_errposition_callback(&pcbstate); + } + else + { + /* + * A recognized navigation name skips catalog lookup entirely. Treat + * it as an ordinary function so the common wrong-kind-of-routine and + * decoration checks below run with the existing messages, then route + * to ParseRPRNavCall to build the RPRNavExpr. + */ + Assert(!proc_call); - cancel_parser_errposition_callback(&pcbstate); + fdresult = FUNCDETAIL_NORMAL; + } /* * Check for various wrong-kind-of-routine cases. @@ -656,6 +692,15 @@ ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs, parser_errposition(pstate, location))); } + /* + * A recognized navigation name has now passed the common decoration and + * wrong-kind checks above; build the RPRNavExpr. No fallback to function + * resolution ever happens here. + */ + if (could_be_rpr_nav) + return ParseRPRNavCall(pstate, funcname, fargs, argnames, fn, + location); + /* * If there are default arguments, we have to include their types in * actual_arg_types for the purpose of checking generic type consistency. @@ -2089,6 +2134,173 @@ FuncNameAsType(List *funcname) return result; } +/* + * is_rpr_navigation_name + * Is this unqualified, parser-downcased name a row pattern navigation + * operation inside a DEFINE clause? + * + * ruleutils.c asks the same question to decide when a user function of one + * of these names has to be printed schema-qualified, so both sides share the + * one list. + */ +bool +is_rpr_navigation_name(const char *name) +{ + return strcmp(name, "prev") == 0 || + strcmp(name, "next") == 0 || + strcmp(name, "first") == 0 || + strcmp(name, "last") == 0; +} + +/* + * ParseRPRNavCall + * Recognize a row pattern navigation operation in a DEFINE clause. + * + * Anywhere inside a DEFINE condition (p_rpr_define is set, even where a + * nested FILTER or aggregate ORDER BY has changed p_expr_kind) an unqualified + * call to one of the names PREV/NEXT/FIRST/LAST denotes the corresponding row + * pattern navigation operation (ISO/IEC 19075-5 Subclause 5.6), not an + * ordinary function call. + * The name is matched here, before any catalog lookup, with no fallback to + * function resolution: once it matches, decoration and argument-count + * violations are hard errors rather than letting an ordinary function of + * the same name take over. A schema-qualified call (the caller restricts us + * to unqualified names) is the documented way to reach such a function + * instead. + * + * The caller routes here only after the name has matched one of the four + * navigation names and the common decoration/wrong-kind checks in + * ParseFuncOrColumn have run, so this always returns an RPRNavExpr. + */ +static Node * +ParseRPRNavCall(ParseState *pstate, List *funcname, List *fargs, + List *argnames, FuncCall *fn, int location) +{ + const char *name = strVal(linitial(funcname)); + RPRNavKind kind; + const char *navname; + int nargs = list_length(fargs); + Node *arg; + RPRNavExpr *navexpr; + + /* match the parser-downcased identifier; otherwise not a navigation name */ + if (strcmp(name, "prev") == 0) + { + kind = RPR_NAV_PREV; + navname = "PREV"; + } + else if (strcmp(name, "next") == 0) + { + kind = RPR_NAV_NEXT; + navname = "NEXT"; + } + else if (strcmp(name, "first") == 0) + { + kind = RPR_NAV_FIRST; + navname = "FIRST"; + } + else if (strcmp(name, "last") == 0) + { + kind = RPR_NAV_LAST; + navname = "LAST"; + } + else + { + /* the caller only routes here after matching one of the four names */ + pg_unreachable(); + return NULL; + } + + /* + * Once the name matches we never fall back to function resolution, so any + * decoration that does not make sense for a navigation operation is a + * hard error. The aggregate/window decorations (agg_star, DISTINCT, + * WITHIN GROUP, ORDER BY, FILTER, OVER, RESPECT/IGNORE NULLS) are already + * rejected by the common path in ParseFuncOrColumn, which treated the + * recognized name as an ordinary function; what remains are the + * decorations that path accepts for a plain function but a navigation + * operation must still reject. + */ + if (fn->func_variadic) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("cannot use VARIADIC with row pattern navigation function %s", + navname), + parser_errposition(pstate, location))); + if (argnames != NIL) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("cannot use named arguments with row pattern navigation function %s", + navname), + parser_errposition(pstate, location))); + /* takes a value expression and an optional offset */ + if (nargs == 0) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("too few arguments for row pattern navigation function %s", + navname), + errdetail("%s takes a value expression and an optional offset argument.", + navname), + parser_errposition(pstate, location))); + if (nargs > 2) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("too many arguments for row pattern navigation function %s", + navname), + errdetail("%s takes a value expression and an optional offset argument.", + navname), + parser_errposition(pstate, location))); + + /* + * Resolve a still-unknown first argument to text, the same way the + * anycompatible family does. A navigation operation is not a polymorphic + * function, so the old "could not determine polymorphic type" error does + * not apply; an unknown literal cannot contain a column reference, so the + * walker still rejects it later. + */ + arg = linitial(fargs); + if (exprType(arg) == UNKNOWNOID) + arg = coerce_to_common_type(pstate, arg, TEXTOID, navname); + + navexpr = makeNode(RPRNavExpr); + navexpr->kind = kind; + navexpr->arg = (Expr *) arg; + navexpr->navno = -1; /* assigned while planning */ + + /* an explicit offset is coerced to int8, which the executor reads */ + if (nargs == 2) + { + Node *offset = lsecond(fargs); + Oid offtype = exprType(offset); + + if (offtype != INT8OID) + { + Node *newoffset; + + newoffset = coerce_to_target_type(pstate, offset, offtype, + INT8OID, -1, COERCION_IMPLICIT, + COERCE_IMPLICIT_CAST, -1); + if (newoffset == NULL) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("offset argument of %s must be type %s, not type %s", + navname, "bigint", format_type_be(offtype)), + parser_errposition(pstate, exprLocation(offset)))); + offset = newoffset; + } + navexpr->offset_arg = (Expr *) offset; + } + else + navexpr->offset_arg = NULL; + + /* compound_offset_arg stays NULL; define_walker flattening fills it in */ + navexpr->resulttype = exprType(arg); + /* resultcollid will be set by parse_collate.c */ + navexpr->location = location; + + return (Node *) navexpr; +} + /* * ParseComplexProjection - * handles function calls with a single argument that is of complex type. @@ -2842,6 +3054,9 @@ check_srf_call_placement(ParseState *pstate, Node *last_srf, int location) case EXPR_KIND_CYCLE_MARK: errkind = true; 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..63a4acbeb19 --- /dev/null +++ b/src/backend/parser/parse_rpr.c @@ -0,0 +1,561 @@ +/*------------------------------------------------------------------------- + * + * 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 (ROWS only, must start at CURRENT ROW, no + * EXCLUDE, and CURRENT ROW is not accepted as the frame end) + * - Validates PATTERN variable count (max RPR_VARID_MAX + 1) + * - Transforms DEFINE clause + * - Stores the PATTERN parse tree and the AFTER MATCH SKIP TO flag + * + * 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/rpr.h" +#include "parser/parse_clause.h" +#include "parser/parse_rpr.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 **varNames); +static List *transformDefineClause(ParseState *pstate, WindowDef *windef); +static bool define_walker(Node *node, void *context); +static bool rpr_frame_is_supported(int frameOptions); + +/* + * transformRPR + * Process Row Pattern Recognition related clauses. + * + * Validates and transforms RPR clauses from parse tree to planner structures: + * - Validates frame options (ROWS only, must start at CURRENT ROW, no + * EXCLUDE, and CURRENT ROW is not accepted as the frame end) + * - Set AFTER MATCH SKIP TO flag + * - Transforms DEFINE clause into TargetEntry list + * - Stores PATTERN parse tree for deparsing (optimization happens in planner) + * + * Returns early if windef has no rpCommonSyntax (non-RPR window). + */ +void +transformRPR(ParseState *pstate, WindowClause *wc, WindowDef *windef) +{ + /* Nothing to do unless the window carries a row pattern */ + if (windef->rpCommonSyntax == NULL) + return; + + if (!rpr_frame_is_supported(wc->frameOptions)) + { + /* + * The frame type keyword beats the start of the window definition, + * which is all a defaulted frame leaves to point at. + */ + int location = windef->frameLocation >= 0 ? + windef->frameLocation : windef->location; + + ereport(ERROR, + errcode(ERRCODE_WINDOWING_ERROR), + errmsg("unsupported frame for row pattern recognition"), + /*- translator: both %s are SQL window frame specifications */ + errdetail("The frame must be \"%s\" or \"%s\".", + "ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING", + "ROWS BETWEEN CURRENT ROW AND offset FOLLOWING"), + parser_errposition(pstate, location)); + } + + /* + * EXCLUDE is not part of the frame shape, so it is reported on its own, + * and from its own location. + */ + if (wc->frameOptions & FRAMEOPTION_EXCLUSION) + { + int location = windef->excludeLocation >= 0 ? + windef->excludeLocation : windef->location; + + ereport(ERROR, + errcode(ERRCODE_WINDOWING_ERROR), + errmsg("cannot use EXCLUDE with row pattern recognition"), + parser_errposition(pstate, location)); + } + + Assert(wc->frameOptions & FRAMEOPTION_ROWS); + + /* Assign AFTER MATCH SKIP TO flag */ + wc->rpSkipTo = windef->rpCommonSyntax->rpSkipTo; + + /* Transform DEFINE clause into list of TargetEntry's */ + wc->defineClause = transformDefineClause(pstate, windef); + + /* Store PATTERN parse tree for deparsing */ + wc->rpPattern = windef->rpCommonSyntax->rpPattern; +} + +/* + * rpr_frame_is_supported + * Is this the frame shape row pattern recognition matches over? + * + * Only ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING and ROWS BETWEEN + * CURRENT ROW AND offset FOLLOWING are supported. EXCLUDE is rejected by + * the caller, since it is not part of the shape. + * + * The offset's value is not settled until execution; + * calculate_frame_offsets() rejects a non-positive one there. + */ +static bool +rpr_frame_is_supported(int frameOptions) +{ + if ((frameOptions & FRAMEOPTION_ROWS) == 0) + return false; + if ((frameOptions & FRAMEOPTION_START_CURRENT_ROW) == 0) + return false; + if ((frameOptions & (FRAMEOPTION_END_UNBOUNDED_FOLLOWING | + FRAMEOPTION_END_OFFSET_FOLLOWING)) == 0) + return false; + + return true; +} + +/* + * 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. + * + * varNames collects the unique PATTERN variable names, which is what + * transformColumnRef checks via p_rpr_pattern_vars to identify pattern + * variable qualifiers. Cross-checking DEFINE variable names against this + * list is the caller's responsibility, since it only needs to run once. + */ +static void +validateRPRPatternVarCount(ParseState *pstate, RPRPatternNode *node, + List **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 row pattern variables"), + errdetail("The maximum number of row pattern variables 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, varNames); + } + break; + } +} + +/* + * transformDefineClause + * Process DEFINE clause and transform ResTarget into list of TargetEntry. + * + * 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, WindowDef *windef) +{ + List *defineClause = NIL; + List *patternVarNames = NIL; + + /* + * The grammar builds an RPCommonSyntax only for a window specification + * that carries DEFINE, so the list is never empty here. + */ + Assert(windef->rpCommonSyntax->rpDefs != NULL); + + /* + * Validate PATTERN variable count and collect the PATTERN variable names + * for transformColumnRef. + */ + validateRPRPatternVarCount(pstate, windef->rpCommonSyntax->rpPattern, + &patternVarNames); + pstate->p_rpr_pattern_vars = patternVarNames; + + /* + * Open the DEFINE scope. The restrictions a DEFINE condition is under + * hold for the whole condition, so they are keyed on this rather than on + * p_expr_kind, which names only the innermost clause and is replaced by + * anything nested in the condition that has a kind of its own. It is + * closed below, once every DEFINE expression has been transformed. + */ + pstate->p_rpr_define = true; + + /* + * Reject any DEFINE variable whose name does not appear in PATTERN. This + * cross-check only needs to run once, so it lives here in the caller + * rather than in the recursive validateRPRPatternVarCount(). + */ + foreach_node(ResTarget, rt, windef->rpCommonSyntax->rpDefs) + { + bool found = false; + + foreach_node(String, varname, patternVarNames) + { + if (strcmp(strVal(varname), rt->name) == 0) + { + found = true; + break; + } + } + if (!found) + ereport(ERROR, + errcode(ERRCODE_SYNTAX_ERROR), + errmsg("DEFINE variable \"%s\" is not used in PATTERN", + rt->name), + parser_errposition(pstate, rt->location)); + } + + /* + * Check for duplicate row pattern definition variables. The standard + * requires that no two row pattern definition variable names shall be + * equivalent. Report the error at the later (duplicate) definition. + */ + foreach_node(ResTarget, restarget, windef->rpCommonSyntax->rpDefs) + { + foreach_node(ResTarget, prior, windef->rpCommonSyntax->rpDefs) + { + if (prior == restarget) + break; + if (strcmp(prior->name, restarget->name) == 0) + ereport(ERROR, + errcode(ERRCODE_SYNTAX_ERROR), + errmsg("DEFINE variable \"%s\" appears more than once", + restarget->name), + parser_errposition(pstate, + exprLocation((Node *) restarget))); + } + } + + foreach_node(ResTarget, restarget, windef->rpCommonSyntax->rpDefs) + { + TargetEntry *teDefine; + Node *expr; + + /* + * Transform the DEFINE expression and coerce it to boolean. The + * result belongs in wc->defineClause, never in the query targetlist + * as a whole: it may contain RPRNavExpr nodes (PREV/NEXT/FIRST/LAST) + * that only the owning WindowAgg can evaluate. + */ + expr = transformWhereClause(pstate, restarget->val, + EXPR_KIND_RPR_DEFINE, "DEFINE"); + + /* Build the defineClause entry directly from the transformed expr */ + teDefine = makeTargetEntry((Expr *) expr, + list_length(defineClause) + 1, + pstrdup(restarget->name), + true); + + /* build transformed DEFINE clause (list of TargetEntry) */ + defineClause = lappend(defineClause, teDefine); + } + pstate->p_rpr_define = false; + pstate->p_rpr_pattern_vars = NIL; + + /* + * Validate DEFINE expressions: nested PREV/NEXT, column references, + * compound flatten -- 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); + } + + return defineClause; +} + +/* + * 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) + * - an inner navigation that is not nav.arg itself is + * rejected as not being a direct argument + * - 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) + * - must not contain a row pattern navigation operation + * + * Entering an outer nav, the walker walks nav.arg in PHASE_NAV_ARG to collect + * nesting and column-ref state, flattens a compound form or raises a nesting + * error, then walks the post-flatten offset(s) in PHASE_NAV_OFFSET. A + * compound form's inner offset is walked in both passes: PHASE_NAV_ARG only + * asks whether nav.arg as a whole holds a column reference, so the offset is + * walked again to catch one it would have leaked. + * + * Var sightings feed the column-ref rule for the enclosing nav scope; + * RPRNavExpr sightings inside PHASE_NAV_ARG feed the nesting decision. + * The phases themselves are described where DefinePhase is declared. + */ +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 + * are still observed. + */ + if (ctx->nav_count == 0) + ctx->inner_kind = nav->kind; + ctx->nav_count++; + return expression_tree_walker(node, define_walker, ctx); + } + else if (ctx->phase == DEFINE_PHASE_NAV_OFFSET) + { + /* + * A navigation offset must be a run-time constant, so it cannot + * contain a navigation operation. + */ + ereport(ERROR, + errcode(ERRCODE_SYNTAX_ERROR), + errmsg("row pattern navigation offset cannot contain a row pattern navigation operation"), + errdetail("A navigation offset must be a run-time constant."), + parser_errposition(ctx->pstate, nav->location)); + } + else + { + /* + * PHASE_BODY: this is an outer nav at top level. Walk arg first + * to collect nesting / column-ref state, then validate and (for + * compound forms) flatten, then walk offset(s). + */ + DefineWalkCtx saved = *ctx; + bool outer_phys = (nav->kind == RPR_NAV_PREV || + nav->kind == RPR_NAV_NEXT); + 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 an inner nav that is not the whole argument */ + 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)); + + /* Reject triple-or-deeper nesting; siblings caught above */ + 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)); + + 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_SYNTAX_ERROR), + 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_SYNTAX_ERROR), + 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/backend/parser/parse_target.c b/src/backend/parser/parse_target.c index 728936b502e..441e10fb5c3 100644 --- a/src/backend/parser/parse_target.c +++ b/src/backend/parser/parse_target.c @@ -45,7 +45,7 @@ static Node *transformAssignmentSubscripts(ParseState *pstate, CoercionContext ccontext, int location); static List *ExpandColumnRefStar(ParseState *pstate, ColumnRef *cref, - bool make_target_entry); + bool make_target_entry, bool *expanded); static List *ExpandAllTables(ParseState *pstate, int location); static List *ExpandIndirectionStar(ParseState *pstate, A_Indirection *ind, bool make_target_entry, ParseExprKind exprKind); @@ -147,11 +147,15 @@ transformTargetList(ParseState *pstate, List *targetlist, if (IsA(llast(cref->fields), A_Star)) { + bool expanded; + List *items; + /* It is something.*, expand into multiple items */ - p_target = list_concat(p_target, - ExpandColumnRefStar(pstate, - cref, - true)); + items = ExpandColumnRefStar(pstate, cref, true, + &expanded); + /* only a DEFINE condition declines, and this is not one */ + Assert(expanded); + p_target = list_concat(p_target, items); continue; } } @@ -237,11 +241,22 @@ transformExpressionList(ParseState *pstate, List *exprlist, if (IsA(llast(cref->fields), A_Star)) { - /* It is something.*, expand into multiple items */ - result = list_concat(result, - ExpandColumnRefStar(pstate, cref, - false)); - continue; + bool expanded; + List *items; + + /* + * It is something.*, expand into multiple items -- unless + * ExpandColumnRefStar() declines, which it does for a + * reference a row pattern DEFINE condition may not expand. + * Fall through then and let transformExpr() have the + * reference, which is where that is diagnosed. + */ + items = ExpandColumnRefStar(pstate, cref, false, &expanded); + if (expanded) + { + result = list_concat(result, items); + continue; + } } } else if (IsA(e, A_Indirection)) @@ -250,7 +265,18 @@ transformExpressionList(ParseState *pstate, List *exprlist, if (IsA(llast(ind->indirection), A_Star)) { - /* It is something.*, expand into multiple items */ + /* + * It is something.*, expand into multiple items. + * + * No DEFINE test is needed here, unlike the ColumnRef arm + * above. ExpandIndirectionStar() transforms the + * parenthesized argument with p_rpr_define still set, so + * transformColumnRef() still rejects a range variable before + * it becomes a whole-row reference; what survives is field + * selection on a value, "(x).*", which occupies no qualifier + * slot and is allowed in DEFINE for the same reason "(x).f" + * is. + */ result = list_concat(result, ExpandIndirectionStar(pstate, ind, false, exprKind)); @@ -1119,14 +1145,22 @@ checkInsertTargets(ParseState *pstate, List *cols, List **attrnos) * expressions). * * The referenced columns are marked as requiring SELECT access. + * + * *expanded is set false, and NIL returned, if the reference is one this + * refuses to expand; the caller is then to leave it to transformExpr(). A + * row pattern DEFINE condition is the only thing that brings that about, and + * the DEFINE branch below says why. It is a separate flag because NIL is + * also what expanding a relation with no columns of its own returns. */ static List * ExpandColumnRefStar(ParseState *pstate, ColumnRef *cref, - bool make_target_entry) + bool make_target_entry, bool *expanded) { List *fields = cref->fields; int numnames = list_length(fields); + *expanded = true; + if (numnames == 1) { /* @@ -1246,6 +1280,32 @@ ExpandColumnRefStar(ParseState *pstate, ColumnRef *cref, } } + /* + * Both hooks have had their shot, so what is left is a reference to a + * FROM-clause relation, or a name that resolves to nothing at all. A + * row pattern DEFINE condition may have neither, and expanding one + * here binds it by RTE rather than by name, past the checks in + * transformColumnRef(). Decline, so that the caller hands the whole + * reference to transformExpr() and it is diagnosed there, where every + * other DEFINE spelling is: a pattern variable as the qualifier it + * reserves, and any other name, resolved or not, as the whole-row + * reference its form makes it. + * + * A name a hook owns has returned above, which is the point of + * deciding here rather than in the caller. Withholding the expansion + * would not reject such a name -- none of those checks has anything + * to say about one the query parser never resolves -- it would leave + * transformColumnRef() to read "rec.*" as the single whole value + * "rec", which is a different condition rather than a refused one, + * and differs silently wherever a row constructor is not counting its + * entries. + */ + if (pstate->p_rpr_define) + { + *expanded = false; + return NIL; + } + /* * Throw error if no translation found. */ diff --git a/src/fe_utils/psqlscan.l b/src/fe_utils/psqlscan.l index 76819112ba1..134143637db 100644 --- a/src/fe_utils/psqlscan.l +++ b/src/fe_utils/psqlscan.l @@ -308,7 +308,7 @@ not_equals "!=" * If you change either set, adjust the character lists appearing in the * rule for "operator"! */ -self [,()\[\].;\:\+\-\*\/\%\^\<\>\=] +self [,()\[\].;\:\|\+\-\*\/\%\^\<\>\=] op_chars [\~\!\@\#\^\&\|\`\?\+\-\*\/\%\<\>\=] operator {op_chars}+ diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 726c186c7d2..b2521cf2fa8 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -644,6 +644,65 @@ 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, /* offset default: 1 */ + RPR_NAV_NEXT, /* offset default: 1 */ + RPR_NAV_FIRST, /* offset default: 0 */ + RPR_NAV_LAST, /* offset default: 0 */ + /* compound: outer(inner(arg)) */ + RPR_NAV_PREV_FIRST, /* (offset, compound_offset) default: (0, 1) */ + RPR_NAV_PREV_LAST, /* (offset, compound_offset) default: (0, 1) */ + RPR_NAV_NEXT_FIRST, /* (offset, compound_offset) default: (0, 1) */ + RPR_NAV_NEXT_LAST, /* (offset, compound_offset) default: (0, 1) */ +} 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 for its default */ + + /* unique ID within the WindowAgg; -1 until the planner assigns it */ + int navno pg_node_attr(query_jumble_ignore); + + /* result type (same as arg's type) */ + Oid resulttype pg_node_attr(query_jumble_ignore); + /* OID of collation of result */ + Oid resultcollid pg_node_attr(query_jumble_ignore); + /* token location, or -1 if unknown */ + ParseLoc location; +} RPRNavExpr; + /* * MergeSupportFunc * diff --git a/src/include/parser/parse_func.h b/src/include/parser/parse_func.h index cbff17abee0..ef9fa5cb06e 100644 --- a/src/include/parser/parse_func.h +++ b/src/include/parser/parse_func.h @@ -72,4 +72,6 @@ extern Oid LookupFuncWithArgs(ObjectType objtype, ObjectWithArgs *func, extern void check_srf_call_placement(ParseState *pstate, Node *last_srf, int location); +extern bool is_rpr_navigation_name(const char *name); + #endif /* PARSE_FUNC_H */ diff --git a/src/include/parser/parse_rpr.h b/src/include/parser/parse_rpr.h new file mode 100644 index 00000000000..ff5037e585e --- /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); + +#endif /* PARSE_RPR_H */ diff --git a/src/interfaces/ecpg/preproc/pgc.l b/src/interfaces/ecpg/preproc/pgc.l index 96eb076ea1f..9dad34e39bb 100644 --- a/src/interfaces/ecpg/preproc/pgc.l +++ b/src/interfaces/ecpg/preproc/pgc.l @@ -346,7 +346,7 @@ not_equals "!=" * If you change either set, adjust the character lists appearing in the * rule for "operator"! */ -self [,()\[\].;\:\+\-\*\/\%\^\<\>\=] +self [,()\[\].;\:\|\+\-\*\/\%\^\<\>\=] op_chars [\~\!\@\#\^\&\|\`\?\+\-\*\/\%\<\>\=] operator {op_chars}+ @@ -947,7 +947,7 @@ cppline {space}*#([^i][A-Za-z]*|{if}|{ifdef}|{ifndef}|{import})((\/\*[^*/]*\*+ * that the "self" rule would have. */ if (nchars == 1 && - strchr(",()[].;:+-*/%^<>=", yytext[0])) + strchr(",()[].;:|+-*/%^<>=", yytext[0])) return yytext[0]; /* -- 2.43.0