Skip to main content

polars_io/parquet/read/
async_impl.rs

1//! Read parquet files in parallel from the Object Store without a third party crate.
2
3use object_store::path::Path as ObjectPath;
4use polars_arrow::datatypes::ArrowSchemaRef;
5use polars_buffer::Buffer;
6use polars_core::prelude::*;
7use polars_parquet::parquet::error::ParquetError;
8use polars_parquet::parquet::read::{deserialize_metadata, deserialize_num_rows};
9use polars_parquet::parquet::{ENCRYPTED_PARQUET_MAGIC, FOOTER_SIZE, PARQUET_MAGIC};
10use polars_utils::pl_path::PlRefPath;
11
12use crate::cloud::concurrency_config::FetchConfig;
13use crate::cloud::{
14    CloudLocation, CloudOptions, PolarsObjectStore, build_object_store, object_path_from_str,
15};
16use crate::configs::cloud_footer_read_size;
17use crate::parquet::metadata::FileMetadataRef;
18use crate::parquet::read::PlFileDecryptionProperties;
19
20pub struct ParquetObjectStore {
21    store: PolarsObjectStore,
22    path: ObjectPath,
23    metadata: Option<FileMetadataRef>,
24    schema: Option<ArrowSchemaRef>,
25    decryption_properties: Option<PlFileDecryptionProperties>,
26}
27
28impl ParquetObjectStore {
29    pub async fn from_uri(
30        uri: PlRefPath,
31        options: Option<&CloudOptions>,
32        metadata: Option<FileMetadataRef>,
33    ) -> PolarsResult<Self> {
34        let (CloudLocation { prefix, .. }, store) = build_object_store(uri, options, false).await?;
35        let path = object_path_from_str(&prefix)?;
36
37        Ok(ParquetObjectStore {
38            store,
39            path,
40            metadata,
41            schema: None,
42            decryption_properties: None,
43        })
44    }
45
46    /// Set the properties used to decrypt the metadata of encrypted files.
47    pub fn with_decryption_properties(
48        mut self,
49        decryption_properties: Option<PlFileDecryptionProperties>,
50    ) -> Self {
51        self.decryption_properties = decryption_properties;
52        self
53    }
54
55    /// Number of rows in the parquet file.
56    pub async fn num_rows(&mut self) -> PolarsResult<usize> {
57        let metadata = self.get_metadata().await?;
58        Ok(metadata.num_rows)
59    }
60
61    /// Fetch and memoize the metadata of the parquet file.
62    pub async fn get_metadata(&mut self) -> PolarsResult<&FileMetadataRef> {
63        if self.metadata.is_none() {
64            let footer = fetch_footer_bytes(&self.store, &self.path).await?;
65            self.metadata = Some(Arc::new(deserialize_metadata(
66                footer,
67                self.decryption_properties.as_ref().map(|p| &p.0),
68            )?));
69        }
70        Ok(self.metadata.as_ref().unwrap())
71    }
72
73    /// Decode only `FileMetaData.num_rows` from the remote footer.
74    /// Not memoized. Used by `RowCounts` resolve mode.
75    pub async fn num_rows_only(&mut self) -> PolarsResult<i64> {
76        let footer = fetch_footer_bytes(&self.store, &self.path).await?;
77        Ok(deserialize_num_rows(
78            footer,
79            self.decryption_properties.as_ref().map(|p| &p.0),
80        )?)
81    }
82
83    pub async fn schema(&mut self) -> PolarsResult<ArrowSchemaRef> {
84        self.schema = Some(match self.schema.as_ref() {
85            Some(schema) => Arc::clone(schema),
86            None => {
87                let metadata = self.get_metadata().await?;
88                let arrow_schema = polars_parquet::arrow::read::infer_schema(metadata)?;
89                Arc::new(arrow_schema)
90            },
91        });
92
93        Ok(self.schema.clone().unwrap())
94    }
95}
96
97fn read_n<const N: usize>(reader: &mut &[u8]) -> Option<[u8; N]> {
98    if N <= reader.len() {
99        let (head, tail) = reader.split_at(N);
100        *reader = tail;
101        Some(head.try_into().unwrap())
102    } else {
103        None
104    }
105}
106
107fn read_i32le(reader: &mut &[u8]) -> Option<i32> {
108    read_n(reader).map(i32::from_le_bytes)
109}
110
111/// Speculatively read `cloud_footer_read_size()` bytes from the tail. If the
112/// footer fits in the prefetch (the common case), we're done in one range
113/// request; otherwise re-fetch the full footer. Mirrors the sync
114/// `fetch_footer_buf` strategy.
115async fn fetch_footer_bytes(
116    store: &PolarsObjectStore,
117    path: &ObjectPath,
118) -> PolarsResult<Buffer<u8>> {
119    let out_of_spec = |msg: &str| ParquetError::OutOfSpec(msg.to_string());
120
121    let (prefetched, file_byte_length) = store
122        .get_suffix(path, cloud_footer_read_size(), FetchConfig::random_access())
123        .await?;
124
125    if prefetched.len() < FOOTER_SIZE as usize {
126        return Err(out_of_spec("not enough bytes to contain parquet footer").into());
127    }
128
129    // Trailing 8 bytes: footer size (i32 LE) + magic.
130    let footer_byte_length: usize = {
131        let tail_start = prefetched.len() - FOOTER_SIZE as usize;
132        let reader = &mut &prefetched.as_ref()[tail_start..];
133        let footer_byte_size = read_i32le(reader).unwrap();
134        let magic = read_n(reader).unwrap();
135        debug_assert!(reader.is_empty());
136        if magic != PARQUET_MAGIC && magic != ENCRYPTED_PARQUET_MAGIC {
137            return Err(out_of_spec("incorrect magic in parquet footer").into());
138        }
139        footer_byte_size
140            .try_into()
141            .map_err(|_| out_of_spec("negative footer byte length"))?
142    };
143
144    let footer_len = FOOTER_SIZE as usize + footer_byte_length;
145    if footer_len <= prefetched.len() {
146        // Common case: footer already in the prefetch; zero extra round trips.
147        let footer = prefetched.clone().sliced((prefetched.len() - footer_len)..);
148
149        // The footer is held for the lifetime of the metadata, so copy it out rather than
150        // pin the whole prefetch for a fraction of its bytes.
151        return Ok(if prefetched.len() >= 2 * footer_len {
152            Buffer::from_vec(footer.to_vec())
153        } else {
154            footer
155        });
156    }
157
158    // Fallback: footer larger than the prefetch; re-fetch the full footer.
159    store
160        .get_range(
161            path,
162            file_byte_length
163                .checked_sub(footer_len)
164                .ok_or_else(|| out_of_spec("not enough bytes to contain parquet footer"))?
165                ..file_byte_length,
166            FetchConfig::random_access(),
167        )
168        .await
169}