Skip to main content

parquet/file/
properties.rs

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