Skip to main content

mito2/sst/parquet/
index_reader.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
15//! Reader for standalone Parquet index files.
16
17use std::collections::HashSet;
18use std::sync::Arc;
19
20use datafusion_common::pruning::PruningStatistics;
21use datafusion_common::{Column, ScalarValue};
22use datatypes::arrow::array::{ArrayRef, BooleanArray};
23use datatypes::arrow::datatypes::SchemaRef;
24use datatypes::arrow::record_batch::RecordBatch;
25use futures::StreamExt;
26use futures::stream::BoxStream;
27use object_store::ObjectStore;
28use parquet::DecodeResult;
29use parquet::arrow::ProjectionMask;
30use parquet::arrow::arrow_reader::{ArrowReaderMetadata, ArrowReaderOptions};
31use parquet::arrow::push_decoder::ParquetPushDecoderBuilder;
32use parquet::file::metadata::RowGroupMetaData;
33use snafu::{OptionExt, ResultExt};
34use table::predicate::Predicate;
35
36use crate::error::{InvalidRecordBatchSnafu, OpenDalSnafu, ReadParquetSnafu, Result};
37use crate::sst::parquet::format::{column_null_counts, column_values_by_type};
38use crate::sst::parquet::helper::fetch_byte_ranges;
39use crate::sst::parquet::metadata::MetadataLoader;
40use crate::sst::parquet::reader::MetadataCacheMetrics;
41
42/// Reads a standalone index file stored in Parquet format.
43pub(crate) struct ParquetIndexReader {
44    object_store: ObjectStore,
45    path: String,
46    arrow_metadata: ArrowReaderMetadata,
47}
48
49impl ParquetIndexReader {
50    /// Opens `path` and loads its Parquet metadata.
51    pub(crate) async fn open(object_store: ObjectStore, path: &str) -> Result<Self> {
52        let mut metrics = MetadataCacheMetrics::default();
53        let parquet_metadata = MetadataLoader::new(object_store.clone(), path, 0)
54            .load(&mut metrics)
55            .await?;
56        let arrow_metadata =
57            ArrowReaderMetadata::try_new(Arc::new(parquet_metadata), ArrowReaderOptions::new())
58                .with_context(|_| ReadParquetSnafu {
59                    path: path.to_string(),
60                })?;
61
62        Ok(Self {
63            object_store,
64            path: path.to_string(),
65            arrow_metadata,
66        })
67    }
68
69    /// Returns the Arrow schema of the index file.
70    pub(crate) fn schema(&self) -> &SchemaRef {
71        self.arrow_metadata.schema()
72    }
73
74    /// Returns row groups that may match `predicate`.
75    pub(crate) fn row_groups_to_read(&self, predicate: &Predicate) -> Vec<usize> {
76        let stats = IndexRowGroupPruningStats {
77            row_groups: self.arrow_metadata.metadata().row_groups(),
78            schema: self.arrow_metadata.schema(),
79        };
80        predicate
81            .prune_with_stats(&stats, stats.schema)
82            .into_iter()
83            .enumerate()
84            .filter_map(|(row_group, keep)| keep.then_some(row_group))
85            .collect()
86    }
87
88    /// Returns a stream of projected batches from row groups matching `predicate`.
89    pub(crate) fn read(
90        &self,
91        predicate: &Predicate,
92        projection_columns: &[&str],
93    ) -> Result<BoxStream<'static, Result<RecordBatch>>> {
94        let projection = self.projection_mask(projection_columns)?;
95        let row_groups = self.row_groups_to_read(predicate);
96        if row_groups.is_empty() {
97            return Ok(futures::stream::empty().boxed());
98        }
99
100        let mut decoder = ParquetPushDecoderBuilder::new_with_metadata(self.arrow_metadata.clone())
101            .with_row_groups(row_groups)
102            .with_projection(projection)
103            .build()
104            .with_context(|_| ReadParquetSnafu {
105                path: self.path.clone(),
106            })?;
107        let path = self.path.clone();
108        let object_store = self.object_store.clone();
109
110        Ok(async_stream::try_stream! {
111            loop {
112                match decoder
113                    .try_decode()
114                    .with_context(|_| ReadParquetSnafu { path: path.clone() })?
115                {
116                    DecodeResult::NeedsData(ranges) => {
117                        let data = fetch_byte_ranges(&path, object_store.clone(), &ranges)
118                            .await
119                            .context(OpenDalSnafu)?;
120                        decoder
121                            .push_ranges(ranges, data)
122                            .with_context(|_| ReadParquetSnafu { path: path.clone() })?;
123                    }
124                    DecodeResult::Data(batch) => yield batch,
125                    DecodeResult::Finished => break,
126                }
127            }
128        }
129        .boxed())
130    }
131
132    fn projection_mask(&self, projection_columns: &[&str]) -> Result<ProjectionMask> {
133        let mut indices = HashSet::with_capacity(projection_columns.len());
134        for name in projection_columns {
135            let index = self
136                .arrow_metadata
137                .schema()
138                .index_of(name)
139                .ok()
140                .with_context(|| InvalidRecordBatchSnafu {
141                    reason: format!("Parquet index is missing projected column {name}"),
142                })?;
143            indices.insert(index);
144        }
145        Ok(ProjectionMask::roots(
146            self.arrow_metadata.parquet_schema(),
147            indices,
148        ))
149    }
150}
151
152struct IndexRowGroupPruningStats<'a> {
153    row_groups: &'a [RowGroupMetaData],
154    schema: &'a SchemaRef,
155}
156
157impl PruningStatistics for IndexRowGroupPruningStats<'_> {
158    fn min_values(&self, column: &Column) -> Option<ArrayRef> {
159        self.column_values(column, true)
160    }
161
162    fn max_values(&self, column: &Column) -> Option<ArrayRef> {
163        self.column_values(column, false)
164    }
165
166    fn num_containers(&self) -> usize {
167        self.row_groups.len()
168    }
169
170    fn null_counts(&self, column: &Column) -> Option<ArrayRef> {
171        let column_index = self.schema.index_of(&column.name).ok()?;
172        column_null_counts(self.row_groups, column_index)
173    }
174
175    fn row_counts(&self, _column: &Column) -> Option<ArrayRef> {
176        None
177    }
178
179    fn contained(&self, _column: &Column, _values: &HashSet<ScalarValue>) -> Option<BooleanArray> {
180        None
181    }
182}
183
184impl IndexRowGroupPruningStats<'_> {
185    fn column_values(&self, column: &Column, is_min: bool) -> Option<ArrayRef> {
186        let column_index = self.schema.index_of(&column.name).ok()?;
187        let data_type = self.schema.field(column_index).data_type();
188        column_values_by_type(self.row_groups, data_type, column_index, is_min)
189    }
190}