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