@@ -1473,15 +1473,29 @@ impl DefaultPhysicalPlanner {
14731473 && session_state. config ( ) . repartition_joins ( )
14741474 && !prefer_hash_join
14751475 {
1476- // Use SortMergeJoin if hash join is not preferred
1477- let join_on_len = join_on. len ( ) ;
1476+ // Derive sort options from the left input's existing ordering
1477+ // rather than hardcoding SortOptions::default()
1478+ let sort_options: Vec < SortOptions > = join_on
1479+ . iter ( )
1480+ . map ( |( left_col, _) | {
1481+ physical_left
1482+ . output_ordering ( )
1483+ . and_then ( |ordering| {
1484+ ordering
1485+ . iter ( )
1486+ . find ( |sort_expr| sort_expr. expr . eq ( left_col) )
1487+ . map ( |sort_expr| sort_expr. options )
1488+ } )
1489+ . unwrap_or_default ( )
1490+ } )
1491+ . collect ( ) ;
14781492 Arc :: new ( SortMergeJoinExec :: try_new (
14791493 physical_left,
14801494 physical_right,
14811495 join_on,
14821496 join_filter,
14831497 * join_type,
1484- vec ! [ SortOptions :: default ( ) ; join_on_len ] ,
1498+ sort_options ,
14851499 * null_equality,
14861500 ) ?)
14871501 } else if session_state. config ( ) . target_partitions ( ) > 1
@@ -4356,4 +4370,191 @@ digraph {
43564370 assert_contains ! ( & err_str, "field nullability at index" ) ;
43574371 assert_contains ! ( & err_str, "field metadata at index" ) ;
43584372 }
4373+
4374+ /// Helper: make a session state configured to use SortMergeJoin
4375+ fn make_smj_session_state ( ) -> SessionState {
4376+ let runtime = Arc :: new ( RuntimeEnv :: default ( ) ) ;
4377+ let config = SessionConfig :: new ( )
4378+ . with_target_partitions ( 4 )
4379+ . set_bool ( "datafusion.optimizer.prefer_hash_join" , false )
4380+ . set_bool ( "datafusion.optimizer.skip_failed_rules" , false ) ;
4381+ SessionStateBuilder :: new ( )
4382+ . with_config ( config)
4383+ . with_runtime_env ( runtime)
4384+ . with_default_features ( )
4385+ . build ( )
4386+ }
4387+
4388+ /// Helper: create a fully-optimized physical plan and return its
4389+ /// displayable string for snapshot testing
4390+ async fn plan_smj_display ( ctx : & SessionContext , sql : & str ) -> Result < String > {
4391+ let state = ctx. state ( ) ;
4392+ let logical_plan = state. create_logical_plan ( sql) . await ?;
4393+ let logical_plan = state. optimize ( & logical_plan) ?;
4394+ let planner = DefaultPhysicalPlanner :: default ( ) ;
4395+ let plan = planner. create_physical_plan ( & logical_plan, & state) . await ?;
4396+ Ok ( format ! ( "{}" , displayable( plan. as_ref( ) ) . indent( false ) ) )
4397+ }
4398+
4399+ /// Test: left child sorted DESC on join key -> SMJ derives DESC sort
4400+ /// options, avoiding a re-sort on the left side
4401+ #[ tokio:: test]
4402+ async fn test_smj_sort_options_derived_from_left_desc ( ) -> Result < ( ) > {
4403+ let schema = Arc :: new ( Schema :: new ( vec ! [
4404+ Field :: new( "a" , DataType :: Int32 , false ) ,
4405+ Field :: new( "b" , DataType :: Int32 , false ) ,
4406+ ] ) ) ;
4407+ let batch = RecordBatch :: try_new (
4408+ Arc :: clone ( & schema) ,
4409+ vec ! [
4410+ Arc :: new( Int32Array :: from( vec![ 3 , 2 , 1 ] ) ) ,
4411+ Arc :: new( Int32Array :: from( vec![ 10 , 20 , 30 ] ) ) ,
4412+ ] ,
4413+ ) ?;
4414+
4415+ let left = MemTable :: try_new ( Arc :: clone ( & schema) , vec ! [ vec![ batch. clone( ) ] ] ) ?
4416+ . with_sort_order ( vec ! [ vec![
4417+ col( "a" ) . sort( false , false ) , // DESC, nulls last
4418+ ] ] ) ;
4419+ let right = MemTable :: try_new ( Arc :: clone ( & schema) , vec ! [ vec![ batch] ] ) ?;
4420+
4421+ let state = make_smj_session_state ( ) ;
4422+ let ctx = SessionContext :: new_with_state ( state) ;
4423+ ctx. register_table ( "left_table" , Arc :: new ( left) ) ?;
4424+ ctx. register_table ( "right_table" , Arc :: new ( right) ) ?;
4425+
4426+ let plan = plan_smj_display (
4427+ & ctx,
4428+ "SELECT * FROM left_table JOIN right_table ON left_table.a = right_table.a" ,
4429+ )
4430+ . await ?;
4431+ insta:: assert_snapshot!( plan, @r"
4432+ SortMergeJoinExec: join_type=Inner, on=[(a@0, a@0)]
4433+ DataSourceExec: partitions=1, partition_sizes=[1], output_ordering=a@0 DESC NULLS LAST
4434+ SortExec: expr=[a@0 DESC NULLS LAST], preserve_partitioning=[false]
4435+ DataSourceExec: partitions=1, partition_sizes=[1]
4436+ " ) ;
4437+ Ok ( ( ) )
4438+ }
4439+
4440+ /// Test: left child not sorted on join key -> SMJ falls back to default
4441+ /// ASC behavior
4442+ #[ tokio:: test]
4443+ async fn test_smj_sort_options_default_when_left_unsorted ( ) -> Result < ( ) > {
4444+ let schema = Arc :: new ( Schema :: new ( vec ! [
4445+ Field :: new( "a" , DataType :: Int32 , false ) ,
4446+ Field :: new( "b" , DataType :: Int32 , false ) ,
4447+ ] ) ) ;
4448+ let batch = RecordBatch :: try_new (
4449+ Arc :: clone ( & schema) ,
4450+ vec ! [
4451+ Arc :: new( Int32Array :: from( vec![ 1 , 2 , 3 ] ) ) ,
4452+ Arc :: new( Int32Array :: from( vec![ 10 , 20 , 30 ] ) ) ,
4453+ ] ,
4454+ ) ?;
4455+
4456+ let left = MemTable :: try_new ( Arc :: clone ( & schema) , vec ! [ vec![ batch. clone( ) ] ] ) ?;
4457+ let right = MemTable :: try_new ( Arc :: clone ( & schema) , vec ! [ vec![ batch] ] ) ?;
4458+
4459+ let state = make_smj_session_state ( ) ;
4460+ let ctx = SessionContext :: new_with_state ( state) ;
4461+ ctx. register_table ( "left_table" , Arc :: new ( left) ) ?;
4462+ ctx. register_table ( "right_table" , Arc :: new ( right) ) ?;
4463+
4464+ let plan = plan_smj_display (
4465+ & ctx,
4466+ "SELECT * FROM left_table JOIN right_table ON left_table.a = right_table.a" ,
4467+ )
4468+ . await ?;
4469+ insta:: assert_snapshot!( plan, @r"
4470+ SortMergeJoinExec: join_type=Inner, on=[(a@0, a@0)]
4471+ SortExec: expr=[a@0 ASC], preserve_partitioning=[false]
4472+ DataSourceExec: partitions=1, partition_sizes=[1]
4473+ SortExec: expr=[a@0 ASC], preserve_partitioning=[false]
4474+ DataSourceExec: partitions=1, partition_sizes=[1]
4475+ " ) ;
4476+ Ok ( ( ) )
4477+ }
4478+
4479+ /// Test: multi-column join where left child's ordering partially matches
4480+ #[ tokio:: test]
4481+ async fn test_smj_sort_options_partial_match_multi_column ( ) -> Result < ( ) > {
4482+ let schema = Arc :: new ( Schema :: new ( vec ! [
4483+ Field :: new( "a" , DataType :: Int32 , false ) ,
4484+ Field :: new( "b" , DataType :: Int32 , false ) ,
4485+ ] ) ) ;
4486+ let batch = RecordBatch :: try_new (
4487+ Arc :: clone ( & schema) ,
4488+ vec ! [
4489+ Arc :: new( Int32Array :: from( vec![ 1 , 2 , 3 ] ) ) ,
4490+ Arc :: new( Int32Array :: from( vec![ 30 , 20 , 10 ] ) ) ,
4491+ ] ,
4492+ ) ?;
4493+
4494+ // Left sorted by "a" DESC only; "b" is not in the ordering
4495+ let left = MemTable :: try_new ( Arc :: clone ( & schema) , vec ! [ vec![ batch. clone( ) ] ] ) ?
4496+ . with_sort_order ( vec ! [ vec![
4497+ col( "a" ) . sort( false , false ) , // DESC, nulls last
4498+ ] ] ) ;
4499+ let right = MemTable :: try_new ( Arc :: clone ( & schema) , vec ! [ vec![ batch] ] ) ?;
4500+
4501+ let state = make_smj_session_state ( ) ;
4502+ let ctx = SessionContext :: new_with_state ( state) ;
4503+ ctx. register_table ( "left_table" , Arc :: new ( left) ) ?;
4504+ ctx. register_table ( "right_table" , Arc :: new ( right) ) ?;
4505+
4506+ let plan = plan_smj_display (
4507+ & ctx,
4508+ "SELECT * FROM left_table JOIN right_table \
4509+ ON left_table.a = right_table.a AND left_table.b = right_table.b",
4510+ )
4511+ . await ?;
4512+ insta:: assert_snapshot!( plan, @r"
4513+ SortMergeJoinExec: join_type=Inner, on=[(a@0, a@0), (b@1, b@1)]
4514+ SortExec: expr=[a@0 DESC NULLS LAST, b@1 ASC], preserve_partitioning=[false]
4515+ DataSourceExec: partitions=1, partition_sizes=[1], output_ordering=a@0 DESC NULLS LAST
4516+ SortExec: expr=[a@0 DESC NULLS LAST, b@1 ASC], preserve_partitioning=[false]
4517+ DataSourceExec: partitions=1, partition_sizes=[1]
4518+ " ) ;
4519+ Ok ( ( ) )
4520+ }
4521+
4522+ /// Test: both sides already sorted DESC on join key -> no SortExec needed
4523+ #[ tokio:: test]
4524+ async fn test_smj_sort_options_both_sides_sorted_desc ( ) -> Result < ( ) > {
4525+ let schema = Arc :: new ( Schema :: new ( vec ! [
4526+ Field :: new( "a" , DataType :: Int32 , false ) ,
4527+ Field :: new( "b" , DataType :: Int32 , false ) ,
4528+ ] ) ) ;
4529+ let batch = RecordBatch :: try_new (
4530+ Arc :: clone ( & schema) ,
4531+ vec ! [
4532+ Arc :: new( Int32Array :: from( vec![ 3 , 2 , 1 ] ) ) ,
4533+ Arc :: new( Int32Array :: from( vec![ 10 , 20 , 30 ] ) ) ,
4534+ ] ,
4535+ ) ?;
4536+
4537+ let sort_order = vec ! [ vec![ col( "a" ) . sort( false , false ) ] ] ; // DESC, nulls last
4538+ let left = MemTable :: try_new ( Arc :: clone ( & schema) , vec ! [ vec![ batch. clone( ) ] ] ) ?
4539+ . with_sort_order ( sort_order. clone ( ) ) ;
4540+ let right = MemTable :: try_new ( Arc :: clone ( & schema) , vec ! [ vec![ batch] ] ) ?
4541+ . with_sort_order ( sort_order) ;
4542+
4543+ let state = make_smj_session_state ( ) ;
4544+ let ctx = SessionContext :: new_with_state ( state) ;
4545+ ctx. register_table ( "left_table" , Arc :: new ( left) ) ?;
4546+ ctx. register_table ( "right_table" , Arc :: new ( right) ) ?;
4547+
4548+ let plan = plan_smj_display (
4549+ & ctx,
4550+ "SELECT * FROM left_table JOIN right_table ON left_table.a = right_table.a" ,
4551+ )
4552+ . await ?;
4553+ insta:: assert_snapshot!( plan, @r"
4554+ SortMergeJoinExec: join_type=Inner, on=[(a@0, a@0)]
4555+ DataSourceExec: partitions=1, partition_sizes=[1], output_ordering=a@0 DESC NULLS LAST
4556+ DataSourceExec: partitions=1, partition_sizes=[1], output_ordering=a@0 DESC NULLS LAST
4557+ " ) ;
4558+ Ok ( ( ) )
4559+ }
43594560}
0 commit comments