polars_io/ipc/
ipc_reader_async.rs1use std::sync::Arc;
2
3use object_store::path::Path;
4use polars_arrow::io::ipc::read::{FileMetadata, OutOfSpecKind, get_row_count};
5use polars_buffer::Buffer;
6use polars_core::datatypes::IDX_DTYPE;
7use polars_core::frame::DataFrame;
8use polars_core::runtime::ASYNC;
9use polars_core::schema::{Schema, SchemaExt};
10use polars_error::{PolarsResult, polars_bail, polars_ensure, polars_err, to_compute_err};
11use polars_utils::mmap::MMapSemaphore;
12use polars_utils::pl_path::PlRefPath;
13use polars_utils::pl_str::PlSmallStr;
14
15use crate::RowIndex;
16use crate::cloud::concurrency_config::FetchConfig;
17use crate::cloud::{
18 CloudLocation, CloudOptions, PolarsObjectStore, build_object_store, object_path_from_str,
19};
20use crate::configs::cloud_footer_read_size;
21use crate::file_cache::{FileCacheEntry, init_entries_from_uri_list};
22use crate::predicates::PhysicalIoExpr;
23use crate::prelude::{IpcReader, materialize_projection};
24use crate::shared::SerReader;
25
26pub struct IpcReaderAsync {
28 store: PolarsObjectStore,
29 cache_entry: Arc<FileCacheEntry>,
30 path: Path,
31}
32
33#[derive(Default, Clone)]
34pub struct IpcReadOptions {
35 projection: Option<Arc<[PlSmallStr]>>,
37
38 row_limit: Option<usize>,
40
41 row_index: Option<RowIndex>,
43
44 predicate: Option<Arc<dyn PhysicalIoExpr>>,
46}
47
48impl IpcReadOptions {
49 pub fn with_projection(mut self, projection: Option<Arc<[PlSmallStr]>>) -> Self {
50 self.projection = projection;
51 self
52 }
53
54 pub fn with_row_limit(mut self, row_limit: impl Into<Option<usize>>) -> Self {
55 self.row_limit = row_limit.into();
56 self
57 }
58
59 pub fn with_row_index(mut self, row_index: impl Into<Option<RowIndex>>) -> Self {
60 self.row_index = row_index.into();
61 self
62 }
63
64 pub fn with_predicate(mut self, predicate: impl Into<Option<Arc<dyn PhysicalIoExpr>>>) -> Self {
65 self.predicate = predicate.into();
66 self
67 }
68}
69
70impl IpcReaderAsync {
71 pub async fn from_uri(
72 uri: PlRefPath,
73 cloud_options: Option<&CloudOptions>,
74 ) -> PolarsResult<IpcReaderAsync> {
75 let cache_entry =
76 init_entries_from_uri_list([uri.clone()].into_iter(), cloud_options).await?[0].clone();
77 let (CloudLocation { prefix, .. }, store) =
78 build_object_store(uri, cloud_options, false).await?;
79
80 let path = object_path_from_str(&prefix)?;
81
82 Ok(Self {
83 store,
84 cache_entry,
85 path,
86 })
87 }
88
89 pub async fn metadata(&self) -> PolarsResult<FileMetadata> {
90 let (tail, file_size) = self
93 .store
94 .get_suffix(
95 &self.path,
96 cloud_footer_read_size(),
97 FetchConfig::random_access(),
98 )
99 .await?;
100
101 polars_ensure!(
102 tail.len() >= FOOTER_METADATA_SIZE,
103 ComputeError: "ipc file size is smaller than the minimum"
104 );
105
106 let footer_size = deserialize_footer_metadata(
107 tail[tail.len() - FOOTER_METADATA_SIZE..]
108 .try_into()
109 .map_err(to_compute_err)?,
110 )?;
111 let footer_len = FOOTER_METADATA_SIZE + footer_size;
112
113 polars_ensure!(
114 file_size >= footer_len,
115 ComputeError: "invalid ipc footer metadata: footer size too large"
116 );
117
118 let footer = if tail.len() >= footer_len {
119 let offset = tail.len() - footer_len;
120 tail.sliced(offset..)
121 } else {
122 let missing = self
124 .store
125 .get_range(
126 &self.path,
127 file_size - footer_len..file_size - tail.len(),
128 FetchConfig::random_access(),
129 )
130 .await?;
131 let mut footer = Vec::with_capacity(footer_len);
132 footer.extend_from_slice(&missing);
133 footer.extend_from_slice(&tail);
134 Buffer::from_vec(footer)
135 };
136
137 polars_arrow::io::ipc::read::deserialize_footer(
138 footer.as_ref(),
139 footer_size.try_into().map_err(to_compute_err)?,
140 )
141 }
142
143 pub async fn data(
144 &self,
145 metadata: Option<&FileMetadata>,
146 options: IpcReadOptions,
147 verbose: bool,
148 ) -> PolarsResult<DataFrame> {
149 let file = ASYNC.block_in_place(|| self.cache_entry.try_open_check_latest())?;
152 let bytes = MMapSemaphore::new_from_file(&file).unwrap();
153
154 let projection = match options.projection.as_deref() {
155 Some(projection) => {
156 fn prepare_schema(mut schema: Schema, row_index: Option<&RowIndex>) -> Schema {
157 if let Some(rc) = row_index {
158 let _ = schema.insert_at_index(0, rc.name.clone(), IDX_DTYPE);
159 }
160 schema
161 }
162
163 let fetched_metadata;
165 let metadata = if let Some(metadata) = metadata {
166 metadata
167 } else {
168 fetched_metadata = self.metadata().await?;
170 &fetched_metadata
171 };
172
173 let schema = prepare_schema(
174 Schema::from_arrow_schema(metadata.schema.as_ref()),
175 options.row_index.as_ref(),
176 );
177
178 let hive_partitions = None;
179
180 materialize_projection(
181 Some(projection),
182 &schema,
183 hive_partitions,
184 options.row_index.is_some(),
185 )
186 },
187 None => None,
188 };
189
190 let reader = <IpcReader<_> as SerReader<_>>::new(std::io::Cursor::new(bytes.as_ref()))
191 .with_row_index(options.row_index)
192 .with_n_rows(options.row_limit)
193 .with_projection(projection);
194 reader.finish_with_scan_ops(options.predicate, verbose)
195 }
196
197 pub async fn count_rows(&self, _metadata: Option<&FileMetadata>) -> PolarsResult<i64> {
198 let file = ASYNC.block_in_place(|| self.cache_entry.try_open_check_latest())?;
201 let bytes = MMapSemaphore::new_from_file(&file).unwrap();
202 get_row_count(&mut std::io::Cursor::new(bytes.as_ref()))
203 }
204}
205
206const FOOTER_METADATA_SIZE: usize = 10;
207
208fn deserialize_footer_metadata(bytes: [u8; FOOTER_METADATA_SIZE]) -> PolarsResult<usize> {
211 let footer_size: usize =
212 i32::from_le_bytes(bytes[0..4].try_into().unwrap_or_else(|_| unreachable!()))
213 .try_into()
214 .map_err(|_| polars_err!(oos = OutOfSpecKind::NegativeFooterLength))?;
215
216 if &bytes[4..] != b"ARROW1" {
217 polars_bail!(oos = OutOfSpecKind::InvalidFooter);
218 }
219
220 Ok(footer_size)
221}