11// SPDX-License-Identifier: Apache-2.0
22// SPDX-FileCopyrightText: Copyright the Vortex contributors
33
4+ use std:: cmp:: Ordering ;
45use std:: fmt:: Formatter ;
56use std:: ops:: Range ;
67use std:: sync:: Arc ;
78use std:: sync:: Weak ;
89
910use datafusion_common:: Result as DFResult ;
11+ use datafusion_common:: ScalarValue ;
1012use datafusion_common:: config:: ConfigOptions ;
13+ use datafusion_datasource:: PartitionedFile ;
1114use datafusion_datasource:: TableSchema ;
1215use datafusion_datasource:: file:: FileSource ;
1316use datafusion_datasource:: file_scan_config:: FileScanConfig ;
@@ -17,9 +20,11 @@ use datafusion_physical_expr::EquivalenceProperties;
1720use datafusion_physical_expr:: PhysicalExprRef ;
1821use datafusion_physical_expr:: PhysicalSortExpr ;
1922use datafusion_physical_expr:: conjunction;
23+ use datafusion_physical_expr:: expressions:: Column ;
2024use datafusion_physical_expr:: projection:: ProjectionExprs ;
2125use datafusion_physical_expr_adapter:: DefaultPhysicalExprAdapterFactory ;
2226use datafusion_physical_expr_common:: physical_expr:: fmt_sql;
27+ use datafusion_physical_expr_common:: sort_expr:: LexOrdering ;
2328use datafusion_physical_plan:: DisplayFormatType ;
2429use datafusion_physical_plan:: PhysicalExpr ;
2530use datafusion_physical_plan:: SortOrderPushdownResult ;
@@ -200,7 +205,7 @@ pub struct VortexSource {
200205 natural_split_ranges : Arc < DashMap < Path , Arc < [ Range < u64 > ] > > > ,
201206 expression_convertor : Arc < dyn ExpressionConvertor > ,
202207 pub ( crate ) vortex_reader_factory : Option < Arc < dyn VortexReaderFactory > > ,
203- pub ( crate ) ordered : bool ,
208+ pub ( crate ) sort_order : Option < LexOrdering > ,
204209 vx_metrics_registry : Arc < dyn MetricsRegistry > ,
205210 file_metadata_cache : Option < Arc < dyn FileMetadataCache > > ,
206211 /// Options controlling scan planning and execution behavior.
@@ -235,7 +240,7 @@ impl VortexSource {
235240 vortex_reader_factory : None ,
236241 vx_metrics_registry : Arc :: new ( DefaultMetricsRegistry :: default ( ) ) ,
237242 file_metadata_cache : None ,
238- ordered : false ,
243+ sort_order : None ,
239244 options : VortexTableOptions :: default ( ) ,
240245 }
241246 }
@@ -362,7 +367,8 @@ impl VortexSource {
362367 metrics_registry : Arc :: clone ( & self . vx_metrics_registry ) ,
363368 layout_readers : Arc :: clone ( & self . layout_readers ) ,
364369 natural_split_ranges : Arc :: clone ( & self . natural_split_ranges ) ,
365- has_output_ordering : !base_config. output_ordering . is_empty ( ) || self . ordered ,
370+ has_output_ordering : !base_config. output_ordering . is_empty ( )
371+ || self . sort_order . is_some ( ) ,
366372 expression_convertor : Arc :: clone ( & self . expression_convertor ) ,
367373 file_metadata_cache : self . file_metadata_cache . clone ( ) ,
368374 projection_pushdown : self . options . projection_pushdown ,
@@ -414,16 +420,46 @@ impl FileSource for VortexSource {
414420 return Ok ( SortOrderPushdownResult :: Unsupported ) ;
415421 }
416422
417- if eq_properties. ordering_satisfy ( order. iter ( ) . cloned ( ) ) ? {
418- let mut this = self . clone ( ) ;
419- this. ordered = true ;
423+ let Some ( sort_order) = LexOrdering :: new ( order. iter ( ) . cloned ( ) ) else {
424+ return Ok ( SortOrderPushdownResult :: Unsupported ) ;
425+ } ;
426+
427+ let mut this = self . clone ( ) ;
428+ this. sort_order = Some ( sort_order) ;
420429
430+ if eq_properties. ordering_satisfy ( order. iter ( ) . cloned ( ) ) ? {
421431 return Ok ( SortOrderPushdownResult :: Exact {
422432 inner : Arc :: new ( this) as Arc < dyn FileSource > ,
423433 } ) ;
424434 }
425435
426- Ok ( SortOrderPushdownResult :: Unsupported )
436+ Ok ( SortOrderPushdownResult :: Inexact {
437+ inner : Arc :: new ( this) as Arc < dyn FileSource > ,
438+ } )
439+ }
440+
441+ fn reorder_files ( & self , mut files : Vec < PartitionedFile > ) -> Vec < PartitionedFile > {
442+ let Some ( ( col_idx, descending) ) = self
443+ . sort_order
444+ . as_ref ( )
445+ . and_then ( |sort_order| sort_reorder_key ( sort_order, & self . table_schema ) )
446+ else {
447+ return files;
448+ } ;
449+
450+ files. sort_unstable_by ( |a, b| {
451+ match ( file_min_value ( a, col_idx) , file_min_value ( b, col_idx) ) {
452+ ( Some ( va) , Some ( vb) ) => {
453+ let cmp = va. partial_cmp ( vb) . unwrap_or ( Ordering :: Equal ) ;
454+ if descending { cmp. reverse ( ) } else { cmp }
455+ }
456+ ( Some ( _) , None ) => Ordering :: Less ,
457+ ( None , Some ( _) ) => Ordering :: Greater ,
458+ ( None , None ) => Ordering :: Equal ,
459+ }
460+ } ) ;
461+
462+ files
427463 }
428464
429465 fn fmt_extra ( & self , t : DisplayFormatType , f : & mut Formatter ) -> std:: fmt:: Result {
@@ -542,13 +578,35 @@ impl FileSource for VortexSource {
542578 }
543579}
544580
581+ fn sort_reorder_key ( sort_order : & LexOrdering , table_schema : & TableSchema ) -> Option < ( usize , bool ) > {
582+ let first = sort_order. first ( ) ;
583+ let col = first. expr . downcast_ref :: < Column > ( ) ?;
584+ let col_idx = table_schema. table_schema ( ) . index_of ( col. name ( ) ) . ok ( ) ?;
585+ Some ( ( col_idx, first. options . descending ) )
586+ }
587+
588+ fn file_min_value ( file : & PartitionedFile , col_idx : usize ) -> Option < & ScalarValue > {
589+ file. statistics
590+ . as_ref ( ) ?
591+ . column_statistics
592+ . get ( col_idx) ?
593+ . min_value
594+ . get_value ( )
595+ }
596+
545597#[ cfg( test) ]
546598mod tests {
599+ use std:: sync:: Arc ;
600+
547601 use arrow_schema:: DataType ;
548602 use arrow_schema:: Field ;
549603 use arrow_schema:: Schema ;
604+ use datafusion_common:: ColumnStatistics ;
550605 use datafusion_common:: ScalarValue ;
606+ use datafusion_common:: Statistics ;
551607 use datafusion_common:: config:: ConfigOptions ;
608+ use datafusion_common:: stats:: Precision ;
609+ use datafusion_datasource:: PartitionedFile ;
552610 use datafusion_datasource:: file_scan_config:: FileScanConfigBuilder ;
553611 use datafusion_execution:: object_store:: ObjectStoreUrl ;
554612 use datafusion_expr:: Operator ;
@@ -601,6 +659,10 @@ mod tests {
601659 PhysicalSortExpr :: new_default ( expr)
602660 }
603661
662+ fn sort_column_desc ( name : & str , index : usize ) -> PhysicalSortExpr {
663+ sort_column ( name, index) . desc ( )
664+ }
665+
604666 fn sort_test_schema ( ) -> Arc < Schema > {
605667 Arc :: new ( Schema :: new ( vec ! [
606668 Field :: new( "a" , DataType :: Int32 , false ) ,
@@ -636,10 +698,48 @@ mod tests {
636698 . downcast_ref :: < VortexSource > ( )
637699 . ok_or_else ( || anyhow:: anyhow!( "expected VortexSource" ) ) ?;
638700
639- assert ! ( source. ordered ) ;
701+ assert ! ( source. sort_order . is_some ( ) ) ;
640702 Ok ( ( ) )
641703 }
642704
705+ fn assert_inexact_source (
706+ result : SortOrderPushdownResult < Arc < dyn FileSource > > ,
707+ ) -> anyhow:: Result < VortexSource > {
708+ let SortOrderPushdownResult :: Inexact { inner } = result else {
709+ anyhow:: bail!( "expected inexact sort pushdown" ) ;
710+ } ;
711+
712+ Ok ( inner
713+ . downcast_ref :: < VortexSource > ( )
714+ . ok_or_else ( || anyhow:: anyhow!( "expected VortexSource" ) ) ?
715+ . clone ( ) )
716+ }
717+
718+ fn file_with_min ( path : & str , min : Option < i32 > ) -> PartitionedFile {
719+ let column_stats = min. map_or_else ( ColumnStatistics :: new_unknown, |min| {
720+ let value = ScalarValue :: Int32 ( Some ( min) ) ;
721+ ColumnStatistics {
722+ min_value : Precision :: Exact ( value. clone ( ) ) ,
723+ max_value : Precision :: Exact ( value) ,
724+ ..ColumnStatistics :: new_unknown ( )
725+ }
726+ } ) ;
727+ let statistics = Statistics {
728+ num_rows : Precision :: Exact ( 1 ) ,
729+ total_byte_size : Precision :: Absent ,
730+ column_statistics : vec ! [ column_stats, ColumnStatistics :: new_unknown( ) ] ,
731+ } ;
732+
733+ PartitionedFile :: new ( path, 1 ) . with_statistics ( Arc :: new ( statistics) )
734+ }
735+
736+ fn file_names ( files : & [ PartitionedFile ] ) -> Vec < & str > {
737+ files
738+ . iter ( )
739+ . map ( |file| file. object_meta . location . as_ref ( ) )
740+ . collect ( )
741+ }
742+
643743 #[ test]
644744 fn try_pushdown_sort_returns_exact_when_ordering_is_satisfied ( ) -> anyhow:: Result < ( ) > {
645745 let schema = sort_test_schema ( ) ;
@@ -655,7 +755,71 @@ mod tests {
655755 anyhow:: bail!( "expected exact sort pushdown" )
656756 }
657757 }
658- assert ! ( !source. ordered) ;
758+ assert ! ( source. sort_order. is_none( ) ) ;
759+ Ok ( ( ) )
760+ }
761+
762+ #[ test]
763+ fn try_pushdown_sort_returns_inexact_and_keeps_order ( ) -> anyhow:: Result < ( ) > {
764+ let schema = sort_test_schema ( ) ;
765+ let source = sort_test_source ( Arc :: clone ( & schema) ) ;
766+ let order = vec ! [ sort_column( "a" , 0 ) ] ;
767+ let eq_properties = EquivalenceProperties :: new ( schema) ;
768+
769+ let updated_source =
770+ assert_inexact_source ( source. try_pushdown_sort ( & order, & eq_properties) ?) ?;
771+
772+ let sort_order = updated_source
773+ . sort_order
774+ . as_ref ( )
775+ . ok_or_else ( || anyhow:: anyhow!( "expected sort order for file reordering" ) ) ?;
776+ assert_eq ! ( sort_order. first( ) . expr. to_string( ) , "a@0" ) ;
777+ Ok ( ( ) )
778+ }
779+
780+ #[ test]
781+ fn reorder_files_sorts_by_min_statistics_ascending ( ) -> anyhow:: Result < ( ) > {
782+ let schema = sort_test_schema ( ) ;
783+ let source = sort_test_source ( Arc :: clone ( & schema) ) ;
784+ let order = vec ! [ sort_column( "a" , 0 ) ] ;
785+ let eq_properties = EquivalenceProperties :: new ( schema) ;
786+ let updated_source =
787+ assert_inexact_source ( source. try_pushdown_sort ( & order, & eq_properties) ?) ?;
788+
789+ let reordered = updated_source. reorder_files ( vec ! [
790+ file_with_min( "middle" , Some ( 50 ) ) ,
791+ file_with_min( "missing" , None ) ,
792+ file_with_min( "small" , Some ( 10 ) ) ,
793+ file_with_min( "large" , Some ( 100 ) ) ,
794+ ] ) ;
795+
796+ assert_eq ! (
797+ file_names( & reordered) ,
798+ vec![ "small" , "middle" , "large" , "missing" ]
799+ ) ;
800+ Ok ( ( ) )
801+ }
802+
803+ #[ test]
804+ fn reorder_files_sorts_by_min_statistics_descending ( ) -> anyhow:: Result < ( ) > {
805+ let schema = sort_test_schema ( ) ;
806+ let source = sort_test_source ( Arc :: clone ( & schema) ) ;
807+ let order = vec ! [ sort_column_desc( "a" , 0 ) ] ;
808+ let eq_properties = EquivalenceProperties :: new ( schema) ;
809+ let updated_source =
810+ assert_inexact_source ( source. try_pushdown_sort ( & order, & eq_properties) ?) ?;
811+
812+ let reordered = updated_source. reorder_files ( vec ! [
813+ file_with_min( "middle" , Some ( 50 ) ) ,
814+ file_with_min( "missing" , None ) ,
815+ file_with_min( "small" , Some ( 10 ) ) ,
816+ file_with_min( "large" , Some ( 100 ) ) ,
817+ ] ) ;
818+
819+ assert_eq ! (
820+ file_names( & reordered) ,
821+ vec![ "large" , "middle" , "small" , "missing" ]
822+ ) ;
659823 Ok ( ( ) )
660824 }
661825
0 commit comments