parquet/arrow/async_reader/
store.rs1use std::{ops::Range, sync::Arc};
19
20use crate::arrow::arrow_reader::ArrowReaderOptions;
21use crate::arrow::async_reader::{AsyncFileReader, MetadataSuffixFetch};
22use crate::errors::{ParquetError, Result};
23use crate::file::metadata::{PageIndexPolicy, ParquetMetaData, ParquetMetaDataReader};
24use bytes::Bytes;
25use futures::{FutureExt, TryFutureExt, future::BoxFuture};
26use object_store::ObjectStoreExt;
27use object_store::{GetOptions, GetRange};
28use object_store::{ObjectStore, path::Path};
29use tokio::runtime::Handle;
30#[deprecated(
55 since = "59.2.0",
56 note = "Implement `AsyncFileReader` directly instead; see the example on the `AsyncFileReader` trait documentation and `parquet/examples/object_store.rs`. Use `SpawnedReader` to perform I/O on a dedicated runtime. See https://github.com/apache/arrow-rs/issues/10308"
57)]
58#[derive(Clone, Debug)]
59pub struct ParquetObjectReader {
60 store: Arc<dyn ObjectStore>,
61 path: Path,
62 file_size: Option<u64>,
63 metadata_size_hint: Option<usize>,
64 preload_column_index: bool,
65 preload_offset_index: bool,
66 runtime: Option<Handle>,
67}
68
69#[expect(deprecated)]
70impl ParquetObjectReader {
71 pub fn new(store: Arc<dyn ObjectStore>, path: Path) -> Self {
73 Self {
74 store,
75 path,
76 file_size: None,
77 metadata_size_hint: None,
78 preload_column_index: false,
79 preload_offset_index: false,
80 runtime: None,
81 }
82 }
83
84 pub fn with_footer_size_hint(self, hint: usize) -> Self {
87 Self {
88 metadata_size_hint: Some(hint),
89 ..self
90 }
91 }
92
93 pub fn with_file_size(self, file_size: u64) -> Self {
103 Self {
104 file_size: Some(file_size),
105 ..self
106 }
107 }
108
109 pub fn with_preload_column_index(self, preload_column_index: bool) -> Self {
115 Self {
116 preload_column_index,
117 ..self
118 }
119 }
120
121 pub fn with_preload_offset_index(self, preload_offset_index: bool) -> Self {
127 Self {
128 preload_offset_index,
129 ..self
130 }
131 }
132
133 #[deprecated(
142 since = "59.2.0",
143 note = "Wrap the reader in a `SpawnedReader` instead, e.g. `SpawnedReader::new(reader, handle)`. See https://github.com/apache/arrow-rs/issues/10308"
144 )]
145 pub fn with_runtime(self, handle: Handle) -> Self {
146 Self {
147 runtime: Some(handle),
148 ..self
149 }
150 }
151
152 fn spawn<F, O, E>(&self, f: F) -> BoxFuture<'_, Result<O>>
153 where
154 F: for<'a> FnOnce(&'a Arc<dyn ObjectStore>, &'a Path) -> BoxFuture<'a, Result<O, E>>
155 + Send
156 + 'static,
157 O: Send + 'static,
158 E: Into<ParquetError> + Send + 'static,
159 {
160 match &self.runtime {
161 Some(handle) => {
162 let path = self.path.clone();
163 let store = Arc::clone(&self.store);
164 handle
165 .spawn(async move { f(&store, &path).await })
166 .map_ok_or_else(
167 |e| match e.try_into_panic() {
168 Err(e) => Err(ParquetError::External(Box::new(e))),
169 Ok(p) => std::panic::resume_unwind(p),
170 },
171 |res| res.map_err(|e| e.into()),
172 )
173 .boxed()
174 }
175 None => f(&self.store, &self.path).map_err(|e| e.into()).boxed(),
176 }
177 }
178}
179
180#[expect(deprecated)]
181impl MetadataSuffixFetch for &mut ParquetObjectReader {
182 fn fetch_suffix(&mut self, suffix: usize) -> BoxFuture<'_, Result<Bytes>> {
183 let options = GetOptions {
184 range: Some(GetRange::Suffix(suffix as u64)),
185 ..Default::default()
186 };
187 self.spawn(|store, path| {
188 async move {
189 let resp = store.get_opts(path, options).await?;
190 Ok::<_, ParquetError>(resp.bytes().await?)
191 }
192 .boxed()
193 })
194 }
195}
196
197#[expect(deprecated)]
198impl AsyncFileReader for ParquetObjectReader {
199 fn get_bytes(&mut self, range: Range<u64>) -> BoxFuture<'_, Result<Bytes>> {
200 self.spawn(|store, path| store.get_range(path, range).boxed())
201 }
202
203 fn get_byte_ranges(&mut self, ranges: Vec<Range<u64>>) -> BoxFuture<'_, Result<Vec<Bytes>>>
204 where
205 Self: Send,
206 {
207 self.spawn(|store, path| async move { store.get_ranges(path, &ranges).await }.boxed())
208 }
209
210 fn get_metadata<'a>(
217 &'a mut self,
218 options: Option<&'a ArrowReaderOptions>,
219 ) -> BoxFuture<'a, Result<Arc<ParquetMetaData>>> {
220 Box::pin(async move {
221 let metadata_opts = options.map(|o| o.metadata_options().clone());
222 let mut metadata = ParquetMetaDataReader::new()
223 .with_metadata_options(metadata_opts)
224 .with_column_index_policy(PageIndexPolicy::from(self.preload_column_index))
225 .with_offset_index_policy(PageIndexPolicy::from(self.preload_offset_index))
226 .with_prefetch_hint(self.metadata_size_hint);
227
228 #[cfg(feature = "encryption")]
229 if let Some(options) = options {
230 metadata = metadata.with_decryption_properties(
231 options.file_decryption_properties.as_ref().map(Arc::clone),
232 );
233 }
234
235 if let Some(options) = options
240 && (options.column_index_policy() != PageIndexPolicy::Skip
241 || options.offset_index_policy() != PageIndexPolicy::Skip)
242 {
243 metadata = metadata
244 .with_column_index_policy(options.column_index_policy())
245 .with_offset_index_policy(options.offset_index_policy());
246 }
247 if let Some(options) = options {
250 if !options.column_index_mask().is_all() {
251 metadata = metadata.with_column_index_mask(options.column_index_mask().clone());
252 }
253 if !options.offset_index_mask().is_all() {
254 metadata = metadata.with_offset_index_mask(options.offset_index_mask().clone());
255 }
256 }
257
258 let metadata = if let Some(file_size) = self.file_size {
259 metadata.load_and_finish(self, file_size).await?
260 } else {
261 metadata.load_via_suffix_and_finish(self).await?
262 };
263
264 Ok(Arc::new(metadata))
265 })
266 }
267}
268
269#[cfg(test)]
270#[expect(deprecated)]
271mod tests {
272 use crate::arrow::async_reader::ArrowReaderOptions;
273 use crate::file::metadata::PageIndexPolicy;
274 use std::sync::{
275 Arc,
276 atomic::{AtomicUsize, Ordering},
277 };
278
279 use futures::TryStreamExt;
280
281 use crate::arrow::ParquetRecordBatchStreamBuilder;
282 use crate::arrow::async_reader::{AsyncFileReader, ParquetObjectReader};
283 use crate::errors::ParquetError;
284 use arrow::util::test_util::parquet_test_data;
285 use futures::FutureExt;
286 use object_store::local::LocalFileSystem;
287 use object_store::path::Path;
288 use object_store::{ObjectMeta, ObjectStore, ObjectStoreExt};
289
290 async fn get_meta_store() -> (ObjectMeta, Arc<dyn ObjectStore>) {
291 let res = parquet_test_data();
292 let store = LocalFileSystem::new_with_prefix(res).unwrap();
293
294 let meta = store
295 .head(&Path::from("alltypes_plain.parquet"))
296 .await
297 .unwrap();
298
299 (meta, Arc::new(store) as Arc<dyn ObjectStore>)
300 }
301
302 async fn get_meta_store_with_page_index() -> (ObjectMeta, Arc<dyn ObjectStore>) {
303 let res = parquet_test_data();
304 let store = LocalFileSystem::new_with_prefix(res).unwrap();
305
306 let meta = store
307 .head(&Path::from("alltypes_tiny_pages_plain.parquet"))
308 .await
309 .unwrap();
310
311 (meta, Arc::new(store) as Arc<dyn ObjectStore>)
312 }
313
314 #[tokio::test]
315 async fn test_simple() {
316 let (meta, store) = get_meta_store().await;
317 let object_reader =
318 ParquetObjectReader::new(store, meta.location).with_file_size(meta.size);
319
320 let builder = ParquetRecordBatchStreamBuilder::new(object_reader)
321 .await
322 .unwrap();
323 let batches: Vec<_> = builder.build().unwrap().try_collect().await.unwrap();
324
325 assert_eq!(batches.len(), 1);
326 assert_eq!(batches[0].num_rows(), 8);
327 }
328
329 #[tokio::test]
330 async fn test_simple_without_file_length() {
331 let (meta, store) = get_meta_store().await;
332 let object_reader = ParquetObjectReader::new(store, meta.location);
333
334 let builder = ParquetRecordBatchStreamBuilder::new(object_reader)
335 .await
336 .unwrap();
337 let batches: Vec<_> = builder.build().unwrap().try_collect().await.unwrap();
338
339 assert_eq!(batches.len(), 1);
340 assert_eq!(batches[0].num_rows(), 8);
341 }
342
343 #[tokio::test]
344 async fn test_not_found() {
345 let (mut meta, store) = get_meta_store().await;
346 meta.location = Path::from("I don't exist.parquet");
347
348 let object_reader =
349 ParquetObjectReader::new(store, meta.location).with_file_size(meta.size);
350 match ParquetRecordBatchStreamBuilder::new(object_reader).await {
352 Ok(_) => panic!("expected failure"),
353 Err(e) => {
354 let err = e.to_string();
355 assert!(err.contains("I don't exist.parquet not found:"), "{err}");
356 }
357 }
358 }
359
360 #[tokio::test]
361 async fn test_runtime_is_used() {
362 let num_actions = Arc::new(AtomicUsize::new(0));
363
364 let (a1, a2) = (num_actions.clone(), num_actions.clone());
365 let rt = tokio::runtime::Builder::new_multi_thread()
366 .on_thread_park(move || {
367 a1.fetch_add(1, Ordering::Relaxed);
368 })
369 .on_thread_unpark(move || {
370 a2.fetch_add(1, Ordering::Relaxed);
371 })
372 .build()
373 .unwrap();
374
375 let (meta, store) = get_meta_store().await;
376
377 let initial_actions = num_actions.load(Ordering::Relaxed);
378
379 let reader = ParquetObjectReader::new(store, meta.location)
380 .with_file_size(meta.size)
381 .with_runtime(rt.handle().clone());
382
383 let builder = ParquetRecordBatchStreamBuilder::new(reader).await.unwrap();
384 let batches: Vec<_> = builder.build().unwrap().try_collect().await.unwrap();
385
386 assert_eq!(batches.len(), 1);
388 assert_eq!(batches[0].num_rows(), 8);
389
390 assert!(num_actions.load(Ordering::Relaxed) - initial_actions > 0);
391
392 tokio::runtime::Handle::current().spawn_blocking(move || drop(rt));
395 }
396
397 #[tokio::test]
399 async fn test_runtime_thread_id_different() {
400 let rt = tokio::runtime::Builder::new_multi_thread()
401 .worker_threads(1)
402 .build()
403 .unwrap();
404
405 let (meta, store) = get_meta_store().await;
406
407 let reader = ParquetObjectReader::new(store, meta.location)
408 .with_file_size(meta.size)
409 .with_runtime(rt.handle().clone());
410
411 let current_id = std::thread::current().id();
412
413 let other_id = reader
414 .spawn(|_, _| async move { Ok::<_, ParquetError>(std::thread::current().id()) }.boxed())
415 .await
416 .unwrap();
417
418 assert_ne!(current_id, other_id);
419
420 tokio::runtime::Handle::current().spawn_blocking(move || drop(rt));
421 }
422
423 #[tokio::test]
424 async fn io_fails_on_shutdown_runtime() {
425 let rt = tokio::runtime::Builder::new_multi_thread()
426 .worker_threads(1)
427 .build()
428 .unwrap();
429
430 let (meta, store) = get_meta_store().await;
431
432 let mut reader = ParquetObjectReader::new(store, meta.location)
433 .with_file_size(meta.size)
434 .with_runtime(rt.handle().clone());
435
436 rt.shutdown_background();
437
438 let err = reader.get_bytes(0..1).await.unwrap_err().to_string();
439
440 assert!(err.contains("was cancelled"));
441 }
442
443 #[tokio::test]
444 async fn test_page_index_policy_skip_uses_preload_true() {
445 let (meta, store) = get_meta_store_with_page_index().await;
446
447 let mut reader = ParquetObjectReader::new(store.clone(), meta.location.clone())
449 .with_file_size(meta.size)
450 .with_preload_column_index(true)
451 .with_preload_offset_index(true);
452
453 let options = ArrowReaderOptions::new().with_page_index_policy(PageIndexPolicy::Skip);
455
456 let metadata = reader.get_metadata(Some(&options)).await.unwrap();
458
459 assert!(metadata.page_index().is_some_and(|idx| idx.is_complete()));
461 }
462
463 #[tokio::test]
464 async fn test_page_index_policy_optional_overrides_preload_false() {
465 let (meta, store) = get_meta_store_with_page_index().await;
466
467 let mut reader = ParquetObjectReader::new(store.clone(), meta.location.clone())
469 .with_file_size(meta.size)
470 .with_preload_column_index(false)
471 .with_preload_offset_index(false);
472
473 let options = ArrowReaderOptions::new().with_page_index_policy(PageIndexPolicy::Optional);
475
476 let metadata = reader.get_metadata(Some(&options)).await.unwrap();
478
479 assert!(metadata.page_index().is_some());
482 }
483
484 #[tokio::test]
485 async fn test_page_index_policy_optional_vs_skip() {
486 let (meta, store) = get_meta_store_with_page_index().await;
487
488 let mut reader1 = ParquetObjectReader::new(store.clone(), meta.location.clone())
490 .with_file_size(meta.size)
491 .with_preload_column_index(false)
492 .with_preload_offset_index(false);
493
494 let options1 = ArrowReaderOptions::new().with_page_index_policy(PageIndexPolicy::Skip);
495 let metadata1 = reader1.get_metadata(Some(&options1)).await.unwrap();
496
497 let mut reader2 = ParquetObjectReader::new(store.clone(), meta.location.clone())
499 .with_file_size(meta.size)
500 .with_preload_column_index(false)
501 .with_preload_offset_index(false);
502
503 let options2 = ArrowReaderOptions::new().with_page_index_policy(PageIndexPolicy::Optional);
504 let metadata2 = reader2.get_metadata(Some(&options2)).await.unwrap();
505
506 assert!(metadata1.page_index().is_none());
510 assert!(metadata2.page_index().is_some());
511 }
512
513 #[tokio::test]
514 async fn test_page_index_policy_no_options_uses_preload() {
515 let (meta, store) = get_meta_store_with_page_index().await;
516
517 let mut reader = ParquetObjectReader::new(store, meta.location)
519 .with_file_size(meta.size)
520 .with_preload_column_index(true)
521 .with_preload_offset_index(true);
522
523 let metadata = reader.get_metadata(None).await.unwrap();
525
526 assert!(metadata.page_index().is_some_and(|idx| idx.is_complete()));
530 }
531}