Skip to main content

datafusion_datasource_parquet/
file_format.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18//! [`ParquetFormat`]: Parquet [`FileFormat`] abstractions
19
20use std::fmt;
21use std::fmt::Debug;
22use std::ops::Range;
23use std::sync::Arc;
24
25// Re-export so the historical `file_format::*` paths still resolve.
26#[expect(deprecated)]
27pub use crate::schema_coercion::coerce_int96_to_resolution;
28pub use crate::schema_coercion::{
29    Int96Coercer, apply_file_schema_type_coercions, transform_binary_to_string,
30    transform_schema_to_view,
31};
32
33pub use crate::sink::ParquetSink;
34
35use arrow::datatypes::{Fields, Schema, SchemaRef};
36use datafusion_datasource::TableSchema;
37use datafusion_datasource::file_compression_type::FileCompressionType;
38use datafusion_datasource::file_sink_config::FileSinkConfig;
39
40use datafusion_datasource::file_format::{FileFormat, FileFormatFactory};
41
42use datafusion_common::Statistics;
43use datafusion_common::config::{ConfigField, ConfigFileType, TableParquetOptions};
44use datafusion_common::encryption::FileDecryptionProperties;
45use datafusion_common::parsers::CompressionTypeVariant;
46use datafusion_common::{
47    DEFAULT_PARQUET_EXTENSION, DataFusionError, GetExt, Result, internal_datafusion_err,
48    internal_err, not_impl_err,
49};
50use datafusion_datasource::file::FileSource;
51use datafusion_datasource::file_scan_config::{FileScanConfig, FileScanConfigBuilder};
52use datafusion_datasource::sink::DataSinkExec;
53use datafusion_datasource::write::get_writer_schema;
54use datafusion_expr::dml::InsertOp;
55use datafusion_physical_expr_common::sort_expr::{LexOrdering, LexRequirement};
56use datafusion_physical_plan::ExecutionPlan;
57use datafusion_session::Session;
58
59use crate::metadata::{DFParquetMetadata, lex_ordering_to_sorting_columns};
60use crate::reader::CachedParquetFileReaderFactory;
61use crate::source::{
62    ParquetSource, parse_coerce_int96_string, parse_coerce_int96_tz_string,
63};
64use async_trait::async_trait;
65use bytes::Bytes;
66use datafusion_datasource::source::DataSourceExec;
67use datafusion_execution::cache::cache_manager::FileMetadataCache;
68use futures::future::BoxFuture;
69use futures::{FutureExt, StreamExt, TryStreamExt};
70use object_store::path::Path;
71use object_store::{ObjectMeta, ObjectStore, ObjectStoreExt};
72use parquet::arrow::async_reader::MetadataFetch;
73use parquet::errors::ParquetError;
74use parquet::file::metadata::ParquetMetaData;
75
76#[derive(Default)]
77/// Factory struct used to create [ParquetFormat]
78pub struct ParquetFormatFactory {
79    /// inner options for parquet
80    pub options: Option<TableParquetOptions>,
81}
82
83impl ParquetFormatFactory {
84    /// Creates an instance of [ParquetFormatFactory]
85    pub fn new() -> Self {
86        Self { options: None }
87    }
88
89    /// Creates an instance of [ParquetFormatFactory] with customized default options
90    pub fn new_with_options(options: TableParquetOptions) -> Self {
91        Self {
92            options: Some(options),
93        }
94    }
95}
96
97impl FileFormatFactory for ParquetFormatFactory {
98    fn create(
99        &self,
100        state: &dyn Session,
101        format_options: &std::collections::HashMap<String, String>,
102    ) -> Result<Arc<dyn FileFormat>> {
103        let parquet_options = match &self.options {
104            None => {
105                let mut table_options = state.default_table_options();
106                table_options.set_config_format(ConfigFileType::PARQUET);
107                table_options.alter_with_string_hash_map(format_options)?;
108                table_options.parquet
109            }
110            Some(parquet_options) => {
111                let mut parquet_options = parquet_options.clone();
112                for (k, v) in format_options {
113                    parquet_options.set(k, v)?;
114                }
115                parquet_options
116            }
117        };
118
119        Ok(Arc::new(
120            ParquetFormat::default().with_options(parquet_options),
121        ))
122    }
123
124    fn default(&self) -> Arc<dyn FileFormat> {
125        Arc::new(ParquetFormat::default())
126    }
127}
128
129impl GetExt for ParquetFormatFactory {
130    fn get_ext(&self) -> String {
131        // Removes the dot, i.e. ".parquet" -> "parquet"
132        DEFAULT_PARQUET_EXTENSION[1..].to_string()
133    }
134}
135
136impl Debug for ParquetFormatFactory {
137    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
138        f.debug_struct("ParquetFormatFactory")
139            .field("ParquetFormatFactory", &self.options)
140            .finish()
141    }
142}
143/// The Apache Parquet `FileFormat` implementation
144#[derive(Debug, Default)]
145pub struct ParquetFormat {
146    options: TableParquetOptions,
147}
148
149impl ParquetFormat {
150    /// Construct a new Format with no local overrides
151    pub fn new() -> Self {
152        Self::default()
153    }
154
155    /// Activate statistics based row group level pruning
156    /// - If `None`, defaults to value on `config_options`
157    pub fn with_enable_pruning(mut self, enable: bool) -> Self {
158        self.options.global.pruning = enable;
159        self
160    }
161
162    /// Return `true` if pruning is enabled
163    pub fn enable_pruning(&self) -> bool {
164        self.options.global.pruning
165    }
166
167    /// Provide a hint to the size of the file metadata. If a hint is provided
168    /// the reader will try and fetch the last `size_hint` bytes of the parquet file optimistically.
169    /// Without a hint, two read are required. One read to fetch the 8-byte parquet footer and then
170    /// another read to fetch the metadata length encoded in the footer.
171    ///
172    /// - If `None`, defaults to value on `config_options`
173    pub fn with_metadata_size_hint(mut self, size_hint: Option<usize>) -> Self {
174        self.options.global.metadata_size_hint = size_hint;
175        self
176    }
177
178    /// Return the metadata size hint if set
179    pub fn metadata_size_hint(&self) -> Option<usize> {
180        self.options.global.metadata_size_hint
181    }
182
183    /// Tell the parquet reader to skip any metadata that may be in
184    /// the file Schema. This can help avoid schema conflicts due to
185    /// metadata.
186    ///
187    /// - If `None`, defaults to value on `config_options`
188    pub fn with_skip_metadata(mut self, skip_metadata: bool) -> Self {
189        self.options.global.skip_metadata = skip_metadata;
190        self
191    }
192
193    /// Returns `true` if schema metadata will be cleared prior to
194    /// schema merging.
195    pub fn skip_metadata(&self) -> bool {
196        self.options.global.skip_metadata
197    }
198
199    /// Set Parquet options for the ParquetFormat
200    pub fn with_options(mut self, options: TableParquetOptions) -> Self {
201        self.options = options;
202        self
203    }
204
205    /// Parquet options
206    pub fn options(&self) -> &TableParquetOptions {
207        &self.options
208    }
209
210    /// Return `true` if should use view types.
211    ///
212    /// If this returns true, DataFusion will instruct the parquet reader
213    /// to read string / binary columns using view `StringView` or `BinaryView`
214    /// if the table schema specifies those types, regardless of any embedded metadata
215    /// that may specify an alternate Arrow type. The parquet reader is optimized
216    /// for reading `StringView` and `BinaryView` and such queries are significantly faster.
217    ///
218    /// If this returns false, the parquet reader will read the columns according to the
219    /// defaults or any embedded Arrow type information. This may result in reading
220    /// `StringArrays` and then casting to `StringViewArray` which is less efficient.
221    pub fn force_view_types(&self) -> bool {
222        self.options.global.schema_force_view_types
223    }
224
225    /// If true, will use view types. See [`Self::force_view_types`] for details
226    pub fn with_force_view_types(mut self, use_views: bool) -> Self {
227        self.options.global.schema_force_view_types = use_views;
228        self
229    }
230
231    /// Return `true` if binary types will be read as strings.
232    ///
233    /// If this returns true, DataFusion will instruct the parquet reader
234    /// to read binary columns such as `Binary` or `BinaryView` as the
235    /// corresponding string type such as `Utf8` or `LargeUtf8`.
236    /// The parquet reader has special optimizations for `Utf8` and `LargeUtf8`
237    /// validation, and such queries are significantly faster than reading
238    /// binary columns and then casting to string columns.
239    pub fn binary_as_string(&self) -> bool {
240        self.options.global.binary_as_string
241    }
242
243    /// If true, will read binary types as strings. See [`Self::binary_as_string`] for details
244    pub fn with_binary_as_string(mut self, binary_as_string: bool) -> Self {
245        self.options.global.binary_as_string = binary_as_string;
246        self
247    }
248
249    pub fn coerce_int96(&self) -> Option<String> {
250        self.options.global.coerce_int96.clone()
251    }
252
253    pub fn with_coerce_int96(mut self, time_unit: Option<String>) -> Self {
254        self.options.global.coerce_int96 = time_unit;
255        self
256    }
257}
258
259/// Clears all metadata (Schema level and field level) on an iterator
260/// of Schemas
261fn clear_metadata(
262    schemas: impl IntoIterator<Item = Schema>,
263) -> impl Iterator<Item = Schema> {
264    schemas.into_iter().map(|schema| {
265        let fields = schema
266            .fields()
267            .iter()
268            .map(|field| {
269                field.as_ref().clone().with_metadata(Default::default()) // clear meta
270            })
271            .collect::<Fields>();
272        Schema::new(fields)
273    })
274}
275
276#[cfg(feature = "parquet_encryption")]
277async fn get_file_decryption_properties(
278    state: &dyn Session,
279    options: &TableParquetOptions,
280    file_path: &Path,
281) -> Result<Option<Arc<FileDecryptionProperties>>> {
282    Ok(match &options.crypto.file_decryption {
283        Some(cfd) => Some(Arc::new(FileDecryptionProperties::try_from(cfd.clone())?)),
284        None => match &options.crypto.factory_id {
285            Some(factory_id) => {
286                let factory =
287                    state.runtime_env().parquet_encryption_factory(factory_id)?;
288                factory
289                    .get_file_decryption_properties(
290                        &options.crypto.factory_options,
291                        file_path,
292                    )
293                    .await?
294            }
295            None => None,
296        },
297    })
298}
299
300#[cfg(not(feature = "parquet_encryption"))]
301#[expect(clippy::unused_async)]
302async fn get_file_decryption_properties(
303    _state: &dyn Session,
304    _options: &TableParquetOptions,
305    _file_path: &Path,
306) -> Result<Option<Arc<FileDecryptionProperties>>> {
307    Ok(None)
308}
309
310#[async_trait]
311impl FileFormat for ParquetFormat {
312    fn get_ext(&self) -> String {
313        ParquetFormatFactory::new().get_ext()
314    }
315
316    fn get_ext_with_compression(
317        &self,
318        file_compression_type: &FileCompressionType,
319    ) -> Result<String> {
320        let ext = self.get_ext();
321        match file_compression_type.get_variant() {
322            CompressionTypeVariant::UNCOMPRESSED => Ok(ext),
323            _ => internal_err!("Parquet FileFormat does not support compression."),
324        }
325    }
326
327    fn compression_type(&self) -> Option<FileCompressionType> {
328        None
329    }
330
331    async fn infer_schema(
332        &self,
333        state: &dyn Session,
334        store: &Arc<dyn ObjectStore>,
335        objects: &[ObjectMeta],
336    ) -> Result<SchemaRef> {
337        let coerce_int96 = match self.coerce_int96() {
338            Some(time_unit) => Some(parse_coerce_int96_string(time_unit.as_str())?),
339            None => None,
340        };
341        let coerce_int96_tz = self
342            .options
343            .global
344            .coerce_int96_tz
345            .as_ref()
346            .map(|tz| parse_coerce_int96_tz_string(tz))
347            .transpose()?;
348
349        let file_metadata_cache =
350            state.runtime_env().cache_manager.get_file_metadata_cache();
351
352        let mut schemas: Vec<_> = futures::stream::iter(objects)
353            .map(|object| async {
354                let file_decryption_properties = get_file_decryption_properties(
355                    state,
356                    &self.options,
357                    &object.location,
358                )
359                .await?;
360                let result = DFParquetMetadata::new(store.as_ref(), object)
361                    .with_metadata_size_hint(self.metadata_size_hint())
362                    .with_decryption_properties(file_decryption_properties)
363                    .with_file_metadata_cache(Some(Arc::clone(&file_metadata_cache)))
364                    .with_coerce_int96(coerce_int96)
365                    .with_coerce_int96_tz(coerce_int96_tz.clone())
366                    .fetch_schema_with_location()
367                    .await?;
368                Ok::<_, DataFusionError>(result)
369            })
370            .boxed() // Workaround https://github.com/rust-lang/rust/issues/64552
371            // fetch schemas concurrently, if requested
372            .buffer_unordered(
373                state
374                    .config_options()
375                    .execution
376                    .meta_fetch_concurrency
377                    .get(),
378            )
379            .try_collect()
380            .await?;
381
382        // Schema inference adds fields based the order they are seen
383        // which depends on the order the files are processed. For some
384        // object stores (like local file systems) the order returned from list
385        // is not deterministic. Thus, to ensure deterministic schema inference
386        // sort the files first.
387        // https://github.com/apache/datafusion/pull/6629
388        schemas
389            .sort_unstable_by(|(location1, _), (location2, _)| location1.cmp(location2));
390
391        let schemas = schemas.into_iter().map(|(_, schema)| schema);
392
393        let schema = if self.skip_metadata() {
394            Schema::try_merge(clear_metadata(schemas))
395        } else {
396            Schema::try_merge(schemas)
397        }?;
398
399        let schema = if self.binary_as_string() {
400            transform_binary_to_string(&schema)
401        } else {
402            schema
403        };
404
405        let schema = if self.force_view_types() {
406            transform_schema_to_view(&schema)
407        } else {
408            schema
409        };
410
411        Ok(Arc::new(schema))
412    }
413
414    async fn infer_stats(
415        &self,
416        state: &dyn Session,
417        store: &Arc<dyn ObjectStore>,
418        table_schema: SchemaRef,
419        object: &ObjectMeta,
420    ) -> Result<Statistics> {
421        let file_decryption_properties =
422            get_file_decryption_properties(state, &self.options, &object.location)
423                .await?;
424        let file_metadata_cache =
425            state.runtime_env().cache_manager.get_file_metadata_cache();
426        DFParquetMetadata::new(store, object)
427            .with_metadata_size_hint(self.metadata_size_hint())
428            .with_decryption_properties(file_decryption_properties)
429            .with_file_metadata_cache(Some(file_metadata_cache))
430            .fetch_statistics(&table_schema)
431            .await
432    }
433
434    async fn infer_ordering(
435        &self,
436        state: &dyn Session,
437        store: &Arc<dyn ObjectStore>,
438        table_schema: SchemaRef,
439        object: &ObjectMeta,
440    ) -> Result<Option<LexOrdering>> {
441        let file_decryption_properties =
442            get_file_decryption_properties(state, &self.options, &object.location)
443                .await?;
444        let file_metadata_cache =
445            state.runtime_env().cache_manager.get_file_metadata_cache();
446        let metadata = DFParquetMetadata::new(store, object)
447            .with_metadata_size_hint(self.metadata_size_hint())
448            .with_decryption_properties(file_decryption_properties)
449            .with_file_metadata_cache(Some(file_metadata_cache))
450            .fetch_metadata()
451            .await?;
452        crate::metadata::ordering_from_parquet_metadata(&metadata, &table_schema)
453    }
454
455    async fn infer_stats_and_ordering(
456        &self,
457        state: &dyn Session,
458        store: &Arc<dyn ObjectStore>,
459        table_schema: SchemaRef,
460        object: &ObjectMeta,
461    ) -> Result<datafusion_datasource::file_format::FileMeta> {
462        let file_decryption_properties =
463            get_file_decryption_properties(state, &self.options, &object.location)
464                .await?;
465        let file_metadata_cache =
466            state.runtime_env().cache_manager.get_file_metadata_cache();
467        let metadata = DFParquetMetadata::new(store, object)
468            .with_metadata_size_hint(self.metadata_size_hint())
469            .with_decryption_properties(file_decryption_properties)
470            .with_file_metadata_cache(Some(file_metadata_cache))
471            .fetch_metadata()
472            .await?;
473        let statistics = DFParquetMetadata::statistics_from_parquet_metadata(
474            &metadata,
475            &table_schema,
476        )?;
477        let ordering =
478            crate::metadata::ordering_from_parquet_metadata(&metadata, &table_schema)?;
479        Ok(
480            datafusion_datasource::file_format::FileMeta::new(statistics)
481                .with_ordering(ordering),
482        )
483    }
484
485    async fn create_physical_plan(
486        &self,
487        state: &dyn Session,
488        conf: FileScanConfig,
489    ) -> Result<Arc<dyn ExecutionPlan>> {
490        let mut metadata_size_hint = None;
491
492        if let Some(metadata) = self.metadata_size_hint() {
493            metadata_size_hint = Some(metadata);
494        }
495
496        let mut source = conf
497            .file_source()
498            .downcast_ref::<ParquetSource>()
499            .cloned()
500            .ok_or_else(|| internal_datafusion_err!("Expected ParquetSource"))?;
501        source = source.with_table_parquet_options(self.options.clone());
502
503        // Use the CachedParquetFileReaderFactory
504        let metadata_cache = state.runtime_env().cache_manager.get_file_metadata_cache();
505        let store = state
506            .runtime_env()
507            .object_store(conf.object_store_url.clone())?;
508        let cached_parquet_read_factory =
509            Arc::new(CachedParquetFileReaderFactory::new(store, metadata_cache));
510        source = source.with_parquet_file_reader_factory(cached_parquet_read_factory);
511
512        if let Some(metadata_size_hint) = metadata_size_hint {
513            source = source.with_metadata_size_hint(metadata_size_hint)
514        }
515
516        source = self.set_source_encryption_factory(source, state)?;
517
518        let conf = FileScanConfigBuilder::from(conf)
519            .with_source(Arc::new(source))
520            .build();
521        Ok(DataSourceExec::from_data_source(conf))
522    }
523
524    async fn create_writer_physical_plan(
525        &self,
526        input: Arc<dyn ExecutionPlan>,
527        _state: &dyn Session,
528        conf: FileSinkConfig,
529        order_requirements: Option<LexRequirement>,
530    ) -> Result<Arc<dyn ExecutionPlan>> {
531        if conf.insert_op != InsertOp::Append {
532            return not_impl_err!("Overwrites are not implemented yet for Parquet");
533        }
534
535        // Convert ordering requirements to Parquet SortingColumns for file metadata
536        let sorting_columns = if let Some(ref requirements) = order_requirements {
537            let ordering: LexOrdering = requirements.clone().into();
538            let writer_schema = get_writer_schema(&conf);
539            // In cases like `COPY (... ORDER BY ...) TO ...` the ORDER BY clause
540            // may not be compatible with Parquet sorting columns (e.g. ordering on `random()`).
541            // So if we cannot create a Parquet sorting column from the ordering requirement,
542            // we skip setting sorting columns on the Parquet sink.
543            lex_ordering_to_sorting_columns(
544                &ordering,
545                conf.output_schema(),
546                &writer_schema,
547            )
548            .ok()
549            .filter(|columns| !columns.is_empty())
550        } else {
551            None
552        };
553
554        let sink = Arc::new(
555            ParquetSink::new(conf, self.options.clone())
556                .with_sorting_columns(sorting_columns),
557        );
558
559        Ok(Arc::new(DataSinkExec::new(input, sink, order_requirements)) as _)
560    }
561
562    fn file_source(&self, table_schema: TableSchema) -> Arc<dyn FileSource> {
563        Arc::new(
564            ParquetSource::new(table_schema)
565                .with_table_parquet_options(self.options.clone()),
566        )
567    }
568}
569
570#[cfg(feature = "parquet_encryption")]
571impl ParquetFormat {
572    fn set_source_encryption_factory(
573        &self,
574        source: ParquetSource,
575        state: &dyn Session,
576    ) -> Result<ParquetSource> {
577        if let Some(encryption_factory_id) = &self.options.crypto.factory_id {
578            Ok(source.with_encryption_factory(
579                state
580                    .runtime_env()
581                    .parquet_encryption_factory(encryption_factory_id)?,
582            ))
583        } else {
584            Ok(source)
585        }
586    }
587}
588
589#[cfg(not(feature = "parquet_encryption"))]
590impl ParquetFormat {
591    fn set_source_encryption_factory(
592        &self,
593        source: ParquetSource,
594        _state: &dyn Session,
595    ) -> Result<ParquetSource> {
596        if let Some(encryption_factory_id) = &self.options.crypto.factory_id {
597            Err(DataFusionError::Configuration(format!(
598                "Parquet encryption factory id is set to '{encryption_factory_id}' but the parquet_encryption feature is disabled"
599            )))
600        } else {
601            Ok(source)
602        }
603    }
604}
605
606/// [`MetadataFetch`] adapter for reading bytes from an [`ObjectStore`]
607pub struct ObjectStoreFetch<'a> {
608    store: &'a dyn ObjectStore,
609    meta: &'a ObjectMeta,
610}
611
612impl<'a> ObjectStoreFetch<'a> {
613    pub fn new(store: &'a dyn ObjectStore, meta: &'a ObjectMeta) -> Self {
614        Self { store, meta }
615    }
616}
617
618impl MetadataFetch for ObjectStoreFetch<'_> {
619    fn fetch(&mut self, range: Range<u64>) -> BoxFuture<'_, Result<Bytes, ParquetError>> {
620        async {
621            self.store
622                .get_range(&self.meta.location, range)
623                .await
624                .map_err(ParquetError::from)
625        }
626        .boxed()
627    }
628}
629
630/// Fetches parquet metadata from ObjectStore for given object
631///
632/// This component is a subject to **change** in near future and is exposed for low level integrations
633/// through [`ParquetFileReaderFactory`].
634///
635/// [`ParquetFileReaderFactory`]: crate::ParquetFileReaderFactory
636#[deprecated(
637    since = "50.0.0",
638    note = "Use `DFParquetMetadata::fetch_metadata` instead"
639)]
640pub async fn fetch_parquet_metadata(
641    store: &dyn ObjectStore,
642    object_meta: &ObjectMeta,
643    size_hint: Option<usize>,
644    decryption_properties: Option<&FileDecryptionProperties>,
645    file_metadata_cache: Option<Arc<FileMetadataCache>>,
646) -> Result<Arc<ParquetMetaData>> {
647    let decryption_properties = decryption_properties.cloned().map(Arc::new);
648    DFParquetMetadata::new(store, object_meta)
649        .with_metadata_size_hint(size_hint)
650        .with_decryption_properties(decryption_properties)
651        .with_file_metadata_cache(file_metadata_cache)
652        .fetch_metadata()
653        .await
654}
655
656/// Read and parse the statistics of the Parquet file at location `path`
657///
658/// See [`statistics_from_parquet_meta_calc`] for more details
659#[deprecated(
660    since = "50.0.0",
661    note = "Use `DFParquetMetadata::fetch_statistics` instead"
662)]
663pub async fn fetch_statistics(
664    store: &dyn ObjectStore,
665    table_schema: SchemaRef,
666    file: &ObjectMeta,
667    metadata_size_hint: Option<usize>,
668    decryption_properties: Option<&FileDecryptionProperties>,
669    file_metadata_cache: Option<Arc<FileMetadataCache>>,
670) -> Result<Statistics> {
671    let decryption_properties = decryption_properties.cloned().map(Arc::new);
672    DFParquetMetadata::new(store, file)
673        .with_metadata_size_hint(metadata_size_hint)
674        .with_decryption_properties(decryption_properties)
675        .with_file_metadata_cache(file_metadata_cache)
676        .fetch_statistics(&table_schema)
677        .await
678}
679
680#[deprecated(
681    since = "50.0.0",
682    note = "Use `DFParquetMetadata::statistics_from_parquet_metadata` instead"
683)]
684#[expect(clippy::needless_pass_by_value)]
685pub fn statistics_from_parquet_meta_calc(
686    metadata: &ParquetMetaData,
687    table_schema: SchemaRef,
688) -> Result<Statistics> {
689    DFParquetMetadata::statistics_from_parquet_metadata(metadata, &table_schema)
690}
691
692#[cfg(feature = "proto")]
693use datafusion_proto_models::protobuf::{self, parquet_column_options, parquet_options};
694
695/// Encode a [`ParquetFormatFactory`]'s options as their protobuf form.
696///
697/// The reverse direction is `TryFrom<&protobuf::TableParquetOptions> for
698/// TableParquetOptions` in `datafusion-proto-models`: `TableParquetOptions` is
699/// a `datafusion-common` type, so that half cannot live here.
700#[cfg(feature = "proto")]
701impl From<&ParquetFormatFactory> for protobuf::TableParquetOptions {
702    fn from(factory: &ParquetFormatFactory) -> Self {
703        let global_options = if let Some(ref options) = factory.options {
704            options.clone()
705        } else {
706            return protobuf::TableParquetOptions::default();
707        };
708
709        let column_specific_options = global_options.column_specific_options;
710        protobuf::TableParquetOptions {
711        global: Some(protobuf::ParquetOptions {
712            enable_page_index: global_options.global.enable_page_index,
713            pruning: global_options.global.pruning,
714            skip_metadata: global_options.global.skip_metadata,
715            metadata_size_hint_opt: global_options.global.metadata_size_hint.map(|size| {
716                parquet_options::MetadataSizeHintOpt::MetadataSizeHint(size as u64)
717            }),
718            pushdown_filters: global_options.global.pushdown_filters,
719            reorder_filters: global_options.global.reorder_filters,
720            force_filter_selections: global_options.global.force_filter_selections,
721            data_pagesize_limit: global_options.global.data_pagesize_limit as u64,
722            write_batch_size: global_options.global.write_batch_size as u64,
723            writer_version: global_options.global.writer_version.to_string(),
724            compression_opt: global_options.global.compression.map(|compression| {
725                parquet_options::CompressionOpt::Compression(compression)
726            }),
727            dictionary_enabled_opt: global_options.global.dictionary_enabled.map(|enabled| {
728                parquet_options::DictionaryEnabledOpt::DictionaryEnabled(enabled)
729            }),
730            dictionary_page_size_limit: global_options.global.dictionary_page_size_limit as u64,
731            statistics_enabled_opt: global_options.global.statistics_enabled.map(|enabled| {
732                parquet_options::StatisticsEnabledOpt::StatisticsEnabled(enabled)
733            }),
734            max_row_group_size: global_options.global.max_row_group_size as u64,
735            max_in_list_size: global_options.global.max_in_list_size as u64,
736            created_by: global_options.global.created_by.clone(),
737            column_index_truncate_length_opt: global_options.global.column_index_truncate_length.map(|length| {
738                parquet_options::ColumnIndexTruncateLengthOpt::ColumnIndexTruncateLength(length as u64)
739            }),
740            statistics_truncate_length_opt: global_options.global.statistics_truncate_length.map(|length| {
741                parquet_options::StatisticsTruncateLengthOpt::StatisticsTruncateLength(length as u64)
742            }),
743            data_page_row_count_limit: global_options.global.data_page_row_count_limit as u64,
744            encoding_opt: global_options.global.encoding.map(|encoding| {
745                parquet_options::EncodingOpt::Encoding(encoding)
746            }),
747            bloom_filter_on_read: global_options.global.bloom_filter_on_read,
748            bloom_filter_on_write: global_options.global.bloom_filter_on_write,
749            bloom_filter_fpp_opt: global_options.global.bloom_filter_fpp.map(|fpp| {
750                parquet_options::BloomFilterFppOpt::BloomFilterFpp(fpp)
751            }),
752            bloom_filter_ndv_opt: global_options.global.bloom_filter_ndv.map(|ndv| {
753                parquet_options::BloomFilterNdvOpt::BloomFilterNdv(ndv)
754            }),
755            allow_single_file_parallelism: global_options.global.allow_single_file_parallelism,
756            maximum_parallel_row_group_writers: global_options.global.maximum_parallel_row_group_writers as u64,
757            maximum_buffered_record_batches_per_stream: global_options.global.maximum_buffered_record_batches_per_stream as u64,
758            schema_force_view_types: global_options.global.schema_force_view_types,
759            binary_as_string: global_options.global.binary_as_string,
760            skip_arrow_metadata: global_options.global.skip_arrow_metadata,
761            coerce_int96_opt: global_options.global.coerce_int96.map(|compression| {
762                parquet_options::CoerceInt96Opt::CoerceInt96(compression)
763            }),
764            coerce_int96_tz_opt: global_options.global.coerce_int96_tz.map(|tz| {
765                parquet_options::CoerceInt96TzOpt::CoerceInt96Tz(tz)
766            }),
767            max_predicate_cache_size_opt: global_options.global.max_predicate_cache_size.map(|size| {
768                parquet_options::MaxPredicateCacheSizeOpt::MaxPredicateCacheSize(size as u64)
769            }),
770            max_row_group_bytes_opt: global_options.global.max_row_group_bytes.map(|size| {
771                parquet_options::MaxRowGroupBytesOpt::MaxRowGroupBytes(size.get() as u64)
772            }),
773            content_defined_chunking: Some(protobuf::ParquetCdcOptions {
774                enabled: global_options.global.content_defined_chunking.enabled,
775                min_chunk_size: global_options.global.content_defined_chunking.min_chunk_size as u64,
776                max_chunk_size: global_options.global.content_defined_chunking.max_chunk_size as u64,
777                norm_level: global_options.global.content_defined_chunking.norm_level,
778            }),
779        }),
780        column_specific_options: column_specific_options.into_iter().map(|(column_name, options)| {
781            protobuf::ParquetColumnSpecificOptions {
782                column_name,
783                options: Some(protobuf::ParquetColumnOptions {
784                    bloom_filter_enabled_opt: options.bloom_filter_enabled.map(|enabled| {
785                        parquet_column_options::BloomFilterEnabledOpt::BloomFilterEnabled(enabled)
786                    }),
787                    encoding_opt: options.encoding.map(|encoding| {
788                        parquet_column_options::EncodingOpt::Encoding(encoding)
789                    }),
790                    dictionary_enabled_opt: options.dictionary_enabled.map(|enabled| {
791                        parquet_column_options::DictionaryEnabledOpt::DictionaryEnabled(enabled)
792                    }),
793                    compression_opt: options.compression.map(|compression| {
794                        parquet_column_options::CompressionOpt::Compression(compression)
795                    }),
796                    statistics_enabled_opt: options.statistics_enabled.map(|enabled| {
797                        parquet_column_options::StatisticsEnabledOpt::StatisticsEnabled(enabled)
798                    }),
799                    bloom_filter_fpp_opt: options.bloom_filter_fpp.map(|fpp| {
800                        parquet_column_options::BloomFilterFppOpt::BloomFilterFpp(fpp)
801                    }),
802                    bloom_filter_ndv_opt: options.bloom_filter_ndv.map(|ndv| {
803                        parquet_column_options::BloomFilterNdvOpt::BloomFilterNdv(ndv)
804                    }),
805                })
806            }
807        }).collect(),
808        key_value_metadata: global_options.key_value_metadata
809            .iter()
810            .filter_map(|(key, value)| {
811                value.as_ref().map(|v| (key.clone(), v.clone()))
812            })
813            .collect(),
814    }
815    }
816}