@@ -9396,14 +9396,15 @@ mod tests {
93969396
93979397 /// Two sides of `batches` batches each, with duplicate keys, keys that
93989398 /// only exist on one side, and a non-key column to filter on.
9399- fn sort_merge_fallback_inputs (
9399+ fn sort_merge_fallback_batches (
94009400 batches : usize ,
9401- ) -> ( Arc < dyn ExecutionPlan > , Arc < dyn ExecutionPlan > ) {
9401+ ) -> ( Vec < RecordBatch > , Vec < RecordBatch > ) {
94029402 let rows_per_batch = 16 ;
94039403 let side = |modulus : i32 , offset : i32 , a : & str , b : & str , c : & str | {
9404- let batches : Vec < RecordBatch > = ( 0 ..batches as i32 )
9404+ ( 0 ..batches as i32 )
94059405 . map ( |batch| {
94069406 let start = batch * rows_per_batch;
9407+ // ids ascend within and across batches
94079408 let ids: Vec < i32 > = ( start..start + rows_per_batch) . collect ( ) ;
94089409 // keys repeat within and across batches, and each side
94099410 // has keys the other side lacks
@@ -9412,13 +9413,22 @@ mod tests {
94129413 let values: Vec < i32 > = ids. iter ( ) . map ( |id| ( id * 13 ) % 17 ) . collect ( ) ;
94139414 build_table_i32 ( ( a, & ids) , ( b, & keys) , ( c, & values) )
94149415 } )
9415- . collect ( ) ;
9416+ . collect :: < Vec < RecordBatch > > ( )
9417+ } ;
9418+ // left keys are 0..47, right keys 5..58
9419+ ( side ( 47 , 0 , "a1" , "b1" , "c1" ) , side ( 53 , 5 , "a2" , "b2" , "c2" ) )
9420+ }
9421+
9422+ fn sort_merge_fallback_inputs (
9423+ batches : usize ,
9424+ ) -> ( Arc < dyn ExecutionPlan > , Arc < dyn ExecutionPlan > ) {
9425+ let ( left, right) = sort_merge_fallback_batches ( batches) ;
9426+ let exec = |batches : Vec < RecordBatch > | {
94169427 let schema = batches[ 0 ] . schema ( ) ;
94179428 TestMemoryExec :: try_new_exec ( & [ batches] , schema, None ) . unwrap ( )
94189429 as Arc < dyn ExecutionPlan >
94199430 } ;
9420- // left keys are 0..47, right keys 5..58
9421- ( side ( 47 , 0 , "a1" , "b1" , "c1" ) , side ( 53 , 5 , "a2" , "b2" , "c2" ) )
9431+ ( exec ( left) , exec ( right) )
94229432 }
94239433
94249434 /// `c1 < c2`, so it references both sides
@@ -9472,6 +9482,22 @@ mod tests {
94729482 )
94739483 }
94749484
9485+ /// An unbounded pool with `hash_join_max_build_size` set, so that only the
9486+ /// cap, or a reason to decline, decides whether a partition falls back.
9487+ fn sort_merge_fallback_capped_ctx ( max_build_size : Option < usize > ) -> Arc < TaskContext > {
9488+ let unlimited = sort_merge_fallback_task_ctx ( None , DiskManagerBuilder :: default ( ) ) ;
9489+ let mut session_config = unlimited. session_config ( ) . clone ( ) ;
9490+ session_config
9491+ . options_mut ( )
9492+ . execution
9493+ . hash_join_max_build_size = max_build_size;
9494+ Arc :: new (
9495+ TaskContext :: default ( )
9496+ . with_session_config ( session_config)
9497+ . with_runtime ( unlimited. runtime_env ( ) ) ,
9498+ )
9499+ }
9500+
94759501 fn sorted_rows ( batches : & [ RecordBatch ] ) -> Vec < String > {
94769502 batches_to_sort_string ( batches)
94779503 . lines ( )
@@ -9699,6 +9725,88 @@ mod tests {
96999725 Ok ( ( ) )
97009726 }
97019727
9728+ /// The fallback is declined whenever the join promises its probe side's
9729+ /// ordering, because a merge emits join-key order instead. The promise is
9730+ /// what matters, not the input: `maintains_input_order` makes it only for
9731+ /// the join types that emit every row while scanning the probe side, and
9732+ /// only an ordered probe input turns it into an advertised output ordering.
9733+ #[ tokio:: test]
9734+ async fn sort_merge_fallback_honors_a_promised_probe_ordering ( ) -> Result < ( ) > {
9735+ let ( left, _) = sort_merge_fallback_inputs ( 32 ) ;
9736+ let ( _, right) = sort_merge_fallback_batches ( 32 ) ;
9737+ let schema = right[ 0 ] . schema ( ) ;
9738+ // `a2` is the probe side's id column, ascending across batches
9739+ let ordering = datafusion_physical_expr_common:: sort_expr:: LexOrdering :: new ( [
9740+ PhysicalSortExpr :: new_default ( Arc :: new ( Column :: new_with_schema (
9741+ "a2" , & schema,
9742+ ) ?) ) ,
9743+ ] )
9744+ . unwrap ( ) ;
9745+ let right = TestMemoryExec :: try_new ( & [ right] , schema, None ) ?
9746+ . try_with_sort_information ( vec ! [ ordering] ) ?;
9747+ let right: Arc < dyn ExecutionPlan > =
9748+ Arc :: new ( TestMemoryExec :: update_cache ( & Arc :: new ( right) ) ) ;
9749+ let on: JoinOn = vec ! [ (
9750+ Arc :: new( Column :: new_with_schema( "b1" , & left. schema( ) ) ?) as _,
9751+ Arc :: new( Column :: new_with_schema( "b2" , & right. schema( ) ) ?) as _,
9752+ ) ] ;
9753+ let join = |join_type : JoinType | {
9754+ HashJoinExec :: try_new (
9755+ Arc :: clone ( & left) ,
9756+ Arc :: clone ( & right) ,
9757+ on. clone ( ) ,
9758+ None ,
9759+ & join_type,
9760+ None ,
9761+ PartitionMode :: Partitioned ,
9762+ NullEquality :: NullEqualsNothing ,
9763+ false ,
9764+ )
9765+ } ;
9766+ // A join that declined up front never registers the counter at all.
9767+ let fallbacks = |join : & HashJoinExec | {
9768+ join. metrics ( )
9769+ . unwrap ( )
9770+ . sum_by_name ( SORT_MERGE_FALLBACK_COUNT_METRIC_NAME )
9771+ . map_or ( 0 , |v| v. as_usize ( ) )
9772+ } ;
9773+
9774+ // An inner join promises the probe order, so under a cap that would
9775+ // otherwise switch it, it stays a hash join and keeps that order.
9776+ let inner = join ( JoinType :: Inner ) ?;
9777+ assert ! ( inner. properties( ) . output_ordering( ) . is_some( ) ) ;
9778+ let batches = common:: collect (
9779+ inner. execute ( 0 , sort_merge_fallback_capped_ctx ( Some ( 1024 ) ) ) ?,
9780+ )
9781+ . await ?;
9782+ let output = concat_batches ( & inner. schema ( ) , & batches) ?;
9783+ let ids = output
9784+ . column_by_name ( "a2" )
9785+ . unwrap ( )
9786+ . as_primitive :: < Int32Type > ( ) ;
9787+ assert ! (
9788+ ids. values( ) . is_sorted( ) ,
9789+ "the output must stay in probe order"
9790+ ) ;
9791+ assert_eq ! (
9792+ fallbacks( & inner) ,
9793+ 0 ,
9794+ "a promised ordering must stop the fallback"
9795+ ) ;
9796+
9797+ // A left join never promises it, so the same ordered input does not
9798+ // stop the fallback.
9799+ let left_join = join ( JoinType :: Left ) ?;
9800+ assert ! ( left_join. properties( ) . output_ordering( ) . is_none( ) ) ;
9801+ common:: collect (
9802+ left_join. execute ( 0 , sort_merge_fallback_capped_ctx ( Some ( 1024 ) ) ) ?,
9803+ )
9804+ . await ?;
9805+ assert_eq ! ( fallbacks( & left_join) , 1 ) ;
9806+
9807+ Ok ( ( ) )
9808+ }
9809+
97029810 /// Without disk the join fails as before
97039811 #[ tokio:: test]
97049812 async fn sort_merge_fallback_needs_disk ( ) -> Result < ( ) > {
@@ -9760,23 +9868,10 @@ mod tests {
97609868 false ,
97619869 )
97629870 } ;
9763- let task_ctx = |max_build_size : Option < usize > | {
9764- let unlimited =
9765- sort_merge_fallback_task_ctx ( None , DiskManagerBuilder :: default ( ) ) ;
9766- let mut session_config = unlimited. session_config ( ) . clone ( ) ;
9767- session_config
9768- . options_mut ( )
9769- . execution
9770- . hash_join_max_build_size = max_build_size;
9771- Arc :: new (
9772- TaskContext :: default ( )
9773- . with_session_config ( session_config)
9774- . with_runtime ( unlimited. runtime_env ( ) ) ,
9775- )
9776- } ;
9777-
97789871 let in_memory = join ( ) ?;
9779- let expected = common:: collect ( in_memory. execute ( 0 , task_ctx ( None ) ) ?) . await ?;
9872+ let expected =
9873+ common:: collect ( in_memory. execute ( 0 , sort_merge_fallback_capped_ctx ( None ) ) ?)
9874+ . await ?;
97809875 assert_eq ! (
97819876 in_memory
97829877 . metrics( )
@@ -9787,7 +9882,10 @@ mod tests {
97879882 ) ;
97889883
97899884 let fallback = join ( ) ?;
9790- let actual = common:: collect ( fallback. execute ( 0 , task_ctx ( Some ( 1024 ) ) ) ?) . await ?;
9885+ let actual = common:: collect (
9886+ fallback. execute ( 0 , sort_merge_fallback_capped_ctx ( Some ( 1024 ) ) ) ?,
9887+ )
9888+ . await ?;
97919889 assert_eq ! (
97929890 fallback
97939891 . metrics( )
0 commit comments