1use std::collections::{HashMap, HashSet};
23
24use arrow::array::{ArrayRef, BooleanArray};
25use datafusion_common::pruning::PruningStatistics;
26use datafusion_common::{Column, ScalarValue};
27use parquet::basic::Type;
28use parquet::bloom_filter::Sbbf;
29use parquet::data_type::Decimal;
30
31#[derive(Debug, Clone, Default)]
36pub struct BloomFilterStatistics {
37 column_sbbf: HashMap<String, ColumnBloomFilter>,
39}
40
41#[derive(Debug, Clone)]
42struct ColumnBloomFilter {
43 sbbf: Sbbf,
45 physical_type: Type,
47 type_length: i32,
49}
50
51impl BloomFilterStatistics {
52 pub fn new() -> Self {
54 Default::default()
55 }
56
57 pub fn with_capacity(capacity: usize) -> Self {
59 Self {
60 column_sbbf: HashMap::with_capacity(capacity),
61 }
62 }
63
64 pub fn insert(
67 &mut self,
68 column: impl Into<String>,
69 sbbf: Sbbf,
70 ty: Type,
71 type_length: i32,
72 ) {
73 self.column_sbbf.insert(
74 column.into(),
75 ColumnBloomFilter {
76 sbbf,
77 physical_type: ty,
78 type_length,
79 },
80 );
81 }
82
83 fn check_scalar(
88 sbbf: &Sbbf,
89 value: &ScalarValue,
90 parquet_type: &Type,
91 type_length: i32,
92 ) -> bool {
93 match value {
94 ScalarValue::Utf8(Some(v))
95 | ScalarValue::Utf8View(Some(v))
96 | ScalarValue::LargeUtf8(Some(v)) => sbbf.check(&v.as_str()),
97 ScalarValue::Binary(Some(v))
98 | ScalarValue::BinaryView(Some(v))
99 | ScalarValue::LargeBinary(Some(v)) => sbbf.check(v),
100 ScalarValue::FixedSizeBinary(_size, Some(v)) => sbbf.check(v),
101 ScalarValue::Boolean(Some(v)) => sbbf.check(v),
102 ScalarValue::Float64(Some(v)) => sbbf.check(v),
103 ScalarValue::Float32(Some(v)) => sbbf.check(v),
104 ScalarValue::Int64(Some(v)) => sbbf.check(v),
105 ScalarValue::Int32(Some(v)) => sbbf.check(v),
106 ScalarValue::UInt64(Some(v)) => sbbf.check(v),
107 ScalarValue::UInt32(Some(v)) => sbbf.check(v),
108 ScalarValue::Decimal128(Some(v), p, s) => match parquet_type {
109 Type::INT32 => {
110 if *p > 9 {
113 return true;
118 }
119 let b = (*v as i32).to_le_bytes();
120 let decimal = Decimal::Int32 {
122 value: b,
123 precision: *p as i32,
124 scale: *s as i32,
125 };
126 sbbf.check(&decimal)
127 }
128 Type::INT64 => {
129 if *p > 18 {
130 return true;
131 }
132 let b = (*v as i64).to_le_bytes();
133 let decimal = Decimal::Int64 {
134 value: b,
135 precision: *p as i32,
136 scale: *s as i32,
137 };
138 sbbf.check(&decimal)
139 }
140 Type::FIXED_LEN_BYTE_ARRAY => {
141 let Ok(type_length) = usize::try_from(type_length) else {
142 return true;
143 };
144 if type_length == 0 || type_length > 16 {
145 return true;
146 }
147 let b = v.to_be_bytes();
148 let b = b[(b.len() - type_length)..].to_vec();
149 let decimal = Decimal::Bytes {
151 value: b.into(),
152 precision: *p as i32,
153 scale: *s as i32,
154 };
155 sbbf.check(&decimal)
156 }
157 _ => true,
158 },
159 ScalarValue::Dictionary(_, inner) => BloomFilterStatistics::check_scalar(
160 sbbf,
161 inner,
162 parquet_type,
163 type_length,
164 ),
165 _ => true,
166 }
167 }
168}
169
170impl PruningStatistics for BloomFilterStatistics {
171 fn min_values(&self, _column: &Column) -> Option<ArrayRef> {
172 None
173 }
174
175 fn max_values(&self, _column: &Column) -> Option<ArrayRef> {
176 None
177 }
178
179 fn num_containers(&self) -> usize {
180 1
181 }
182
183 fn null_counts(&self, _column: &Column) -> Option<ArrayRef> {
184 None
185 }
186
187 fn row_counts(&self) -> Option<ArrayRef> {
188 None
189 }
190
191 fn contained(
197 &self,
198 column: &Column,
199 values: &HashSet<ScalarValue>,
200 ) -> Option<BooleanArray> {
201 let column_bloom_filter = self.column_sbbf.get(column.name.as_str())?;
202
203 let known_not_present = values
209 .iter()
210 .map(|value| {
211 BloomFilterStatistics::check_scalar(
212 &column_bloom_filter.sbbf,
213 value,
214 &column_bloom_filter.physical_type,
215 column_bloom_filter.type_length,
216 )
217 })
218 .all(|v| !v);
221
222 let contains = if known_not_present {
223 Some(false)
224 } else {
225 None
229 };
230
231 Some(BooleanArray::from(vec![contains]))
232 }
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238
239 use std::sync::Arc;
240
241 use crate::reader::ParquetFileReader;
242 use crate::test_util::ExpectedPruning;
243 use crate::{ParquetAccessPlan, ParquetFileMetrics, RowGroupAccessPlanFilter};
244
245 use arrow::array::Decimal128Array;
246 use arrow::datatypes::{DataType, Field, Schema};
247 use bytes::{BufMut, BytesMut};
248 use datafusion_common::Result;
249 use datafusion_expr::{Expr, col, lit};
250 use datafusion_physical_expr::planner::logical2physical;
251 use datafusion_physical_plan::metrics::ExecutionPlanMetricsSet;
252 use datafusion_pruning::{PruningPredicate, PruningPredicateBuilder};
253 use object_store::{ObjectStore, ObjectStoreExt};
254 use parquet::arrow::ArrowWriter;
255 use parquet::arrow::ParquetRecordBatchStreamBuilder;
256 use parquet::file::properties::{EnabledStatistics, WriterProperties};
257
258 fn build_test_pruning_predicate(
259 expr: Arc<dyn datafusion_physical_plan::PhysicalExpr>,
260 schema: Schema,
261 ) -> PruningPredicate {
262 PruningPredicateBuilder::new()
263 .with_file_schema(Arc::new(schema))
264 .try_build(expr)
265 .unwrap()
266 }
267
268 #[tokio::test]
269 async fn test_row_group_bloom_filter_pruning_predicate_simple_expr() {
270 BloomFilterTest::new_data_index_bloom_encoding_stats()
271 .with_expect_all_pruned()
272 .run(col(r#""String""#).eq(lit("Hello_Not_Exists")))
274 .await
275 }
276
277 #[tokio::test]
278 async fn test_row_group_bloom_filter_pruning_predicate_multiple_expr() {
279 BloomFilterTest::new_data_index_bloom_encoding_stats()
280 .with_expect_all_pruned()
281 .run(
283 lit("1").eq(lit("1")).and(
284 col(r#""String""#)
285 .eq(lit("Hello_Not_Exists"))
286 .or(col(r#""String""#).eq(lit("Hello_Not_Exists2"))),
287 ),
288 )
289 .await
290 }
291
292 #[tokio::test]
293 async fn test_row_group_bloom_filter_pruning_predicate_multiple_expr_view() {
294 BloomFilterTest::new_data_index_bloom_encoding_stats()
295 .with_expect_all_pruned()
296 .run(
298 lit("1").eq(lit("1")).and(
299 col(r#""String""#)
300 .eq(Expr::Literal(
301 ScalarValue::Utf8View(Some(String::from("Hello_Not_Exists"))),
302 None,
303 ))
304 .or(col(r#""String""#).eq(Expr::Literal(
305 ScalarValue::Utf8View(Some(String::from(
306 "Hello_Not_Exists2",
307 ))),
308 None,
309 ))),
310 ),
311 )
312 .await
313 }
314
315 #[tokio::test]
316 async fn test_row_group_bloom_filter_pruning_predicate_sql_in() {
317 let testdata = datafusion_common::test_util::parquet_test_data();
319 let file_name = "data_index_bloom_encoding_stats.parquet";
320 let path = format!("{testdata}/{file_name}");
321 let data = bytes::Bytes::from(std::fs::read(path).unwrap());
322
323 let schema = Schema::new(vec![Field::new("String", DataType::Utf8, false)]);
325
326 let expr = col(r#""String""#).in_list(
327 (1..25)
328 .map(|i| lit(format!("Hello_Not_Exists{i}")))
329 .collect::<Vec<_>>(),
330 false,
331 );
332 let expr = logical2physical(&expr, &schema);
333 let pruning_predicate = build_test_pruning_predicate(expr, schema);
334
335 let pruned_row_groups = test_row_group_bloom_filter_pruning_predicate(
336 file_name,
337 data,
338 &pruning_predicate,
339 )
340 .await
341 .unwrap();
342 assert!(
343 pruned_row_groups
344 .access_plan()
345 .row_group_indexes()
346 .is_empty()
347 );
348 }
349
350 #[tokio::test]
351 async fn test_row_group_bloom_filter_pruning_predicate_with_exists_value() {
352 BloomFilterTest::new_data_index_bloom_encoding_stats()
353 .with_expect_none_pruned()
354 .run(col(r#""String""#).eq(lit("Hello")))
356 .await
357 }
358
359 #[tokio::test]
360 async fn test_row_group_bloom_filter_pruning_predicate_with_exists_2_values() {
361 BloomFilterTest::new_data_index_bloom_encoding_stats()
362 .with_expect_none_pruned()
363 .run(
365 col(r#""String""#)
366 .eq(lit("Hello"))
367 .or(col(r#""String""#).eq(lit("the quick"))),
368 )
369 .await
370 }
371
372 #[tokio::test]
373 async fn test_row_group_bloom_filter_pruning_predicate_with_exists_3_values() {
374 BloomFilterTest::new_data_index_bloom_encoding_stats()
375 .with_expect_none_pruned()
376 .run(
378 col(r#""String""#)
379 .eq(lit("Hello"))
380 .or(col(r#""String""#).eq(lit("the quick")))
381 .or(col(r#""String""#).eq(lit("are you"))),
382 )
383 .await
384 }
385
386 #[tokio::test]
387 async fn test_row_group_bloom_filter_pruning_predicate_with_exists_3_values_view() {
388 BloomFilterTest::new_data_index_bloom_encoding_stats()
389 .with_expect_none_pruned()
390 .run(
392 col(r#""String""#)
393 .eq(Expr::Literal(
394 ScalarValue::Utf8View(Some(String::from("Hello"))),
395 None,
396 ))
397 .or(col(r#""String""#).eq(Expr::Literal(
398 ScalarValue::Utf8View(Some(String::from("the quick"))),
399 None,
400 )))
401 .or(col(r#""String""#).eq(Expr::Literal(
402 ScalarValue::Utf8View(Some(String::from("are you"))),
403 None,
404 ))),
405 )
406 .await
407 }
408
409 #[tokio::test]
410 async fn test_row_group_bloom_filter_pruning_predicate_with_or_not_eq() {
411 BloomFilterTest::new_data_index_bloom_encoding_stats()
412 .with_expect_none_pruned()
413 .run(
415 col(r#""String""#)
416 .not_eq(lit("foo"))
417 .or(col(r#""String""#).not_eq(lit("bar"))),
418 )
419 .await
420 }
421
422 #[tokio::test]
423 async fn test_row_group_bloom_filter_pruning_predicate_without_bloom_filter() {
424 BloomFilterTest::new_all_types()
426 .with_expect_none_pruned()
427 .run(col(r#""string_col""#).eq(lit("0")))
428 .await
429 }
430
431 #[tokio::test]
432 async fn test_row_group_bloom_filter_pruning_predicate_decimal128() {
433 for precision in [19, 20, 21, 28, 38] {
434 let scale = 2;
435 let data = parquet_decimal128_with_bloom_filter(
436 precision,
437 scale,
438 vec![100, 200, 300, 400, 500, 600],
439 );
440 let schema = Schema::new(vec![Field::new(
441 "decimal_col",
442 DataType::Decimal128(precision, scale),
443 true,
444 )]);
445 let expr = col("decimal_col").eq(Expr::Literal(
446 ScalarValue::Decimal128(Some(500), precision, scale),
447 None,
448 ));
449 let expr = logical2physical(&expr, &schema);
450 let pruning_predicate = build_test_pruning_predicate(expr, schema);
451
452 let pruned_row_groups = test_row_group_bloom_filter_pruning_predicate(
453 &format!("decimal128-{precision}.parquet"),
454 data,
455 &pruning_predicate,
456 )
457 .await
458 .unwrap();
459
460 assert_eq!(
461 pruned_row_groups.access_plan().row_group_indexes(),
462 vec![2],
463 "precision {precision}"
464 );
465 }
466 }
467
468 #[tokio::test]
469 async fn test_row_group_bloom_filter_pruning_predicate_negative_decimal128() {
470 for precision in [19, 20, 21, 28, 38] {
471 let scale = 2;
472 let data = parquet_decimal128_with_bloom_filter(
473 precision,
474 scale,
475 vec![-100, -200, -300, -400, -500, -600],
476 );
477 let schema = Schema::new(vec![Field::new(
478 "decimal_col",
479 DataType::Decimal128(precision, scale),
480 true,
481 )]);
482 let expr = col("decimal_col").eq(Expr::Literal(
483 ScalarValue::Decimal128(Some(-500), precision, scale),
484 None,
485 ));
486 let expr = logical2physical(&expr, &schema);
487 let pruning_predicate = build_test_pruning_predicate(expr, schema);
488
489 let pruned_row_groups = test_row_group_bloom_filter_pruning_predicate(
490 &format!("negative-decimal128-{precision}.parquet"),
491 data,
492 &pruning_predicate,
493 )
494 .await
495 .unwrap();
496
497 assert_eq!(
498 pruned_row_groups.access_plan().row_group_indexes(),
499 vec![2],
500 "precision {precision}"
501 );
502 }
503 }
504
505 struct BloomFilterTest {
506 file_name: String,
507 schema: Schema,
508 post_pruning_row_groups: ExpectedPruning,
510 }
511
512 impl BloomFilterTest {
513 fn new_data_index_bloom_encoding_stats() -> Self {
537 Self {
538 file_name: String::from("data_index_bloom_encoding_stats.parquet"),
539 schema: Schema::new(vec![Field::new("String", DataType::Utf8, false)]),
540 post_pruning_row_groups: ExpectedPruning::None,
541 }
542 }
543
544 fn new_all_types() -> Self {
546 Self {
547 file_name: String::from("alltypes_plain.parquet"),
548 schema: Schema::new(vec![Field::new(
549 "string_col",
550 DataType::Utf8,
551 false,
552 )]),
553 post_pruning_row_groups: ExpectedPruning::None,
554 }
555 }
556
557 pub fn with_expect_all_pruned(mut self) -> Self {
559 self.post_pruning_row_groups = ExpectedPruning::All;
560 self
561 }
562
563 pub fn with_expect_none_pruned(mut self) -> Self {
565 self.post_pruning_row_groups = ExpectedPruning::None;
566 self
567 }
568
569 async fn run(self, expr: Expr) {
571 let Self {
572 file_name,
573 schema,
574 post_pruning_row_groups,
575 } = self;
576
577 let testdata = datafusion_common::test_util::parquet_test_data();
578 let path = format!("{testdata}/{file_name}");
579 let data = bytes::Bytes::from(std::fs::read(path).unwrap());
580
581 let expr = logical2physical(&expr, &schema);
582 let pruning_predicate = build_test_pruning_predicate(expr, schema);
583
584 let pruned_row_groups = test_row_group_bloom_filter_pruning_predicate(
585 &file_name,
586 data,
587 &pruning_predicate,
588 )
589 .await
590 .unwrap();
591
592 post_pruning_row_groups.assert(&pruned_row_groups);
593 }
594 }
595
596 fn parquet_decimal128_with_bloom_filter(
597 precision: u8,
598 scale: i8,
599 values: Vec<i128>,
600 ) -> bytes::Bytes {
601 let schema = Arc::new(Schema::new(vec![Field::new(
602 "decimal_col",
603 DataType::Decimal128(precision, scale),
604 true,
605 )]));
606 let array = Arc::new(
607 Decimal128Array::from(values)
608 .with_precision_and_scale(precision, scale)
609 .unwrap(),
610 ) as ArrayRef;
611 let batch =
612 arrow::array::RecordBatch::try_new(schema.clone(), vec![array]).unwrap();
613 let props = WriterProperties::builder()
614 .set_max_row_group_row_count(Some(2))
615 .set_bloom_filter_enabled(true)
616 .set_statistics_enabled(EnabledStatistics::None)
617 .build();
618 let mut out = BytesMut::new().writer();
619 {
620 let mut writer = ArrowWriter::try_new(&mut out, schema, Some(props)).unwrap();
621 writer.write(&batch).unwrap();
622 writer.finish().unwrap();
623 }
624 out.into_inner().freeze()
625 }
626
627 async fn test_row_group_bloom_filter_pruning_predicate(
629 file_name: &str,
630 data: bytes::Bytes,
631 pruning_predicate: &PruningPredicate,
632 ) -> Result<RowGroupAccessPlanFilter> {
633 use datafusion_datasource::PartitionedFile;
634 use object_store::ObjectMeta;
635
636 let object_meta = ObjectMeta {
637 location: object_store::path::Path::parse(file_name).expect("creating path"),
638 last_modified: chrono::DateTime::from(std::time::SystemTime::now()),
639 size: data.len() as u64,
640 e_tag: None,
641 version: None,
642 };
643 let in_memory = object_store::memory::InMemory::new();
644 in_memory
645 .put(&object_meta.location, data.into())
646 .await
647 .expect("put parquet file into in memory object store");
648
649 let metrics = ExecutionPlanMetricsSet::new();
650 let file_metrics =
651 ParquetFileMetrics::new(0, object_meta.location.as_ref(), &metrics);
652 let store: Arc<dyn ObjectStore> = Arc::new(in_memory);
653 let partitioned_file = PartitionedFile::new_from_meta(object_meta);
654
655 let reader =
656 ParquetFileReader::new(file_metrics.clone(), store, partitioned_file);
657 let mut builder = ParquetRecordBatchStreamBuilder::new(reader).await.unwrap();
658
659 let access_plan = ParquetAccessPlan::new_all(builder.metadata().num_row_groups());
660 let mut pruned_row_groups = RowGroupAccessPlanFilter::new(access_plan);
661 let literal_columns = pruning_predicate.literal_columns();
662 let parquet_columns: Vec<_> = literal_columns
663 .into_iter()
664 .filter_map(|column_name| {
665 let (column_idx, _) = parquet::arrow::parquet_column(
666 builder.parquet_schema(),
667 pruning_predicate.schema(),
668 &column_name,
669 )?;
670 Some((
671 column_name.to_string(),
672 column_idx,
673 builder.parquet_schema().column(column_idx).physical_type(),
674 builder.parquet_schema().column(column_idx).type_length(),
675 ))
676 })
677 .collect::<Vec<_>>();
678 let mut row_group_bloom_filters =
679 Vec::with_capacity(builder.metadata().num_row_groups());
680 row_group_bloom_filters.resize_with(
681 builder.metadata().num_row_groups(),
682 BloomFilterStatistics::new,
683 );
684 for idx in pruned_row_groups.row_group_indexes() {
685 let mut bloom_filters =
686 BloomFilterStatistics::with_capacity(parquet_columns.len());
687 for (column_name, column_idx, physical_type, type_length) in &parquet_columns
688 {
689 let bf = match builder
690 .get_row_group_column_bloom_filter(idx, *column_idx)
691 .await
692 {
693 Ok(Some(bf)) => bf,
694 Ok(None) => continue,
695 Err(e) => {
696 log::debug!("Ignoring error reading bloom filter: {e}");
697 file_metrics.predicate_evaluation_errors.add(1);
698 continue;
699 }
700 };
701 bloom_filters.insert(
702 column_name.clone(),
703 bf,
704 *physical_type,
705 *type_length,
706 );
707 }
708 row_group_bloom_filters[idx] = bloom_filters;
709 }
710 pruned_row_groups.prune_by_bloom_filters(
711 pruning_predicate,
712 &file_metrics,
713 &row_group_bloom_filters,
714 );
715
716 Ok(pruned_row_groups)
717 }
718}