1818//! [`DFParquetMetadata`] for fetching Parquet file metadata, statistics
1919//! and schema information.
2020
21+ use crate :: file_format:: ObjectStoreFetch ;
2122use crate :: { Int96Coercer , apply_file_schema_type_coercions} ;
2223use arrow:: array:: { Array , ArrayRef , BooleanArray } ;
2324use arrow:: compute:: and;
@@ -43,8 +44,8 @@ use parquet::DecodeResult;
4344use parquet:: arrow:: arrow_reader:: statistics:: StatisticsConverter ;
4445use parquet:: arrow:: { parquet_column, parquet_to_arrow_schema} ;
4546use parquet:: file:: metadata:: {
46- PageIndexPolicy , ParquetMetaData , ParquetMetaDataPushDecoder , RowGroupMetaData ,
47- SortingColumn ,
47+ PageIndexPolicy , ParquetMetaData , ParquetMetaDataPushDecoder , ParquetMetaDataReader ,
48+ RowGroupMetaData , SortingColumn ,
4849} ;
4950use parquet:: schema:: types:: SchemaDescriptor ;
5051use std:: any:: Any ;
@@ -70,6 +71,7 @@ pub struct DFParquetMetadata<'a> {
7071 metadata_size_hint : Option < usize > ,
7172 decryption_properties : Option < Arc < FileDecryptionProperties > > ,
7273 file_metadata_cache : Option < Arc < dyn FileMetadataCache > > ,
74+ page_index_policy : Option < PageIndexPolicy > ,
7375 /// timeunit to coerce INT96 timestamps to
7476 pub coerce_int96 : Option < TimeUnit > ,
7577 /// Optional timezone applied to INT96-coerced timestamps.
@@ -84,6 +86,7 @@ impl<'a> DFParquetMetadata<'a> {
8486 metadata_size_hint : None ,
8587 decryption_properties : None ,
8688 file_metadata_cache : None ,
89+ page_index_policy : None ,
8790 coerce_int96 : None ,
8891 coerce_int96_tz : None ,
8992 }
@@ -113,6 +116,15 @@ impl<'a> DFParquetMetadata<'a> {
113116 self
114117 }
115118
119+ /// Sets the policy for loading parquet page index structures (column and offset indexes).
120+ pub fn with_page_index_policy (
121+ mut self ,
122+ page_index_policy : Option < PageIndexPolicy > ,
123+ ) -> Self {
124+ self . page_index_policy = page_index_policy;
125+ self
126+ }
127+
116128 /// Set timeunit to coerce INT96 timestamps to
117129 pub fn with_coerce_int96 ( mut self , time_unit : Option < TimeUnit > ) -> Self {
118130 self . coerce_int96 = time_unit;
@@ -127,9 +139,27 @@ impl<'a> DFParquetMetadata<'a> {
127139
128140 /// Fetch parquet metadata from the remote object store
129141 pub async fn fetch_metadata ( & self ) -> Result < Arc < ParquetMetaData > > {
130- // implementation to fetch parquet metadata
142+ // fetch_metadata
143+ // │
144+ // ├─ cache_metadata = encryption check
145+ // ├─ page_index_policy = caller override OR default
146+ // │
147+ // ├─ CACHE HIT?
148+ // │ │
149+ // │ ├─ has index OR policy=Skip? → return cache
150+ // │ │
151+ // │ └─ else (footer only, wants index)
152+ // │ → load_page_index (index bytes only)
153+ // │ → cache_metadata() if allowed
154+ // │ → return
155+ // │
156+ // └─ CACHE MISS
157+ // → fetch_metadata_from_store(policy)
158+ // → cache_metadata() if allowed
159+ // → return
131160 let cache_metadata =
132161 !cfg ! ( feature = "parquet_encryption" ) || self . decryption_properties . is_none ( ) ;
162+ let page_index_policy = self . effective_page_index_policy ( cache_metadata) ;
133163
134164 if cache_metadata
135165 && let Some ( file_metadata_cache) = self . file_metadata_cache . as_ref ( )
@@ -140,9 +170,61 @@ impl<'a> DFParquetMetadata<'a> {
140170 . as_any ( )
141171 . downcast_ref :: < CachedParquetMetaData > ( )
142172 {
143- return Ok ( Arc :: clone ( cached_parquet. parquet_metadata ( ) ) ) ;
173+ let cached_metadata = Arc :: clone ( cached_parquet. parquet_metadata ( ) ) ;
174+ // Reuse the cache when it already has page index, or when the caller
175+ // asked to skip page index I/O (footer-only metadata is sufficient).
176+ if Self :: metadata_has_page_index ( cached_metadata. as_ref ( ) )
177+ || page_index_policy == PageIndexPolicy :: Skip
178+ {
179+ return Ok ( cached_metadata) ;
180+ }
181+ let metadata =
182+ Self :: load_page_index ( self . store , self . object_meta , cached_metadata)
183+ . await ?;
184+ if cache_metadata {
185+ self . cache_metadata ( Arc :: clone ( & metadata) ) . await ?;
186+ }
187+ return Ok ( metadata) ;
188+ }
189+
190+ let metadata = self . fetch_metadata_from_store ( page_index_policy) . await ?;
191+ if cache_metadata {
192+ self . cache_metadata ( Arc :: clone ( & metadata) ) . await ?;
193+ }
194+ Ok ( metadata)
195+ }
196+
197+ fn effective_page_index_policy ( & self , cache_metadata : bool ) -> PageIndexPolicy {
198+ self . page_index_policy . unwrap_or_else ( || {
199+ if cache_metadata && self . file_metadata_cache . is_some ( ) {
200+ PageIndexPolicy :: Optional
201+ } else {
202+ PageIndexPolicy :: Skip
203+ }
204+ } )
205+ }
206+
207+ fn metadata_has_page_index ( metadata : & ParquetMetaData ) -> bool {
208+ metadata. column_index ( ) . is_some ( ) && metadata. offset_index ( ) . is_some ( )
209+ }
210+
211+ async fn cache_metadata ( & self , metadata : Arc < ParquetMetaData > ) -> Result < ( ) > {
212+ if let Some ( file_metadata_cache) = & self . file_metadata_cache {
213+ file_metadata_cache. put (
214+ & self . object_meta . location ,
215+ CachedFileMetadataEntry :: new (
216+ self . object_meta . clone ( ) ,
217+ Arc :: new ( CachedParquetMetaData :: new ( metadata) ) ,
218+ ) ,
219+ ) ;
144220 }
221+ Ok ( ( ) )
222+ }
145223
224+ async fn fetch_metadata_from_store (
225+ & self ,
226+ page_index_policy : PageIndexPolicy ,
227+ ) -> Result < Arc < ParquetMetaData > > {
146228 let file_size = self . object_meta . size ;
147229 let mut decoder = ParquetMetaDataPushDecoder :: try_new ( file_size)
148230 . map_err ( DataFusionError :: from) ?;
@@ -153,14 +235,8 @@ impl<'a> DFParquetMetadata<'a> {
153235 . with_file_decryption_properties ( Some ( Arc :: clone ( decryption_properties) ) ) ;
154236 }
155237
156- if cache_metadata && self . file_metadata_cache . is_some ( ) {
157- // Need to retrieve the entire metadata for the caching to be effective.
158- decoder = decoder. with_page_index_policy ( PageIndexPolicy :: Optional ) ;
159- } else {
160- decoder = decoder. with_page_index_policy ( PageIndexPolicy :: Skip ) ;
161- }
238+ decoder = decoder. with_page_index_policy ( page_index_policy) ;
162239
163- // If we have a size hint, prefetch that many bytes from the end of the file
164240 if let Some ( hint) = self . metadata_size_hint {
165241 let prefetch_start = file_size. saturating_sub ( hint as u64 ) ;
166242 let prefetch_range = prefetch_start..file_size;
@@ -199,19 +275,27 @@ impl<'a> DFParquetMetadata<'a> {
199275 }
200276 } ;
201277
202- let metadata = Arc :: new ( metadata) ;
278+ Ok ( Arc :: new ( metadata) )
279+ }
203280
204- if cache_metadata && let Some ( file_metadata_cache) = & self . file_metadata_cache {
205- file_metadata_cache. put (
206- & self . object_meta . location ,
207- CachedFileMetadataEntry :: new (
208- self . object_meta . clone ( ) ,
209- Arc :: new ( CachedParquetMetaData :: new ( Arc :: clone ( & metadata) ) ) ,
210- ) ,
211- ) ;
281+ async fn load_page_index (
282+ store : & dyn ObjectStore ,
283+ object_meta : & ObjectMeta ,
284+ metadata : Arc < ParquetMetaData > ,
285+ ) -> Result < Arc < ParquetMetaData > > {
286+ if metadata. column_index ( ) . is_some ( ) && metadata. offset_index ( ) . is_some ( ) {
287+ return Ok ( metadata) ;
212288 }
213-
214- Ok ( metadata)
289+ let metadata =
290+ Arc :: try_unwrap ( metadata) . unwrap_or_else ( |shared| ( * shared) . clone ( ) ) ;
291+ let mut reader = ParquetMetaDataReader :: new_with_metadata ( metadata)
292+ . with_page_index_policy ( PageIndexPolicy :: Optional ) ;
293+ let fetch = ObjectStoreFetch :: new ( store, object_meta) ;
294+ reader
295+ . load_page_index ( fetch)
296+ . await
297+ . map_err ( DataFusionError :: from) ?;
298+ Ok ( Arc :: new ( reader. finish ( ) . map_err ( DataFusionError :: from) ?) )
215299 }
216300
217301 /// Read and parse the schema of the Parquet file
0 commit comments