Skip to main content

datafusion_datasource_parquet/
bloom_filter.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18//! Loaded Parquet Split Block Bloom Filter (SBBF) data, with a
19//! [`PruningStatistics`] adapter so the predicate-pruning machinery in
20//! [`datafusion_pruning`] can consume it.
21
22use 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/// In memory Parquet Split Block Bloom Filters (SBBF).
32///
33/// This structure implements [`PruningStatistics`] and is used to prune
34/// Parquet row groups and data pages based on the query predicate.
35#[derive(Debug, Clone, Default)]
36pub struct BloomFilterStatistics {
37    /// Per-column Bloom filters keyed by predicate column name.
38    column_sbbf: HashMap<String, ColumnBloomFilter>,
39}
40
41#[derive(Debug, Clone)]
42struct ColumnBloomFilter {
43    /// [`Sbbf`] (Bloom filter).
44    sbbf: Sbbf,
45    /// Parquet physical [`Type`] needed to evaluate literals against the filter.
46    physical_type: Type,
47    /// Type length from the Parquet column descriptor.
48    type_length: i32,
49}
50
51impl BloomFilterStatistics {
52    /// Create an empty [`BloomFilterStatistics`]
53    pub fn new() -> Self {
54        Default::default()
55    }
56
57    /// Create an empty [`BloomFilterStatistics`] with the specified capacity
58    pub fn with_capacity(capacity: usize) -> Self {
59        Self {
60            column_sbbf: HashMap::with_capacity(capacity),
61        }
62    }
63
64    /// Add a Bloom filter for the specified column, along with the column's
65    /// Parquet physical [`Type`] and type length from the column descriptor.
66    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    /// Helper function for checking if [`Sbbf`] filter contains [`ScalarValue`].
84    ///
85    /// In case the type of scalar is not supported, returns `true`, assuming that the
86    /// value may be present.
87    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                    //https://github.com/apache/parquet-format/blob/eb4b31c1d64a01088d02a2f9aefc6c17c54cc6fc/Encodings.md?plain=1#L35-L42
111                    // All physical type  are little-endian
112                    if *p > 9 {
113                        //DECIMAL can be used to annotate the following types:
114                        //
115                        // int32: for 1 <= precision <= 9
116                        // int64: for 1 <= precision <= 18
117                        return true;
118                    }
119                    let b = (*v as i32).to_le_bytes();
120                    // Use Decimal constructor after https://github.com/apache/arrow-rs/issues/5325
121                    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                    // Use Decimal constructor after https://github.com/apache/arrow-rs/issues/5325
150                    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    /// Use bloom filters to determine if we are sure this column can not
192    /// possibly contain `values`
193    ///
194    /// The `contained` API returns false if the bloom filters knows that *ALL*
195    /// of the values in a column are not present.
196    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        // Bloom filters are probabilistic data structures that can return false
204        // positives (i.e. it might return true even if the value is not
205        // present) however, the bloom filter will return `false` if the value is
206        // definitely not present.
207
208        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            // The row group doesn't contain any of the values if
219            // all the checks are false
220            .all(|v| !v);
221
222        let contains = if known_not_present {
223            Some(false)
224        } else {
225            // Given the bloom filter is probabilistic, we can't be sure that
226            // the row group actually contains the values. Return `None` to
227            // indicate this uncertainty
228            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            // generate pruning predicate `(String = "Hello_Not_exists")`
273            .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            // generate pruning predicate `(String = "Hello_Not_exists" OR String = "Hello_Not_exists2")`
282            .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            // generate pruning predicate `(String = "Hello_Not_exists" OR String = "Hello_Not_exists2")`
297            .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        // load parquet file
318        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        // generate pruning predicate
324        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            // generate pruning predicate `(String = "Hello")`
355            .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            // generate pruning predicate `(String = "Hello") OR (String = "the quick")`
364            .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            // generate pruning predicate `(String = "Hello") OR (String = "the quick") OR (String = "are you")`
377            .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            // generate pruning predicate `(String = "Hello") OR (String = "the quick") OR (String = "are you")`
391            .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            // generate pruning predicate `(String = "foo") OR (String != "bar")`
414            .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        // generate pruning predicate on a column without a bloom filter
425        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        // which row groups are expected to be left after pruning
509        post_pruning_row_groups: ExpectedPruning,
510    }
511
512    impl BloomFilterTest {
513        /// Return a test for data_index_bloom_encoding_stats.parquet
514        /// Note the values in the `String` column are:
515        /// ```sql
516        /// > select * from './parquet-testing/data/data_index_bloom_encoding_stats.parquet';
517        /// +-----------+
518        /// | String    |
519        /// +-----------+
520        /// | Hello     |
521        /// | This is   |
522        /// | a         |
523        /// | test      |
524        /// | How       |
525        /// | are you   |
526        /// | doing     |
527        /// | today     |
528        /// | the quick |
529        /// | brown fox |
530        /// | jumps     |
531        /// | over      |
532        /// | the lazy  |
533        /// | dog       |
534        /// +-----------+
535        /// ```
536        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        // Return a test for alltypes_plain.parquet
545        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        /// Expect all row groups to be pruned
558        pub fn with_expect_all_pruned(mut self) -> Self {
559            self.post_pruning_row_groups = ExpectedPruning::All;
560            self
561        }
562
563        /// Expect all row groups not to be pruned
564        pub fn with_expect_none_pruned(mut self) -> Self {
565            self.post_pruning_row_groups = ExpectedPruning::None;
566            self
567        }
568
569        /// Prune this file using the specified expression and check that the expected row groups are left
570        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    /// Evaluates the pruning predicate on the specified row groups and returns the row groups that are left
628    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}