alamb commented on code in PR #12090:
URL: https://github.com/apache/arrow-rs/pull/12090#discussion_r4517932469
##########
parquet/src/file/metadata/reader.rs:
##########
@@ -801,6 +805,77 @@ impl ParquetMetaDataReader {
self.load_page_index_with_remainder_impl(fetch, None).await
}
+ /// Reads the Bloom filter for one physical leaf column in a row group.
+ ///
+ /// The metadata and reader must refer to the same file. `column_idx` is a
+ /// physical Parquet leaf index, not a top-level Arrow field index.
+ ///
+ /// Returns `Ok(None)` without I/O when the column chunk has no Bloom
filter.
+ /// Invalid coordinates, invalid Bloom metadata, malformed filters, and
read
+ /// failures return an error. The filter is returned separately; the
supplied
+ /// metadata is neither modified nor reloaded.
+ ///
+ /// Prefer reading filters after other pruning has narrowed the row groups
and
Review Comment:
I recommend we also note that reading BloomFilters typically requires more
I/O as they are not stored inline in the footer
##########
parquet/src/file/metadata/reader.rs:
##########
@@ -801,6 +805,77 @@ impl ParquetMetaDataReader {
self.load_page_index_with_remainder_impl(fetch, None).await
}
+ /// Reads the Bloom filter for one physical leaf column in a row group.
+ ///
+ /// The metadata and reader must refer to the same file. `column_idx` is a
+ /// physical Parquet leaf index, not a top-level Arrow field index.
+ ///
+ /// Returns `Ok(None)` without I/O when the column chunk has no Bloom
filter.
+ /// Invalid coordinates, invalid Bloom metadata, malformed filters, and
read
Review Comment:
I am not sure we need to spell out all the possible errors -- like "if there
is an error, it is returned" is already kind of redundant
##########
parquet/src/file/metadata/reader.rs:
##########
@@ -801,6 +805,77 @@ impl ParquetMetaDataReader {
self.load_page_index_with_remainder_impl(fetch, None).await
}
+ /// Reads the Bloom filter for one physical leaf column in a row group.
+ ///
+ /// The metadata and reader must refer to the same file. `column_idx` is a
+ /// physical Parquet leaf index, not a top-level Arrow field index.
+ ///
+ /// Returns `Ok(None)` without I/O when the column chunk has no Bloom
filter.
+ /// Invalid coordinates, invalid Bloom metadata, malformed filters, and
read
+ /// failures return an error. The filter is returned separately; the
supplied
+ /// metadata is neither modified nor reloaded.
+ ///
+ /// Prefer reading filters after other pruning has narrowed the row groups
and
+ /// columns of interest. Footer and page index reads never load Bloom
filters.
+ ///
+ /// # Example
+ ///
+ /// Read footer metadata first, then request a filter only when needed:
+ ///
+ /// ```rust
+ /// use bytes::Bytes;
+ /// use parquet::errors::Result;
+ /// use parquet::file::metadata::ParquetMetaDataReader;
+ /// # use std::sync::Arc;
+ /// # use parquet::data_type::Int32Type;
+ /// # use parquet::file::properties::WriterProperties;
+ /// # use parquet::file::writer::SerializedFileWriter;
+ /// # use parquet::schema::parser::parse_message_type;
+ /// # fn main() -> Result<()> {
+ /// # let schema = Arc::new(parse_message_type("message schema { REQUIRED
INT32 value; }")?);
+ /// # let properties =
Arc::new(WriterProperties::builder().set_bloom_filter_enabled(true).build());
+ /// # let mut writer = SerializedFileWriter::new(Vec::new(), schema,
properties)?;
+ /// # let mut row_group = writer.next_row_group()?;
+ /// # let mut column = row_group.next_column()?.unwrap();
+ /// # column.typed::<Int32Type>().write_batch(&[42], None, None)?;
+ /// # column.close()?;
+ /// # row_group.close()?;
+ /// # let data = Bytes::from(writer.into_inner()?);
+ /// let metadata = ParquetMetaDataReader::new().parse_and_finish(&data)?;
+ /// let filter = ParquetMetaDataReader::read_column_bloom_filter(&data,
&metadata, 0, 0)?;
+ /// assert!(filter.unwrap().check(&42_i32));
+ /// # Ok(())
+ /// # }
+ /// ```
+ pub fn read_column_bloom_filter<R: ChunkReader>(
+ reader: &R,
+ metadata: &ParquetMetaData,
+ row_group_idx: usize,
+ column_idx: usize,
+ ) -> Result<Option<Sbbf>> {
+ let column = bloom_filter_column(metadata, row_group_idx, column_idx)?;
+ Sbbf::read_from_column_chunk(column, reader)
+ }
+
+ /// Asynchronous version of [`Self::read_column_bloom_filter`].
+ ///
+ /// Accepts a [`MetadataFetch`] byte source without requiring an Arrow
reader
+ /// builder or an [`crate::arrow::async_reader::AsyncFileReader`]
implementation.
+ /// Existing async file readers can be passed by mutable reference and
reused
+ /// for subsequent reads. For a boxed trait-object reader, pass `&mut
reader`
Review Comment:
> For a boxed trait-object reader, pass `&mut reader`
Is that specific advice for this function? It seems like general advice for
any boxed trait (and thus we don't need to document anything special for
Parquet rust)
##########
parquet/src/file/metadata/reader.rs:
##########
@@ -801,6 +805,77 @@ impl ParquetMetaDataReader {
self.load_page_index_with_remainder_impl(fetch, None).await
}
+ /// Reads the Bloom filter for one physical leaf column in a row group.
+ ///
+ /// The metadata and reader must refer to the same file. `column_idx` is a
+ /// physical Parquet leaf index, not a top-level Arrow field index.
+ ///
+ /// Returns `Ok(None)` without I/O when the column chunk has no Bloom
filter.
+ /// Invalid coordinates, invalid Bloom metadata, malformed filters, and
read
+ /// failures return an error. The filter is returned separately; the
supplied
+ /// metadata is neither modified nor reloaded.
+ ///
+ /// Prefer reading filters after other pruning has narrowed the row groups
and
+ /// columns of interest. Footer and page index reads never load Bloom
filters.
+ ///
+ /// # Example
+ ///
+ /// Read footer metadata first, then request a filter only when needed:
+ ///
+ /// ```rust
+ /// use bytes::Bytes;
+ /// use parquet::errors::Result;
+ /// use parquet::file::metadata::ParquetMetaDataReader;
+ /// # use std::sync::Arc;
+ /// # use parquet::data_type::Int32Type;
+ /// # use parquet::file::properties::WriterProperties;
+ /// # use parquet::file::writer::SerializedFileWriter;
+ /// # use parquet::schema::parser::parse_message_type;
+ /// # fn main() -> Result<()> {
+ /// # let schema = Arc::new(parse_message_type("message schema { REQUIRED
INT32 value; }")?);
+ /// # let properties =
Arc::new(WriterProperties::builder().set_bloom_filter_enabled(true).build());
+ /// # let mut writer = SerializedFileWriter::new(Vec::new(), schema,
properties)?;
+ /// # let mut row_group = writer.next_row_group()?;
+ /// # let mut column = row_group.next_column()?.unwrap();
+ /// # column.typed::<Int32Type>().write_batch(&[42], None, None)?;
+ /// # column.close()?;
+ /// # row_group.close()?;
+ /// # let data = Bytes::from(writer.into_inner()?);
+ /// let metadata = ParquetMetaDataReader::new().parse_and_finish(&data)?;
+ /// let filter = ParquetMetaDataReader::read_column_bloom_filter(&data,
&metadata, 0, 0)?;
Review Comment:
❤️
##########
parquet/src/file/metadata/reader.rs:
##########
@@ -1233,6 +1308,25 @@ fn parse_page_index_data(
}
}
+fn bloom_filter_column(
Review Comment:
Should this be a method on `ParquetMetadata` ? It seems like it is a very
reasonable accessor
##########
parquet/src/file/metadata/reader.rs:
##########
@@ -801,6 +805,77 @@ impl ParquetMetaDataReader {
self.load_page_index_with_remainder_impl(fetch, None).await
}
+ /// Reads the Bloom filter for one physical leaf column in a row group.
+ ///
+ /// The metadata and reader must refer to the same file. `column_idx` is a
+ /// physical Parquet leaf index, not a top-level Arrow field index.
+ ///
+ /// Returns `Ok(None)` without I/O when the column chunk has no Bloom
filter.
+ /// Invalid coordinates, invalid Bloom metadata, malformed filters, and
read
+ /// failures return an error. The filter is returned separately; the
supplied
+ /// metadata is neither modified nor reloaded.
+ ///
+ /// Prefer reading filters after other pruning has narrowed the row groups
and
+ /// columns of interest. Footer and page index reads never load Bloom
filters.
+ ///
+ /// # Example
+ ///
+ /// Read footer metadata first, then request a filter only when needed:
+ ///
+ /// ```rust
+ /// use bytes::Bytes;
+ /// use parquet::errors::Result;
+ /// use parquet::file::metadata::ParquetMetaDataReader;
+ /// # use std::sync::Arc;
+ /// # use parquet::data_type::Int32Type;
+ /// # use parquet::file::properties::WriterProperties;
+ /// # use parquet::file::writer::SerializedFileWriter;
+ /// # use parquet::schema::parser::parse_message_type;
+ /// # fn main() -> Result<()> {
+ /// # let schema = Arc::new(parse_message_type("message schema { REQUIRED
INT32 value; }")?);
+ /// # let properties =
Arc::new(WriterProperties::builder().set_bloom_filter_enabled(true).build());
+ /// # let mut writer = SerializedFileWriter::new(Vec::new(), schema,
properties)?;
+ /// # let mut row_group = writer.next_row_group()?;
+ /// # let mut column = row_group.next_column()?.unwrap();
+ /// # column.typed::<Int32Type>().write_batch(&[42], None, None)?;
+ /// # column.close()?;
+ /// # row_group.close()?;
+ /// # let data = Bytes::from(writer.into_inner()?);
+ /// let metadata = ParquetMetaDataReader::new().parse_and_finish(&data)?;
+ /// let filter = ParquetMetaDataReader::read_column_bloom_filter(&data,
&metadata, 0, 0)?;
+ /// assert!(filter.unwrap().check(&42_i32));
+ /// # Ok(())
+ /// # }
+ /// ```
+ pub fn read_column_bloom_filter<R: ChunkReader>(
+ reader: &R,
+ metadata: &ParquetMetaData,
+ row_group_idx: usize,
+ column_idx: usize,
+ ) -> Result<Option<Sbbf>> {
+ let column = bloom_filter_column(metadata, row_group_idx, column_idx)?;
+ Sbbf::read_from_column_chunk(column, reader)
+ }
+
+ /// Asynchronous version of [`Self::read_column_bloom_filter`].
+ ///
+ /// Accepts a [`MetadataFetch`] byte source without requiring an Arrow
reader
Review Comment:
is "without requiring an Arrow reader builder or an
[`crate::arrow::async_reader::AsyncFileReader`] implementation." an important
detail 🤔
It seems like that is obvious from the signature
##########
parquet/tests/arrow_reader/bloom_filter/metadata.rs:
##########
@@ -0,0 +1,280 @@
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-FileCopyrightText: See the NOTICE file distributed with this work for
additional information regarding copyright ownership
+// SPDX-FileContributor: Licensed to the Apache Software Foundation (ASF)
under one or more contributor license agreements
+
+use super::{RecordingReader, column};
+use bytes::Bytes;
+use parquet::bloom_filter::Sbbf;
+use parquet::file::metadata::{
+ FileMetaData, ParquetMetaData, ParquetMetaDataReader, RowGroupMetaData,
+};
+use parquet::schema::{parser::parse_message_type, types::SchemaDescriptor};
+use std::sync::Arc;
+
+fn metadata(offset: Option<i64>, length: Option<i32>) -> ParquetMetaData {
+ let schema = Arc::new(SchemaDescriptor::new(Arc::new(
+ parse_message_type("message schema { REQUIRED BYTE_ARRAY value;
}").unwrap(),
+ )));
+ let row_group = RowGroupMetaData::builder(schema.clone())
+ .set_column_metadata(vec![column(offset, length)])
+ .build()
+ .unwrap();
+ ParquetMetaData::new(
+ FileMetaData::new(1, 0, None, None, schema, None),
+ vec![row_group],
+ )
+}
+
+#[test]
+fn sync_metadata_bloom_absence_and_invalid_requests_do_not_read() {
+ let reader = RecordingReader::default();
+ let absent = metadata(None, Some(-1));
+ assert!(
+ ParquetMetaDataReader::read_column_bloom_filter(&reader, &absent, 0, 0)
+ .unwrap()
+ .is_none()
+ );
+ for (row_group, column) in [(1, 0), (0, 1), (usize::MAX, 0), (0,
usize::MAX)] {
+ assert!(
+ ParquetMetaDataReader::read_column_bloom_filter(&reader, &absent,
row_group, column)
+ .is_err()
+ );
+ }
+ for invalid in [metadata(Some(-1), None), metadata(Some(0), Some(0))] {
+ assert!(ParquetMetaDataReader::read_column_bloom_filter(&reader,
&invalid, 0, 0).is_err());
+ }
+ assert!(reader.requests().is_empty());
+}
+
+#[test]
+fn sync_metadata_bloom_selects_requested_row_group() {
+ let mut original = Sbbf::new_with_num_of_bytes(32);
+ original.insert("present");
+ let mut bytes = Vec::new();
+ original.write(&mut bytes).unwrap();
+ let bytes = Bytes::from(bytes);
+ let absent = metadata(None, None);
+ let present = metadata(Some(9), Some(bytes.len() as i32));
+ let metadata = ParquetMetaData::new(
+ absent.file_metadata().clone(),
+ vec![absent.row_group(0).clone(), present.row_group(0).clone()],
+ );
+ let reader = RecordingReader::new([bytes.clone()]);
+ assert!(
+ ParquetMetaDataReader::read_column_bloom_filter(&reader, &metadata, 0,
0)
+ .unwrap()
+ .is_none()
+ );
+ assert!(reader.requests().is_empty());
+ let filter = ParquetMetaDataReader::read_column_bloom_filter(&reader,
&metadata, 1, 0)
+ .unwrap()
+ .unwrap();
+ assert!(filter.check("present"));
+ assert_eq!(reader.requests(), vec![9..9 + bytes.len() as u64]);
+}
+
+#[cfg(feature = "async")]
+mod r#async {
+ use super::super::encode;
+ use super::*;
+ use futures::future::{BoxFuture, ready};
+ use parquet::arrow::async_reader::{AsyncFileReader, MetadataFetch};
+ use parquet::errors::Result;
+ use std::ops::Range;
+
+ // A metadata-only byte source: no AsyncFileReader or metadata getter
needed.
+ struct FetchOnly<'a>(&'a RecordingReader);
+
+ impl MetadataFetch for FetchOnly<'_> {
+ fn fetch(&mut self, range: Range<u64>) -> BoxFuture<'_, Result<Bytes>>
{
+ Box::pin(ready(self.0.read(range)))
+ }
+ }
+
+ #[tokio::test]
Review Comment:
seems like a lot of boiler plate in this test -- I wonder if we can use
fixtures or something to reduce the replication
##########
parquet/tests/arrow_reader/bloom_filter/metadata.rs:
##########
@@ -0,0 +1,280 @@
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-FileCopyrightText: See the NOTICE file distributed with this work for
additional information regarding copyright ownership
+// SPDX-FileContributor: Licensed to the Apache Software Foundation (ASF)
under one or more contributor license agreements
+
+use super::{RecordingReader, column};
+use bytes::Bytes;
+use parquet::bloom_filter::Sbbf;
+use parquet::file::metadata::{
+ FileMetaData, ParquetMetaData, ParquetMetaDataReader, RowGroupMetaData,
+};
+use parquet::schema::{parser::parse_message_type, types::SchemaDescriptor};
+use std::sync::Arc;
+
+fn metadata(offset: Option<i64>, length: Option<i32>) -> ParquetMetaData {
+ let schema = Arc::new(SchemaDescriptor::new(Arc::new(
+ parse_message_type("message schema { REQUIRED BYTE_ARRAY value;
}").unwrap(),
+ )));
+ let row_group = RowGroupMetaData::builder(schema.clone())
+ .set_column_metadata(vec![column(offset, length)])
+ .build()
+ .unwrap();
+ ParquetMetaData::new(
+ FileMetaData::new(1, 0, None, None, schema, None),
+ vec![row_group],
+ )
+}
+
+#[test]
+fn sync_metadata_bloom_absence_and_invalid_requests_do_not_read() {
+ let reader = RecordingReader::default();
+ let absent = metadata(None, Some(-1));
+ assert!(
+ ParquetMetaDataReader::read_column_bloom_filter(&reader, &absent, 0, 0)
+ .unwrap()
+ .is_none()
+ );
+ for (row_group, column) in [(1, 0), (0, 1), (usize::MAX, 0), (0,
usize::MAX)] {
+ assert!(
+ ParquetMetaDataReader::read_column_bloom_filter(&reader, &absent,
row_group, column)
+ .is_err()
+ );
+ }
+ for invalid in [metadata(Some(-1), None), metadata(Some(0), Some(0))] {
+ assert!(ParquetMetaDataReader::read_column_bloom_filter(&reader,
&invalid, 0, 0).is_err());
+ }
+ assert!(reader.requests().is_empty());
+}
+
+#[test]
+fn sync_metadata_bloom_selects_requested_row_group() {
+ let mut original = Sbbf::new_with_num_of_bytes(32);
+ original.insert("present");
+ let mut bytes = Vec::new();
+ original.write(&mut bytes).unwrap();
+ let bytes = Bytes::from(bytes);
+ let absent = metadata(None, None);
+ let present = metadata(Some(9), Some(bytes.len() as i32));
+ let metadata = ParquetMetaData::new(
+ absent.file_metadata().clone(),
+ vec![absent.row_group(0).clone(), present.row_group(0).clone()],
+ );
+ let reader = RecordingReader::new([bytes.clone()]);
+ assert!(
+ ParquetMetaDataReader::read_column_bloom_filter(&reader, &metadata, 0,
0)
+ .unwrap()
+ .is_none()
+ );
+ assert!(reader.requests().is_empty());
+ let filter = ParquetMetaDataReader::read_column_bloom_filter(&reader,
&metadata, 1, 0)
+ .unwrap()
+ .unwrap();
+ assert!(filter.check("present"));
+ assert_eq!(reader.requests(), vec![9..9 + bytes.len() as u64]);
+}
+
+#[cfg(feature = "async")]
+mod r#async {
+ use super::super::encode;
Review Comment:
There is a burrito place near us that sells "super super burritos" which
this made me think of
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]