datafusion_datasource_parquet/
file_format.rs1use std::fmt;
21use std::fmt::Debug;
22use std::ops::Range;
23use std::sync::Arc;
24
25#[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)]
75pub struct ParquetFormatFactory {
77 pub options: Option<TableParquetOptions>,
79}
80
81impl ParquetFormatFactory {
82 pub fn new() -> Self {
84 Self { options: None }
85 }
86
87 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 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#[derive(Debug, Default)]
143pub struct ParquetFormat {
144 options: TableParquetOptions,
145}
146
147impl ParquetFormat {
148 pub fn new() -> Self {
150 Self::default()
151 }
152
153 pub fn with_enable_pruning(mut self, enable: bool) -> Self {
156 self.options.global.pruning = enable;
157 self
158 }
159
160 pub fn enable_pruning(&self) -> bool {
162 self.options.global.pruning
163 }
164
165 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 pub fn metadata_size_hint(&self) -> Option<usize> {
178 self.options.global.metadata_size_hint
179 }
180
181 pub fn with_skip_metadata(mut self, skip_metadata: bool) -> Self {
187 self.options.global.skip_metadata = skip_metadata;
188 self
189 }
190
191 pub fn skip_metadata(&self) -> bool {
194 self.options.global.skip_metadata
195 }
196
197 pub fn with_options(mut self, options: TableParquetOptions) -> Self {
199 self.options = options;
200 self
201 }
202
203 pub fn options(&self) -> &TableParquetOptions {
205 &self.options
206 }
207
208 pub fn force_view_types(&self) -> bool {
220 self.options.global.schema_force_view_types
221 }
222
223 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 pub fn binary_as_string(&self) -> bool {
238 self.options.global.binary_as_string
239 }
240
241 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
257fn 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()) })
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() .buffer_unordered(state.config_options().execution.meta_fetch_concurrency)
370 .try_collect()
371 .await?;
372
373 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 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 let sorting_columns = if let Some(ref requirements) = order_requirements {
528 let ordering: LexOrdering = requirements.clone().into();
529 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
590pub 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#[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#[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}