Skip to main content

mito2/sst/index/bloom_filter/
applier.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15mod builder;
16
17use std::collections::BTreeMap;
18use std::ops::Range;
19use std::sync::Arc;
20use std::time::Instant;
21
22use common_base::range_read::RangeReader;
23use common_telemetry::{tracing, warn};
24use datatypes::data_type::ConcreteDataType;
25use index::bloom_filter::applier::{BloomFilterApplier, InListPredicate};
26use index::bloom_filter::reader::{
27    BloomFilterReadMetrics, BloomFilterReader, BloomFilterReaderImpl,
28};
29use index::target::IndexTarget;
30use object_store::ObjectStore;
31use puffin::puffin_manager::cache::PuffinMetadataCacheRef;
32use puffin::puffin_manager::file_key::PuffinFileKey;
33use puffin::puffin_manager::{PuffinManager, PuffinReader};
34use snafu::ResultExt;
35use store_api::metadata::RegionMetadataRef;
36use store_api::region_request::PathType;
37
38use crate::access_layer::{RegionFilePathFactory, WriteCachePathProvider};
39use crate::cache::file_cache::{FileCacheRef, FileType, IndexKey};
40use crate::cache::index::bloom_filter_index::{
41    BloomFilterIndexCacheRef, CachedBloomFilterIndexBlobReader, Tag,
42};
43use crate::error::{
44    ApplyBloomFilterIndexSnafu, Error, MetadataSnafu, PuffinBuildReaderSnafu, PuffinReadBlobSnafu,
45    Result,
46};
47use crate::metrics::INDEX_APPLY_ELAPSED;
48use crate::sst::file::RegionIndexId;
49use crate::sst::index::bloom_filter::INDEX_BLOB_TYPE;
50pub use crate::sst::index::bloom_filter::applier::builder::BloomFilterIndexApplierBuilder;
51use crate::sst::index::column::skipping_target_data_type;
52use crate::sst::index::puffin_manager::{BlobReader, PuffinManagerFactory, SstPuffinManager};
53use crate::sst::index::{TYPE_BLOOM_FILTER_INDEX, trigger_index_background_download};
54
55/// Metrics for tracking bloom filter index apply operations.
56#[derive(Default, Clone)]
57pub struct BloomFilterIndexApplyMetrics {
58    /// Total time spent applying the index.
59    pub apply_elapsed: std::time::Duration,
60    /// Number of blob cache misses.
61    pub blob_cache_miss: usize,
62    /// Total size of blobs read (in bytes).
63    pub blob_read_bytes: u64,
64    /// Metrics for bloom filter read operations.
65    pub read_metrics: BloomFilterReadMetrics,
66}
67
68impl std::fmt::Debug for BloomFilterIndexApplyMetrics {
69    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70        let Self {
71            apply_elapsed,
72            blob_cache_miss,
73            blob_read_bytes,
74            read_metrics,
75        } = self;
76
77        if self.is_empty() {
78            return write!(f, "{{}}");
79        }
80        write!(f, "{{")?;
81
82        write!(f, "\"apply_elapsed\":\"{:?}\"", apply_elapsed)?;
83
84        if *blob_cache_miss > 0 {
85            write!(f, ", \"blob_cache_miss\":{}", blob_cache_miss)?;
86        }
87        if *blob_read_bytes > 0 {
88            write!(f, ", \"blob_read_bytes\":{}", blob_read_bytes)?;
89        }
90        write!(f, ", \"read_metrics\":{:?}", read_metrics)?;
91
92        write!(f, "}}")
93    }
94}
95
96impl BloomFilterIndexApplyMetrics {
97    /// Returns true if the metrics are empty (contain no meaningful data).
98    pub fn is_empty(&self) -> bool {
99        self.apply_elapsed.is_zero()
100    }
101
102    /// Merges another metrics into this one.
103    pub fn merge_from(&mut self, other: &Self) {
104        self.apply_elapsed += other.apply_elapsed;
105        self.blob_cache_miss += other.blob_cache_miss;
106        self.blob_read_bytes += other.blob_read_bytes;
107        self.read_metrics.merge_from(&other.read_metrics);
108    }
109}
110
111pub(crate) type BloomFilterIndexApplierRef = Arc<BloomFilterIndexApplier>;
112
113/// `BloomFilterIndexApplier` applies bloom filter predicates to the SST file.
114pub struct BloomFilterIndexApplier {
115    /// Directory of the table.
116    table_dir: String,
117
118    /// Path type for generating file paths.
119    path_type: PathType,
120
121    /// Object store to read the index file.
122    object_store: ObjectStore,
123
124    /// File cache to read the index file.
125    file_cache: Option<FileCacheRef>,
126
127    /// Factory to create puffin manager.
128    puffin_manager_factory: PuffinManagerFactory,
129
130    /// Managers keep backend identity resolution out of per-blob reads.
131    remote_puffin_manager: SstPuffinManager,
132    cached_puffin_manager: Option<SstPuffinManager>,
133
134    /// Cache for puffin metadata.
135    puffin_metadata_cache: Option<PuffinMetadataCacheRef>,
136
137    /// Cache for bloom filter index.
138    bloom_filter_index_cache: Option<BloomFilterIndexCacheRef>,
139
140    /// Bloom filter predicates.
141    /// For each column, the value will be retained only if it contains __all__ predicates.
142    default_predicates: Arc<BTreeMap<IndexTarget, Vec<InListPredicate>>>,
143
144    /// Expected predicate target types from the latest region metadata.
145    expected_predicate_target_types: BTreeMap<IndexTarget, ConcreteDataType>,
146}
147
148impl BloomFilterIndexApplier {
149    /// Creates a new `BloomFilterIndexApplier`.
150    ///
151    /// For each column, the value will be retained only if it contains __all__ predicates.
152    pub fn new(
153        table_dir: String,
154        path_type: PathType,
155        object_store: ObjectStore,
156        puffin_manager_factory: PuffinManagerFactory,
157        predicates: BTreeMap<IndexTarget, Vec<InListPredicate>>,
158        expected_predicate_target_types: BTreeMap<IndexTarget, ConcreteDataType>,
159    ) -> Self {
160        let remote_puffin_manager = puffin_manager_factory.build(
161            object_store.clone(),
162            RegionFilePathFactory::new(table_dir.clone(), path_type),
163        );
164        let default_predicates = Arc::new(predicates);
165        Self {
166            table_dir,
167            path_type,
168            object_store,
169            file_cache: None,
170            puffin_manager_factory,
171            remote_puffin_manager,
172            cached_puffin_manager: None,
173            puffin_metadata_cache: None,
174            bloom_filter_index_cache: None,
175            default_predicates,
176            expected_predicate_target_types,
177        }
178    }
179
180    pub fn with_file_cache(mut self, file_cache: Option<FileCacheRef>) -> Self {
181        self.cached_puffin_manager = file_cache.as_ref().map(|cache| {
182            self.puffin_manager_factory.build_cached(
183                &self.remote_puffin_manager,
184                cache.local_store(),
185                WriteCachePathProvider::new(cache.clone()),
186            )
187        });
188        self.file_cache = file_cache;
189        self
190    }
191
192    pub fn with_puffin_metadata_cache(
193        mut self,
194        puffin_metadata_cache: Option<PuffinMetadataCacheRef>,
195    ) -> Self {
196        self.puffin_metadata_cache = puffin_metadata_cache;
197        self
198    }
199
200    pub fn with_bloom_filter_cache(
201        mut self,
202        bloom_filter_index_cache: Option<BloomFilterIndexCacheRef>,
203    ) -> Self {
204        self.bloom_filter_index_cache = bloom_filter_index_cache;
205        self
206    }
207
208    /// Applies bloom filter predicates to the provided SST file and returns a
209    /// list of row group ranges that match the predicates.
210    ///
211    /// The `row_groups` iterator provides the row group lengths and whether to search in the row group.
212    ///
213    /// Row group id existing in the returned result means that the row group is searched.
214    /// Empty ranges means that the row group is searched but no rows are found.
215    ///
216    ///
217    /// # Arguments
218    /// * `file_id` - The region file ID to apply predicates to
219    /// * `file_size_hint` - Optional hint for file size to avoid extra metadata reads
220    /// * `row_groups` - Iterator of row group lengths and whether to search in the row group
221    /// * `metrics` - Optional mutable reference to collect metrics on demand
222    #[tracing::instrument(
223        skip_all,
224        fields(file_id = %file_id)
225    )]
226    pub async fn apply(
227        &self,
228        file_id: RegionIndexId,
229        file_size_hint: Option<u64>,
230        predicates: &BTreeMap<IndexTarget, Vec<InListPredicate>>,
231        row_groups: impl Iterator<Item = (usize, bool)>,
232        mut metrics: Option<&mut BloomFilterIndexApplyMetrics>,
233    ) -> Result<Vec<(usize, Vec<Range<usize>>)>> {
234        let apply_start = Instant::now();
235
236        // Calculates row groups' ranges based on start of the file.
237        let mut input = Vec::with_capacity(row_groups.size_hint().0);
238        let mut start = 0;
239        for (i, (len, to_search)) in row_groups.enumerate() {
240            let end = start + len;
241            if to_search {
242                input.push((i, start..end));
243            }
244            start = end;
245        }
246
247        // Initializes output with input ranges, but ranges are based on start of the file not the row group,
248        // so we need to adjust them later.
249        let mut output = input
250            .iter()
251            .map(|(i, range)| (*i, vec![range.clone()]))
252            .collect::<Vec<_>>();
253
254        for (target, predicates) in predicates {
255            let (file_key, blob) = match self
256                .blob_reader(file_id, target, file_size_hint, metrics.as_deref_mut())
257                .await?
258            {
259                Some(blob) => blob,
260                None => continue,
261            };
262
263            // Create appropriate reader based on whether we have caching enabled
264            if let Some(bloom_filter_cache) = &self.bloom_filter_index_cache {
265                let blob_size = blob.metadata().await.context(MetadataSnafu)?.content_length;
266                if let Some(m) = &mut metrics {
267                    m.blob_read_bytes += blob_size;
268                }
269                let reader = CachedBloomFilterIndexBlobReader::new(
270                    file_key,
271                    target.clone(),
272                    Tag::Skipping,
273                    blob_size,
274                    BloomFilterReaderImpl::new(blob),
275                    bloom_filter_cache.clone(),
276                );
277                self.apply_predicates(reader, predicates, &mut output, metrics.as_deref_mut())
278                    .await
279                    .context(ApplyBloomFilterIndexSnafu)?;
280            } else {
281                let reader = BloomFilterReaderImpl::new(blob);
282                self.apply_predicates(reader, predicates, &mut output, metrics.as_deref_mut())
283                    .await
284                    .context(ApplyBloomFilterIndexSnafu)?;
285            }
286        }
287
288        // adjust ranges to be based on row group
289        for ((_, output), (_, input)) in output.iter_mut().zip(input) {
290            let start = input.start;
291            for range in output.iter_mut() {
292                range.start -= start;
293                range.end -= start;
294            }
295        }
296
297        // Record elapsed time to histogram and collect metrics if requested
298        let elapsed = apply_start.elapsed();
299        INDEX_APPLY_ELAPSED
300            .with_label_values(&[TYPE_BLOOM_FILTER_INDEX])
301            .observe(elapsed.as_secs_f64());
302
303        if let Some(m) = metrics {
304            m.apply_elapsed += elapsed;
305        }
306
307        Ok(output)
308    }
309
310    /// Creates a blob reader from the cached or remote index file.
311    ///
312    /// Returns `None` if the target does not have an index.
313    async fn blob_reader(
314        &self,
315        file_id: RegionIndexId,
316        target: &IndexTarget,
317        file_size_hint: Option<u64>,
318        metrics: Option<&mut BloomFilterIndexApplyMetrics>,
319    ) -> Result<Option<(PuffinFileKey<RegionIndexId>, BlobReader)>> {
320        let reader = match self
321            .cached_blob_reader(file_id, target, file_size_hint)
322            .await
323        {
324            Ok(Some(puffin_reader)) => puffin_reader,
325            other => {
326                if let Some(m) = metrics {
327                    m.blob_cache_miss += 1;
328                }
329                if let Err(err) = other {
330                    // Blob not found means no index for this target.
331                    if is_blob_not_found(&err) {
332                        return Ok(None);
333                    }
334                    warn!(err; "An unexpected error occurred while reading the cached index file. Fallback to remote index file.")
335                }
336                let res = self
337                    .remote_blob_reader(file_id, target, file_size_hint)
338                    .await;
339                if let Err(err) = res {
340                    // Blob not found means no index for this target.
341                    if is_blob_not_found(&err) {
342                        return Ok(None);
343                    }
344                    return Err(err);
345                }
346
347                res?
348            }
349        };
350
351        Ok(Some(reader))
352    }
353
354    /// Creates a blob reader from the cached index file
355    async fn cached_blob_reader(
356        &self,
357        file_id: RegionIndexId,
358        target: &IndexTarget,
359        file_size_hint: Option<u64>,
360    ) -> Result<Option<(PuffinFileKey<RegionIndexId>, BlobReader)>> {
361        let (Some(file_cache), Some(puffin_manager)) =
362            (&self.file_cache, &self.cached_puffin_manager)
363        else {
364            return Ok(None);
365        };
366
367        let index_key = IndexKey::new(
368            file_id.region_id(),
369            file_id.file_id(),
370            FileType::Puffin(file_id.version),
371        );
372        if file_cache.get(index_key).await.is_none() {
373            return Ok(None);
374        };
375
376        let blob_name = Self::target_blob_name(target);
377
378        let reader = puffin_manager
379            .reader(&file_id)
380            .await
381            .context(PuffinBuildReaderSnafu)?
382            .with_file_size_hint(file_size_hint)
383            .blob(&blob_name)
384            .await
385            .context(PuffinReadBlobSnafu)?
386            .reader()
387            .await
388            .context(PuffinBuildReaderSnafu)?;
389        Ok(Some((puffin_manager.file_key(&file_id), reader)))
390    }
391
392    // TODO(ruihang): use the same util with the code in creator
393    fn target_blob_name(target: &IndexTarget) -> String {
394        let target_key = target.encode();
395        format!("{INDEX_BLOB_TYPE}-{target_key}")
396    }
397
398    /// Creates a blob reader from the remote index file
399    async fn remote_blob_reader(
400        &self,
401        file_id: RegionIndexId,
402        target: &IndexTarget,
403        file_size_hint: Option<u64>,
404    ) -> Result<(PuffinFileKey<RegionIndexId>, BlobReader)> {
405        let path_factory = RegionFilePathFactory::new(self.table_dir.clone(), self.path_type);
406
407        // Trigger background download if file cache and file size are available
408        trigger_index_background_download(
409            self.file_cache.as_ref(),
410            &file_id,
411            file_size_hint,
412            &path_factory,
413            &self.object_store,
414        );
415
416        let puffin_manager = self
417            .remote_puffin_manager
418            .clone()
419            .with_puffin_metadata_cache(self.puffin_metadata_cache.clone());
420
421        let blob_name = Self::target_blob_name(target);
422
423        let reader = puffin_manager
424            .reader(&file_id)
425            .await
426            .context(PuffinBuildReaderSnafu)?
427            .with_file_size_hint(file_size_hint)
428            .blob(&blob_name)
429            .await
430            .context(PuffinReadBlobSnafu)?
431            .reader()
432            .await
433            .context(PuffinBuildReaderSnafu)?;
434        Ok((puffin_manager.file_key(&file_id), reader))
435    }
436
437    async fn apply_predicates<R: BloomFilterReader + Send + 'static>(
438        &self,
439        reader: R,
440        predicates: &[InListPredicate],
441        output: &mut [(usize, Vec<Range<usize>>)],
442        metrics: Option<&mut BloomFilterIndexApplyMetrics>,
443    ) -> std::result::Result<(), index::bloom_filter::error::Error> {
444        let mut applier = BloomFilterApplier::new(Box::new(reader)).await?;
445        let mut row_groups = output.iter_mut().map(|(_, r)| r).collect::<Vec<_>>();
446        let read_metrics = metrics.map(|m| &mut m.read_metrics);
447        applier
448            .search_groups(predicates, &mut row_groups, read_metrics)
449            .await
450    }
451
452    /// Returns compatible bloom filter predicates with the given SST metadata.
453    ///
454    /// Returns `None` when no compatible predicate remains for this SST.
455    pub fn compatible_predicate_for_sst(
456        &self,
457        sst_metadata: &RegionMetadataRef,
458    ) -> Option<Arc<BTreeMap<IndexTarget, Vec<InListPredicate>>>> {
459        let mut has_type_mismatch = false;
460        let mut compatible_targets = Vec::new();
461
462        for (target, expected) in &self.expected_predicate_target_types {
463            if skipping_target_data_type(sst_metadata, target).as_ref() != Some(expected) {
464                has_type_mismatch = true;
465                continue;
466            }
467            compatible_targets.push(target.clone());
468        }
469
470        if compatible_targets.is_empty() {
471            return None;
472        }
473
474        if !has_type_mismatch {
475            return Some(self.default_predicates.clone());
476        }
477
478        let mut compatible_predicates = BTreeMap::new();
479        for target in compatible_targets {
480            if let Some(predicates) = self.default_predicates.get(&target) {
481                compatible_predicates.insert(target, predicates.clone());
482            }
483        }
484
485        Some(Arc::new(compatible_predicates))
486    }
487}
488
489fn is_blob_not_found(err: &Error) -> bool {
490    matches!(
491        err,
492        Error::PuffinReadBlob {
493            source: puffin::error::Error::BlobNotFound { .. },
494            ..
495        }
496    )
497}
498
499#[cfg(test)]
500mod tests {
501    use std::collections::BTreeSet;
502
503    use datafusion_expr::{Expr, col, lit};
504    use futures::future::BoxFuture;
505    use index::Bytes;
506    use object_store::services::Memory;
507    use puffin::puffin_manager::PuffinWriter;
508    use store_api::metadata::RegionMetadata;
509    use store_api::storage::FileId;
510
511    use super::*;
512    use crate::sst::file::RegionFileId;
513    use crate::sst::index::IndexBuildType;
514    use crate::sst::index::bloom_filter::creator::BloomFilterIndexer;
515    use crate::sst::index::bloom_filter::creator::tests::{
516        mock_object_store, mock_region_metadata, new_batch, new_intm_mgr,
517    };
518
519    #[tokio::test]
520    async fn test_json_skipping_build_and_prune_both_formats() {
521        use crate::read::{Batch, BatchColumn};
522        use common_function::scalars::json::json_get::JsonGetWithType;
523        use common_function::scalars::udf::create_udf;
524        use datafusion_common::ScalarValue;
525        use datafusion_expr::expr::ScalarFunction;
526        use datatypes::arrow::array::{ArrayRef, Int64Array, StructArray};
527        use datatypes::arrow::buffer::NullBuffer;
528        use datatypes::arrow::datatypes::{Field, Schema};
529        use datatypes::arrow::record_batch::RecordBatch;
530        use datatypes::schema::SkippingIndexOptions;
531        use datatypes::vectors::{Helper, UInt8Vector, UInt64Vector};
532
533        for flat in [false, true] {
534            let (dir, factory) =
535                PuffinManagerFactory::new_for_test_async("json_skipping_index").await;
536            let mut metadata = (*mock_region_metadata()).clone();
537            let column = &mut metadata.column_metadatas[2].column_schema;
538            column.data_type =
539                ConcreteDataType::json2(datatypes::types::json_type::JsonNativeType::Object(
540                    datatypes::types::json_type::JsonObjectType::new(),
541                ));
542            column.with_extension_type(&datatypes::extension::json::Json2ExtensionType::new(
543                Arc::new(datatypes::extension::json::JsonMetadata::new(
544                    datatypes::json::JsonSettings::try_new(
545                        vec![datatypes::json::JsonTypeHint {
546                            path: vec!["value".into()],
547                            data_type: ConcreteDataType::int64_datatype(),
548                            inverted_index: false,
549                            fulltext_index: None,
550                            skipping_index: Some(
551                                SkippingIndexOptions::new(2, 0.01, Default::default()).unwrap(),
552                            ),
553                        }],
554                        Some(0),
555                    )
556                    .unwrap(),
557                )),
558            ));
559            // Other ordinary indexed columns are absent in this synthetic batch.
560            for col in &mut metadata.column_metadatas {
561                col.column_schema.unset_skipping_options().unwrap();
562            }
563            let metadata = Arc::new(metadata);
564            let file_id = FileId::random();
565            let target = IndexTarget::new_json_path(3, vec!["value".into()]).unwrap();
566            let mut creator = BloomFilterIndexer::new(
567                file_id,
568                &metadata,
569                new_intm_mgr(dir.path().to_string_lossy()).await,
570                None,
571                IndexBuildType::Flush,
572            )
573            .unwrap()
574            .unwrap();
575            let values: ArrayRef = Arc::new(Int64Array::from(vec![Some(1), None, Some(1), None]));
576            let root: ArrayRef = Arc::new(StructArray::new(
577                vec![Field::new("value", values.data_type().clone(), true)].into(),
578                vec![values],
579                Some(NullBuffer::from(vec![true, false, true, true])),
580            ));
581            for start in [0, 2] {
582                let array = root.slice(start, 2);
583                if flat {
584                    let batch = RecordBatch::try_new(
585                        Arc::new(Schema::new(vec![Field::new(
586                            "field_u64",
587                            array.data_type().clone(),
588                            true,
589                        )])),
590                        vec![array],
591                    )
592                    .unwrap();
593                    creator.update_flat(&batch).await.unwrap();
594                } else {
595                    let mut batch = Batch::new(
596                        vec![],
597                        Arc::new(UInt64Vector::from_vec(vec![0, 1])),
598                        Arc::new(UInt64Vector::from_vec(vec![0, 0])),
599                        Arc::new(UInt8Vector::from_vec(vec![1, 1])),
600                        vec![BatchColumn {
601                            column_id: 3,
602                            data: Helper::try_into_vector(array).unwrap(),
603                        }],
604                    )
605                    .unwrap();
606                    creator.update(&mut batch).await.unwrap();
607                }
608            }
609
610            assert_eq!(creator.indexed_targets().collect::<Vec<_>>(), vec![&target]);
611            let object_store = mock_object_store();
612            let manager = factory.build(
613                object_store.clone(),
614                RegionFilePathFactory::new("json_test".into(), PathType::Bare),
615            );
616            let index_id = RegionIndexId::new(RegionFileId::new(metadata.region_id, file_id), 0);
617            let mut writer = manager.writer(&index_id).await.unwrap();
618            let (rows, bytes) = creator.finish(&mut writer).await.unwrap();
619            assert_eq!(rows, 4);
620            assert!(bytes > 0);
621            writer.finish().await.unwrap();
622            let json_expr = |path: &str, ty| {
623                Expr::ScalarFunction(ScalarFunction::new_udf(
624                    Arc::new(create_udf(Arc::new(JsonGetWithType::default()))),
625                    vec![col("field_u64"), lit(path), Expr::Literal(ty, None)],
626                ))
627            };
628            let cache = Arc::new(
629                crate::cache::index::bloom_filter_index::BloomFilterIndexCache::new(
630                    1024 * 1024,
631                    1024 * 1024,
632                    1024,
633                ),
634            );
635            for expr in [
636                json_expr("$.value", ScalarValue::Int64(None)).eq(lit(9_i64)),
637                json_expr("$.value", ScalarValue::Int64(None))
638                    .in_list(vec![lit(8_i64), lit(9_i64)], false),
639            ] {
640                let applier = BloomFilterIndexApplierBuilder::new(
641                    "json_test".into(),
642                    PathType::Bare,
643                    object_store.clone(),
644                    &metadata,
645                    factory.clone(),
646                )
647                .with_bloom_filter_index_cache(Some(cache.clone()))
648                .build(&[expr])
649                .unwrap()
650                .unwrap();
651                let predicates = applier.compatible_predicate_for_sst(&metadata).unwrap();
652                assert!(predicates.contains_key(&target));
653                // Run twice to cover cold reads and cached index reads.
654                for _ in 0..2 {
655                    let result = applier
656                        .apply(index_id, None, &predicates, [(4, true)].into_iter(), None)
657                        .await
658                        .unwrap();
659                    assert!(result[0].1.is_empty());
660                }
661                assert!(
662                    applier
663                        .compatible_predicate_for_sst(&mock_region_metadata())
664                        .is_none()
665                );
666            }
667            let applier = BloomFilterIndexApplierBuilder::new(
668                "json_test".into(),
669                PathType::Bare,
670                object_store.clone(),
671                &metadata,
672                factory.clone(),
673            )
674            .build(&[json_expr("$.value", ScalarValue::Int64(None)).eq(lit(1_i64))])
675            .unwrap()
676            .unwrap();
677            let predicates = applier.compatible_predicate_for_sst(&metadata).unwrap();
678            let result = applier
679                .apply(index_id, None, &predicates, [(4, true)].into_iter(), None)
680                .await
681                .unwrap();
682            assert_eq!(result[0].1.len(), 1);
683            assert_eq!(result[0].1[0], 0..4);
684            for expr in [
685                json_expr("$.value", ScalarValue::Float64(None)),
686                json_expr("$.missing", ScalarValue::Int64(None)),
687            ] {
688                assert!(
689                    BloomFilterIndexApplierBuilder::new(
690                        "json_test".into(),
691                        PathType::Bare,
692                        object_store.clone(),
693                        &metadata,
694                        factory.clone()
695                    )
696                    .build(&[expr.eq(lit(1_i64))])
697                    .unwrap()
698                    .is_none()
699                );
700            }
701        }
702    }
703
704    #[tokio::test]
705    async fn test_compatible_predicate_for_sst() {
706        let (_d, puffin_manager_factory) =
707            PuffinManagerFactory::new_for_test_async("test_plan_for_sst_basic_").await;
708        let object_store = ObjectStore::new(Memory::default()).unwrap();
709        let table_dir = "table_dir".to_string();
710
711        let predicates = BTreeMap::from_iter([(
712            IndexTarget::ColumnId(1),
713            vec![InListPredicate {
714                list: BTreeSet::from_iter([Bytes::from("foo")]),
715            }],
716        )]);
717        let expected_predicate_target_types = BTreeMap::from_iter([(
718            IndexTarget::ColumnId(1),
719            ConcreteDataType::string_datatype(),
720        )]);
721
722        let applier = BloomFilterIndexApplier::new(
723            table_dir,
724            PathType::Bare,
725            object_store,
726            puffin_manager_factory,
727            predicates,
728            expected_predicate_target_types,
729        );
730        let predicates = applier.compatible_predicate_for_sst(&mock_region_metadata());
731        assert!(predicates.is_some());
732    }
733
734    #[tokio::test]
735    async fn test_compatible_predicate_for_sst_type_mismatch() {
736        let (_d, puffin_manager_factory) =
737            PuffinManagerFactory::new_for_test_async("test_plan_for_sst_type_mismatch_").await;
738        let object_store = ObjectStore::new(Memory::default()).unwrap();
739        let table_dir = "table_dir".to_string();
740
741        let predicates = BTreeMap::from_iter([(
742            IndexTarget::ColumnId(1),
743            vec![InListPredicate {
744                list: BTreeSet::from_iter([Bytes::from("foo")]),
745            }],
746        )]);
747        let expected_predicate_target_types =
748            BTreeMap::from_iter([(IndexTarget::ColumnId(1), ConcreteDataType::int64_datatype())]);
749
750        let applier = BloomFilterIndexApplier::new(
751            table_dir,
752            PathType::Bare,
753            object_store,
754            puffin_manager_factory,
755            predicates,
756            expected_predicate_target_types,
757        );
758        let predicates = applier.compatible_predicate_for_sst(&mock_region_metadata());
759        assert!(predicates.is_none());
760    }
761
762    #[tokio::test]
763    async fn test_compatible_predicate_for_sst_partial_type_mismatch() {
764        let (_d, puffin_manager_factory) =
765            PuffinManagerFactory::new_for_test_async("test_plan_for_sst_partial_mismatch_").await;
766        let object_store = ObjectStore::new(Memory::default()).unwrap();
767        let table_dir = "table_dir".to_string();
768
769        // Column 1 (tag_str): expected string — matches SST (compatible).
770        // Column 3 (field_u64): expected int64 — SST has uint64 (mismatched).
771        let predicates = BTreeMap::from_iter([
772            (
773                IndexTarget::ColumnId(1),
774                vec![InListPredicate {
775                    list: BTreeSet::from_iter([Bytes::from("foo")]),
776                }],
777            ),
778            (
779                IndexTarget::ColumnId(3),
780                vec![InListPredicate {
781                    list: BTreeSet::from_iter([Bytes::from("bar")]),
782                }],
783            ),
784        ]);
785        let expected_predicate_target_types = BTreeMap::from_iter([
786            (
787                IndexTarget::ColumnId(1),
788                ConcreteDataType::string_datatype(),
789            ),
790            (IndexTarget::ColumnId(3), ConcreteDataType::int64_datatype()), // intentional mismatch
791        ]);
792
793        let applier = BloomFilterIndexApplier::new(
794            table_dir,
795            PathType::Bare,
796            object_store,
797            puffin_manager_factory,
798            predicates,
799            expected_predicate_target_types,
800        );
801        let result = applier.compatible_predicate_for_sst(&mock_region_metadata());
802
803        // The subset containing only the compatible column must be returned.
804        let result = result.expect("expected Some with compatible subset");
805        assert!(
806            result.contains_key(&IndexTarget::ColumnId(1)),
807            "compatible column 1 must be present"
808        );
809        assert!(
810            !result.contains_key(&IndexTarget::ColumnId(3)),
811            "mismatched column 3 must be absent"
812        );
813        assert_eq!(result.len(), 1, "only the compatible predicate must remain");
814    }
815
816    #[allow(clippy::type_complexity)]
817    fn tester(
818        table_dir: String,
819        object_store: ObjectStore,
820        metadata: &RegionMetadata,
821        puffin_manager_factory: PuffinManagerFactory,
822        file_id: RegionIndexId,
823    ) -> impl Fn(&[Expr], Vec<(usize, bool)>) -> BoxFuture<'static, Vec<(usize, Vec<Range<usize>>)>>
824    + use<'_> {
825        move |exprs, row_groups| {
826            let table_dir = table_dir.clone();
827            let object_store: ObjectStore = object_store.clone();
828            let metadata = metadata.clone();
829            let puffin_manager_factory = puffin_manager_factory.clone();
830            let exprs = exprs.to_vec();
831
832            Box::pin(async move {
833                let builder = BloomFilterIndexApplierBuilder::new(
834                    table_dir,
835                    PathType::Bare,
836                    object_store,
837                    &metadata,
838                    puffin_manager_factory,
839                );
840
841                let applier = builder.build(&exprs).unwrap().unwrap();
842                let predicates = applier
843                    .compatible_predicate_for_sst(&Arc::new(metadata.clone()))
844                    .unwrap();
845                applier
846                    .apply(file_id, None, &predicates, row_groups.into_iter(), None)
847                    .await
848                    .unwrap()
849                    .into_iter()
850                    .filter(|(_, ranges)| !ranges.is_empty())
851                    .collect()
852            })
853        }
854    }
855
856    #[tokio::test]
857    #[allow(clippy::single_range_in_vec_init)]
858    async fn test_bloom_filter_applier() {
859        // tag_str:
860        //   - type: string
861        //   - index: bloom filter
862        //   - granularity: 2
863        //   - column_id: 1
864        //
865        // ts:
866        //   - type: timestamp
867        //   - index: time index
868        //   - column_id: 2
869        //
870        // field_u64:
871        //   - type: uint64
872        //   - index: bloom filter
873        //   - granularity: 4
874        //   - column_id: 3
875        let region_metadata = mock_region_metadata();
876        let prefix = "test_bloom_filter_applier_";
877        let (d, factory) = PuffinManagerFactory::new_for_test_async(prefix).await;
878        let object_store = mock_object_store();
879        let intm_mgr = new_intm_mgr(d.path().to_string_lossy()).await;
880        let memory_usage_threshold = Some(1024);
881        let file_id = RegionFileId::new(region_metadata.region_id, FileId::random());
882        let file_id = RegionIndexId::new(file_id, 0);
883        let table_dir = "table_dir".to_string();
884
885        let mut indexer = BloomFilterIndexer::new(
886            file_id.file_id(),
887            &region_metadata,
888            intm_mgr,
889            memory_usage_threshold,
890            IndexBuildType::Flush,
891        )
892        .unwrap()
893        .unwrap();
894
895        // push 20 rows
896        let mut batch = new_batch("tag1", 0..10);
897        indexer.update(&mut batch).await.unwrap();
898        let mut batch = new_batch("tag2", 10..20);
899        indexer.update(&mut batch).await.unwrap();
900
901        let puffin_manager = factory.build(
902            object_store.clone(),
903            RegionFilePathFactory::new(table_dir.clone(), PathType::Bare),
904        );
905
906        let mut puffin_writer = puffin_manager.writer(&file_id).await.unwrap();
907        indexer.finish(&mut puffin_writer).await.unwrap();
908        puffin_writer.finish().await.unwrap();
909
910        let tester = tester(
911            table_dir.clone(),
912            object_store.clone(),
913            &region_metadata,
914            factory.clone(),
915            file_id,
916        );
917
918        // rows        0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19
919        // row group: | o  row group |  o row group | o  row group     |  o row group     |
920        // tag_str:   |      o pred                 |   x pred                            |
921        let res = tester(
922            &[col("tag_str").eq(lit("tag1"))],
923            vec![(5, true), (5, true), (5, true), (5, true)],
924        )
925        .await;
926        assert_eq!(res, vec![(0, vec![0..5]), (1, vec![0..5])]);
927
928        // rows        0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19
929        // row group: | o  row group |  x row group | o  row group     |  o row group     |
930        // tag_str:   |      o pred                 |   x pred                            |
931        let res = tester(
932            &[col("tag_str").eq(lit("tag1"))],
933            vec![(5, true), (5, false), (5, true), (5, true)],
934        )
935        .await;
936        assert_eq!(res, vec![(0, vec![0..5])]);
937
938        // rows        0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19
939        // row group: | o  row group |  o row group | o  row group     |  o row group     |
940        // tag_str:   |      o pred                 |   x pred                            |
941        // field_u64: | o pred   | x pred    |  x pred     |  x pred       | x pred       |
942        let res = tester(
943            &[
944                col("tag_str").eq(lit("tag1")),
945                col("field_u64").eq(lit(1u64)),
946            ],
947            vec![(5, true), (5, true), (5, true), (5, true)],
948        )
949        .await;
950        assert_eq!(res, vec![(0, vec![0..4])]);
951
952        // rows        0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19
953        // row group: | o  row group |  o row group | x  row group     |  o row group     |
954        // field_u64: | o pred   | x pred    |  o pred     |  x pred       | x pred       |
955        let res = tester(
956            &[col("field_u64").in_list(vec![lit(1u64), lit(11u64)], false)],
957            vec![(5, true), (5, true), (5, false), (5, true)],
958        )
959        .await;
960        assert_eq!(res, vec![(0, vec![0..4]), (1, vec![3..5])]);
961    }
962}