@@ -425,6 +425,7 @@ mod tests {
425425 use itertools:: Itertools ;
426426 use serde_json:: json;
427427 use test_utils:: engine_contract:: test_json_handler_file_path_contract;
428+ use test_utils:: TestCancellationToken ;
428429 use tracing:: info;
429430
430431 use super :: * ;
@@ -1129,6 +1130,132 @@ mod tests {
11291130 }
11301131 }
11311132
1133+ #[ tokio:: test( flavor = "multi_thread" ) ]
1134+ async fn test_read_json_files_parallel_empty_files ( ) {
1135+ let store = Arc :: new ( InMemory :: new ( ) ) ;
1136+ let handler = DefaultJsonHandler :: new (
1137+ store,
1138+ Arc :: new ( TokioMultiThreadExecutor :: new (
1139+ tokio:: runtime:: Handle :: current ( ) ,
1140+ ) ) ,
1141+ )
1142+ . with_parallel_chunks ( NonZero :: new ( 4 ) ) ;
1143+ let physical_schema = schema_ref ! { nullable "val" : INTEGER } ;
1144+ let result: Vec < _ > = handler
1145+ . read_json_files ( & [ ] , physical_schema, None )
1146+ . unwrap ( )
1147+ . try_collect ( )
1148+ . unwrap ( ) ;
1149+ assert ! ( result. is_empty( ) , "empty file list must yield no batches" ) ;
1150+ }
1151+
1152+ #[ tokio:: test( flavor = "multi_thread" ) ]
1153+ async fn test_read_json_files_parallel_missing_file_errors ( ) {
1154+ let store = Arc :: new ( InMemory :: new ( ) ) ;
1155+ let missing_path = Path :: from ( "test/missing" ) ;
1156+ let url = Url :: parse ( & format ! ( "memory:/{missing_path}" ) ) . unwrap ( ) ;
1157+ let files = vec ! [ FileMeta {
1158+ location: url,
1159+ last_modified: 0 ,
1160+ size: 100 ,
1161+ } ] ;
1162+ let handler = DefaultJsonHandler :: new (
1163+ store,
1164+ Arc :: new ( TokioMultiThreadExecutor :: new (
1165+ tokio:: runtime:: Handle :: current ( ) ,
1166+ ) ) ,
1167+ )
1168+ . with_parallel_chunks ( NonZero :: new ( 4 ) ) ;
1169+ let physical_schema = schema_ref ! { nullable "val" : INTEGER } ;
1170+ let result: DeltaResult < Vec < _ > > = handler
1171+ . read_json_files ( & files, physical_schema, None )
1172+ . unwrap ( )
1173+ . try_collect ( ) ;
1174+ assert ! ( result. is_err( ) , "missing file must produce an error" ) ;
1175+ }
1176+
1177+ #[ tokio:: test( flavor = "multi_thread" ) ]
1178+ async fn test_read_json_files_parallel_with_cancelled_token ( ) {
1179+ let store = Arc :: new ( InMemory :: new ( ) ) ;
1180+ store
1181+ . put (
1182+ & Path :: from ( "test/0" ) ,
1183+ Bytes :: from ( r#"{"val": 0}"# ) . into ( ) ,
1184+ )
1185+ . await
1186+ . unwrap ( ) ;
1187+ let url = Url :: parse ( "memory:///test/0" ) . unwrap ( ) ;
1188+ let files = vec ! [ FileMeta {
1189+ location: url,
1190+ last_modified: 0 ,
1191+ size: 12 ,
1192+ } ] ;
1193+ let executor = Arc :: new ( TokioMultiThreadExecutor :: new (
1194+ tokio:: runtime:: Handle :: current ( ) ,
1195+ ) ) ;
1196+ let handler = DefaultJsonHandler :: new ( store, executor)
1197+ . with_parallel_chunks ( NonZero :: new ( 4 ) ) ;
1198+ let physical_schema = schema_ref ! { nullable "val" : INTEGER } ;
1199+ let token: CancellationTokenRef =
1200+ Arc :: new ( TestCancellationToken :: cancelled ( ) ) ;
1201+ let result = handler
1202+ . read_json_files_with_cancellation ( & files, physical_schema, None , Some ( token) ) ;
1203+ assert ! (
1204+ matches!( result, Err ( Error :: Cancelled ) ) ,
1205+ "pre-cancelled token must yield Cancelled, not data"
1206+ ) ;
1207+ }
1208+
1209+ #[ tokio:: test( flavor = "multi_thread" ) ]
1210+ async fn test_read_json_files_parallel_via_builder ( ) {
1211+ let store = Arc :: new ( InMemory :: new ( ) ) ;
1212+ for i in 0 ..100 {
1213+ store
1214+ . put (
1215+ & Path :: from ( format ! ( "test/{i}" ) ) ,
1216+ Bytes :: from ( format ! ( "{{\" val\" : {i}}}" ) ) . into ( ) ,
1217+ )
1218+ . await
1219+ . unwrap ( ) ;
1220+ }
1221+ let files: Vec < FileMeta > = ( 0 ..100 )
1222+ . map ( |i| {
1223+ let url = Url :: parse ( & format ! ( "memory:///test/{i}" ) ) . unwrap ( ) ;
1224+ FileMeta {
1225+ location : url,
1226+ last_modified : 0 ,
1227+ size : 12 ,
1228+ }
1229+ } )
1230+ . collect ( ) ;
1231+
1232+ let executor = Arc :: new ( TokioMultiThreadExecutor :: new (
1233+ tokio:: runtime:: Handle :: current ( ) ,
1234+ ) ) ;
1235+ let engine = crate :: DefaultEngineBuilder :: new ( store)
1236+ . with_task_executor ( executor)
1237+ . with_parallel_chunks ( NonZero :: new ( 10 ) )
1238+ . build ( ) ;
1239+
1240+ let json_handler = engine. json_handler ( ) ;
1241+ let physical_schema = schema_ref ! { nullable "val" : INTEGER } ;
1242+ let data: Vec < RecordBatch > = json_handler
1243+ . read_json_files ( & files, physical_schema, None )
1244+ . unwrap ( )
1245+ . map_ok ( into_record_batch)
1246+ . try_collect ( )
1247+ . unwrap ( ) ;
1248+
1249+ let all_values: Vec < i32 > = data
1250+ . iter ( )
1251+ . flat_map ( |batch| {
1252+ let val_col: & Int32Array = batch. column ( 0 ) . as_primitive ( ) ;
1253+ ( 0 ..val_col. len ( ) ) . map ( |i| val_col. value ( i) ) . collect_vec ( )
1254+ } )
1255+ . collect ( ) ;
1256+ assert_eq ! ( all_values, ( 0 ..100 ) . collect_vec( ) ) ;
1257+ }
1258+
11321259 // Helper function to create test data
11331260 fn create_test_data ( values : Vec < & str > ) -> DeltaResult < Box < dyn EngineData > > {
11341261 let schema = Arc :: new ( ArrowSchema :: new ( vec ! [ Field :: new(
0 commit comments