qualify_v1.patch
application/octet-stream
Filename: qualify_v1.patch
Type: application/octet-stream
Part: 0
Message:
Re: Proposal: QUALIFY clause
Patch
Same data as JSON:
GET /api/v1/attachments/:id/patch
the parsed metadata as JSON — format, series position, per-file stats; never the diff bytes.
API reference →
Format: unified
Series: patch v1
| File | + | − |
|---|---|---|
| src/backend/optimizer/plan/planner.c | 19 | 6 |
| src/backend/parser/analyze.c | 156 | 143 |
| src/backend/parser/gram.y | 14 | 2 |
| src/backend/parser/parse_agg.c | 4 | 0 |
| src/backend/parser/parse_clause.c | 196 | 197 |
| src/backend/parser/parse_expr.c | 35 | 0 |
| src/backend/parser/parse_func.c | 4 | 1 |
| src/include/nodes/parsenodes.h | 228 | 209 |
| src/include/parser/kwlist.h | 1 | 0 |
| src/include/parser/parse_clause.h | 2 | 1 |
| src/include/parser/parse_node.h | 3 | 0 |
| src/test/regress/expected/qualify.out | 147 | 0 |
| src/test/regress/parallel_schedule | 1 | 1 |
| src/test/regress/sql/qualify.sql | 101 | 0 |
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 <node> select_no_parens select_with_parens select_clause
simple_select values_clause
+ qualify_clause
PLpgSQL_Expr PLAssignStmt
%type <str> 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
- * <window clause> 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;