1use std::fmt;
21use std::fmt::Debug;
22use std::ops::Range;
23use std::sync::Arc;
24
25#[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)]
77pub struct ParquetFormatFactory {
79 pub options: Option<TableParquetOptions>,
81}
82
83impl ParquetFormatFactory {
84 pub fn new() -> Self {
86 Self { options: None }
87 }
88
89 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 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#[derive(Debug, Default)]
145pub struct ParquetFormat {
146 options: TableParquetOptions,
147}
148
149impl ParquetFormat {
150 pub fn new() -> Self {
152 Self::default()
153 }
154
155 pub fn with_enable_pruning(mut self, enable: bool) -> Self {
158 self.options.global.pruning = enable;
159 self
160 }
161
162 pub fn enable_pruning(&self) -> bool {
164 self.options.global.pruning
165 }
166
167 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 pub fn metadata_size_hint(&self) -> Option<usize> {
180 self.options.global.metadata_size_hint
181 }
182
183 pub fn with_skip_metadata(mut self, skip_metadata: bool) -> Self {
189 self.options.global.skip_metadata = skip_metadata;
190 self
191 }
192
193 pub fn skip_metadata(&self) -> bool {
196 self.options.global.skip_metadata
197 }
198
199 pub fn with_options(mut self, options: TableParquetOptions) -> Self {
201 self.options = options;
202 self
203 }
204
205 pub fn options(&self) -> &TableParquetOptions {
207 &self.options
208 }
209
210 pub fn force_view_types(&self) -> bool {
222 self.options.global.schema_force_view_types
223 }
224
225 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 pub fn binary_as_string(&self) -> bool {
240 self.options.global.binary_as_string
241 }
242
243 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
259fn 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()) })
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() .buffer_unordered(
373 state
374 .config_options()
375 .execution
376 .meta_fetch_concurrency
377 .get(),
378 )
379 .try_collect()
380 .await?;
381
382 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 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 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 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
606pub 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#[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#[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#[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}