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