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::{
28    Int96Coercer, apply_file_schema_type_coercions, coerce_file_schema_to_string_type,
29    coerce_file_schema_to_view_type, coerce_int96_to_resolution,
30    transform_binary_to_string, transform_schema_to_view,
31};
32pub use crate::sink::ParquetSink;
33
34use arrow::datatypes::{Fields, Schema, SchemaRef};
35use datafusion_datasource::TableSchema;
36use datafusion_datasource::file_compression_type::FileCompressionType;
37use datafusion_datasource::file_sink_config::FileSinkConfig;
38
39use datafusion_datasource::file_format::{FileFormat, FileFormatFactory};
40
41use datafusion_common::Statistics;
42use datafusion_common::config::{ConfigField, ConfigFileType, TableParquetOptions};
43use datafusion_common::encryption::FileDecryptionProperties;
44use datafusion_common::parsers::CompressionTypeVariant;
45use datafusion_common::{
46    DEFAULT_PARQUET_EXTENSION, DataFusionError, GetExt, Result, internal_datafusion_err,
47    internal_err, not_impl_err,
48};
49use datafusion_datasource::file::FileSource;
50use datafusion_datasource::file_scan_config::{FileScanConfig, FileScanConfigBuilder};
51use datafusion_datasource::sink::DataSinkExec;
52use datafusion_expr::dml::InsertOp;
53use datafusion_physical_expr_common::sort_expr::{LexOrdering, LexRequirement};
54use datafusion_physical_plan::ExecutionPlan;
55use datafusion_session::Session;
56
57use crate::metadata::{DFParquetMetadata, lex_ordering_to_sorting_columns};
58use crate::reader::CachedParquetFileReaderFactory;
59use crate::source::{
60    ParquetSource, parse_coerce_int96_string, parse_coerce_int96_tz_string,
61};
62use async_trait::async_trait;
63use bytes::Bytes;
64use datafusion_datasource::source::DataSourceExec;
65use datafusion_execution::cache::cache_manager::FileMetadataCache;
66use futures::future::BoxFuture;
67use futures::{FutureExt, StreamExt, TryStreamExt};
68use object_store::path::Path;
69use object_store::{ObjectMeta, ObjectStore, ObjectStoreExt};
70use parquet::arrow::async_reader::MetadataFetch;
71use parquet::errors::ParquetError;
72use parquet::file::metadata::ParquetMetaData;
73
74#[derive(Default)]
75/// Factory struct used to create [ParquetFormat]
76pub struct ParquetFormatFactory {
77    /// inner options for parquet
78    pub options: Option<TableParquetOptions>,
79}
80
81impl ParquetFormatFactory {
82    /// Creates an instance of [ParquetFormatFactory]
83    pub fn new() -> Self {
84        Self { options: None }
85    }
86
87    /// Creates an instance of [ParquetFormatFactory] with customized default options
88    pub fn new_with_options(options: TableParquetOptions) -> Self {
89        Self {
90            options: Some(options),
91        }
92    }
93}
94
95impl FileFormatFactory for ParquetFormatFactory {
96    fn create(
97        &self,
98        state: &dyn Session,
99        format_options: &std::collections::HashMap<String, String>,
100    ) -> Result<Arc<dyn FileFormat>> {
101        let parquet_options = match &self.options {
102            None => {
103                let mut table_options = state.default_table_options();
104                table_options.set_config_format(ConfigFileType::PARQUET);
105                table_options.alter_with_string_hash_map(format_options)?;
106                table_options.parquet
107            }
108            Some(parquet_options) => {
109                let mut parquet_options = parquet_options.clone();
110                for (k, v) in format_options {
111                    parquet_options.set(k, v)?;
112                }
113                parquet_options
114            }
115        };
116
117        Ok(Arc::new(
118            ParquetFormat::default().with_options(parquet_options),
119        ))
120    }
121
122    fn default(&self) -> Arc<dyn FileFormat> {
123        Arc::new(ParquetFormat::default())
124    }
125}
126
127impl GetExt for ParquetFormatFactory {
128    fn get_ext(&self) -> String {
129        // Removes the dot, i.e. ".parquet" -> "parquet"
130        DEFAULT_PARQUET_EXTENSION[1..].to_string()
131    }
132}
133
134impl Debug for ParquetFormatFactory {
135    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136        f.debug_struct("ParquetFormatFactory")
137            .field("ParquetFormatFactory", &self.options)
138            .finish()
139    }
140}
141/// The Apache Parquet `FileFormat` implementation
142#[derive(Debug, Default)]
143pub struct ParquetFormat {
144    options: TableParquetOptions,
145}
146
147impl ParquetFormat {
148    /// Construct a new Format with no local overrides
149    pub fn new() -> Self {
150        Self::default()
151    }
152
153    /// Activate statistics based row group level pruning
154    /// - If `None`, defaults to value on `config_options`
155    pub fn with_enable_pruning(mut self, enable: bool) -> Self {
156        self.options.global.pruning = enable;
157        self
158    }
159
160    /// Return `true` if pruning is enabled
161    pub fn enable_pruning(&self) -> bool {
162        self.options.global.pruning
163    }
164
165    /// Provide a hint to the size of the file metadata. If a hint is provided
166    /// the reader will try and fetch the last `size_hint` bytes of the parquet file optimistically.
167    /// Without a hint, two read are required. One read to fetch the 8-byte parquet footer and then
168    /// another read to fetch the metadata length encoded in the footer.
169    ///
170    /// - If `None`, defaults to value on `config_options`
171    pub fn with_metadata_size_hint(mut self, size_hint: Option<usize>) -> Self {
172        self.options.global.metadata_size_hint = size_hint;
173        self
174    }
175
176    /// Return the metadata size hint if set
177    pub fn metadata_size_hint(&self) -> Option<usize> {
178        self.options.global.metadata_size_hint
179    }
180
181    /// Tell the parquet reader to skip any metadata that may be in
182    /// the file Schema. This can help avoid schema conflicts due to
183    /// metadata.
184    ///
185    /// - If `None`, defaults to value on `config_options`
186    pub fn with_skip_metadata(mut self, skip_metadata: bool) -> Self {
187        self.options.global.skip_metadata = skip_metadata;
188        self
189    }
190
191    /// Returns `true` if schema metadata will be cleared prior to
192    /// schema merging.
193    pub fn skip_metadata(&self) -> bool {
194        self.options.global.skip_metadata
195    }
196
197    /// Set Parquet options for the ParquetFormat
198    pub fn with_options(mut self, options: TableParquetOptions) -> Self {
199        self.options = options;
200        self
201    }
202
203    /// Parquet options
204    pub fn options(&self) -> &TableParquetOptions {
205        &self.options
206    }
207
208    /// Return `true` if should use view types.
209    ///
210    /// If this returns true, DataFusion will instruct the parquet reader
211    /// to read string / binary columns using view `StringView` or `BinaryView`
212    /// if the table schema specifies those types, regardless of any embedded metadata
213    /// that may specify an alternate Arrow type. The parquet reader is optimized
214    /// for reading `StringView` and `BinaryView` and such queries are significantly faster.
215    ///
216    /// If this returns false, the parquet reader will read the columns according to the
217    /// defaults or any embedded Arrow type information. This may result in reading
218    /// `StringArrays` and then casting to `StringViewArray` which is less efficient.
219    pub fn force_view_types(&self) -> bool {
220        self.options.global.schema_force_view_types
221    }
222
223    /// If true, will use view types. See [`Self::force_view_types`] for details
224    pub fn with_force_view_types(mut self, use_views: bool) -> Self {
225        self.options.global.schema_force_view_types = use_views;
226        self
227    }
228
229    /// Return `true` if binary types will be read as strings.
230    ///
231    /// If this returns true, DataFusion will instruct the parquet reader
232    /// to read binary columns such as `Binary` or `BinaryView` as the
233    /// corresponding string type such as `Utf8` or `LargeUtf8`.
234    /// The parquet reader has special optimizations for `Utf8` and `LargeUtf8`
235    /// validation, and such queries are significantly faster than reading
236    /// binary columns and then casting to string columns.
237    pub fn binary_as_string(&self) -> bool {
238        self.options.global.binary_as_string
239    }
240
241    /// If true, will read binary types as strings. See [`Self::binary_as_string`] for details
242    pub fn with_binary_as_string(mut self, binary_as_string: bool) -> Self {
243        self.options.global.binary_as_string = binary_as_string;
244        self
245    }
246
247    pub fn coerce_int96(&self) -> Option<String> {
248        self.options.global.coerce_int96.clone()
249    }
250
251    pub fn with_coerce_int96(mut self, time_unit: Option<String>) -> Self {
252        self.options.global.coerce_int96 = time_unit;
253        self
254    }
255}
256
257/// Clears all metadata (Schema level and field level) on an iterator
258/// of Schemas
259fn clear_metadata(
260    schemas: impl IntoIterator<Item = Schema>,
261) -> impl Iterator<Item = Schema> {
262    schemas.into_iter().map(|schema| {
263        let fields = schema
264            .fields()
265            .iter()
266            .map(|field| {
267                field.as_ref().clone().with_metadata(Default::default()) // clear meta
268            })
269            .collect::<Fields>();
270        Schema::new(fields)
271    })
272}
273
274#[cfg(feature = "parquet_encryption")]
275async fn get_file_decryption_properties(
276    state: &dyn Session,
277    options: &TableParquetOptions,
278    file_path: &Path,
279) -> Result<Option<Arc<FileDecryptionProperties>>> {
280    Ok(match &options.crypto.file_decryption {
281        Some(cfd) => Some(Arc::new(FileDecryptionProperties::try_from(cfd.clone())?)),
282        None => match &options.crypto.factory_id {
283            Some(factory_id) => {
284                let factory =
285                    state.runtime_env().parquet_encryption_factory(factory_id)?;
286                factory
287                    .get_file_decryption_properties(
288                        &options.crypto.factory_options,
289                        file_path,
290                    )
291                    .await?
292            }
293            None => None,
294        },
295    })
296}
297
298#[cfg(not(feature = "parquet_encryption"))]
299async fn get_file_decryption_properties(
300    _state: &dyn Session,
301    _options: &TableParquetOptions,
302    _file_path: &Path,
303) -> Result<Option<Arc<FileDecryptionProperties>>> {
304    Ok(None)
305}
306
307#[async_trait]
308impl FileFormat for ParquetFormat {
309    fn get_ext(&self) -> String {
310        ParquetFormatFactory::new().get_ext()
311    }
312
313    fn get_ext_with_compression(
314        &self,
315        file_compression_type: &FileCompressionType,
316    ) -> Result<String> {
317        let ext = self.get_ext();
318        match file_compression_type.get_variant() {
319            CompressionTypeVariant::UNCOMPRESSED => Ok(ext),
320            _ => internal_err!("Parquet FileFormat does not support compression."),
321        }
322    }
323
324    fn compression_type(&self) -> Option<FileCompressionType> {
325        None
326    }
327
328    async fn infer_schema(
329        &self,
330        state: &dyn Session,
331        store: &Arc<dyn ObjectStore>,
332        objects: &[ObjectMeta],
333    ) -> Result<SchemaRef> {
334        let coerce_int96 = match self.coerce_int96() {
335            Some(time_unit) => Some(parse_coerce_int96_string(time_unit.as_str())?),
336            None => None,
337        };
338        let coerce_int96_tz = self
339            .options
340            .global
341            .coerce_int96_tz
342            .as_ref()
343            .map(|tz| parse_coerce_int96_tz_string(tz))
344            .transpose()?;
345
346        let file_metadata_cache =
347            state.runtime_env().cache_manager.get_file_metadata_cache();
348
349        let mut schemas: Vec<_> = futures::stream::iter(objects)
350            .map(|object| async {
351                let file_decryption_properties = get_file_decryption_properties(
352                    state,
353                    &self.options,
354                    &object.location,
355                )
356                .await?;
357                let result = DFParquetMetadata::new(store.as_ref(), object)
358                    .with_metadata_size_hint(self.metadata_size_hint())
359                    .with_decryption_properties(file_decryption_properties)
360                    .with_file_metadata_cache(Some(Arc::clone(&file_metadata_cache)))
361                    .with_coerce_int96(coerce_int96)
362                    .with_coerce_int96_tz(coerce_int96_tz.clone())
363                    .fetch_schema_with_location()
364                    .await?;
365                Ok::<_, DataFusionError>(result)
366            })
367            .boxed() // Workaround https://github.com/rust-lang/rust/issues/64552
368            // fetch schemas concurrently, if requested
369            .buffer_unordered(state.config_options().execution.meta_fetch_concurrency)
370            .try_collect()
371            .await?;
372
373        // Schema inference adds fields based the order they are seen
374        // which depends on the order the files are processed. For some
375        // object stores (like local file systems) the order returned from list
376        // is not deterministic. Thus, to ensure deterministic schema inference
377        // sort the files first.
378        // https://github.com/apache/datafusion/pull/6629
379        schemas
380            .sort_unstable_by(|(location1, _), (location2, _)| location1.cmp(location2));
381
382        let schemas = schemas.into_iter().map(|(_, schema)| schema);
383
384        let schema = if self.skip_metadata() {
385            Schema::try_merge(clear_metadata(schemas))
386        } else {
387            Schema::try_merge(schemas)
388        }?;
389
390        let schema = if self.binary_as_string() {
391            transform_binary_to_string(&schema)
392        } else {
393            schema
394        };
395
396        let schema = if self.force_view_types() {
397            transform_schema_to_view(&schema)
398        } else {
399            schema
400        };
401
402        Ok(Arc::new(schema))
403    }
404
405    async fn infer_stats(
406        &self,
407        state: &dyn Session,
408        store: &Arc<dyn ObjectStore>,
409        table_schema: SchemaRef,
410        object: &ObjectMeta,
411    ) -> Result<Statistics> {
412        let file_decryption_properties =
413            get_file_decryption_properties(state, &self.options, &object.location)
414                .await?;
415        let file_metadata_cache =
416            state.runtime_env().cache_manager.get_file_metadata_cache();
417        DFParquetMetadata::new(store, object)
418            .with_metadata_size_hint(self.metadata_size_hint())
419            .with_decryption_properties(file_decryption_properties)
420            .with_file_metadata_cache(Some(file_metadata_cache))
421            .fetch_statistics(&table_schema)
422            .await
423    }
424
425    async fn infer_ordering(
426        &self,
427        state: &dyn Session,
428        store: &Arc<dyn ObjectStore>,
429        table_schema: SchemaRef,
430        object: &ObjectMeta,
431    ) -> Result<Option<LexOrdering>> {
432        let file_decryption_properties =
433            get_file_decryption_properties(state, &self.options, &object.location)
434                .await?;
435        let file_metadata_cache =
436            state.runtime_env().cache_manager.get_file_metadata_cache();
437        let metadata = DFParquetMetadata::new(store, object)
438            .with_metadata_size_hint(self.metadata_size_hint())
439            .with_decryption_properties(file_decryption_properties)
440            .with_file_metadata_cache(Some(file_metadata_cache))
441            .fetch_metadata()
442            .await?;
443        crate::metadata::ordering_from_parquet_metadata(&metadata, &table_schema)
444    }
445
446    async fn infer_stats_and_ordering(
447        &self,
448        state: &dyn Session,
449        store: &Arc<dyn ObjectStore>,
450        table_schema: SchemaRef,
451        object: &ObjectMeta,
452    ) -> Result<datafusion_datasource::file_format::FileMeta> {
453        let file_decryption_properties =
454            get_file_decryption_properties(state, &self.options, &object.location)
455                .await?;
456        let file_metadata_cache =
457            state.runtime_env().cache_manager.get_file_metadata_cache();
458        let metadata = DFParquetMetadata::new(store, object)
459            .with_metadata_size_hint(self.metadata_size_hint())
460            .with_decryption_properties(file_decryption_properties)
461            .with_file_metadata_cache(Some(file_metadata_cache))
462            .fetch_metadata()
463            .await?;
464        let statistics = DFParquetMetadata::statistics_from_parquet_metadata(
465            &metadata,
466            &table_schema,
467        )?;
468        let ordering =
469            crate::metadata::ordering_from_parquet_metadata(&metadata, &table_schema)?;
470        Ok(
471            datafusion_datasource::file_format::FileMeta::new(statistics)
472                .with_ordering(ordering),
473        )
474    }
475
476    async fn create_physical_plan(
477        &self,
478        state: &dyn Session,
479        conf: FileScanConfig,
480    ) -> Result<Arc<dyn ExecutionPlan>> {
481        let mut metadata_size_hint = None;
482
483        if let Some(metadata) = self.metadata_size_hint() {
484            metadata_size_hint = Some(metadata);
485        }
486
487        let mut source = conf
488            .file_source()
489            .downcast_ref::<ParquetSource>()
490            .cloned()
491            .ok_or_else(|| internal_datafusion_err!("Expected ParquetSource"))?;
492        source = source.with_table_parquet_options(self.options.clone());
493
494        // Use the CachedParquetFileReaderFactory
495        let metadata_cache = state.runtime_env().cache_manager.get_file_metadata_cache();
496        let store = state
497            .runtime_env()
498            .object_store(conf.object_store_url.clone())?;
499        let cached_parquet_read_factory =
500            Arc::new(CachedParquetFileReaderFactory::new(store, metadata_cache));
501        source = source.with_parquet_file_reader_factory(cached_parquet_read_factory);
502
503        if let Some(metadata_size_hint) = metadata_size_hint {
504            source = source.with_metadata_size_hint(metadata_size_hint)
505        }
506
507        source = self.set_source_encryption_factory(source, state)?;
508
509        let conf = FileScanConfigBuilder::from(conf)
510            .with_source(Arc::new(source))
511            .build();
512        Ok(DataSourceExec::from_data_source(conf))
513    }
514
515    async fn create_writer_physical_plan(
516        &self,
517        input: Arc<dyn ExecutionPlan>,
518        _state: &dyn Session,
519        conf: FileSinkConfig,
520        order_requirements: Option<LexRequirement>,
521    ) -> Result<Arc<dyn ExecutionPlan>> {
522        if conf.insert_op != InsertOp::Append {
523            return not_impl_err!("Overwrites are not implemented yet for Parquet");
524        }
525
526        // Convert ordering requirements to Parquet SortingColumns for file metadata
527        let sorting_columns = if let Some(ref requirements) = order_requirements {
528            let ordering: LexOrdering = requirements.clone().into();
529            // In cases like `COPY (... ORDER BY ...) TO ...` the ORDER BY clause
530            // may not be compatible with Parquet sorting columns (e.g. ordering on `random()`).
531            // So if we cannot create a Parquet sorting column from the ordering requirement,
532            // we skip setting sorting columns on the Parquet sink.
533            lex_ordering_to_sorting_columns(&ordering).ok()
534        } else {
535            None
536        };
537
538        let sink = Arc::new(
539            ParquetSink::new(conf, self.options.clone())
540                .with_sorting_columns(sorting_columns),
541        );
542
543        Ok(Arc::new(DataSinkExec::new(input, sink, order_requirements)) as _)
544    }
545
546    fn file_source(&self, table_schema: TableSchema) -> Arc<dyn FileSource> {
547        Arc::new(
548            ParquetSource::new(table_schema)
549                .with_table_parquet_options(self.options.clone()),
550        )
551    }
552}
553
554#[cfg(feature = "parquet_encryption")]
555impl ParquetFormat {
556    fn set_source_encryption_factory(
557        &self,
558        source: ParquetSource,
559        state: &dyn Session,
560    ) -> Result<ParquetSource> {
561        if let Some(encryption_factory_id) = &self.options.crypto.factory_id {
562            Ok(source.with_encryption_factory(
563                state
564                    .runtime_env()
565                    .parquet_encryption_factory(encryption_factory_id)?,
566            ))
567        } else {
568            Ok(source)
569        }
570    }
571}
572
573#[cfg(not(feature = "parquet_encryption"))]
574impl ParquetFormat {
575    fn set_source_encryption_factory(
576        &self,
577        source: ParquetSource,
578        _state: &dyn Session,
579    ) -> Result<ParquetSource> {
580        if let Some(encryption_factory_id) = &self.options.crypto.factory_id {
581            Err(DataFusionError::Configuration(format!(
582                "Parquet encryption factory id is set to '{encryption_factory_id}' but the parquet_encryption feature is disabled"
583            )))
584        } else {
585            Ok(source)
586        }
587    }
588}
589
590/// [`MetadataFetch`] adapter for reading bytes from an [`ObjectStore`]
591pub struct ObjectStoreFetch<'a> {
592    store: &'a dyn ObjectStore,
593    meta: &'a ObjectMeta,
594}
595
596impl<'a> ObjectStoreFetch<'a> {
597    pub fn new(store: &'a dyn ObjectStore, meta: &'a ObjectMeta) -> Self {
598        Self { store, meta }
599    }
600}
601
602impl MetadataFetch for ObjectStoreFetch<'_> {
603    fn fetch(&mut self, range: Range<u64>) -> BoxFuture<'_, Result<Bytes, ParquetError>> {
604        async {
605            self.store
606                .get_range(&self.meta.location, range)
607                .await
608                .map_err(ParquetError::from)
609        }
610        .boxed()
611    }
612}
613
614/// Fetches parquet metadata from ObjectStore for given object
615///
616/// This component is a subject to **change** in near future and is exposed for low level integrations
617/// through [`ParquetFileReaderFactory`].
618///
619/// [`ParquetFileReaderFactory`]: crate::ParquetFileReaderFactory
620#[deprecated(
621    since = "50.0.0",
622    note = "Use `DFParquetMetadata::fetch_metadata` instead"
623)]
624pub async fn fetch_parquet_metadata(
625    store: &dyn ObjectStore,
626    object_meta: &ObjectMeta,
627    size_hint: Option<usize>,
628    decryption_properties: Option<&FileDecryptionProperties>,
629    file_metadata_cache: Option<Arc<dyn FileMetadataCache>>,
630) -> Result<Arc<ParquetMetaData>> {
631    let decryption_properties = decryption_properties.cloned().map(Arc::new);
632    DFParquetMetadata::new(store, object_meta)
633        .with_metadata_size_hint(size_hint)
634        .with_decryption_properties(decryption_properties)
635        .with_file_metadata_cache(file_metadata_cache)
636        .fetch_metadata()
637        .await
638}
639
640/// Read and parse the statistics of the Parquet file at location `path`
641///
642/// See [`statistics_from_parquet_meta_calc`] for more details
643#[deprecated(
644    since = "50.0.0",
645    note = "Use `DFParquetMetadata::fetch_statistics` instead"
646)]
647pub async fn fetch_statistics(
648    store: &dyn ObjectStore,
649    table_schema: SchemaRef,
650    file: &ObjectMeta,
651    metadata_size_hint: Option<usize>,
652    decryption_properties: Option<&FileDecryptionProperties>,
653    file_metadata_cache: Option<Arc<dyn FileMetadataCache>>,
654) -> Result<Statistics> {
655    let decryption_properties = decryption_properties.cloned().map(Arc::new);
656    DFParquetMetadata::new(store, file)
657        .with_metadata_size_hint(metadata_size_hint)
658        .with_decryption_properties(decryption_properties)
659        .with_file_metadata_cache(file_metadata_cache)
660        .fetch_statistics(&table_schema)
661        .await
662}
663
664#[deprecated(
665    since = "50.0.0",
666    note = "Use `DFParquetMetadata::statistics_from_parquet_metadata` instead"
667)]
668#[expect(clippy::needless_pass_by_value)]
669pub fn statistics_from_parquet_meta_calc(
670    metadata: &ParquetMetaData,
671    table_schema: SchemaRef,
672) -> Result<Statistics> {
673    DFParquetMetadata::statistics_from_parquet_metadata(metadata, &table_schema)
674}