@@ -1209,6 +1209,66 @@ mod tests {
12091209 Ok ( ( ) )
12101210 }
12111211
1212+ #[ tokio:: test]
1213+ async fn test_filter_statistics_ceil_scalar_fn ( ) -> Result < ( ) > {
1214+ // Table: x Float64, min=8.0, max=16.0, 100 rows.
1215+ // Filter: ceil(x) > 12.0
1216+ //
1217+ // The range [8.0, 16.0] lies within a single IEEE-754 binade so
1218+ // Float64 cardinality is proportional to the value range, making
1219+ // the selectivity estimate predictable.
1220+ //
1221+ // With check_support recognising ScalarFunctionExpr and CeilFunc
1222+ // implementing evaluate_bounds/propagate_constraints the solver
1223+ // narrows x to roughly [11.0, 16.0]:
1224+ // ceil(x) > 12 → x ∈ (11, 16] → conservative [11, 16]
1225+ // selectivity ≈ (16−11)/(16−8) = 5/8 = 0.625 → ~62 rows
1226+ //
1227+ // Without the fix the estimate stays at 100 (no interval analysis).
1228+ let schema = Schema :: new ( vec ! [ Field :: new( "x" , DataType :: Float64 , false ) ] ) ;
1229+ let input = Arc :: new ( StatisticsExec :: new (
1230+ Statistics {
1231+ num_rows : Precision :: Inexact ( 100 ) ,
1232+ total_byte_size : Precision :: Absent ,
1233+ column_statistics : vec ! [ ColumnStatistics {
1234+ min_value: Precision :: Inexact ( ScalarValue :: Float64 ( Some ( 8.0 ) ) ) ,
1235+ max_value: Precision :: Inexact ( ScalarValue :: Float64 ( Some ( 16.0 ) ) ) ,
1236+ ..Default :: default ( )
1237+ } ] ,
1238+ } ,
1239+ schema. clone ( ) ,
1240+ ) ) ;
1241+
1242+ let x = col ( "x" , & schema) ?;
1243+ let ceil_udf = datafusion_functions:: math:: ceil ( ) ;
1244+ let config = Arc :: new ( ConfigOptions :: new ( ) ) ;
1245+ let ceil_x: Arc < dyn PhysicalExpr > =
1246+ Arc :: new ( datafusion_physical_expr:: ScalarFunctionExpr :: try_new (
1247+ Arc :: clone ( & ceil_udf) ,
1248+ vec ! [ x] ,
1249+ & schema,
1250+ config,
1251+ ) ?) ;
1252+ let predicate = binary ( ceil_x, Operator :: Gt , lit ( 12.0f64 ) , & schema) ?;
1253+
1254+ let filter = Arc :: new ( FilterExec :: try_new ( predicate, input) ?) ;
1255+ let statistics = filter. partition_statistics ( None ) ?;
1256+
1257+ let num_rows = statistics. num_rows . get_value ( ) . copied ( ) . unwrap_or ( 100 ) ;
1258+ // Interval analysis must narrow the estimate below the full 100-row input.
1259+ assert ! (
1260+ num_rows < 100 ,
1261+ "expected interval analysis to narrow row estimate, got {num_rows}"
1262+ ) ;
1263+ // The conservative bound is x ∈ [11, 16] out of [8, 16] → ~62 rows.
1264+ // Allow a generous range to be robust to float-cardinality rounding.
1265+ assert ! (
1266+ num_rows >= 50 ,
1267+ "expected at least 50 rows after ceil(x) > 12.0 on [8,16], got {num_rows}"
1268+ ) ;
1269+ Ok ( ( ) )
1270+ }
1271+
12121272 #[ tokio:: test]
12131273 async fn test_filter_statistics_basic_expr ( ) -> Result < ( ) > {
12141274 // Table:
0 commit comments