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