@@ -61,6 +61,7 @@ use datafusion_physical_expr::window::StandardWindowExpr;
6161use datafusion_physical_plan:: ExecutionPlan ;
6262use datafusion_physical_plan:: filter:: FilterExec ;
6363use datafusion_physical_plan:: projection:: ProjectionExec ;
64+ use datafusion_physical_plan:: repartition:: RepartitionExec ;
6465use datafusion_physical_plan:: sorts:: partitioned_topk:: {
6566 PartitionedTopKExec , WindowFnKind ,
6667} ;
@@ -140,24 +141,25 @@ impl WindowTopN {
140141 // Step 2: Extract limit from predicate (rn <= K, rn < K, etc.)
141142 let ( col_idx, limit_n) = extract_window_limit ( filter. predicate ( ) ) ?;
142143
143- // Step 3: Walk through optional ProjectionExec to find BoundedWindowAggExec
144+ // Step 3: Walk through optional ProjectionExec and RepartitionExec to find BoundedWindowAggExec
144145 let child = filter. input ( ) ;
145- let ( window_exec, proj_between ) = find_window_below ( child) ?;
146+ let ( window_exec, intermediates ) = find_window_below ( child) ?;
146147
147148 // Step 4: Verify col_idx references a supported window function output column
148- let input_field_count = window_exec. input ( ) . schema ( ) . fields ( ) . len ( ) ;
149+ let window_exec_typed = window_exec. downcast_ref :: < BoundedWindowAggExec > ( ) ?;
150+ let sort_exec = window_exec_typed. input ( ) . downcast_ref :: < SortExec > ( ) ?;
151+ let input_field_count = window_exec_typed. input ( ) . schema ( ) . fields ( ) . len ( ) ;
149152 if col_idx < input_field_count {
150153 return None ; // Filter is on an input column, not a window column
151154 }
152155 let window_expr_idx = col_idx - input_field_count;
153- let window_exprs = window_exec . window_expr ( ) ;
156+ let window_exprs = window_exec_typed . window_expr ( ) ;
154157 if window_expr_idx >= window_exprs. len ( ) {
155158 return None ;
156159 }
157160 let fn_kind = supported_window_fn ( & window_exprs[ window_expr_idx] ) ?;
158161
159- // Step 5: Verify child of window is SortExec
160- let sort_exec = window_exec. input ( ) . downcast_ref :: < SortExec > ( ) ?;
162+ // Step 5: child of window is SortExec (verified above)
161163 let sort_child = sort_exec. input ( ) ;
162164
163165 // Step 6: Determine partition_prefix_len from the window expression
@@ -190,28 +192,19 @@ impl WindowTopN {
190192 . ok ( ) ?;
191193
192194 // Step 8: Rebuild window with new child
193- let new_window = Arc :: clone ( & child_as_arc ( window_exec) )
195+ let mut result = window_exec
194196 . with_new_children ( vec ! [ Arc :: new( partitioned_topk) ] )
195197 . ok ( ) ?;
196198
197- // Step 9: If ProjectionExec was between Filter and Window, rebuild it
198- let result = match proj_between {
199- Some ( proj) => Arc :: clone ( & child_as_arc ( proj) )
200- . with_new_children ( vec ! [ new_window] )
201- . ok ( ) ?,
202- None => new_window,
203- } ;
199+ // Step 9: Rebuild intermediate nodes (ProjectionExec/RepartitionExec)
200+ for node in intermediates. into_iter ( ) . rev ( ) {
201+ result = node. with_new_children ( vec ! [ result] ) . ok ( ) ?;
202+ }
204203
205204 Some ( result)
206205 }
207206}
208207
209- /// Helper to get an `Arc<dyn ExecutionPlan>` from a reference.
210- /// We need this because `with_new_children` takes `Arc<Self>`.
211- fn child_as_arc < T : ExecutionPlan + Clone + ' static > ( plan : & T ) -> Arc < dyn ExecutionPlan > {
212- Arc :: new ( plan. clone ( ) )
213- }
214-
215208impl PhysicalOptimizerRule for WindowTopN {
216209 fn optimize (
217210 & self ,
@@ -336,29 +329,32 @@ fn supported_window_fn(
336329 }
337330}
338331
332+ type PlanAndIntermediates = ( Arc < dyn ExecutionPlan > , Vec < Arc < dyn ExecutionPlan > > ) ;
333+
339334/// Walk below a plan node looking for a [`BoundedWindowAggExec`].
340335///
341- /// Handles two cases:
342- /// - Direct child: `FilterExec → BoundedWindowAggExec`
343- /// - With projection: `FilterExec → ProjectionExec → BoundedWindowAggExec`
336+ /// Handles sequences of `ProjectionExec` and `RepartitionExec`.
337+ /// This is safe because `PartitionedTopKExec` can be pushed below them:
338+ /// projections only provide aliases, and pushing the limit below repartitions
339+ /// is safe because the limit is computed per-partition.
344340///
345- /// Returns the window exec and an optional `ProjectionExec` in between,
346- /// or `None` if no `BoundedWindowAggExec` is found within one or two levels.
347- fn find_window_below (
348- plan : & Arc < dyn ExecutionPlan > ,
349- ) -> Option < ( & BoundedWindowAggExec , Option < & ProjectionExec > ) > {
350- // Direct child is BoundedWindowAggExec
351- if let Some ( window) = plan. downcast_ref :: < BoundedWindowAggExec > ( ) {
352- return Some ( ( window, None ) ) ;
353- }
354-
355- // Child is ProjectionExec with BoundedWindowAggExec below
356- if let Some ( proj) = plan. downcast_ref :: < ProjectionExec > ( ) {
357- let proj_child = proj. input ( ) ;
358- if let Some ( window) = proj_child. downcast_ref :: < BoundedWindowAggExec > ( ) {
359- return Some ( ( window, Some ( proj) ) ) ;
341+ /// Returns the window exec and a list of intermediate nodes to rebuild,
342+ /// or `None` if no `BoundedWindowAggExec` is found.
343+ fn find_window_below ( plan : & Arc < dyn ExecutionPlan > ) -> Option < PlanAndIntermediates > {
344+ let mut current = Arc :: clone ( plan) ;
345+ let mut intermediates = Vec :: new ( ) ;
346+
347+ loop {
348+ if current. downcast_ref :: < BoundedWindowAggExec > ( ) . is_some ( ) {
349+ return Some ( ( current, intermediates) ) ;
350+ } else if current. downcast_ref :: < ProjectionExec > ( ) . is_some ( )
351+ || current. downcast_ref :: < RepartitionExec > ( ) . is_some ( )
352+ {
353+ let next = Arc :: clone ( current. children ( ) . first ( ) ?) ;
354+ intermediates. push ( current) ;
355+ current = next;
356+ } else {
357+ return None ;
360358 }
361359 }
362-
363- None
364360}
0 commit comments