parquet/arrow/async_reader/mod.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//! `async` API for reading Parquet files as [`RecordBatch`]es
19//!
20//! See the [crate-level documentation](crate) for more details.
21//!
22//! See example on [`ParquetRecordBatchStreamBuilder::new`]
23
24use std::fmt::Formatter;
25use std::io::SeekFrom;
26use std::ops::Range;
27use std::pin::Pin;
28use std::sync::Arc;
29use std::task::{Context, Poll};
30
31use bytes::Bytes;
32use futures::future::{BoxFuture, FutureExt};
33use futures::stream::Stream;
34use tokio::io::{AsyncRead, AsyncReadExt, AsyncSeek, AsyncSeekExt};
35
36use arrow_array::{ArrayRef, RecordBatch};
37use arrow_schema::{Schema, SchemaRef};
38
39use crate::arrow::arrow_reader::{
40 ArrowReaderBuilder, ArrowReaderMetadata, ArrowReaderOptions, ParquetRecordBatchReader,
41};
42
43use crate::basic::{BloomFilterAlgorithm, BloomFilterCompression, BloomFilterHash};
44use crate::bloom_filter::{
45 SBBF_HEADER_SIZE_ESTIMATE, Sbbf, chunk_read_bloom_filter_header_and_offset,
46};
47use crate::errors::{ParquetError, Result};
48use crate::file::metadata::{ParquetMetaData, ParquetMetaDataReader};
49
50mod metadata;
51pub use metadata::*;
52
53mod spawn;
54pub use spawn::SpawnedReader;
55
56/// Re-exported so [`ParquetRecordBatchStreamBuilder::with_row_group_selections`]
57/// can be used without importing from another module.
58pub use crate::arrow::arrow_reader::RowGroupSelection;
59
60#[cfg(feature = "object_store")]
61mod store;
62
63use crate::DecodeResult;
64use crate::arrow::push_decoder::{ParquetPushDecoder, ParquetPushDecoderBuilder, PushDecoderInput};
65#[cfg(feature = "object_store")]
66pub use store::*;
67
68/// The asynchronous interface used by [`ParquetRecordBatchStream`] to read parquet files
69///
70/// Notes:
71///
72/// 1. There is a default implementation for types that implement [`AsyncRead`]
73/// and [`AsyncSeek`], for example [`tokio::fs::File`].
74///
75/// 2. Implementations for remote storage, such as the `object_store` crate,
76/// can implement this interface directly, typically by pairing a store
77/// handle with an object path and delegating [`Self::get_bytes`] and
78/// [`Self::get_byte_ranges`] to ranged reads. [`SpawnedReader`] can wrap
79/// such a reader to perform its I/O on a dedicated runtime, and
80/// [`ParquetMetaDataReader::with_arrow_reader_options`] simplifies
81/// implementing [`Self::get_metadata`].
82///
83/// # Example: implementing `AsyncFileReader` for the `object_store` crate
84///
85/// ```no_run
86/// # use std::ops::Range;
87/// # use std::sync::Arc;
88/// use bytes::Bytes;
89/// use futures::future::BoxFuture;
90/// use futures::{FutureExt, TryFutureExt};
91/// use object_store::path::Path;
92/// use object_store::{GetOptions, GetRange, ObjectStore, ObjectStoreExt};
93/// use parquet::arrow::arrow_reader::ArrowReaderOptions;
94/// use parquet::arrow::async_reader::{AsyncFileReader, MetadataSuffixFetch};
95/// use parquet::errors::{ParquetError, Result};
96/// use parquet::file::metadata::{ParquetMetaData, ParquetMetaDataReader};
97///
98/// fn to_parquet_err(e: object_store::Error) -> ParquetError {
99/// ParquetError::External(Box::new(e))
100/// }
101///
102/// #[derive(Clone)]
103/// struct ObjectStoreReader {
104/// store: Arc<dyn ObjectStore>,
105/// path: Path,
106/// }
107///
108/// impl AsyncFileReader for ObjectStoreReader {
109/// fn get_bytes(&mut self, range: Range<u64>) -> BoxFuture<'_, Result<Bytes>> {
110/// self.store
111/// .get_range(&self.path, range)
112/// .map_err(to_parquet_err)
113/// .boxed()
114/// }
115///
116/// fn get_byte_ranges(&mut self, ranges: Vec<Range<u64>>) -> BoxFuture<'_, Result<Vec<Bytes>>> {
117/// async move {
118/// self.store
119/// .get_ranges(&self.path, &ranges)
120/// .await
121/// .map_err(to_parquet_err)
122/// }
123/// .boxed()
124/// }
125///
126/// fn get_metadata<'a>(
127/// &'a mut self,
128/// options: Option<&'a ArrowReaderOptions>,
129/// ) -> BoxFuture<'a, Result<Arc<ParquetMetaData>>> {
130/// async move {
131/// let metadata = ParquetMetaDataReader::new()
132/// .with_arrow_reader_options(options)
133/// .load_via_suffix_and_finish(self)
134/// .await?;
135/// Ok(Arc::new(metadata))
136/// }
137/// .boxed()
138/// }
139/// }
140///
141/// /// Supports fetching the parquet footer without knowing the file size,
142/// /// via suffix range requests
143/// impl MetadataSuffixFetch for &mut ObjectStoreReader {
144/// fn fetch_suffix(&mut self, suffix: usize) -> BoxFuture<'_, Result<Bytes>> {
145/// let options = GetOptions {
146/// range: Some(GetRange::Suffix(suffix as u64)),
147/// ..Default::default()
148/// };
149/// async move {
150/// let resp = self
151/// .store
152/// .get_opts(&self.path, options)
153/// .await
154/// .map_err(to_parquet_err)?;
155/// resp.bytes().await.map_err(to_parquet_err)
156/// }
157/// .boxed()
158/// }
159/// }
160/// ```
161///
162/// [`ParquetMetaDataReader::with_arrow_reader_options`]: crate::file::metadata::ParquetMetaDataReader::with_arrow_reader_options
163///
164/// [`tokio::fs::File`]: https://docs.rs/tokio/latest/tokio/fs/struct.File.html
165pub trait AsyncFileReader: Send {
166 /// Retrieve the bytes in `range`
167 fn get_bytes(&mut self, range: Range<u64>) -> BoxFuture<'_, Result<Bytes>>;
168
169 /// Retrieve multiple byte ranges. The default implementation will call `get_bytes` sequentially
170 fn get_byte_ranges(&mut self, ranges: Vec<Range<u64>>) -> BoxFuture<'_, Result<Vec<Bytes>>> {
171 async move {
172 let mut result = Vec::with_capacity(ranges.len());
173
174 for range in ranges {
175 let data = self.get_bytes(range).await?;
176 result.push(data);
177 }
178
179 Ok(result)
180 }
181 .boxed()
182 }
183
184 /// Return a future which results in the [`ParquetMetaData`] for this Parquet file.
185 ///
186 /// This is an asynchronous operation as it may involve reading the file
187 /// footer and potentially other metadata from disk or a remote source.
188 ///
189 /// Reading data from Parquet requires the metadata to understand the
190 /// schema, row groups, and location of pages within the file. This metadata
191 /// is stored primarily in the footer of the Parquet file, and can be read using
192 /// [`ParquetMetaDataReader`].
193 ///
194 /// However, implementations can significantly speed up reading Parquet by
195 /// supplying cached metadata or pre-fetched metadata via this API.
196 ///
197 /// # Parameters
198 /// * `options`: Optional [`ArrowReaderOptions`] that may contain decryption
199 /// and other options that affect how the metadata is read.
200 fn get_metadata<'a>(
201 &'a mut self,
202 options: Option<&'a ArrowReaderOptions>,
203 ) -> BoxFuture<'a, Result<Arc<ParquetMetaData>>>;
204}
205
206/// This allows Box<dyn AsyncFileReader + '_> to be used as an AsyncFileReader,
207impl AsyncFileReader for Box<dyn AsyncFileReader + '_> {
208 fn get_bytes(&mut self, range: Range<u64>) -> BoxFuture<'_, Result<Bytes>> {
209 self.as_mut().get_bytes(range)
210 }
211
212 fn get_byte_ranges(&mut self, ranges: Vec<Range<u64>>) -> BoxFuture<'_, Result<Vec<Bytes>>> {
213 self.as_mut().get_byte_ranges(ranges)
214 }
215
216 fn get_metadata<'a>(
217 &'a mut self,
218 options: Option<&'a ArrowReaderOptions>,
219 ) -> BoxFuture<'a, Result<Arc<ParquetMetaData>>> {
220 self.as_mut().get_metadata(options)
221 }
222}
223
224impl<T: AsyncFileReader + MetadataFetch + AsyncRead + AsyncSeek + Unpin> MetadataSuffixFetch for T {
225 fn fetch_suffix(&mut self, suffix: usize) -> BoxFuture<'_, Result<Bytes>> {
226 async move {
227 self.seek(SeekFrom::End(-(suffix as i64))).await?;
228 let mut buf = Vec::with_capacity(suffix);
229 self.take(suffix as _).read_to_end(&mut buf).await?;
230 Ok(buf.into())
231 }
232 .boxed()
233 }
234}
235
236impl<T: AsyncRead + AsyncSeek + Unpin + Send> AsyncFileReader for T {
237 fn get_bytes(&mut self, range: Range<u64>) -> BoxFuture<'_, Result<Bytes>> {
238 async move {
239 self.seek(SeekFrom::Start(range.start)).await?;
240
241 let to_read = range.end - range.start;
242 let mut buffer = Vec::with_capacity(to_read.try_into()?);
243 let read = self.take(to_read).read_to_end(&mut buffer).await?;
244 if read as u64 != to_read {
245 return Err(eof_err!("expected to read {} bytes, got {}", to_read, read));
246 }
247
248 Ok(buffer.into())
249 }
250 .boxed()
251 }
252
253 fn get_metadata<'a>(
254 &'a mut self,
255 options: Option<&'a ArrowReaderOptions>,
256 ) -> BoxFuture<'a, Result<Arc<ParquetMetaData>>> {
257 async move {
258 let metadata_reader = ParquetMetaDataReader::new().with_arrow_reader_options(options);
259 let parquet_metadata = metadata_reader.load_via_suffix_and_finish(self).await?;
260 Ok(Arc::new(parquet_metadata))
261 }
262 .boxed()
263 }
264}
265
266impl ArrowReaderMetadata {
267 /// Returns a new [`ArrowReaderMetadata`] for this builder
268 ///
269 /// See [`ParquetRecordBatchStreamBuilder::new_with_metadata`] for how this can be used
270 pub async fn load_async<T: AsyncFileReader>(
271 input: &mut T,
272 options: ArrowReaderOptions,
273 ) -> Result<Self> {
274 let metadata = input.get_metadata(Some(&options)).await?;
275 Self::try_new(metadata, options)
276 }
277}
278
279#[doc(hidden)]
280/// Newtype (wrapper) used within [`ArrowReaderBuilder`] to distinguish sync readers from async
281///
282/// Allows sharing the same builder for different readers while keeping the same
283/// ParquetRecordBatchStreamBuilder API
284pub struct AsyncReader<T>(T);
285
286/// A builder for reading parquet files from an `async` source as [`ParquetRecordBatchStream`]
287///
288/// This can be used to decode a Parquet file in streaming fashion (without
289/// downloading the whole file at once) from a remote source, such as an object store.
290///
291/// This builder handles reading the parquet file metadata, allowing consumers
292/// to use this information to select what specific columns, row groups, etc.
293/// they wish to be read by the resulting stream.
294///
295/// See examples on [`ParquetRecordBatchStreamBuilder::new`], including how to
296/// issue multiple I/O requests in parallel using multiple streams.
297///
298/// # See also:
299/// * [`ParquetPushDecoderBuilder`] for lower level control over buffering and
300/// decoding.
301/// * [`ParquetRecordBatchStream::next_row_group`] for I/O prefetching
302///
303///
304/// See [`ArrowReaderBuilder`] for additional member functions
305pub type ParquetRecordBatchStreamBuilder<T> = ArrowReaderBuilder<AsyncReader<T>>;
306
307impl<T: AsyncFileReader + Send + 'static> ParquetRecordBatchStreamBuilder<T> {
308 /// Create a new [`ParquetRecordBatchStreamBuilder`] for reading from the
309 /// specified source.
310 ///
311 /// # Examples:
312 /// * [Basic example reading from an async source](#example)
313 /// * [Configuring options and reading metadata](#example-configuring-options-and-reading-metadata)
314 /// * [Reading Row Groups in Parallel](#example-reading-row-groups-in-parallel)
315 ///
316 /// # Example
317 /// ```
318 /// # #[tokio::main(flavor="current_thread")]
319 /// # async fn main() {
320 /// #
321 /// # use arrow_array::RecordBatch;
322 /// # use arrow::util::pretty::pretty_format_batches;
323 /// # use futures::TryStreamExt;
324 /// #
325 /// # use parquet::arrow::{ParquetRecordBatchStreamBuilder, ProjectionMask};
326 /// #
327 /// # fn assert_batches_eq(batches: &[RecordBatch], expected_lines: &[&str]) {
328 /// # let formatted = pretty_format_batches(batches).unwrap().to_string();
329 /// # let actual_lines: Vec<_> = formatted.trim().lines().collect();
330 /// # assert_eq!(
331 /// # &actual_lines, expected_lines,
332 /// # "\n\nexpected:\n\n{:#?}\nactual:\n\n{:#?}\n\n",
333 /// # expected_lines, actual_lines
334 /// # );
335 /// # }
336 /// #
337 /// # let testdata = arrow::util::test_util::parquet_test_data();
338 /// # let path = format!("{}/alltypes_plain.parquet", testdata);
339 /// // Use tokio::fs::File to read data using an async I/O. This can be replaced with
340 /// // another async I/O reader such as a reader from an object store.
341 /// let file = tokio::fs::File::open(path).await.unwrap();
342 ///
343 /// // Configure options for reading from the async source
344 /// let builder = ParquetRecordBatchStreamBuilder::new(file)
345 /// .await
346 /// .unwrap();
347 /// // Building the stream opens the parquet file (reads metadata, etc) and returns
348 /// // a stream that can be used to incrementally read the data in batches
349 /// let stream = builder.build().unwrap();
350 /// // In this example, we collect the stream into a Vec<RecordBatch>
351 /// // but real applications would likely process the batches as they are read
352 /// let results = stream.try_collect::<Vec<_>>().await.unwrap();
353 /// // Demonstrate the results are as expected
354 /// assert_batches_eq(
355 /// &results,
356 /// &[
357 /// "+----+----------+-------------+--------------+---------+------------+-----------+------------+------------------+------------+---------------------+",
358 /// "| id | bool_col | tinyint_col | smallint_col | int_col | bigint_col | float_col | double_col | date_string_col | string_col | timestamp_col |",
359 /// "+----+----------+-------------+--------------+---------+------------+-----------+------------+------------------+------------+---------------------+",
360 /// "| 4 | true | 0 | 0 | 0 | 0 | 0.0 | 0.0 | 30332f30312f3039 | 30 | 2009-03-01T00:00:00 |",
361 /// "| 5 | false | 1 | 1 | 1 | 10 | 1.1 | 10.1 | 30332f30312f3039 | 31 | 2009-03-01T00:01:00 |",
362 /// "| 6 | true | 0 | 0 | 0 | 0 | 0.0 | 0.0 | 30342f30312f3039 | 30 | 2009-04-01T00:00:00 |",
363 /// "| 7 | false | 1 | 1 | 1 | 10 | 1.1 | 10.1 | 30342f30312f3039 | 31 | 2009-04-01T00:01:00 |",
364 /// "| 2 | true | 0 | 0 | 0 | 0 | 0.0 | 0.0 | 30322f30312f3039 | 30 | 2009-02-01T00:00:00 |",
365 /// "| 3 | false | 1 | 1 | 1 | 10 | 1.1 | 10.1 | 30322f30312f3039 | 31 | 2009-02-01T00:01:00 |",
366 /// "| 0 | true | 0 | 0 | 0 | 0 | 0.0 | 0.0 | 30312f30312f3039 | 30 | 2009-01-01T00:00:00 |",
367 /// "| 1 | false | 1 | 1 | 1 | 10 | 1.1 | 10.1 | 30312f30312f3039 | 31 | 2009-01-01T00:01:00 |",
368 /// "+----+----------+-------------+--------------+---------+------------+-----------+------------+------------------+------------+---------------------+",
369 /// ],
370 /// );
371 /// # }
372 /// ```
373 ///
374 /// # Example Configuring Options and Reading Metadata
375 ///
376 /// There are many options that control the behavior of the reader, such as
377 /// `with_batch_size`, `with_projection`, `with_filter`, etc...
378 ///
379 /// ```
380 /// # #[tokio::main(flavor="current_thread")]
381 /// # async fn main() {
382 /// #
383 /// # use arrow_array::RecordBatch;
384 /// # use arrow::util::pretty::pretty_format_batches;
385 /// # use futures::TryStreamExt;
386 /// #
387 /// # use parquet::arrow::{ParquetRecordBatchStreamBuilder, ProjectionMask};
388 /// #
389 /// # fn assert_batches_eq(batches: &[RecordBatch], expected_lines: &[&str]) {
390 /// # let formatted = pretty_format_batches(batches).unwrap().to_string();
391 /// # let actual_lines: Vec<_> = formatted.trim().lines().collect();
392 /// # assert_eq!(
393 /// # &actual_lines, expected_lines,
394 /// # "\n\nexpected:\n\n{:#?}\nactual:\n\n{:#?}\n\n",
395 /// # expected_lines, actual_lines
396 /// # );
397 /// # }
398 /// #
399 /// # let testdata = arrow::util::test_util::parquet_test_data();
400 /// # let path = format!("{}/alltypes_plain.parquet", testdata);
401 /// // As before, use tokio::fs::File to read data using an async I/O.
402 /// let file = tokio::fs::File::open(path).await.unwrap();
403 ///
404 /// // Configure options for reading from the async source, in this case we set the batch size
405 /// // to 3 which produces 3 rows at a time.
406 /// let builder = ParquetRecordBatchStreamBuilder::new(file)
407 /// .await
408 /// .unwrap()
409 /// .with_batch_size(3);
410 ///
411 /// // We can also read the metadata to inspect the schema and other metadata
412 /// // before actually reading the data
413 /// let file_metadata = builder.metadata().file_metadata();
414 /// // Specify that we only want to read the 1st, 2nd, and 6th columns
415 /// let mask = ProjectionMask::roots(file_metadata.schema_descr(), [1, 2, 6]);
416 ///
417 /// let stream = builder.with_projection(mask).build().unwrap();
418 /// let results = stream.try_collect::<Vec<_>>().await.unwrap();
419 /// // Print out the results
420 /// assert_batches_eq(
421 /// &results,
422 /// &[
423 /// "+----------+-------------+-----------+",
424 /// "| bool_col | tinyint_col | float_col |",
425 /// "+----------+-------------+-----------+",
426 /// "| true | 0 | 0.0 |",
427 /// "| false | 1 | 1.1 |",
428 /// "| true | 0 | 0.0 |",
429 /// "| false | 1 | 1.1 |",
430 /// "| true | 0 | 0.0 |",
431 /// "| false | 1 | 1.1 |",
432 /// "| true | 0 | 0.0 |",
433 /// "| false | 1 | 1.1 |",
434 /// "+----------+-------------+-----------+",
435 /// ],
436 /// );
437 ///
438 /// // The results has 8 rows, so since we set the batch size to 3, we expect
439 /// // 3 batches, two with 3 rows each and the last batch with 2 rows.
440 /// assert_eq!(results.len(), 3);
441 /// # }
442 /// ```
443 ///
444 /// # Example reading Row Groups in Parallel
445 ///
446 /// Each [`ParquetRecordBatchStream`] is independent and can be used to read
447 /// from the same underlying source in parallel. Use
448 /// [`ParquetRecordBatchStream::next_row_group`] with a single stream to
449 /// begin prefetching the next Row Group. To read a file in parallel, create
450 /// a stream for each subset of the file. For example, you can read each
451 /// row group in parallel by creating a stream for each row group using the
452 /// [`ParquetRecordBatchStreamBuilder::with_row_groups`] API as shown below
453 ///
454 /// ```
455 /// # use std::sync::Arc;
456 /// # use arrow_array::{ArrayRef, Int32Array, RecordBatch};
457 /// # use arrow::util::pretty::pretty_format_batches;
458 /// # use futures::{StreamExt, TryStreamExt};
459 /// # use tempfile::NamedTempFile;
460 /// # use parquet::arrow::{ArrowWriter, ParquetRecordBatchStreamBuilder, ProjectionMask};
461 /// # use parquet::arrow::arrow_reader::{ArrowReaderMetadata, ArrowReaderOptions};
462 /// # use parquet::file::metadata::ParquetMetaDataReader;
463 /// # use parquet::file::properties::{WriterProperties};
464 /// # // write to a temporary file with 10 RowGroups and read back with async API
465 /// # fn write_file() -> parquet::errors::Result<NamedTempFile> {
466 /// # let mut file = NamedTempFile::new().unwrap();
467 /// # let small_batch = RecordBatch::try_from_iter([
468 /// # ("id", Arc::new(Int32Array::from(vec![0, 1, 2, 3, 4])) as ArrayRef),
469 /// # ]).unwrap();
470 /// # let props = WriterProperties::builder()
471 /// # .set_max_row_group_row_count(Some(5))
472 /// # .set_write_batch_size(5)
473 /// # .build();
474 /// # let mut writer = ArrowWriter::try_new(&mut file, small_batch.schema(), Some(props))?;
475 /// # for i in 0..10 {
476 /// # writer.write(&small_batch)?
477 /// # };
478 /// # writer.close()?;
479 /// # Ok(file)
480 /// # }
481 /// # #[tokio::main(flavor="current_thread")]
482 /// # async fn main() -> parquet::errors::Result<()> {
483 /// # let t = write_file()?;
484 /// # let path = t.path();
485 /// // This example uses a tokio::fs::File as the async source, but it
486 /// // could be any async source such as an object store reader)
487 /// let mut file = tokio::fs::File::open(path).await?;
488 /// // To read Row Groups in parallel, create a separate stream builder for each Row Group.
489 /// // First get the metadata to find the row group information
490 /// let file_size = file.metadata().await?.len();
491 /// let metadata = ParquetMetaDataReader::new().load_and_finish(&mut file, file_size).await?;
492 /// assert_eq!(metadata.num_row_groups(), 10); // file has 10 row groups with 5 rows each
493 /// // Create a stream reader for each row group
494 /// let reader_metadata = ArrowReaderMetadata::try_new(
495 /// Arc::new(metadata),
496 /// ArrowReaderOptions::new()
497 /// )?;
498 /// let mut streams = vec![];
499 /// for row_group_index in 0..10 {
500 /// // Each stream needs its own source instance to issue
501 /// // parallel IO requests, so clone the file for each stream
502 /// let this_file = file.try_clone().await?;
503 /// let stream = ParquetRecordBatchStreamBuilder::new_with_metadata(
504 /// this_file,
505 /// reader_metadata.clone()
506 /// )
507 /// .with_row_groups(vec![row_group_index]) // read only this row group
508 /// .build()?;
509 /// streams.push(stream);
510 /// }
511 /// // Each reader can now be polled independently and in parallel, for
512 /// // example using StreamExt::buffered to read from 3 at a time
513 /// let results = futures::stream::iter(streams)
514 /// .map(|stream| async move { stream })
515 /// .buffered(3)
516 /// .flatten()
517 /// .try_collect::<Vec<_>>().await?;
518 /// // read all 50 rows (10 row groups x 5 rows per group)
519 /// assert_eq!(50, results.iter().map(|s| s.num_rows()).sum::<usize>());
520 /// # Ok(())
521 /// # }
522 /// ```
523 pub async fn new(input: T) -> Result<Self> {
524 Self::new_with_options(input, Default::default()).await
525 }
526
527 /// Create a new [`ParquetRecordBatchStreamBuilder`] with the provided async source
528 /// and [`ArrowReaderOptions`].
529 pub async fn new_with_options(mut input: T, options: ArrowReaderOptions) -> Result<Self> {
530 let metadata = ArrowReaderMetadata::load_async(&mut input, options).await?;
531 Ok(Self::new_with_metadata(input, metadata))
532 }
533
534 /// Create a [`ParquetRecordBatchStreamBuilder`] from the provided [`ArrowReaderMetadata`]
535 ///
536 /// This allows loading metadata once and using it to create multiple builders with
537 /// potentially different settings, that can be read in parallel.
538 ///
539 /// # Example of reading from multiple streams in parallel
540 ///
541 /// ```
542 /// # use std::fs::metadata;
543 /// # use std::sync::Arc;
544 /// # use bytes::Bytes;
545 /// # use arrow_array::{Int32Array, RecordBatch};
546 /// # use arrow_schema::{DataType, Field, Schema};
547 /// # use parquet::arrow::arrow_reader::ArrowReaderMetadata;
548 /// # use parquet::arrow::{ArrowWriter, ParquetRecordBatchStreamBuilder};
549 /// # use tempfile::tempfile;
550 /// # use futures::StreamExt;
551 /// # #[tokio::main(flavor="current_thread")]
552 /// # async fn main() {
553 /// #
554 /// # let mut file = tempfile().unwrap();
555 /// # let schema = Arc::new(Schema::new(vec![Field::new("i32", DataType::Int32, false)]));
556 /// # let mut writer = ArrowWriter::try_new(&mut file, schema.clone(), None).unwrap();
557 /// # let batch = RecordBatch::try_new(schema, vec![Arc::new(Int32Array::from(vec![1, 2, 3]))]).unwrap();
558 /// # writer.write(&batch).unwrap();
559 /// # writer.close().unwrap();
560 /// // open file with parquet data
561 /// let mut file = tokio::fs::File::from_std(file);
562 /// // load metadata once
563 /// let meta = ArrowReaderMetadata::load_async(&mut file, Default::default()).await.unwrap();
564 /// // create two readers, a and b, from the same underlying file
565 /// // without reading the metadata again
566 /// let mut a = ParquetRecordBatchStreamBuilder::new_with_metadata(
567 /// file.try_clone().await.unwrap(),
568 /// meta.clone()
569 /// ).build().unwrap();
570 /// let mut b = ParquetRecordBatchStreamBuilder::new_with_metadata(file, meta).build().unwrap();
571 ///
572 /// // Can read batches from both readers in parallel
573 /// assert_eq!(
574 /// a.next().await.unwrap().unwrap(),
575 /// b.next().await.unwrap().unwrap(),
576 /// );
577 /// # }
578 /// ```
579 pub fn new_with_metadata(input: T, metadata: ArrowReaderMetadata) -> Self {
580 Self::new_builder(AsyncReader(input), metadata)
581 }
582
583 /// Read bloom filter for a column in a row group
584 ///
585 /// Returns `None` if the column does not have a bloom filter
586 ///
587 /// We should call this function after other forms pruning, such as projection and predicate pushdown.
588 pub async fn get_row_group_column_bloom_filter(
589 &mut self,
590 row_group_idx: usize,
591 column_idx: usize,
592 ) -> Result<Option<Sbbf>> {
593 let metadata = self.metadata.row_group(row_group_idx);
594 let column_metadata = metadata.column(column_idx);
595
596 let offset: u64 = if let Some(offset) = column_metadata.bloom_filter_offset() {
597 offset
598 .try_into()
599 .map_err(|_| ParquetError::General("Bloom filter offset is invalid".to_string()))?
600 } else {
601 return Ok(None);
602 };
603
604 let buffer = match column_metadata.bloom_filter_length() {
605 Some(length) => self.input.0.get_bytes(offset..offset + length as u64),
606 None => self
607 .input
608 .0
609 .get_bytes(offset..offset + SBBF_HEADER_SIZE_ESTIMATE as u64),
610 }
611 .await?;
612
613 let (header, bitset_offset) =
614 chunk_read_bloom_filter_header_and_offset(offset, buffer.clone())?;
615
616 match header.algorithm {
617 BloomFilterAlgorithm::BLOCK => {
618 // this match exists to future proof the singleton algorithm enum
619 }
620 }
621 match header.compression {
622 BloomFilterCompression::UNCOMPRESSED => {
623 // this match exists to future proof the singleton compression enum
624 }
625 }
626 match header.hash {
627 BloomFilterHash::XXHASH => {
628 // this match exists to future proof the singleton hash enum
629 }
630 }
631
632 let bitset = match column_metadata.bloom_filter_length() {
633 Some(_) => {
634 let bitset_start = bitset_offset
635 .checked_sub(offset)
636 .and_then(|start| usize::try_from(start).ok())
637 .ok_or_else(|| {
638 ParquetError::General("Bloom filter offset is invalid".to_string())
639 })?;
640 buffer.slice(bitset_start..)
641 }
642 None => {
643 let bitset_length: u64 = header.num_bytes.try_into().map_err(|_| {
644 ParquetError::General("Bloom filter length is invalid".to_string())
645 })?;
646 self.input
647 .0
648 .get_bytes(bitset_offset..bitset_offset + bitset_length)
649 .await?
650 }
651 };
652 Ok(Some(Sbbf::new(&bitset)))
653 }
654
655 /// Select row groups and rows using row-group-local coordinates.
656 ///
657 /// Entries are decoded in the supplied order, omitted row groups are
658 /// skipped, and a `None` selection reads the whole row group. This is
659 /// mutually exclusive with [`ArrowReaderBuilder::with_row_groups`] and
660 /// [`ArrowReaderBuilder::with_row_selection`]; combining them returns an
661 /// error from [`Self::build`].
662 ///
663 /// See [`ParquetPushDecoderBuilder::with_row_group_selections`] for the
664 /// full semantics and a worked example. This builder supports the same API
665 /// because the async stream is implemented using the push decoder; the
666 /// synchronous reader does not support row-group-local selections.
667 ///
668 /// [`ParquetPushDecoderBuilder::with_row_group_selections`]: crate::arrow::push_decoder::ParquetPushDecoderBuilder::with_row_group_selections
669 pub fn with_row_group_selections(
670 mut self,
671 row_group_selections: Vec<RowGroupSelection>,
672 ) -> Self {
673 self.row_group_plan
674 .set_row_group_selections(row_group_selections);
675 self
676 }
677
678 /// Read and decode the dictionary page for a column in a row group, if any.
679 ///
680 /// Returns `Ok(None)` if the column chunk has no dictionary page, or if
681 /// its physical type is not `BYTE_ARRAY` (the only physical type
682 /// currently supported).
683 ///
684 /// The returned array contains raw `Binary` values, even for columns
685 /// annotated as strings. Callers can compare byte slices directly or
686 /// convert values to UTF-8 explicitly.
687 ///
688 /// This can be used to inspect dictionary values when selecting or pruning
689 /// row groups before passing the selected indices to
690 /// [`ParquetRecordBatchStreamBuilder::with_row_groups`].
691 ///
692 /// Note this does not verify that the *entire* column chunk is
693 /// dictionary-encoded -- callers that need that guarantee (e.g. to treat
694 /// the dictionary as an exhaustive set of the column's values) should
695 /// check
696 /// [`crate::file::metadata::ColumnChunkMetaData::page_encoding_stats_mask`].
697 pub async fn get_column_chunk_dictionary(
698 &mut self,
699 row_group_idx: usize,
700 column_idx: usize,
701 ) -> Result<Option<ArrayRef>> {
702 ParquetMetaDataReader::read_column_dictionary_async(
703 &mut self.input.0,
704 &self.metadata,
705 row_group_idx,
706 column_idx,
707 )
708 .await
709 }
710
711 /// Build a new [`ParquetRecordBatchStream`]
712 ///
713 /// See examples on [`ParquetRecordBatchStreamBuilder::new`]
714 pub fn build(self) -> Result<ParquetRecordBatchStream<T>> {
715 let Self {
716 input,
717 metadata,
718 schema,
719 fields,
720 batch_size,
721 row_group_plan,
722 projection,
723 filter,
724 row_selection_policy: selection_strategy,
725 limit,
726 offset,
727 metrics,
728 max_predicate_cache_size,
729 } = self;
730
731 // Ensure schema of ParquetRecordBatchStream respects projection, and does
732 // not store metadata (same as for ParquetRecordBatchReader and emitted RecordBatches)
733 let projection_len = projection.mask.as_ref().map_or(usize::MAX, |m| m.len());
734 let projected_fields = schema
735 .fields
736 .filter_leaves(|idx, _| idx < projection_len && projection.leaf_included(idx));
737 let projected_schema = Arc::new(Schema::new(projected_fields));
738
739 let decoder = ParquetPushDecoderBuilder {
740 input: PushDecoderInput::default(),
741 metadata,
742 schema,
743 fields,
744 projection,
745 filter,
746 row_group_plan,
747 row_selection_policy: selection_strategy,
748 batch_size,
749 limit,
750 offset,
751 metrics,
752 max_predicate_cache_size,
753 }
754 .build()?;
755
756 let request_state = RequestState::None { input: input.0 };
757
758 Ok(ParquetRecordBatchStream {
759 schema: projected_schema,
760 decoder,
761 request_state,
762 })
763 }
764}
765
766/// State machine that tracks outstanding requests to fetch data
767///
768/// The parameter `T` is the input, typically an `AsyncFileReader`
769enum RequestState<T> {
770 /// No outstanding requests
771 None {
772 input: T,
773 },
774 /// There is an outstanding request for data
775 Outstanding {
776 /// Ranges that have been requested
777 ranges: Vec<Range<u64>>,
778 /// Future that will resolve (input, requested_ranges)
779 ///
780 /// Note the future owns the reader while the request is outstanding
781 /// and returns it upon completion
782 future: BoxFuture<'static, Result<(T, Vec<Bytes>)>>,
783 },
784 Done,
785}
786
787impl<T> RequestState<T>
788where
789 T: AsyncFileReader + Unpin + Send + 'static,
790{
791 /// Issue a request to fetch `ranges`, returning the Outstanding state
792 fn begin_request(mut input: T, ranges: Vec<Range<u64>>) -> Self {
793 let ranges_captured = ranges.clone();
794
795 // Note this must move the input *into* the future
796 // because the get_byte_ranges future has a lifetime
797 // (aka can have references internally) and thus must
798 // own the input while the request is outstanding.
799 let future = async move {
800 let data = input.get_byte_ranges(ranges_captured).await?;
801 Ok((input, data))
802 }
803 .boxed();
804 RequestState::Outstanding { ranges, future }
805 }
806}
807
808impl<T> std::fmt::Debug for RequestState<T> {
809 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
810 match self {
811 RequestState::None { input: _ } => f
812 .debug_struct("RequestState::None")
813 .field("input", &"...")
814 .finish(),
815 RequestState::Outstanding { ranges, .. } => f
816 .debug_struct("RequestState::Outstanding")
817 .field("ranges", &ranges)
818 .finish(),
819 RequestState::Done => {
820 write!(f, "RequestState::Done")
821 }
822 }
823 }
824}
825
826/// An asynchronous [`Stream`]of [`RecordBatch`] constructed using [`ParquetRecordBatchStreamBuilder`] to read parquet files.
827///
828/// `ParquetRecordBatchStream` also provides [`ParquetRecordBatchStream::next_row_group`] for fetching row groups,
829/// allowing users to decode record batches separately from I/O.
830///
831/// # I/O Buffering
832///
833/// `ParquetRecordBatchStream` buffers *all* data pages selected after predicates
834/// (projection + filtering, etc) and decodes the rows from those buffered pages.
835///
836/// For example, if all rows and columns are selected, the entire row group is
837/// buffered in memory during decode. This minimizes the number of IO operations
838/// required, which is especially important for object stores, where IO operations
839/// have latencies in the hundreds of milliseconds
840///
841/// See [`ParquetPushDecoderBuilder`] for an API with lower level control over
842/// buffering.
843///
844/// [`Stream`]: https://docs.rs/futures/latest/futures/stream/trait.Stream.html
845pub struct ParquetRecordBatchStream<T> {
846 /// Output schema of the stream
847 schema: SchemaRef,
848 /// Input and Outstanding IO request, if any
849 request_state: RequestState<T>,
850 /// Decoding state machine (no IO)
851 decoder: ParquetPushDecoder,
852}
853
854impl<T> std::fmt::Debug for ParquetRecordBatchStream<T> {
855 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
856 f.debug_struct("ParquetRecordBatchStream")
857 .field("request_state", &self.request_state)
858 .finish()
859 }
860}
861
862impl<T> ParquetRecordBatchStream<T> {
863 /// Returns the projected [`SchemaRef`] for reading the parquet file.
864 ///
865 /// Note that the schema metadata will be stripped here. See
866 /// [`ParquetRecordBatchStreamBuilder::schema`] if the metadata is desired.
867 pub fn schema(&self) -> &SchemaRef {
868 &self.schema
869 }
870}
871
872impl<T> ParquetRecordBatchStream<T>
873where
874 T: AsyncFileReader + Unpin + Send + 'static,
875{
876 /// Fetches the next row group from the stream.
877 ///
878 /// Users can continue to call this function to get row groups and decode them concurrently.
879 ///
880 /// ## Notes
881 ///
882 /// ParquetRecordBatchStream should be used either as a `Stream` or with `next_row_group`; they should not be used simultaneously.
883 ///
884 /// ## Returns
885 ///
886 /// - `Ok(None)` if the stream has ended.
887 /// - `Err(error)` if the stream has errored. All subsequent calls will return `Ok(None)`.
888 /// - `Ok(Some(reader))` which holds all the data for the row group.
889 pub async fn next_row_group(&mut self) -> Result<Option<ParquetRecordBatchReader>> {
890 loop {
891 // Take ownership of request state to process, leaving self in a
892 // valid state
893 let request_state = std::mem::replace(&mut self.request_state, RequestState::Done);
894 match request_state {
895 // No outstanding requests, proceed to setup next row group
896 RequestState::None { input } => {
897 match self.decoder.try_next_reader()? {
898 DecodeResult::NeedsData(ranges) => {
899 self.request_state = RequestState::begin_request(input, ranges);
900 // Will loop again: the input might be ready immediately.
901 }
902 DecodeResult::Data(reader) => {
903 self.request_state = RequestState::None { input };
904 return Ok(Some(reader));
905 }
906 DecodeResult::Finished => return Ok(None),
907 }
908 }
909 RequestState::Outstanding { ranges, future } => {
910 let (input, data) = future.await?;
911 // Push the requested data to the decoder and try again
912 self.decoder.push_ranges(ranges, data)?;
913 self.request_state = RequestState::None { input };
914 // Will try and decode on the next iteration.
915 }
916 RequestState::Done => {
917 self.request_state = RequestState::Done;
918 return Ok(None);
919 }
920 }
921 }
922 }
923}
924
925impl<T> Stream for ParquetRecordBatchStream<T>
926where
927 T: AsyncFileReader + Unpin + Send + 'static,
928{
929 type Item = Result<RecordBatch>;
930 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
931 match self.poll_next_inner(cx) {
932 Ok(res) => {
933 // Successfully decoded a batch, or reached end of stream.
934 // convert Option<RecordBatch> to Option<Result<RecordBatch>>
935 res.map(|res| Ok(res).transpose())
936 }
937 Err(e) => {
938 self.request_state = RequestState::Done;
939 Poll::Ready(Some(Err(e)))
940 }
941 }
942 }
943}
944
945impl<T> ParquetRecordBatchStream<T>
946where
947 T: AsyncFileReader + Unpin + Send + 'static,
948{
949 /// Inner state machine
950 ///
951 /// Note this is separate from poll_next so we can use ? operator to check for errors
952 /// as it returns `Result<Poll<Option<RecordBatch>>>`
953 fn poll_next_inner(&mut self, cx: &mut Context<'_>) -> Result<Poll<Option<RecordBatch>>> {
954 loop {
955 let request_state = std::mem::replace(&mut self.request_state, RequestState::Done);
956 match request_state {
957 RequestState::None { input } => {
958 // No outstanding requests, proceed to decode the next batch
959 match self.decoder.try_decode()? {
960 DecodeResult::NeedsData(ranges) => {
961 self.request_state = RequestState::begin_request(input, ranges);
962 // Will loop again: the input might be ready immediately.
963 }
964 DecodeResult::Data(batch) => {
965 self.request_state = RequestState::None { input };
966 return Ok(Poll::Ready(Some(batch)));
967 }
968 DecodeResult::Finished => {
969 self.request_state = RequestState::Done;
970 return Ok(Poll::Ready(None));
971 }
972 }
973 }
974 RequestState::Outstanding { ranges, mut future } => match future.poll_unpin(cx) {
975 // Data was ready, push it to the decoder and continue
976 Poll::Ready(result) => {
977 let (input, data) = result?;
978 // Push the requested data to the decoder
979 self.decoder.push_ranges(ranges, data)?;
980 self.request_state = RequestState::None { input };
981 // The next iteration will try to decode the next batch.
982 }
983 Poll::Pending => {
984 self.request_state = RequestState::Outstanding { ranges, future };
985 return Ok(Poll::Pending);
986 }
987 },
988 RequestState::Done => {
989 // Stream is done (error or end), return None
990 self.request_state = RequestState::Done;
991 return Ok(Poll::Ready(None));
992 }
993 }
994 }
995 }
996}
997
998#[cfg(test)]
999mod tests {
1000 use super::*;
1001 use crate::arrow::ArrowWriter;
1002 use crate::arrow::ProjectionMask;
1003 use crate::arrow::arrow_reader::tests::test_row_numbers_with_multiple_row_groups_helper;
1004 use crate::arrow::arrow_reader::{ArrowReaderMetadata, ArrowReaderOptions};
1005 use crate::arrow::schema::virtual_type::RowNumber;
1006 use crate::basic::Encoding;
1007 use crate::file::metadata::PageIndexPolicy;
1008 use crate::file::metadata::ParquetMetaDataReader;
1009 use crate::file::metadata::page_index::PageIndex;
1010 use crate::file::properties::WriterProperties;
1011 use arrow_array::cast::AsArray;
1012 use arrow_array::{Array, ArrayRef, BinaryArray, StringArray};
1013 use arrow_schema::{DataType, Field, Schema};
1014 use futures::TryStreamExt;
1015 use std::sync::{Arc, Mutex};
1016
1017 #[derive(Clone)]
1018 struct TestReader {
1019 data: Bytes,
1020 metadata: Option<Arc<ParquetMetaData>>,
1021 requests: Arc<Mutex<Vec<Range<usize>>>>,
1022 }
1023
1024 impl TestReader {
1025 fn new(data: Bytes) -> Self {
1026 Self {
1027 data,
1028 metadata: Default::default(),
1029 requests: Default::default(),
1030 }
1031 }
1032 }
1033
1034 impl AsyncFileReader for TestReader {
1035 fn get_bytes(&mut self, range: Range<u64>) -> BoxFuture<'_, Result<Bytes>> {
1036 let range = range.clone();
1037 self.requests
1038 .lock()
1039 .unwrap()
1040 .push(range.start as usize..range.end as usize);
1041 futures::future::ready(Ok(self
1042 .data
1043 .slice(range.start as usize..range.end as usize)))
1044 .boxed()
1045 }
1046
1047 fn get_metadata<'a>(
1048 &'a mut self,
1049 options: Option<&'a ArrowReaderOptions>,
1050 ) -> BoxFuture<'a, Result<Arc<ParquetMetaData>>> {
1051 let metadata_reader = ParquetMetaDataReader::new().with_arrow_reader_options(options);
1052 self.metadata = Some(Arc::new(
1053 metadata_reader.parse_and_finish(&self.data).unwrap(),
1054 ));
1055 futures::future::ready(Ok(self.metadata.clone().unwrap().clone())).boxed()
1056 }
1057 }
1058
1059 #[tokio::test]
1060 async fn test_get_column_chunk_dictionary() {
1061 let schema = Arc::new(Schema::new(vec![Field::new("s", DataType::Utf8, false)]));
1062 let values: Vec<&str> = ["alpha", "beta", "gamma"]
1063 .iter()
1064 .copied()
1065 .cycle()
1066 .take(30)
1067 .collect();
1068 let array: ArrayRef = Arc::new(StringArray::from(values));
1069 let batch = RecordBatch::try_new(schema.clone(), vec![array]).unwrap();
1070
1071 let props = WriterProperties::builder()
1072 .set_dictionary_enabled(true)
1073 .build();
1074 let mut buf = Vec::new();
1075 {
1076 let mut writer = ArrowWriter::try_new(&mut buf, schema, Some(props)).unwrap();
1077 writer.write(&batch).unwrap();
1078 writer.close().unwrap();
1079 }
1080 let data = Bytes::from(buf);
1081
1082 let direct_metadata = ParquetMetaDataReader::new()
1083 .parse_and_finish(&data)
1084 .unwrap();
1085 let mut direct_reader = TestReader::new(data.clone());
1086 let direct = ParquetMetaDataReader::read_column_dictionary_async(
1087 &mut direct_reader,
1088 &direct_metadata,
1089 0,
1090 0,
1091 )
1092 .await
1093 .unwrap()
1094 .unwrap();
1095 let direct = direct.as_any().downcast_ref::<BinaryArray>().unwrap();
1096 assert_eq!(direct.value(0), b"alpha");
1097
1098 let async_reader = TestReader::new(data);
1099 let mut builder = ParquetRecordBatchStreamBuilder::new(async_reader)
1100 .await
1101 .unwrap();
1102
1103 let dictionary = builder
1104 .get_column_chunk_dictionary(0, 0)
1105 .await
1106 .unwrap()
1107 .unwrap();
1108 let dictionary = dictionary.as_any().downcast_ref::<BinaryArray>().unwrap();
1109 let dictionary_values: Vec<&[u8]> = dictionary.iter().map(|v| v.unwrap()).collect();
1110 assert_eq!(
1111 dictionary_values,
1112 vec![b"alpha".as_slice(), b"beta", b"gamma"]
1113 );
1114 }
1115
1116 // This test demonstrates row group pruning using dictionary pages,
1117 // verifying that data pages of skipped row groups are not read
1118 #[tokio::test]
1119 async fn test_dictionary_selects_row_groups_without_reading_skipped_data() {
1120 // Write two row groups, each dictionary-encoded and containing a single
1121 // distinct string repeated 30 times: row group 0 is all "skip", row
1122 // group 1 is all "target".
1123 let schema = Arc::new(Schema::new(vec![Field::new("s", DataType::Utf8, false)]));
1124 let row_group_values = ["skip", "target"];
1125 let props = WriterProperties::builder()
1126 .set_dictionary_enabled(true)
1127 .build();
1128 let mut buf = Vec::new();
1129 {
1130 let mut writer = ArrowWriter::try_new(&mut buf, schema.clone(), Some(props)).unwrap();
1131 for value in row_group_values {
1132 let array: ArrayRef = Arc::new(StringArray::from(vec![value; 30]));
1133 let batch = RecordBatch::try_new(schema.clone(), vec![array]).unwrap();
1134 writer.write(&batch).unwrap();
1135 writer.flush().unwrap();
1136 }
1137 writer.close().unwrap();
1138 }
1139
1140 let async_reader = TestReader::new(Bytes::from(buf));
1141 // `requests` records the byte ranges fetched from the underlying reader,
1142 // which we later use to check that we read only the needed page
1143 let requests = async_reader.requests.clone();
1144 let mut builder = ParquetRecordBatchStreamBuilder::new(async_reader)
1145 .await
1146 .unwrap();
1147 let metadata = builder.metadata().clone();
1148 assert_eq!(metadata.num_row_groups(), 2);
1149
1150 // For each row group, fetch and decode just the dictionary page
1151 // to decide whether to read the whole row group
1152 let mut selected_row_groups = Vec::new();
1153 let mut dictionary_ranges = Vec::new();
1154 for row_group_idx in 0..metadata.num_row_groups() {
1155 let column = metadata.row_group(row_group_idx).column(0);
1156 let encoding_mask = column.page_encoding_stats_mask().unwrap();
1157 assert!(
1158 encoding_mask.is_only(Encoding::PLAIN_DICTIONARY)
1159 || encoding_mask.is_only(Encoding::RLE_DICTIONARY)
1160 );
1161
1162 let dictionary_start = column.dictionary_page_offset().unwrap() as usize;
1163 let data_start = column.data_page_offset() as usize;
1164 dictionary_ranges.push(dictionary_start..data_start);
1165
1166 let dictionary = builder
1167 .get_column_chunk_dictionary(row_group_idx, 0)
1168 .await
1169 .unwrap()
1170 .unwrap();
1171 let dictionary = dictionary.as_binary::<i32>();
1172 if dictionary
1173 .iter()
1174 .any(|value| value == Some(b"target".as_slice()))
1175 {
1176 selected_row_groups.push(row_group_idx);
1177 }
1178 }
1179 assert_eq!(selected_row_groups, vec![1]);
1180
1181 // Read the filtered row group and verify the values
1182 let batches: Vec<_> = builder
1183 .with_row_groups(selected_row_groups)
1184 .build()
1185 .unwrap()
1186 .try_collect()
1187 .await
1188 .unwrap();
1189 let values: Vec<_> = batches
1190 .iter()
1191 .flat_map(|batch| batch.column(0).as_string::<i32>().iter())
1192 .collect();
1193 assert_eq!(values, vec![Some("target"); 30]);
1194
1195 // Finally, verify none of the requests overlapped the data pages
1196 // of the skipped row group
1197 let skipped_column = metadata.row_group(0).column(0);
1198 let (skipped_start, skipped_len) = skipped_column.byte_range();
1199 let skipped_data_range =
1200 skipped_column.data_page_offset() as usize..(skipped_start + skipped_len) as usize;
1201 let requests = requests.lock().unwrap();
1202 for dictionary_range in dictionary_ranges {
1203 assert!(requests.contains(&dictionary_range));
1204 }
1205 assert!(requests.iter().all(|request| {
1206 request.end <= skipped_data_range.start || request.start >= skipped_data_range.end
1207 }));
1208 }
1209
1210 #[tokio::test]
1211 async fn test_batch_size_overallocate() {
1212 let testdata = arrow::util::test_util::parquet_test_data();
1213 // `alltypes_plain.parquet` only have 8 rows
1214 let path = format!("{testdata}/alltypes_plain.parquet");
1215 let data = Bytes::from(std::fs::read(path).unwrap());
1216
1217 let async_reader = TestReader::new(data.clone());
1218
1219 let builder = ParquetRecordBatchStreamBuilder::new(async_reader)
1220 .await
1221 .unwrap();
1222
1223 let file_rows = builder.metadata().file_metadata().num_rows() as usize;
1224
1225 let builder = builder
1226 .with_projection(ProjectionMask::all())
1227 .with_batch_size(1024);
1228
1229 // even though the batch size is set to 1024, it should adjust to the max
1230 // number of rows in the file (8)
1231 assert_ne!(1024, file_rows);
1232 assert_eq!(builder.batch_size, file_rows);
1233
1234 let _stream = builder.build().unwrap();
1235 }
1236
1237 #[tokio::test]
1238 async fn empty_offset_index_doesnt_panic_in_read_row_group() {
1239 use tokio::fs::File;
1240 let testdata = arrow::util::test_util::parquet_test_data();
1241 let path = format!("{testdata}/alltypes_plain.parquet");
1242 let mut file = File::open(&path).await.unwrap();
1243 let file_size = file.metadata().await.unwrap().len();
1244 let mut metadata = ParquetMetaDataReader::new()
1245 .with_page_index_policy(PageIndexPolicy::Required)
1246 .load_and_finish(&mut file, file_size)
1247 .await
1248 .unwrap();
1249
1250 let page_index = PageIndex::new(None, Some(vec![]));
1251 metadata.set_page_index(Some(Arc::new(page_index)));
1252 let options = ArrowReaderOptions::new().with_page_index_policy(PageIndexPolicy::Required);
1253 let arrow_reader_metadata = ArrowReaderMetadata::try_new(metadata.into(), options).unwrap();
1254 let reader =
1255 ParquetRecordBatchStreamBuilder::new_with_metadata(file, arrow_reader_metadata)
1256 .build()
1257 .unwrap();
1258
1259 let result = reader.try_collect::<Vec<_>>().await.unwrap();
1260 assert_eq!(result.len(), 1);
1261 }
1262
1263 #[test]
1264 fn test_row_numbers_with_multiple_row_groups() {
1265 test_row_numbers_with_multiple_row_groups_helper(
1266 false,
1267 |path, selection, _row_filter, batch_size| {
1268 let runtime = tokio::runtime::Builder::new_current_thread()
1269 .enable_all()
1270 .build()
1271 .expect("Could not create runtime");
1272 runtime.block_on(async move {
1273 let file = tokio::fs::File::open(path).await.unwrap();
1274 let row_number_field = Arc::new(
1275 Field::new("row_number", DataType::Int64, false)
1276 .with_extension_type(RowNumber),
1277 );
1278 let options = ArrowReaderOptions::new()
1279 .with_virtual_columns(vec![row_number_field])
1280 .unwrap();
1281 let reader = ParquetRecordBatchStreamBuilder::new_with_options(file, options)
1282 .await
1283 .unwrap()
1284 .with_row_selection(selection)
1285 .with_batch_size(batch_size)
1286 .build()
1287 .expect("Could not create reader");
1288 reader.try_collect::<Vec<_>>().await.unwrap()
1289 })
1290 },
1291 );
1292 }
1293
1294 #[test]
1295 fn test_row_numbers_with_multiple_row_groups_and_filter() {
1296 test_row_numbers_with_multiple_row_groups_helper(
1297 true,
1298 |path, selection, row_filter, batch_size| {
1299 let runtime = tokio::runtime::Builder::new_current_thread()
1300 .enable_all()
1301 .build()
1302 .expect("Could not create runtime");
1303 runtime.block_on(async move {
1304 let file = tokio::fs::File::open(path).await.unwrap();
1305 let row_number_field = Arc::new(
1306 Field::new("row_number", DataType::Int64, false)
1307 .with_extension_type(RowNumber),
1308 );
1309 let options = ArrowReaderOptions::new()
1310 .with_virtual_columns(vec![row_number_field])
1311 .unwrap();
1312 let reader = ParquetRecordBatchStreamBuilder::new_with_options(file, options)
1313 .await
1314 .unwrap()
1315 .with_row_selection(selection)
1316 .with_row_filter(row_filter.expect("No row filter"))
1317 .with_batch_size(batch_size)
1318 .build()
1319 .expect("Could not create reader");
1320 reader.try_collect::<Vec<_>>().await.unwrap()
1321 })
1322 },
1323 );
1324 }
1325}