@@ -3,6 +3,7 @@ use crate::{DistributedTaskContext, NetworkBoundaryExt};
33use datafusion:: common:: Result ;
44use datafusion:: common:: tree_node:: { Transformed , TreeNode , TreeNodeIterator , TreeNodeRecursion } ;
55use datafusion:: physical_plan:: ExecutionPlan ;
6+ use datafusion:: physical_plan:: joins:: { CrossJoinExec , HashJoinExec , NestedLoopJoinExec } ;
67use std:: cell:: RefCell ;
78use std:: sync:: Arc ;
89
@@ -26,6 +27,36 @@ pub(crate) trait TreeNodeExt {
2627 f : F ,
2728 ) -> Result < TreeNodeRecursion > ;
2829
30+ /// Applies `f` top-down (pre-order) to the nodes on the plan's *driver path*: the operators
31+ /// whose ongoing data production feeds, batch for batch, into the root's output. The
32+ /// [`TreeNodeRecursion`] returned by `f` steers it as in [`TreeNode::apply`].
33+ ///
34+ /// It differs from a full traversal only at the pipeline-breaking joins ([`HashJoinExec`],
35+ /// [`NestedLoopJoinExec`], [`CrossJoinExec`]): it follows the probe side (`right`) and skips the
36+ /// build side (`left`), since a join fully materializes its build side before emitting any
37+ /// output row, so build-side rows are setup work rather than output progress. Every other
38+ /// operator is transparent.
39+ ///
40+ /// Used to estimate a running stage's completion: the total rows to pull come from the leaves
41+ /// on the driver path (see `estimated_driver_path_leaf_rows`).
42+ ///
43+ /// ```text
44+ /// ┌────────────┐
45+ /// │ HashJoin │ visited
46+ /// └─────┬──────┘
47+ /// build │ probe
48+ /// (left) │ │ (right)
49+ /// ┌────────┘ └────────┐
50+ /// ▼ ▼
51+ /// ┌────────┐ ┌────────┐
52+ /// │ Scan B │ SKIPPED │ Scan P │ visited
53+ /// └────────┘ └────────┘
54+ /// ```
55+ fn apply_driver_path < F : FnMut ( & Self ) -> Result < TreeNodeRecursion > > (
56+ & self ,
57+ f : F ,
58+ ) -> Result < TreeNodeRecursion > ;
59+
2960 /// Recursively rewrite the tree using `f` in a top-down (pre-order) fashion, propagating
3061 /// the appropriate [DistributedTaskContext] based on the presence of nodes that can isolate
3162 /// tasks, like [ChildrenIsolatorUnionExec].
@@ -104,6 +135,32 @@ impl TreeNodeExt for Arc<dyn ExecutionPlan> {
104135 recurse ( self , ctx, & mut f)
105136 }
106137
138+ fn apply_driver_path < F : FnMut ( & Self ) -> Result < TreeNodeRecursion > > (
139+ & self ,
140+ mut f : F ,
141+ ) -> Result < TreeNodeRecursion > {
142+ fn recurse < F : FnMut ( & Arc < dyn ExecutionPlan > ) -> Result < TreeNodeRecursion > > (
143+ plan : & Arc < dyn ExecutionPlan > ,
144+ f : & mut F ,
145+ ) -> Result < TreeNodeRecursion > {
146+ f ( plan) ?. visit_children ( || {
147+ if let Some ( hash_join) = plan. downcast_ref :: < HashJoinExec > ( ) {
148+ recurse ( hash_join. right ( ) , f)
149+ } else if let Some ( nested_loop_join) = plan. downcast_ref :: < NestedLoopJoinExec > ( ) {
150+ recurse ( nested_loop_join. right ( ) , f)
151+ } else if let Some ( cross_join) = plan. downcast_ref :: < CrossJoinExec > ( ) {
152+ recurse ( cross_join. right ( ) , f)
153+ } else {
154+ plan. children ( )
155+ . into_iter ( )
156+ . apply_until_stop ( |child| recurse ( child, f) )
157+ }
158+ } )
159+ }
160+
161+ recurse ( self , & mut f)
162+ }
163+
107164 fn transform_down_with_dt_ctx <
108165 F : FnMut ( Self , DistributedTaskContext ) -> Result < Transformed < Self > > ,
109166 > (
@@ -213,9 +270,13 @@ mod tests {
213270 use crate :: execution_plans:: ChildWeight ;
214271 use crate :: stage:: RemoteStage ;
215272 use crate :: { NetworkCoalesceExec , Stage } ;
216- use datafusion:: arrow:: datatypes:: Schema ;
273+ use datafusion:: arrow:: datatypes:: { DataType , Field , Schema } ;
274+ use datafusion:: common:: { JoinType , NullEquality } ;
275+ use datafusion:: physical_expr:: PhysicalExpr ;
276+ use datafusion:: physical_expr:: expressions:: Column ;
217277 use datafusion:: physical_plan:: coalesce_partitions:: CoalescePartitionsExec ;
218278 use datafusion:: physical_plan:: empty:: EmptyExec ;
279+ use datafusion:: physical_plan:: joins:: PartitionMode ;
219280 use datafusion:: physical_plan:: union:: UnionExec ;
220281 use insta:: assert_snapshot;
221282 // ── apply_with_dt_ctx ────────────────────────────────────────────────────────
@@ -348,6 +409,81 @@ mod tests {
348409 " ) ;
349410 }
350411
412+ // ── apply_driver_path ────────────────────────────────────────────────────
413+
414+ #[ test]
415+ fn driver_path_leaf ( ) {
416+ let plan = leaf ( ) ;
417+ assert_snapshot ! ( trace_driver_path( & plan) , @"Leaf" ) ;
418+ }
419+
420+ #[ test]
421+ fn driver_path_top_down_order ( ) {
422+ let plan = union ( vec ! [ single( leaf( ) ) , leaf( ) ] ) ;
423+ assert_snapshot ! ( trace_driver_path( & plan) , @r"
424+ Union
425+ Single
426+ Leaf
427+ Leaf
428+ " ) ;
429+ }
430+
431+ #[ test]
432+ fn driver_path_jump_skips_subtree ( ) {
433+ let child = single ( leaf ( ) ) ;
434+ let plan = single ( Arc :: clone ( & child) ) ;
435+ assert_snapshot ! (
436+ trace_driver_path_with( & plan, |p| {
437+ if Arc :: ptr_eq( p, & child) { TreeNodeRecursion :: Jump } else { TreeNodeRecursion :: Continue }
438+ } ) ,
439+ @r"
440+ Single
441+ Single [->jump]
442+ " ) ;
443+ }
444+
445+ #[ test]
446+ fn driver_path_hash_join_uses_probe_side ( ) {
447+ let plan = hash_join (
448+ single ( leaf_i32 ( "l" ) ) ,
449+ union ( vec ! [ leaf_i32( "r" ) , leaf_i32( "r" ) ] ) ,
450+ ) ;
451+ assert_snapshot ! ( trace_driver_path( & plan) , @r"
452+ HashJoin
453+ Union
454+ Leaf
455+ Leaf
456+ " ) ;
457+ }
458+
459+ #[ test]
460+ fn driver_path_nested_loop_join_uses_probe_side ( ) {
461+ let plan = nested_loop_join (
462+ single ( leaf_i32 ( "l" ) ) ,
463+ union ( vec ! [ leaf_i32( "r" ) , leaf_i32( "r" ) ] ) ,
464+ ) ;
465+ assert_snapshot ! ( trace_driver_path( & plan) , @r"
466+ NestedLoopJoin
467+ Union
468+ Leaf
469+ Leaf
470+ " ) ;
471+ }
472+
473+ #[ test]
474+ fn driver_path_cross_join_uses_probe_side ( ) {
475+ let plan = cross_join (
476+ single ( leaf_i32 ( "l" ) ) ,
477+ union ( vec ! [ leaf_i32( "r" ) , leaf_i32( "r" ) ] ) ,
478+ ) ;
479+ assert_snapshot ! ( trace_driver_path( & plan) , @r"
480+ CrossJoin
481+ Union
482+ Leaf
483+ Leaf
484+ " ) ;
485+ }
486+
351487 // ── transform_down_with_dt_ctx ────────────────────────────────────────────
352488
353489 #[ test]
@@ -566,6 +702,14 @@ mod tests {
566702 Arc :: new ( EmptyExec :: new ( Arc :: new ( Schema :: empty ( ) ) ) )
567703 }
568704
705+ fn leaf_i32 ( name : & str ) -> Arc < dyn ExecutionPlan > {
706+ Arc :: new ( EmptyExec :: new ( Arc :: new ( Schema :: new ( vec ! [ Field :: new(
707+ name,
708+ DataType :: Int32 ,
709+ true ,
710+ ) ] ) ) ) )
711+ }
712+
569713 fn single ( child : Arc < dyn ExecutionPlan > ) -> Arc < dyn ExecutionPlan > {
570714 Arc :: new ( CoalescePartitionsExec :: new ( child) )
571715 }
@@ -581,6 +725,47 @@ mod tests {
581725 Arc :: new ( NetworkCoalesceExec :: try_new ( input, producer_tasks, 1 ) . unwrap ( ) )
582726 }
583727
728+ fn hash_join (
729+ left : Arc < dyn ExecutionPlan > ,
730+ right : Arc < dyn ExecutionPlan > ,
731+ ) -> Arc < dyn ExecutionPlan > {
732+ Arc :: new (
733+ HashJoinExec :: try_new (
734+ left,
735+ right,
736+ join_on ( ) ,
737+ None ,
738+ & JoinType :: Inner ,
739+ None ,
740+ PartitionMode :: CollectLeft ,
741+ NullEquality :: NullEqualsNothing ,
742+ false ,
743+ )
744+ . unwrap ( ) ,
745+ )
746+ }
747+
748+ fn nested_loop_join (
749+ left : Arc < dyn ExecutionPlan > ,
750+ right : Arc < dyn ExecutionPlan > ,
751+ ) -> Arc < dyn ExecutionPlan > {
752+ Arc :: new ( NestedLoopJoinExec :: try_new ( left, right, None , & JoinType :: Inner , None ) . unwrap ( ) )
753+ }
754+
755+ fn cross_join (
756+ left : Arc < dyn ExecutionPlan > ,
757+ right : Arc < dyn ExecutionPlan > ,
758+ ) -> Arc < dyn ExecutionPlan > {
759+ Arc :: new ( CrossJoinExec :: new ( left, right) )
760+ }
761+
762+ fn join_on ( ) -> Vec < ( Arc < dyn PhysicalExpr > , Arc < dyn PhysicalExpr > ) > {
763+ vec ! [ (
764+ Arc :: new( Column :: new( "l" , 0 ) ) as Arc <dyn PhysicalExpr >,
765+ Arc :: new( Column :: new( "r" , 0 ) ) as Arc <dyn PhysicalExpr >,
766+ ) ]
767+ }
768+
584769 fn remote_network_boundary ( ) -> Arc < dyn ExecutionPlan > {
585770 network_boundary ( leaf ( ) , 1 )
586771 . as_network_boundary ( )
@@ -630,6 +815,12 @@ mod tests {
630815 "CIU"
631816 } else if p. is :: < NetworkCoalesceExec > ( ) {
632817 "Network"
818+ } else if p. is :: < HashJoinExec > ( ) {
819+ "HashJoin"
820+ } else if p. is :: < NestedLoopJoinExec > ( ) {
821+ "NestedLoopJoin"
822+ } else if p. is :: < CrossJoinExec > ( ) {
823+ "CrossJoin"
633824 } else {
634825 "?"
635826 }
@@ -664,6 +855,29 @@ mod tests {
664855 lines. join ( "\n " )
665856 }
666857
858+ fn trace_driver_path ( root : & Arc < dyn ExecutionPlan > ) -> String {
859+ trace_driver_path_with ( root, |_| TreeNodeRecursion :: Continue )
860+ }
861+
862+ fn trace_driver_path_with < F : FnMut ( & Arc < dyn ExecutionPlan > ) -> TreeNodeRecursion > (
863+ root : & Arc < dyn ExecutionPlan > ,
864+ mut decide : F ,
865+ ) -> String {
866+ let mut lines = vec ! [ ] ;
867+ root. apply_driver_path ( |p| {
868+ let rec = decide ( p) ;
869+ let suffix = match rec {
870+ TreeNodeRecursion :: Continue => "" ,
871+ TreeNodeRecursion :: Jump => " [->jump]" ,
872+ TreeNodeRecursion :: Stop => " [->stop]" ,
873+ } ;
874+ lines. push ( format ! ( "{}{suffix}" , plan_label( p) ) ) ;
875+ Ok ( rec)
876+ } )
877+ . unwrap ( ) ;
878+ lines. join ( "\n " )
879+ }
880+
667881 fn trace_dt_ctx_down ( root : Arc < dyn ExecutionPlan > , dt_ctx : DistributedTaskContext ) -> String {
668882 trace_dt_ctx_down_with ( root, dt_ctx, |_| TreeNodeRecursion :: Continue )
669883 }
0 commit comments