@@ -66,9 +66,6 @@ impl<S: ContextProvider> SqlToRel<'_, S> {
6666 if !select. lateral_views . is_empty ( ) {
6767 return not_impl_err ! ( "LATERAL VIEWS" ) ;
6868 }
69- if select. qualify . is_some ( ) {
70- return not_impl_err ! ( "QUALIFY" ) ;
71- }
7269 if select. top . is_some ( ) {
7370 return not_impl_err ! ( "TOP" ) ;
7471 }
@@ -148,9 +145,27 @@ impl<S: ContextProvider> SqlToRel<'_, S> {
148145 } )
149146 . transpose ( ) ?;
150147
148+ // Optionally the QUALIFY expression (filters after window functions)
149+ let qualify_expr_opt_pre_aggr = select
150+ . qualify
151+ . map :: < Result < Expr > , _ > ( |qualify_expr| {
152+ let qualify_expr = self . sql_expr_to_logical_expr (
153+ qualify_expr,
154+ & combined_schema,
155+ planner_context,
156+ ) ?;
157+ let qualify_expr = resolve_aliases_to_exprs ( qualify_expr, & alias_map) ?;
158+ normalize_col ( qualify_expr, & projected_plan)
159+ } )
160+ . transpose ( ) ?;
161+ let has_qualify = qualify_expr_opt_pre_aggr. is_some ( ) ;
162+
151163 // The outer expressions we will search through for aggregates.
152- // Aggregates may be sourced from the SELECT list or from the HAVING expression.
153- let aggr_expr_haystack = select_exprs. iter ( ) . chain ( having_expr_opt. iter ( ) ) ;
164+ // Aggregates may be sourced from the SELECT list, HAVING expression, or QUALIFY expression.
165+ let aggr_expr_haystack = select_exprs
166+ . iter ( )
167+ . chain ( having_expr_opt. iter ( ) )
168+ . chain ( qualify_expr_opt_pre_aggr. iter ( ) ) ;
154169 // All of the aggregate expressions (deduplicated).
155170 let aggr_exprs = find_aggregate_exprs ( aggr_expr_haystack) ;
156171
@@ -198,22 +213,30 @@ impl<S: ContextProvider> SqlToRel<'_, S> {
198213 . collect ( )
199214 } ;
200215
201- // Process group by, aggregation or having
202- let ( plan, mut select_exprs_post_aggr, having_expr_post_aggr) = if !group_by_exprs
203- . is_empty ( )
204- || !aggr_exprs. is_empty ( )
205- {
216+ // Process group by, aggregation, having (and prepare qualify for post-aggregation)
217+ let (
218+ plan,
219+ mut select_exprs_post_aggr,
220+ having_expr_post_aggr,
221+ mut qualify_expr_post_aggr,
222+ ) = if !group_by_exprs. is_empty ( ) || !aggr_exprs. is_empty ( ) {
206223 self . aggregate (
207224 & base_plan,
208225 & select_exprs,
209226 having_expr_opt. as_ref ( ) ,
210227 & group_by_exprs,
211228 & aggr_exprs,
229+ qualify_expr_opt_pre_aggr. as_ref ( ) ,
212230 ) ?
213231 } else {
214232 match having_expr_opt {
215233 Some ( having_expr) => return plan_err ! ( "HAVING clause references: {having_expr} must appear in the GROUP BY clause or be used in an aggregate function" ) ,
216- None => ( base_plan. clone ( ) , select_exprs. clone ( ) , having_expr_opt)
234+ None => (
235+ base_plan. clone ( ) ,
236+ select_exprs. clone ( ) ,
237+ having_expr_opt,
238+ qualify_expr_opt_pre_aggr,
239+ )
217240 }
218241 } ;
219242
@@ -226,7 +249,21 @@ impl<S: ContextProvider> SqlToRel<'_, S> {
226249 } ;
227250
228251 // Process window function
229- let window_func_exprs = find_window_exprs ( & select_exprs_post_aggr) ;
252+ let window_search_exprs: Vec < Expr > =
253+ if let Some ( ref qualify_expr) = qualify_expr_post_aggr {
254+ let mut v = select_exprs_post_aggr. clone ( ) ;
255+ v. push ( qualify_expr. clone ( ) ) ;
256+ v
257+ } else {
258+ select_exprs_post_aggr. clone ( )
259+ } ;
260+ let window_func_exprs = find_window_exprs ( & window_search_exprs) ;
261+
262+ if has_qualify && window_func_exprs. is_empty ( ) {
263+ return plan_err ! (
264+ "QUALIFY clause requires at least one window function in the SELECT list or QUALIFY predicate"
265+ ) ;
266+ }
230267
231268 let plan = if window_func_exprs. is_empty ( ) {
232269 plan
@@ -239,6 +276,21 @@ impl<S: ContextProvider> SqlToRel<'_, S> {
239276 . map ( |expr| rebase_expr ( expr, & window_func_exprs, & plan) )
240277 . collect :: < Result < Vec < Expr > > > ( ) ?;
241278
279+ // Re-write QUALIFY predicate to reference computed window columns
280+ if let Some ( q) = qualify_expr_post_aggr. take ( ) {
281+ qualify_expr_post_aggr =
282+ Some ( rebase_expr ( & q, & window_func_exprs, & plan) ?) ;
283+ }
284+
285+ plan
286+ } ;
287+
288+ // Apply QUALIFY filter
289+ let plan = if let Some ( qualify_expr) = qualify_expr_post_aggr {
290+ LogicalPlanBuilder :: from ( plan)
291+ . filter ( qualify_expr) ?
292+ . build ( ) ?
293+ } else {
242294 plan
243295 } ;
244296
@@ -782,7 +834,8 @@ impl<S: ContextProvider> SqlToRel<'_, S> {
782834 having_expr_opt : Option < & Expr > ,
783835 group_by_exprs : & [ Expr ] ,
784836 aggr_exprs : & [ Expr ] ,
785- ) -> Result < ( LogicalPlan , Vec < Expr > , Option < Expr > ) > {
837+ qualify_expr_opt : Option < & Expr > ,
838+ ) -> Result < ( LogicalPlan , Vec < Expr > , Option < Expr > , Option < Expr > ) > {
786839 // create the aggregate plan
787840 let options =
788841 LogicalPlanBuilderOptions :: new ( ) . with_add_implicit_group_by_exprs ( true ) ;
@@ -866,7 +919,21 @@ impl<S: ContextProvider> SqlToRel<'_, S> {
866919 None
867920 } ;
868921
869- Ok ( ( plan, select_exprs_post_aggr, having_expr_post_aggr) )
922+ // Rewrite the QUALIFY expression (if any) to use columns produced by the aggregation
923+ let qualify_expr_post_aggr = if let Some ( qualify_expr) = qualify_expr_opt {
924+ let qualify_expr_post_aggr =
925+ rebase_expr ( qualify_expr, & aggr_projection_exprs, input) ?;
926+ Some ( qualify_expr_post_aggr)
927+ } else {
928+ None
929+ } ;
930+
931+ Ok ( (
932+ plan,
933+ select_exprs_post_aggr,
934+ having_expr_post_aggr,
935+ qualify_expr_post_aggr,
936+ ) )
870937 }
871938
872939 // If the projection is done over a named window, that window
0 commit comments