Skip to main content

parquet/file/
properties.rs

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