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::errors::{ParquetError, Result};
24use crate::file::metadata::{KeyValue, SortingColumn};
25use crate::schema::types::ColumnPath;
26use std::str::FromStr;
27use std::{collections::HashMap, sync::Arc};
28
29/// Default value for [`WriterProperties::data_page_size_limit`]
30pub const DEFAULT_PAGE_SIZE: usize = 1024 * 1024;
31/// Default value for [`WriterProperties::write_batch_size`]
32pub const DEFAULT_WRITE_BATCH_SIZE: usize = 1024;
33/// Default value for [`WriterProperties::writer_version`]
34pub const DEFAULT_WRITER_VERSION: WriterVersion = WriterVersion::PARQUET_1_0;
35/// Default value for [`WriterProperties::compression`]
36pub const DEFAULT_COMPRESSION: Compression = Compression::UNCOMPRESSED;
37/// Default value for [`WriterProperties::dictionary_enabled`]
38pub const DEFAULT_DICTIONARY_ENABLED: bool = true;
39/// Default value for [`WriterProperties::dictionary_page_size_limit`]
40pub const DEFAULT_DICTIONARY_PAGE_SIZE_LIMIT: usize = DEFAULT_PAGE_SIZE;
41/// Default value for [`WriterProperties::data_page_row_count_limit`]
42pub const DEFAULT_DATA_PAGE_ROW_COUNT_LIMIT: usize = 20_000;
43/// Default value for [`WriterProperties::statistics_enabled`]
44pub const DEFAULT_STATISTICS_ENABLED: EnabledStatistics = EnabledStatistics::Page;
45/// Default value for [`WriterProperties::write_page_header_statistics`]
46pub const DEFAULT_WRITE_PAGE_HEADER_STATISTICS: bool = false;
47/// Default value for [`WriterProperties::max_row_group_row_count`]
48pub const DEFAULT_MAX_ROW_GROUP_ROW_COUNT: usize = 1024 * 1024;
49/// Default value for [`WriterProperties::bloom_filter_for_dictionary_encoded_chunks`]
50pub const DEFAULT_BLOOM_FILTER_FOR_DICTIONARY_ENCODED_CHUNKS: bool = true;
51/// Default value for [`WriterProperties::bloom_filter_position`]
52pub const DEFAULT_BLOOM_FILTER_POSITION: BloomFilterPosition = BloomFilterPosition::AfterRowGroup;
53/// Default value for [`WriterProperties::created_by`]
54pub const DEFAULT_CREATED_BY: &str = concat!("parquet-rs version ", env!("CARGO_PKG_VERSION"));
55/// Default value for [`WriterProperties::column_index_truncate_length`]
56pub const DEFAULT_COLUMN_INDEX_TRUNCATE_LENGTH: Option<usize> = Some(64);
57/// Default value for [`BloomFilterProperties::fpp()`]
58pub const DEFAULT_BLOOM_FILTER_FPP: f64 = 0.05;
59/// Default value for [`BloomFilterProperties::ndv()`].
60///
61/// Note: this is only the fallback default used when constructing [`BloomFilterProperties`]
62/// directly. When using [`WriterPropertiesBuilder`], columns with bloom filters enabled
63/// but without an explicit NDV will have their NDV resolved at build time to
64/// [`WriterProperties::max_row_group_row_count`], which may differ from this constant
65/// if the user configured a custom row group size.
66pub const DEFAULT_BLOOM_FILTER_NDV: u64 = DEFAULT_MAX_ROW_GROUP_ROW_COUNT as u64;
67/// Default values for [`WriterProperties::statistics_truncate_length`]
68pub const DEFAULT_STATISTICS_TRUNCATE_LENGTH: Option<usize> = Some(64);
69/// Default value for [`WriterProperties::offset_index_disabled`]
70pub const DEFAULT_OFFSET_INDEX_DISABLED: bool = false;
71/// Default values for [`WriterProperties::coerce_types`]
72pub const DEFAULT_COERCE_TYPES: bool = false;
73/// Default value for [`WriterProperties::write_row_group_number_distinct_values`]
74pub const DEFAULT_WRITE_ROW_GROUP_NUMBER_DISTINCT_VALUES: bool = false;
75/// Default value for [`WriterProperties::data_page_v2_compression_ratio_threshold`]
76pub const DEFAULT_DATA_PAGE_V2_COMPRESSION_RATIO_THRESHOLD: f64 = 1.0;
77/// Default value for [`WriterProperties::write_path_in_schema`]
78pub const DEFAULT_WRITE_PATH_IN_SCHEMA: bool = true;
79/// Default minimum chunk size for content-defined chunking: 256 KiB.
80pub const DEFAULT_CDC_MIN_CHUNK_SIZE: usize = 256 * 1024;
81/// Default maximum chunk size for content-defined chunking: 1024 KiB.
82pub const DEFAULT_CDC_MAX_CHUNK_SIZE: usize = 1024 * 1024;
83/// Default normalization level for content-defined chunking.
84pub const DEFAULT_CDC_NORM_LEVEL: i32 = 0;
85
86/// EXPERIMENTAL: Options for content-defined chunking (CDC).
87///
88/// Content-defined chunking is an experimental feature that optimizes parquet
89/// files for content addressable storage (CAS) systems by writing data pages
90/// according to content-defined chunk boundaries. This allows for more
91/// efficient deduplication of data across files, hence more efficient network
92/// transfers and storage.
93///
94/// Each content-defined chunk is written as a separate parquet data page. The
95/// following options control the chunks' size and the chunking process. Note
96/// that the chunk size is calculated based on the logical value of the data,
97/// before any encoding or compression is applied.
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
99pub struct CdcOptions {
100    /// Minimum chunk size in bytes, default is 256 KiB.
101    /// The rolling hash will not be updated until this size is reached for each chunk.
102    /// Note that all data sent through the hash function is counted towards the chunk
103    /// size, including definition and repetition levels if present.
104    pub min_chunk_size: usize,
105    /// Maximum chunk size in bytes, default is 1024 KiB.
106    /// The chunker will create a new chunk whenever the chunk size exceeds this value.
107    /// Note that the parquet writer has a related [`data_page_size_limit`] property that
108    /// controls the maximum size of a parquet data page after encoding. While setting
109    /// `data_page_size_limit` to a smaller value than `max_chunk_size` doesn't affect
110    /// the chunking effectiveness, it results in more small parquet data pages.
111    ///
112    /// [`data_page_size_limit`]: WriterPropertiesBuilder::set_data_page_size_limit
113    pub max_chunk_size: usize,
114    /// Number of bit adjustment to the gearhash mask in order to center the chunk size
115    /// around the average size more aggressively, default is 0.
116    /// Increasing the normalization level increases the probability of finding a chunk,
117    /// improving the deduplication ratio, but also increasing the number of small chunks
118    /// resulting in many small parquet data pages. The default value provides a good
119    /// balance between deduplication ratio and fragmentation.
120    /// Use norm_level=1 or norm_level=2 to reach a higher deduplication ratio at the
121    /// expense of fragmentation. Negative values can also be used to reduce the
122    /// probability of finding a chunk, resulting in larger chunks and fewer data pages.
123    /// Note that values outside [-3, 3] are not recommended, prefer using the default
124    /// value of 0 for most use cases.
125    pub norm_level: i32,
126}
127
128impl Default for CdcOptions {
129    fn default() -> Self {
130        Self {
131            min_chunk_size: DEFAULT_CDC_MIN_CHUNK_SIZE,
132            max_chunk_size: DEFAULT_CDC_MAX_CHUNK_SIZE,
133            norm_level: DEFAULT_CDC_NORM_LEVEL,
134        }
135    }
136}
137
138/// Parquet writer version.
139///
140/// Basic constant, which is not part of the Thrift definition.
141#[derive(Debug, Clone, Copy, PartialEq, Eq)]
142#[expect(non_camel_case_types)]
143pub enum WriterVersion {
144    /// Parquet format version 1.0
145    PARQUET_1_0,
146    /// Parquet format version 2.0
147    PARQUET_2_0,
148}
149
150impl WriterVersion {
151    /// Returns writer version as `i32`.
152    pub fn as_num(&self) -> i32 {
153        match self {
154            WriterVersion::PARQUET_1_0 => 1,
155            WriterVersion::PARQUET_2_0 => 2,
156        }
157    }
158}
159
160impl FromStr for WriterVersion {
161    type Err = String;
162
163    fn from_str(s: &str) -> Result<Self, Self::Err> {
164        match s {
165            "PARQUET_1_0" | "parquet_1_0" => Ok(WriterVersion::PARQUET_1_0),
166            "PARQUET_2_0" | "parquet_2_0" => Ok(WriterVersion::PARQUET_2_0),
167            _ => Err(format!("Invalid writer version: {s}")),
168        }
169    }
170}
171
172/// Where in the file [`ArrowWriter`](crate::arrow::arrow_writer::ArrowWriter) should
173/// write Bloom filters
174///
175/// Basic constant, which is not part of the Thrift definition.
176#[derive(Debug, Clone, Copy, PartialEq, Eq)]
177pub enum BloomFilterPosition {
178    /// Write Bloom Filters of each row group right after the row group
179    ///
180    /// This saves memory by writing it as soon as it is computed, at the cost
181    /// of data locality for readers
182    AfterRowGroup,
183    /// Write Bloom Filters at the end of the file
184    ///
185    /// This allows better data locality for readers, at the cost of memory usage
186    /// for writers.
187    End,
188}
189
190/// Reference counted writer properties.
191pub type WriterPropertiesPtr = Arc<WriterProperties>;
192
193/// Resolved state of [`WriterPropertiesBuilder::set_offset_index_disabled`].
194///
195/// When a user disables offset indexes but page-level statistics are enabled,
196/// the setting is overridden (offset indexes remain enabled). This enum
197/// preserves the user's original intent so that a round-trip through
198/// `WriterPropertiesBuilder` does not lose it.
199#[derive(Debug, Clone, Copy, PartialEq, Eq)]
200enum OffsetIndexSetting {
201    /// Offset indexes are enabled (the default).
202    Enabled,
203    /// User disabled offset indexes and no page-level statistics override it.
204    Disabled,
205    /// User disabled offset indexes, but page-level statistics require them,
206    /// so they remain enabled.
207    DisabledOverridden,
208}
209
210/// Configuration settings for writing parquet files.
211///
212/// Use [`Self::builder`] to create a [`WriterPropertiesBuilder`] to change settings.
213///
214/// # Example
215///
216/// ```rust
217/// # use parquet::{
218/// #    basic::{Compression, Encoding},
219/// #    file::properties::*,
220/// #    schema::types::ColumnPath,
221/// # };
222/// #
223/// // Create properties with default configuration.
224/// let props = WriterProperties::default();
225///
226/// // Use properties builder to set certain options and assemble the configuration.
227/// let props = WriterProperties::builder()
228///     .set_writer_version(WriterVersion::PARQUET_1_0)
229///     .set_encoding(Encoding::PLAIN)
230///     .set_column_encoding(ColumnPath::from("col1"), Encoding::DELTA_BINARY_PACKED)
231///     .set_compression(Compression::SNAPPY)
232///     .build();
233///
234/// assert_eq!(props.writer_version(), WriterVersion::PARQUET_1_0);
235/// assert_eq!(
236///     props.encoding(&ColumnPath::from("col1")),
237///     Some(Encoding::DELTA_BINARY_PACKED)
238/// );
239/// assert_eq!(
240///     props.encoding(&ColumnPath::from("col2")),
241///     Some(Encoding::PLAIN)
242/// );
243/// ```
244#[derive(Debug, Clone)]
245pub struct WriterProperties {
246    data_page_row_count_limit: usize,
247    write_batch_size: usize,
248    max_row_group_row_count: Option<usize>,
249    max_row_group_bytes: Option<usize>,
250    bloom_filter_position: BloomFilterPosition,
251    bloom_filter_for_dictionary_encoded_chunks: bool,
252    writer_version: WriterVersion,
253    created_by: String,
254    offset_index_setting: OffsetIndexSetting,
255    pub(crate) key_value_metadata: Option<Vec<KeyValue>>,
256    default_column_properties: ColumnProperties,
257    column_properties: HashMap<ColumnPath, ColumnProperties>,
258    sorting_columns: Option<Vec<SortingColumn>>,
259    column_index_truncate_length: Option<usize>,
260    statistics_truncate_length: Option<usize>,
261    coerce_types: bool,
262    write_row_group_number_distinct_values: bool,
263    content_defined_chunking: Option<CdcOptions>,
264    write_path_in_schema: bool,
265    #[cfg(feature = "encryption")]
266    pub(crate) file_encryption_properties: Option<Arc<FileEncryptionProperties>>,
267}
268
269impl Default for WriterProperties {
270    fn default() -> Self {
271        Self::builder().build()
272    }
273}
274
275impl WriterProperties {
276    /// Create a new [`WriterProperties`] with the default settings
277    ///
278    /// See [`WriterProperties::builder`] for customising settings
279    pub fn new() -> Self {
280        Self::default()
281    }
282
283    /// Returns a new default [`WriterPropertiesBuilder`] for creating writer
284    /// properties.
285    pub fn builder() -> WriterPropertiesBuilder {
286        WriterPropertiesBuilder::default()
287    }
288
289    /// Converts this [`WriterProperties`] into a [`WriterPropertiesBuilder`]
290    /// Used for mutating existing property settings
291    pub fn into_builder(self) -> WriterPropertiesBuilder {
292        self.into()
293    }
294
295    /// Returns data page size limit.
296    ///
297    /// Note: this is a best effort limit based on the write batch size
298    ///
299    /// For more details see [`WriterPropertiesBuilder::set_data_page_size_limit`]
300    pub fn data_page_size_limit(&self) -> usize {
301        self.default_column_properties
302            .data_page_size_limit()
303            .unwrap_or(DEFAULT_PAGE_SIZE)
304    }
305
306    /// Returns data page size limit for a specific column.
307    ///
308    /// Takes precedence over [`Self::data_page_size_limit`].
309    ///
310    /// Note: this is a best effort limit based on the write batch size.
311    pub fn column_data_page_size_limit(&self, col: &ColumnPath) -> usize {
312        resolve_data_page_size_limit(self.column_override(col), &self.default_column_properties)
313    }
314
315    /// Returns dictionary page size limit.
316    ///
317    /// Note: this is a best effort limit based on the write batch size
318    ///
319    /// For more details see [`WriterPropertiesBuilder::set_dictionary_page_size_limit`]
320    pub fn dictionary_page_size_limit(&self) -> usize {
321        self.default_column_properties
322            .dictionary_page_size_limit()
323            .unwrap_or(DEFAULT_DICTIONARY_PAGE_SIZE_LIMIT)
324    }
325
326    /// Returns dictionary page size limit for a specific column.
327    pub fn column_dictionary_page_size_limit(&self, col: &ColumnPath) -> usize {
328        resolve_dictionary_page_size_limit(
329            self.column_override(col),
330            &self.default_column_properties,
331        )
332    }
333
334    /// Returns the maximum page row count
335    ///
336    /// Note: this is a best effort limit based on the write batch size
337    ///
338    /// For more details see [`WriterPropertiesBuilder::set_data_page_row_count_limit`]
339    pub fn data_page_row_count_limit(&self) -> usize {
340        self.data_page_row_count_limit
341    }
342
343    /// Returns configured batch size for writes.
344    ///
345    /// When writing a batch of data, this setting allows to split it internally into
346    /// smaller batches so we can better estimate the size of a page currently being
347    /// written.
348    ///
349    /// For more details see [`WriterPropertiesBuilder::set_write_batch_size`]
350    pub fn write_batch_size(&self) -> usize {
351        self.write_batch_size
352    }
353
354    /// Returns maximum number of rows in a row group, or `None` if unlimited.
355    ///
356    /// For more details see [`WriterPropertiesBuilder::set_max_row_group_row_count`]
357    pub fn max_row_group_row_count(&self) -> Option<usize> {
358        self.max_row_group_row_count
359    }
360
361    /// Returns maximum size of a row group in bytes, or `None` if unlimited.
362    ///
363    /// For more details see [`WriterPropertiesBuilder::set_max_row_group_bytes`]
364    pub fn max_row_group_bytes(&self) -> Option<usize> {
365        self.max_row_group_bytes
366    }
367
368    /// Returns bloom filter position.
369    ///
370    /// For more details see [`WriterPropertiesBuilder::set_bloom_filter_position`]
371    pub fn bloom_filter_position(&self) -> BloomFilterPosition {
372        self.bloom_filter_position
373    }
374
375    /// Returns whether a column chunk whose data pages are all dictionary encoded gets a
376    /// bloom filter.
377    ///
378    /// For more details see [`WriterPropertiesBuilder::set_bloom_filter_for_dictionary_encoded_chunks`]
379    pub fn bloom_filter_for_dictionary_encoded_chunks(&self) -> bool {
380        self.bloom_filter_for_dictionary_encoded_chunks
381    }
382
383    /// Returns configured writer version.
384    ///
385    /// For more details see [`WriterPropertiesBuilder::set_writer_version`]
386    pub fn writer_version(&self) -> WriterVersion {
387        self.writer_version
388    }
389
390    /// Returns `created_by` string.
391    ///
392    /// For more details see [`WriterPropertiesBuilder::set_created_by`]
393    pub fn created_by(&self) -> &str {
394        &self.created_by
395    }
396
397    /// Returns `true` if offset index writing is disabled.
398    ///
399    /// For more details see [`WriterPropertiesBuilder::set_offset_index_disabled`]
400    pub fn offset_index_disabled(&self) -> bool {
401        matches!(self.offset_index_setting, OffsetIndexSetting::Disabled)
402    }
403
404    /// Returns `key_value_metadata` KeyValue pairs.
405    ///
406    /// For more details see [`WriterPropertiesBuilder::set_key_value_metadata`]
407    pub fn key_value_metadata(&self) -> Option<&Vec<KeyValue>> {
408        self.key_value_metadata.as_ref()
409    }
410
411    /// Returns sorting columns.
412    ///
413    /// For more details see [`WriterPropertiesBuilder::set_sorting_columns`]
414    pub fn sorting_columns(&self) -> Option<&Vec<SortingColumn>> {
415        self.sorting_columns.as_ref()
416    }
417
418    /// Returns the maximum length of truncated min/max values in the column index.
419    ///
420    /// `None` if truncation is disabled, must be greater than 0 otherwise.
421    ///
422    /// For more details see [`WriterPropertiesBuilder::set_column_index_truncate_length`]
423    pub fn column_index_truncate_length(&self) -> Option<usize> {
424        self.column_index_truncate_length
425    }
426
427    /// Returns the maximum length of truncated min/max values in [`Statistics`].
428    ///
429    /// `None` if truncation is disabled, must be greater than 0 otherwise.
430    ///
431    /// For more details see [`WriterPropertiesBuilder::set_statistics_truncate_length`]
432    ///
433    /// [`Statistics`]: crate::file::statistics::Statistics
434    pub fn statistics_truncate_length(&self) -> Option<usize> {
435        self.statistics_truncate_length
436    }
437
438    /// Returns `true` if type coercion is enabled.
439    ///
440    /// For more details see [`WriterPropertiesBuilder::set_coerce_types`]
441    pub fn coerce_types(&self) -> bool {
442        self.coerce_types
443    }
444
445    /// Returns `true` if the writer should compute and store the distinct count
446    /// (`num_distinct_values`) in row group column chunk statistics.
447    ///
448    /// For more details see [`WriterPropertiesBuilder::set_write_row_group_number_distinct_values`]
449    pub fn write_row_group_number_distinct_values(&self) -> bool {
450        self.write_row_group_number_distinct_values
451    }
452
453    /// Returns `true` if the `path_in_schema` field of the `ColumnMetaData` Thrift struct
454    /// should be written.
455    ///
456    /// For more details see [`WriterPropertiesBuilder::set_write_path_in_schema`]
457    pub fn write_path_in_schema(&self) -> bool {
458        self.write_path_in_schema
459    }
460
461    /// EXPERIMENTAL: Returns content-defined chunking options, or `None` if CDC is disabled.
462    ///
463    /// For more details see [`WriterPropertiesBuilder::set_content_defined_chunking`]
464    pub fn content_defined_chunking(&self) -> Option<&CdcOptions> {
465        self.content_defined_chunking.as_ref()
466    }
467
468    /// Returns the compression ratio threshold at or above which a Data Page v2's
469    /// compressed values are discarded in favor of writing the values uncompressed.
470    ///
471    /// For more details see [`WriterPropertiesBuilder::set_data_page_v2_compression_ratio_threshold`]
472    pub fn data_page_v2_compression_ratio_threshold(&self) -> f64 {
473        self.default_column_properties
474            .data_page_v2_compression_ratio_threshold()
475            .unwrap_or(DEFAULT_DATA_PAGE_V2_COMPRESSION_RATIO_THRESHOLD)
476    }
477
478    /// Returns the Data Page v2 compression ratio threshold for a specific column.
479    ///
480    /// Takes precedence over [`Self::data_page_v2_compression_ratio_threshold`].
481    pub fn column_data_page_v2_compression_ratio_threshold(&self, col: &ColumnPath) -> f64 {
482        resolve_data_page_v2_compression_ratio_threshold(
483            self.column_override(col),
484            &self.default_column_properties,
485        )
486    }
487
488    /// Returns encoding for a data page, when dictionary encoding is enabled.
489    ///
490    /// This is not configurable.
491    #[inline]
492    pub fn dictionary_data_page_encoding(&self) -> Encoding {
493        // PLAIN_DICTIONARY encoding is deprecated in writer version 1.
494        // Dictionary values are encoded using RLE_DICTIONARY encoding.
495        Encoding::RLE_DICTIONARY
496    }
497
498    /// Returns encoding for dictionary page, when dictionary encoding is enabled.
499    ///
500    /// This is not configurable.
501    #[inline]
502    pub fn dictionary_page_encoding(&self) -> Encoding {
503        // PLAIN_DICTIONARY is deprecated in writer version 1.
504        // Dictionary is encoded using plain encoding.
505        Encoding::PLAIN
506    }
507
508    /// Returns encoding for a column, if set.
509    ///
510    /// In case when dictionary is enabled, returns fallback encoding.
511    ///
512    /// If encoding is not set, then column writer will choose the best encoding
513    /// based on the column type.
514    pub fn encoding(&self, col: &ColumnPath) -> Option<Encoding> {
515        resolve_encoding(self.column_override(col), &self.default_column_properties)
516    }
517
518    /// Returns compression codec for a column.
519    ///
520    /// For more details see [`WriterPropertiesBuilder::set_column_compression`]
521    pub fn compression(&self, col: &ColumnPath) -> Compression {
522        resolve_compression(self.column_override(col), &self.default_column_properties)
523    }
524
525    /// Returns `true` if dictionary encoding is enabled for a column.
526    ///
527    /// For more details see [`WriterPropertiesBuilder::set_dictionary_enabled`]
528    pub fn dictionary_enabled(&self, col: &ColumnPath) -> bool {
529        resolve_dictionary_enabled(self.column_override(col), &self.default_column_properties)
530    }
531
532    /// Returns which statistics are written for a column.
533    ///
534    /// For more details see [`WriterPropertiesBuilder::set_statistics_enabled`]
535    pub fn statistics_enabled(&self, col: &ColumnPath) -> EnabledStatistics {
536        resolve_statistics_enabled(self.column_override(col), &self.default_column_properties)
537    }
538
539    /// Returns `true` if [`Statistics`] are to be written to the page header for a column.
540    ///
541    /// For more details see [`WriterPropertiesBuilder::set_write_page_header_statistics`]
542    ///
543    /// [`Statistics`]: crate::file::statistics::Statistics
544    pub fn write_page_header_statistics(&self, col: &ColumnPath) -> bool {
545        resolve_write_page_header_statistics(
546            self.column_override(col),
547            &self.default_column_properties,
548        )
549    }
550
551    /// Returns the [`BloomFilterProperties`] for the given column
552    ///
553    /// Returns `None` if bloom filter is disabled
554    ///
555    /// For more details see [`WriterPropertiesBuilder::set_column_bloom_filter_enabled`]
556    pub fn bloom_filter_properties(&self, col: &ColumnPath) -> Option<&BloomFilterProperties> {
557        resolve_bloom_filter_properties(self.column_override(col), &self.default_column_properties)
558    }
559
560    /// Returns the per-column override entry for `col`, if any.
561    ///
562    /// This is the only place the per-column map is searched. Searching it hashes
563    /// `col`, which is a `Vec<String>`, so callers that need more than one setting
564    /// should go through [`Self::resolve_column_properties`] rather than call
565    /// several single-setting accessors.
566    #[inline]
567    fn column_override(&self, col: &ColumnPath) -> Option<&ColumnProperties> {
568        self.column_properties.get(col)
569    }
570
571    /// Resolves every per-column writer setting for `col` with a single search of
572    /// the per-column override map.
573    ///
574    /// A column writer needs most of these settings, and needs some of them again
575    /// on every batch and every page, so it resolves them once when it is created
576    /// and reads the result from then on.
577    pub(crate) fn resolve_column_properties(&self, col: &ColumnPath) -> ResolvedColumnProperties {
578        let column = self.column_override(col);
579        let default = &self.default_column_properties;
580        ResolvedColumnProperties {
581            encoding: resolve_encoding(column, default),
582            compression: resolve_compression(column, default),
583            dictionary_enabled: resolve_dictionary_enabled(column, default),
584            statistics_enabled: resolve_statistics_enabled(column, default),
585            write_page_header_statistics: resolve_write_page_header_statistics(column, default),
586            data_page_size_limit: resolve_data_page_size_limit(column, default),
587            dictionary_page_size_limit: resolve_dictionary_page_size_limit(column, default),
588            data_page_v2_compression_ratio_threshold:
589                resolve_data_page_v2_compression_ratio_threshold(column, default),
590            bloom_filter_properties: resolve_bloom_filter_properties(column, default).cloned(),
591        }
592    }
593
594    /// Return file encryption properties
595    ///
596    /// For more details see [`WriterPropertiesBuilder::with_file_encryption_properties`]
597    #[cfg(feature = "encryption")]
598    pub fn file_encryption_properties(&self) -> Option<&Arc<FileEncryptionProperties>> {
599        self.file_encryption_properties.as_ref()
600    }
601}
602
603/// Builder for  [`WriterProperties`] Parquet writer configuration.
604///
605/// See example on [`WriterProperties`]
606#[derive(Debug, Clone)]
607pub struct WriterPropertiesBuilder {
608    data_page_row_count_limit: usize,
609    write_batch_size: usize,
610    max_row_group_row_count: Option<usize>,
611    max_row_group_bytes: Option<usize>,
612    bloom_filter_position: BloomFilterPosition,
613    bloom_filter_for_dictionary_encoded_chunks: bool,
614    writer_version: WriterVersion,
615    created_by: String,
616    offset_index_disabled: bool,
617    key_value_metadata: Option<Vec<KeyValue>>,
618    default_column_properties: ColumnProperties,
619    column_properties: HashMap<ColumnPath, ColumnProperties>,
620    sorting_columns: Option<Vec<SortingColumn>>,
621    column_index_truncate_length: Option<usize>,
622    statistics_truncate_length: Option<usize>,
623    coerce_types: bool,
624    write_row_group_number_distinct_values: bool,
625    content_defined_chunking: Option<CdcOptions>,
626    write_path_in_schema: bool,
627    #[cfg(feature = "encryption")]
628    file_encryption_properties: Option<Arc<FileEncryptionProperties>>,
629}
630
631impl Default for WriterPropertiesBuilder {
632    /// Returns default state of the builder.
633    fn default() -> Self {
634        Self {
635            data_page_row_count_limit: DEFAULT_DATA_PAGE_ROW_COUNT_LIMIT,
636            write_batch_size: DEFAULT_WRITE_BATCH_SIZE,
637            max_row_group_row_count: Some(DEFAULT_MAX_ROW_GROUP_ROW_COUNT),
638            max_row_group_bytes: None,
639            bloom_filter_position: DEFAULT_BLOOM_FILTER_POSITION,
640            bloom_filter_for_dictionary_encoded_chunks:
641                DEFAULT_BLOOM_FILTER_FOR_DICTIONARY_ENCODED_CHUNKS,
642            writer_version: DEFAULT_WRITER_VERSION,
643            created_by: DEFAULT_CREATED_BY.to_string(),
644            offset_index_disabled: DEFAULT_OFFSET_INDEX_DISABLED,
645            key_value_metadata: None,
646            default_column_properties: Default::default(),
647            column_properties: HashMap::new(),
648            sorting_columns: None,
649            column_index_truncate_length: DEFAULT_COLUMN_INDEX_TRUNCATE_LENGTH,
650            statistics_truncate_length: DEFAULT_STATISTICS_TRUNCATE_LENGTH,
651            coerce_types: DEFAULT_COERCE_TYPES,
652            write_row_group_number_distinct_values: DEFAULT_WRITE_ROW_GROUP_NUMBER_DISTINCT_VALUES,
653            content_defined_chunking: None,
654            write_path_in_schema: DEFAULT_WRITE_PATH_IN_SCHEMA,
655            #[cfg(feature = "encryption")]
656            file_encryption_properties: None,
657        }
658    }
659}
660
661impl WriterPropertiesBuilder {
662    /// Finalizes the configuration and returns immutable writer properties struct.
663    pub fn build(self) -> WriterProperties {
664        // Pre-compute offset_index_setting
665        let offset_index_setting = if self.offset_index_disabled {
666            let default_page_stats_enabled = self.default_column_properties.statistics_enabled()
667                == Some(EnabledStatistics::Page);
668            let column_page_stats_enabled = self.column_properties.iter().any(|path_props| {
669                path_props.1.statistics_enabled() == Some(EnabledStatistics::Page)
670            });
671            if default_page_stats_enabled || column_page_stats_enabled {
672                OffsetIndexSetting::DisabledOverridden
673            } else {
674                OffsetIndexSetting::Disabled
675            }
676        } else {
677            OffsetIndexSetting::Enabled
678        };
679
680        // Resolve bloom filter NDV for columns where it wasn't explicitly set:
681        // default to max_row_group_row_count so the filter is never undersized.
682        let default_ndv = self
683            .max_row_group_row_count
684            .unwrap_or(DEFAULT_MAX_ROW_GROUP_ROW_COUNT) as u64;
685        let mut default_column_properties = self.default_column_properties;
686        default_column_properties.resolve_bloom_filter_ndv(default_ndv);
687        let mut column_properties = self.column_properties;
688        for props in column_properties.values_mut() {
689            props.resolve_bloom_filter_ndv(default_ndv);
690        }
691
692        WriterProperties {
693            data_page_row_count_limit: self.data_page_row_count_limit,
694            write_batch_size: self.write_batch_size,
695            max_row_group_row_count: self.max_row_group_row_count,
696            max_row_group_bytes: self.max_row_group_bytes,
697            bloom_filter_position: self.bloom_filter_position,
698            bloom_filter_for_dictionary_encoded_chunks: self
699                .bloom_filter_for_dictionary_encoded_chunks,
700            writer_version: self.writer_version,
701            created_by: self.created_by,
702            offset_index_setting,
703            key_value_metadata: self.key_value_metadata,
704            default_column_properties,
705            column_properties,
706            sorting_columns: self.sorting_columns,
707            column_index_truncate_length: self.column_index_truncate_length,
708            statistics_truncate_length: self.statistics_truncate_length,
709            coerce_types: self.coerce_types,
710            write_row_group_number_distinct_values: self.write_row_group_number_distinct_values,
711            content_defined_chunking: self.content_defined_chunking,
712            write_path_in_schema: self.write_path_in_schema,
713            #[cfg(feature = "encryption")]
714            file_encryption_properties: self.file_encryption_properties,
715        }
716    }
717
718    // ----------------------------------------------------------------------
719    // Writer properties related to a file
720
721    /// Sets the `WriterVersion` written into the parquet metadata (defaults to [`PARQUET_1_0`]
722    /// via [`DEFAULT_WRITER_VERSION`])
723    ///
724    /// This value can determine what features some readers will support.
725    ///
726    /// [`PARQUET_1_0`]: [WriterVersion::PARQUET_1_0]
727    pub fn set_writer_version(mut self, value: WriterVersion) -> Self {
728        self.writer_version = value;
729        self
730    }
731
732    /// Sets best effort maximum number of rows in a data page (defaults to `20_000`
733    /// via [`DEFAULT_DATA_PAGE_ROW_COUNT_LIMIT`]).
734    ///
735    /// The parquet writer will attempt to limit the number of rows in
736    /// each `DataPage` to this value. Reducing this value will result
737    /// in larger parquet files, but may improve the effectiveness of
738    /// page index based predicate pushdown during reading.
739    ///
740    /// Note: this is a best effort limit based on value of
741    /// [`set_write_batch_size`](Self::set_write_batch_size).
742    ///
743    /// # Panics
744    /// If the value is `0`.
745    pub fn set_data_page_row_count_limit(mut self, value: usize) -> Self {
746        assert_ne!(value, 0, "Cannot have a 0 data page row count limit");
747        self.data_page_row_count_limit = value;
748        self
749    }
750
751    /// Sets write batch size (defaults to 1024 via [`DEFAULT_WRITE_BATCH_SIZE`]).
752    ///
753    /// For performance reasons, data for each column is written in
754    /// batches of this size.
755    ///
756    /// Additional limits such as such as
757    /// [`set_data_page_row_count_limit`](Self::set_data_page_row_count_limit)
758    /// are checked between batches, and thus the write batch size value acts as an
759    /// upper-bound on the enforcement granularity of other limits.
760    ///
761    /// # Panics
762    /// If the value is `0`.
763    pub fn set_write_batch_size(mut self, value: usize) -> Self {
764        assert_ne!(value, 0, "Cannot have a 0 write batch size");
765        self.write_batch_size = value;
766        self
767    }
768
769    /// Sets maximum number of rows in a row group, or `None` for unlimited.
770    ///
771    /// If both `max_row_group_row_count` and `max_row_group_bytes` are set,
772    /// the row group with the smaller limit will be produced.
773    ///
774    /// # Panics
775    /// If the value is `Some(0)`.
776    pub fn set_max_row_group_row_count(mut self, value: Option<usize>) -> Self {
777        assert_ne!(value, Some(0), "Cannot have a 0 max row group row count");
778        self.max_row_group_row_count = value;
779        self
780    }
781
782    /// Sets maximum size of a row group in bytes, or `None` for unlimited.
783    ///
784    /// Row groups are flushed when their estimated encoded size exceeds this threshold.
785    /// This is similar to the official Java implementation for `parquet.block.size`'s behavior.
786    ///
787    /// If both `max_row_group_row_count` and `max_row_group_bytes` are set,
788    /// the row group with the smaller limit will be produced.
789    ///
790    /// # Panics
791    /// If the value is `Some(0)`.
792    pub fn set_max_row_group_bytes(mut self, value: Option<usize>) -> Self {
793        assert_ne!(value, Some(0), "Cannot have a 0 max row group bytes");
794        self.max_row_group_bytes = value;
795        self
796    }
797
798    /// Sets where in the final file Bloom Filters are written (defaults to  [`AfterRowGroup`]
799    /// via [`DEFAULT_BLOOM_FILTER_POSITION`])
800    ///
801    /// [`AfterRowGroup`]: BloomFilterPosition::AfterRowGroup
802    pub fn set_bloom_filter_position(mut self, value: BloomFilterPosition) -> Self {
803        self.bloom_filter_position = value;
804        self
805    }
806
807    /// Sets whether a column chunk whose data pages are all dictionary encoded gets a bloom
808    /// filter (defaults to `true` via [`DEFAULT_BLOOM_FILTER_FOR_DICTIONARY_ENCODED_CHUNKS`]).
809    ///
810    /// The dictionary page of such a chunk already holds every distinct value, so its bloom
811    /// filter is redundant; set this to `false` to skip writing it and save the space.
812    pub fn set_bloom_filter_for_dictionary_encoded_chunks(mut self, value: bool) -> Self {
813        self.bloom_filter_for_dictionary_encoded_chunks = value;
814        self
815    }
816
817    /// Sets "created by" property (defaults to `parquet-rs version <VERSION>` via
818    /// [`DEFAULT_CREATED_BY`]).
819    ///
820    /// This is a string that will be written into the file metadata
821    pub fn set_created_by(mut self, value: String) -> Self {
822        self.created_by = value;
823        self
824    }
825
826    /// Sets whether the writing of offset indexes is disabled (defaults to `false` via
827    /// [`DEFAULT_OFFSET_INDEX_DISABLED`]).
828    ///
829    /// If statistics level is set to [`Page`] this setting will be overridden with `false`.
830    ///
831    /// Note: As the offset indexes are useful for accessing data by row number,
832    /// they are always written by default, regardless of whether other statistics
833    /// are enabled. Disabling this metadata may result in a degradation in read
834    /// performance, so use this option with care.
835    ///
836    /// [`Page`]: EnabledStatistics::Page
837    pub fn set_offset_index_disabled(mut self, value: bool) -> Self {
838        self.offset_index_disabled = value;
839        self
840    }
841
842    /// Sets "key_value_metadata" property (defaults to `None`).
843    pub fn set_key_value_metadata(mut self, value: Option<Vec<KeyValue>>) -> Self {
844        self.key_value_metadata = value;
845        self
846    }
847
848    /// Sets sorting order of rows in the row group if any (defaults to `None`).
849    pub fn set_sorting_columns(mut self, value: Option<Vec<SortingColumn>>) -> Self {
850        self.sorting_columns = value;
851        self
852    }
853
854    /// Sets the max length of min/max value fields when writing the column
855    /// [`Index`] (defaults to `Some(64)` via [`DEFAULT_COLUMN_INDEX_TRUNCATE_LENGTH`]).
856    ///
857    /// This can be used to prevent columns with very long values (hundreds of
858    /// bytes long) from causing the parquet metadata to become huge.
859    ///
860    /// # Notes
861    ///
862    /// The column [`Index`] is written when [`Self::set_statistics_enabled`] is
863    /// set to [`EnabledStatistics::Page`].
864    ///
865    /// * If `Some`, must be greater than 0, otherwise will panic
866    /// * If `None`, there's no effective limit.
867    ///
868    /// [`Index`]: crate::file::page_index::column_index::ColumnIndexMetaData
869    ///
870    /// # Panics
871    ///
872    /// Panics if `max_length` is `Some(0)`
873    pub fn set_column_index_truncate_length(mut self, max_length: Option<usize>) -> Self {
874        if let Some(value) = max_length {
875            assert!(
876                value > 0,
877                "Cannot have a 0 column index truncate length. If you wish to disable min/max value truncation, set it to `None`."
878            );
879        }
880
881        self.column_index_truncate_length = max_length;
882        self
883    }
884
885    /// Sets the max length of min/max value fields in row group and data page header
886    /// [`Statistics`] (defaults to `Some(64)` via [`DEFAULT_STATISTICS_TRUNCATE_LENGTH`]).
887    ///
888    /// # Notes
889    /// Row group [`Statistics`] are written when [`Self::set_statistics_enabled`] is
890    /// set to [`EnabledStatistics::Chunk`] or [`EnabledStatistics::Page`]. Data page header
891    /// [`Statistics`] are written when [`Self::set_statistics_enabled`] is set to
892    /// [`EnabledStatistics::Page`].
893    ///
894    /// * If `Some`, must be greater than 0, otherwise will panic
895    /// * If `None`, there's no effective limit.
896    ///
897    /// # See also
898    /// Truncation of Page Index statistics is controlled separately via
899    /// [`WriterPropertiesBuilder::set_column_index_truncate_length`]
900    ///
901    /// [`Statistics`]: crate::file::statistics::Statistics
902    ///
903    /// # Panics
904    ///
905    /// Panics if `max_length` is `Some(0)`
906    pub fn set_statistics_truncate_length(mut self, max_length: Option<usize>) -> Self {
907        if let Some(value) = max_length {
908            assert!(
909                value > 0,
910                "Cannot have a 0 statistics truncate length. If you wish to disable min/max value truncation, set it to `None`."
911            );
912        }
913
914        self.statistics_truncate_length = max_length;
915        self
916    }
917
918    /// Should the writer coerce types to parquet native types (defaults to `false` via
919    /// [`DEFAULT_COERCE_TYPES`]).
920    ///
921    /// Leaving this option the default `false` will ensure the exact same data
922    /// written to parquet using this library will be read.
923    ///
924    /// Setting this option to `true` will result in parquet files that can be
925    /// read by more readers, but potentially lose information in the process.
926    ///
927    /// * Types such as [`DataType::Date64`], which have no direct corresponding
928    ///   Parquet type, may be stored with lower precision.
929    ///
930    /// * The internal field names of `List` and `Map` types will be renamed if
931    ///   necessary to match what is required by the newest Parquet specification.
932    ///
933    /// See [`ArrowToParquetSchemaConverter::with_coerce_types`] for more details
934    ///
935    /// [`DataType::Date64`]: arrow_schema::DataType::Date64
936    /// [`ArrowToParquetSchemaConverter::with_coerce_types`]: crate::arrow::ArrowSchemaConverter::with_coerce_types
937    pub fn set_coerce_types(mut self, coerce_types: bool) -> Self {
938        self.coerce_types = coerce_types;
939        self
940    }
941
942    /// Enable or disable writing the distinct value count (`num_distinct_values`) into
943    /// row group column chunk statistics (defaults to `false` via
944    /// [`DEFAULT_WRITE_ROW_GROUP_NUMBER_DISTINCT_VALUES`]).
945    ///
946    /// When enabled, the [`ArrowWriter`] scans each column's values before encoding
947    /// and stores the number of distinct non-null values in the row group statistics
948    /// footer.
949    ///
950    /// # Compatibility
951    ///
952    /// This setting only takes effect when using [`ArrowWriter`]. The row-based
953    /// [`SerializedFileWriter`] / [`SerializedRowGroupWriter`] APIs do not populate
954    /// `num_distinct_values` and will ignore this flag.
955    ///
956    /// # Performance
957    ///
958    /// Computing the distinct count requires hashing every non-null value in the column.
959    /// For large row groups or columns with many values this adds significant overhead.
960    /// Benchmark your workload before enabling this globally.
961    ///
962    /// [`ArrowWriter`]: crate::arrow::ArrowWriter
963    /// [`SerializedFileWriter`]: crate::file::writer::SerializedFileWriter
964    /// [`SerializedRowGroupWriter`]: crate::file::writer::SerializedRowGroupWriter
965    pub fn set_write_row_group_number_distinct_values(mut self, value: bool) -> Self {
966        self.write_row_group_number_distinct_values = value;
967        self
968    }
969
970    /// EXPERIMENTAL: Should the writer emit the `path_in_schema` element of the
971    /// `ColumnMetaData` Thrift struct. Defaults to `true` via [`DEFAULT_WRITE_PATH_IN_SCHEMA`].
972    ///
973    /// Because `path_in_schema` is a field on the `ColumnMetaData`, it is repeated
974    /// `num_columns * num_rowgroups` times. Compounding this is any level of nesting or
975    /// repetition in the schema. For instance, a top-level list column named `foo` will have
976    /// a `path_in_schema` of `["foo", "list", "element"]`. A list-of-struct is even worse,
977    /// because the necessary list wrapping is repeated for each element of the struct. A
978    /// file with a deeply nested schema and many row groups can have a large percentage of the
979    /// footer taken up by this field. For example, a file of 38 row groups with a schema containing
980    /// several lists of structs containing lists had 36% of the footer taken up by `path_in_schema`.
981    /// Removing this redundant information can greatly speed up footer parsing, which is particularly
982    /// important in scenarios where one does not wish to read the entire file (e.g. point
983    /// lookups).
984    ///
985    /// <div class="warning">
986    ///
987    /// **WARNING:**
988    /// Setting this to `false` will break compatibility with Parquet readers that
989    /// still expect this field to be present. Virtually all Parquet readers (parquet-java,
990    /// Spark, arrow-cpp, pyarrow, pandas to name a few), with the exception
991    /// of the one in this crate, expect this field to be present, and will terminate execution
992    /// if it is not. This will continue to be the case unless/until the Parquet format
993    /// specification is explicitly changed to allow this field to be missing. As a consequence,
994    /// users should only set this to `false` if they have verified that any reader(s) they plan
995    /// to use can tolerate the absence of this field.
996    ///
997    /// For more context, see [GH-563].
998    ///
999    /// </div>
1000    ///
1001    /// [GH-563]: https://github.com/apache/parquet-format/issues/563
1002    pub fn set_write_path_in_schema(mut self, write_path_in_schema: bool) -> Self {
1003        self.write_path_in_schema = write_path_in_schema;
1004        self
1005    }
1006
1007    /// EXPERIMENTAL: Sets content-defined chunking options, or disables CDC with `None`.
1008    ///
1009    /// When enabled, data page boundaries are determined by a rolling hash of the
1010    /// column values, so unchanged data produces identical byte sequences across
1011    /// file versions. This enables efficient deduplication on content-addressable
1012    /// storage systems.
1013    ///
1014    /// Only supported through the Arrow writer interface ([`ArrowWriter`]).
1015    ///
1016    /// # Panics
1017    ///
1018    /// Panics if `min_chunk_size == 0` or `max_chunk_size <= min_chunk_size`.
1019    ///
1020    /// [`ArrowWriter`]: crate::arrow::arrow_writer::ArrowWriter
1021    pub fn set_content_defined_chunking(mut self, options: Option<CdcOptions>) -> Self {
1022        if let Some(ref options) = options {
1023            assert!(
1024                options.min_chunk_size > 0,
1025                "min_chunk_size must be positive"
1026            );
1027            assert!(
1028                options.max_chunk_size > options.min_chunk_size,
1029                "max_chunk_size ({}) must be greater than min_chunk_size ({})",
1030                options.max_chunk_size,
1031                options.min_chunk_size
1032            );
1033        }
1034        self.content_defined_chunking = options;
1035        self
1036    }
1037
1038    /// Sets the default compression ratio threshold at or above which a Data Page
1039    /// v2's compressed values are discarded in favor of writing the values
1040    /// uncompressed, for all columns (defaults to `1.0` via
1041    /// [`DEFAULT_DATA_PAGE_V2_COMPRESSION_RATIO_THRESHOLD`]).
1042    ///
1043    /// When writing a Data Page v2 with a configured compression codec, the writer
1044    /// first compresses the values and then compares the compressed size to the
1045    /// uncompressed size. If `compressed_size >= uncompressed_size * threshold`, the
1046    /// compressed buffer is discarded and the values are written uncompressed for
1047    /// that page (the page's `is_compressed` flag is set to `false`).
1048    ///
1049    /// The default of `1.0` preserves the historical behavior of only keeping
1050    /// compression when it strictly reduces the size. Setting a value below `1.0`
1051    /// requires a minimum amount of size reduction to keep the compressed page —
1052    /// for example `0.9` requires at least a 10% reduction. Setting a value above
1053    /// `1.0` keeps the compressed buffer even if it's somewhat larger than the
1054    /// uncompressed values.
1055    ///
1056    /// This setting only affects Data Page v2; Data Page v1 always stores the
1057    /// compressor's output regardless of the resulting size.
1058    ///
1059    /// # Panics
1060    /// If `value` is not finite or is not strictly positive.
1061    pub fn set_data_page_v2_compression_ratio_threshold(mut self, value: f64) -> Self {
1062        self.default_column_properties
1063            .set_data_page_v2_compression_ratio_threshold(value);
1064        self
1065    }
1066
1067    /// Sets FileEncryptionProperties (defaults to `None`)
1068    #[cfg(feature = "encryption")]
1069    pub fn with_file_encryption_properties(
1070        mut self,
1071        file_encryption_properties: Arc<FileEncryptionProperties>,
1072    ) -> Self {
1073        self.file_encryption_properties = Some(file_encryption_properties);
1074        self
1075    }
1076
1077    // ----------------------------------------------------------------------
1078    // Setters for any column (global)
1079
1080    /// Sets default encoding for all columns.
1081    ///
1082    /// If dictionary is not enabled, this is treated as a primary encoding for all
1083    /// columns. In case when dictionary is enabled for any column, this value is
1084    /// considered to be a fallback encoding for that column.
1085    ///
1086    /// # Panics
1087    ///
1088    /// if dictionary encoding is specified, regardless of dictionary
1089    /// encoding flag being set.
1090    pub fn set_encoding(mut self, value: Encoding) -> Self {
1091        self.default_column_properties.set_encoding(value);
1092        self
1093    }
1094
1095    /// Sets default compression codec for all columns (default to [`UNCOMPRESSED`] via
1096    /// [`DEFAULT_COMPRESSION`]).
1097    ///
1098    /// [`UNCOMPRESSED`]: Compression::UNCOMPRESSED
1099    pub fn set_compression(mut self, value: Compression) -> Self {
1100        self.default_column_properties.set_compression(value);
1101        self
1102    }
1103
1104    /// Sets default flag to enable/disable dictionary encoding for all columns (defaults to `true`
1105    /// via [`DEFAULT_DICTIONARY_ENABLED`]).
1106    ///
1107    /// Use this method to set dictionary encoding, instead of explicitly specifying
1108    /// encoding in `set_encoding` method.
1109    pub fn set_dictionary_enabled(mut self, value: bool) -> Self {
1110        self.default_column_properties.set_dictionary_enabled(value);
1111        self
1112    }
1113
1114    /// Sets best effort maximum dictionary page size, in bytes (defaults to `1024 * 1024`
1115    /// via [`DEFAULT_DICTIONARY_PAGE_SIZE_LIMIT`]).
1116    ///
1117    /// The parquet writer will attempt to limit the size of each
1118    /// `DataPage` used to store dictionaries to this many
1119    /// bytes. Reducing this value will result in larger parquet
1120    /// files, but may improve the effectiveness of page index based
1121    /// predicate pushdown during reading.
1122    ///
1123    /// Note: this is a best effort limit based on value of
1124    /// [`set_write_batch_size`](Self::set_write_batch_size).
1125    pub fn set_dictionary_page_size_limit(mut self, value: usize) -> Self {
1126        self.default_column_properties
1127            .set_dictionary_page_size_limit(value);
1128        self
1129    }
1130
1131    /// Sets best effort maximum size of a data page in bytes (defaults to `1024 * 1024`
1132    /// via [`DEFAULT_PAGE_SIZE`]).
1133    ///
1134    /// The parquet writer will attempt to limit the sizes of each
1135    /// `DataPage` to this many bytes. Reducing this value will result
1136    /// in larger parquet files, but may improve the effectiveness of
1137    /// page index based predicate pushdown during reading.
1138    ///
1139    /// Note: this is a best effort limit based on value of
1140    /// [`set_write_batch_size`](Self::set_write_batch_size).
1141    pub fn set_data_page_size_limit(mut self, value: usize) -> Self {
1142        self.default_column_properties
1143            .set_data_page_size_limit(value);
1144        self
1145    }
1146
1147    /// Sets default [`EnabledStatistics`] level for all columns (defaults to [`Page`] via
1148    /// [`DEFAULT_STATISTICS_ENABLED`]).
1149    ///
1150    /// [`Page`]: EnabledStatistics::Page
1151    pub fn set_statistics_enabled(mut self, value: EnabledStatistics) -> Self {
1152        self.default_column_properties.set_statistics_enabled(value);
1153        self
1154    }
1155
1156    /// enable/disable writing [`Statistics`] in the page header
1157    /// (defaults to `false` via [`DEFAULT_WRITE_PAGE_HEADER_STATISTICS`]).
1158    ///
1159    /// Only applicable if [`Page`] level statistics are gathered.
1160    ///
1161    /// Setting this value to `true` can greatly increase the size of the resulting Parquet
1162    /// file while yielding very little added benefit. Most modern Parquet implementations
1163    /// will use the min/max values stored in the [`PageIndex`] rather than
1164    /// those in the page header.
1165    ///
1166    /// # Note
1167    ///
1168    /// Prior to version 56.0.0, the `parquet` crate always wrote these
1169    /// statistics (the equivalent of setting this option to `true`). This was
1170    /// changed in 56.0.0 to follow the recommendation in the Parquet
1171    /// specification. See [issue #7580] for more details.
1172    ///
1173    /// [`Statistics`]: crate::file::statistics::Statistics
1174    /// [`PageIndex`]: crate::file::metadata::page_index::PageIndex
1175    /// [`Page`]: EnabledStatistics::Page
1176    /// [issue #7580]: https://github.com/apache/arrow-rs/issues/7580
1177    pub fn set_write_page_header_statistics(mut self, value: bool) -> Self {
1178        self.default_column_properties
1179            .set_write_page_header_statistics(value);
1180        self
1181    }
1182
1183    /// Sets if bloom filter should be written for all columns (defaults to `false`).
1184    ///
1185    /// # Notes
1186    ///
1187    /// * If the bloom filter is enabled previously then it is a no-op.
1188    ///
1189    /// * If the bloom filter is not enabled, default values for ndv and fpp
1190    ///   value are used used. See [`set_bloom_filter_max_ndv`] and
1191    ///   [`set_bloom_filter_fpp`] to further adjust the ndv and fpp.
1192    ///
1193    /// [`set_bloom_filter_max_ndv`]: Self::set_bloom_filter_max_ndv
1194    /// [`set_bloom_filter_fpp`]: Self::set_bloom_filter_fpp
1195    pub fn set_bloom_filter_enabled(mut self, value: bool) -> Self {
1196        self.default_column_properties
1197            .set_bloom_filter_enabled(value);
1198        self
1199    }
1200
1201    /// Sets the default target bloom filter false positive probability (fpp)
1202    /// for all columns (defaults to `0.05` via [`DEFAULT_BLOOM_FILTER_FPP`]).
1203    ///
1204    /// Implicitly enables bloom writing, as if [`set_bloom_filter_enabled`] had
1205    /// been called.
1206    ///
1207    /// [`set_bloom_filter_enabled`]: Self::set_bloom_filter_enabled
1208    pub fn set_bloom_filter_fpp(mut self, value: f64) -> Self {
1209        self.default_column_properties.set_bloom_filter_fpp(value);
1210        self
1211    }
1212
1213    /// Sets default maximum expected number of distinct values (ndv) for bloom filter
1214    /// for all columns (defaults to [`DEFAULT_BLOOM_FILTER_NDV`]).
1215    ///
1216    /// The bloom filter is initially sized for this many distinct values at the
1217    /// configured FPP, then folded down after all values are inserted to achieve
1218    /// optimal size. A good heuristic is to set this to the expected number of rows
1219    /// in the row group.
1220    ///
1221    /// Implicitly enables bloom writing, as if [`set_bloom_filter_enabled`] had
1222    /// been called.
1223    ///
1224    /// [`set_bloom_filter_enabled`]: Self::set_bloom_filter_enabled
1225    pub fn set_bloom_filter_max_ndv(mut self, value: u64) -> Self {
1226        self.default_column_properties.set_bloom_filter_ndv(value);
1227        self
1228    }
1229
1230    /// Deprecated alias for [`Self::set_bloom_filter_max_ndv`].
1231    #[deprecated(since = "59.0.0", note = "Use `set_bloom_filter_max_ndv` instead")]
1232    pub fn set_bloom_filter_ndv(self, value: u64) -> Self {
1233        self.set_bloom_filter_max_ndv(value)
1234    }
1235
1236    // ----------------------------------------------------------------------
1237    // Setters for a specific column
1238
1239    /// Helper method to get existing or new mutable reference of column properties.
1240    #[inline]
1241    fn get_mut_props(&mut self, col: ColumnPath) -> &mut ColumnProperties {
1242        self.column_properties.entry(col).or_default()
1243    }
1244
1245    /// Sets encoding for a specific column.
1246    ///
1247    /// Takes precedence over [`Self::set_encoding`].
1248    ///
1249    /// If dictionary is not enabled, this is treated as a primary encoding for this
1250    /// column. In case when dictionary is enabled for this column, either through
1251    /// global defaults or explicitly, this value is considered to be a fallback
1252    /// encoding for this column.
1253    ///
1254    /// # Panics
1255    /// If user tries to set dictionary encoding here, regardless of dictionary
1256    /// encoding flag being set.
1257    pub fn set_column_encoding(mut self, col: ColumnPath, value: Encoding) -> Self {
1258        self.get_mut_props(col).set_encoding(value);
1259        self
1260    }
1261
1262    /// Sets compression codec for a specific column.
1263    ///
1264    /// Takes precedence over [`Self::set_compression`].
1265    pub fn set_column_compression(mut self, col: ColumnPath, value: Compression) -> Self {
1266        self.get_mut_props(col).set_compression(value);
1267        self
1268    }
1269
1270    /// Sets flag to enable/disable dictionary encoding for a specific column.
1271    ///
1272    /// Takes precedence over [`Self::set_dictionary_enabled`].
1273    pub fn set_column_dictionary_enabled(mut self, col: ColumnPath, value: bool) -> Self {
1274        self.get_mut_props(col).set_dictionary_enabled(value);
1275        self
1276    }
1277
1278    /// Sets dictionary page size limit for a specific column.
1279    ///
1280    /// Takes precedence over [`Self::set_dictionary_page_size_limit`].
1281    pub fn set_column_dictionary_page_size_limit(mut self, col: ColumnPath, value: usize) -> Self {
1282        self.get_mut_props(col)
1283            .set_dictionary_page_size_limit(value);
1284        self
1285    }
1286
1287    /// Sets data page size limit for a specific column.
1288    ///
1289    /// Takes precedence over [`Self::set_data_page_size_limit`].
1290    pub fn set_column_data_page_size_limit(mut self, col: ColumnPath, value: usize) -> Self {
1291        self.get_mut_props(col).set_data_page_size_limit(value);
1292        self
1293    }
1294
1295    /// Sets [`EnabledStatistics`] level for a specific column.
1296    ///
1297    /// Takes precedence over [`Self::set_statistics_enabled`].
1298    pub fn set_column_statistics_enabled(
1299        mut self,
1300        col: ColumnPath,
1301        value: EnabledStatistics,
1302    ) -> Self {
1303        self.get_mut_props(col).set_statistics_enabled(value);
1304        self
1305    }
1306
1307    /// Sets whether to write [`Statistics`] in the page header for a specific column.
1308    ///
1309    /// Takes precedence over [`Self::set_write_page_header_statistics`].
1310    ///
1311    /// [`Statistics`]: crate::file::statistics::Statistics
1312    pub fn set_column_write_page_header_statistics(mut self, col: ColumnPath, value: bool) -> Self {
1313        self.get_mut_props(col)
1314            .set_write_page_header_statistics(value);
1315        self
1316    }
1317
1318    /// Sets whether a bloom filter should be written for a specific column.
1319    ///
1320    /// Takes precedence over [`Self::set_bloom_filter_enabled`].
1321    pub fn set_column_bloom_filter_enabled(mut self, col: ColumnPath, value: bool) -> Self {
1322        self.get_mut_props(col).set_bloom_filter_enabled(value);
1323        self
1324    }
1325
1326    /// Sets the false positive probability for bloom filter for a specific column.
1327    ///
1328    /// Takes precedence over [`Self::set_bloom_filter_fpp`].
1329    pub fn set_column_bloom_filter_fpp(mut self, col: ColumnPath, value: f64) -> Self {
1330        self.get_mut_props(col).set_bloom_filter_fpp(value);
1331        self
1332    }
1333
1334    /// Sets the maximum expected number of distinct values for bloom filter for
1335    /// a specific column.
1336    ///
1337    /// Takes precedence over [`Self::set_bloom_filter_max_ndv`].
1338    pub fn set_column_bloom_filter_max_ndv(mut self, col: ColumnPath, value: u64) -> Self {
1339        self.get_mut_props(col).set_bloom_filter_ndv(value);
1340        self
1341    }
1342
1343    /// Sets the Data Page v2 compression ratio threshold for a specific column.
1344    ///
1345    /// Takes precedence over [`Self::set_data_page_v2_compression_ratio_threshold`].
1346    ///
1347    /// # Panics
1348    /// If `value` is not finite or is not strictly positive.
1349    pub fn set_column_data_page_v2_compression_ratio_threshold(
1350        mut self,
1351        col: ColumnPath,
1352        value: f64,
1353    ) -> Self {
1354        self.get_mut_props(col)
1355            .set_data_page_v2_compression_ratio_threshold(value);
1356        self
1357    }
1358
1359    /// Deprecated alias for [`Self::set_column_bloom_filter_max_ndv`].
1360    #[deprecated(
1361        since = "59.0.0",
1362        note = "Use `set_column_bloom_filter_max_ndv` instead"
1363    )]
1364    pub fn set_column_bloom_filter_ndv(self, col: ColumnPath, value: u64) -> Self {
1365        self.set_column_bloom_filter_max_ndv(col, value)
1366    }
1367
1368    /// Sets the [`BloomFilterProperties`] for all columns, implicitly enabling
1369    /// the bloom filter.
1370    ///
1371    /// Both `fpp` and `ndv` from `value` are treated as explicit and will not
1372    /// be overridden by the build-time row-group-size NDV fallback. For
1373    /// dynamic NDV sizing (resolved to `max_row_group_row_count` at build
1374    /// time), use [`Self::set_bloom_filter_enabled`] or
1375    /// [`Self::set_bloom_filter_fpp`] instead.
1376    pub fn set_bloom_filter_properties(mut self, value: BloomFilterProperties) -> Self {
1377        self.default_column_properties
1378            .set_bloom_filter_properties(value);
1379        self
1380    }
1381
1382    /// Sets the [`BloomFilterProperties`] for a specific column.
1383    ///
1384    /// Takes precedence over [`Self::set_bloom_filter_properties`].
1385    pub fn set_column_bloom_filter_properties(
1386        mut self,
1387        col: ColumnPath,
1388        value: BloomFilterProperties,
1389    ) -> Self {
1390        self.get_mut_props(col).set_bloom_filter_properties(value);
1391        self
1392    }
1393}
1394
1395impl From<WriterProperties> for WriterPropertiesBuilder {
1396    fn from(props: WriterProperties) -> Self {
1397        WriterPropertiesBuilder {
1398            data_page_row_count_limit: props.data_page_row_count_limit,
1399            write_batch_size: props.write_batch_size,
1400            max_row_group_row_count: props.max_row_group_row_count,
1401            max_row_group_bytes: props.max_row_group_bytes,
1402            bloom_filter_position: props.bloom_filter_position,
1403            bloom_filter_for_dictionary_encoded_chunks: props
1404                .bloom_filter_for_dictionary_encoded_chunks,
1405            writer_version: props.writer_version,
1406            created_by: props.created_by,
1407            offset_index_disabled: !matches!(
1408                props.offset_index_setting,
1409                OffsetIndexSetting::Enabled
1410            ),
1411            key_value_metadata: props.key_value_metadata,
1412            default_column_properties: props.default_column_properties,
1413            column_properties: props.column_properties,
1414            sorting_columns: props.sorting_columns,
1415            column_index_truncate_length: props.column_index_truncate_length,
1416            statistics_truncate_length: props.statistics_truncate_length,
1417            coerce_types: props.coerce_types,
1418            write_row_group_number_distinct_values: props.write_row_group_number_distinct_values,
1419            content_defined_chunking: props.content_defined_chunking,
1420            write_path_in_schema: props.write_path_in_schema,
1421            #[cfg(feature = "encryption")]
1422            file_encryption_properties: props.file_encryption_properties,
1423        }
1424    }
1425}
1426
1427/// Controls the level of statistics to be computed by the writer and stored in
1428/// the parquet file.
1429///
1430/// Enabling statistics makes the resulting Parquet file larger and requires
1431/// more time to read the parquet footer.
1432///
1433/// Statistics can be used to improve query performance by pruning row groups
1434/// and pages during query execution if the query engine supports evaluating the
1435/// predicate using the statistics.
1436#[derive(Debug, Clone, Copy, Eq, PartialEq)]
1437pub enum EnabledStatistics {
1438    /// Compute no statistics.
1439    None,
1440    /// Compute column chunk-level statistics but not page-level.
1441    ///
1442    /// Setting this option will store one set of statistics for each relevant
1443    /// column for each row group. The more row groups written, the more
1444    /// statistics will be stored.
1445    Chunk,
1446    /// Compute page-level and column chunk-level statistics.
1447    ///
1448    /// Setting this option will store one set of statistics for each relevant
1449    /// column for each row group. In addition, this will enable the writing
1450    /// of the column index (the offset index is always written regardless of
1451    /// this setting). See [`PageIndex`] for
1452    /// more information.
1453    ///
1454    /// [`PageIndex`]: crate::file::metadata::page_index::PageIndex
1455    Page,
1456}
1457
1458impl FromStr for EnabledStatistics {
1459    type Err = String;
1460
1461    fn from_str(s: &str) -> Result<Self, Self::Err> {
1462        match s {
1463            "NONE" | "none" => Ok(EnabledStatistics::None),
1464            "CHUNK" | "chunk" => Ok(EnabledStatistics::Chunk),
1465            "PAGE" | "page" => Ok(EnabledStatistics::Page),
1466            _ => Err(format!("Invalid statistics arg: {s}")),
1467        }
1468    }
1469}
1470
1471impl Default for EnabledStatistics {
1472    fn default() -> Self {
1473        DEFAULT_STATISTICS_ENABLED
1474    }
1475}
1476
1477/// Controls the bloom filter to be computed by the writer.
1478///
1479/// The bloom filter is initially sized for `ndv` distinct values at the given `fpp`, then
1480/// automatically folded down after all values are inserted to achieve optimal size while
1481/// maintaining the target `fpp`. See [`Sbbf::fold_to_target_fpp`] for details on the
1482/// folding algorithm.
1483///
1484/// # Example
1485///
1486/// ```rust
1487/// # use parquet::{
1488/// #    file::properties::{BloomFilterProperties, WriterProperties},
1489/// #    schema::types::ColumnPath,
1490/// # };
1491/// // Build a BloomFilterProperties via the builder, then apply it to one column.
1492/// let bf = BloomFilterProperties::builder()
1493///     .with_fpp(0.01)
1494///     .with_max_ndv(10_000)
1495///     .build();
1496///
1497/// let props = WriterProperties::builder()
1498///     .set_column_bloom_filter_properties(ColumnPath::from("user_id"), bf.clone())
1499///     .build();
1500///
1501/// assert_eq!(
1502///     props.bloom_filter_properties(&ColumnPath::from("user_id")),
1503///     Some(&bf)
1504/// );
1505/// ```
1506///
1507/// [`Sbbf::fold_to_target_fpp`]: crate::bloom_filter::Sbbf::fold_to_target_fpp
1508#[derive(Debug, Clone, PartialEq)]
1509pub struct BloomFilterProperties {
1510    fpp: f64,
1511    ndv: u64,
1512}
1513
1514impl Default for BloomFilterProperties {
1515    fn default() -> Self {
1516        BloomFilterProperties {
1517            fpp: DEFAULT_BLOOM_FILTER_FPP,
1518            ndv: DEFAULT_BLOOM_FILTER_NDV,
1519        }
1520    }
1521}
1522
1523impl BloomFilterProperties {
1524    /// Returns a new [`BloomFilterPropertiesBuilder`] for constructing
1525    /// [`BloomFilterProperties`] with custom values.
1526    pub fn builder() -> BloomFilterPropertiesBuilder {
1527        BloomFilterPropertiesBuilder::new()
1528    }
1529
1530    /// False positive probability. This should be always between 0 and 1 exclusive. Defaults to [`DEFAULT_BLOOM_FILTER_FPP`].
1531    ///
1532    /// You should set this value by calling [`WriterPropertiesBuilder::set_bloom_filter_fpp`].
1533    ///
1534    /// The bloom filter data structure is a trade of between disk and memory space versus fpp, the
1535    /// smaller the fpp, the more memory and disk space is required, thus setting it to a reasonable value
1536    /// e.g. 0.1, 0.05, or 0.001 is recommended.
1537    ///
1538    /// This value also serves as the target FPP for bloom filter folding: after all values
1539    /// are inserted, the filter is folded down to the smallest size that still meets this FPP.
1540    pub fn fpp(&self) -> f64 {
1541        self.fpp
1542    }
1543
1544    /// Maximum expected number of distinct values. Defaults to [`DEFAULT_BLOOM_FILTER_NDV`].
1545    ///
1546    /// You should set this value by calling [`WriterPropertiesBuilder::set_bloom_filter_max_ndv`].
1547    ///
1548    /// When not explicitly set via the builder, this defaults to
1549    /// [`max_row_group_row_count`](WriterProperties::max_row_group_row_count) (resolved at
1550    /// build time). The bloom filter is initially sized for this many distinct values at the
1551    /// given `fpp`, then folded down after insertion to achieve optimal size. A good heuristic
1552    /// is to set this to the expected number of rows in the row group. If fewer distinct values
1553    /// are actually written, the filter will be automatically compacted via folding.
1554    ///
1555    /// Thus the only negative side of overestimating this value is that the bloom filter
1556    /// will use more memory during writing than necessary, but it will not affect the final
1557    /// bloom filter size on disk.
1558    ///
1559    /// If you wish to reduce memory usage during writing and are able to make a reasonable estimate
1560    /// of the number of distinct values in a row group, it is recommended to set this value explicitly
1561    /// rather than relying on the default dynamic sizing based on `max_row_group_row_count`.
1562    /// If you do set this value explicitly it is probably best to set it for each column
1563    /// individually via [`WriterPropertiesBuilder::set_column_bloom_filter_max_ndv`] rather than globally,
1564    /// since different columns may have different numbers of distinct values.
1565    pub fn ndv(&self) -> u64 {
1566        self.ndv
1567    }
1568}
1569
1570/// Builder for [`BloomFilterProperties`].
1571///
1572/// Use [`BloomFilterProperties::builder`] or [`BloomFilterPropertiesBuilder::new`]
1573/// as the entry point.
1574#[derive(Debug, Clone, Default)]
1575pub struct BloomFilterPropertiesBuilder {
1576    fpp: Option<f64>,
1577    ndv: Option<u64>,
1578}
1579
1580impl BloomFilterPropertiesBuilder {
1581    /// Returns a new builder with no fields set.
1582    ///
1583    /// Equivalent to [`BloomFilterProperties::builder`].
1584    pub fn new() -> Self {
1585        Self::default()
1586    }
1587
1588    /// Sets the target false positive probability.
1589    ///
1590    /// The value must be in `(0.0, 1.0)` exclusively; this is validated at
1591    /// build time by [`Self::build`] / [`Self::try_build`]. When unset, the
1592    /// default is `0.05` (5%, see [`DEFAULT_BLOOM_FILTER_FPP`]).
1593    pub fn with_fpp(mut self, fpp: f64) -> Self {
1594        self.fpp = Some(fpp);
1595        self
1596    }
1597
1598    /// Sets the maximum expected number of distinct values used to size the
1599    /// bloom filter before folding.
1600    ///
1601    /// When unset, the default is `1_048_576` (see [`DEFAULT_BLOOM_FILTER_NDV`]),
1602    /// which at the default fpp of 5% reserves roughly 1 MiB per column for the
1603    /// filter bitset, derived as follows:
1604    ///
1605    /// ```text
1606    /// ndv = 1,048,576, fpp = 0.05
1607    ///   0.05^(1/8)                         ≈ 0.6877
1608    ///   1 - 0.6877                         ≈ 0.3123
1609    ///   ln(0.3123)                         ≈ -1.164
1610    ///   num_bits = -8 * 1,048,576 / -1.164 ≈ 7,206,000 bits
1611    ///                                      ≈   900,750 bytes (~900 KB)
1612    ///   next_power_of_two(900 KB)          = 1 MiB (= 1,048,576 bytes)
1613    /// ```
1614    pub fn with_max_ndv(mut self, ndv: u64) -> Self {
1615        self.ndv = Some(ndv);
1616        self
1617    }
1618
1619    /// Builds [`BloomFilterProperties`].
1620    ///
1621    ///
1622    /// # Panics
1623    ///
1624    /// Panics if the configured `fpp` is not in `(0.0, 1.0)` exclusive.
1625    /// Use [`Self::try_build`] for a non-panicking alternative.
1626    pub fn build(self) -> BloomFilterProperties {
1627        self.try_build().unwrap_or_else(|e| panic!("{e}"))
1628    }
1629
1630    /// Builds [`BloomFilterProperties`], returning an error instead of
1631    /// panicking when the configured `fpp` is not in `(0.0, 1.0)` exclusive.
1632    pub fn try_build(self) -> Result<BloomFilterProperties> {
1633        let fpp = self.fpp.unwrap_or(DEFAULT_BLOOM_FILTER_FPP);
1634        validate_bloom_filter_fpp(fpp).map_err(ParquetError::General)?;
1635        let ndv = self.ndv.unwrap_or(DEFAULT_BLOOM_FILTER_NDV);
1636        Ok(BloomFilterProperties { fpp, ndv })
1637    }
1638}
1639
1640/// Single source of truth for the bloom filter fpp range check, shared by
1641/// [`ColumnProperties::set_bloom_filter_fpp`] (panic path) and
1642/// [`BloomFilterPropertiesBuilder::try_build`] (Result path).
1643fn validate_bloom_filter_fpp(fpp: f64) -> std::result::Result<(), String> {
1644    if !(fpp > 0.0 && fpp < 1.0) {
1645        return Err(format!(
1646            "fpp must be between 0.0 and 1.0 exclusive, got {fpp}"
1647        ));
1648    }
1649    Ok(())
1650}
1651
1652/// Container for column properties that can be changed as part of writer.
1653///
1654/// If a field is `None`, it means that no specific value has been set for this column,
1655/// so some subsequent or default value must be used.
1656#[derive(Debug, Clone, Default, PartialEq)]
1657struct ColumnProperties {
1658    encoding: Option<Encoding>,
1659    codec: Option<Compression>,
1660    data_page_size_limit: Option<usize>,
1661    dictionary_page_size_limit: Option<usize>,
1662    dictionary_enabled: Option<bool>,
1663    statistics_enabled: Option<EnabledStatistics>,
1664    write_page_header_statistics: Option<bool>,
1665    /// bloom filter related properties
1666    bloom_filter_properties: Option<BloomFilterProperties>,
1667    /// Whether the bloom filter NDV was explicitly set by the user
1668    bloom_filter_ndv_is_set: bool,
1669    data_page_v2_compression_ratio_threshold: Option<f64>,
1670}
1671
1672impl ColumnProperties {
1673    /// Sets encoding for this column.
1674    ///
1675    /// If dictionary is not enabled, this is treated as a primary encoding for a column.
1676    /// In case when dictionary is enabled for a column, this value is considered to
1677    /// be a fallback encoding.
1678    ///
1679    /// Panics if user tries to set dictionary encoding here, regardless of dictionary
1680    /// encoding flag being set. Use `set_dictionary_enabled` method to enable dictionary
1681    /// for a column.
1682    fn set_encoding(&mut self, value: Encoding) {
1683        if value == Encoding::PLAIN_DICTIONARY || value == Encoding::RLE_DICTIONARY {
1684            panic!("Dictionary encoding can not be used as fallback encoding");
1685        }
1686        self.encoding = Some(value);
1687    }
1688
1689    /// Sets compression codec for this column.
1690    fn set_compression(&mut self, value: Compression) {
1691        self.codec = Some(value);
1692    }
1693
1694    /// Sets data page size limit for this column.
1695    fn set_data_page_size_limit(&mut self, value: usize) {
1696        self.data_page_size_limit = Some(value);
1697    }
1698
1699    /// Sets whether dictionary encoding is enabled for this column.
1700    fn set_dictionary_enabled(&mut self, enabled: bool) {
1701        self.dictionary_enabled = Some(enabled);
1702    }
1703
1704    /// Sets dictionary page size limit for this column.
1705    fn set_dictionary_page_size_limit(&mut self, value: usize) {
1706        self.dictionary_page_size_limit = Some(value);
1707    }
1708
1709    /// Sets the statistics level for this column.
1710    fn set_statistics_enabled(&mut self, enabled: EnabledStatistics) {
1711        self.statistics_enabled = Some(enabled);
1712    }
1713
1714    /// Sets whether to write statistics in the page header for this column.
1715    fn set_write_page_header_statistics(&mut self, enabled: bool) {
1716        self.write_page_header_statistics = Some(enabled);
1717    }
1718
1719    /// If `value` is `true`, sets bloom filter properties to default values if not previously set,
1720    /// otherwise it is a no-op.
1721    /// If `value` is `false`, resets bloom filter properties to `None`.
1722    fn set_bloom_filter_enabled(&mut self, value: bool) {
1723        if value && self.bloom_filter_properties.is_none() {
1724            self.bloom_filter_properties = Some(Default::default())
1725        } else if !value {
1726            self.bloom_filter_properties = None
1727        }
1728    }
1729
1730    /// Sets the false positive probability for bloom filter for this column, and implicitly enables
1731    /// bloom filter if not previously enabled.
1732    ///
1733    /// # Panics
1734    ///
1735    /// Panics if the `value` is not between 0 and 1 exclusive
1736    fn set_bloom_filter_fpp(&mut self, value: f64) {
1737        if let Err(msg) = validate_bloom_filter_fpp(value) {
1738            panic!("{msg}");
1739        }
1740        self.bloom_filter_properties
1741            .get_or_insert_with(Default::default)
1742            .fpp = value;
1743    }
1744
1745    /// Sets the maximum expected number of distinct (unique) values for bloom filter for this
1746    /// column, and implicitly enables bloom filter if not previously enabled.
1747    fn set_bloom_filter_ndv(&mut self, value: u64) {
1748        self.bloom_filter_properties
1749            .get_or_insert_with(Default::default)
1750            .ndv = value;
1751        self.bloom_filter_ndv_is_set = true;
1752    }
1753
1754    /// Sets the bloom filter properties for this column from a fully-built
1755    /// [`BloomFilterProperties`], implicitly enabling the bloom filter.
1756    ///
1757    /// Both `fpp` and `ndv` from `value` are treated as explicit, so the
1758    /// build-time row-group-size NDV fallback in
1759    /// [`WriterPropertiesBuilder::build`] will not override them.
1760    fn set_bloom_filter_properties(&mut self, value: BloomFilterProperties) {
1761        self.bloom_filter_properties = Some(value);
1762        self.bloom_filter_ndv_is_set = true;
1763    }
1764
1765    /// Sets the Data Page v2 compression ratio threshold for this column.
1766    ///
1767    /// # Panics
1768    /// If `value` is not finite or is not strictly positive.
1769    fn set_data_page_v2_compression_ratio_threshold(&mut self, value: f64) {
1770        assert!(
1771            value.is_finite() && value > 0.0,
1772            "data_page_v2_compression_ratio_threshold must be a positive finite number, got {value}"
1773        );
1774        self.data_page_v2_compression_ratio_threshold = Some(value);
1775    }
1776
1777    /// Returns optional encoding for this column.
1778    fn encoding(&self) -> Option<Encoding> {
1779        self.encoding
1780    }
1781
1782    /// Returns optional compression codec for this column.
1783    fn compression(&self) -> Option<Compression> {
1784        self.codec
1785    }
1786
1787    /// Returns `Some(true)` if dictionary encoding is enabled for this column, if
1788    /// disabled then returns `Some(false)`. If result is `None`, then no setting has
1789    /// been provided.
1790    fn dictionary_enabled(&self) -> Option<bool> {
1791        self.dictionary_enabled
1792    }
1793
1794    /// Returns optional dictionary page size limit for this column.
1795    fn dictionary_page_size_limit(&self) -> Option<usize> {
1796        self.dictionary_page_size_limit
1797    }
1798
1799    /// Returns optional data page size limit for this column.
1800    fn data_page_size_limit(&self) -> Option<usize> {
1801        self.data_page_size_limit
1802    }
1803
1804    /// Returns optional statistics level requested for this column. If result is `None`,
1805    /// then no setting has been provided.
1806    fn statistics_enabled(&self) -> Option<EnabledStatistics> {
1807        self.statistics_enabled
1808    }
1809
1810    /// Returns `Some(true)` if [`Statistics`] are to be written to the page header for this
1811    /// column.
1812    ///
1813    /// [`Statistics`]: crate::file::statistics::Statistics
1814    fn write_page_header_statistics(&self) -> Option<bool> {
1815        self.write_page_header_statistics
1816    }
1817
1818    /// Returns the bloom filter properties, or `None` if not enabled
1819    fn bloom_filter_properties(&self) -> Option<&BloomFilterProperties> {
1820        self.bloom_filter_properties.as_ref()
1821    }
1822
1823    /// Returns optional Data Page v2 compression ratio threshold for this column.
1824    fn data_page_v2_compression_ratio_threshold(&self) -> Option<f64> {
1825        self.data_page_v2_compression_ratio_threshold
1826    }
1827
1828    /// If bloom filter is enabled and NDV was not explicitly set, resolve it to the
1829    /// given `default_ndv` (typically derived from `max_row_group_row_count`).
1830    fn resolve_bloom_filter_ndv(&mut self, default_ndv: u64) {
1831        if !self.bloom_filter_ndv_is_set
1832            && let Some(ref mut bf) = self.bloom_filter_properties
1833        {
1834            bf.ndv = default_ndv;
1835        }
1836    }
1837}
1838
1839/// Every per-column writer setting for one leaf column, resolved against the
1840/// per-column overrides and the file-wide defaults.
1841///
1842/// Built by [`WriterProperties::resolve_column_properties`].
1843#[derive(Debug, Clone, PartialEq)]
1844pub(crate) struct ResolvedColumnProperties {
1845    /// See [`WriterProperties::encoding`].
1846    pub(crate) encoding: Option<Encoding>,
1847    /// See [`WriterProperties::compression`].
1848    pub(crate) compression: Compression,
1849    /// See [`WriterProperties::dictionary_enabled`].
1850    pub(crate) dictionary_enabled: bool,
1851    /// See [`WriterProperties::statistics_enabled`].
1852    pub(crate) statistics_enabled: EnabledStatistics,
1853    /// See [`WriterProperties::write_page_header_statistics`].
1854    pub(crate) write_page_header_statistics: bool,
1855    /// See [`WriterProperties::column_data_page_size_limit`].
1856    pub(crate) data_page_size_limit: usize,
1857    /// See [`WriterProperties::column_dictionary_page_size_limit`].
1858    pub(crate) dictionary_page_size_limit: usize,
1859    /// See [`WriterProperties::column_data_page_v2_compression_ratio_threshold`].
1860    pub(crate) data_page_v2_compression_ratio_threshold: f64,
1861    /// See [`WriterProperties::bloom_filter_properties`].
1862    pub(crate) bloom_filter_properties: Option<BloomFilterProperties>,
1863}
1864
1865/// Returns the setting read by `get` for `column` if it sets one, otherwise the
1866/// setting on `default`.
1867///
1868/// `column` is the per-column override entry, if the column has one.
1869#[inline]
1870fn column_or_default<T>(
1871    column: Option<&ColumnProperties>,
1872    default: &ColumnProperties,
1873    get: impl Fn(&ColumnProperties) -> Option<T>,
1874) -> Option<T> {
1875    column.and_then(&get).or_else(|| get(default))
1876}
1877
1878fn resolve_encoding(
1879    column: Option<&ColumnProperties>,
1880    default: &ColumnProperties,
1881) -> Option<Encoding> {
1882    column_or_default(column, default, ColumnProperties::encoding)
1883}
1884
1885fn resolve_compression(
1886    column: Option<&ColumnProperties>,
1887    default: &ColumnProperties,
1888) -> Compression {
1889    column_or_default(column, default, ColumnProperties::compression).unwrap_or(DEFAULT_COMPRESSION)
1890}
1891
1892fn resolve_dictionary_enabled(
1893    column: Option<&ColumnProperties>,
1894    default: &ColumnProperties,
1895) -> bool {
1896    column_or_default(column, default, ColumnProperties::dictionary_enabled)
1897        .unwrap_or(DEFAULT_DICTIONARY_ENABLED)
1898}
1899
1900fn resolve_statistics_enabled(
1901    column: Option<&ColumnProperties>,
1902    default: &ColumnProperties,
1903) -> EnabledStatistics {
1904    column_or_default(column, default, ColumnProperties::statistics_enabled)
1905        .unwrap_or(DEFAULT_STATISTICS_ENABLED)
1906}
1907
1908fn resolve_write_page_header_statistics(
1909    column: Option<&ColumnProperties>,
1910    default: &ColumnProperties,
1911) -> bool {
1912    column_or_default(
1913        column,
1914        default,
1915        ColumnProperties::write_page_header_statistics,
1916    )
1917    .unwrap_or(DEFAULT_WRITE_PAGE_HEADER_STATISTICS)
1918}
1919
1920fn resolve_data_page_size_limit(
1921    column: Option<&ColumnProperties>,
1922    default: &ColumnProperties,
1923) -> usize {
1924    column_or_default(column, default, ColumnProperties::data_page_size_limit)
1925        .unwrap_or(DEFAULT_PAGE_SIZE)
1926}
1927
1928fn resolve_dictionary_page_size_limit(
1929    column: Option<&ColumnProperties>,
1930    default: &ColumnProperties,
1931) -> usize {
1932    column_or_default(
1933        column,
1934        default,
1935        ColumnProperties::dictionary_page_size_limit,
1936    )
1937    .unwrap_or(DEFAULT_DICTIONARY_PAGE_SIZE_LIMIT)
1938}
1939
1940fn resolve_data_page_v2_compression_ratio_threshold(
1941    column: Option<&ColumnProperties>,
1942    default: &ColumnProperties,
1943) -> f64 {
1944    column_or_default(
1945        column,
1946        default,
1947        ColumnProperties::data_page_v2_compression_ratio_threshold,
1948    )
1949    .unwrap_or(DEFAULT_DATA_PAGE_V2_COMPRESSION_RATIO_THRESHOLD)
1950}
1951
1952fn resolve_bloom_filter_properties<'a>(
1953    column: Option<&'a ColumnProperties>,
1954    default: &'a ColumnProperties,
1955) -> Option<&'a BloomFilterProperties> {
1956    column
1957        .and_then(ColumnProperties::bloom_filter_properties)
1958        .or_else(|| default.bloom_filter_properties())
1959}
1960
1961/// Reference counted reader properties.
1962pub type ReaderPropertiesPtr = Arc<ReaderProperties>;
1963
1964const DEFAULT_READ_BLOOM_FILTER: bool = false;
1965const DEFAULT_READ_PAGE_STATS: bool = false;
1966
1967/// Configuration settings for reading parquet files.
1968///
1969/// All properties are immutable and `Send` + `Sync`.
1970/// Use [`ReaderPropertiesBuilder`] to assemble these properties.
1971///
1972/// # Example
1973///
1974/// ```rust
1975/// use parquet::file::properties::ReaderProperties;
1976///
1977/// // Create properties with default configuration.
1978/// let props = ReaderProperties::builder().build();
1979///
1980/// // Use properties builder to set certain options and assemble the configuration.
1981/// let props = ReaderProperties::builder()
1982///     .set_backward_compatible_lz4(false)
1983///     .build();
1984/// ```
1985pub struct ReaderProperties {
1986    codec_options: CodecOptions,
1987    read_bloom_filter: bool,
1988    read_page_stats: bool,
1989}
1990
1991impl ReaderProperties {
1992    /// Returns builder for reader properties with default values.
1993    pub fn builder() -> ReaderPropertiesBuilder {
1994        ReaderPropertiesBuilder::with_defaults()
1995    }
1996
1997    /// Returns codec options.
1998    pub(crate) fn codec_options(&self) -> &CodecOptions {
1999        &self.codec_options
2000    }
2001
2002    /// Returns whether to read bloom filter
2003    pub(crate) fn read_bloom_filter(&self) -> bool {
2004        self.read_bloom_filter
2005    }
2006
2007    /// Returns whether to read page level statistics
2008    pub(crate) fn read_page_stats(&self) -> bool {
2009        self.read_page_stats
2010    }
2011}
2012
2013/// Builder for parquet file reader configuration. See example on
2014/// [`ReaderProperties`]
2015pub struct ReaderPropertiesBuilder {
2016    codec_options_builder: CodecOptionsBuilder,
2017    read_bloom_filter: Option<bool>,
2018    read_page_stats: Option<bool>,
2019}
2020
2021/// Reader properties builder.
2022impl ReaderPropertiesBuilder {
2023    /// Returns default state of the builder.
2024    fn with_defaults() -> Self {
2025        Self {
2026            codec_options_builder: CodecOptionsBuilder::default(),
2027            read_bloom_filter: None,
2028            read_page_stats: None,
2029        }
2030    }
2031
2032    /// Finalizes the configuration and returns immutable reader properties struct.
2033    pub fn build(self) -> ReaderProperties {
2034        ReaderProperties {
2035            codec_options: self.codec_options_builder.build(),
2036            read_bloom_filter: self.read_bloom_filter.unwrap_or(DEFAULT_READ_BLOOM_FILTER),
2037            read_page_stats: self.read_page_stats.unwrap_or(DEFAULT_READ_PAGE_STATS),
2038        }
2039    }
2040
2041    /// Enable/disable backward compatible LZ4.
2042    ///
2043    /// If backward compatible LZ4 is enable, on LZ4_HADOOP error it will fallback
2044    /// to the older versions LZ4 algorithms. That is LZ4_FRAME, for backward compatibility
2045    /// with files generated by older versions of this library, and LZ4_RAW, for backward
2046    /// compatibility with files generated by older versions of parquet-cpp.
2047    ///
2048    /// If backward compatible LZ4 is disabled, on LZ4_HADOOP error it will return the error.
2049    pub fn set_backward_compatible_lz4(mut self, value: bool) -> Self {
2050        self.codec_options_builder = self
2051            .codec_options_builder
2052            .set_backward_compatible_lz4(value);
2053        self
2054    }
2055
2056    /// Enable/disable reading bloom filter
2057    ///
2058    /// If reading bloom filter is enabled, bloom filter will be read from the file.
2059    /// If reading bloom filter is disabled, bloom filter will not be read from the file.
2060    ///
2061    /// By default bloom filter is set to be read.
2062    pub fn set_read_bloom_filter(mut self, value: bool) -> Self {
2063        self.read_bloom_filter = Some(value);
2064        self
2065    }
2066
2067    /// Enable/disable reading page-level statistics
2068    ///
2069    /// If set to `true`, then the reader will decode and populate the [`Statistics`] for
2070    /// each page, if present.
2071    /// If set to `false`, then the reader will skip decoding the statistics.
2072    ///
2073    /// By default statistics will not be decoded.
2074    ///
2075    /// [`Statistics`]: crate::file::statistics::Statistics
2076    pub fn set_read_page_statistics(mut self, value: bool) -> Self {
2077        self.read_page_stats = Some(value);
2078        self
2079    }
2080}
2081
2082#[cfg(test)]
2083mod tests {
2084    use super::*;
2085
2086    #[test]
2087    fn test_writer_version() {
2088        assert_eq!(WriterVersion::PARQUET_1_0.as_num(), 1);
2089        assert_eq!(WriterVersion::PARQUET_2_0.as_num(), 2);
2090    }
2091
2092    /// Every setting resolved in one pass must equal what the individual
2093    /// per-column accessors return, for a column that overrides settings, a
2094    /// column that inherits them, and settings left at their defaults.
2095    #[test]
2096    fn test_resolve_column_properties_matches_individual_accessors() {
2097        let overridden = ColumnPath::from("overridden");
2098        let inherited = ColumnPath::from("inherited");
2099
2100        let props = WriterProperties::builder()
2101            .set_encoding(Encoding::DELTA_BINARY_PACKED)
2102            .set_compression(Compression::SNAPPY)
2103            .set_dictionary_enabled(false)
2104            .set_statistics_enabled(EnabledStatistics::Chunk)
2105            .set_write_page_header_statistics(false)
2106            .set_data_page_size_limit(1111)
2107            .set_dictionary_page_size_limit(2222)
2108            .set_data_page_v2_compression_ratio_threshold(0.25)
2109            .set_bloom_filter_enabled(true)
2110            .set_column_encoding(overridden.clone(), Encoding::PLAIN)
2111            .set_column_compression(overridden.clone(), Compression::UNCOMPRESSED)
2112            .set_column_dictionary_enabled(overridden.clone(), true)
2113            .set_column_statistics_enabled(overridden.clone(), EnabledStatistics::Page)
2114            .set_column_write_page_header_statistics(overridden.clone(), true)
2115            .set_column_data_page_size_limit(overridden.clone(), 3333)
2116            .set_column_dictionary_page_size_limit(overridden.clone(), 4444)
2117            .set_column_data_page_v2_compression_ratio_threshold(overridden.clone(), 0.75)
2118            .set_column_bloom_filter_fpp(overridden.clone(), 0.5)
2119            .build();
2120
2121        // A column with no overrides at all, on properties that are themselves
2122        // entirely default.
2123        let bare = WriterProperties::builder().build();
2124
2125        for (props, col) in [
2126            (&props, &overridden),
2127            (&props, &inherited),
2128            (&bare, &inherited),
2129        ] {
2130            let resolved = props.resolve_column_properties(col);
2131            assert_eq!(resolved.encoding, props.encoding(col), "{col:?}");
2132            assert_eq!(resolved.compression, props.compression(col), "{col:?}");
2133            assert_eq!(
2134                resolved.dictionary_enabled,
2135                props.dictionary_enabled(col),
2136                "{col:?}"
2137            );
2138            assert_eq!(
2139                resolved.statistics_enabled,
2140                props.statistics_enabled(col),
2141                "{col:?}"
2142            );
2143            assert_eq!(
2144                resolved.write_page_header_statistics,
2145                props.write_page_header_statistics(col),
2146                "{col:?}"
2147            );
2148            assert_eq!(
2149                resolved.data_page_size_limit,
2150                props.column_data_page_size_limit(col),
2151                "{col:?}"
2152            );
2153            assert_eq!(
2154                resolved.dictionary_page_size_limit,
2155                props.column_dictionary_page_size_limit(col),
2156                "{col:?}"
2157            );
2158            assert_eq!(
2159                resolved.data_page_v2_compression_ratio_threshold,
2160                props.column_data_page_v2_compression_ratio_threshold(col),
2161                "{col:?}"
2162            );
2163            assert_eq!(
2164                resolved.bloom_filter_properties.as_ref(),
2165                props.bloom_filter_properties(col),
2166                "{col:?}"
2167            );
2168        }
2169    }
2170
2171    #[test]
2172    fn test_writer_properties_default_settings() {
2173        let props = WriterProperties::default();
2174        assert_eq!(props.data_page_size_limit(), DEFAULT_PAGE_SIZE);
2175        assert_eq!(
2176            props.dictionary_page_size_limit(),
2177            DEFAULT_DICTIONARY_PAGE_SIZE_LIMIT
2178        );
2179        assert_eq!(props.write_batch_size(), DEFAULT_WRITE_BATCH_SIZE);
2180        assert_eq!(
2181            props.max_row_group_row_count(),
2182            Some(DEFAULT_MAX_ROW_GROUP_ROW_COUNT)
2183        );
2184        assert_eq!(props.max_row_group_bytes(), None);
2185        assert_eq!(props.bloom_filter_position(), DEFAULT_BLOOM_FILTER_POSITION);
2186        assert_eq!(props.writer_version(), DEFAULT_WRITER_VERSION);
2187        assert_eq!(props.created_by(), DEFAULT_CREATED_BY);
2188        assert_eq!(props.key_value_metadata(), None);
2189        assert_eq!(props.encoding(&ColumnPath::from("col")), None);
2190        assert_eq!(
2191            props.compression(&ColumnPath::from("col")),
2192            DEFAULT_COMPRESSION
2193        );
2194        assert_eq!(
2195            props.dictionary_enabled(&ColumnPath::from("col")),
2196            DEFAULT_DICTIONARY_ENABLED
2197        );
2198        assert_eq!(
2199            props.statistics_enabled(&ColumnPath::from("col")),
2200            DEFAULT_STATISTICS_ENABLED
2201        );
2202        assert!(
2203            props
2204                .bloom_filter_properties(&ColumnPath::from("col"))
2205                .is_none()
2206        );
2207    }
2208
2209    #[test]
2210    fn test_writer_properties_dictionary_encoding() {
2211        // dictionary encoding is not configurable, and it should be the same for both
2212        // writer version 1 and 2.
2213        for version in &[WriterVersion::PARQUET_1_0, WriterVersion::PARQUET_2_0] {
2214            let props = WriterProperties::builder()
2215                .set_writer_version(*version)
2216                .build();
2217            assert_eq!(props.dictionary_page_encoding(), Encoding::PLAIN);
2218            assert_eq!(
2219                props.dictionary_data_page_encoding(),
2220                Encoding::RLE_DICTIONARY
2221            );
2222        }
2223    }
2224
2225    #[test]
2226    #[should_panic(expected = "Dictionary encoding can not be used as fallback encoding")]
2227    fn test_writer_properties_panic_when_plain_dictionary_is_fallback() {
2228        // Should panic when user specifies dictionary encoding as fallback encoding.
2229        WriterProperties::builder()
2230            .set_encoding(Encoding::PLAIN_DICTIONARY)
2231            .build();
2232    }
2233
2234    #[test]
2235    #[should_panic(expected = "Dictionary encoding can not be used as fallback encoding")]
2236    fn test_writer_properties_panic_when_rle_dictionary_is_fallback() {
2237        // Should panic when user specifies dictionary encoding as fallback encoding.
2238        WriterProperties::builder()
2239            .set_encoding(Encoding::RLE_DICTIONARY)
2240            .build();
2241    }
2242
2243    #[test]
2244    #[should_panic(expected = "Dictionary encoding can not be used as fallback encoding")]
2245    fn test_writer_properties_panic_when_dictionary_is_enabled() {
2246        WriterProperties::builder()
2247            .set_dictionary_enabled(true)
2248            .set_column_encoding(ColumnPath::from("col"), Encoding::RLE_DICTIONARY)
2249            .build();
2250    }
2251
2252    #[test]
2253    #[should_panic(expected = "Dictionary encoding can not be used as fallback encoding")]
2254    fn test_writer_properties_panic_when_dictionary_is_disabled() {
2255        WriterProperties::builder()
2256            .set_dictionary_enabled(false)
2257            .set_column_encoding(ColumnPath::from("col"), Encoding::RLE_DICTIONARY)
2258            .build();
2259    }
2260
2261    #[test]
2262    fn test_writer_properties_builder() {
2263        let props = WriterProperties::builder()
2264            // file settings
2265            .set_writer_version(WriterVersion::PARQUET_2_0)
2266            .set_data_page_size_limit(10)
2267            .set_dictionary_page_size_limit(20)
2268            .set_write_batch_size(30)
2269            .set_max_row_group_row_count(Some(40))
2270            .set_created_by("default".to_owned())
2271            .set_key_value_metadata(Some(vec![KeyValue::new(
2272                "key".to_string(),
2273                "value".to_string(),
2274            )]))
2275            // global column settings
2276            .set_encoding(Encoding::DELTA_BINARY_PACKED)
2277            .set_compression(Compression::GZIP(Default::default()))
2278            .set_dictionary_enabled(false)
2279            .set_statistics_enabled(EnabledStatistics::None)
2280            // specific column settings
2281            .set_column_encoding(ColumnPath::from("col"), Encoding::RLE)
2282            .set_column_compression(ColumnPath::from("col"), Compression::SNAPPY)
2283            .set_column_dictionary_enabled(ColumnPath::from("col"), true)
2284            .set_column_statistics_enabled(ColumnPath::from("col"), EnabledStatistics::Chunk)
2285            .set_column_bloom_filter_enabled(ColumnPath::from("col"), true)
2286            .set_column_bloom_filter_max_ndv(ColumnPath::from("col"), 100_u64)
2287            .set_column_bloom_filter_fpp(ColumnPath::from("col"), 0.1)
2288            .build();
2289
2290        fn test_props(props: &WriterProperties) {
2291            assert_eq!(props.writer_version(), WriterVersion::PARQUET_2_0);
2292            assert_eq!(props.data_page_size_limit(), 10);
2293            assert_eq!(props.dictionary_page_size_limit(), 20);
2294            assert_eq!(props.write_batch_size(), 30);
2295            assert_eq!(props.max_row_group_row_count(), Some(40));
2296            assert_eq!(props.created_by(), "default");
2297            assert_eq!(
2298                props.key_value_metadata(),
2299                Some(&vec![
2300                    KeyValue::new("key".to_string(), "value".to_string(),)
2301                ])
2302            );
2303
2304            assert_eq!(
2305                props.encoding(&ColumnPath::from("a")),
2306                Some(Encoding::DELTA_BINARY_PACKED)
2307            );
2308            assert_eq!(
2309                props.compression(&ColumnPath::from("a")),
2310                Compression::GZIP(Default::default())
2311            );
2312            assert!(!props.dictionary_enabled(&ColumnPath::from("a")));
2313            assert_eq!(
2314                props.statistics_enabled(&ColumnPath::from("a")),
2315                EnabledStatistics::None
2316            );
2317
2318            assert_eq!(
2319                props.encoding(&ColumnPath::from("col")),
2320                Some(Encoding::RLE)
2321            );
2322            assert_eq!(
2323                props.compression(&ColumnPath::from("col")),
2324                Compression::SNAPPY
2325            );
2326            assert!(props.dictionary_enabled(&ColumnPath::from("col")));
2327            assert_eq!(
2328                props.statistics_enabled(&ColumnPath::from("col")),
2329                EnabledStatistics::Chunk
2330            );
2331            assert_eq!(
2332                props.bloom_filter_properties(&ColumnPath::from("col")),
2333                Some(&BloomFilterProperties { fpp: 0.1, ndv: 100 })
2334            );
2335        }
2336
2337        // Test direct build of properties
2338        test_props(&props);
2339
2340        // Test that into_builder() gives the same result
2341        let props_into_builder_and_back = props.into_builder().build();
2342        test_props(&props_into_builder_and_back);
2343    }
2344
2345    #[test]
2346    fn test_writer_properties_builder_partial_defaults() {
2347        let props = WriterProperties::builder()
2348            .set_encoding(Encoding::DELTA_BINARY_PACKED)
2349            .set_compression(Compression::GZIP(Default::default()))
2350            .set_bloom_filter_enabled(true)
2351            .set_column_encoding(ColumnPath::from("col"), Encoding::RLE)
2352            .build();
2353
2354        assert_eq!(
2355            props.encoding(&ColumnPath::from("col")),
2356            Some(Encoding::RLE)
2357        );
2358        assert_eq!(
2359            props.compression(&ColumnPath::from("col")),
2360            Compression::GZIP(Default::default())
2361        );
2362        assert_eq!(
2363            props.dictionary_enabled(&ColumnPath::from("col")),
2364            DEFAULT_DICTIONARY_ENABLED
2365        );
2366        assert_eq!(
2367            props.bloom_filter_properties(&ColumnPath::from("col")),
2368            Some(&BloomFilterProperties {
2369                fpp: DEFAULT_BLOOM_FILTER_FPP,
2370                ndv: DEFAULT_BLOOM_FILTER_NDV,
2371            })
2372        );
2373    }
2374
2375    #[test]
2376    #[should_panic(expected = "Cannot have a 0 max row group row count")]
2377    fn test_writer_properties_panic_on_zero_row_group_row_count() {
2378        let _ = WriterProperties::builder().set_max_row_group_row_count(Some(0));
2379    }
2380
2381    #[test]
2382    #[should_panic(expected = "Cannot have a 0 max row group bytes")]
2383    fn test_writer_properties_panic_on_zero_row_group_bytes() {
2384        let _ = WriterProperties::builder().set_max_row_group_bytes(Some(0));
2385    }
2386
2387    #[test]
2388    #[should_panic(expected = "Cannot have a 0 write batch size")]
2389    fn test_writer_properties_panic_on_zero_write_batch_size() {
2390        let _ = WriterProperties::builder().set_write_batch_size(0);
2391    }
2392
2393    #[test]
2394    #[should_panic(expected = "Cannot have a 0 data page row count limit")]
2395    fn test_writer_properties_panic_on_zero_data_page_row_count_limit() {
2396        let _ = WriterProperties::builder().set_data_page_row_count_limit(0);
2397    }
2398
2399    #[test]
2400    fn test_writer_properties_bloom_filter_ndv_fpp_set() {
2401        assert_eq!(
2402            WriterProperties::builder()
2403                .build()
2404                .bloom_filter_properties(&ColumnPath::from("col")),
2405            None
2406        );
2407        assert_eq!(
2408            WriterProperties::builder()
2409                .set_bloom_filter_max_ndv(100)
2410                .build()
2411                .bloom_filter_properties(&ColumnPath::from("col")),
2412            Some(&BloomFilterProperties {
2413                fpp: DEFAULT_BLOOM_FILTER_FPP,
2414                ndv: 100,
2415            })
2416        );
2417        assert_eq!(
2418            WriterProperties::builder()
2419                .set_bloom_filter_fpp(0.1)
2420                .build()
2421                .bloom_filter_properties(&ColumnPath::from("col")),
2422            Some(&BloomFilterProperties {
2423                fpp: 0.1,
2424                ndv: DEFAULT_BLOOM_FILTER_NDV,
2425            })
2426        );
2427    }
2428
2429    #[test]
2430    fn test_writer_properties_column_data_page_v2_compression_ratio_threshold() {
2431        let props = WriterProperties::builder()
2432            .set_data_page_v2_compression_ratio_threshold(0.5)
2433            .set_column_data_page_v2_compression_ratio_threshold(ColumnPath::from("col"), 0.1)
2434            .build();
2435
2436        assert_eq!(props.data_page_v2_compression_ratio_threshold(), 0.5);
2437        assert_eq!(
2438            props.column_data_page_v2_compression_ratio_threshold(&ColumnPath::from("col")),
2439            0.1
2440        );
2441        assert_eq!(
2442            props.column_data_page_v2_compression_ratio_threshold(&ColumnPath::from("other")),
2443            0.5
2444        );
2445    }
2446
2447    #[test]
2448    #[should_panic(
2449        expected = "data_page_v2_compression_ratio_threshold must be a positive finite number"
2450    )]
2451    fn test_writer_properties_panic_on_invalid_data_page_v2_compression_ratio_threshold() {
2452        WriterProperties::builder()
2453            .set_data_page_v2_compression_ratio_threshold(0.0)
2454            .build();
2455    }
2456
2457    #[test]
2458    #[expect(deprecated)]
2459    fn test_writer_properties_deprecated_bloom_filter_ndv_setters_still_work() {
2460        let col = ColumnPath::from("col");
2461        let props = WriterProperties::builder()
2462            .set_bloom_filter_ndv(100)
2463            .set_column_bloom_filter_ndv(col.clone(), 200)
2464            .build();
2465        assert_eq!(
2466            props.bloom_filter_properties(&ColumnPath::from("other")),
2467            Some(&BloomFilterProperties {
2468                fpp: DEFAULT_BLOOM_FILTER_FPP,
2469                ndv: 100,
2470            })
2471        );
2472        assert_eq!(
2473            props.bloom_filter_properties(&col),
2474            Some(&BloomFilterProperties {
2475                fpp: DEFAULT_BLOOM_FILTER_FPP,
2476                ndv: 200,
2477            })
2478        );
2479    }
2480
2481    #[test]
2482    fn test_writer_properties_column_dictionary_page_size_limit() {
2483        let props = WriterProperties::builder()
2484            .set_dictionary_page_size_limit(100)
2485            .set_column_dictionary_page_size_limit(ColumnPath::from("col"), 10)
2486            .build();
2487
2488        assert_eq!(props.dictionary_page_size_limit(), 100);
2489        assert_eq!(
2490            props.column_dictionary_page_size_limit(&ColumnPath::from("col")),
2491            10
2492        );
2493        assert_eq!(
2494            props.column_dictionary_page_size_limit(&ColumnPath::from("other")),
2495            100
2496        );
2497    }
2498
2499    #[test]
2500    fn test_writer_properties_column_data_page_size_limit() {
2501        let props = WriterProperties::builder()
2502            .set_data_page_size_limit(100)
2503            .set_column_data_page_size_limit(ColumnPath::from("col"), 10)
2504            .build();
2505
2506        assert_eq!(props.data_page_size_limit(), 100);
2507        assert_eq!(
2508            props.column_data_page_size_limit(&ColumnPath::from("col")),
2509            10
2510        );
2511        assert_eq!(
2512            props.column_data_page_size_limit(&ColumnPath::from("other")),
2513            100
2514        );
2515    }
2516
2517    #[test]
2518    fn test_reader_properties_default_settings() {
2519        let props = ReaderProperties::builder().build();
2520
2521        let codec_options = CodecOptionsBuilder::default()
2522            .set_backward_compatible_lz4(true)
2523            .build();
2524
2525        assert_eq!(props.codec_options(), &codec_options);
2526        assert!(!props.read_bloom_filter());
2527    }
2528
2529    #[test]
2530    fn test_reader_properties_builder() {
2531        let props = ReaderProperties::builder()
2532            .set_backward_compatible_lz4(false)
2533            .build();
2534
2535        let codec_options = CodecOptionsBuilder::default()
2536            .set_backward_compatible_lz4(false)
2537            .build();
2538
2539        assert_eq!(props.codec_options(), &codec_options);
2540    }
2541
2542    #[test]
2543    fn test_parse_writerversion() {
2544        let mut writer_version = "PARQUET_1_0".parse::<WriterVersion>().unwrap();
2545        assert_eq!(writer_version, WriterVersion::PARQUET_1_0);
2546        writer_version = "PARQUET_2_0".parse::<WriterVersion>().unwrap();
2547        assert_eq!(writer_version, WriterVersion::PARQUET_2_0);
2548
2549        // test lowercase
2550        writer_version = "parquet_1_0".parse::<WriterVersion>().unwrap();
2551        assert_eq!(writer_version, WriterVersion::PARQUET_1_0);
2552
2553        // test invalid version
2554        match "PARQUET_-1_0".parse::<WriterVersion>() {
2555            Ok(_) => panic!("Should not be able to parse PARQUET_-1_0"),
2556            Err(e) => {
2557                assert_eq!(e, "Invalid writer version: PARQUET_-1_0");
2558            }
2559        }
2560    }
2561
2562    #[test]
2563    fn test_parse_enabledstatistics() {
2564        let mut enabled_statistics = "NONE".parse::<EnabledStatistics>().unwrap();
2565        assert_eq!(enabled_statistics, EnabledStatistics::None);
2566        enabled_statistics = "CHUNK".parse::<EnabledStatistics>().unwrap();
2567        assert_eq!(enabled_statistics, EnabledStatistics::Chunk);
2568        enabled_statistics = "PAGE".parse::<EnabledStatistics>().unwrap();
2569        assert_eq!(enabled_statistics, EnabledStatistics::Page);
2570
2571        // test lowercase
2572        enabled_statistics = "none".parse::<EnabledStatistics>().unwrap();
2573        assert_eq!(enabled_statistics, EnabledStatistics::None);
2574
2575        //test invalid statistics
2576        match "ChunkAndPage".parse::<EnabledStatistics>() {
2577            Ok(_) => panic!("Should not be able to parse ChunkAndPage"),
2578            Err(e) => {
2579                assert_eq!(e, "Invalid statistics arg: ChunkAndPage");
2580            }
2581        }
2582    }
2583
2584    #[test]
2585    fn test_cdc_options_equality() {
2586        let opts = CdcOptions::default();
2587        assert_eq!(opts, CdcOptions::default());
2588
2589        let custom = CdcOptions {
2590            min_chunk_size: 1024,
2591            max_chunk_size: 8192,
2592            norm_level: 1,
2593        };
2594        assert_eq!(custom, custom);
2595        assert_ne!(opts, custom);
2596    }
2597
2598    #[test]
2599    fn test_bloom_filter_builder_default() {
2600        let props = BloomFilterProperties::builder().build();
2601        assert_eq!(props.fpp, DEFAULT_BLOOM_FILTER_FPP);
2602        assert_eq!(props.ndv, DEFAULT_BLOOM_FILTER_NDV);
2603        assert_eq!(props, BloomFilterProperties::default());
2604        assert_eq!(
2605            BloomFilterPropertiesBuilder::new().build(),
2606            BloomFilterProperties::default()
2607        );
2608    }
2609
2610    #[test]
2611    fn test_bloom_filter_builder_explicit_fpp() {
2612        let props = BloomFilterProperties::builder().with_fpp(0.01).build();
2613        assert_eq!(props.fpp, 0.01);
2614        assert_eq!(props.ndv, DEFAULT_BLOOM_FILTER_NDV);
2615    }
2616
2617    #[test]
2618    fn test_bloom_filter_builder_explicit_ndv() {
2619        let props = BloomFilterProperties::builder().with_max_ndv(1000).build();
2620        assert_eq!(props.fpp, DEFAULT_BLOOM_FILTER_FPP);
2621        assert_eq!(props.ndv, 1000);
2622    }
2623
2624    #[test]
2625    fn test_bloom_filter_builder_validates_fpp() {
2626        for wrong_val in [0.0_f64, 1.0, -0.5, 2.0] {
2627            let result = std::panic::catch_unwind(|| {
2628                BloomFilterProperties::builder().with_fpp(wrong_val).build()
2629            });
2630            assert!(
2631                result.is_err(),
2632                "with_fpp({wrong_val}).build() should reject value outside (0, 1)"
2633            );
2634        }
2635    }
2636
2637    #[test]
2638    fn test_bloom_filter_builder_try_build_validates_fpp() {
2639        for wrong_val in [0.0_f64, 1.0, -0.5, 2.0] {
2640            let result = BloomFilterProperties::builder()
2641                .with_fpp(wrong_val)
2642                .try_build();
2643            assert!(
2644                result.is_err(),
2645                "try_build() should return Err for fpp outside (0, 1)"
2646            );
2647        }
2648
2649        let ok = BloomFilterProperties::builder()
2650            .with_fpp(0.01)
2651            .with_max_ndv(1000)
2652            .try_build()
2653            .expect("valid fpp should yield Ok");
2654        assert_eq!(ok.fpp, 0.01);
2655        assert_eq!(ok.ndv, 1000);
2656    }
2657
2658    #[test]
2659    fn test_column_specific_implicit_ndv_uses_row_group_size() {
2660        let custom_row_group_size: usize = 7777;
2661        let col = ColumnPath::from("col");
2662        let props = WriterProperties::builder()
2663            .set_max_row_group_row_count(Some(custom_row_group_size))
2664            .set_column_bloom_filter_enabled(col.clone(), true)
2665            .build();
2666        let bf = props
2667            .bloom_filter_properties(&col)
2668            .expect("bloom filter should be enabled for col");
2669
2670        assert_eq!(bf.ndv, custom_row_group_size as u64);
2671        assert_eq!(bf.fpp, DEFAULT_BLOOM_FILTER_FPP);
2672    }
2673
2674    #[test]
2675    fn test_set_bloom_filter_properties_applied_globally() {
2676        let bf = BloomFilterProperties::builder()
2677            .with_fpp(0.01)
2678            .with_max_ndv(500)
2679            .build();
2680        let props = WriterProperties::builder()
2681            .set_bloom_filter_properties(bf.clone())
2682            .build();
2683
2684        assert_eq!(
2685            props.bloom_filter_properties(&ColumnPath::from("a")),
2686            Some(&bf),
2687        );
2688        assert_eq!(
2689            props.bloom_filter_properties(&ColumnPath::from("b")),
2690            Some(&bf),
2691        );
2692    }
2693
2694    #[test]
2695    fn test_set_column_bloom_filter_properties_overrides_global() {
2696        let global = BloomFilterProperties::builder()
2697            .with_fpp(0.01)
2698            .with_max_ndv(500)
2699            .build();
2700        let tailored = BloomFilterProperties::builder()
2701            .with_fpp(0.02)
2702            .with_max_ndv(1000)
2703            .build();
2704
2705        let col = ColumnPath::from("col");
2706        let props = WriterProperties::builder()
2707            .set_bloom_filter_properties(global.clone())
2708            .set_column_bloom_filter_properties(col.clone(), tailored.clone())
2709            .build();
2710
2711        assert_eq!(props.bloom_filter_properties(&col), Some(&tailored));
2712        assert_eq!(
2713            props.bloom_filter_properties(&ColumnPath::from("other")),
2714            Some(&global)
2715        );
2716    }
2717
2718    #[test]
2719    fn test_set_bloom_filter_properties_preserve_explicit_ndv() {
2720        let bf = BloomFilterProperties::builder().with_max_ndv(42).build();
2721        let props = WriterProperties::builder()
2722            .set_max_row_group_row_count(Some(99_999))
2723            .set_bloom_filter_properties(bf)
2724            .build();
2725        let result = props
2726            .bloom_filter_properties(&ColumnPath::from("col"))
2727            .expect("bloom filter should be enabled");
2728
2729        assert_eq!(
2730            result.ndv, 42,
2731            "explicit ndv must not be overridden by row-group-size fallback"
2732        );
2733    }
2734}