Skip to main content

parquet/file/
properties.rs

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