Skip to main content

parquet/file/
properties.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//! Configuration via [`WriterProperties`] and [`ReaderProperties`]
19use crate::basic::{Compression, Encoding};
20use crate::compression::{CodecOptions, CodecOptionsBuilder};
21#[cfg(feature = "encryption")]
22use crate::encryption::encrypt::FileEncryptionProperties;
23use crate::file::metadata::{KeyValue, SortingColumn};
24use crate::schema::types::ColumnPath;
25use std::str::FromStr;
26use std::{collections::HashMap, sync::Arc};
27
28/// Default value for [`WriterProperties::data_page_size_limit`]
29pub const DEFAULT_PAGE_SIZE: usize = 1024 * 1024;
30/// Default value for [`WriterProperties::write_batch_size`]
31pub const DEFAULT_WRITE_BATCH_SIZE: usize = 1024;
32/// Default value for [`WriterProperties::writer_version`]
33pub const DEFAULT_WRITER_VERSION: WriterVersion = WriterVersion::PARQUET_1_0;
34/// Default value for [`WriterProperties::compression`]
35pub const DEFAULT_COMPRESSION: Compression = Compression::UNCOMPRESSED;
36/// Default value for [`WriterProperties::dictionary_enabled`]
37pub const DEFAULT_DICTIONARY_ENABLED: bool = true;
38/// Default value for [`WriterProperties::dictionary_page_size_limit`]
39pub const DEFAULT_DICTIONARY_PAGE_SIZE_LIMIT: usize = DEFAULT_PAGE_SIZE;
40/// Default value for [`WriterProperties::data_page_row_count_limit`]
41pub const DEFAULT_DATA_PAGE_ROW_COUNT_LIMIT: usize = 20_000;
42/// Default value for [`WriterProperties::statistics_enabled`]
43pub const DEFAULT_STATISTICS_ENABLED: EnabledStatistics = EnabledStatistics::Page;
44/// Default value for [`WriterProperties::write_page_header_statistics`]
45pub const DEFAULT_WRITE_PAGE_HEADER_STATISTICS: bool = false;
46/// Default value for [`WriterProperties::max_row_group_row_count`]
47pub const DEFAULT_MAX_ROW_GROUP_ROW_COUNT: usize = 1024 * 1024;
48/// Default value for [`WriterProperties::bloom_filter_position`]
49pub const DEFAULT_BLOOM_FILTER_POSITION: BloomFilterPosition = BloomFilterPosition::AfterRowGroup;
50/// Default value for [`WriterProperties::created_by`]
51pub const DEFAULT_CREATED_BY: &str = concat!("parquet-rs version ", env!("CARGO_PKG_VERSION"));
52/// Default value for [`WriterProperties::column_index_truncate_length`]
53pub const DEFAULT_COLUMN_INDEX_TRUNCATE_LENGTH: Option<usize> = Some(64);
54/// Default value for [`BloomFilterProperties::fpp`]
55pub const DEFAULT_BLOOM_FILTER_FPP: f64 = 0.05;
56/// Default value for [`BloomFilterProperties::ndv`].
57///
58/// Note: this is only the fallback default used when constructing [`BloomFilterProperties`]
59/// directly. When using [`WriterPropertiesBuilder`], columns with bloom filters enabled
60/// but without an explicit NDV will have their NDV resolved at build time to
61/// [`WriterProperties::max_row_group_row_count`], which may differ from this constant
62/// if the user configured a custom row group size.
63pub const DEFAULT_BLOOM_FILTER_NDV: u64 = DEFAULT_MAX_ROW_GROUP_ROW_COUNT as u64;
64/// Default values for [`WriterProperties::statistics_truncate_length`]
65pub const DEFAULT_STATISTICS_TRUNCATE_LENGTH: Option<usize> = Some(64);
66/// Default value for [`WriterProperties::offset_index_disabled`]
67pub const DEFAULT_OFFSET_INDEX_DISABLED: bool = false;
68/// Default values for [`WriterProperties::coerce_types`]
69pub const DEFAULT_COERCE_TYPES: bool = false;
70/// Default value for [`WriterProperties::data_page_v2_compression_ratio_threshold`]
71pub const DEFAULT_DATA_PAGE_V2_COMPRESSION_RATIO_THRESHOLD: f64 = 1.0;
72/// Default minimum chunk size for content-defined chunking: 256 KiB.
73pub const DEFAULT_CDC_MIN_CHUNK_SIZE: usize = 256 * 1024;
74/// Default maximum chunk size for content-defined chunking: 1024 KiB.
75pub const DEFAULT_CDC_MAX_CHUNK_SIZE: usize = 1024 * 1024;
76/// Default normalization level for content-defined chunking.
77pub const DEFAULT_CDC_NORM_LEVEL: i32 = 0;
78
79/// EXPERIMENTAL: Options for content-defined chunking (CDC).
80///
81/// Content-defined chunking is an experimental feature that optimizes parquet
82/// files for content addressable storage (CAS) systems by writing data pages
83/// according to content-defined chunk boundaries. This allows for more
84/// efficient deduplication of data across files, hence more efficient network
85/// transfers and storage.
86///
87/// Each content-defined chunk is written as a separate parquet data page. The
88/// following options control the chunks' size and the chunking process. Note
89/// that the chunk size is calculated based on the logical value of the data,
90/// before any encoding or compression is applied.
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub struct CdcOptions {
93    /// Minimum chunk size in bytes, default is 256 KiB.
94    /// The rolling hash will not be updated until this size is reached for each chunk.
95    /// Note that all data sent through the hash function is counted towards the chunk
96    /// size, including definition and repetition levels if present.
97    pub min_chunk_size: usize,
98    /// Maximum chunk size in bytes, default is 1024 KiB.
99    /// The chunker will create a new chunk whenever the chunk size exceeds this value.
100    /// Note that the parquet writer has a related [`data_page_size_limit`] property that
101    /// controls the maximum size of a parquet data page after encoding. While setting
102    /// `data_page_size_limit` to a smaller value than `max_chunk_size` doesn't affect
103    /// the chunking effectiveness, it results in more small parquet data pages.
104    ///
105    /// [`data_page_size_limit`]: WriterPropertiesBuilder::set_data_page_size_limit
106    pub max_chunk_size: usize,
107    /// Number of bit adjustment to the gearhash mask in order to center the chunk size
108    /// around the average size more aggressively, default is 0.
109    /// Increasing the normalization level increases the probability of finding a chunk,
110    /// improving the deduplication ratio, but also increasing the number of small chunks
111    /// resulting in many small parquet data pages. The default value provides a good
112    /// balance between deduplication ratio and fragmentation.
113    /// Use norm_level=1 or norm_level=2 to reach a higher deduplication ratio at the
114    /// expense of fragmentation. Negative values can also be used to reduce the
115    /// probability of finding a chunk, resulting in larger chunks and fewer data pages.
116    /// Note that values outside [-3, 3] are not recommended, prefer using the default
117    /// value of 0 for most use cases.
118    pub norm_level: i32,
119}
120
121impl Default for CdcOptions {
122    fn default() -> Self {
123        Self {
124            min_chunk_size: DEFAULT_CDC_MIN_CHUNK_SIZE,
125            max_chunk_size: DEFAULT_CDC_MAX_CHUNK_SIZE,
126            norm_level: DEFAULT_CDC_NORM_LEVEL,
127        }
128    }
129}
130
131/// Parquet writer version.
132///
133/// Basic constant, which is not part of the Thrift definition.
134#[derive(Debug, Clone, Copy, PartialEq, Eq)]
135#[allow(non_camel_case_types)]
136pub enum WriterVersion {
137    /// Parquet format version 1.0
138    PARQUET_1_0,
139    /// Parquet format version 2.0
140    PARQUET_2_0,
141}
142
143impl WriterVersion {
144    /// Returns writer version as `i32`.
145    pub fn as_num(&self) -> i32 {
146        match self {
147            WriterVersion::PARQUET_1_0 => 1,
148            WriterVersion::PARQUET_2_0 => 2,
149        }
150    }
151}
152
153impl FromStr for WriterVersion {
154    type Err = String;
155
156    fn from_str(s: &str) -> Result<Self, Self::Err> {
157        match s {
158            "PARQUET_1_0" | "parquet_1_0" => Ok(WriterVersion::PARQUET_1_0),
159            "PARQUET_2_0" | "parquet_2_0" => Ok(WriterVersion::PARQUET_2_0),
160            _ => Err(format!("Invalid writer version: {s}")),
161        }
162    }
163}
164
165/// Where in the file [`ArrowWriter`](crate::arrow::arrow_writer::ArrowWriter) should
166/// write Bloom filters
167///
168/// Basic constant, which is not part of the Thrift definition.
169#[derive(Debug, Clone, Copy, PartialEq, Eq)]
170pub enum BloomFilterPosition {
171    /// Write Bloom Filters of each row group right after the row group
172    ///
173    /// This saves memory by writing it as soon as it is computed, at the cost
174    /// of data locality for readers
175    AfterRowGroup,
176    /// Write Bloom Filters at the end of the file
177    ///
178    /// This allows better data locality for readers, at the cost of memory usage
179    /// for writers.
180    End,
181}
182
183/// Reference counted writer properties.
184pub type WriterPropertiesPtr = Arc<WriterProperties>;
185
186/// Resolved state of [`WriterPropertiesBuilder::set_offset_index_disabled`].
187///
188/// When a user disables offset indexes but page-level statistics are enabled,
189/// the setting is overridden (offset indexes remain enabled). This enum
190/// preserves the user's original intent so that a round-trip through
191/// `WriterPropertiesBuilder` does not lose it.
192#[derive(Debug, Clone, Copy, PartialEq, Eq)]
193enum OffsetIndexSetting {
194    /// Offset indexes are enabled (the default).
195    Enabled,
196    /// User disabled offset indexes and no page-level statistics override it.
197    Disabled,
198    /// User disabled offset indexes, but page-level statistics require them,
199    /// so they remain enabled.
200    DisabledOverridden,
201}
202
203/// Configuration settings for writing parquet files.
204///
205/// Use [`Self::builder`] to create a [`WriterPropertiesBuilder`] to change settings.
206///
207/// # Example
208///
209/// ```rust
210/// # use parquet::{
211/// #    basic::{Compression, Encoding},
212/// #    file::properties::*,
213/// #    schema::types::ColumnPath,
214/// # };
215/// #
216/// // Create properties with default configuration.
217/// let props = WriterProperties::default();
218///
219/// // Use properties builder to set certain options and assemble the configuration.
220/// let props = WriterProperties::builder()
221///     .set_writer_version(WriterVersion::PARQUET_1_0)
222///     .set_encoding(Encoding::PLAIN)
223///     .set_column_encoding(ColumnPath::from("col1"), Encoding::DELTA_BINARY_PACKED)
224///     .set_compression(Compression::SNAPPY)
225///     .build();
226///
227/// assert_eq!(props.writer_version(), WriterVersion::PARQUET_1_0);
228/// assert_eq!(
229///     props.encoding(&ColumnPath::from("col1")),
230///     Some(Encoding::DELTA_BINARY_PACKED)
231/// );
232/// assert_eq!(
233///     props.encoding(&ColumnPath::from("col2")),
234///     Some(Encoding::PLAIN)
235/// );
236/// ```
237#[derive(Debug, Clone)]
238pub struct WriterProperties {
239    data_page_row_count_limit: usize,
240    write_batch_size: usize,
241    max_row_group_row_count: Option<usize>,
242    max_row_group_bytes: Option<usize>,
243    bloom_filter_position: BloomFilterPosition,
244    writer_version: WriterVersion,
245    created_by: String,
246    offset_index_setting: OffsetIndexSetting,
247    pub(crate) key_value_metadata: Option<Vec<KeyValue>>,
248    default_column_properties: ColumnProperties,
249    column_properties: HashMap<ColumnPath, ColumnProperties>,
250    sorting_columns: Option<Vec<SortingColumn>>,
251    column_index_truncate_length: Option<usize>,
252    statistics_truncate_length: Option<usize>,
253    coerce_types: bool,
254    content_defined_chunking: Option<CdcOptions>,
255    #[cfg(feature = "encryption")]
256    pub(crate) file_encryption_properties: Option<Arc<FileEncryptionProperties>>,
257}
258
259impl Default for WriterProperties {
260    fn default() -> Self {
261        Self::builder().build()
262    }
263}
264
265impl WriterProperties {
266    /// Create a new [`WriterProperties`] with the default settings
267    ///
268    /// See [`WriterProperties::builder`] for customising settings
269    pub fn new() -> Self {
270        Self::default()
271    }
272
273    /// Returns a new default [`WriterPropertiesBuilder`] for creating writer
274    /// properties.
275    pub fn builder() -> WriterPropertiesBuilder {
276        WriterPropertiesBuilder::default()
277    }
278
279    /// Converts this [`WriterProperties`] into a [`WriterPropertiesBuilder`]
280    /// Used for mutating existing property settings
281    pub fn into_builder(self) -> WriterPropertiesBuilder {
282        self.into()
283    }
284
285    /// Returns data page size limit.
286    ///
287    /// Note: this is a best effort limit based on the write batch size
288    ///
289    /// For more details see [`WriterPropertiesBuilder::set_data_page_size_limit`]
290    pub fn data_page_size_limit(&self) -> usize {
291        self.default_column_properties
292            .data_page_size_limit()
293            .unwrap_or(DEFAULT_PAGE_SIZE)
294    }
295
296    /// Returns data page size limit for a specific column.
297    ///
298    /// Takes precedence over [`Self::data_page_size_limit`].
299    ///
300    /// Note: this is a best effort limit based on the write batch size.
301    pub fn column_data_page_size_limit(&self, col: &ColumnPath) -> usize {
302        self.column_properties
303            .get(col)
304            .and_then(|c| c.data_page_size_limit())
305            .or_else(|| self.default_column_properties.data_page_size_limit())
306            .unwrap_or(DEFAULT_PAGE_SIZE)
307    }
308
309    /// Returns dictionary page size limit.
310    ///
311    /// Note: this is a best effort limit based on the write batch size
312    ///
313    /// For more details see [`WriterPropertiesBuilder::set_dictionary_page_size_limit`]
314    pub fn dictionary_page_size_limit(&self) -> usize {
315        self.default_column_properties
316            .dictionary_page_size_limit()
317            .unwrap_or(DEFAULT_DICTIONARY_PAGE_SIZE_LIMIT)
318    }
319
320    /// Returns dictionary page size limit for a specific column.
321    pub fn column_dictionary_page_size_limit(&self, col: &ColumnPath) -> usize {
322        self.column_properties
323            .get(col)
324            .and_then(|c| c.dictionary_page_size_limit())
325            .or_else(|| self.default_column_properties.dictionary_page_size_limit())
326            .unwrap_or(DEFAULT_DICTIONARY_PAGE_SIZE_LIMIT)
327    }
328
329    /// Returns the maximum page row count
330    ///
331    /// Note: this is a best effort limit based on the write batch size
332    ///
333    /// For more details see [`WriterPropertiesBuilder::set_data_page_row_count_limit`]
334    pub fn data_page_row_count_limit(&self) -> usize {
335        self.data_page_row_count_limit
336    }
337
338    /// Returns configured batch size for writes.
339    ///
340    /// When writing a batch of data, this setting allows to split it internally into
341    /// smaller batches so we can better estimate the size of a page currently being
342    /// written.
343    ///
344    /// For more details see [`WriterPropertiesBuilder::set_write_batch_size`]
345    pub fn write_batch_size(&self) -> usize {
346        self.write_batch_size
347    }
348
349    /// Returns maximum number of rows in a row group, or `usize::MAX` if unlimited.
350    ///
351    /// For more details see [`WriterPropertiesBuilder::set_max_row_group_size`]
352    #[deprecated(since = "58.0.0", note = "Use `max_row_group_row_count` instead")]
353    pub fn max_row_group_size(&self) -> usize {
354        self.max_row_group_row_count.unwrap_or(usize::MAX)
355    }
356
357    /// Returns maximum number of rows in a row group, or `None` if unlimited.
358    ///
359    /// For more details see [`WriterPropertiesBuilder::set_max_row_group_row_count`]
360    pub fn max_row_group_row_count(&self) -> Option<usize> {
361        self.max_row_group_row_count
362    }
363
364    /// Returns maximum size of a row group in bytes, or `None` if unlimited.
365    ///
366    /// For more details see [`WriterPropertiesBuilder::set_max_row_group_bytes`]
367    pub fn max_row_group_bytes(&self) -> Option<usize> {
368        self.max_row_group_bytes
369    }
370
371    /// Returns bloom filter position.
372    ///
373    /// For more details see [`WriterPropertiesBuilder::set_bloom_filter_position`]
374    pub fn bloom_filter_position(&self) -> BloomFilterPosition {
375        self.bloom_filter_position
376    }
377
378    /// Returns configured writer version.
379    ///
380    /// For more details see [`WriterPropertiesBuilder::set_writer_version`]
381    pub fn writer_version(&self) -> WriterVersion {
382        self.writer_version
383    }
384
385    /// Returns `created_by` string.
386    ///
387    /// For more details see [`WriterPropertiesBuilder::set_created_by`]
388    pub fn created_by(&self) -> &str {
389        &self.created_by
390    }
391
392    /// Returns `true` if offset index writing is disabled.
393    ///
394    /// For more details see [`WriterPropertiesBuilder::set_offset_index_disabled`]
395    pub fn offset_index_disabled(&self) -> bool {
396        matches!(self.offset_index_setting, OffsetIndexSetting::Disabled)
397    }
398
399    /// Returns `key_value_metadata` KeyValue pairs.
400    ///
401    /// For more details see [`WriterPropertiesBuilder::set_key_value_metadata`]
402    pub fn key_value_metadata(&self) -> Option<&Vec<KeyValue>> {
403        self.key_value_metadata.as_ref()
404    }
405
406    /// Returns sorting columns.
407    ///
408    /// For more details see [`WriterPropertiesBuilder::set_sorting_columns`]
409    pub fn sorting_columns(&self) -> Option<&Vec<SortingColumn>> {
410        self.sorting_columns.as_ref()
411    }
412
413    /// Returns the maximum length of truncated min/max values in the column index.
414    ///
415    /// `None` if truncation is disabled, must be greater than 0 otherwise.
416    ///
417    /// For more details see [`WriterPropertiesBuilder::set_column_index_truncate_length`]
418    pub fn column_index_truncate_length(&self) -> Option<usize> {
419        self.column_index_truncate_length
420    }
421
422    /// Returns the maximum length of truncated min/max values in [`Statistics`].
423    ///
424    /// `None` if truncation is disabled, must be greater than 0 otherwise.
425    ///
426    /// For more details see [`WriterPropertiesBuilder::set_statistics_truncate_length`]
427    ///
428    /// [`Statistics`]: crate::file::statistics::Statistics
429    pub fn statistics_truncate_length(&self) -> Option<usize> {
430        self.statistics_truncate_length
431    }
432
433    /// Returns `true` if type coercion is enabled.
434    ///
435    /// For more details see [`WriterPropertiesBuilder::set_coerce_types`]
436    pub fn coerce_types(&self) -> bool {
437        self.coerce_types
438    }
439
440    /// EXPERIMENTAL: Returns content-defined chunking options, or `None` if CDC is disabled.
441    ///
442    /// For more details see [`WriterPropertiesBuilder::set_content_defined_chunking`]
443    pub fn content_defined_chunking(&self) -> Option<&CdcOptions> {
444        self.content_defined_chunking.as_ref()
445    }
446
447    /// Returns the compression ratio threshold at or above which a Data Page v2's
448    /// compressed values are discarded in favor of writing the values uncompressed.
449    ///
450    /// For more details see [`WriterPropertiesBuilder::set_data_page_v2_compression_ratio_threshold`]
451    pub fn data_page_v2_compression_ratio_threshold(&self) -> f64 {
452        self.default_column_properties
453            .data_page_v2_compression_ratio_threshold()
454            .unwrap_or(DEFAULT_DATA_PAGE_V2_COMPRESSION_RATIO_THRESHOLD)
455    }
456
457    /// Returns the Data Page v2 compression ratio threshold for a specific column.
458    ///
459    /// Takes precedence over [`Self::data_page_v2_compression_ratio_threshold`].
460    pub fn column_data_page_v2_compression_ratio_threshold(&self, col: &ColumnPath) -> f64 {
461        self.column_properties
462            .get(col)
463            .and_then(|c| c.data_page_v2_compression_ratio_threshold())
464            .or_else(|| {
465                self.default_column_properties
466                    .data_page_v2_compression_ratio_threshold()
467            })
468            .unwrap_or(DEFAULT_DATA_PAGE_V2_COMPRESSION_RATIO_THRESHOLD)
469    }
470
471    /// Returns encoding for a data page, when dictionary encoding is enabled.
472    ///
473    /// This is not configurable.
474    #[inline]
475    pub fn dictionary_data_page_encoding(&self) -> Encoding {
476        // PLAIN_DICTIONARY encoding is deprecated in writer version 1.
477        // Dictionary values are encoded using RLE_DICTIONARY encoding.
478        Encoding::RLE_DICTIONARY
479    }
480
481    /// Returns encoding for dictionary page, when dictionary encoding is enabled.
482    ///
483    /// This is not configurable.
484    #[inline]
485    pub fn dictionary_page_encoding(&self) -> Encoding {
486        // PLAIN_DICTIONARY is deprecated in writer version 1.
487        // Dictionary is encoded using plain encoding.
488        Encoding::PLAIN
489    }
490
491    /// Returns encoding for a column, if set.
492    ///
493    /// In case when dictionary is enabled, returns fallback encoding.
494    ///
495    /// If encoding is not set, then column writer will choose the best encoding
496    /// based on the column type.
497    pub fn encoding(&self, col: &ColumnPath) -> Option<Encoding> {
498        self.column_properties
499            .get(col)
500            .and_then(|c| c.encoding())
501            .or_else(|| self.default_column_properties.encoding())
502    }
503
504    /// Returns compression codec for a column.
505    ///
506    /// For more details see [`WriterPropertiesBuilder::set_column_compression`]
507    pub fn compression(&self, col: &ColumnPath) -> Compression {
508        self.column_properties
509            .get(col)
510            .and_then(|c| c.compression())
511            .or_else(|| self.default_column_properties.compression())
512            .unwrap_or(DEFAULT_COMPRESSION)
513    }
514
515    /// Returns `true` if dictionary encoding is enabled for a column.
516    ///
517    /// For more details see [`WriterPropertiesBuilder::set_dictionary_enabled`]
518    pub fn dictionary_enabled(&self, col: &ColumnPath) -> bool {
519        self.column_properties
520            .get(col)
521            .and_then(|c| c.dictionary_enabled())
522            .or_else(|| self.default_column_properties.dictionary_enabled())
523            .unwrap_or(DEFAULT_DICTIONARY_ENABLED)
524    }
525
526    /// Returns which statistics are written for a column.
527    ///
528    /// For more details see [`WriterPropertiesBuilder::set_statistics_enabled`]
529    pub fn statistics_enabled(&self, col: &ColumnPath) -> EnabledStatistics {
530        self.column_properties
531            .get(col)
532            .and_then(|c| c.statistics_enabled())
533            .or_else(|| self.default_column_properties.statistics_enabled())
534            .unwrap_or(DEFAULT_STATISTICS_ENABLED)
535    }
536
537    /// Returns `true` if [`Statistics`] are to be written to the page header for a column.
538    ///
539    /// For more details see [`WriterPropertiesBuilder::set_write_page_header_statistics`]
540    ///
541    /// [`Statistics`]: crate::file::statistics::Statistics
542    pub fn write_page_header_statistics(&self, col: &ColumnPath) -> bool {
543        self.column_properties
544            .get(col)
545            .and_then(|c| c.write_page_header_statistics())
546            .or_else(|| {
547                self.default_column_properties
548                    .write_page_header_statistics()
549            })
550            .unwrap_or(DEFAULT_WRITE_PAGE_HEADER_STATISTICS)
551    }
552
553    /// Returns the [`BloomFilterProperties`] for the given column
554    ///
555    /// Returns `None` if bloom filter is disabled
556    ///
557    /// For more details see [`WriterPropertiesBuilder::set_column_bloom_filter_enabled`]
558    pub fn bloom_filter_properties(&self, col: &ColumnPath) -> Option<&BloomFilterProperties> {
559        self.column_properties
560            .get(col)
561            .and_then(|c| c.bloom_filter_properties())
562            .or_else(|| self.default_column_properties.bloom_filter_properties())
563    }
564
565    /// Return file encryption properties
566    ///
567    /// For more details see [`WriterPropertiesBuilder::with_file_encryption_properties`]
568    #[cfg(feature = "encryption")]
569    pub fn file_encryption_properties(&self) -> Option<&Arc<FileEncryptionProperties>> {
570        self.file_encryption_properties.as_ref()
571    }
572}
573
574/// Builder for  [`WriterProperties`] Parquet writer configuration.
575///
576/// See example on [`WriterProperties`]
577#[derive(Debug, Clone)]
578pub struct WriterPropertiesBuilder {
579    data_page_row_count_limit: usize,
580    write_batch_size: usize,
581    max_row_group_row_count: Option<usize>,
582    max_row_group_bytes: Option<usize>,
583    bloom_filter_position: BloomFilterPosition,
584    writer_version: WriterVersion,
585    created_by: String,
586    offset_index_disabled: bool,
587    key_value_metadata: Option<Vec<KeyValue>>,
588    default_column_properties: ColumnProperties,
589    column_properties: HashMap<ColumnPath, ColumnProperties>,
590    sorting_columns: Option<Vec<SortingColumn>>,
591    column_index_truncate_length: Option<usize>,
592    statistics_truncate_length: Option<usize>,
593    coerce_types: bool,
594    content_defined_chunking: Option<CdcOptions>,
595    #[cfg(feature = "encryption")]
596    file_encryption_properties: Option<Arc<FileEncryptionProperties>>,
597}
598
599impl Default for WriterPropertiesBuilder {
600    /// Returns default state of the builder.
601    fn default() -> Self {
602        Self {
603            data_page_row_count_limit: DEFAULT_DATA_PAGE_ROW_COUNT_LIMIT,
604            write_batch_size: DEFAULT_WRITE_BATCH_SIZE,
605            max_row_group_row_count: Some(DEFAULT_MAX_ROW_GROUP_ROW_COUNT),
606            max_row_group_bytes: None,
607            bloom_filter_position: DEFAULT_BLOOM_FILTER_POSITION,
608            writer_version: DEFAULT_WRITER_VERSION,
609            created_by: DEFAULT_CREATED_BY.to_string(),
610            offset_index_disabled: DEFAULT_OFFSET_INDEX_DISABLED,
611            key_value_metadata: None,
612            default_column_properties: Default::default(),
613            column_properties: HashMap::new(),
614            sorting_columns: None,
615            column_index_truncate_length: DEFAULT_COLUMN_INDEX_TRUNCATE_LENGTH,
616            statistics_truncate_length: DEFAULT_STATISTICS_TRUNCATE_LENGTH,
617            coerce_types: DEFAULT_COERCE_TYPES,
618            content_defined_chunking: None,
619            #[cfg(feature = "encryption")]
620            file_encryption_properties: None,
621        }
622    }
623}
624
625impl WriterPropertiesBuilder {
626    /// Finalizes the configuration and returns immutable writer properties struct.
627    pub fn build(self) -> WriterProperties {
628        // Pre-compute offset_index_setting
629        let offset_index_setting = if self.offset_index_disabled {
630            let default_page_stats_enabled = self.default_column_properties.statistics_enabled()
631                == Some(EnabledStatistics::Page);
632            let column_page_stats_enabled = self.column_properties.iter().any(|path_props| {
633                path_props.1.statistics_enabled() == Some(EnabledStatistics::Page)
634            });
635            if default_page_stats_enabled || column_page_stats_enabled {
636                OffsetIndexSetting::DisabledOverridden
637            } else {
638                OffsetIndexSetting::Disabled
639            }
640        } else {
641            OffsetIndexSetting::Enabled
642        };
643
644        // Resolve bloom filter NDV for columns where it wasn't explicitly set:
645        // default to max_row_group_row_count so the filter is never undersized.
646        let default_ndv = self
647            .max_row_group_row_count
648            .unwrap_or(DEFAULT_MAX_ROW_GROUP_ROW_COUNT) as u64;
649        let mut default_column_properties = self.default_column_properties;
650        default_column_properties.resolve_bloom_filter_ndv(default_ndv);
651        let mut column_properties = self.column_properties;
652        for props in column_properties.values_mut() {
653            props.resolve_bloom_filter_ndv(default_ndv);
654        }
655
656        WriterProperties {
657            data_page_row_count_limit: self.data_page_row_count_limit,
658            write_batch_size: self.write_batch_size,
659            max_row_group_row_count: self.max_row_group_row_count,
660            max_row_group_bytes: self.max_row_group_bytes,
661            bloom_filter_position: self.bloom_filter_position,
662            writer_version: self.writer_version,
663            created_by: self.created_by,
664            offset_index_setting,
665            key_value_metadata: self.key_value_metadata,
666            default_column_properties,
667            column_properties,
668            sorting_columns: self.sorting_columns,
669            column_index_truncate_length: self.column_index_truncate_length,
670            statistics_truncate_length: self.statistics_truncate_length,
671            coerce_types: self.coerce_types,
672            content_defined_chunking: self.content_defined_chunking,
673            #[cfg(feature = "encryption")]
674            file_encryption_properties: self.file_encryption_properties,
675        }
676    }
677
678    // ----------------------------------------------------------------------
679    // Writer properties related to a file
680
681    /// Sets the `WriterVersion` written into the parquet metadata (defaults to [`PARQUET_1_0`]
682    /// via [`DEFAULT_WRITER_VERSION`])
683    ///
684    /// This value can determine what features some readers will support.
685    ///
686    /// [`PARQUET_1_0`]: [WriterVersion::PARQUET_1_0]
687    pub fn set_writer_version(mut self, value: WriterVersion) -> Self {
688        self.writer_version = value;
689        self
690    }
691
692    /// Sets best effort maximum number of rows in a data page (defaults to `20_000`
693    /// via [`DEFAULT_DATA_PAGE_ROW_COUNT_LIMIT`]).
694    ///
695    /// The parquet writer will attempt to limit the number of rows in
696    /// each `DataPage` to this value. Reducing this value will result
697    /// in larger parquet files, but may improve the effectiveness of
698    /// page index based predicate pushdown during reading.
699    ///
700    /// Note: this is a best effort limit based on value of
701    /// [`set_write_batch_size`](Self::set_write_batch_size).
702    pub fn set_data_page_row_count_limit(mut self, value: usize) -> Self {
703        self.data_page_row_count_limit = value;
704        self
705    }
706
707    /// Sets write batch size (defaults to 1024 via [`DEFAULT_WRITE_BATCH_SIZE`]).
708    ///
709    /// For performance reasons, data for each column is written in
710    /// batches of this size.
711    ///
712    /// Additional limits such as such as
713    /// [`set_data_page_row_count_limit`](Self::set_data_page_row_count_limit)
714    /// are checked between batches, and thus the write batch size value acts as an
715    /// upper-bound on the enforcement granularity of other limits.
716    pub fn set_write_batch_size(mut self, value: usize) -> Self {
717        self.write_batch_size = value;
718        self
719    }
720
721    /// Sets maximum number of rows in a row group (defaults to `1024 * 1024`
722    /// via [`DEFAULT_MAX_ROW_GROUP_ROW_COUNT`]).
723    ///
724    /// # Panics
725    /// If the value is set to 0.
726    #[deprecated(since = "58.0.0", note = "Use `set_max_row_group_row_count` instead")]
727    pub fn set_max_row_group_size(mut self, value: usize) -> Self {
728        assert!(value > 0, "Cannot have a 0 max row group size");
729        self.max_row_group_row_count = Some(value);
730        self
731    }
732
733    /// Sets maximum number of rows in a row group, or `None` for unlimited.
734    ///
735    /// If both `max_row_group_row_count` and `max_row_group_bytes` are set,
736    /// the row group with the smaller limit will be produced.
737    ///
738    /// # Panics
739    /// If the value is `Some(0)`.
740    pub fn set_max_row_group_row_count(mut self, value: Option<usize>) -> Self {
741        assert_ne!(value, Some(0), "Cannot have a 0 max row group row count");
742        self.max_row_group_row_count = value;
743        self
744    }
745
746    /// Sets maximum size of a row group in bytes, or `None` for unlimited.
747    ///
748    /// Row groups are flushed when their estimated encoded size exceeds this threshold.
749    /// This is similar to the official Java implementation for `parquet.block.size`'s behavior.
750    ///
751    /// If both `max_row_group_row_count` and `max_row_group_bytes` are set,
752    /// the row group with the smaller limit will be produced.
753    ///
754    /// # Panics
755    /// If the value is `Some(0)`.
756    pub fn set_max_row_group_bytes(mut self, value: Option<usize>) -> Self {
757        assert_ne!(value, Some(0), "Cannot have a 0 max row group bytes");
758        self.max_row_group_bytes = value;
759        self
760    }
761
762    /// Sets where in the final file Bloom Filters are written (defaults to  [`AfterRowGroup`]
763    /// via [`DEFAULT_BLOOM_FILTER_POSITION`])
764    ///
765    /// [`AfterRowGroup`]: BloomFilterPosition::AfterRowGroup
766    pub fn set_bloom_filter_position(mut self, value: BloomFilterPosition) -> Self {
767        self.bloom_filter_position = value;
768        self
769    }
770
771    /// Sets "created by" property (defaults to `parquet-rs version <VERSION>` via
772    /// [`DEFAULT_CREATED_BY`]).
773    ///
774    /// This is a string that will be written into the file metadata
775    pub fn set_created_by(mut self, value: String) -> Self {
776        self.created_by = value;
777        self
778    }
779
780    /// Sets whether the writing of offset indexes is disabled (defaults to `false` via
781    /// [`DEFAULT_OFFSET_INDEX_DISABLED`]).
782    ///
783    /// If statistics level is set to [`Page`] this setting will be overridden with `false`.
784    ///
785    /// Note: As the offset indexes are useful for accessing data by row number,
786    /// they are always written by default, regardless of whether other statistics
787    /// are enabled. Disabling this metadata may result in a degradation in read
788    /// performance, so use this option with care.
789    ///
790    /// [`Page`]: EnabledStatistics::Page
791    pub fn set_offset_index_disabled(mut self, value: bool) -> Self {
792        self.offset_index_disabled = value;
793        self
794    }
795
796    /// Sets "key_value_metadata" property (defaults to `None`).
797    pub fn set_key_value_metadata(mut self, value: Option<Vec<KeyValue>>) -> Self {
798        self.key_value_metadata = value;
799        self
800    }
801
802    /// Sets sorting order of rows in the row group if any (defaults to `None`).
803    pub fn set_sorting_columns(mut self, value: Option<Vec<SortingColumn>>) -> Self {
804        self.sorting_columns = value;
805        self
806    }
807
808    /// Sets the max length of min/max value fields when writing the column
809    /// [`Index`] (defaults to `Some(64)` via [`DEFAULT_COLUMN_INDEX_TRUNCATE_LENGTH`]).
810    ///
811    /// This can be used to prevent columns with very long values (hundreds of
812    /// bytes long) from causing the parquet metadata to become huge.
813    ///
814    /// # Notes
815    ///
816    /// The column [`Index`] is written when [`Self::set_statistics_enabled`] is
817    /// set to [`EnabledStatistics::Page`].
818    ///
819    /// * If `Some`, must be greater than 0, otherwise will panic
820    /// * If `None`, there's no effective limit.
821    ///
822    /// [`Index`]: crate::file::page_index::column_index::ColumnIndexMetaData
823    pub fn set_column_index_truncate_length(mut self, max_length: Option<usize>) -> Self {
824        if let Some(value) = max_length {
825            assert!(
826                value > 0,
827                "Cannot have a 0 column index truncate length. If you wish to disable min/max value truncation, set it to `None`."
828            );
829        }
830
831        self.column_index_truncate_length = max_length;
832        self
833    }
834
835    /// Sets the max length of min/max value fields in row group and data page header
836    /// [`Statistics`] (defaults to `Some(64)` via [`DEFAULT_STATISTICS_TRUNCATE_LENGTH`]).
837    ///
838    /// # Notes
839    /// Row group [`Statistics`] are written when [`Self::set_statistics_enabled`] is
840    /// set to [`EnabledStatistics::Chunk`] or [`EnabledStatistics::Page`]. Data page header
841    /// [`Statistics`] are written when [`Self::set_statistics_enabled`] is set to
842    /// [`EnabledStatistics::Page`].
843    ///
844    /// * If `Some`, must be greater than 0, otherwise will panic
845    /// * If `None`, there's no effective limit.
846    ///
847    /// # See also
848    /// Truncation of Page Index statistics is controlled separately via
849    /// [`WriterPropertiesBuilder::set_column_index_truncate_length`]
850    ///
851    /// [`Statistics`]: crate::file::statistics::Statistics
852    pub fn set_statistics_truncate_length(mut self, max_length: Option<usize>) -> Self {
853        if let Some(value) = max_length {
854            assert!(
855                value > 0,
856                "Cannot have a 0 statistics truncate length. If you wish to disable min/max value truncation, set it to `None`."
857            );
858        }
859
860        self.statistics_truncate_length = max_length;
861        self
862    }
863
864    /// Should the writer coerce types to parquet native types (defaults to `false` via
865    /// [`DEFAULT_COERCE_TYPES`]).
866    ///
867    /// Leaving this option the default `false` will ensure the exact same data
868    /// written to parquet using this library will be read.
869    ///
870    /// Setting this option to `true` will result in parquet files that can be
871    /// read by more readers, but potentially lose information in the process.
872    ///
873    /// * Types such as [`DataType::Date64`], which have no direct corresponding
874    ///   Parquet type, may be stored with lower precision.
875    ///
876    /// * The internal field names of `List` and `Map` types will be renamed if
877    ///   necessary to match what is required by the newest Parquet specification.
878    ///
879    /// See [`ArrowToParquetSchemaConverter::with_coerce_types`] for more details
880    ///
881    /// [`DataType::Date64`]: arrow_schema::DataType::Date64
882    /// [`ArrowToParquetSchemaConverter::with_coerce_types`]: crate::arrow::ArrowSchemaConverter::with_coerce_types
883    pub fn set_coerce_types(mut self, coerce_types: bool) -> Self {
884        self.coerce_types = coerce_types;
885        self
886    }
887
888    /// EXPERIMENTAL: Sets content-defined chunking options, or disables CDC with `None`.
889    ///
890    /// When enabled, data page boundaries are determined by a rolling hash of the
891    /// column values, so unchanged data produces identical byte sequences across
892    /// file versions. This enables efficient deduplication on content-addressable
893    /// storage systems.
894    ///
895    /// Only supported through the Arrow writer interface ([`ArrowWriter`]).
896    ///
897    /// # Panics
898    ///
899    /// Panics if `min_chunk_size == 0` or `max_chunk_size <= min_chunk_size`.
900    ///
901    /// [`ArrowWriter`]: crate::arrow::arrow_writer::ArrowWriter
902    pub fn set_content_defined_chunking(mut self, options: Option<CdcOptions>) -> Self {
903        if let Some(ref options) = options {
904            assert!(
905                options.min_chunk_size > 0,
906                "min_chunk_size must be positive"
907            );
908            assert!(
909                options.max_chunk_size > options.min_chunk_size,
910                "max_chunk_size ({}) must be greater than min_chunk_size ({})",
911                options.max_chunk_size,
912                options.min_chunk_size
913            );
914        }
915        self.content_defined_chunking = options;
916        self
917    }
918
919    /// Sets the default compression ratio threshold at or above which a Data Page
920    /// v2's compressed values are discarded in favor of writing the values
921    /// uncompressed, for all columns (defaults to `1.0` via
922    /// [`DEFAULT_DATA_PAGE_V2_COMPRESSION_RATIO_THRESHOLD`]).
923    ///
924    /// When writing a Data Page v2 with a configured compression codec, the writer
925    /// first compresses the values and then compares the compressed size to the
926    /// uncompressed size. If `compressed_size >= uncompressed_size * threshold`, the
927    /// compressed buffer is discarded and the values are written uncompressed for
928    /// that page (the page's `is_compressed` flag is set to `false`).
929    ///
930    /// The default of `1.0` preserves the historical behavior of only keeping
931    /// compression when it strictly reduces the size. Setting a value below `1.0`
932    /// requires a minimum amount of size reduction to keep the compressed page —
933    /// for example `0.9` requires at least a 10% reduction. Setting a value above
934    /// `1.0` keeps the compressed buffer even if it's somewhat larger than the
935    /// uncompressed values.
936    ///
937    /// This setting only affects Data Page v2; Data Page v1 always stores the
938    /// compressor's output regardless of the resulting size.
939    ///
940    /// # Panics
941    /// If `value` is not finite or is not strictly positive.
942    pub fn set_data_page_v2_compression_ratio_threshold(mut self, value: f64) -> Self {
943        self.default_column_properties
944            .set_data_page_v2_compression_ratio_threshold(value);
945        self
946    }
947
948    /// Sets FileEncryptionProperties (defaults to `None`)
949    #[cfg(feature = "encryption")]
950    pub fn with_file_encryption_properties(
951        mut self,
952        file_encryption_properties: Arc<FileEncryptionProperties>,
953    ) -> Self {
954        self.file_encryption_properties = Some(file_encryption_properties);
955        self
956    }
957
958    // ----------------------------------------------------------------------
959    // Setters for any column (global)
960
961    /// Sets default encoding for all columns.
962    ///
963    /// If dictionary is not enabled, this is treated as a primary encoding for all
964    /// columns. In case when dictionary is enabled for any column, this value is
965    /// considered to be a fallback encoding for that column.
966    ///
967    /// # Panics
968    ///
969    /// if dictionary encoding is specified, regardless of dictionary
970    /// encoding flag being set.
971    pub fn set_encoding(mut self, value: Encoding) -> Self {
972        self.default_column_properties.set_encoding(value);
973        self
974    }
975
976    /// Sets default compression codec for all columns (default to [`UNCOMPRESSED`] via
977    /// [`DEFAULT_COMPRESSION`]).
978    ///
979    /// [`UNCOMPRESSED`]: Compression::UNCOMPRESSED
980    pub fn set_compression(mut self, value: Compression) -> Self {
981        self.default_column_properties.set_compression(value);
982        self
983    }
984
985    /// Sets default flag to enable/disable dictionary encoding for all columns (defaults to `true`
986    /// via [`DEFAULT_DICTIONARY_ENABLED`]).
987    ///
988    /// Use this method to set dictionary encoding, instead of explicitly specifying
989    /// encoding in `set_encoding` method.
990    pub fn set_dictionary_enabled(mut self, value: bool) -> Self {
991        self.default_column_properties.set_dictionary_enabled(value);
992        self
993    }
994
995    /// Sets best effort maximum dictionary page size, in bytes (defaults to `1024 * 1024`
996    /// via [`DEFAULT_DICTIONARY_PAGE_SIZE_LIMIT`]).
997    ///
998    /// The parquet writer will attempt to limit the size of each
999    /// `DataPage` used to store dictionaries to this many
1000    /// bytes. Reducing this value will result in larger parquet
1001    /// files, but may improve the effectiveness of page index based
1002    /// predicate pushdown during reading.
1003    ///
1004    /// Note: this is a best effort limit based on value of
1005    /// [`set_write_batch_size`](Self::set_write_batch_size).
1006    pub fn set_dictionary_page_size_limit(mut self, value: usize) -> Self {
1007        self.default_column_properties
1008            .set_dictionary_page_size_limit(value);
1009        self
1010    }
1011
1012    /// Sets best effort maximum size of a data page in bytes (defaults to `1024 * 1024`
1013    /// via [`DEFAULT_PAGE_SIZE`]).
1014    ///
1015    /// The parquet writer will attempt to limit the sizes of each
1016    /// `DataPage` to this many bytes. Reducing this value will result
1017    /// in larger parquet files, but may improve the effectiveness of
1018    /// page index based predicate pushdown during reading.
1019    ///
1020    /// Note: this is a best effort limit based on value of
1021    /// [`set_write_batch_size`](Self::set_write_batch_size).
1022    pub fn set_data_page_size_limit(mut self, value: usize) -> Self {
1023        self.default_column_properties
1024            .set_data_page_size_limit(value);
1025        self
1026    }
1027
1028    /// Sets default [`EnabledStatistics`] level for all columns (defaults to [`Page`] via
1029    /// [`DEFAULT_STATISTICS_ENABLED`]).
1030    ///
1031    /// [`Page`]: EnabledStatistics::Page
1032    pub fn set_statistics_enabled(mut self, value: EnabledStatistics) -> Self {
1033        self.default_column_properties.set_statistics_enabled(value);
1034        self
1035    }
1036
1037    /// enable/disable writing [`Statistics`] in the page header
1038    /// (defaults to `false` via [`DEFAULT_WRITE_PAGE_HEADER_STATISTICS`]).
1039    ///
1040    /// Only applicable if [`Page`] level statistics are gathered.
1041    ///
1042    /// Setting this value to `true` can greatly increase the size of the resulting Parquet
1043    /// file while yielding very little added benefit. Most modern Parquet implementations
1044    /// will use the min/max values stored in the [`ParquetColumnIndex`] rather than
1045    /// those in the page header.
1046    ///
1047    /// # Note
1048    ///
1049    /// Prior to version 56.0.0, the `parquet` crate always wrote these
1050    /// statistics (the equivalent of setting this option to `true`). This was
1051    /// changed in 56.0.0 to follow the recommendation in the Parquet
1052    /// specification. See [issue #7580] for more details.
1053    ///
1054    /// [`Statistics`]: crate::file::statistics::Statistics
1055    /// [`ParquetColumnIndex`]: crate::file::metadata::ParquetColumnIndex
1056    /// [`Page`]: EnabledStatistics::Page
1057    /// [issue #7580]: https://github.com/apache/arrow-rs/issues/7580
1058    pub fn set_write_page_header_statistics(mut self, value: bool) -> Self {
1059        self.default_column_properties
1060            .set_write_page_header_statistics(value);
1061        self
1062    }
1063
1064    /// Sets if bloom filter should be written for all columns (defaults to `false`).
1065    ///
1066    /// # Notes
1067    ///
1068    /// * If the bloom filter is enabled previously then it is a no-op.
1069    ///
1070    /// * If the bloom filter is not enabled, default values for ndv and fpp
1071    ///   value are used used. See [`set_bloom_filter_ndv`] and
1072    ///   [`set_bloom_filter_fpp`] to further adjust the ndv and fpp.
1073    ///
1074    /// [`set_bloom_filter_ndv`]: Self::set_bloom_filter_ndv
1075    /// [`set_bloom_filter_fpp`]: Self::set_bloom_filter_fpp
1076    pub fn set_bloom_filter_enabled(mut self, value: bool) -> Self {
1077        self.default_column_properties
1078            .set_bloom_filter_enabled(value);
1079        self
1080    }
1081
1082    /// Sets the default target bloom filter false positive probability (fpp)
1083    /// for all columns (defaults to `0.05` via [`DEFAULT_BLOOM_FILTER_FPP`]).
1084    ///
1085    /// Implicitly enables bloom writing, as if [`set_bloom_filter_enabled`] had
1086    /// been called.
1087    ///
1088    /// [`set_bloom_filter_enabled`]: Self::set_bloom_filter_enabled
1089    pub fn set_bloom_filter_fpp(mut self, value: f64) -> Self {
1090        self.default_column_properties.set_bloom_filter_fpp(value);
1091        self
1092    }
1093
1094    /// Sets default maximum expected number of distinct values (ndv) for bloom filter
1095    /// for all columns (defaults to [`DEFAULT_BLOOM_FILTER_NDV`]).
1096    ///
1097    /// The bloom filter is initially sized for this many distinct values at the
1098    /// configured FPP, then folded down after all values are inserted to achieve
1099    /// optimal size. A good heuristic is to set this to the expected number of rows
1100    /// in the row group.
1101    ///
1102    /// Implicitly enables bloom writing, as if [`set_bloom_filter_enabled`] had
1103    /// been called.
1104    ///
1105    /// [`set_bloom_filter_enabled`]: Self::set_bloom_filter_enabled
1106    pub fn set_bloom_filter_ndv(mut self, value: u64) -> Self {
1107        self.default_column_properties.set_bloom_filter_ndv(value);
1108        self
1109    }
1110
1111    // ----------------------------------------------------------------------
1112    // Setters for a specific column
1113
1114    /// Helper method to get existing or new mutable reference of column properties.
1115    #[inline]
1116    fn get_mut_props(&mut self, col: ColumnPath) -> &mut ColumnProperties {
1117        self.column_properties.entry(col).or_default()
1118    }
1119
1120    /// Sets encoding for a specific column.
1121    ///
1122    /// Takes precedence over [`Self::set_encoding`].
1123    ///
1124    /// If dictionary is not enabled, this is treated as a primary encoding for this
1125    /// column. In case when dictionary is enabled for this column, either through
1126    /// global defaults or explicitly, this value is considered to be a fallback
1127    /// encoding for this column.
1128    ///
1129    /// # Panics
1130    /// If user tries to set dictionary encoding here, regardless of dictionary
1131    /// encoding flag being set.
1132    pub fn set_column_encoding(mut self, col: ColumnPath, value: Encoding) -> Self {
1133        self.get_mut_props(col).set_encoding(value);
1134        self
1135    }
1136
1137    /// Sets compression codec for a specific column.
1138    ///
1139    /// Takes precedence over [`Self::set_compression`].
1140    pub fn set_column_compression(mut self, col: ColumnPath, value: Compression) -> Self {
1141        self.get_mut_props(col).set_compression(value);
1142        self
1143    }
1144
1145    /// Sets flag to enable/disable dictionary encoding for a specific column.
1146    ///
1147    /// Takes precedence over [`Self::set_dictionary_enabled`].
1148    pub fn set_column_dictionary_enabled(mut self, col: ColumnPath, value: bool) -> Self {
1149        self.get_mut_props(col).set_dictionary_enabled(value);
1150        self
1151    }
1152
1153    /// Sets dictionary page size limit for a specific column.
1154    ///
1155    /// Takes precedence over [`Self::set_dictionary_page_size_limit`].
1156    pub fn set_column_dictionary_page_size_limit(mut self, col: ColumnPath, value: usize) -> Self {
1157        self.get_mut_props(col)
1158            .set_dictionary_page_size_limit(value);
1159        self
1160    }
1161
1162    /// Sets data page size limit for a specific column.
1163    ///
1164    /// Takes precedence over [`Self::set_data_page_size_limit`].
1165    pub fn set_column_data_page_size_limit(mut self, col: ColumnPath, value: usize) -> Self {
1166        self.get_mut_props(col).set_data_page_size_limit(value);
1167        self
1168    }
1169
1170    /// Sets [`EnabledStatistics`] level for a specific column.
1171    ///
1172    /// Takes precedence over [`Self::set_statistics_enabled`].
1173    pub fn set_column_statistics_enabled(
1174        mut self,
1175        col: ColumnPath,
1176        value: EnabledStatistics,
1177    ) -> Self {
1178        self.get_mut_props(col).set_statistics_enabled(value);
1179        self
1180    }
1181
1182    /// Sets whether to write [`Statistics`] in the page header for a specific column.
1183    ///
1184    /// Takes precedence over [`Self::set_write_page_header_statistics`].
1185    ///
1186    /// [`Statistics`]: crate::file::statistics::Statistics
1187    pub fn set_column_write_page_header_statistics(mut self, col: ColumnPath, value: bool) -> Self {
1188        self.get_mut_props(col)
1189            .set_write_page_header_statistics(value);
1190        self
1191    }
1192
1193    /// Sets whether a bloom filter should be written for a specific column.
1194    ///
1195    /// Takes precedence over [`Self::set_bloom_filter_enabled`].
1196    pub fn set_column_bloom_filter_enabled(mut self, col: ColumnPath, value: bool) -> Self {
1197        self.get_mut_props(col).set_bloom_filter_enabled(value);
1198        self
1199    }
1200
1201    /// Sets the false positive probability for bloom filter for a specific column.
1202    ///
1203    /// Takes precedence over [`Self::set_bloom_filter_fpp`].
1204    pub fn set_column_bloom_filter_fpp(mut self, col: ColumnPath, value: f64) -> Self {
1205        self.get_mut_props(col).set_bloom_filter_fpp(value);
1206        self
1207    }
1208
1209    /// Sets the number of distinct values for bloom filter for a specific column.
1210    ///
1211    /// Takes precedence over [`Self::set_bloom_filter_ndv`].
1212    pub fn set_column_bloom_filter_ndv(mut self, col: ColumnPath, value: u64) -> Self {
1213        self.get_mut_props(col).set_bloom_filter_ndv(value);
1214        self
1215    }
1216
1217    /// Sets the Data Page v2 compression ratio threshold for a specific column.
1218    ///
1219    /// Takes precedence over [`Self::set_data_page_v2_compression_ratio_threshold`].
1220    ///
1221    /// # Panics
1222    /// If `value` is not finite or is not strictly positive.
1223    pub fn set_column_data_page_v2_compression_ratio_threshold(
1224        mut self,
1225        col: ColumnPath,
1226        value: f64,
1227    ) -> Self {
1228        self.get_mut_props(col)
1229            .set_data_page_v2_compression_ratio_threshold(value);
1230        self
1231    }
1232}
1233
1234impl From<WriterProperties> for WriterPropertiesBuilder {
1235    fn from(props: WriterProperties) -> Self {
1236        WriterPropertiesBuilder {
1237            data_page_row_count_limit: props.data_page_row_count_limit,
1238            write_batch_size: props.write_batch_size,
1239            max_row_group_row_count: props.max_row_group_row_count,
1240            max_row_group_bytes: props.max_row_group_bytes,
1241            bloom_filter_position: props.bloom_filter_position,
1242            writer_version: props.writer_version,
1243            created_by: props.created_by,
1244            offset_index_disabled: !matches!(
1245                props.offset_index_setting,
1246                OffsetIndexSetting::Enabled
1247            ),
1248            key_value_metadata: props.key_value_metadata,
1249            default_column_properties: props.default_column_properties,
1250            column_properties: props.column_properties,
1251            sorting_columns: props.sorting_columns,
1252            column_index_truncate_length: props.column_index_truncate_length,
1253            statistics_truncate_length: props.statistics_truncate_length,
1254            coerce_types: props.coerce_types,
1255            content_defined_chunking: props.content_defined_chunking,
1256            #[cfg(feature = "encryption")]
1257            file_encryption_properties: props.file_encryption_properties,
1258        }
1259    }
1260}
1261
1262/// Controls the level of statistics to be computed by the writer and stored in
1263/// the parquet file.
1264///
1265/// Enabling statistics makes the resulting Parquet file larger and requires
1266/// more time to read the parquet footer.
1267///
1268/// Statistics can be used to improve query performance by pruning row groups
1269/// and pages during query execution if the query engine supports evaluating the
1270/// predicate using the statistics.
1271#[derive(Debug, Clone, Copy, Eq, PartialEq)]
1272pub enum EnabledStatistics {
1273    /// Compute no statistics.
1274    None,
1275    /// Compute column chunk-level statistics but not page-level.
1276    ///
1277    /// Setting this option will store one set of statistics for each relevant
1278    /// column for each row group. The more row groups written, the more
1279    /// statistics will be stored.
1280    Chunk,
1281    /// Compute page-level and column chunk-level statistics.
1282    ///
1283    /// Setting this option will store one set of statistics for each relevant
1284    /// column for each row group. In addition, this will enable the writing
1285    /// of the column index (the offset index is always written regardless of
1286    /// this setting). See [`ParquetColumnIndex`] for
1287    /// more information.
1288    ///
1289    /// [`ParquetColumnIndex`]: crate::file::metadata::ParquetColumnIndex
1290    Page,
1291}
1292
1293impl FromStr for EnabledStatistics {
1294    type Err = String;
1295
1296    fn from_str(s: &str) -> Result<Self, Self::Err> {
1297        match s {
1298            "NONE" | "none" => Ok(EnabledStatistics::None),
1299            "CHUNK" | "chunk" => Ok(EnabledStatistics::Chunk),
1300            "PAGE" | "page" => Ok(EnabledStatistics::Page),
1301            _ => Err(format!("Invalid statistics arg: {s}")),
1302        }
1303    }
1304}
1305
1306impl Default for EnabledStatistics {
1307    fn default() -> Self {
1308        DEFAULT_STATISTICS_ENABLED
1309    }
1310}
1311
1312/// Controls the bloom filter to be computed by the writer.
1313///
1314/// The bloom filter is initially sized for `ndv` distinct values at the given `fpp`, then
1315/// automatically folded down after all values are inserted to achieve optimal size while
1316/// maintaining the target `fpp`. See [`Sbbf::fold_to_target_fpp`] for details on the
1317/// folding algorithm.
1318///
1319/// [`Sbbf::fold_to_target_fpp`]: crate::bloom_filter::Sbbf::fold_to_target_fpp
1320#[derive(Debug, Clone, PartialEq)]
1321pub struct BloomFilterProperties {
1322    /// False positive probability. This should be always between 0 and 1 exclusive. Defaults to [`DEFAULT_BLOOM_FILTER_FPP`].
1323    ///
1324    /// You should set this value by calling [`WriterPropertiesBuilder::set_bloom_filter_fpp`].
1325    ///
1326    /// The bloom filter data structure is a trade of between disk and memory space versus fpp, the
1327    /// smaller the fpp, the more memory and disk space is required, thus setting it to a reasonable value
1328    /// e.g. 0.1, 0.05, or 0.001 is recommended.
1329    ///
1330    /// This value also serves as the target FPP for bloom filter folding: after all values
1331    /// are inserted, the filter is folded down to the smallest size that still meets this FPP.
1332    pub fpp: f64,
1333    /// Maximum expected number of distinct values. Defaults to [`DEFAULT_BLOOM_FILTER_NDV`].
1334    ///
1335    /// You should set this value by calling [`WriterPropertiesBuilder::set_bloom_filter_ndv`].
1336    ///
1337    /// When not explicitly set via the builder, this defaults to
1338    /// [`max_row_group_row_count`](WriterProperties::max_row_group_row_count) (resolved at
1339    /// build time). The bloom filter is initially sized for this many distinct values at the
1340    /// given `fpp`, then folded down after insertion to achieve optimal size. A good heuristic
1341    /// is to set this to the expected number of rows in the row group. If fewer distinct values
1342    /// are actually written, the filter will be automatically compacted via folding.
1343    ///
1344    /// Thus the only negative side of overestimating this value is that the bloom filter
1345    /// will use more memory during writing than necessary, but it will not affect the final
1346    /// bloom filter size on disk.
1347    ///
1348    /// If you wish to reduce memory usage during writing and are able to make a reasonable estimate
1349    /// of the number of distinct values in a row group, it is recommended to set this value explicitly
1350    /// rather than relying on the default dynamic sizing based on `max_row_group_row_count`.
1351    /// If you do set this value explicitly it is probably best to set it for each column
1352    /// individually via [`WriterPropertiesBuilder::set_column_bloom_filter_ndv`] rather than globally,
1353    /// since different columns may have different numbers of distinct values.
1354    pub ndv: u64,
1355}
1356
1357impl Default for BloomFilterProperties {
1358    fn default() -> Self {
1359        BloomFilterProperties {
1360            fpp: DEFAULT_BLOOM_FILTER_FPP,
1361            ndv: DEFAULT_BLOOM_FILTER_NDV,
1362        }
1363    }
1364}
1365
1366/// Container for column properties that can be changed as part of writer.
1367///
1368/// If a field is `None`, it means that no specific value has been set for this column,
1369/// so some subsequent or default value must be used.
1370#[derive(Debug, Clone, Default, PartialEq)]
1371struct ColumnProperties {
1372    encoding: Option<Encoding>,
1373    codec: Option<Compression>,
1374    data_page_size_limit: Option<usize>,
1375    dictionary_page_size_limit: Option<usize>,
1376    dictionary_enabled: Option<bool>,
1377    statistics_enabled: Option<EnabledStatistics>,
1378    write_page_header_statistics: Option<bool>,
1379    /// bloom filter related properties
1380    bloom_filter_properties: Option<BloomFilterProperties>,
1381    /// Whether the bloom filter NDV was explicitly set by the user
1382    bloom_filter_ndv_is_set: bool,
1383    data_page_v2_compression_ratio_threshold: Option<f64>,
1384}
1385
1386impl ColumnProperties {
1387    /// Sets encoding for this column.
1388    ///
1389    /// If dictionary is not enabled, this is treated as a primary encoding for a column.
1390    /// In case when dictionary is enabled for a column, this value is considered to
1391    /// be a fallback encoding.
1392    ///
1393    /// Panics if user tries to set dictionary encoding here, regardless of dictionary
1394    /// encoding flag being set. Use `set_dictionary_enabled` method to enable dictionary
1395    /// for a column.
1396    fn set_encoding(&mut self, value: Encoding) {
1397        if value == Encoding::PLAIN_DICTIONARY || value == Encoding::RLE_DICTIONARY {
1398            panic!("Dictionary encoding can not be used as fallback encoding");
1399        }
1400        self.encoding = Some(value);
1401    }
1402
1403    /// Sets compression codec for this column.
1404    fn set_compression(&mut self, value: Compression) {
1405        self.codec = Some(value);
1406    }
1407
1408    /// Sets data page size limit for this column.
1409    fn set_data_page_size_limit(&mut self, value: usize) {
1410        self.data_page_size_limit = Some(value);
1411    }
1412
1413    /// Sets whether dictionary encoding is enabled for this column.
1414    fn set_dictionary_enabled(&mut self, enabled: bool) {
1415        self.dictionary_enabled = Some(enabled);
1416    }
1417
1418    /// Sets dictionary page size limit for this column.
1419    fn set_dictionary_page_size_limit(&mut self, value: usize) {
1420        self.dictionary_page_size_limit = Some(value);
1421    }
1422
1423    /// Sets the statistics level for this column.
1424    fn set_statistics_enabled(&mut self, enabled: EnabledStatistics) {
1425        self.statistics_enabled = Some(enabled);
1426    }
1427
1428    /// Sets whether to write statistics in the page header for this column.
1429    fn set_write_page_header_statistics(&mut self, enabled: bool) {
1430        self.write_page_header_statistics = Some(enabled);
1431    }
1432
1433    /// If `value` is `true`, sets bloom filter properties to default values if not previously set,
1434    /// otherwise it is a no-op.
1435    /// If `value` is `false`, resets bloom filter properties to `None`.
1436    fn set_bloom_filter_enabled(&mut self, value: bool) {
1437        if value && self.bloom_filter_properties.is_none() {
1438            self.bloom_filter_properties = Some(Default::default())
1439        } else if !value {
1440            self.bloom_filter_properties = None
1441        }
1442    }
1443
1444    /// Sets the false positive probability for bloom filter for this column, and implicitly enables
1445    /// bloom filter if not previously enabled.
1446    ///
1447    /// # Panics
1448    ///
1449    /// Panics if the `value` is not between 0 and 1 exclusive
1450    fn set_bloom_filter_fpp(&mut self, value: f64) {
1451        assert!(
1452            value > 0. && value < 1.0,
1453            "fpp must be between 0 and 1 exclusive, got {value}"
1454        );
1455
1456        self.bloom_filter_properties
1457            .get_or_insert_with(Default::default)
1458            .fpp = value;
1459    }
1460
1461    /// Sets the maximum expected number of distinct (unique) values for bloom filter for this
1462    /// column, and implicitly enables bloom filter if not previously enabled.
1463    fn set_bloom_filter_ndv(&mut self, value: u64) {
1464        self.bloom_filter_properties
1465            .get_or_insert_with(Default::default)
1466            .ndv = value;
1467        self.bloom_filter_ndv_is_set = true;
1468    }
1469
1470    /// Sets the Data Page v2 compression ratio threshold for this column.
1471    ///
1472    /// # Panics
1473    /// If `value` is not finite or is not strictly positive.
1474    fn set_data_page_v2_compression_ratio_threshold(&mut self, value: f64) {
1475        assert!(
1476            value.is_finite() && value > 0.0,
1477            "data_page_v2_compression_ratio_threshold must be a positive finite number, got {value}"
1478        );
1479        self.data_page_v2_compression_ratio_threshold = Some(value);
1480    }
1481
1482    /// Returns optional encoding for this column.
1483    fn encoding(&self) -> Option<Encoding> {
1484        self.encoding
1485    }
1486
1487    /// Returns optional compression codec for this column.
1488    fn compression(&self) -> Option<Compression> {
1489        self.codec
1490    }
1491
1492    /// Returns `Some(true)` if dictionary encoding is enabled for this column, if
1493    /// disabled then returns `Some(false)`. If result is `None`, then no setting has
1494    /// been provided.
1495    fn dictionary_enabled(&self) -> Option<bool> {
1496        self.dictionary_enabled
1497    }
1498
1499    /// Returns optional dictionary page size limit for this column.
1500    fn dictionary_page_size_limit(&self) -> Option<usize> {
1501        self.dictionary_page_size_limit
1502    }
1503
1504    /// Returns optional data page size limit for this column.
1505    fn data_page_size_limit(&self) -> Option<usize> {
1506        self.data_page_size_limit
1507    }
1508
1509    /// Returns optional statistics level requested for this column. If result is `None`,
1510    /// then no setting has been provided.
1511    fn statistics_enabled(&self) -> Option<EnabledStatistics> {
1512        self.statistics_enabled
1513    }
1514
1515    /// Returns `Some(true)` if [`Statistics`] are to be written to the page header for this
1516    /// column.
1517    ///
1518    /// [`Statistics`]: crate::file::statistics::Statistics
1519    fn write_page_header_statistics(&self) -> Option<bool> {
1520        self.write_page_header_statistics
1521    }
1522
1523    /// Returns the bloom filter properties, or `None` if not enabled
1524    fn bloom_filter_properties(&self) -> Option<&BloomFilterProperties> {
1525        self.bloom_filter_properties.as_ref()
1526    }
1527
1528    /// Returns optional Data Page v2 compression ratio threshold for this column.
1529    fn data_page_v2_compression_ratio_threshold(&self) -> Option<f64> {
1530        self.data_page_v2_compression_ratio_threshold
1531    }
1532
1533    /// If bloom filter is enabled and NDV was not explicitly set, resolve it to the
1534    /// given `default_ndv` (typically derived from `max_row_group_row_count`).
1535    fn resolve_bloom_filter_ndv(&mut self, default_ndv: u64) {
1536        if !self.bloom_filter_ndv_is_set {
1537            if let Some(ref mut bf) = self.bloom_filter_properties {
1538                bf.ndv = default_ndv;
1539            }
1540        }
1541    }
1542}
1543
1544/// Reference counted reader properties.
1545pub type ReaderPropertiesPtr = Arc<ReaderProperties>;
1546
1547const DEFAULT_READ_BLOOM_FILTER: bool = false;
1548const DEFAULT_READ_PAGE_STATS: bool = false;
1549
1550/// Configuration settings for reading parquet files.
1551///
1552/// All properties are immutable and `Send` + `Sync`.
1553/// Use [`ReaderPropertiesBuilder`] to assemble these properties.
1554///
1555/// # Example
1556///
1557/// ```rust
1558/// use parquet::file::properties::ReaderProperties;
1559///
1560/// // Create properties with default configuration.
1561/// let props = ReaderProperties::builder().build();
1562///
1563/// // Use properties builder to set certain options and assemble the configuration.
1564/// let props = ReaderProperties::builder()
1565///     .set_backward_compatible_lz4(false)
1566///     .build();
1567/// ```
1568pub struct ReaderProperties {
1569    codec_options: CodecOptions,
1570    read_bloom_filter: bool,
1571    read_page_stats: bool,
1572}
1573
1574impl ReaderProperties {
1575    /// Returns builder for reader properties with default values.
1576    pub fn builder() -> ReaderPropertiesBuilder {
1577        ReaderPropertiesBuilder::with_defaults()
1578    }
1579
1580    /// Returns codec options.
1581    pub(crate) fn codec_options(&self) -> &CodecOptions {
1582        &self.codec_options
1583    }
1584
1585    /// Returns whether to read bloom filter
1586    pub(crate) fn read_bloom_filter(&self) -> bool {
1587        self.read_bloom_filter
1588    }
1589
1590    /// Returns whether to read page level statistics
1591    pub(crate) fn read_page_stats(&self) -> bool {
1592        self.read_page_stats
1593    }
1594}
1595
1596/// Builder for parquet file reader configuration. See example on
1597/// [`ReaderProperties`]
1598pub struct ReaderPropertiesBuilder {
1599    codec_options_builder: CodecOptionsBuilder,
1600    read_bloom_filter: Option<bool>,
1601    read_page_stats: Option<bool>,
1602}
1603
1604/// Reader properties builder.
1605impl ReaderPropertiesBuilder {
1606    /// Returns default state of the builder.
1607    fn with_defaults() -> Self {
1608        Self {
1609            codec_options_builder: CodecOptionsBuilder::default(),
1610            read_bloom_filter: None,
1611            read_page_stats: None,
1612        }
1613    }
1614
1615    /// Finalizes the configuration and returns immutable reader properties struct.
1616    pub fn build(self) -> ReaderProperties {
1617        ReaderProperties {
1618            codec_options: self.codec_options_builder.build(),
1619            read_bloom_filter: self.read_bloom_filter.unwrap_or(DEFAULT_READ_BLOOM_FILTER),
1620            read_page_stats: self.read_page_stats.unwrap_or(DEFAULT_READ_PAGE_STATS),
1621        }
1622    }
1623
1624    /// Enable/disable backward compatible LZ4.
1625    ///
1626    /// If backward compatible LZ4 is enable, on LZ4_HADOOP error it will fallback
1627    /// to the older versions LZ4 algorithms. That is LZ4_FRAME, for backward compatibility
1628    /// with files generated by older versions of this library, and LZ4_RAW, for backward
1629    /// compatibility with files generated by older versions of parquet-cpp.
1630    ///
1631    /// If backward compatible LZ4 is disabled, on LZ4_HADOOP error it will return the error.
1632    pub fn set_backward_compatible_lz4(mut self, value: bool) -> Self {
1633        self.codec_options_builder = self
1634            .codec_options_builder
1635            .set_backward_compatible_lz4(value);
1636        self
1637    }
1638
1639    /// Enable/disable reading bloom filter
1640    ///
1641    /// If reading bloom filter is enabled, bloom filter will be read from the file.
1642    /// If reading bloom filter is disabled, bloom filter will not be read from the file.
1643    ///
1644    /// By default bloom filter is set to be read.
1645    pub fn set_read_bloom_filter(mut self, value: bool) -> Self {
1646        self.read_bloom_filter = Some(value);
1647        self
1648    }
1649
1650    /// Enable/disable reading page-level statistics
1651    ///
1652    /// If set to `true`, then the reader will decode and populate the [`Statistics`] for
1653    /// each page, if present.
1654    /// If set to `false`, then the reader will skip decoding the statistics.
1655    ///
1656    /// By default statistics will not be decoded.
1657    ///
1658    /// [`Statistics`]: crate::file::statistics::Statistics
1659    pub fn set_read_page_statistics(mut self, value: bool) -> Self {
1660        self.read_page_stats = Some(value);
1661        self
1662    }
1663}
1664
1665#[cfg(test)]
1666mod tests {
1667    use super::*;
1668
1669    #[test]
1670    fn test_writer_version() {
1671        assert_eq!(WriterVersion::PARQUET_1_0.as_num(), 1);
1672        assert_eq!(WriterVersion::PARQUET_2_0.as_num(), 2);
1673    }
1674
1675    #[test]
1676    fn test_writer_properties_default_settings() {
1677        let props = WriterProperties::default();
1678        assert_eq!(props.data_page_size_limit(), DEFAULT_PAGE_SIZE);
1679        assert_eq!(
1680            props.dictionary_page_size_limit(),
1681            DEFAULT_DICTIONARY_PAGE_SIZE_LIMIT
1682        );
1683        assert_eq!(props.write_batch_size(), DEFAULT_WRITE_BATCH_SIZE);
1684        assert_eq!(
1685            props.max_row_group_row_count(),
1686            Some(DEFAULT_MAX_ROW_GROUP_ROW_COUNT)
1687        );
1688        assert_eq!(props.max_row_group_bytes(), None);
1689        assert_eq!(props.bloom_filter_position(), DEFAULT_BLOOM_FILTER_POSITION);
1690        assert_eq!(props.writer_version(), DEFAULT_WRITER_VERSION);
1691        assert_eq!(props.created_by(), DEFAULT_CREATED_BY);
1692        assert_eq!(props.key_value_metadata(), None);
1693        assert_eq!(props.encoding(&ColumnPath::from("col")), None);
1694        assert_eq!(
1695            props.compression(&ColumnPath::from("col")),
1696            DEFAULT_COMPRESSION
1697        );
1698        assert_eq!(
1699            props.dictionary_enabled(&ColumnPath::from("col")),
1700            DEFAULT_DICTIONARY_ENABLED
1701        );
1702        assert_eq!(
1703            props.statistics_enabled(&ColumnPath::from("col")),
1704            DEFAULT_STATISTICS_ENABLED
1705        );
1706        assert!(
1707            props
1708                .bloom_filter_properties(&ColumnPath::from("col"))
1709                .is_none()
1710        );
1711    }
1712
1713    #[test]
1714    fn test_writer_properties_dictionary_encoding() {
1715        // dictionary encoding is not configurable, and it should be the same for both
1716        // writer version 1 and 2.
1717        for version in &[WriterVersion::PARQUET_1_0, WriterVersion::PARQUET_2_0] {
1718            let props = WriterProperties::builder()
1719                .set_writer_version(*version)
1720                .build();
1721            assert_eq!(props.dictionary_page_encoding(), Encoding::PLAIN);
1722            assert_eq!(
1723                props.dictionary_data_page_encoding(),
1724                Encoding::RLE_DICTIONARY
1725            );
1726        }
1727    }
1728
1729    #[test]
1730    #[should_panic(expected = "Dictionary encoding can not be used as fallback encoding")]
1731    fn test_writer_properties_panic_when_plain_dictionary_is_fallback() {
1732        // Should panic when user specifies dictionary encoding as fallback encoding.
1733        WriterProperties::builder()
1734            .set_encoding(Encoding::PLAIN_DICTIONARY)
1735            .build();
1736    }
1737
1738    #[test]
1739    #[should_panic(expected = "Dictionary encoding can not be used as fallback encoding")]
1740    fn test_writer_properties_panic_when_rle_dictionary_is_fallback() {
1741        // Should panic when user specifies dictionary encoding as fallback encoding.
1742        WriterProperties::builder()
1743            .set_encoding(Encoding::RLE_DICTIONARY)
1744            .build();
1745    }
1746
1747    #[test]
1748    #[should_panic(expected = "Dictionary encoding can not be used as fallback encoding")]
1749    fn test_writer_properties_panic_when_dictionary_is_enabled() {
1750        WriterProperties::builder()
1751            .set_dictionary_enabled(true)
1752            .set_column_encoding(ColumnPath::from("col"), Encoding::RLE_DICTIONARY)
1753            .build();
1754    }
1755
1756    #[test]
1757    #[should_panic(expected = "Dictionary encoding can not be used as fallback encoding")]
1758    fn test_writer_properties_panic_when_dictionary_is_disabled() {
1759        WriterProperties::builder()
1760            .set_dictionary_enabled(false)
1761            .set_column_encoding(ColumnPath::from("col"), Encoding::RLE_DICTIONARY)
1762            .build();
1763    }
1764
1765    #[test]
1766    fn test_writer_properties_builder() {
1767        let props = WriterProperties::builder()
1768            // file settings
1769            .set_writer_version(WriterVersion::PARQUET_2_0)
1770            .set_data_page_size_limit(10)
1771            .set_dictionary_page_size_limit(20)
1772            .set_write_batch_size(30)
1773            .set_max_row_group_row_count(Some(40))
1774            .set_created_by("default".to_owned())
1775            .set_key_value_metadata(Some(vec![KeyValue::new(
1776                "key".to_string(),
1777                "value".to_string(),
1778            )]))
1779            // global column settings
1780            .set_encoding(Encoding::DELTA_BINARY_PACKED)
1781            .set_compression(Compression::GZIP(Default::default()))
1782            .set_dictionary_enabled(false)
1783            .set_statistics_enabled(EnabledStatistics::None)
1784            // specific column settings
1785            .set_column_encoding(ColumnPath::from("col"), Encoding::RLE)
1786            .set_column_compression(ColumnPath::from("col"), Compression::SNAPPY)
1787            .set_column_dictionary_enabled(ColumnPath::from("col"), true)
1788            .set_column_statistics_enabled(ColumnPath::from("col"), EnabledStatistics::Chunk)
1789            .set_column_bloom_filter_enabled(ColumnPath::from("col"), true)
1790            .set_column_bloom_filter_ndv(ColumnPath::from("col"), 100_u64)
1791            .set_column_bloom_filter_fpp(ColumnPath::from("col"), 0.1)
1792            .build();
1793
1794        fn test_props(props: &WriterProperties) {
1795            assert_eq!(props.writer_version(), WriterVersion::PARQUET_2_0);
1796            assert_eq!(props.data_page_size_limit(), 10);
1797            assert_eq!(props.dictionary_page_size_limit(), 20);
1798            assert_eq!(props.write_batch_size(), 30);
1799            assert_eq!(props.max_row_group_row_count(), Some(40));
1800            assert_eq!(props.created_by(), "default");
1801            assert_eq!(
1802                props.key_value_metadata(),
1803                Some(&vec![
1804                    KeyValue::new("key".to_string(), "value".to_string(),)
1805                ])
1806            );
1807
1808            assert_eq!(
1809                props.encoding(&ColumnPath::from("a")),
1810                Some(Encoding::DELTA_BINARY_PACKED)
1811            );
1812            assert_eq!(
1813                props.compression(&ColumnPath::from("a")),
1814                Compression::GZIP(Default::default())
1815            );
1816            assert!(!props.dictionary_enabled(&ColumnPath::from("a")));
1817            assert_eq!(
1818                props.statistics_enabled(&ColumnPath::from("a")),
1819                EnabledStatistics::None
1820            );
1821
1822            assert_eq!(
1823                props.encoding(&ColumnPath::from("col")),
1824                Some(Encoding::RLE)
1825            );
1826            assert_eq!(
1827                props.compression(&ColumnPath::from("col")),
1828                Compression::SNAPPY
1829            );
1830            assert!(props.dictionary_enabled(&ColumnPath::from("col")));
1831            assert_eq!(
1832                props.statistics_enabled(&ColumnPath::from("col")),
1833                EnabledStatistics::Chunk
1834            );
1835            assert_eq!(
1836                props.bloom_filter_properties(&ColumnPath::from("col")),
1837                Some(&BloomFilterProperties { fpp: 0.1, ndv: 100 })
1838            );
1839        }
1840
1841        // Test direct build of properties
1842        test_props(&props);
1843
1844        // Test that into_builder() gives the same result
1845        let props_into_builder_and_back = props.into_builder().build();
1846        test_props(&props_into_builder_and_back);
1847    }
1848
1849    #[test]
1850    fn test_writer_properties_builder_partial_defaults() {
1851        let props = WriterProperties::builder()
1852            .set_encoding(Encoding::DELTA_BINARY_PACKED)
1853            .set_compression(Compression::GZIP(Default::default()))
1854            .set_bloom_filter_enabled(true)
1855            .set_column_encoding(ColumnPath::from("col"), Encoding::RLE)
1856            .build();
1857
1858        assert_eq!(
1859            props.encoding(&ColumnPath::from("col")),
1860            Some(Encoding::RLE)
1861        );
1862        assert_eq!(
1863            props.compression(&ColumnPath::from("col")),
1864            Compression::GZIP(Default::default())
1865        );
1866        assert_eq!(
1867            props.dictionary_enabled(&ColumnPath::from("col")),
1868            DEFAULT_DICTIONARY_ENABLED
1869        );
1870        assert_eq!(
1871            props.bloom_filter_properties(&ColumnPath::from("col")),
1872            Some(&BloomFilterProperties {
1873                fpp: DEFAULT_BLOOM_FILTER_FPP,
1874                ndv: DEFAULT_BLOOM_FILTER_NDV,
1875            })
1876        );
1877    }
1878
1879    #[test]
1880    #[allow(deprecated)]
1881    fn test_writer_properties_deprecated_max_row_group_size_still_works() {
1882        let props = WriterProperties::builder()
1883            .set_max_row_group_size(42)
1884            .build();
1885
1886        assert_eq!(props.max_row_group_row_count(), Some(42));
1887        assert_eq!(props.max_row_group_size(), 42);
1888    }
1889
1890    #[test]
1891    #[should_panic(expected = "Cannot have a 0 max row group row count")]
1892    fn test_writer_properties_panic_on_zero_row_group_row_count() {
1893        let _ = WriterProperties::builder().set_max_row_group_row_count(Some(0));
1894    }
1895
1896    #[test]
1897    #[should_panic(expected = "Cannot have a 0 max row group bytes")]
1898    fn test_writer_properties_panic_on_zero_row_group_bytes() {
1899        let _ = WriterProperties::builder().set_max_row_group_bytes(Some(0));
1900    }
1901
1902    #[test]
1903    fn test_writer_properties_bloom_filter_ndv_fpp_set() {
1904        assert_eq!(
1905            WriterProperties::builder()
1906                .build()
1907                .bloom_filter_properties(&ColumnPath::from("col")),
1908            None
1909        );
1910        assert_eq!(
1911            WriterProperties::builder()
1912                .set_bloom_filter_ndv(100)
1913                .build()
1914                .bloom_filter_properties(&ColumnPath::from("col")),
1915            Some(&BloomFilterProperties {
1916                fpp: DEFAULT_BLOOM_FILTER_FPP,
1917                ndv: 100,
1918            })
1919        );
1920        assert_eq!(
1921            WriterProperties::builder()
1922                .set_bloom_filter_fpp(0.1)
1923                .build()
1924                .bloom_filter_properties(&ColumnPath::from("col")),
1925            Some(&BloomFilterProperties {
1926                fpp: 0.1,
1927                ndv: DEFAULT_BLOOM_FILTER_NDV,
1928            })
1929        );
1930    }
1931
1932    #[test]
1933    fn test_writer_properties_column_data_page_v2_compression_ratio_threshold() {
1934        let props = WriterProperties::builder()
1935            .set_data_page_v2_compression_ratio_threshold(0.5)
1936            .set_column_data_page_v2_compression_ratio_threshold(ColumnPath::from("col"), 0.1)
1937            .build();
1938
1939        assert_eq!(props.data_page_v2_compression_ratio_threshold(), 0.5);
1940        assert_eq!(
1941            props.column_data_page_v2_compression_ratio_threshold(&ColumnPath::from("col")),
1942            0.1
1943        );
1944        assert_eq!(
1945            props.column_data_page_v2_compression_ratio_threshold(&ColumnPath::from("other")),
1946            0.5
1947        );
1948    }
1949
1950    #[test]
1951    #[should_panic(
1952        expected = "data_page_v2_compression_ratio_threshold must be a positive finite number"
1953    )]
1954    fn test_writer_properties_panic_on_invalid_data_page_v2_compression_ratio_threshold() {
1955        WriterProperties::builder()
1956            .set_data_page_v2_compression_ratio_threshold(0.0)
1957            .build();
1958    }
1959
1960    #[test]
1961    fn test_writer_properties_column_dictionary_page_size_limit() {
1962        let props = WriterProperties::builder()
1963            .set_dictionary_page_size_limit(100)
1964            .set_column_dictionary_page_size_limit(ColumnPath::from("col"), 10)
1965            .build();
1966
1967        assert_eq!(props.dictionary_page_size_limit(), 100);
1968        assert_eq!(
1969            props.column_dictionary_page_size_limit(&ColumnPath::from("col")),
1970            10
1971        );
1972        assert_eq!(
1973            props.column_dictionary_page_size_limit(&ColumnPath::from("other")),
1974            100
1975        );
1976    }
1977
1978    #[test]
1979    fn test_writer_properties_column_data_page_size_limit() {
1980        let props = WriterProperties::builder()
1981            .set_data_page_size_limit(100)
1982            .set_column_data_page_size_limit(ColumnPath::from("col"), 10)
1983            .build();
1984
1985        assert_eq!(props.data_page_size_limit(), 100);
1986        assert_eq!(
1987            props.column_data_page_size_limit(&ColumnPath::from("col")),
1988            10
1989        );
1990        assert_eq!(
1991            props.column_data_page_size_limit(&ColumnPath::from("other")),
1992            100
1993        );
1994    }
1995
1996    #[test]
1997    fn test_reader_properties_default_settings() {
1998        let props = ReaderProperties::builder().build();
1999
2000        let codec_options = CodecOptionsBuilder::default()
2001            .set_backward_compatible_lz4(true)
2002            .build();
2003
2004        assert_eq!(props.codec_options(), &codec_options);
2005        assert!(!props.read_bloom_filter());
2006    }
2007
2008    #[test]
2009    fn test_reader_properties_builder() {
2010        let props = ReaderProperties::builder()
2011            .set_backward_compatible_lz4(false)
2012            .build();
2013
2014        let codec_options = CodecOptionsBuilder::default()
2015            .set_backward_compatible_lz4(false)
2016            .build();
2017
2018        assert_eq!(props.codec_options(), &codec_options);
2019    }
2020
2021    #[test]
2022    fn test_parse_writerversion() {
2023        let mut writer_version = "PARQUET_1_0".parse::<WriterVersion>().unwrap();
2024        assert_eq!(writer_version, WriterVersion::PARQUET_1_0);
2025        writer_version = "PARQUET_2_0".parse::<WriterVersion>().unwrap();
2026        assert_eq!(writer_version, WriterVersion::PARQUET_2_0);
2027
2028        // test lowercase
2029        writer_version = "parquet_1_0".parse::<WriterVersion>().unwrap();
2030        assert_eq!(writer_version, WriterVersion::PARQUET_1_0);
2031
2032        // test invalid version
2033        match "PARQUET_-1_0".parse::<WriterVersion>() {
2034            Ok(_) => panic!("Should not be able to parse PARQUET_-1_0"),
2035            Err(e) => {
2036                assert_eq!(e, "Invalid writer version: PARQUET_-1_0");
2037            }
2038        }
2039    }
2040
2041    #[test]
2042    fn test_parse_enabledstatistics() {
2043        let mut enabled_statistics = "NONE".parse::<EnabledStatistics>().unwrap();
2044        assert_eq!(enabled_statistics, EnabledStatistics::None);
2045        enabled_statistics = "CHUNK".parse::<EnabledStatistics>().unwrap();
2046        assert_eq!(enabled_statistics, EnabledStatistics::Chunk);
2047        enabled_statistics = "PAGE".parse::<EnabledStatistics>().unwrap();
2048        assert_eq!(enabled_statistics, EnabledStatistics::Page);
2049
2050        // test lowercase
2051        enabled_statistics = "none".parse::<EnabledStatistics>().unwrap();
2052        assert_eq!(enabled_statistics, EnabledStatistics::None);
2053
2054        //test invalid statistics
2055        match "ChunkAndPage".parse::<EnabledStatistics>() {
2056            Ok(_) => panic!("Should not be able to parse ChunkAndPage"),
2057            Err(e) => {
2058                assert_eq!(e, "Invalid statistics arg: ChunkAndPage");
2059            }
2060        }
2061    }
2062
2063    #[test]
2064    fn test_cdc_options_equality() {
2065        let opts = CdcOptions::default();
2066        assert_eq!(opts, CdcOptions::default());
2067
2068        let custom = CdcOptions {
2069            min_chunk_size: 1024,
2070            max_chunk_size: 8192,
2071            norm_level: 1,
2072        };
2073        assert_eq!(custom, custom);
2074        assert_ne!(opts, custom);
2075    }
2076}