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