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::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 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 pub async fn num_rows(&mut self) -> PolarsResult<usize> {
57 let metadata = self.get_metadata().await?;
58 Ok(metadata.num_rows)
59 }
60
61 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 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
111async 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 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 let footer = prefetched.clone().sliced((prefetched.len() - footer_len)..);
148
149 return Ok(if prefetched.len() >= 2 * footer_len {
152 Buffer::from_vec(footer.to_vec())
153 } else {
154 footer
155 });
156 }
157
158 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}