1616// under the License.
1717
1818use std:: sync:: Arc ;
19+ use std:: time:: Duration ;
1920
2021use crate :: memory_limit:: DummyStreamPartition ;
2122use crate :: physical_optimizer:: test_utils:: {
@@ -31,9 +32,12 @@ use crate::physical_optimizer::test_utils::{
3132
3233use arrow:: compute:: { SortOptions } ;
3334use arrow:: datatypes:: { DataType , SchemaRef } ;
35+ use arrow:: error:: ArrowError ;
3436use datafusion_common:: config:: { ConfigOptions , CsvOptions } ;
3537use datafusion_common:: tree_node:: { TreeNode , TransformedResult } ;
36- use datafusion_common:: { create_array, DataFusionError , NullEquality , Result , TableReference } ;
38+ use datafusion_common:: {
39+ DataFusionError , NullEquality , Result , ScalarValue , TableReference , create_array,
40+ } ;
3741use datafusion_datasource:: file_scan_config:: { FileScanConfig , FileScanConfigBuilder } ;
3842use datafusion_datasource:: source:: DataSourceExec ;
3943use datafusion_expr_common:: operator:: Operator ;
@@ -49,7 +53,12 @@ use datafusion_physical_plan::limit::{GlobalLimitExec, LocalLimitExec};
4953use datafusion_physical_plan:: repartition:: RepartitionExec ;
5054use datafusion_physical_plan:: sorts:: sort_preserving_merge:: SortPreservingMergeExec ;
5155use datafusion_physical_plan:: sorts:: sort:: SortExec ;
52- use datafusion_physical_plan:: { displayable, get_plan_string, ExecutionPlan , ExecutionPlanProperties } ;
56+ use datafusion_physical_plan:: stream:: RecordBatchStreamAdapter ;
57+ use datafusion_physical_plan:: streaming:: PartitionStream ;
58+ use datafusion_physical_plan:: {
59+ ExecutionPlan , ExecutionPlanProperties , SendableRecordBatchStream , displayable,
60+ get_plan_string,
61+ } ;
5362use datafusion:: datasource:: physical_plan:: CsvSource ;
5463use datafusion:: datasource:: listing:: PartitionedFile ;
5564use datafusion_physical_optimizer:: enforce_sorting:: { PlanWithCorrespondingCoalescePartitions , PlanWithCorrespondingSort , parallelize_sorts, ensure_sorting} ;
@@ -2944,6 +2953,334 @@ async fn test_sort_with_streaming_table() -> Result<()> {
29442953 Ok ( ( ) )
29452954}
29462955
2956+ #[ derive( Debug ) ]
2957+ struct PendingOrderedPartition {
2958+ schema : SchemaRef ,
2959+ batches : Vec < RecordBatch > ,
2960+ }
2961+
2962+ impl PartitionStream for PendingOrderedPartition {
2963+ fn schema ( & self ) -> & SchemaRef {
2964+ & self . schema
2965+ }
2966+
2967+ fn execute ( & self , _ctx : Arc < TaskContext > ) -> SendableRecordBatchStream {
2968+ let stream = futures:: stream:: iter (
2969+ self . batches
2970+ . clone ( )
2971+ . into_iter ( )
2972+ . map ( Ok :: < _ , DataFusionError > ) ,
2973+ )
2974+ . chain ( futures:: stream:: pending ( ) ) ;
2975+ Box :: pin ( RecordBatchStreamAdapter :: new (
2976+ Arc :: clone ( & self . schema ) ,
2977+ stream,
2978+ ) )
2979+ }
2980+ }
2981+
2982+ fn pending_ordered_context (
2983+ value : & ScalarValue ,
2984+ nullable : bool ,
2985+ ) -> Result < SessionContext > {
2986+ let schema: SchemaRef = Arc :: new ( Schema :: new ( vec ! [
2987+ Field :: new( "i" , value. data_type( ) , nullable) ,
2988+ Field :: new( "j" , DataType :: Int64 , false ) ,
2989+ ] ) ) ;
2990+ let batches = [ 0_i64 , 1_i64 ]
2991+ . into_iter ( )
2992+ . map ( |j| -> Result < RecordBatch > {
2993+ Ok ( RecordBatch :: try_new (
2994+ Arc :: clone ( & schema) ,
2995+ vec ! [
2996+ value. to_array_of_size( 1 ) ?,
2997+ ScalarValue :: Int64 ( Some ( j) ) . to_array_of_size( 1 ) ?,
2998+ ] ,
2999+ ) ?)
3000+ } )
3001+ . collect :: < Result < Vec < _ > > > ( ) ?;
3002+ let partition: Arc < dyn PartitionStream > = Arc :: new ( PendingOrderedPartition {
3003+ schema : Arc :: clone ( & schema) ,
3004+ batches,
3005+ } ) ;
3006+ let provider = StreamingTable :: try_new ( Arc :: clone ( & schema) , vec ! [ partition] ) ?
3007+ . with_sort_order ( vec ! [
3008+ datafusion_expr:: col( "i" ) . sort( true , false ) ,
3009+ datafusion_expr:: col( "j" ) . sort( true , false ) ,
3010+ ] )
3011+ . with_infinite_table ( true ) ;
3012+ let ctx = SessionContext :: new_with_config (
3013+ SessionConfig :: new ( )
3014+ . with_target_partitions ( 1 )
3015+ . with_batch_size ( 1 ) ,
3016+ ) ;
3017+ ctx. register_table ( "s" , Arc :: new ( provider) ) ?;
3018+ Ok ( ctx)
3019+ }
3020+
3021+ async fn assert_double_negation_group_streams (
3022+ case : & str ,
3023+ value : ScalarValue ,
3024+ nullable : bool ,
3025+ ) -> Result < ( ) > {
3026+ let ctx = pending_ordered_context ( & value, nullable) ?;
3027+ let plan = ctx
3028+ . sql (
3029+ "SELECT -(-i) AS k, j, COUNT(*) AS n \
3030+ FROM s GROUP BY -(-i), j LIMIT 1",
3031+ )
3032+ . await ?
3033+ . create_physical_plan ( )
3034+ . await ?;
3035+ let plan_text = displayable ( plan. as_ref ( ) ) . indent ( true ) . to_string ( ) ;
3036+ let aggregate = plan_text
3037+ . lines ( )
3038+ . find ( |line| line. contains ( "AggregateExec:" ) )
3039+ . expect ( "aggregate plan" ) ;
3040+ assert ! (
3041+ aggregate. contains( "ordering_mode=Sorted" ) ,
3042+ "{case}: expected fully sorted aggregation:\n {plan_text}"
3043+ ) ;
3044+ assert ! (
3045+ !plan_text. contains( "SortExec:" ) ,
3046+ "{case}: existing input order should avoid a sort:\n {plan_text}"
3047+ ) ;
3048+
3049+ let mut stream = plan. execute ( 0 , ctx. task_ctx ( ) ) ?;
3050+ let output = tokio:: time:: timeout ( Duration :: from_secs ( 5 ) , stream. next ( ) )
3051+ . await
3052+ . expect ( "sorted aggregation withheld a completed group" )
3053+ . expect ( "stream ended before producing a group" ) ?;
3054+ assert_eq ! ( output. num_rows( ) , 1 , "{case}" ) ;
3055+ assert_eq ! (
3056+ ScalarValue :: try_from_array( output. column( 0 ) , 0 ) ?,
3057+ value,
3058+ "{case}"
3059+ ) ;
3060+ assert_eq ! (
3061+ ScalarValue :: try_from_array( output. column( 1 ) , 0 ) ?,
3062+ ScalarValue :: Int64 ( Some ( 0 ) ) ,
3063+ "{case}"
3064+ ) ;
3065+ assert_eq ! (
3066+ ScalarValue :: try_from_array( output. column( 2 ) , 0 ) ?,
3067+ ScalarValue :: Int64 ( Some ( 1 ) ) ,
3068+ "{case}"
3069+ ) ;
3070+ Ok ( ( ) )
3071+ }
3072+
3073+ #[ tokio:: test]
3074+ async fn double_negation_group_by_streams_on_full_input_order ( ) -> Result < ( ) > {
3075+ for ( case, value, nullable) in [
3076+ (
3077+ "timestamp" ,
3078+ ScalarValue :: TimestampNanosecond ( Some ( 1 ) , None ) ,
3079+ false ,
3080+ ) ,
3081+ (
3082+ "nullable timestamp" ,
3083+ ScalarValue :: TimestampNanosecond ( None , None ) ,
3084+ true ,
3085+ ) ,
3086+ (
3087+ "year-month interval" ,
3088+ ScalarValue :: IntervalYearMonth ( Some ( 1 ) ) ,
3089+ false ,
3090+ ) ,
3091+ ] {
3092+ assert_double_negation_group_streams ( case, value, nullable) . await ?;
3093+ }
3094+ Ok ( ( ) )
3095+ }
3096+
3097+ #[ tokio:: test]
3098+ async fn double_negation_projection_preserves_suffix_ordering ( ) -> Result < ( ) > {
3099+ for ( case, sql) in [
3100+ (
3101+ "projection" ,
3102+ "SELECT k, j FROM (SELECT -(-i) AS k, j FROM s) \
3103+ ORDER BY k, j LIMIT 1",
3104+ ) ,
3105+ (
3106+ "aggregate output" ,
3107+ "SELECT k, j FROM ( \
3108+ SELECT -(-i) AS k, j, COUNT(*) AS n \
3109+ FROM s GROUP BY -(-i), j \
3110+ ) ORDER BY k, j LIMIT 1",
3111+ ) ,
3112+ ] {
3113+ let ctx = pending_ordered_context (
3114+ & ScalarValue :: TimestampNanosecond ( Some ( 1 ) , None ) ,
3115+ false ,
3116+ ) ?;
3117+ let plan = ctx. sql ( sql) . await ?. create_physical_plan ( ) . await ?;
3118+ let plan_text = displayable ( plan. as_ref ( ) ) . indent ( true ) . to_string ( ) ;
3119+ assert ! (
3120+ !plan_text. contains( "SortExec:" ) ,
3121+ "{case}: projected ordering should include the suffix:\n {plan_text}"
3122+ ) ;
3123+
3124+ let mut stream = plan. execute ( 0 , ctx. task_ctx ( ) ) ?;
3125+ let output = tokio:: time:: timeout ( Duration :: from_secs ( 5 ) , stream. next ( ) )
3126+ . await
3127+ . unwrap_or_else ( |_| panic ! ( "{case}: ordered projection did not stream" ) )
3128+ . expect ( "stream ended before producing a row" ) ?;
3129+ assert_eq ! ( output. num_rows( ) , 1 , "{case}" ) ;
3130+ assert_eq ! (
3131+ ScalarValue :: try_from_array( output. column( 0 ) , 0 ) ?,
3132+ ScalarValue :: TimestampNanosecond ( Some ( 1 ) , None ) ,
3133+ "{case}"
3134+ ) ;
3135+ assert_eq ! (
3136+ ScalarValue :: try_from_array( output. column( 1 ) , 0 ) ?,
3137+ ScalarValue :: Int64 ( Some ( 0 ) ) ,
3138+ "{case}"
3139+ ) ;
3140+ }
3141+ Ok ( ( ) )
3142+ }
3143+
3144+ #[ tokio:: test]
3145+ async fn double_negation_group_by_preserves_timestamp_overflow ( ) -> Result < ( ) > {
3146+ let ctx = pending_ordered_context (
3147+ & ScalarValue :: TimestampNanosecond ( Some ( i64:: MIN ) , None ) ,
3148+ false ,
3149+ ) ?;
3150+ let plan = ctx
3151+ . sql (
3152+ "SELECT -(-i) AS k, j, COUNT(*) AS n \
3153+ FROM s GROUP BY -(-i), j LIMIT 1",
3154+ )
3155+ . await ?
3156+ . create_physical_plan ( )
3157+ . await ?;
3158+
3159+ let mut stream = plan. execute ( 0 , ctx. task_ctx ( ) ) ?;
3160+ let err = tokio:: time:: timeout ( Duration :: from_secs ( 5 ) , stream. next ( ) )
3161+ . await
3162+ . expect ( "timestamp overflow was not reported" )
3163+ . expect ( "stream ended before reporting timestamp overflow" )
3164+ . expect_err ( "double negation must still evaluate its checked inner negation" ) ;
3165+ assert ! (
3166+ matches!(
3167+ err. find_root( ) ,
3168+ DataFusionError :: ArrowError ( error, _)
3169+ if matches!( error. as_ref( ) , ArrowError :: ArithmeticOverflow ( _) )
3170+ ) ,
3171+ "unexpected error: {err}"
3172+ ) ;
3173+ Ok ( ( ) )
3174+ }
3175+
3176+ async fn assert_double_negation_lead_streams (
3177+ case : & str ,
3178+ value : ScalarValue ,
3179+ nullable : bool ,
3180+ ) -> Result < ( ) > {
3181+ let ctx = pending_ordered_context ( & value, nullable) ?;
3182+ let plan = ctx
3183+ . sql (
3184+ "SELECT i, j, LEAD(j, 1) OVER (PARTITION BY -(-i), j) AS n \
3185+ FROM s LIMIT 1",
3186+ )
3187+ . await ?
3188+ . create_physical_plan ( )
3189+ . await ?;
3190+ let plan_text = displayable ( plan. as_ref ( ) ) . indent ( true ) . to_string ( ) ;
3191+ let window = plan_text
3192+ . lines ( )
3193+ . find ( |line| line. contains ( "BoundedWindowAggExec:" ) )
3194+ . expect ( "bounded window plan" ) ;
3195+ assert ! (
3196+ window. contains( "mode=[Sorted]" ) ,
3197+ "{case}: expected fully sorted window partitions:\n {plan_text}"
3198+ ) ;
3199+ assert ! (
3200+ !plan_text. contains( "SortExec:" ) ,
3201+ "{case}: existing input order should avoid a sort:\n {plan_text}"
3202+ ) ;
3203+
3204+ let mut stream = plan. execute ( 0 , ctx. task_ctx ( ) ) ?;
3205+ let output = tokio:: time:: timeout ( Duration :: from_secs ( 5 ) , stream. next ( ) )
3206+ . await
3207+ . unwrap_or_else ( |_| panic ! ( "{case}: LEAD withheld a completed partition" ) )
3208+ . expect ( "stream ended before producing a row" ) ?;
3209+ assert_eq ! ( output. num_rows( ) , 1 , "{case}" ) ;
3210+ assert_eq ! (
3211+ ScalarValue :: try_from_array( output. column( 0 ) , 0 ) ?,
3212+ value,
3213+ "{case}"
3214+ ) ;
3215+ assert_eq ! (
3216+ ScalarValue :: try_from_array( output. column( 1 ) , 0 ) ?,
3217+ ScalarValue :: Int64 ( Some ( 0 ) ) ,
3218+ "{case}"
3219+ ) ;
3220+ assert_eq ! (
3221+ ScalarValue :: try_from_array( output. column( 2 ) , 0 ) ?,
3222+ ScalarValue :: Int64 ( None ) ,
3223+ "{case}"
3224+ ) ;
3225+ Ok ( ( ) )
3226+ }
3227+
3228+ #[ tokio:: test]
3229+ async fn double_negation_lead_partition_streams_on_full_input_order ( ) -> Result < ( ) > {
3230+ for ( case, value, nullable) in [
3231+ (
3232+ "timestamp" ,
3233+ ScalarValue :: TimestampNanosecond ( Some ( 1 ) , None ) ,
3234+ false ,
3235+ ) ,
3236+ (
3237+ "nullable timestamp" ,
3238+ ScalarValue :: TimestampNanosecond ( None , None ) ,
3239+ true ,
3240+ ) ,
3241+ (
3242+ "year-month interval" ,
3243+ ScalarValue :: IntervalYearMonth ( Some ( 1 ) ) ,
3244+ false ,
3245+ ) ,
3246+ ] {
3247+ assert_double_negation_lead_streams ( case, value, nullable) . await ?;
3248+ }
3249+ Ok ( ( ) )
3250+ }
3251+
3252+ #[ tokio:: test]
3253+ async fn double_negation_window_partition_preserves_timestamp_overflow ( ) -> Result < ( ) > {
3254+ let ctx = pending_ordered_context (
3255+ & ScalarValue :: TimestampNanosecond ( Some ( i64:: MIN ) , None ) ,
3256+ false ,
3257+ ) ?;
3258+ let plan = ctx
3259+ . sql (
3260+ "SELECT LEAD(j, 1) OVER (PARTITION BY -(-i), j) AS n \
3261+ FROM s LIMIT 1",
3262+ )
3263+ . await ?
3264+ . create_physical_plan ( )
3265+ . await ?;
3266+
3267+ let mut stream = plan. execute ( 0 , ctx. task_ctx ( ) ) ?;
3268+ let err = tokio:: time:: timeout ( Duration :: from_secs ( 5 ) , stream. next ( ) )
3269+ . await
3270+ . expect ( "timestamp overflow was not reported" )
3271+ . expect ( "stream ended before reporting timestamp overflow" )
3272+ . expect_err ( "the original PARTITION BY expression must still be evaluated" ) ;
3273+ assert ! (
3274+ matches!(
3275+ err. find_root( ) ,
3276+ DataFusionError :: ArrowError ( error, _)
3277+ if matches!( error. as_ref( ) , ArrowError :: ArithmeticOverflow ( _) )
3278+ ) ,
3279+ "unexpected error: {err}"
3280+ ) ;
3281+ Ok ( ( ) )
3282+ }
3283+
29473284/// Regression: `parallelize_sorts` must not relocate a per-partition `SortExec`
29483285/// below an order-preserving `ProjectionExec` that *reorders* columns without
29493286/// remapping the sort-key column indices.
0 commit comments