@@ -37,7 +37,6 @@ use futures::FutureExt;
3737use futures:: StreamExt ;
3838use futures:: TryStreamExt ;
3939use futures:: stream;
40- use itertools:: Itertools ;
4140use object_store:: path:: Path ;
4241use tracing:: Instrument ;
4342use vortex:: array:: VortexSessionExecute ;
@@ -100,8 +99,8 @@ pub(crate) struct VortexOpener {
10099 /// To save on the overhead of reparsing FlatBuffers and rebuilding the layout tree, we cache
101100 /// a file reader the first time we read a file.
102101 pub layout_readers : Arc < DashMap < Path , Weak < dyn LayoutReader > > > ,
103- /// Shared full-file natural split ranges keyed by file path.
104- pub natural_split_ranges : Arc < DashMap < Path , Arc < [ Range < u64 > ] > > > ,
102+ /// Shared full-file natural splits keyed by file path.
103+ pub natural_splits : Arc < DashMap < Path , Arc < NaturalSplits > > > ,
105104 /// Whether the query has output ordering specified
106105 pub has_output_ordering : bool ,
107106
@@ -143,7 +142,7 @@ impl FileOpener for VortexOpener {
143142 let batch_size = self . batch_size ;
144143 let limit = self . limit ;
145144 let layout_readers = Arc :: clone ( & self . layout_readers ) ;
146- let natural_split_ranges = Arc :: clone ( & self . natural_split_ranges ) ;
145+ let natural_splits = Arc :: clone ( & self . natural_splits ) ;
147146 let has_output_ordering = self . has_output_ordering ;
148147 let scan_concurrency = self . scan_concurrency ;
149148
@@ -373,21 +372,22 @@ impl FileOpener for VortexOpener {
373372 if byte_range. start != 0 || byte_range. end != file. object_meta . size {
374373 // Full-file scans already cover every natural split. Only translate the
375374 // byte range back into row boundaries when DataFusion has trimmed the file.
376- let natural_split_ranges = natural_split_ranges_for_file (
377- natural_split_ranges . as_ref ( ) ,
375+ let natural_splits = natural_splits_for_file (
376+ natural_splits . as_ref ( ) ,
378377 & file. object_meta . location ,
379378 & layout_reader,
379+ file. object_meta . size ,
380380 ) ?;
381381
382- let Some ( row_range) = split_aligned_row_range (
383- byte_range,
384- file. object_meta . size ,
385- natural_split_ranges. as_ref ( ) ,
386- ) else {
382+ let Some ( row_range) =
383+ split_aligned_row_range ( byte_range, natural_splits. as_ref ( ) )
384+ else {
387385 return Ok ( stream:: empty ( ) . boxed ( ) ) ;
388386 } ;
389387
390- scan_builder = scan_builder. with_row_range ( row_range) ;
388+ scan_builder = scan_builder
389+ . with_row_range ( row_range)
390+ . with_natural_splits ( Arc :: clone ( & natural_splits. row_boundaries ) ) ;
391391 }
392392 }
393393
@@ -499,74 +499,108 @@ impl FileOpener for VortexOpener {
499499 }
500500}
501501
502- fn natural_split_ranges_for_file (
503- natural_split_ranges : & DashMap < Path , Arc < [ Range < u64 > ] > > ,
502+ #[ derive( Debug ) ]
503+ pub ( crate ) struct NaturalSplits {
504+ row_boundaries : Arc < [ u64 ] > ,
505+ assignment_bytes : Box < [ u64 ] > ,
506+ }
507+
508+ impl NaturalSplits {
509+ fn new ( row_boundaries : Arc < [ u64 ] > , total_size : u64 ) -> Self {
510+ let row_count = row_boundaries. last ( ) . copied ( ) . unwrap_or_default ( ) ;
511+ let assignment_bytes = if row_count == 0 {
512+ Box :: default ( )
513+ } else {
514+ row_boundaries
515+ . windows ( 2 )
516+ . enumerate ( )
517+ . map ( |( idx, boundaries) | {
518+ split_assignment_byte (
519+ idx,
520+ & ( boundaries[ 0 ] ..boundaries[ 1 ] ) ,
521+ row_count,
522+ total_size,
523+ )
524+ } )
525+ . collect ( )
526+ } ;
527+
528+ debug_assert ! ( assignment_bytes. is_sorted( ) ) ;
529+ debug_assert_eq ! (
530+ assignment_bytes. len( ) + usize :: from( !row_boundaries. is_empty( ) ) ,
531+ row_boundaries. len( )
532+ ) ;
533+
534+ Self {
535+ row_boundaries,
536+ assignment_bytes,
537+ }
538+ }
539+ }
540+
541+ fn natural_splits_for_file (
542+ natural_splits : & DashMap < Path , Arc < NaturalSplits > > ,
504543 path : & Path ,
505544 layout_reader : & Arc < dyn LayoutReader > ,
506- ) -> DFResult < Arc < [ Range < u64 > ] > > {
507- if let Some ( split_ranges) = natural_split_ranges. get ( path) {
508- return Ok ( Arc :: clone ( split_ranges. value ( ) ) ) ;
545+ total_size : u64 ,
546+ ) -> DFResult < Arc < NaturalSplits > > {
547+ if let Some ( splits) = natural_splits. get ( path) {
548+ return Ok ( Arc :: clone ( splits. value ( ) ) ) ;
509549 }
510550
511551 // Compute while holding the entry so concurrent partitions opening the same file wait
512552 // for the winner instead of all walking the layout tree; the redundant walks contend on
513553 // the lazily-initialized layout children and dominate the cost of the computation itself.
514- match natural_split_ranges . entry ( path. clone ( ) ) {
554+ match natural_splits . entry ( path. clone ( ) ) {
515555 Entry :: Occupied ( entry) => Ok ( Arc :: clone ( entry. get ( ) ) ) ,
516556 Entry :: Vacant ( entry) => {
517- let split_ranges = compute_natural_split_ranges ( layout_reader. as_ref ( ) ) ?;
518- entry. insert ( Arc :: clone ( & split_ranges ) ) ;
519- Ok ( split_ranges )
557+ let splits = compute_natural_splits ( layout_reader. as_ref ( ) , total_size ) ?;
558+ entry. insert ( Arc :: clone ( & splits ) ) ;
559+ Ok ( splits )
520560 }
521561 }
522562}
523563
524- fn compute_natural_split_ranges ( layout_reader : & dyn LayoutReader ) -> DFResult < Arc < [ Range < u64 > ] > > {
564+ fn compute_natural_splits (
565+ layout_reader : & dyn LayoutReader ,
566+ total_size : u64 ,
567+ ) -> DFResult < Arc < NaturalSplits > > {
525568 let row_count = layout_reader. row_count ( ) ;
526569 let row_range = 0 ..row_count;
527- let split_points : Vec < _ > = SplitBy :: Layout
570+ let row_boundaries = SplitBy :: Layout
528571 . splits ( layout_reader, & row_range, & [ FieldMask :: All ] )
529- . map_err ( |e| exec_datafusion_err ! ( "Failed to compute Vortex natural splits: {e}" ) ) ?
530- . into_iter ( )
531- . tuple_windows ( )
532- . map ( |( s, e) | s..e)
533- . collect :: < Vec < _ > > ( ) ;
572+ . map_err ( |e| exec_datafusion_err ! ( "Failed to compute Vortex natural splits: {e}" ) ) ?;
534573
535- Ok ( split_points. into ( ) )
574+ Ok ( Arc :: new ( NaturalSplits :: new (
575+ row_boundaries. into ( ) ,
576+ total_size,
577+ ) ) )
536578}
537579
538580/// Translate a DataFusion byte range to the contiguous natural split ranges it owns.
539581/// Most splits are assigned by midpoint, but the leading split stays with the range that owns
540582/// byte 0 so a tiny first byte range still claims the first rows.
541583fn split_aligned_row_range (
542584 byte_range : Range < u64 > ,
543- total_size : u64 ,
544- split_ranges : & [ Range < u64 > ] ,
585+ natural_splits : & NaturalSplits ,
545586) -> Option < Range < u64 > > {
546587 if byte_range. start >= byte_range. end {
547588 return None ;
548589 }
549590
550- let row_count = split_ranges. last ( ) . map ( |split| split. end ) ?;
551- if row_count == 0 {
591+ let first_split = natural_splits
592+ . assignment_bytes
593+ . partition_point ( |& assignment_byte| assignment_byte < byte_range. start ) ;
594+ let after_last_split = natural_splits
595+ . assignment_bytes
596+ . partition_point ( |& assignment_byte| assignment_byte < byte_range. end ) ;
597+ if first_split == after_last_split {
552598 return None ;
553599 }
554600
555- let mut owned_splits = split_ranges
556- . iter ( )
557- . enumerate ( )
558- . filter_map ( |( idx, split_range) | {
559- let assignment_byte = split_assignment_byte ( idx, split_range, row_count, total_size) ;
560- byte_range. contains ( & assignment_byte) . then_some ( split_range)
561- } ) ;
562-
563- let first_split = owned_splits. next ( ) ?;
564- let mut row_range = first_split. start ..first_split. end ;
565- for split_range in owned_splits {
566- row_range. end = split_range. end ;
567- }
568-
569- Some ( row_range)
601+ Some (
602+ natural_splits. row_boundaries [ first_split] ..natural_splits. row_boundaries [ after_last_split] ,
603+ )
570604}
571605
572606fn split_assignment_byte (
@@ -701,6 +735,15 @@ mod tests {
701735 }
702736 }
703737
738+ fn natural_splits ( total_size : u64 , split_ranges : & [ Range < u64 > ] ) -> NaturalSplits {
739+ let mut row_boundaries = Vec :: with_capacity ( split_ranges. len ( ) + 1 ) ;
740+ if let Some ( first) = split_ranges. first ( ) {
741+ row_boundaries. push ( first. start ) ;
742+ row_boundaries. extend ( split_ranges. iter ( ) . map ( |range| range. end ) ) ;
743+ }
744+ NaturalSplits :: new ( row_boundaries. into ( ) , total_size)
745+ }
746+
704747 #[ rstest]
705748 #[ case( 0 ..3 , 10 , vec![ 0 ..2 , 2 ..5 , 5 ..10 ] , Some ( 0 ..2 ) ) ]
706749 #[ case( 3 ..7 , 10 , vec![ 0 ..2 , 2 ..5 , 5 ..10 ] , Some ( 2 ..5 ) ) ]
@@ -714,7 +757,7 @@ mod tests {
714757 #[ case] expected : Option < Range < u64 > > ,
715758 ) {
716759 assert_eq ! (
717- split_aligned_row_range( byte_range, total_size, & split_ranges) ,
760+ split_aligned_row_range( byte_range, & natural_splits ( total_size, & split_ranges) ) ,
718761 expected
719762 ) ;
720763 }
@@ -723,10 +766,11 @@ mod tests {
723766 fn test_split_aligned_ranges_cover_splits_exactly_once ( ) {
724767 let split_ranges = vec ! [ 0 ..1 , 1 ..4 , 4 ..10 , 10 ..13 ] ;
725768 let byte_ranges = [ 0 ..4 , 4 ..8 , 8 ..12 , 12 ..16 ] ;
769+ let natural_splits = natural_splits ( 16 , & split_ranges) ;
726770
727771 let assigned = byte_ranges
728772 . into_iter ( )
729- . filter_map ( |byte_range| split_aligned_row_range ( byte_range, 16 , & split_ranges ) )
773+ . filter_map ( |byte_range| split_aligned_row_range ( byte_range, & natural_splits ) )
730774 . collect :: < Vec < _ > > ( ) ;
731775
732776 assert_eq ! ( assigned, vec![ 0 ..4 , 4 ..10 , 10 ..13 ] ) ;
@@ -757,6 +801,15 @@ mod tests {
757801 }
758802 }
759803
804+ #[ test]
805+ fn test_split_aligned_row_range_keeps_colliding_assignments_together ( ) {
806+ let natural_splits = natural_splits ( 2 , & [ 0 ..1 , 1 ..2 , 2 ..3 , 3 ..4 ] ) ;
807+
808+ assert_eq ! ( natural_splits. assignment_bytes. as_ref( ) , [ 0 , 0 , 1 , 1 ] ) ;
809+ assert_eq ! ( split_aligned_row_range( 0 ..1 , & natural_splits) , Some ( 0 ..2 ) ) ;
810+ assert_eq ! ( split_aligned_row_range( 1 ..2 , & natural_splits) , Some ( 2 ..4 ) ) ;
811+ }
812+
760813 async fn write_arrow_to_vortex (
761814 object_store : Arc < dyn ObjectStore > ,
762815 path : & str ,
@@ -794,7 +847,7 @@ mod tests {
794847 metrics_registry : Arc :: new ( DefaultMetricsRegistry :: default ( ) ) ,
795848 df_metrics : ExecutionPlanMetricsSet :: new ( ) ,
796849 layout_readers : Default :: default ( ) ,
797- natural_split_ranges : Default :: default ( ) ,
850+ natural_splits : Default :: default ( ) ,
798851 has_output_ordering : false ,
799852 expression_convertor : Arc :: new ( DefaultExpressionConvertor :: default ( ) ) ,
800853 file_metadata_cache : None ,
@@ -1200,7 +1253,7 @@ mod tests {
12001253 metrics_registry : Arc :: new ( DefaultMetricsRegistry :: default ( ) ) ,
12011254 df_metrics : ExecutionPlanMetricsSet :: new ( ) ,
12021255 layout_readers : Default :: default ( ) ,
1203- natural_split_ranges : Default :: default ( ) ,
1256+ natural_splits : Default :: default ( ) ,
12041257 has_output_ordering : false ,
12051258 expression_convertor : Arc :: new ( DefaultExpressionConvertor :: default ( ) ) ,
12061259 file_metadata_cache : None ,
@@ -1288,7 +1341,7 @@ mod tests {
12881341 metrics_registry : Arc :: new ( DefaultMetricsRegistry :: default ( ) ) ,
12891342 df_metrics : ExecutionPlanMetricsSet :: new ( ) ,
12901343 layout_readers : Default :: default ( ) ,
1291- natural_split_ranges : Default :: default ( ) ,
1344+ natural_splits : Default :: default ( ) ,
12921345 has_output_ordering : false ,
12931346 expression_convertor : Arc :: new ( DefaultExpressionConvertor :: default ( ) ) ,
12941347 file_metadata_cache : None ,
@@ -1446,7 +1499,7 @@ mod tests {
14461499 metrics_registry : Arc :: new ( DefaultMetricsRegistry :: default ( ) ) ,
14471500 df_metrics : ExecutionPlanMetricsSet :: new ( ) ,
14481501 layout_readers : Default :: default ( ) ,
1449- natural_split_ranges : Default :: default ( ) ,
1502+ natural_splits : Default :: default ( ) ,
14501503 has_output_ordering : false ,
14511504 expression_convertor : Arc :: new ( DefaultExpressionConvertor :: default ( ) ) ,
14521505 file_metadata_cache : None ,
@@ -1507,7 +1560,7 @@ mod tests {
15071560 metrics_registry : Arc :: new ( DefaultMetricsRegistry :: default ( ) ) ,
15081561 df_metrics : ExecutionPlanMetricsSet :: new ( ) ,
15091562 layout_readers : Default :: default ( ) ,
1510- natural_split_ranges : Default :: default ( ) ,
1563+ natural_splits : Default :: default ( ) ,
15111564 has_output_ordering : false ,
15121565 expression_convertor : Arc :: new ( DefaultExpressionConvertor :: default ( ) ) ,
15131566 file_metadata_cache : None ,
@@ -1717,7 +1770,7 @@ mod tests {
17171770 metrics_registry : Arc :: new ( DefaultMetricsRegistry :: default ( ) ) ,
17181771 df_metrics : ExecutionPlanMetricsSet :: new ( ) ,
17191772 layout_readers : Default :: default ( ) ,
1720- natural_split_ranges : Default :: default ( ) ,
1773+ natural_splits : Default :: default ( ) ,
17211774 has_output_ordering : false ,
17221775 expression_convertor : Arc :: new ( DefaultExpressionConvertor :: default ( ) ) ,
17231776 file_metadata_cache : None ,
0 commit comments