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;
