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