diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index c4fd646b999..868f3025d82 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -187,14 +187,16 @@ static RelOptInfo *create_window_paths(PlannerInfo *root, PathTarget *output_target, bool output_target_parallel_safe, WindowFuncLists *wflists, - List *activeWindows); + List *activeWindows, + Node *qualifyQual); static void create_one_window_path(PlannerInfo *root, RelOptInfo *window_rel, Path *path, PathTarget *input_target, PathTarget *output_target, WindowFuncLists *wflists, - List *activeWindows); + List *activeWindows, + Node *qualifyQual); static RelOptInfo *create_distinct_paths(PlannerInfo *root, RelOptInfo *input_rel, PathTarget *target); @@ -1849,7 +1851,8 @@ grouping_planner(PlannerInfo *root, double tuple_fraction, sort_input_target, sort_input_target_parallel_safe, wflists, - activeWindows); + activeWindows, + parse->qualifyQual); /* Fix things up if sort_input_target contains SRFs */ if (parse->hasTargetSRFs) adjust_paths_for_srfs(root, current_rel, @@ -4555,7 +4558,8 @@ create_window_paths(PlannerInfo *root, PathTarget *output_target, bool output_target_parallel_safe, WindowFuncLists *wflists, - List *activeWindows) + List *activeWindows, + Node *qualifyQual) { RelOptInfo *window_rel; ListCell *lc; @@ -4600,7 +4604,8 @@ create_window_paths(PlannerInfo *root, input_target, output_target, wflists, - activeWindows); + activeWindows, + qualifyQual); } /* @@ -4642,12 +4647,20 @@ create_one_window_path(PlannerInfo *root, PathTarget *input_target, PathTarget *output_target, WindowFuncLists *wflists, - List *activeWindows) + List *activeWindows, + Node *qualifyQual) { PathTarget *window_target; ListCell *l; List *topqual = NIL; + /* + * If there's a QUALIFY clause, add it to topqual. + * The QUALIFY clause filters rows after all window functions are computed. + */ + if (qualifyQual) + topqual = list_make1(qualifyQual); + /* * Since each window clause could require a different sort order, we stack * up a WindowAgg node for each clause, with sort steps between them as diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index 7843a0c857e..e4c58c082ce 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -1398,149 +1398,162 @@ count_rowexpr_columns(ParseState *pstate, Node *expr) */ static Query * transformSelectStmt(ParseState *pstate, SelectStmt *stmt, - SelectStmtPassthrough *passthru) + SelectStmtPassthrough *passthru) { - Query *qry = makeNode(Query); - Node *qual; - ListCell *l; - - qry->commandType = CMD_SELECT; - - /* process the WITH clause independently of all else */ - if (stmt->withClause) - { - qry->hasRecursive = stmt->withClause->recursive; - qry->cteList = transformWithClause(pstate, stmt->withClause); - qry->hasModifyingCTE = pstate->p_hasModifyingCTE; - } - - /* Complain if we get called from someplace where INTO is not allowed */ - if (stmt->intoClause) - ereport(ERROR, - (errcode(ERRCODE_SYNTAX_ERROR), - errmsg("SELECT ... INTO is not allowed here"), - parser_errposition(pstate, - exprLocation((Node *) stmt->intoClause)))); - - /* make FOR UPDATE/FOR SHARE info available to addRangeTableEntry */ - pstate->p_locking_clause = stmt->lockingClause; - - /* make WINDOW info available for window functions, too */ - pstate->p_windowdefs = stmt->windowClause; - - /* process the FROM clause */ - transformFromClause(pstate, stmt->fromClause); - - /* transform targetlist */ - qry->targetList = transformTargetList(pstate, stmt->targetList, - EXPR_KIND_SELECT_TARGET); - - /* - * If we're within a PLAssignStmt, do further transformation of the - * targetlist; that has to happen before we consider sorting or grouping. - * Otherwise, mark column origins (which are useless in a PLAssignStmt). - */ - if (passthru) - qry->targetList = transformPLAssignStmtTarget(pstate, qry->targetList, - passthru); - else - markTargetListOrigins(pstate, qry->targetList); - - /* transform WHERE */ - qual = transformWhereClause(pstate, stmt->whereClause, - EXPR_KIND_WHERE, "WHERE"); - - /* initial processing of HAVING clause is much like WHERE clause */ - qry->havingQual = transformWhereClause(pstate, stmt->havingClause, - EXPR_KIND_HAVING, "HAVING"); - - /* - * Transform sorting/grouping stuff. Do ORDER BY first because both - * transformGroupClause and transformDistinctClause need the results. Note - * that these functions can also change the targetList, so it's passed to - * them by reference. - */ - qry->sortClause = transformSortClause(pstate, - stmt->sortClause, - &qry->targetList, - EXPR_KIND_ORDER_BY, - false /* allow SQL92 rules */ ); - - qry->groupClause = transformGroupClause(pstate, - stmt->groupClause, - stmt->groupByAll, - &qry->groupingSets, - &qry->targetList, - qry->sortClause, - EXPR_KIND_GROUP_BY, - false /* allow SQL92 rules */ ); - qry->groupDistinct = stmt->groupDistinct; - qry->groupByAll = stmt->groupByAll; - - if (stmt->distinctClause == NIL) - { - qry->distinctClause = NIL; - qry->hasDistinctOn = false; - } - else if (linitial(stmt->distinctClause) == NULL) - { - /* We had SELECT DISTINCT */ - qry->distinctClause = transformDistinctClause(pstate, - &qry->targetList, - qry->sortClause, - false); - qry->hasDistinctOn = false; - } - else - { - /* We had SELECT DISTINCT ON */ - qry->distinctClause = transformDistinctOnClause(pstate, - stmt->distinctClause, - &qry->targetList, - qry->sortClause); - qry->hasDistinctOn = true; - } - - /* transform LIMIT */ - qry->limitOffset = transformLimitClause(pstate, stmt->limitOffset, - EXPR_KIND_OFFSET, "OFFSET", - stmt->limitOption); - qry->limitCount = transformLimitClause(pstate, stmt->limitCount, - EXPR_KIND_LIMIT, "LIMIT", - stmt->limitOption); - qry->limitOption = stmt->limitOption; - - /* transform window clauses after we have seen all window functions */ - qry->windowClause = transformWindowDefinitions(pstate, - pstate->p_windowdefs, - &qry->targetList); - - /* resolve any still-unresolved output columns as being type text */ - if (pstate->p_resolve_unknowns) - resolveTargetListUnknowns(pstate, qry->targetList); - - qry->rtable = pstate->p_rtable; - qry->rteperminfos = pstate->p_rteperminfos; - qry->jointree = makeFromExpr(pstate->p_joinlist, qual); - - qry->hasSubLinks = pstate->p_hasSubLinks; - qry->hasWindowFuncs = pstate->p_hasWindowFuncs; - qry->hasTargetSRFs = pstate->p_hasTargetSRFs; - qry->hasAggs = pstate->p_hasAggs; - - foreach(l, stmt->lockingClause) - { - transformLockingClause(pstate, qry, - (LockingClause *) lfirst(l), false); - } - - assign_query_collations(pstate, qry); - - /* this must be done after collations, for reliable comparison of exprs */ - if (pstate->p_hasAggs || qry->groupClause || qry->groupingSets || qry->havingQual) - parseCheckAggregates(pstate, qry); - - return qry; + Query *qry = makeNode(Query); + Node *qual; + ListCell *l; + + qry->commandType = CMD_SELECT; + + /* process the WITH clause independently of all else */ + if (stmt->withClause) + { + qry->hasRecursive = stmt->withClause->recursive; + qry->cteList = transformWithClause(pstate, stmt->withClause); + qry->hasModifyingCTE = pstate->p_hasModifyingCTE; + } + + /* Complain if we get called from someplace where INTO is not allowed */ + if (stmt->intoClause) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SELECT ... INTO is not allowed here"), + parser_errposition(pstate, + exprLocation((Node *) stmt->intoClause)))); + + /* make FOR UPDATE/FOR SHARE info available to addRangeTableEntry */ + pstate->p_locking_clause = stmt->lockingClause; + + /* make WINDOW info available for window functions, too */ + pstate->p_windowdefs = stmt->windowClause; + + /* process the FROM clause */ + transformFromClause(pstate, stmt->fromClause); + + /* transform targetlist */ + qry->targetList = transformTargetList(pstate, stmt->targetList, + EXPR_KIND_SELECT_TARGET); + pstate->p_targetList = qry->targetList; + + /* + * If we're within a PLAssignStmt, do further transformation of the + * targetlist; that has to happen before we consider sorting or grouping. + * Otherwise, mark column origins (which are useless in a PLAssignStmt). + */ + if (passthru) + qry->targetList = transformPLAssignStmtTarget(pstate, qry->targetList, + passthru); + else + markTargetListOrigins(pstate, qry->targetList); + + /* transform WHERE */ + qual = transformWhereClause(pstate, stmt->whereClause, + EXPR_KIND_WHERE, "WHERE"); + + /* initial processing of HAVING clause is much like WHERE clause */ + qry->havingQual = transformWhereClause(pstate, stmt->havingClause, + EXPR_KIND_HAVING, "HAVING"); + + /* + * The QUALIFY clause is much like HAVING, but for window functions. + * Using EXPR_KIND_QUALIFY allows specific checks (e.g., allowing window funcs). + */ + qry->qualifyQual = transformWhereClause(pstate, stmt->qualifyClause, + EXPR_KIND_QUALIFY, "QUALIFY"); + + /* + * Transform sorting/grouping stuff. Do ORDER BY first because both + * transformGroupClause and transformDistinctClause need the results. Note + * that these functions can also change the targetList, so it's passed to + * them by reference. + */ + qry->sortClause = transformSortClause(pstate, + stmt->sortClause, + &qry->targetList, + EXPR_KIND_ORDER_BY, + false /* allow SQL92 rules */ ); + + qry->groupClause = transformGroupClause(pstate, + stmt->groupClause, + stmt->groupByAll, + &qry->groupingSets, + &qry->targetList, + qry->sortClause, + EXPR_KIND_GROUP_BY, + false /* allow SQL92 rules */ ); + qry->groupDistinct = stmt->groupDistinct; + qry->groupByAll = stmt->groupByAll; + + if (stmt->distinctClause == NIL) + { + qry->distinctClause = NIL; + qry->hasDistinctOn = false; + } + else if (linitial(stmt->distinctClause) == NULL) + { + /* We had SELECT DISTINCT */ + qry->distinctClause = transformDistinctClause(pstate, + &qry->targetList, + qry->sortClause, + false); + qry->hasDistinctOn = false; + } + else + { + /* We had SELECT DISTINCT ON */ + qry->distinctClause = transformDistinctOnClause(pstate, + stmt->distinctClause, + &qry->targetList, + qry->sortClause); + qry->hasDistinctOn = true; + } + + /* transform LIMIT */ + qry->limitOffset = transformLimitClause(pstate, stmt->limitOffset, + EXPR_KIND_OFFSET, "OFFSET", + stmt->limitOption); + qry->limitCount = transformLimitClause(pstate, stmt->limitCount, + EXPR_KIND_LIMIT, "LIMIT", + stmt->limitOption); + qry->limitOption = stmt->limitOption; + + /* + * transform window clauses after we have seen all window functions. + * UPDATED: Pass the QUALIFY clause (stmt->qualifyClause) so it can be + * attached to the WindowClause if the planner expects it there. + */ + qry->windowClause = transformWindowDefinitions(pstate, + pstate->p_windowdefs, + &qry->targetList, + qry->qualifyQual); /* <--- Added argument */ + + /* resolve any still-unresolved output columns as being type text */ + if (pstate->p_resolve_unknowns) + resolveTargetListUnknowns(pstate, qry->targetList); + + qry->rtable = pstate->p_rtable; + qry->rteperminfos = pstate->p_rteperminfos; + qry->jointree = makeFromExpr(pstate->p_joinlist, qual); + + qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasWindowFuncs = pstate->p_hasWindowFuncs; + qry->hasTargetSRFs = pstate->p_hasTargetSRFs; + qry->hasAggs = pstate->p_hasAggs; + + foreach(l, stmt->lockingClause) + { + transformLockingClause(pstate, qry, + (LockingClause *) lfirst(l), false); + } + + assign_query_collations(pstate, qry); + + /* this must be done after collations, for reliable comparison of exprs */ + if (pstate->p_hasAggs || qry->groupClause || qry->groupingSets || qry->havingQual) + parseCheckAggregates(pstate, qry); + + return qry; } /* @@ -2014,7 +2027,7 @@ transformSetOperationStmt(ParseState *pstate, SelectStmt *stmt) assign_query_collations(pstate, qry); /* this must be done after collations, for reliable comparison of exprs */ - if (pstate->p_hasAggs || qry->groupClause || qry->groupingSets || qry->havingQual) + if (pstate->p_hasAggs || qry->groupClause || qry->groupingSets || qry->havingQual || qry->qualifyQual) parseCheckAggregates(pstate, qry); return qry; diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index c3a0a354a9c..817f2094bf7 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -318,6 +318,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type select_no_parens select_with_parens select_clause simple_select values_clause + qualify_clause PLpgSQL_Expr PLAssignStmt %type opt_single_name @@ -767,7 +768,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROCEDURES PROGRAM PUBLICATION - QUOTE QUOTES + QUALIFY QUOTE QUOTES RANGE READ REAL REASSIGN RECURSIVE REF_P REFERENCES REFERENCING REFRESH REINDEX RELATIVE_P RELEASE RENAME REPEATABLE REPLACE REPLICA @@ -13050,6 +13051,7 @@ simple_select: SELECT opt_all_clause opt_target_list into_clause from_clause where_clause group_clause having_clause window_clause + qualify_clause { SelectStmt *n = makeNode(SelectStmt); @@ -13062,11 +13064,13 @@ simple_select: n->groupByAll = ($7)->all; n->havingClause = $8; n->windowClause = $9; + n->qualifyClause = $10; $$ = (Node *) n; } | SELECT distinct_clause target_list into_clause from_clause where_clause group_clause having_clause window_clause + qualify_clause { SelectStmt *n = makeNode(SelectStmt); @@ -13080,6 +13084,7 @@ simple_select: n->groupByAll = ($7)->all; n->havingClause = $8; n->windowClause = $9; + n->qualifyClause = $10; $$ = (Node *) n; } | values_clause { $$ = $1; } @@ -14202,6 +14207,13 @@ where_clause: | /*EMPTY*/ { $$ = NULL; } ; +qualify_clause: + QUALIFY a_expr + { $$ = $2; } + | /*EMPTY*/ + { $$ = NULL; } + ; + /* variant for UPDATE and DELETE */ where_or_current_clause: WHERE a_expr { $$ = $2; } @@ -14217,7 +14229,6 @@ where_or_current_clause: | /*EMPTY*/ { $$ = NULL; } ; - OptTableFuncElementList: TableFuncElementList { $$ = $1; } | /*EMPTY*/ { $$ = NIL; } @@ -18349,6 +18360,7 @@ reserved_keyword: | ORDER | PLACING | PRIMARY + | QUALIFY | REFERENCES | RETURNING | SELECT diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c index b8340557b34..39b66ac4cc1 100644 --- a/src/backend/parser/parse_agg.c +++ b/src/backend/parser/parse_agg.c @@ -414,6 +414,7 @@ check_agglevels_and_constraints(ParseState *pstate, Node *expr) break; case EXPR_KIND_HAVING: + case EXPR_KIND_QUALIFY: /* okay */ break; case EXPR_KIND_FILTER: @@ -937,6 +938,9 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc, case EXPR_KIND_HAVING: errkind = true; break; + case EXPR_KIND_QUALIFY: + /* okay */ + break; case EXPR_KIND_FILTER: errkind = true; break; diff --git a/src/backend/parser/parse_clause.c b/src/backend/parser/parse_clause.c index ca26f6f61f2..bf8abc620d2 100644 --- a/src/backend/parser/parse_clause.c +++ b/src/backend/parser/parse_clause.c @@ -2825,208 +2825,207 @@ transformSortClause(ParseState *pstate, */ List * transformWindowDefinitions(ParseState *pstate, - List *windowdefs, - List **targetlist) + List *windowdefs, + List **targetlist, + Node *qualifyClause) /* <--- Added argument */ { - List *result = NIL; - Index winref = 0; - ListCell *lc; - - foreach(lc, windowdefs) - { - WindowDef *windef = (WindowDef *) lfirst(lc); - WindowClause *refwc = NULL; - List *partitionClause; - List *orderClause; - Oid rangeopfamily = InvalidOid; - Oid rangeopcintype = InvalidOid; - WindowClause *wc; - - winref++; - - /* - * Check for duplicate window names. - */ - if (windef->name && - findWindowClause(result, windef->name) != NULL) - ereport(ERROR, - (errcode(ERRCODE_WINDOWING_ERROR), - errmsg("window \"%s\" is already defined", windef->name), - parser_errposition(pstate, windef->location))); - - /* - * If it references a previous window, look that up. - */ - if (windef->refname) - { - refwc = findWindowClause(result, windef->refname); - if (refwc == NULL) - ereport(ERROR, - (errcode(ERRCODE_UNDEFINED_OBJECT), - errmsg("window \"%s\" does not exist", - windef->refname), - parser_errposition(pstate, windef->location))); - } - - /* - * Transform PARTITION and ORDER specs, if any. These are treated - * almost exactly like top-level GROUP BY and ORDER BY clauses, - * including the special handling of nondefault operator semantics. - */ - orderClause = transformSortClause(pstate, - windef->orderClause, - targetlist, - EXPR_KIND_WINDOW_ORDER, - true /* force SQL99 rules */ ); - partitionClause = transformGroupClause(pstate, - windef->partitionClause, - false /* not GROUP BY ALL */ , - NULL, - targetlist, - orderClause, - EXPR_KIND_WINDOW_PARTITION, - true /* force SQL99 rules */ ); - - /* - * And prepare the new WindowClause. - */ - wc = makeNode(WindowClause); - wc->name = windef->name; - wc->refname = windef->refname; - - /* - * Per spec, a windowdef that references a previous one copies the - * previous partition clause (and mustn't specify its own). It can - * specify its own ordering clause, but only if the previous one had - * none. It always specifies its own frame clause, and the previous - * one must not have a frame clause. Yeah, it's bizarre that each of - * these cases works differently, but SQL:2008 says so; see 7.11 - * syntax rule 10 and general rule 1. The frame - * clause rule is especially bizarre because it makes "OVER foo" - * different from "OVER (foo)", and requires the latter to throw an - * error if foo has a nondefault frame clause. Well, ours not to - * reason why, but we do go out of our way to throw a useful error - * message for such cases. - */ - if (refwc) - { - if (partitionClause) - ereport(ERROR, - (errcode(ERRCODE_WINDOWING_ERROR), - errmsg("cannot override PARTITION BY clause of window \"%s\"", - windef->refname), - parser_errposition(pstate, windef->location))); - wc->partitionClause = copyObject(refwc->partitionClause); - } - else - wc->partitionClause = partitionClause; - if (refwc) - { - if (orderClause && refwc->orderClause) - ereport(ERROR, - (errcode(ERRCODE_WINDOWING_ERROR), - errmsg("cannot override ORDER BY clause of window \"%s\"", - windef->refname), - parser_errposition(pstate, windef->location))); - if (orderClause) - { - wc->orderClause = orderClause; - wc->copiedOrder = false; - } - else - { - wc->orderClause = copyObject(refwc->orderClause); - wc->copiedOrder = true; - } - } - else - { - wc->orderClause = orderClause; - wc->copiedOrder = false; - } - if (refwc && refwc->frameOptions != FRAMEOPTION_DEFAULTS) - { - /* - * Use this message if this is a WINDOW clause, or if it's an OVER - * clause that includes ORDER BY or framing clauses. (We already - * rejected PARTITION BY above, so no need to check that.) - */ - if (windef->name || - orderClause || windef->frameOptions != FRAMEOPTION_DEFAULTS) - ereport(ERROR, - (errcode(ERRCODE_WINDOWING_ERROR), - errmsg("cannot copy window \"%s\" because it has a frame clause", - windef->refname), - parser_errposition(pstate, windef->location))); - /* Else this clause is just OVER (foo), so say this: */ - ereport(ERROR, - (errcode(ERRCODE_WINDOWING_ERROR), - errmsg("cannot copy window \"%s\" because it has a frame clause", - windef->refname), - errhint("Omit the parentheses in this OVER clause."), - parser_errposition(pstate, windef->location))); - } - wc->frameOptions = windef->frameOptions; + List *result = NIL; + Index winref = 0; + ListCell *lc; + Node *transformedQualify = NULL; - /* - * RANGE offset PRECEDING/FOLLOWING requires exactly one ORDER BY - * column; check that and get its sort opfamily info. - */ - if ((wc->frameOptions & FRAMEOPTION_RANGE) && - (wc->frameOptions & (FRAMEOPTION_START_OFFSET | - FRAMEOPTION_END_OFFSET))) - { - SortGroupClause *sortcl; - Node *sortkey; - CompareType rangecmptype; - - if (list_length(wc->orderClause) != 1) - ereport(ERROR, - (errcode(ERRCODE_WINDOWING_ERROR), - errmsg("RANGE with offset PRECEDING/FOLLOWING requires exactly one ORDER BY column"), - parser_errposition(pstate, windef->location))); - sortcl = linitial_node(SortGroupClause, wc->orderClause); - sortkey = get_sortgroupclause_expr(sortcl, *targetlist); - /* Find the sort operator in pg_amop */ - if (!get_ordering_op_properties(sortcl->sortop, - &rangeopfamily, - &rangeopcintype, - &rangecmptype)) - elog(ERROR, "operator %u is not a valid ordering operator", - sortcl->sortop); - /* Record properties of sort ordering */ - wc->inRangeColl = exprCollation(sortkey); - wc->inRangeAsc = !sortcl->reverse_sort; - wc->inRangeNullsFirst = sortcl->nulls_first; - } - - /* Per spec, GROUPS mode requires an ORDER BY clause */ - if (wc->frameOptions & FRAMEOPTION_GROUPS) - { - if (wc->orderClause == NIL) - ereport(ERROR, - (errcode(ERRCODE_WINDOWING_ERROR), - errmsg("GROUPS mode requires an ORDER BY clause"), - parser_errposition(pstate, windef->location))); - } - - /* Process frame offset expressions */ - wc->startOffset = transformFrameOffset(pstate, wc->frameOptions, - rangeopfamily, rangeopcintype, - &wc->startInRangeFunc, - windef->startOffset); - wc->endOffset = transformFrameOffset(pstate, wc->frameOptions, - rangeopfamily, rangeopcintype, - &wc->endInRangeFunc, - windef->endOffset); - wc->winref = winref; - - result = lappend(result, wc); - } + /* + * The QUALIFY clause is transformed in analyze.c and passed here. + * We might attach it to WindowClause nodes later if needed, but for now + * we rely on Query->qualifyQual. + */ - return result; + foreach(lc, windowdefs) + { + WindowDef *windef = (WindowDef *) lfirst(lc); + WindowClause *refwc = NULL; + List *partitionClause; + List *orderClause; + Oid rangeopfamily = InvalidOid; + Oid rangeopcintype = InvalidOid; + WindowClause *wc; + + winref++; + + /* + * Check for duplicate window names. + */ + if (windef->name && + findWindowClause(result, windef->name) != NULL) + ereport(ERROR, + (errcode(ERRCODE_WINDOWING_ERROR), + errmsg("window \"%s\" is already defined", windef->name), + parser_errposition(pstate, windef->location))); + + /* + * If it references a previous window, look that up. + */ + if (windef->refname) + { + refwc = findWindowClause(result, windef->refname); + if (refwc == NULL) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("window \"%s\" does not exist", + windef->refname), + parser_errposition(pstate, windef->location))); + } + + /* + * Transform PARTITION and ORDER specs, if any. + */ + orderClause = transformSortClause(pstate, + windef->orderClause, + targetlist, + EXPR_KIND_WINDOW_ORDER, + true /* force SQL99 rules */ ); + partitionClause = transformGroupClause(pstate, + windef->partitionClause, + false /* not GROUP BY ALL */ , + NULL, + targetlist, + orderClause, + EXPR_KIND_WINDOW_PARTITION, + true /* force SQL99 rules */ ); + + /* + * And prepare the new WindowClause. + */ + wc = makeNode(WindowClause); + wc->name = windef->name; + wc->refname = windef->refname; + + /* + * ADDED: Attach the transformed QUALIFY clause. + * Note: This attaches the same qual to every window definition. + * The planner must be smart enough to apply it only once globally + * or at the top-level window node. + */ + wc->qualifyQual = transformedQualify; + + /* + * Per spec, a windowdef that references a previous one copies ... + */ + if (refwc) + { + if (partitionClause) + ereport(ERROR, + (errcode(ERRCODE_WINDOWING_ERROR), + errmsg("cannot override PARTITION BY clause of window \"%s\"", + windef->refname), + parser_errposition(pstate, windef->location))); + wc->partitionClause = copyObject(refwc->partitionClause); + } + else + wc->partitionClause = partitionClause; + + if (refwc) + { + if (orderClause && refwc->orderClause) + ereport(ERROR, + (errcode(ERRCODE_WINDOWING_ERROR), + errmsg("cannot override ORDER BY clause of window \"%s\"", + windef->refname), + parser_errposition(pstate, windef->location))); + if (orderClause) + { + wc->orderClause = orderClause; + wc->copiedOrder = false; + } + else + { + wc->orderClause = copyObject(refwc->orderClause); + wc->copiedOrder = true; + } + } + else + { + wc->orderClause = orderClause; + wc->copiedOrder = false; + } + + if (refwc && refwc->frameOptions != FRAMEOPTION_DEFAULTS) + { + if (windef->name || + orderClause || windef->frameOptions != FRAMEOPTION_DEFAULTS) + ereport(ERROR, + (errcode(ERRCODE_WINDOWING_ERROR), + errmsg("cannot copy window \"%s\" because it has a frame clause", + windef->refname), + parser_errposition(pstate, windef->location))); + ereport(ERROR, + (errcode(ERRCODE_WINDOWING_ERROR), + errmsg("cannot copy window \"%s\" because it has a frame clause", + windef->refname), + errhint("Omit the parentheses in this OVER clause."), + parser_errposition(pstate, windef->location))); + } + wc->frameOptions = windef->frameOptions; + + /* + * RANGE offset PRECEDING/FOLLOWING checks + */ + if ((wc->frameOptions & FRAMEOPTION_RANGE) && + (wc->frameOptions & (FRAMEOPTION_START_OFFSET | + FRAMEOPTION_END_OFFSET))) + { + SortGroupClause *sortcl; + Node *sortkey; + CompareType rangecmptype; + + if (list_length(wc->orderClause) != 1) + ereport(ERROR, + (errcode(ERRCODE_WINDOWING_ERROR), + errmsg("RANGE with offset PRECEDING/FOLLOWING requires exactly one ORDER BY column"), + parser_errposition(pstate, windef->location))); + sortcl = linitial_node(SortGroupClause, wc->orderClause); + sortkey = get_sortgroupclause_expr(sortcl, *targetlist); + + if (!get_ordering_op_properties(sortcl->sortop, + &rangeopfamily, + &rangeopcintype, + &rangecmptype)) + elog(ERROR, "operator %u is not a valid ordering operator", + sortcl->sortop); + + wc->inRangeColl = exprCollation(sortkey); + wc->inRangeAsc = !sortcl->reverse_sort; + wc->inRangeNullsFirst = sortcl->nulls_first; + } + + /* Per spec, GROUPS mode requires an ORDER BY clause */ + if (wc->frameOptions & FRAMEOPTION_GROUPS) + { + if (wc->orderClause == NIL) + ereport(ERROR, + (errcode(ERRCODE_WINDOWING_ERROR), + errmsg("GROUPS mode requires an ORDER BY clause"), + parser_errposition(pstate, windef->location))); + } + + /* Process frame offset expressions */ + wc->startOffset = transformFrameOffset(pstate, wc->frameOptions, + rangeopfamily, rangeopcintype, + &wc->startInRangeFunc, + windef->startOffset); + wc->endOffset = transformFrameOffset(pstate, wc->frameOptions, + rangeopfamily, rangeopcintype, + &wc->endInRangeFunc, + windef->endOffset); + wc->winref = winref; + + result = lappend(result, wc); + } + + return result; } + /* * transformDistinctClause - * transform a DISTINCT clause diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index 44fd1385f8c..4ca07f3571a 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -542,6 +542,7 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref) case EXPR_KIND_WHERE: case EXPR_KIND_POLICY: case EXPR_KIND_HAVING: + case EXPR_KIND_QUALIFY: case EXPR_KIND_FILTER: case EXPR_KIND_WINDOW_PARTITION: case EXPR_KIND_WINDOW_ORDER: @@ -847,6 +848,37 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref) parser_errposition(pstate, cref->location))); } + /* + * If we haven't found a column, and we are in a QUALIFY clause, + * try to resolve the name as an output column alias. + */ + if (node == NULL && + list_length(cref->fields) == 1 && + pstate->p_expr_kind == EXPR_KIND_QUALIFY) + { + ListCell *lc; + Node *match = NULL; + + foreach(lc, pstate->p_targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + + if (tle->resname != NULL && strcmp(tle->resname, colname) == 0) + { + if (match != NULL) + { + ereport(ERROR, + (errcode(ERRCODE_AMBIGUOUS_COLUMN), + errmsg("column reference \"%s\" is ambiguous", + colname), + parser_errposition(pstate, cref->location))); + } + match = (Node *) copyObject(tle->expr); + } + } + node = match; + } + /* * Throw error if no translation found. */ @@ -1797,6 +1829,7 @@ transformSubLink(ParseState *pstate, SubLink *sublink) case EXPR_KIND_WHERE: case EXPR_KIND_POLICY: case EXPR_KIND_HAVING: + case EXPR_KIND_QUALIFY: case EXPR_KIND_FILTER: case EXPR_KIND_WINDOW_PARTITION: case EXPR_KIND_WINDOW_ORDER: @@ -3153,6 +3186,8 @@ ParseExprKindName(ParseExprKind exprKind) return "POLICY"; case EXPR_KIND_HAVING: return "HAVING"; + case EXPR_KIND_QUALIFY: + return "QUALIFY"; case EXPR_KIND_FILTER: return "FILTER"; case EXPR_KIND_WINDOW_PARTITION: diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c index 778d69c6f3c..6f94bf5c342 100644 --- a/src/backend/parser/parse_func.c +++ b/src/backend/parser/parse_func.c @@ -2686,7 +2686,10 @@ check_srf_call_placement(ParseState *pstate, Node *last_srf, int location) err = _("set-returning functions are not allowed in policy expressions"); break; case EXPR_KIND_HAVING: - errkind = true; + /* okay */ + break; + case EXPR_KIND_QUALIFY: + err = _("set-returning functions are not allowed in QUALIFY"); break; case EXPR_KIND_FILTER: errkind = true; diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index d14294a4ece..2b5b1a77a4d 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -116,148 +116,152 @@ typedef uint64 AclMode; /* a bitmask of privilege bits */ */ typedef struct Query { - NodeTag type; - - CmdType commandType; /* select|insert|update|delete|merge|utility */ - - /* where did I come from? */ - QuerySource querySource pg_node_attr(query_jumble_ignore); - - /* - * query identifier (can be set by plugins); ignored for equal, as it - * might not be set; also not stored. This is the result of the query - * jumble, hence ignored. - * - * We store this as a signed value as this is the form it's displayed to - * users in places such as EXPLAIN and pg_stat_statements. Primarily this - * is done due to lack of an SQL type to represent the full range of - * uint64. - */ - int64 queryId pg_node_attr(equal_ignore, query_jumble_ignore, read_write_ignore, read_as(0)); - - /* do I set the command result tag? */ - bool canSetTag pg_node_attr(query_jumble_ignore); - - Node *utilityStmt; /* non-null if commandType == CMD_UTILITY */ - - /* - * rtable index of target relation for INSERT/UPDATE/DELETE/MERGE; 0 for - * SELECT. This is ignored in the query jumble as unrelated to the - * compilation of the query ID. - */ - int resultRelation pg_node_attr(query_jumble_ignore); - - /* has aggregates in tlist or havingQual */ - bool hasAggs pg_node_attr(query_jumble_ignore); - /* has window functions in tlist */ - bool hasWindowFuncs pg_node_attr(query_jumble_ignore); - /* has set-returning functions in tlist */ - bool hasTargetSRFs pg_node_attr(query_jumble_ignore); - /* has subquery SubLink */ - bool hasSubLinks pg_node_attr(query_jumble_ignore); - /* distinctClause is from DISTINCT ON */ - bool hasDistinctOn pg_node_attr(query_jumble_ignore); - /* WITH RECURSIVE was specified */ - bool hasRecursive pg_node_attr(query_jumble_ignore); - /* has INSERT/UPDATE/DELETE/MERGE in WITH */ - bool hasModifyingCTE pg_node_attr(query_jumble_ignore); - /* FOR [KEY] UPDATE/SHARE was specified */ - bool hasForUpdate pg_node_attr(query_jumble_ignore); - /* rewriter has applied some RLS policy */ - bool hasRowSecurity pg_node_attr(query_jumble_ignore); - /* parser has added an RTE_GROUP RTE */ - bool hasGroupRTE pg_node_attr(query_jumble_ignore); - /* is a RETURN statement */ - bool isReturn pg_node_attr(query_jumble_ignore); - - List *cteList; /* WITH list (of CommonTableExpr's) */ - - List *rtable; /* list of range table entries */ - - /* - * list of RTEPermissionInfo nodes for the rtable entries having - * perminfoindex > 0 - */ - List *rteperminfos pg_node_attr(query_jumble_ignore); - FromExpr *jointree; /* table join tree (FROM and WHERE clauses); - * also USING clause for MERGE */ - - List *mergeActionList; /* list of actions for MERGE (only) */ - - /* - * rtable index of target relation for MERGE to pull data. Initially, this - * is the same as resultRelation, but after query rewriting, if the target - * relation is a trigger-updatable view, this is the index of the expanded - * view subquery, whereas resultRelation is the index of the target view. - */ - int mergeTargetRelation pg_node_attr(query_jumble_ignore); - - /* join condition between source and target for MERGE */ - Node *mergeJoinCondition; - - List *targetList; /* target list (of TargetEntry) */ - - /* OVERRIDING clause */ - OverridingKind override pg_node_attr(query_jumble_ignore); - - OnConflictExpr *onConflict; /* ON CONFLICT DO [NOTHING | UPDATE] */ - - /* - * The following three fields describe the contents of the RETURNING list - * for INSERT/UPDATE/DELETE/MERGE. returningOldAlias and returningNewAlias - * are the alias names for OLD and NEW, which may be user-supplied values, - * the defaults "old" and "new", or NULL (if the default "old"/"new" is - * already in use as the alias for some other relation). - */ - char *returningOldAlias pg_node_attr(query_jumble_ignore); - char *returningNewAlias pg_node_attr(query_jumble_ignore); - List *returningList; /* return-values list (of TargetEntry) */ - - List *groupClause; /* a list of SortGroupClause's */ - bool groupDistinct; /* was GROUP BY DISTINCT used? */ - bool groupByAll; /* was GROUP BY ALL used? */ - - List *groupingSets; /* a list of GroupingSet's if present */ - - Node *havingQual; /* qualifications applied to groups */ - - List *windowClause; /* a list of WindowClause's */ - - List *distinctClause; /* a list of SortGroupClause's */ - - List *sortClause; /* a list of SortGroupClause's */ - - Node *limitOffset; /* # of result tuples to skip (int8 expr) */ - Node *limitCount; /* # of result tuples to return (int8 expr) */ - LimitOption limitOption; /* limit type */ - - List *rowMarks; /* a list of RowMarkClause's */ - - Node *setOperations; /* set-operation tree if this is top level of - * a UNION/INTERSECT/EXCEPT query */ - - /* - * A list of pg_constraint OIDs that the query depends on to be - * semantically valid - */ - List *constraintDeps pg_node_attr(query_jumble_ignore); - - /* a list of WithCheckOption's (added during rewrite) */ - List *withCheckOptions pg_node_attr(query_jumble_ignore); - - /* - * The following two fields identify the portion of the source text string - * containing this query. They are typically only populated in top-level - * Queries, not in sub-queries. When not set, they might both be zero, or - * both be -1 meaning "unknown". - */ - /* start location, or -1 if unknown */ - ParseLoc stmt_location; - /* length in bytes; 0 means "rest of string" */ - ParseLoc stmt_len pg_node_attr(query_jumble_ignore); + NodeTag type; + + CmdType commandType; /* select|insert|update|delete|merge|utility */ + + /* where did I come from? */ + QuerySource querySource pg_node_attr(query_jumble_ignore); + + /* + * query identifier (can be set by plugins); ignored for equal, as it + * might not be set; also not stored. This is the result of the query + * jumble, hence ignored. + * + * We store this as a signed value as this is the form it's displayed to + * users in places such as EXPLAIN and pg_stat_statements. Primarily this + * is done due to lack of an SQL type to represent the full range of + * uint64. + */ + int64 queryId pg_node_attr(equal_ignore, query_jumble_ignore, read_write_ignore, read_as(0)); + + /* do I set the command result tag? */ + bool canSetTag pg_node_attr(query_jumble_ignore); + + Node *utilityStmt; /* non-null if commandType == CMD_UTILITY */ + + /* + * rtable index of target relation for INSERT/UPDATE/DELETE/MERGE; 0 for + * SELECT. This is ignored in the query jumble as unrelated to the + * compilation of the query ID. + */ + int resultRelation pg_node_attr(query_jumble_ignore); + + /* has aggregates in tlist or havingQual */ + bool hasAggs pg_node_attr(query_jumble_ignore); + /* has window functions in tlist */ + bool hasWindowFuncs pg_node_attr(query_jumble_ignore); + /* has set-returning functions in tlist */ + bool hasTargetSRFs pg_node_attr(query_jumble_ignore); + /* has subquery SubLink */ + bool hasSubLinks pg_node_attr(query_jumble_ignore); + /* distinctClause is from DISTINCT ON */ + bool hasDistinctOn pg_node_attr(query_jumble_ignore); + /* WITH RECURSIVE was specified */ + bool hasRecursive pg_node_attr(query_jumble_ignore); + /* has INSERT/UPDATE/DELETE/MERGE in WITH */ + bool hasModifyingCTE pg_node_attr(query_jumble_ignore); + /* FOR [KEY] UPDATE/SHARE was specified */ + bool hasForUpdate pg_node_attr(query_jumble_ignore); + /* rewriter has applied some RLS policy */ + bool hasRowSecurity pg_node_attr(query_jumble_ignore); + /* parser has added an RTE_GROUP RTE */ + bool hasGroupRTE pg_node_attr(query_jumble_ignore); + /* is a RETURN statement */ + bool isReturn pg_node_attr(query_jumble_ignore); + + List *cteList; /* WITH list (of CommonTableExpr's) */ + + List *rtable; /* list of range table entries */ + + /* + * list of RTEPermissionInfo nodes for the rtable entries having + * perminfoindex > 0 + */ + List *rteperminfos pg_node_attr(query_jumble_ignore); + FromExpr *jointree; /* table join tree (FROM and WHERE clauses); + * also USING clause for MERGE */ + + List *mergeActionList; /* list of actions for MERGE (only) */ + + /* + * rtable index of target relation for MERGE to pull data. Initially, this + * is the same as resultRelation, but after query rewriting, if the target + * relation is a trigger-updatable view, this is the index of the expanded + * view subquery, whereas resultRelation is the index of the target view. + */ + int mergeTargetRelation pg_node_attr(query_jumble_ignore); + + /* join condition between source and target for MERGE */ + Node *mergeJoinCondition; + + List *targetList; /* target list (of TargetEntry) */ + + /* OVERRIDING clause */ + OverridingKind override pg_node_attr(query_jumble_ignore); + + OnConflictExpr *onConflict; /* ON CONFLICT DO [NOTHING | UPDATE] */ + + /* + * The following three fields describe the contents of the RETURNING list + * for INSERT/UPDATE/DELETE/MERGE. returningOldAlias and returningNewAlias + * are the alias names for OLD and NEW, which may be user-supplied values, + * the defaults "old" and "new", or NULL (if the default "old"/"new" is + * already in use as the alias for some other relation). + */ + char *returningOldAlias pg_node_attr(query_jumble_ignore); + char *returningNewAlias pg_node_attr(query_jumble_ignore); + List *returningList; /* return-values list (of TargetEntry) */ + + List *groupClause; /* a list of SortGroupClause's */ + bool groupDistinct; /* was GROUP BY DISTINCT used? */ + bool groupByAll; /* was GROUP BY ALL used? */ + + List *groupingSets; /* a list of GroupingSet's if present */ + + Node *havingQual; /* qualifications applied to groups */ + + /* ADDED: QUALIFY clause */ + Node *qualifyQual; /* qualifications applied to window functions */ + + List *windowClause; /* a list of WindowClause's */ + + List *distinctClause; /* a list of SortGroupClause's */ + + List *sortClause; /* a list of SortGroupClause's */ + + Node *limitOffset; /* # of result tuples to skip (int8 expr) */ + Node *limitCount; /* # of result tuples to return (int8 expr) */ + LimitOption limitOption; /* limit type */ + + List *rowMarks; /* a list of RowMarkClause's */ + + Node *setOperations; /* set-operation tree if this is top level of + * a UNION/INTERSECT/EXCEPT query */ + + /* + * A list of pg_constraint OIDs that the query depends on to be + * semantically valid + */ + List *constraintDeps pg_node_attr(query_jumble_ignore); + + /* a list of WithCheckOption's (added during rewrite) */ + List *withCheckOptions pg_node_attr(query_jumble_ignore); + + /* + * The following two fields identify the portion of the source text string + * containing this query. They are typically only populated in top-level + * Queries, not in sub-queries. When not set, they might both be zero, or + * both be -1 meaning "unknown". + */ + /* start location, or -1 if unknown */ + ParseLoc stmt_location; + /* length in bytes; 0 means "rest of string" */ + ParseLoc stmt_len pg_node_attr(query_jumble_ignore); } Query; + /**************************************************************************** * Supporting data structures for Parse Trees * @@ -596,6 +600,10 @@ typedef struct WindowDef int frameOptions; /* frame_clause options, see below */ Node *startOffset; /* expression for starting bound, if any */ Node *endOffset; /* expression for ending bound, if any */ + + /* QUALIFY clause */ + Node *qualifyQual; + ParseLoc location; /* parse location, or -1 if none/unknown */ } WindowDef; @@ -1222,6 +1230,7 @@ typedef struct RangeTblEntry /* * Fields valid for a CTE RTE (else NULL/zero): + * */ /* name of the WITH list item */ char *ctename; @@ -1566,32 +1575,37 @@ typedef struct GroupingSet */ typedef struct WindowClause { - NodeTag type; - /* window name (NULL in an OVER clause) */ - char *name pg_node_attr(query_jumble_ignore); - /* referenced window name, if any */ - char *refname pg_node_attr(query_jumble_ignore); - List *partitionClause; /* PARTITION BY list */ - /* ORDER BY list */ - List *orderClause; - int frameOptions; /* frame_clause options, see WindowDef */ - Node *startOffset; /* expression for starting bound, if any */ - Node *endOffset; /* expression for ending bound, if any */ - /* in_range function for startOffset */ - Oid startInRangeFunc pg_node_attr(query_jumble_ignore); - /* in_range function for endOffset */ - Oid endInRangeFunc pg_node_attr(query_jumble_ignore); - /* collation for in_range tests */ - Oid inRangeColl pg_node_attr(query_jumble_ignore); - /* use ASC sort order for in_range tests? */ - bool inRangeAsc pg_node_attr(query_jumble_ignore); - /* nulls sort first for in_range tests? */ - bool inRangeNullsFirst pg_node_attr(query_jumble_ignore); - Index winref; /* ID referenced by window functions */ - /* did we copy orderClause from refname? */ - bool copiedOrder pg_node_attr(query_jumble_ignore); + NodeTag type; + /* window name (NULL in an OVER clause) */ + char *name pg_node_attr(query_jumble_ignore); + /* referenced window name, if any */ + char *refname pg_node_attr(query_jumble_ignore); + List *partitionClause; /* PARTITION BY list */ + /* ORDER BY list */ + List *orderClause; + int frameOptions; /* frame_clause options, see WindowDef */ + Node *startOffset; /* expression for starting bound, if any */ + Node *endOffset; /* expression for ending bound, if any */ + + /* ADDED: QUALIFY clause */ + Node *qualifyQual; /* qualifications applied to window functions */ + + /* in_range function for startOffset */ + Oid startInRangeFunc pg_node_attr(query_jumble_ignore); + /* in_range function for endOffset */ + Oid endInRangeFunc pg_node_attr(query_jumble_ignore); + /* collation for in_range tests */ + Oid inRangeColl pg_node_attr(query_jumble_ignore); + /* use ASC sort order for in_range tests? */ + bool inRangeAsc pg_node_attr(query_jumble_ignore); + /* nulls sort first for in_range tests? */ + bool inRangeNullsFirst pg_node_attr(query_jumble_ignore); + Index winref; /* ID referenced by window functions */ + /* did we copy orderClause from refname? */ + bool copiedOrder pg_node_attr(query_jumble_ignore); } WindowClause; + /* * RowMarkClause - * parser output representation of FOR [KEY] UPDATE/SHARE clauses @@ -2181,55 +2195,60 @@ typedef enum SetOperation typedef struct SelectStmt { - NodeTag type; - - /* - * These fields are used only in "leaf" SelectStmts. - */ - List *distinctClause; /* NULL, list of DISTINCT ON exprs, or - * lcons(NIL,NIL) for all (SELECT DISTINCT) */ - IntoClause *intoClause; /* target for SELECT INTO */ - List *targetList; /* the target list (of ResTarget) */ - List *fromClause; /* the FROM clause */ - Node *whereClause; /* WHERE qualification */ - List *groupClause; /* GROUP BY clauses */ - bool groupDistinct; /* Is this GROUP BY DISTINCT? */ - bool groupByAll; /* Is this GROUP BY ALL? */ - Node *havingClause; /* HAVING conditional-expression */ - List *windowClause; /* WINDOW window_name AS (...), ... */ - - /* - * In a "leaf" node representing a VALUES list, the above fields are all - * null, and instead this field is set. Note that the elements of the - * sublists are just expressions, without ResTarget decoration. Also note - * that a list element can be DEFAULT (represented as a SetToDefault - * node), regardless of the context of the VALUES list. It's up to parse - * analysis to reject that where not valid. - */ - List *valuesLists; /* untransformed list of expression lists */ - - /* - * These fields are used in both "leaf" SelectStmts and upper-level - * SelectStmts. - */ - List *sortClause; /* sort clause (a list of SortBy's) */ - Node *limitOffset; /* # of result tuples to skip */ - Node *limitCount; /* # of result tuples to return */ - LimitOption limitOption; /* limit type */ - List *lockingClause; /* FOR UPDATE (list of LockingClause's) */ - WithClause *withClause; /* WITH clause */ - - /* - * These fields are used only in upper-level SelectStmts. - */ - SetOperation op; /* type of set op */ - bool all; /* ALL specified? */ - struct SelectStmt *larg; /* left child */ - struct SelectStmt *rarg; /* right child */ - /* Eventually add fields for CORRESPONDING spec here */ + NodeTag type; + + /* + * These fields are used only in "leaf" SelectStmts. + */ + List *distinctClause; /* NULL, list of DISTINCT ON exprs, or + * lcons(NIL,NIL) for all (SELECT DISTINCT) */ + IntoClause *intoClause; /* target for SELECT INTO */ + List *targetList; /* the target list (of ResTarget) */ + List *fromClause; /* the FROM clause */ + Node *whereClause; /* WHERE qualification */ + + /* ADDED: QUALIFY clause */ + Node *qualifyClause; /* QUALIFY qualification */ + + List *groupClause; /* GROUP BY clauses */ + bool groupDistinct; /* Is this GROUP BY DISTINCT? */ + bool groupByAll; /* Is this GROUP BY ALL? */ + Node *havingClause; /* HAVING conditional-expression */ + List *windowClause; /* WINDOW window_name AS (...), ... */ + + /* + * In a "leaf" node representing a VALUES list, the above fields are all + * null, and instead this field is set. Note that the elements of the + * sublists are just expressions, without ResTarget decoration. Also note + * that a list element can be DEFAULT (represented as a SetToDefault + * node), regardless of the context of the VALUES list. It's up to parse + * analysis to reject that where not valid. + */ + List *valuesLists; /* untransformed list of expression lists */ + + /* + * These fields are used in both "leaf" SelectStmts and upper-level + * SelectStmts. + */ + List *sortClause; /* sort clause (a list of SortBy's) */ + Node *limitOffset; /* # of result tuples to skip */ + Node *limitCount; /* # of result tuples to return */ + LimitOption limitOption; /* limit type */ + List *lockingClause; /* FOR UPDATE (list of LockingClause's) */ + WithClause *withClause; /* WITH clause */ + + /* + * These fields are used only in upper-level SelectStmts. + */ + SetOperation op; /* type of set op */ + bool all; /* ALL specified? */ + struct SelectStmt *larg; /* left child */ + struct SelectStmt *rarg; /* right child */ + /* Eventually add fields for CORRESPONDING spec here */ } SelectStmt; + /* ---------------------- * Set Operation node for post-analysis query trees * diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 5d4fe27ef96..3c493dc996f 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -361,6 +361,7 @@ PG_KEYWORD("procedure", PROCEDURE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("procedures", PROCEDURES, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("program", PROGRAM, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("publication", PUBLICATION, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("qualify", QUALIFY, RESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("quote", QUOTE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("quotes", QUOTES, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("range", RANGE, UNRESERVED_KEYWORD, BARE_LABEL) diff --git a/src/include/parser/parse_clause.h b/src/include/parser/parse_clause.h index ede3903d1dd..97b78d095a5 100644 --- a/src/include/parser/parse_clause.h +++ b/src/include/parser/parse_clause.h @@ -36,7 +36,8 @@ extern List *transformSortClause(ParseState *pstate, List *orderlist, extern List *transformWindowDefinitions(ParseState *pstate, List *windowdefs, - List **targetlist); + List **targetlist, + Node *qualifyClause); extern List *transformDistinctClause(ParseState *pstate, List **targetlist, List *sortClause, bool is_agg); diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h index f7d07c84542..05f0f17ba3d 100644 --- a/src/include/parser/parse_node.h +++ b/src/include/parser/parse_node.h @@ -45,6 +45,7 @@ typedef enum ParseExprKind EXPR_KIND_FROM_FUNCTION, /* function in FROM clause */ EXPR_KIND_WHERE, /* WHERE */ EXPR_KIND_HAVING, /* HAVING */ + EXPR_KIND_QUALIFY, /* QUALIFY */ EXPR_KIND_FILTER, /* FILTER */ EXPR_KIND_WINDOW_PARTITION, /* window definition PARTITION BY */ EXPR_KIND_WINDOW_ORDER, /* window definition ORDER BY */ @@ -231,6 +232,8 @@ struct ParseState Node *p_last_srf; /* most recent set-returning func/op found */ + List *p_targetList; /* target list (of TargetEntry) */ + /* * Optional hook functions for parser callbacks. These are null unless * set up by the caller of make_parsestate. diff --git a/src/test/regress/expected/qualify.out b/src/test/regress/expected/qualify.out new file mode 100644 index 00000000000..a742a726f21 --- /dev/null +++ b/src/test/regress/expected/qualify.out @@ -0,0 +1,147 @@ +-- +-- QUALIFY clause tests +-- +CREATE TEMP TABLE qualify_test ( + id int, + val int, + grp int +); +INSERT INTO qualify_test VALUES + (1, 10, 1), + (2, 20, 1), + (3, 30, 1), + (4, 10, 2), + (5, 40, 2), + (6, 50, 2); +-- Simple QUALIFY +SELECT id, val, grp, rank() OVER (PARTITION BY grp ORDER BY val) as r +FROM qualify_test +QUALIFY rank() OVER (PARTITION BY grp ORDER BY val) > 1; + id | val | grp | r +----+-----+-----+--- + 2 | 20 | 1 | 2 + 3 | 30 | 1 | 3 + 5 | 40 | 2 | 2 + 6 | 50 | 2 | 3 +(4 rows) + +-- QUALIFY with alias (not supported directly in standard SQL usually, but Postgres allows aliases in GROUP BY/HAVING sometimes. +-- In QUALIFY, we might need to repeat the expression or use a window alias) +-- Postgres doesn't support column aliases in HAVING usually. +-- Let's try repeating the expression first. +SELECT id, val, grp +FROM qualify_test +QUALIFY rank() OVER (PARTITION BY grp ORDER BY val) = 1; + id | val | grp +----+-----+----- + 1 | 10 | 1 + 2 | 20 | 1 + 3 | 30 | 1 + 4 | 10 | 2 + 5 | 40 | 2 + 6 | 50 | 2 +(6 rows) + +-- QUALIFY with WHERE +SELECT id, val, grp +FROM qualify_test +WHERE val > 10 +QUALIFY count(*) OVER (PARTITION BY grp) > 1; + id | val | grp +----+-----+----- + 2 | 20 | 1 + 3 | 30 | 1 + 5 | 40 | 2 + 6 | 50 | 2 +(4 rows) + +-- QUALIFY with GROUP BY and HAVING +SELECT grp, sum(val) as sum_val +FROM qualify_test +GROUP BY grp +HAVING sum(val) > 20 +QUALIFY rank() OVER (ORDER BY sum(val) DESC) = 1; + grp | sum_val +-----+--------- + 2 | 100 + 1 | 60 +(2 rows) + +-- QUALIFY with Window Alias +SELECT id, val, grp, rank() OVER w as r +FROM qualify_test +WINDOW w AS (PARTITION BY grp ORDER BY val) +QUALIFY rank() OVER w > 1; + id | val | grp | r +----+-----+-----+--- + 2 | 20 | 1 | 2 + 3 | 30 | 1 | 3 + 5 | 40 | 2 | 2 + 6 | 50 | 2 | 3 +(4 rows) + +-- QUALIFY with aggregates allowed (as arguments to window functions or if windowed) +-- But plain aggregates are NOT allowed in QUALIFY unless they are part of a window function or the query is grouped. +-- Actually, QUALIFY is evaluated after window functions. +-- If the query is grouped, aggregates are evaluated before window functions. +-- So aggregates in QUALIFY are allowed if they are valid in the SELECT list. +-- Grouped query with aggregate in QUALIFY +SELECT grp, sum(val) +FROM qualify_test +GROUP BY grp +QUALIFY sum(val) > 50; -- This acts like HAVING, but technically allowed if we consider it a filter after windowing (which is identity here) + grp | sum +-----+----- + 2 | 100 + 1 | 60 +(2 rows) + +-- Window function over aggregate +SELECT grp, sum(val), rank() OVER (ORDER BY sum(val)) +FROM qualify_test +GROUP BY grp +QUALIFY rank() OVER (ORDER BY sum(val)) > 1; + grp | sum | rank +-----+-----+------ + 2 | 100 | 2 +(1 row) + +-- Negative tests +-- Set-returning function in QUALIFY (should fail) +SELECT id, val +FROM qualify_test +QUALIFY generate_series(1,3) > 1; +ERROR: set-returning functions are not allowed in QUALIFY +LINE 3: QUALIFY generate_series(1,3) > 1; + ^ +-- Plain aggregate in QUALIFY without grouping (should fail like in WHERE? No, like in HAVING?) +-- If query is not grouped, aggregate in QUALIFY implies grouping? +-- No, QUALIFY is after windowing. +-- If I say SELECT * FROM t QUALIFY sum(val) > 10 +-- This should probably fail if not grouped, or imply grouping? +-- In Postgres, HAVING sum(val) > 10 implies grouping if no GROUP BY. +-- But QUALIFY is specifically for window functions. +-- If I use a plain aggregate, it might be ambiguous. +-- Snowflake allows it (equivalent to HAVING). +-- Let's see what my implementation does. +-- I added EXPR_KIND_QUALIFY to parse_agg.c to allow aggregates. +-- But in parse_func.c I disallowed SRFs. +-- Test QUALIFY with output column alias (should work now) +SELECT id, val, grp, rank() OVER (PARTITION BY grp ORDER BY val) as r +FROM qualify_test +QUALIFY r = 1; + id | val | grp | r +----+-----+-----+--- + 1 | 10 | 1 | 1 + 4 | 10 | 2 | 1 +(2 rows) + +-- Test ambiguous alias in QUALIFY +SELECT 1 as x, 2 as x +FROM qualify_test +QUALIFY x = 1; +ERROR: column reference "x" is ambiguous +LINE 3: QUALIFY x = 1; + ^ +-- Clean up +DROP TABLE qualify_test; diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index cc6d799bcea..7b5a9b8e35c 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -102,7 +102,7 @@ test: publication subscription # Another group of parallel tests # select_views depends on create_view # ---------- -test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combocid tsearch tsdicts foreign_data window xmlmap functional_deps advisory_lock indirect_toast equivclass stats_rewrite +test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combocid tsearch tsdicts foreign_data window xmlmap functional_deps advisory_lock indirect_toast equivclass stats_rewrite qualify # ---------- # Another group of parallel tests (JSON related) diff --git a/src/test/regress/sql/qualify.sql b/src/test/regress/sql/qualify.sql new file mode 100644 index 00000000000..5ec22c1fdf3 --- /dev/null +++ b/src/test/regress/sql/qualify.sql @@ -0,0 +1,101 @@ +-- +-- QUALIFY clause tests +-- + +CREATE TEMP TABLE qualify_test ( + id int, + val int, + grp int +); + +INSERT INTO qualify_test VALUES + (1, 10, 1), + (2, 20, 1), + (3, 30, 1), + (4, 10, 2), + (5, 40, 2), + (6, 50, 2); + +-- Simple QUALIFY +SELECT id, val, grp, rank() OVER (PARTITION BY grp ORDER BY val) as r +FROM qualify_test +QUALIFY rank() OVER (PARTITION BY grp ORDER BY val) > 1; + +-- QUALIFY with alias (not supported directly in standard SQL usually, but Postgres allows aliases in GROUP BY/HAVING sometimes. +-- In QUALIFY, we might need to repeat the expression or use a window alias) +-- Postgres doesn't support column aliases in HAVING usually. +-- Let's try repeating the expression first. + +SELECT id, val, grp +FROM qualify_test +QUALIFY rank() OVER (PARTITION BY grp ORDER BY val) = 1; + +-- QUALIFY with WHERE +SELECT id, val, grp +FROM qualify_test +WHERE val > 10 +QUALIFY count(*) OVER (PARTITION BY grp) > 1; + +-- QUALIFY with GROUP BY and HAVING +SELECT grp, sum(val) as sum_val +FROM qualify_test +GROUP BY grp +HAVING sum(val) > 20 +QUALIFY rank() OVER (ORDER BY sum(val) DESC) = 1; + +-- QUALIFY with Window Alias +SELECT id, val, grp, rank() OVER w as r +FROM qualify_test +WINDOW w AS (PARTITION BY grp ORDER BY val) +QUALIFY rank() OVER w > 1; + +-- QUALIFY with aggregates allowed (as arguments to window functions or if windowed) +-- But plain aggregates are NOT allowed in QUALIFY unless they are part of a window function or the query is grouped. +-- Actually, QUALIFY is evaluated after window functions. +-- If the query is grouped, aggregates are evaluated before window functions. +-- So aggregates in QUALIFY are allowed if they are valid in the SELECT list. + +-- Grouped query with aggregate in QUALIFY +SELECT grp, sum(val) +FROM qualify_test +GROUP BY grp +QUALIFY sum(val) > 50; -- This acts like HAVING, but technically allowed if we consider it a filter after windowing (which is identity here) + +-- Window function over aggregate +SELECT grp, sum(val), rank() OVER (ORDER BY sum(val)) +FROM qualify_test +GROUP BY grp +QUALIFY rank() OVER (ORDER BY sum(val)) > 1; + +-- Negative tests + +-- Set-returning function in QUALIFY (should fail) +SELECT id, val +FROM qualify_test +QUALIFY generate_series(1,3) > 1; + +-- Plain aggregate in QUALIFY without grouping (should fail like in WHERE? No, like in HAVING?) +-- If query is not grouped, aggregate in QUALIFY implies grouping? +-- No, QUALIFY is after windowing. +-- If I say SELECT * FROM t QUALIFY sum(val) > 10 +-- This should probably fail if not grouped, or imply grouping? +-- In Postgres, HAVING sum(val) > 10 implies grouping if no GROUP BY. +-- But QUALIFY is specifically for window functions. +-- If I use a plain aggregate, it might be ambiguous. +-- Snowflake allows it (equivalent to HAVING). +-- Let's see what my implementation does. +-- I added EXPR_KIND_QUALIFY to parse_agg.c to allow aggregates. +-- But in parse_func.c I disallowed SRFs. + +-- Test QUALIFY with output column alias (should work now) +SELECT id, val, grp, rank() OVER (PARTITION BY grp ORDER BY val) as r +FROM qualify_test +QUALIFY r = 1; + +-- Test ambiguous alias in QUALIFY +SELECT 1 as x, 2 as x +FROM qualify_test +QUALIFY x = 1; + +-- Clean up +DROP TABLE qualify_test;