@@ -2281,7 +2281,7 @@ mod tests {
22812281
22822282 use super :: * ;
22832283 use datafusion_common:: test_util:: batches_to_string;
2284- use datafusion_expr:: { and, col, lit, or} ;
2284+ use datafusion_expr:: { and, case , col, lit, or, when } ;
22852285 use datafusion_physical_expr:: utils:: collect_columns;
22862286 use insta:: assert_snapshot;
22872287
@@ -6161,4 +6161,189 @@ mod tests {
61616161 "c1_null_count@2 != row_count@3 AND c1_min@0 <= a AND a <= c1_max@1" ;
61626162 assert_eq ! ( res. to_string( ) , expected) ;
61636163 }
6164+
6165+ /// Schema used by the `CASE` tests: `c1` is the column the arms filter on,
6166+ /// `c2` only appears in the `WHEN` conditions (like a partition selector)
6167+ fn case_test_schema ( ) -> Schema {
6168+ Schema :: new ( vec ! [
6169+ Field :: new( "c1" , DataType :: Int32 , false ) ,
6170+ Field :: new( "c2" , DataType :: Int32 , false ) ,
6171+ ] )
6172+ }
6173+
6174+ /// The distinct source columns whose statistics the predicate requires
6175+ fn required_column_names ( required_columns : & RequiredColumns ) -> Vec < String > {
6176+ required_columns
6177+ . iter ( )
6178+ . map ( |( c, _t, _f) | c. name ( ) . to_string ( ) )
6179+ . unique ( )
6180+ . collect ( )
6181+ }
6182+
6183+ /// `CASE <expr> WHEN .. THEN .. ELSE .. END`: the container predicate is the
6184+ /// disjunction of the arms
6185+ #[ test]
6186+ fn test_build_predicate_expression_case_with_base_expr ( ) {
6187+ let schema = case_test_schema ( ) ;
6188+ // CASE c2 % 2 WHEN 0 THEN c1 < 10 WHEN 1 THEN c1 > 100 ELSE false END
6189+ let expr = case ( col ( "c2" ) . rem ( lit ( 2 ) ) )
6190+ . when ( lit ( 0 ) , col ( "c1" ) . lt ( lit ( 10 ) ) )
6191+ . when ( lit ( 1 ) , col ( "c1" ) . gt ( lit ( 100 ) ) )
6192+ . otherwise ( lit ( ScalarValue :: Boolean ( Some ( false ) ) ) )
6193+ . unwrap ( ) ;
6194+ let mut required_columns = RequiredColumns :: new ( ) ;
6195+ let res = test_build_predicate_expression ( & expr, & schema, & mut required_columns) ;
6196+ assert_eq ! (
6197+ res. to_string( ) ,
6198+ "c1_null_count@1 != row_count@2 AND c1_min@0 < 10 \
6199+ OR c1_null_count@1 != row_count@2 AND c1_max@3 > 100"
6200+ ) ;
6201+ // the `WHEN` values are not prunable and must not require statistics
6202+ assert_eq ! ( required_column_names( & required_columns) , vec![ "c1" ] ) ;
6203+ }
6204+
6205+ /// `CASE WHEN <cond> THEN .. END`: same, for the form without a base
6206+ /// expression
6207+ #[ test]
6208+ fn test_build_predicate_expression_case_without_base_expr ( ) {
6209+ let schema = case_test_schema ( ) ;
6210+ // CASE WHEN c2 = 0 THEN c1 < 10 WHEN c2 = 1 THEN c1 > 100 ELSE false END
6211+ let expr = when ( col ( "c2" ) . eq ( lit ( 0 ) ) , col ( "c1" ) . lt ( lit ( 10 ) ) )
6212+ . when ( col ( "c2" ) . eq ( lit ( 1 ) ) , col ( "c1" ) . gt ( lit ( 100 ) ) )
6213+ . otherwise ( lit ( ScalarValue :: Boolean ( Some ( false ) ) ) )
6214+ . unwrap ( ) ;
6215+ let mut required_columns = RequiredColumns :: new ( ) ;
6216+ let res = test_build_predicate_expression ( & expr, & schema, & mut required_columns) ;
6217+ assert_eq ! (
6218+ res. to_string( ) ,
6219+ "c1_null_count@1 != row_count@2 AND c1_min@0 < 10 \
6220+ OR c1_null_count@1 != row_count@2 AND c1_max@3 > 100"
6221+ ) ;
6222+ assert_eq ! ( required_column_names( & required_columns) , vec![ "c1" ] ) ;
6223+ }
6224+
6225+ /// A `CASE` without an `ELSE` has an implicit `NULL` else, which selects no
6226+ /// rows and so must not weaken the predicate to `true`
6227+ #[ test]
6228+ fn test_build_predicate_expression_case_without_else ( ) {
6229+ let schema = case_test_schema ( ) ;
6230+ // CASE c2 WHEN 0 THEN c1 < 10 END
6231+ let expr = case ( col ( "c2" ) )
6232+ . when ( lit ( 0 ) , col ( "c1" ) . lt ( lit ( 10 ) ) )
6233+ . end ( )
6234+ . unwrap ( ) ;
6235+ let res =
6236+ test_build_predicate_expression ( & expr, & schema, & mut RequiredColumns :: new ( ) ) ;
6237+ assert_eq ! (
6238+ res. to_string( ) ,
6239+ "c1_null_count@1 != row_count@2 AND c1_min@0 < 10"
6240+ ) ;
6241+
6242+ // an explicit NULL else behaves the same way
6243+ let expr = case ( col ( "c2" ) )
6244+ . when ( lit ( 0 ) , col ( "c1" ) . lt ( lit ( 10 ) ) )
6245+ . otherwise ( lit ( ScalarValue :: Boolean ( None ) ) )
6246+ . unwrap ( ) ;
6247+ let res =
6248+ test_build_predicate_expression ( & expr, & schema, & mut RequiredColumns :: new ( ) ) ;
6249+ assert_eq ! (
6250+ res. to_string( ) ,
6251+ "c1_null_count@1 != row_count@2 AND c1_min@0 < 10"
6252+ ) ;
6253+ }
6254+
6255+ /// `lit(false)` arms (e.g. an empty partition) drop out of the disjunction
6256+ #[ test]
6257+ fn test_build_predicate_expression_case_with_false_arms ( ) {
6258+ let schema = case_test_schema ( ) ;
6259+ // CASE c2 WHEN 0 THEN false WHEN 1 THEN c1 < 10 ELSE false END
6260+ let expr = case ( col ( "c2" ) )
6261+ . when ( lit ( 0 ) , lit ( ScalarValue :: Boolean ( Some ( false ) ) ) )
6262+ . when ( lit ( 1 ) , col ( "c1" ) . lt ( lit ( 10 ) ) )
6263+ . otherwise ( lit ( ScalarValue :: Boolean ( Some ( false ) ) ) )
6264+ . unwrap ( ) ;
6265+ let res =
6266+ test_build_predicate_expression ( & expr, & schema, & mut RequiredColumns :: new ( ) ) ;
6267+ assert_eq ! (
6268+ res. to_string( ) ,
6269+ "c1_null_count@1 != row_count@2 AND c1_min@0 < 10"
6270+ ) ;
6271+ }
6272+
6273+ /// If every arm is `false` no row can pass the predicate, so every
6274+ /// container can be pruned
6275+ #[ test]
6276+ fn test_build_predicate_expression_case_all_false_arms ( ) {
6277+ let schema = case_test_schema ( ) ;
6278+ let expr = case ( col ( "c2" ) )
6279+ . when ( lit ( 0 ) , lit ( ScalarValue :: Boolean ( Some ( false ) ) ) )
6280+ . otherwise ( lit ( ScalarValue :: Boolean ( Some ( false ) ) ) )
6281+ . unwrap ( ) ;
6282+ let mut required_columns = RequiredColumns :: new ( ) ;
6283+ let res = test_build_predicate_expression ( & expr, & schema, & mut required_columns) ;
6284+ assert_eq ! ( res. to_string( ) , "false" ) ;
6285+ assert ! ( required_column_names( & required_columns) . is_empty( ) ) ;
6286+ }
6287+
6288+ /// An arm that can not be rewritten degrades the whole `CASE` to `true`
6289+ /// (i.e. no pruning), never to `false`
6290+ #[ test]
6291+ fn test_build_predicate_expression_case_with_unhandled_arm ( ) {
6292+ let schema = case_test_schema ( ) ;
6293+ // CASE c2 WHEN 0 THEN c1 < 10 WHEN 1 THEN array_has([1], c1) END
6294+ let expr = case ( col ( "c2" ) )
6295+ . when ( lit ( 0 ) , col ( "c1" ) . lt ( lit ( 10 ) ) )
6296+ . when ( lit ( 1 ) , array_has ( make_array ( vec ! [ lit( 1 ) ] ) , col ( "c1" ) ) )
6297+ . end ( )
6298+ . unwrap ( ) ;
6299+ let res =
6300+ test_build_predicate_expression ( & expr, & schema, & mut RequiredColumns :: new ( ) ) ;
6301+ assert_eq ! ( res. to_string( ) , "true" ) ;
6302+
6303+ // ... and the same when the unhandled arm comes first
6304+ let expr = case ( col ( "c2" ) )
6305+ . when ( lit ( 0 ) , array_has ( make_array ( vec ! [ lit( 1 ) ] ) , col ( "c1" ) ) )
6306+ . when ( lit ( 1 ) , col ( "c1" ) . lt ( lit ( 10 ) ) )
6307+ . end ( )
6308+ . unwrap ( ) ;
6309+ let res =
6310+ test_build_predicate_expression ( & expr, & schema, & mut RequiredColumns :: new ( ) ) ;
6311+ assert_eq ! ( res. to_string( ) , "true" ) ;
6312+ }
6313+
6314+ /// End to end pruning with a `CASE` predicate of the shape produced by a
6315+ /// dynamic filter pushed down from a hash partitioned join
6316+ #[ test]
6317+ fn prune_case_partitioned_ranges ( ) {
6318+ let schema = Arc :: new ( Schema :: new ( vec ! [
6319+ Field :: new( "c1" , DataType :: Int32 , false ) ,
6320+ Field :: new( "c2" , DataType :: Int32 , false ) ,
6321+ ] ) ) ;
6322+ // CASE c2 % 2
6323+ // WHEN 0 THEN c1 >= 0 AND c1 <= 10
6324+ // WHEN 1 THEN c1 >= 100 AND c1 <= 110
6325+ // ELSE false
6326+ // END
6327+ let expr = case ( col ( "c2" ) . rem ( lit ( 2 ) ) )
6328+ . when (
6329+ lit ( 0 ) ,
6330+ col ( "c1" ) . gt_eq ( lit ( 0 ) ) . and ( col ( "c1" ) . lt_eq ( lit ( 10 ) ) ) ,
6331+ )
6332+ . when (
6333+ lit ( 1 ) ,
6334+ col ( "c1" ) . gt_eq ( lit ( 100 ) ) . and ( col ( "c1" ) . lt_eq ( lit ( 110 ) ) ) ,
6335+ )
6336+ . otherwise ( lit ( ScalarValue :: Boolean ( Some ( false ) ) ) )
6337+ . unwrap ( ) ;
6338+
6339+ let statistics = TestStatistics :: new ( ) . with (
6340+ "c1" ,
6341+ ContainerStats :: new_i32 (
6342+ vec ! [ Some ( 0 ) , Some ( 50 ) , Some ( 100 ) , Some ( 200 ) ] , // min
6343+ vec ! [ Some ( 5 ) , Some ( 60 ) , Some ( 105 ) , Some ( 300 ) ] , // max
6344+ ) ,
6345+ ) ;
6346+ // only the container that overlaps neither range is pruned
6347+ prune_with_expr ( expr, & schema, & statistics, & [ true , false , true , false ] ) ;
6348+ }
61646349}
0 commit comments