use std::fmt::Formatter;
use std::io::SeekFrom;
use std::ops::Range;
use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context, Poll};
use bytes::Bytes;
use futures::future::{BoxFuture, FutureExt};
use futures::stream::Stream;
use tokio::io::{AsyncRead, AsyncReadExt, AsyncSeek, AsyncSeekExt};
use arrow_array::{ArrayRef, RecordBatch};
use arrow_schema::{Schema, SchemaRef};
use crate::arrow::arrow_reader::{
ArrowReaderBuilder, ArrowReaderMetadata, ArrowReaderOptions, ParquetRecordBatchReader,
};
use crate::basic::{BloomFilterAlgorithm, BloomFilterCompression, BloomFilterHash};
use crate::bloom_filter::{
SBBF_HEADER_SIZE_ESTIMATE, Sbbf, chunk_read_bloom_filter_header_and_offset,
};
use crate::errors::{ParquetError, Result};
use crate::file::metadata::{ParquetMetaData, ParquetMetaDataReader};
mod metadata;
pub use metadata::*;
mod spawn;
pub use spawn::SpawnedReader;
pub use crate::arrow::arrow_reader::RowGroupSelection;
#[cfg(feature = "object_store")]
mod store;
use crate::DecodeResult;
use crate::arrow::push_decoder::{ParquetPushDecoder, ParquetPushDecoderBuilder, PushDecoderInput};
#[cfg(feature = "object_store")]
pub use store::*;
pub trait AsyncFileReader: Send {
fn get_bytes(&mut self, range: Range<u64>) -> BoxFuture<'_, Result<Bytes>>;
fn get_byte_ranges(&mut self, ranges: Vec<Range<u64>>) -> BoxFuture<'_, Result<Vec<Bytes>>> {
async move {
let mut result = Vec::with_capacity(ranges.len());
for range in ranges {
let data = self.get_bytes(range).await?;
result.push(data);
}
Ok(result)
}
.boxed()
}
fn get_metadata<'a>(
&'a mut self,
options: Option<&'a ArrowReaderOptions>,
) -> BoxFuture<'a, Result<Arc<ParquetMetaData>>>;
}
impl AsyncFileReader for Box<dyn AsyncFileReader + '_> {
fn get_bytes(&mut self, range: Range<u64>) -> BoxFuture<'_, Result<Bytes>> {
self.as_mut().get_bytes(range)
}
fn get_byte_ranges(&mut self, ranges: Vec<Range<u64>>) -> BoxFuture<'_, Result<Vec<Bytes>>> {
self.as_mut().get_byte_ranges(ranges)
}
fn get_metadata<'a>(
&'a mut self,
options: Option<&'a ArrowReaderOptions>,
) -> BoxFuture<'a, Result<Arc<ParquetMetaData>>> {
self.as_mut().get_metadata(options)
}
}
impl<T: AsyncFileReader + MetadataFetch + AsyncRead + AsyncSeek + Unpin> MetadataSuffixFetch for T {
fn fetch_suffix(&mut self, suffix: usize) -> BoxFuture<'_, Result<Bytes>> {
async move {
self.seek(SeekFrom::End(-(suffix as i64))).await?;
let mut buf = Vec::with_capacity(suffix);
self.take(suffix as _).read_to_end(&mut buf).await?;
Ok(buf.into())
}
.boxed()
}
}
impl<T: AsyncRead + AsyncSeek + Unpin + Send> AsyncFileReader for T {
fn get_bytes(&mut self, range: Range<u64>) -> BoxFuture<'_, Result<Bytes>> {
async move {
self.seek(SeekFrom::Start(range.start)).await?;
let to_read = range.end - range.start;
let mut buffer = Vec::with_capacity(to_read.try_into()?);
let read = self.take(to_read).read_to_end(&mut buffer).await?;
if read as u64 != to_read {
return Err(eof_err!("expected to read {} bytes, got {}", to_read, read));
}
Ok(buffer.into())
}
.boxed()
}
fn get_metadata<'a>(
&'a mut self,
options: Option<&'a ArrowReaderOptions>,
) -> BoxFuture<'a, Result<Arc<ParquetMetaData>>> {
async move {
let metadata_reader = ParquetMetaDataReader::new().with_arrow_reader_options(options);
let parquet_metadata = metadata_reader.load_via_suffix_and_finish(self).await?;
Ok(Arc::new(parquet_metadata))
}
.boxed()
}
}
impl ArrowReaderMetadata {
pub async fn load_async<T: AsyncFileReader>(
input: &mut T,
options: ArrowReaderOptions,
) -> Result<Self> {
let metadata = input.get_metadata(Some(&options)).await?;
Self::try_new(metadata, options)
}
}
#[doc(hidden)]
pub struct AsyncReader<T>(T);
pub type ParquetRecordBatchStreamBuilder<T> = ArrowReaderBuilder<AsyncReader<T>>;
impl<T: AsyncFileReader + Send + 'static> ParquetRecordBatchStreamBuilder<T> {
pub async fn new(input: T) -> Result<Self> {
Self::new_with_options(input, Default::default()).await
}
pub async fn new_with_options(mut input: T, options: ArrowReaderOptions) -> Result<Self> {
let metadata = ArrowReaderMetadata::load_async(&mut input, options).await?;
Ok(Self::new_with_metadata(input, metadata))
}
pub fn new_with_metadata(input: T, metadata: ArrowReaderMetadata) -> Self {
Self::new_builder(AsyncReader(input), metadata)
}
pub async fn get_row_group_column_bloom_filter(
&mut self,
row_group_idx: usize,
column_idx: usize,
) -> Result<Option<Sbbf>> {
let metadata = self.metadata.row_group(row_group_idx);
let column_metadata = metadata.column(column_idx);
let offset: u64 = if let Some(offset) = column_metadata.bloom_filter_offset() {
offset
.try_into()
.map_err(|_| ParquetError::General("Bloom filter offset is invalid".to_string()))?
} else {
return Ok(None);
};
let buffer = match column_metadata.bloom_filter_length() {
Some(length) => self.input.0.get_bytes(offset..offset + length as u64),
None => self
.input
.0
.get_bytes(offset..offset + SBBF_HEADER_SIZE_ESTIMATE as u64),
}
.await?;
let (header, bitset_offset) =
chunk_read_bloom_filter_header_and_offset(offset, buffer.clone())?;
match header.algorithm {
BloomFilterAlgorithm::BLOCK => {
}
}
match header.compression {
BloomFilterCompression::UNCOMPRESSED => {
}
}
match header.hash {
BloomFilterHash::XXHASH => {
}
}
let bitset = match column_metadata.bloom_filter_length() {
Some(_) => {
let bitset_start = bitset_offset
.checked_sub(offset)
.and_then(|start| usize::try_from(start).ok())
.ok_or_else(|| {
ParquetError::General("Bloom filter offset is invalid".to_string())
})?;
buffer.slice(bitset_start..)
}
None => {
let bitset_length: u64 = header.num_bytes.try_into().map_err(|_| {
ParquetError::General("Bloom filter length is invalid".to_string())
})?;
self.input
.0
.get_bytes(bitset_offset..bitset_offset + bitset_length)
.await?
}
};
Ok(Some(Sbbf::new(&bitset)))
}
pub fn with_row_group_selections(
mut self,
row_group_selections: Vec<RowGroupSelection>,
) -> Self {
self.row_group_plan
.set_row_group_selections(row_group_selections);
self
}
pub async fn get_column_chunk_dictionary(
&mut self,
row_group_idx: usize,
column_idx: usize,
) -> Result<Option<ArrayRef>> {
ParquetMetaDataReader::read_column_dictionary_async(
&mut self.input.0,
&self.metadata,
row_group_idx,
column_idx,
)
.await
}
pub fn build(self) -> Result<ParquetRecordBatchStream<T>> {
let Self {
input,
metadata,
schema,
fields,
batch_size,
row_group_plan,
projection,
filter,
row_selection_policy: selection_strategy,
limit,
offset,
metrics,
max_predicate_cache_size,
} = self;
let projection_len = projection.mask.as_ref().map_or(usize::MAX, |m| m.len());
let projected_fields = schema
.fields
.filter_leaves(|idx, _| idx < projection_len && projection.leaf_included(idx));
let projected_schema = Arc::new(Schema::new(projected_fields));
let decoder = ParquetPushDecoderBuilder {
input: PushDecoderInput::default(),
metadata,
schema,
fields,
projection,
filter,
row_group_plan,
row_selection_policy: selection_strategy,
batch_size,
limit,
offset,
metrics,
max_predicate_cache_size,
}
.build()?;
let request_state = RequestState::None { input: input.0 };
Ok(ParquetRecordBatchStream {
schema: projected_schema,
decoder,
request_state,
})
}
}
enum RequestState<T> {
None {
input: T,
},
Outstanding {
ranges: Vec<Range<u64>>,
future: BoxFuture<'static, Result<(T, Vec<Bytes>)>>,
},
Done,
}
impl<T> RequestState<T>
where
T: AsyncFileReader + Unpin + Send + 'static,
{
fn begin_request(mut input: T, ranges: Vec<Range<u64>>) -> Self {
let ranges_captured = ranges.clone();
let future = async move {
let data = input.get_byte_ranges(ranges_captured).await?;
Ok((input, data))
}
.boxed();
RequestState::Outstanding { ranges, future }
}
}
impl<T> std::fmt::Debug for RequestState<T> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
RequestState::None { input: _ } => f
.debug_struct("RequestState::None")
.field("input", &"...")
.finish(),
RequestState::Outstanding { ranges, .. } => f
.debug_struct("RequestState::Outstanding")
.field("ranges", &ranges)
.finish(),
RequestState::Done => {
write!(f, "RequestState::Done")
}
}
}
}
pub struct ParquetRecordBatchStream<T> {
schema: SchemaRef,
request_state: RequestState<T>,
decoder: ParquetPushDecoder,
}
impl<T> std::fmt::Debug for ParquetRecordBatchStream<T> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ParquetRecordBatchStream")
.field("request_state", &self.request_state)
.finish()
}
}
impl<T> ParquetRecordBatchStream<T> {
pub fn schema(&self) -> &SchemaRef {
&self.schema
}
}
impl<T> ParquetRecordBatchStream<T>
where
T: AsyncFileReader + Unpin + Send + 'static,
{
pub async fn next_row_group(&mut self) -> Result<Option<ParquetRecordBatchReader>> {
loop {
let request_state = std::mem::replace(&mut self.request_state, RequestState::Done);
match request_state {
RequestState::None { input } => {
match self.decoder.try_next_reader()? {
DecodeResult::NeedsData(ranges) => {
self.request_state = RequestState::begin_request(input, ranges);
}
DecodeResult::Data(reader) => {
self.request_state = RequestState::None { input };
return Ok(Some(reader));
}
DecodeResult::Finished => return Ok(None),
}
}
RequestState::Outstanding { ranges, future } => {
let (input, data) = future.await?;
self.decoder.push_ranges(ranges, data)?;
self.request_state = RequestState::None { input };
}
RequestState::Done => {
self.request_state = RequestState::Done;
return Ok(None);
}
}
}
}
}
impl<T> Stream for ParquetRecordBatchStream<T>
where
T: AsyncFileReader + Unpin + Send + 'static,
{
type Item = Result<RecordBatch>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
match self.poll_next_inner(cx) {
Ok(res) => {
res.map(|res| Ok(res).transpose())
}
Err(e) => {
self.request_state = RequestState::Done;
Poll::Ready(Some(Err(e)))
}
}
}
}
impl<T> ParquetRecordBatchStream<T>
where
T: AsyncFileReader + Unpin + Send + 'static,
{
fn poll_next_inner(&mut self, cx: &mut Context<'_>) -> Result<Poll<Option<RecordBatch>>> {
loop {
let request_state = std::mem::replace(&mut self.request_state, RequestState::Done);
match request_state {
RequestState::None { input } => {
match self.decoder.try_decode()? {
DecodeResult::NeedsData(ranges) => {
self.request_state = RequestState::begin_request(input, ranges);
}
DecodeResult::Data(batch) => {
self.request_state = RequestState::None { input };
return Ok(Poll::Ready(Some(batch)));
}
DecodeResult::Finished => {
self.request_state = RequestState::Done;
return Ok(Poll::Ready(None));
}
}
}
RequestState::Outstanding { ranges, mut future } => match future.poll_unpin(cx) {
Poll::Ready(result) => {
let (input, data) = result?;
self.decoder.push_ranges(ranges, data)?;
self.request_state = RequestState::None { input };
}
Poll::Pending => {
self.request_state = RequestState::Outstanding { ranges, future };
return Ok(Poll::Pending);
}
},
RequestState::Done => {
self.request_state = RequestState::Done;
return Ok(Poll::Ready(None));
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::arrow::ArrowWriter;
use crate::arrow::ProjectionMask;
use crate::arrow::arrow_reader::tests::test_row_numbers_with_multiple_row_groups_helper;
use crate::arrow::arrow_reader::{ArrowReaderMetadata, ArrowReaderOptions};
use crate::arrow::schema::virtual_type::RowNumber;
use crate::basic::Encoding;
use crate::file::metadata::PageIndexPolicy;
use crate::file::metadata::ParquetMetaDataReader;
use crate::file::metadata::page_index::PageIndex;
use crate::file::properties::WriterProperties;
use arrow_array::cast::AsArray;
use arrow_array::{Array, ArrayRef, BinaryArray, StringArray};
use arrow_schema::{DataType, Field, Schema};
use futures::TryStreamExt;
use std::sync::{Arc, Mutex};
#[derive(Clone)]
struct TestReader {
data: Bytes,
metadata: Option<Arc<ParquetMetaData>>,
requests: Arc<Mutex<Vec<Range<usize>>>>,
}
impl TestReader {
fn new(data: Bytes) -> Self {
Self {
data,
metadata: Default::default(),
requests: Default::default(),
}
}
}
impl AsyncFileReader for TestReader {
fn get_bytes(&mut self, range: Range<u64>) -> BoxFuture<'_, Result<Bytes>> {
let range = range.clone();
self.requests
.lock()
.unwrap()
.push(range.start as usize..range.end as usize);
futures::future::ready(Ok(self
.data
.slice(range.start as usize..range.end as usize)))
.boxed()
}
fn get_metadata<'a>(
&'a mut self,
options: Option<&'a ArrowReaderOptions>,
) -> BoxFuture<'a, Result<Arc<ParquetMetaData>>> {
let metadata_reader = ParquetMetaDataReader::new().with_arrow_reader_options(options);
self.metadata = Some(Arc::new(
metadata_reader.parse_and_finish(&self.data).unwrap(),
));
futures::future::ready(Ok(self.metadata.clone().unwrap().clone())).boxed()
}
}
#[tokio::test]
async fn test_get_column_chunk_dictionary() {
let schema = Arc::new(Schema::new(vec![Field::new("s", DataType::Utf8, false)]));
let values: Vec<&str> = ["alpha", "beta", "gamma"]
.iter()
.copied()
.cycle()
.take(30)
.collect();
let array: ArrayRef = Arc::new(StringArray::from(values));
let batch = RecordBatch::try_new(schema.clone(), vec![array]).unwrap();
let props = WriterProperties::builder()
.set_dictionary_enabled(true)
.build();
let mut buf = Vec::new();
{
let mut writer = ArrowWriter::try_new(&mut buf, schema, Some(props)).unwrap();
writer.write(&batch).unwrap();
writer.close().unwrap();
}
let data = Bytes::from(buf);
let direct_metadata = ParquetMetaDataReader::new()
.parse_and_finish(&data)
.unwrap();
let mut direct_reader = TestReader::new(data.clone());
let direct = ParquetMetaDataReader::read_column_dictionary_async(
&mut direct_reader,
&direct_metadata,
0,
0,
)
.await
.unwrap()
.unwrap();
let direct = direct.as_any().downcast_ref::<BinaryArray>().unwrap();
assert_eq!(direct.value(0), b"alpha");
let async_reader = TestReader::new(data);
let mut builder = ParquetRecordBatchStreamBuilder::new(async_reader)
.await
.unwrap();
let dictionary = builder
.get_column_chunk_dictionary(0, 0)
.await
.unwrap()
.unwrap();
let dictionary = dictionary.as_any().downcast_ref::<BinaryArray>().unwrap();
let dictionary_values: Vec<&[u8]> = dictionary.iter().map(|v| v.unwrap()).collect();
assert_eq!(
dictionary_values,
vec![b"alpha".as_slice(), b"beta", b"gamma"]
);
}
#[tokio::test]
async fn test_dictionary_selects_row_groups_without_reading_skipped_data() {
let schema = Arc::new(Schema::new(vec![Field::new("s", DataType::Utf8, false)]));
let row_group_values = ["skip", "target"];
let props = WriterProperties::builder()
.set_dictionary_enabled(true)
.build();
let mut buf = Vec::new();
{
let mut writer = ArrowWriter::try_new(&mut buf, schema.clone(), Some(props)).unwrap();
for value in row_group_values {
let array: ArrayRef = Arc::new(StringArray::from(vec![value; 30]));
let batch = RecordBatch::try_new(schema.clone(), vec![array]).unwrap();
writer.write(&batch).unwrap();
writer.flush().unwrap();
}
writer.close().unwrap();
}
let async_reader = TestReader::new(Bytes::from(buf));
let requests = async_reader.requests.clone();
let mut builder = ParquetRecordBatchStreamBuilder::new(async_reader)
.await
.unwrap();
let metadata = builder.metadata().clone();
assert_eq!(metadata.num_row_groups(), 2);
let mut selected_row_groups = Vec::new();
let mut dictionary_ranges = Vec::new();
for row_group_idx in 0..metadata.num_row_groups() {
let column = metadata.row_group(row_group_idx).column(0);
let encoding_mask = column.page_encoding_stats_mask().unwrap();
assert!(
encoding_mask.is_only(Encoding::PLAIN_DICTIONARY)
|| encoding_mask.is_only(Encoding::RLE_DICTIONARY)
);
let dictionary_start = column.dictionary_page_offset().unwrap() as usize;
let data_start = column.data_page_offset() as usize;
dictionary_ranges.push(dictionary_start..data_start);
let dictionary = builder
.get_column_chunk_dictionary(row_group_idx, 0)
.await
.unwrap()
.unwrap();
let dictionary = dictionary.as_binary::<i32>();
if dictionary
.iter()
.any(|value| value == Some(b"target".as_slice()))
{
selected_row_groups.push(row_group_idx);
}
}
assert_eq!(selected_row_groups, vec![1]);
let batches: Vec<_> = builder
.with_row_groups(selected_row_groups)
.build()
.unwrap()
.try_collect()
.await
.unwrap();
let values: Vec<_> = batches
.iter()
.flat_map(|batch| batch.column(0).as_string::<i32>().iter())
.collect();
assert_eq!(values, vec![Some("target"); 30]);
let skipped_column = metadata.row_group(0).column(0);
let (skipped_start, skipped_len) = skipped_column.byte_range();
let skipped_data_range =
skipped_column.data_page_offset() as usize..(skipped_start + skipped_len) as usize;
let requests = requests.lock().unwrap();
for dictionary_range in dictionary_ranges {
assert!(requests.contains(&dictionary_range));
}
assert!(requests.iter().all(|request| {
request.end <= skipped_data_range.start || request.start >= skipped_data_range.end
}));
}
#[tokio::test]
async fn test_batch_size_overallocate() {
let testdata = arrow::util::test_util::parquet_test_data();
let path = format!("{testdata}/alltypes_plain.parquet");
let data = Bytes::from(std::fs::read(path).unwrap());
let async_reader = TestReader::new(data.clone());
let builder = ParquetRecordBatchStreamBuilder::new(async_reader)
.await
.unwrap();
let file_rows = builder.metadata().file_metadata().num_rows() as usize;
let builder = builder
.with_projection(ProjectionMask::all())
.with_batch_size(1024);
assert_ne!(1024, file_rows);
assert_eq!(builder.batch_size, file_rows);
let _stream = builder.build().unwrap();
}
#[tokio::test]
async fn empty_offset_index_doesnt_panic_in_read_row_group() {
use tokio::fs::File;
let testdata = arrow::util::test_util::parquet_test_data();
let path = format!("{testdata}/alltypes_plain.parquet");
let mut file = File::open(&path).await.unwrap();
let file_size = file.metadata().await.unwrap().len();
let mut metadata = ParquetMetaDataReader::new()
.with_page_index_policy(PageIndexPolicy::Required)
.load_and_finish(&mut file, file_size)
.await
.unwrap();
let page_index = PageIndex::new(None, Some(vec![]));
metadata.set_page_index(Some(Arc::new(page_index)));
let options = ArrowReaderOptions::new().with_page_index_policy(PageIndexPolicy::Required);
let arrow_reader_metadata = ArrowReaderMetadata::try_new(metadata.into(), options).unwrap();
let reader =
ParquetRecordBatchStreamBuilder::new_with_metadata(file, arrow_reader_metadata)
.build()
.unwrap();
let result = reader.try_collect::<Vec<_>>().await.unwrap();
assert_eq!(result.len(), 1);
}
#[test]
fn test_row_numbers_with_multiple_row_groups() {
test_row_numbers_with_multiple_row_groups_helper(
false,
|path, selection, _row_filter, batch_size| {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("Could not create runtime");
runtime.block_on(async move {
let file = tokio::fs::File::open(path).await.unwrap();
let row_number_field = Arc::new(
Field::new("row_number", DataType::Int64, false)
.with_extension_type(RowNumber),
);
let options = ArrowReaderOptions::new()
.with_virtual_columns(vec![row_number_field])
.unwrap();
let reader = ParquetRecordBatchStreamBuilder::new_with_options(file, options)
.await
.unwrap()
.with_row_selection(selection)
.with_batch_size(batch_size)
.build()
.expect("Could not create reader");
reader.try_collect::<Vec<_>>().await.unwrap()
})
},
);
}
#[test]
fn test_row_numbers_with_multiple_row_groups_and_filter() {
test_row_numbers_with_multiple_row_groups_helper(
true,
|path, selection, row_filter, batch_size| {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("Could not create runtime");
runtime.block_on(async move {
let file = tokio::fs::File::open(path).await.unwrap();
let row_number_field = Arc::new(
Field::new("row_number", DataType::Int64, false)
.with_extension_type(RowNumber),
);
let options = ArrowReaderOptions::new()
.with_virtual_columns(vec![row_number_field])
.unwrap();
let reader = ParquetRecordBatchStreamBuilder::new_with_options(file, options)
.await
.unwrap()
.with_row_selection(selection)
.with_row_filter(row_filter.expect("No row filter"))
.with_batch_size(batch_size)
.build()
.expect("Could not create reader");
reader.try_collect::<Vec<_>>().await.unwrap()
})
},
);
}
}