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