@@ -187,3 +187,267 @@ impl<S: PruningStatistics + ?Sized> LazyStatisticsBatch<'_, S> {
187187 } )
188188 }
189189}
190+
191+ #[ cfg( test) ]
192+ mod tests {
193+ use std:: collections:: { HashMap , HashSet } ;
194+
195+ use arrow:: array:: { ArrayRef , BooleanArray , Int32Array } ;
196+ use arrow:: datatypes:: { DataType , Field , Schema , SchemaRef } ;
197+ use datafusion_common:: { Column , ScalarValue } ;
198+ use datafusion_expr:: { Expr , col, lit} ;
199+ use datafusion_physical_expr:: expressions:: DynamicFilterPhysicalExpr ;
200+ use datafusion_physical_expr:: planner:: logical2physical;
201+ use datafusion_physical_expr:: utils:: collect_columns;
202+
203+ use super :: * ;
204+ use crate :: PruningPredicateBuilder ;
205+
206+ /// Int32 min/max statistics and `contained` results per column.
207+ #[ derive( Default ) ]
208+ struct TestStats {
209+ num_containers : usize ,
210+ min_max : HashMap < String , ( ArrayRef , ArrayRef ) > ,
211+ /// Returned by `contained` for any set of values
212+ contained : HashMap < String , BooleanArray > ,
213+ }
214+
215+ impl TestStats {
216+ fn new ( num_containers : usize ) -> Self {
217+ Self {
218+ num_containers,
219+ ..Default :: default ( )
220+ }
221+ }
222+
223+ fn with_min_max ( mut self , column : & str , min : & [ i32 ] , max : & [ i32 ] ) -> Self {
224+ assert_eq ! ( min. len( ) , self . num_containers) ;
225+ assert_eq ! ( max. len( ) , self . num_containers) ;
226+ let min = Arc :: new ( Int32Array :: from ( min. to_vec ( ) ) ) as ArrayRef ;
227+ let max = Arc :: new ( Int32Array :: from ( max. to_vec ( ) ) ) as ArrayRef ;
228+ self . min_max . insert ( column. to_string ( ) , ( min, max) ) ;
229+ self
230+ }
231+
232+ fn with_contained ( mut self , column : & str , contained : & [ bool ] ) -> Self {
233+ assert_eq ! ( contained. len( ) , self . num_containers) ;
234+ self . contained
235+ . insert ( column. to_string ( ) , BooleanArray :: from ( contained. to_vec ( ) ) ) ;
236+ self
237+ }
238+ }
239+
240+ impl PruningStatistics for TestStats {
241+ fn min_values ( & self , column : & Column ) -> Option < ArrayRef > {
242+ self . min_max
243+ . get ( column. name ( ) )
244+ . map ( |( min, _) | Arc :: clone ( min) )
245+ }
246+
247+ fn max_values ( & self , column : & Column ) -> Option < ArrayRef > {
248+ self . min_max
249+ . get ( column. name ( ) )
250+ . map ( |( _, max) | Arc :: clone ( max) )
251+ }
252+
253+ fn num_containers ( & self ) -> usize {
254+ self . num_containers
255+ }
256+
257+ fn null_counts ( & self , _column : & Column ) -> Option < ArrayRef > {
258+ None
259+ }
260+
261+ fn row_counts ( & self ) -> Option < ArrayRef > {
262+ None
263+ }
264+
265+ fn contained (
266+ & self ,
267+ column : & Column ,
268+ _values : & HashSet < ScalarValue > ,
269+ ) -> Option < BooleanArray > {
270+ self . contained . get ( column. name ( ) ) . cloned ( )
271+ }
272+ }
273+
274+ fn schema ( ) -> SchemaRef {
275+ Arc :: new ( Schema :: new ( vec ! [
276+ Field :: new( "a" , DataType :: Int32 , true ) ,
277+ Field :: new( "b" , DataType :: Int32 , true ) ,
278+ ] ) )
279+ }
280+
281+ /// Three containers:
282+ ///
283+ /// | container | a | b |
284+ /// |-----------|------------|----------|
285+ /// | 0 | 0..=3 | 0..=1 |
286+ /// | 1 | 6..=10 | 20..=30 |
287+ /// | 2 | 200..=300 | 0..=1 |
288+ fn stats ( ) -> TestStats {
289+ TestStats :: new ( 3 )
290+ . with_min_max ( "a" , & [ 0 , 6 , 200 ] , & [ 3 , 10 , 300 ] )
291+ . with_min_max ( "b" , & [ 0 , 20 , 0 ] , & [ 1 , 30 , 1 ] )
292+ }
293+
294+ fn stats_of ( pruned : usize , kept : usize ) -> ConjunctPruningStats {
295+ ConjunctPruningStats {
296+ containers_pruned : pruned,
297+ containers_kept : kept,
298+ }
299+ }
300+
301+ fn physical ( expr : & Expr ) -> Arc < dyn PhysicalExpr > {
302+ logical2physical ( expr, & schema ( ) )
303+ }
304+
305+ /// Prunes with per-conjunct stats, and checks that the pruning result is
306+ /// the same as the result of a predicate built without conjunct stats.
307+ fn prune_with_conjunct_stats (
308+ predicate : Arc < dyn PhysicalExpr > ,
309+ statistics : & TestStats ,
310+ ) -> ( Vec < bool > , Vec < ConjunctPruningStats > ) {
311+ let plain = PruningPredicateBuilder :: new ( )
312+ . with_file_schema ( schema ( ) )
313+ . try_build ( Arc :: clone ( & predicate) )
314+ . unwrap ( ) ;
315+ let with_stats = PruningPredicateBuilder :: new ( )
316+ . with_file_schema ( schema ( ) )
317+ . with_conjunct_stats ( true )
318+ . try_build ( predicate)
319+ . unwrap ( ) ;
320+
321+ let ( result, conjunct_stats) =
322+ with_stats. prune_with_conjunct_stats ( statistics) . unwrap ( ) ;
323+ let expected = plain. prune ( statistics) . unwrap ( ) ;
324+ assert_eq ! (
325+ result, expected,
326+ "result differs from PruningPredicate::prune"
327+ ) ;
328+ assert_eq ! ( with_stats. prune( statistics) . unwrap( ) , expected) ;
329+ for stats in & conjunct_stats {
330+ assert_eq ! (
331+ stats. containers_pruned + stats. containers_kept,
332+ statistics. num_containers( )
333+ ) ;
334+ }
335+ ( result, conjunct_stats)
336+ }
337+
338+ #[ test]
339+ fn and_of_conjuncts ( ) {
340+ // Each conjunct prunes a different container.
341+ let predicate = col ( "a" )
342+ . gt ( lit ( 5 ) )
343+ . and ( col ( "b" ) . lt ( lit ( 10 ) ) )
344+ . and ( col ( "a" ) . lt ( lit ( 100 ) ) ) ;
345+ let ( result, conjunct_stats) =
346+ prune_with_conjunct_stats ( physical ( & predicate) , & stats ( ) ) ;
347+ assert_eq ! ( result, vec![ false , false , false ] ) ;
348+ assert_eq ! (
349+ conjunct_stats,
350+ vec![ stats_of( 1 , 2 ) , stats_of( 1 , 2 ) , stats_of( 1 , 2 ) ]
351+ ) ;
352+ }
353+
354+ #[ test]
355+ fn stats_do_not_depend_on_conjunct_order ( ) {
356+ // `a > 1000` prunes all containers. The other conjuncts are still
357+ // evaluated on all containers.
358+ let predicate = col ( "a" )
359+ . gt ( lit ( 1000 ) )
360+ . and ( col ( "b" ) . lt ( lit ( 10 ) ) )
361+ . and ( col ( "a" ) . gt ( lit ( 5 ) ) ) ;
362+ let ( result, conjunct_stats) =
363+ prune_with_conjunct_stats ( physical ( & predicate) , & stats ( ) ) ;
364+ assert_eq ! ( result, vec![ false , false , false ] ) ;
365+ assert_eq ! (
366+ conjunct_stats,
367+ vec![ stats_of( 3 , 0 ) , stats_of( 1 , 2 ) , stats_of( 1 , 2 ) ]
368+ ) ;
369+ }
370+
371+ #[ test]
372+ fn single_conjunct ( ) {
373+ let ( result, conjunct_stats) =
374+ prune_with_conjunct_stats ( physical ( & col ( "a" ) . gt ( lit ( 5 ) ) ) , & stats ( ) ) ;
375+ assert_eq ! ( result, vec![ false , true , true ] ) ;
376+ assert_eq ! ( conjunct_stats, vec![ stats_of( 1 , 2 ) ] ) ;
377+ }
378+
379+ #[ test]
380+ fn conjunct_that_can_not_be_rewritten_keeps_all_containers ( ) {
381+ // `a + b = 3` refers to two columns, so statistics can not prune it.
382+ let predicate = ( col ( "a" ) + col ( "b" ) ) . eq ( lit ( 3 ) ) . and ( col ( "a" ) . gt ( lit ( 5 ) ) ) ;
383+ let ( result, conjunct_stats) =
384+ prune_with_conjunct_stats ( physical ( & predicate) , & stats ( ) ) ;
385+ assert_eq ! ( result, vec![ false , true , true ] ) ;
386+ assert_eq ! ( conjunct_stats, vec![ stats_of( 0 , 3 ) , stats_of( 1 , 2 ) ] ) ;
387+ }
388+
389+ #[ test]
390+ fn or_and_not_are_one_conjunct ( ) {
391+ // The `OR` is not split, and the `AND` inside the `NOT` is not split.
392+ let predicate =
393+ col ( "a" )
394+ . lt ( lit ( 3 ) )
395+ . or ( col ( "a" ) . gt ( lit ( 250 ) ) )
396+ . and ( Expr :: Not ( Box :: new (
397+ col ( "a" ) . eq ( lit ( 1 ) ) . and ( col ( "b" ) . eq ( lit ( 1 ) ) ) ,
398+ ) ) ) ;
399+ let ( result, conjunct_stats) =
400+ prune_with_conjunct_stats ( physical ( & predicate) , & stats ( ) ) ;
401+ assert_eq ! ( result, vec![ true , false , true ] ) ;
402+ assert_eq ! ( conjunct_stats, vec![ stats_of( 1 , 2 ) , stats_of( 0 , 3 ) ] ) ;
403+ }
404+
405+ #[ test]
406+ fn dynamic_filter_is_one_conjunct ( ) {
407+ // The dynamic filter contains an `AND`, but it is one conjunct.
408+ let inner = physical ( & col ( "a" ) . gt ( lit ( 5 ) ) . and ( col ( "b" ) . lt ( lit ( 10 ) ) ) ) ;
409+ let children = collect_columns ( & inner)
410+ . into_iter ( )
411+ . map ( |c| Arc :: new ( c) as Arc < dyn PhysicalExpr > )
412+ . collect ( ) ;
413+ let dynamic_filter = Arc :: new ( DynamicFilterPhysicalExpr :: new ( children, inner) )
414+ as Arc < dyn PhysicalExpr > ;
415+ let predicate = datafusion_physical_expr:: conjunction ( [
416+ physical ( & col ( "a" ) . lt ( lit ( 100 ) ) ) ,
417+ dynamic_filter,
418+ ] ) ;
419+
420+ let ( result, conjunct_stats) = prune_with_conjunct_stats ( predicate, & stats ( ) ) ;
421+ assert_eq ! ( result, vec![ false , false , false ] ) ;
422+ // Container 0 fails `a > 5` and container 1 fails `b < 10`.
423+ assert_eq ! ( conjunct_stats, vec![ stats_of( 1 , 2 ) , stats_of( 2 , 1 ) ] ) ;
424+ }
425+
426+ #[ test]
427+ fn literal_guarantees_are_attributed_per_conjunct ( ) {
428+ // No min/max statistics, only `contained` (for example Bloom filters).
429+ let statistics = TestStats :: new ( 3 )
430+ . with_contained ( "a" , & [ false , true , true ] )
431+ . with_contained ( "b" , & [ true , true , false ] ) ;
432+ let predicate = col ( "a" )
433+ . in_list ( vec ! [ lit( 7 ) , lit( 8 ) ] , false )
434+ . and ( col ( "b" ) . eq ( lit ( 1 ) ) ) ;
435+ let ( result, conjunct_stats) =
436+ prune_with_conjunct_stats ( physical ( & predicate) , & statistics) ;
437+ assert_eq ! ( result, vec![ false , true , false ] ) ;
438+ assert_eq ! ( conjunct_stats, vec![ stats_of( 1 , 2 ) , stats_of( 1 , 2 ) ] ) ;
439+ }
440+
441+ #[ test]
442+ fn error_without_conjunct_stats ( ) {
443+ let predicate = PruningPredicateBuilder :: new ( )
444+ . with_file_schema ( schema ( ) )
445+ . try_build ( physical ( & col ( "a" ) . gt ( lit ( 5 ) ) ) )
446+ . unwrap ( ) ;
447+ let err = predicate. prune_with_conjunct_stats ( & stats ( ) ) . unwrap_err ( ) ;
448+ assert ! (
449+ err. to_string( ) . contains( "with_conjunct_stats" ) ,
450+ "unexpected error: {err}"
451+ ) ;
452+ }
453+ }
0 commit comments