polars_io/parquet/read/
async_impl.rs1use object_store::path::Path as ObjectPath;
4use polars_arrow::datatypes::ArrowSchemaRef;
5use polars_buffer::Buffer;
6use polars_core::prelude::*;
7use polars_parquet::parquet::encryption::decrypt::FileDecryptionProperties;
8use polars_parquet::parquet::error::ParquetError;
9use polars_parquet::parquet::read::{deserialize_metadata, deserialize_num_rows};
10use polars_parquet::parquet::{ENCRYPTED_PARQUET_MAGIC, FOOTER_SIZE, PARQUET_MAGIC};
11use polars_utils::pl_path::PlRefPath;
12
13use crate::cloud::concurrency_config::FetchConfig;
14use crate::cloud::{
15 CloudLocation, CloudOptions, PolarsObjectStore, build_object_store, object_path_from_str,
16};
17use crate::configs::cloud_footer_read_size;
18use crate::parquet::encryption::{ParquetDecryption, file_decryption_properties};
19use crate::parquet::metadata::FileMetadataRef;
20
21pub struct ParquetObjectStore {
22 store: PolarsObjectStore,
23 path: ObjectPath,
24 metadata: Option<FileMetadataRef>,
25 schema: Option<ArrowSchemaRef>,
26 decryption_properties: Option<Arc<FileDecryptionProperties>>,
27}
28
29impl ParquetObjectStore {
30 pub async fn from_uri(
31 uri: PlRefPath,
32 options: Option<&CloudOptions>,
33 metadata: Option<FileMetadataRef>,
34 ) -> PolarsResult<Self> {
35 let (CloudLocation { prefix, .. }, store) = build_object_store(uri, options, false).await?;
36 let path = object_path_from_str(&prefix)?;
37
38 Ok(ParquetObjectStore {
39 store,
40 path,
41 metadata,
42 schema: None,
43 decryption_properties: None,
44 })
45 }
46
47 pub fn with_decryption(mut self, decryption: Option<&ParquetDecryption>) -> PolarsResult<Self> {
49 self.decryption_properties = file_decryption_properties(decryption)?;
50 Ok(self)
51 }
52
53 pub async fn num_rows(&mut self) -> PolarsResult<usize> {
55 let metadata = self.get_metadata().await?;
56 Ok(metadata.num_rows)
57 }
58
59 pub async fn get_metadata(&mut self) -> PolarsResult<&FileMetadataRef> {
61 if self.metadata.is_none() {
62 let footer = fetch_footer_bytes(&self.store, &self.path).await?;
63 self.metadata = Some(Arc::new(deserialize_metadata(
64 footer,
65 self.decryption_properties.as_ref(),
66 )?));
67 }
68 Ok(self.metadata.as_ref().unwrap())
69 }
70
71 pub async fn num_rows_only(&mut self) -> PolarsResult<i64> {
74 let footer = fetch_footer_bytes(&self.store, &self.path).await?;
75 Ok(deserialize_num_rows(
76 footer,
77 self.decryption_properties.as_ref(),
78 )?)
79 }
80
81 pub async fn schema(&mut self) -> PolarsResult<ArrowSchemaRef> {
82 self.schema = Some(match self.schema.as_ref() {
83 Some(schema) => Arc::clone(schema),
84 None => {
85 let metadata = self.get_metadata().await?;
86 let arrow_schema = polars_parquet::arrow::read::infer_schema(metadata)?;
87 Arc::new(arrow_schema)
88 },
89 });
90
91 Ok(self.schema.clone().unwrap())
92 }
93}
94
95fn read_n<const N: usize>(reader: &mut &[u8]) -> Option<[u8; N]> {
96 if N <= reader.len() {
97 let (head, tail) = reader.split_at(N);
98 *reader = tail;
99 Some(head.try_into().unwrap())
100 } else {
101 None
102 }
103}
104
105fn read_i32le(reader: &mut &[u8]) -> Option<i32> {
106 read_n(reader).map(i32::from_le_bytes)
107}
108
109async fn fetch_footer_bytes(
114 store: &PolarsObjectStore,
115 path: &ObjectPath,
116) -> PolarsResult<Buffer<u8>> {
117 let out_of_spec = |msg: &str| ParquetError::OutOfSpec(msg.to_string());
118
119 let (prefetched, file_byte_length) = store
120 .get_suffix(path, cloud_footer_read_size(), FetchConfig::random_access())
121 .await?;
122
123 if prefetched.len() < FOOTER_SIZE as usize {
124 return Err(out_of_spec("not enough bytes to contain parquet footer").into());
125 }
126
127 let footer_byte_length: usize = {
129 let tail_start = prefetched.len() - FOOTER_SIZE as usize;
130 let reader = &mut &prefetched.as_ref()[tail_start..];
131 let footer_byte_size = read_i32le(reader).unwrap();
132 let magic = read_n(reader).unwrap();
133 debug_assert!(reader.is_empty());
134 if magic != PARQUET_MAGIC && magic != ENCRYPTED_PARQUET_MAGIC {
135 return Err(out_of_spec("incorrect magic in parquet footer").into());
136 }
137 footer_byte_size
138 .try_into()
139 .map_err(|_| out_of_spec("negative footer byte length"))?
140 };
141
142 let footer_len = FOOTER_SIZE as usize + footer_byte_length;
143 if footer_len <= prefetched.len() {
144 let footer = prefetched.clone().sliced((prefetched.len() - footer_len)..);
146
147 return Ok(if prefetched.len() >= 2 * footer_len {
150 Buffer::from_vec(footer.to_vec())
151 } else {
152 footer
153 });
154 }
155
156 store
158 .get_range(
159 path,
160 file_byte_length
161 .checked_sub(footer_len)
162 .ok_or_else(|| out_of_spec("not enough bytes to contain parquet footer"))?
163 ..file_byte_length,
164 FetchConfig::random_access(),
165 )
166 .await
167}