@@ -1109,6 +1109,78 @@ mod supported {
11091109 Ok ( ( ) )
11101110 }
11111111
1112+ /// Regression test: nested CollectLeft HashJoinExec with
1113+ /// CoalescePartitionsExec should not deadlock.
1114+ ///
1115+ /// This reproduces the pattern from TPC-H Q2 where a chain of
1116+ /// small-table inner joins (region→nation→supplier) is broadcast-joined
1117+ /// against a large partitioned table (partsupp). The scheduler enables
1118+ /// CollectLeft for inner joins under the broadcast threshold, and each
1119+ /// executor task runs exactly ONE partition. If any cross-partition
1120+ /// synchronisation (e.g. a tokio Barrier) is used in the build-side
1121+ /// completion path, it will deadlock because only one partition
1122+ /// participates per task.
1123+ #[ rstest]
1124+ #[ case:: standalone( standalone_context( ) ) ]
1125+ #[ tokio:: test]
1126+ async fn nested_collect_left_should_not_deadlock (
1127+ #[ future( awt) ]
1128+ #[ case]
1129+ ctx : SessionContext ,
1130+ test_data : String ,
1131+ ) -> datafusion:: error:: Result < ( ) > {
1132+ // Use alltypes_plain.parquet registered as 3 different tables
1133+ // to create a nested inner join query where the optimizer
1134+ // should choose CollectLeft for the small tables.
1135+ ctx. register_parquet (
1136+ "fact_table" ,
1137+ & format ! ( "{test_data}/alltypes_plain.parquet" ) ,
1138+ Default :: default ( ) ,
1139+ )
1140+ . await ?;
1141+
1142+ ctx. register_parquet (
1143+ "dim_a" ,
1144+ & format ! ( "{test_data}/alltypes_plain.parquet" ) ,
1145+ Default :: default ( ) ,
1146+ )
1147+ . await ?;
1148+
1149+ ctx. register_parquet (
1150+ "dim_b" ,
1151+ & format ! ( "{test_data}/alltypes_plain.parquet" ) ,
1152+ Default :: default ( ) ,
1153+ )
1154+ . await ?;
1155+
1156+ // Query with nested inner joins: dim_b → dim_a → fact_table
1157+ let result = tokio:: time:: timeout (
1158+ std:: time:: Duration :: from_secs ( 120 ) ,
1159+ ctx. sql (
1160+ "SELECT f.id, a.int_col, b.string_col
1161+ FROM fact_table f
1162+ INNER JOIN dim_a a ON f.id = a.id
1163+ INNER JOIN dim_b b ON a.tinyint_col = b.tinyint_col
1164+ ORDER BY f.id
1165+ LIMIT 5" ,
1166+ )
1167+ . await ?
1168+ . collect ( ) ,
1169+ )
1170+ . await
1171+ . expect ( "nested CollectLeft joins should complete within 120s, not deadlock" ) ;
1172+
1173+ let result = result?;
1174+ // Verify we got results
1175+ assert ! ( !result. is_empty( ) , "query should return results" ) ;
1176+ assert ! (
1177+ result[ 0 ] . num_rows( ) > 0 ,
1178+ "query should return at least one row"
1179+ ) ;
1180+
1181+ Ok ( ( ) )
1182+ }
1183+
11121184 #[ rstest]
11131185 #[ case:: standalone( standalone_context( ) ) ]
11141186 #[ case:: remote( remote_context( ) ) ]
0 commit comments