Skip to main content

polars_parquet/arrow/write/
mod.rs

1//! APIs to write to Parquet format.
2//!
3//! # Arrow/Parquet Interoperability
4//! As of [parquet-format v2.9](https://github.com/apache/parquet-format/blob/master/LogicalTypes.md)
5//! there are Arrow [DataTypes](arrow::datatypes::ArrowDataType) which do not have a parquet
6//! representation. These include but are not limited to:
7//! * `ArrowDataType::Timestamp(TimeUnit::Second, _)`
8//! * `ArrowDataType::Int64`
9//! * `ArrowDataType::Duration`
10//! * `ArrowDataType::Date64`
11//! * `ArrowDataType::Time32(TimeUnit::Second)`
12//!
13//! The use of these arrow types will result in no logical type being stored within a parquet file.
14
15mod binary;
16mod binview;
17mod boolean;
18mod dictionary;
19mod file;
20mod fixed_size_binary;
21mod nested;
22mod pages;
23mod primitive;
24mod row_group;
25mod schema;
26mod utils;
27
28use arrow::array::*;
29use arrow::bitmap::Bitmap;
30use arrow::datatypes::*;
31use arrow::types::{NativeType, days_ms, i256};
32pub use nested::{num_values, write_rep_and_def};
33pub use pages::{to_leaves, to_nested, to_parquet_leaves};
34use polars_config::config;
35use polars_utils::float16::pf16;
36use polars_utils::pl_str::PlSmallStr;
37pub use utils::write_def_levels;
38
39pub use crate::parquet::compression::{BrotliLevel, CompressionOptions, GzipLevel, ZstdLevel};
40pub use crate::parquet::encoding::Encoding;
41pub use crate::parquet::metadata::{
42    Descriptor, FileMetadata, KeyValue, SchemaDescriptor, ThriftFileMetadata,
43};
44pub use crate::parquet::page::{CompressedDataPage, CompressedPage, Page};
45use crate::parquet::schema::Repetition;
46use crate::parquet::schema::types::PrimitiveType as ParquetPrimitiveType;
47pub use crate::parquet::schema::types::{
48    FieldInfo, ParquetType, PhysicalType as ParquetPhysicalType,
49};
50pub use crate::parquet::write::{
51    Compressor, DynIter, DynStreamingIterator, RowGroupIterColumns, Version, compress,
52    write_metadata_sidecar,
53};
54pub use crate::parquet::{FallibleStreamingIterator, fallible_streaming_iterator};
55use crate::write::fixed_size_binary::build_statistics_float16;
56
57/// The statistics to write
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
59#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
60#[cfg_attr(feature = "dsl-schema", derive(schemars::JsonSchema))]
61pub struct StatisticsOptions {
62    pub min_value: bool,
63    pub max_value: bool,
64    pub distinct_count: bool,
65    pub null_count: bool,
66    /// Target byte length for binary/string statistics truncation. Set to
67    /// `Some(0)` to disable truncation.
68    pub binary_statistics_truncate_length: Option<u64>,
69}
70
71impl Default for StatisticsOptions {
72    fn default() -> Self {
73        Self {
74            min_value: true,
75            max_value: true,
76            distinct_count: false,
77            null_count: true,
78            binary_statistics_truncate_length: None,
79        }
80    }
81}
82
83/// Options to encode an array
84#[derive(Clone, Copy)]
85pub enum EncodeNullability {
86    Required,
87    Optional,
88}
89
90/// Currently supported options to write to parquet
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub struct WriteOptions {
93    /// Whether to write statistics
94    pub statistics: StatisticsOptions,
95    /// The page and file version to use
96    pub version: Version,
97    /// The compression to apply to every page
98    pub compression: CompressionOptions,
99    /// The size to flush a page, defaults to 1024 * 1024 if None
100    pub data_page_size: Option<usize>,
101}
102
103use arrow::compute::aggregate::estimated_bytes_size;
104use arrow::match_integer_type;
105pub use file::FileWriter;
106pub use pages::{Nested, array_to_columns, arrays_to_columns};
107use polars_error::{PolarsResult, polars_bail};
108pub use row_group::{RowGroupIterator, row_group_iter};
109pub use schema::{schema_to_metadata_key, to_parquet_type};
110
111use self::pages::{FixedSizeListNested, PrimitiveNested, StructNested};
112use crate::write::dictionary::encode_as_dictionary_optional;
113
114impl StatisticsOptions {
115    pub fn empty() -> Self {
116        Self {
117            min_value: false,
118            max_value: false,
119            distinct_count: false,
120            null_count: false,
121            binary_statistics_truncate_length: None,
122        }
123    }
124
125    pub fn full() -> Self {
126        Self {
127            min_value: true,
128            max_value: true,
129            distinct_count: true,
130            null_count: true,
131            binary_statistics_truncate_length: None,
132        }
133    }
134
135    pub fn is_empty(&self) -> bool {
136        !(self.min_value || self.max_value || self.distinct_count || self.null_count)
137    }
138
139    pub fn is_full(&self) -> bool {
140        self.min_value && self.max_value && self.distinct_count && self.null_count
141    }
142
143    /// Truncate statistics for binary columns to this length.
144    pub fn binary_statistics_truncate_length(&self) -> Option<u64> {
145        let len = self
146            .binary_statistics_truncate_length
147            .unwrap_or_else(|| config().parquet_binary_statistics_truncate_length());
148        (len > 0).then_some(len)
149    }
150
151    pub fn binary_statistics_truncate_length_usize(&self) -> Option<usize> {
152        self.binary_statistics_truncate_length()
153            .and_then(|x| usize::try_from(x).ok())
154    }
155}
156
157impl WriteOptions {
158    pub fn has_statistics(&self) -> bool {
159        !self.statistics.is_empty()
160    }
161}
162
163impl EncodeNullability {
164    const fn new(is_optional: bool) -> Self {
165        if is_optional {
166            Self::Optional
167        } else {
168            Self::Required
169        }
170    }
171
172    fn is_optional(self) -> bool {
173        matches!(self, Self::Optional)
174    }
175}
176
177/// `data_page_size`: Set a target threshold for the approximate encoded size of data
178/// pages within a column chunk (in bytes). If None, use the default data page size of 1MByte.
179/// See: https://arrow.apache.org/docs/python/generated/pyarrow.parquet.write_table.html
180pub(crate) fn row_slice_ranges(
181    number_of_rows: usize,
182    byte_size: usize,
183    options: WriteOptions,
184) -> impl Iterator<Item = (usize, usize)> {
185    const DEFAULT_PAGE_SIZE: usize = 1024 * 1024; // 1 MB
186    let max_page_size = options.data_page_size.unwrap_or(DEFAULT_PAGE_SIZE);
187    let max_page_size = max_page_size.min(2usize.pow(31) - 2usize.pow(25)); // allowed maximum page size
188
189    let bytes_per_row = if number_of_rows == 0 {
190        0
191    } else {
192        ((byte_size as f64) / (number_of_rows as f64)) as usize
193    };
194    let rows_per_page = (max_page_size / (bytes_per_row + 1)).max(1);
195
196    (0..number_of_rows)
197        .step_by(rows_per_page)
198        .map(move |offset| {
199            let length = (offset + rows_per_page).min(number_of_rows) - offset;
200            (offset, length)
201        })
202}
203
204/// returns offset and length to slice the leaf values
205pub fn slice_nested_leaf(nested: &[Nested]) -> (usize, usize) {
206    // find the deepest recursive dremel structure as that one determines how many values we must
207    // take
208    let mut out = (0, 0);
209    for nested in nested.iter().rev() {
210        match nested {
211            Nested::LargeList(l_nested) => {
212                let start = *l_nested.offsets.first();
213                let end = *l_nested.offsets.last();
214                return (start as usize, (end - start) as usize);
215            },
216            Nested::List(l_nested) => {
217                let start = *l_nested.offsets.first();
218                let end = *l_nested.offsets.last();
219                return (start as usize, (end - start) as usize);
220            },
221            Nested::FixedSizeList(nested) => return (0, nested.length * nested.width),
222            Nested::Primitive(nested) => out = (0, nested.length),
223            Nested::Struct(_) => {},
224        }
225    }
226    out
227}
228
229fn decimal_length_from_precision(precision: usize) -> usize {
230    // digits = floor(log_10(2^(8*n - 1) - 1))
231    // ceil(digits) = log10(2^(8*n - 1) - 1)
232    // 10^ceil(digits) = 2^(8*n - 1) - 1
233    // 10^ceil(digits) + 1 = 2^(8*n - 1)
234    // log2(10^ceil(digits) + 1) = (8*n - 1)
235    // log2(10^ceil(digits) + 1) + 1 = 8*n
236    // (log2(10^ceil(a) + 1) + 1) / 8 = n
237    (((10.0_f64.powi(precision as i32) + 1.0).log2() + 1.0) / 8.0).ceil() as usize
238}
239
240/// Creates a parquet [`SchemaDescriptor`] from a [`ArrowSchema`].
241pub fn to_parquet_schema(schema: &ArrowSchema) -> PolarsResult<SchemaDescriptor> {
242    let parquet_types = schema
243        .iter_values()
244        .map(to_parquet_type)
245        .collect::<PolarsResult<Vec<_>>>()?;
246    Ok(SchemaDescriptor::new(
247        PlSmallStr::from_static("root"),
248        parquet_types,
249    ))
250}
251
252/// Slices the [`Array`] to `Box<dyn Array>` and `Vec<Nested>`.
253pub fn slice_parquet_array(
254    primitive_array: &mut dyn Array,
255    nested: &mut [Nested],
256    mut current_offset: usize,
257    mut current_length: usize,
258) {
259    for nested in nested.iter_mut() {
260        match nested {
261            Nested::LargeList(l_nested) => {
262                l_nested.offsets.slice(current_offset, current_length + 1);
263                if let Some(validity) = l_nested.validity.as_mut() {
264                    validity.slice(current_offset, current_length)
265                };
266
267                // Update the offset/ length so that the Primitive is sliced properly.
268                current_length = l_nested.offsets.range() as usize;
269                current_offset = *l_nested.offsets.first() as usize;
270            },
271            Nested::List(l_nested) => {
272                l_nested.offsets.slice(current_offset, current_length + 1);
273                if let Some(validity) = l_nested.validity.as_mut() {
274                    validity.slice(current_offset, current_length)
275                };
276
277                // Update the offset/ length so that the Primitive is sliced properly.
278                current_length = l_nested.offsets.range() as usize;
279                current_offset = *l_nested.offsets.first() as usize;
280            },
281            Nested::Struct(StructNested {
282                validity, length, ..
283            }) => {
284                *length = current_length;
285                if let Some(validity) = validity.as_mut() {
286                    validity.slice(current_offset, current_length)
287                };
288            },
289            Nested::Primitive(PrimitiveNested {
290                validity, length, ..
291            }) => {
292                *length = current_length;
293                if let Some(validity) = validity.as_mut() {
294                    validity.slice(current_offset, current_length)
295                };
296                primitive_array.slice(current_offset, current_length);
297            },
298            Nested::FixedSizeList(FixedSizeListNested {
299                validity,
300                length,
301                width,
302                ..
303            }) => {
304                if let Some(validity) = validity.as_mut() {
305                    validity.slice(current_offset, current_length)
306                };
307                *length = current_length;
308                // Update the offset/ length so that the Primitive is sliced properly.
309                current_length *= *width;
310                current_offset *= *width;
311            },
312        }
313    }
314}
315
316/// Get the length of [`Array`] that should be sliced.
317pub fn get_max_length(nested: &[Nested]) -> usize {
318    let mut length = 0;
319    for nested in nested.iter() {
320        match nested {
321            Nested::LargeList(l_nested) => length += l_nested.offsets.range() as usize,
322            Nested::List(l_nested) => length += l_nested.offsets.range() as usize,
323            Nested::FixedSizeList(nested) => length += nested.length * nested.width,
324            _ => {},
325        }
326    }
327    length
328}
329
330/// Returns an iterator of [`Page`].
331pub fn array_to_pages(
332    primitive_array: &dyn Array,
333    type_: ParquetPrimitiveType,
334    nested: &[Nested],
335    options: WriteOptions,
336    mut encoding: Encoding,
337) -> PolarsResult<DynIter<'static, PolarsResult<Page>>> {
338    if let ArrowDataType::Dictionary(key_type, _, _) = primitive_array.dtype().to_storage() {
339        return match_integer_type!(key_type, |$T| {
340            dictionary::array_to_pages::<$T>(
341                primitive_array.as_any().downcast_ref().unwrap(),
342                type_,
343                &nested,
344                options,
345                encoding,
346            )
347        });
348    };
349    if let Encoding::RleDictionary = encoding {
350        // Only take this path for primitive columns
351        if matches!(nested.first(), Some(Nested::Primitive(_))) {
352            if let Some(result) =
353                encode_as_dictionary_optional(primitive_array, nested, type_.clone(), options)
354            {
355                return result;
356            }
357        }
358
359        // We didn't succeed, fallback to plain
360        encoding = Encoding::Plain;
361    }
362
363    let nested = nested.to_vec();
364    let number_of_rows = nested[0].len();
365    // note: this is not correct if the array is sliced - the estimation should happen on the
366    // primitive after sliced for parquet
367    let byte_size = estimated_bytes_size(primitive_array);
368    let primitive_array = primitive_array.to_boxed();
369
370    let pages =
371        row_slice_ranges(number_of_rows, byte_size, options).map(move |(offset, length)| {
372            let mut right_array = primitive_array.clone();
373            let mut right_nested = nested.clone();
374            slice_parquet_array(right_array.as_mut(), &mut right_nested, offset, length);
375
376            array_to_page(
377                right_array.as_ref(),
378                type_.clone(),
379                &right_nested,
380                options,
381                encoding,
382            )
383        });
384    Ok(DynIter::new(pages))
385}
386
387/// Converts an [`Array`] to a [`CompressedPage`] based on options, descriptor and `encoding`.
388pub fn array_to_page(
389    array: &dyn Array,
390    type_: ParquetPrimitiveType,
391    nested: &[Nested],
392    options: WriteOptions,
393    encoding: Encoding,
394) -> PolarsResult<Page> {
395    if nested.len() == 1 {
396        // special case where validity == def levels
397        return array_to_page_simple(array, type_, options, encoding);
398    }
399    array_to_page_nested(array, type_, nested, options, encoding)
400}
401
402/// Converts an [`Array`] to a [`CompressedPage`] based on options, descriptor and `encoding`.
403pub fn array_to_page_simple(
404    array: &dyn Array,
405    type_: ParquetPrimitiveType,
406    options: WriteOptions,
407    encoding: Encoding,
408) -> PolarsResult<Page> {
409    let dtype = array.dtype();
410
411    if type_.field_info.repetition == Repetition::Required && array.null_count() > 0 {
412        polars_bail!(InvalidOperation: "writing a missing value to required parquet column '{}'", type_.field_info.name);
413    }
414
415    match dtype {
416        // Map empty struct to boolean array with same validity.
417        ArrowDataType::Struct(fs) if fs.is_empty() => boolean::array_to_page(
418            &BooleanArray::new(
419                ArrowDataType::Boolean,
420                Bitmap::new_zeroed(array.len()),
421                array.validity().cloned(),
422            ),
423            options,
424            type_,
425            encoding,
426        ),
427
428        ArrowDataType::Boolean => boolean::array_to_page(
429            array.as_any().downcast_ref().unwrap(),
430            options,
431            type_,
432            encoding,
433        ),
434        // casts below MUST match the casts done at the metadata (field -> parquet type).
435        ArrowDataType::UInt8 => {
436            return primitive::array_to_page_integer::<u8, i32>(
437                array.as_any().downcast_ref().unwrap(),
438                options,
439                type_,
440                encoding,
441            );
442        },
443        ArrowDataType::UInt16 => {
444            return primitive::array_to_page_integer::<u16, i32>(
445                array.as_any().downcast_ref().unwrap(),
446                options,
447                type_,
448                encoding,
449            );
450        },
451        ArrowDataType::UInt32 => {
452            return primitive::array_to_page_integer::<u32, i32>(
453                array.as_any().downcast_ref().unwrap(),
454                options,
455                type_,
456                encoding,
457            );
458        },
459        ArrowDataType::UInt64 => {
460            return primitive::array_to_page_integer::<u64, i64>(
461                array.as_any().downcast_ref().unwrap(),
462                options,
463                type_,
464                encoding,
465            );
466        },
467        ArrowDataType::Int8 => {
468            return primitive::array_to_page_integer::<i8, i32>(
469                array.as_any().downcast_ref().unwrap(),
470                options,
471                type_,
472                encoding,
473            );
474        },
475        ArrowDataType::Int16 => {
476            return primitive::array_to_page_integer::<i16, i32>(
477                array.as_any().downcast_ref().unwrap(),
478                options,
479                type_,
480                encoding,
481            );
482        },
483        ArrowDataType::Int32 | ArrowDataType::Date32 | ArrowDataType::Time32(_) => {
484            return primitive::array_to_page_integer::<i32, i32>(
485                array.as_any().downcast_ref().unwrap(),
486                options,
487                type_,
488                encoding,
489            );
490        },
491        ArrowDataType::Int64
492        | ArrowDataType::Date64
493        | ArrowDataType::Time64(_)
494        | ArrowDataType::Timestamp(_, _)
495        | ArrowDataType::Duration(_) => {
496            return primitive::array_to_page_integer::<i64, i64>(
497                array.as_any().downcast_ref().unwrap(),
498                options,
499                type_,
500                encoding,
501            );
502        },
503        ArrowDataType::Float16 => {
504            let array: &PrimitiveArray<pf16> = array.as_any().downcast_ref().unwrap();
505            let statistics = options
506                .has_statistics()
507                .then(|| build_statistics_float16(array, type_.clone(), &options.statistics));
508            let array = FixedSizeBinaryArray::new(
509                ArrowDataType::FixedSizeBinary(2),
510                array.values().clone().try_transmute().unwrap(),
511                array.validity().cloned(),
512            );
513            fixed_size_binary::array_to_page(&array, options, type_, statistics)
514        },
515        ArrowDataType::Float32 => primitive::array_to_page_plain::<f32, f32>(
516            array.as_any().downcast_ref().unwrap(),
517            options,
518            type_,
519        ),
520        ArrowDataType::Float64 => primitive::array_to_page_plain::<f64, f64>(
521            array.as_any().downcast_ref().unwrap(),
522            options,
523            type_,
524        ),
525        ArrowDataType::LargeUtf8 => {
526            let array =
527                polars_compute::cast::cast(array, &ArrowDataType::LargeBinary, Default::default())
528                    .unwrap();
529            return binary::array_to_page::<i64>(
530                array.as_any().downcast_ref().unwrap(),
531                options,
532                type_,
533                encoding,
534            );
535        },
536        ArrowDataType::LargeBinary => {
537            return binary::array_to_page::<i64>(
538                array.as_any().downcast_ref().unwrap(),
539                options,
540                type_,
541                encoding,
542            );
543        },
544        ArrowDataType::BinaryView => {
545            return binview::array_to_page(
546                array.as_any().downcast_ref().unwrap(),
547                options,
548                type_,
549                encoding,
550            );
551        },
552        ArrowDataType::Utf8View => {
553            let array =
554                polars_compute::cast::cast(array, &ArrowDataType::BinaryView, Default::default())
555                    .unwrap();
556            return binview::array_to_page(
557                array.as_any().downcast_ref().unwrap(),
558                options,
559                type_,
560                encoding,
561            );
562        },
563        ArrowDataType::Null => {
564            let array = Int32Array::new_null(ArrowDataType::Int32, array.len());
565            primitive::array_to_page_plain::<i32, i32>(&array, options, type_)
566        },
567        ArrowDataType::Interval(IntervalUnit::YearMonth) => {
568            let array = array
569                .as_any()
570                .downcast_ref::<PrimitiveArray<i32>>()
571                .unwrap();
572            let mut values = Vec::<u8>::with_capacity(12 * array.len());
573            array.values().iter().for_each(|x| {
574                let bytes = &x.to_le_bytes();
575                values.extend_from_slice(bytes);
576                values.extend_from_slice(&[0; 8]);
577            });
578            let array = FixedSizeBinaryArray::new(
579                ArrowDataType::FixedSizeBinary(12),
580                values.into(),
581                array.validity().cloned(),
582            );
583            let statistics = if options.has_statistics() {
584                Some(fixed_size_binary::build_statistics(
585                    &array,
586                    type_.clone(),
587                    &options.statistics,
588                ))
589            } else {
590                None
591            };
592            fixed_size_binary::array_to_page(&array, options, type_, statistics)
593        },
594        ArrowDataType::Interval(IntervalUnit::DayTime) => {
595            let array = array
596                .as_any()
597                .downcast_ref::<PrimitiveArray<days_ms>>()
598                .unwrap();
599            let mut values = Vec::<u8>::with_capacity(12 * array.len());
600            array.values().iter().for_each(|x| {
601                let bytes = &x.to_le_bytes();
602                values.extend_from_slice(&[0; 4]); // months
603                values.extend_from_slice(bytes); // days and seconds
604            });
605            let array = FixedSizeBinaryArray::new(
606                ArrowDataType::FixedSizeBinary(12),
607                values.into(),
608                array.validity().cloned(),
609            );
610            let statistics = if options.has_statistics() {
611                Some(fixed_size_binary::build_statistics(
612                    &array,
613                    type_.clone(),
614                    &options.statistics,
615                ))
616            } else {
617                None
618            };
619            fixed_size_binary::array_to_page(&array, options, type_, statistics)
620        },
621        ArrowDataType::FixedSizeBinary(_) => {
622            let array = array.as_any().downcast_ref().unwrap();
623            let statistics = if options.has_statistics() {
624                Some(fixed_size_binary::build_statistics(
625                    array,
626                    type_.clone(),
627                    &options.statistics,
628                ))
629            } else {
630                None
631            };
632
633            fixed_size_binary::array_to_page(array, options, type_, statistics)
634        },
635        ArrowDataType::Decimal256(precision, _) => {
636            let precision = *precision;
637            let array = array
638                .as_any()
639                .downcast_ref::<PrimitiveArray<i256>>()
640                .unwrap();
641            if precision <= 9 {
642                let values = array
643                    .values()
644                    .iter()
645                    .map(|x| x.0.as_i32())
646                    .collect::<Vec<_>>()
647                    .into();
648
649                let array = PrimitiveArray::<i32>::new(
650                    ArrowDataType::Int32,
651                    values,
652                    array.validity().cloned(),
653                );
654                return primitive::array_to_page_integer::<i32, i32>(
655                    &array, options, type_, encoding,
656                );
657            } else if precision <= 18 {
658                let values = array
659                    .values()
660                    .iter()
661                    .map(|x| x.0.as_i64())
662                    .collect::<Vec<_>>()
663                    .into();
664
665                let array = PrimitiveArray::<i64>::new(
666                    ArrowDataType::Int64,
667                    values,
668                    array.validity().cloned(),
669                );
670                return primitive::array_to_page_integer::<i64, i64>(
671                    &array, options, type_, encoding,
672                );
673            } else if precision <= 38 {
674                let size = decimal_length_from_precision(precision);
675                let statistics = if options.has_statistics() {
676                    let stats = fixed_size_binary::build_statistics_decimal256_with_i128(
677                        array,
678                        type_.clone(),
679                        size,
680                        &options.statistics,
681                    );
682                    Some(stats)
683                } else {
684                    None
685                };
686
687                let mut values = Vec::<u8>::with_capacity(size * array.len());
688                array.values().iter().for_each(|x| {
689                    let bytes = &x.0.low().to_be_bytes()[16 - size..];
690                    values.extend_from_slice(bytes)
691                });
692                let array = FixedSizeBinaryArray::new(
693                    ArrowDataType::FixedSizeBinary(size),
694                    values.into(),
695                    array.validity().cloned(),
696                );
697                fixed_size_binary::array_to_page(&array, options, type_, statistics)
698            } else {
699                let size = 32;
700                let array = array
701                    .as_any()
702                    .downcast_ref::<PrimitiveArray<i256>>()
703                    .unwrap();
704                let statistics = if options.has_statistics() {
705                    let stats = fixed_size_binary::build_statistics_decimal256(
706                        array,
707                        type_.clone(),
708                        size,
709                        &options.statistics,
710                    );
711                    Some(stats)
712                } else {
713                    None
714                };
715                let mut values = Vec::<u8>::with_capacity(size * array.len());
716                array.values().iter().for_each(|x| {
717                    let bytes = &x.to_be_bytes();
718                    values.extend_from_slice(bytes)
719                });
720                let array = FixedSizeBinaryArray::new(
721                    ArrowDataType::FixedSizeBinary(size),
722                    values.into(),
723                    array.validity().cloned(),
724                );
725
726                fixed_size_binary::array_to_page(&array, options, type_, statistics)
727            }
728        },
729        ArrowDataType::Decimal(precision, _) => {
730            let precision = *precision;
731            let array = array
732                .as_any()
733                .downcast_ref::<PrimitiveArray<i128>>()
734                .unwrap();
735            if precision <= 9 {
736                let values = array
737                    .values()
738                    .iter()
739                    .map(|x| *x as i32)
740                    .collect::<Vec<_>>()
741                    .into();
742
743                let array = PrimitiveArray::<i32>::new(
744                    ArrowDataType::Int32,
745                    values,
746                    array.validity().cloned(),
747                );
748                return primitive::array_to_page_integer::<i32, i32>(
749                    &array, options, type_, encoding,
750                );
751            } else if precision <= 18 {
752                let values = array
753                    .values()
754                    .iter()
755                    .map(|x| *x as i64)
756                    .collect::<Vec<_>>()
757                    .into();
758
759                let array = PrimitiveArray::<i64>::new(
760                    ArrowDataType::Int64,
761                    values,
762                    array.validity().cloned(),
763                );
764                return primitive::array_to_page_integer::<i64, i64>(
765                    &array, options, type_, encoding,
766                );
767            } else {
768                let size = decimal_length_from_precision(precision);
769
770                let statistics = if options.has_statistics() {
771                    let stats = fixed_size_binary::build_statistics_decimal(
772                        array,
773                        type_.clone(),
774                        size,
775                        &options.statistics,
776                    );
777                    Some(stats)
778                } else {
779                    None
780                };
781
782                let mut values = Vec::<u8>::with_capacity(size * array.len());
783                array.values().iter().for_each(|x| {
784                    let bytes = &x.to_be_bytes()[16 - size..];
785                    values.extend_from_slice(bytes)
786                });
787                let array = FixedSizeBinaryArray::new(
788                    ArrowDataType::FixedSizeBinary(size),
789                    values.into(),
790                    array.validity().cloned(),
791                );
792                fixed_size_binary::array_to_page(&array, options, type_, statistics)
793            }
794        },
795        ArrowDataType::UInt128 => {
796            let array: &PrimitiveArray<u128> = array.as_any().downcast_ref().unwrap();
797            let statistics = if options.has_statistics() {
798                let stats = fixed_size_binary::build_statistics_decimal(
799                    array,
800                    type_.clone(),
801                    16,
802                    &options.statistics,
803                );
804                Some(stats)
805            } else {
806                None
807            };
808            let array = FixedSizeBinaryArray::new(
809                ArrowDataType::FixedSizeBinary(16),
810                array.values().clone().try_transmute().unwrap(),
811                array.validity().cloned(),
812            );
813            fixed_size_binary::array_to_page(&array, options, type_, statistics)
814        },
815        ArrowDataType::Int128 => {
816            let array: &PrimitiveArray<i128> = array.as_any().downcast_ref().unwrap();
817            let statistics = if options.has_statistics() {
818                let stats = fixed_size_binary::build_statistics_decimal(
819                    array,
820                    type_.clone(),
821                    16,
822                    &options.statistics,
823                );
824                Some(stats)
825            } else {
826                None
827            };
828            let array = FixedSizeBinaryArray::new(
829                ArrowDataType::FixedSizeBinary(16),
830                array.values().clone().try_transmute().unwrap(),
831                array.validity().cloned(),
832            );
833            fixed_size_binary::array_to_page(&array, options, type_, statistics)
834        },
835        ArrowDataType::Extension(ext) => {
836            let mut boxed = array.to_boxed();
837            assert!(matches!(boxed.dtype(), ArrowDataType::Extension(ext2) if ext2 == ext));
838            *boxed.dtype_mut() = ext.inner.clone();
839            return array_to_page_simple(boxed.as_ref(), type_, options, encoding);
840        },
841        other => polars_bail!(nyi = "Writing parquet pages for data type {other:?}"),
842    }
843    .map(Page::Data)
844}
845
846fn array_to_page_nested(
847    array: &dyn Array,
848    type_: ParquetPrimitiveType,
849    nested: &[Nested],
850    options: WriteOptions,
851    _encoding: Encoding,
852) -> PolarsResult<Page> {
853    if type_.field_info.repetition == Repetition::Required
854        && array.validity().is_some_and(|v| v.unset_bits() > 0)
855    {
856        polars_bail!(InvalidOperation: "writing a missing value to required parquet column '{}'", type_.field_info.name);
857    }
858
859    use ArrowDataType::*;
860    match array.dtype().to_storage() {
861        Null => {
862            let array = Int32Array::new_null(ArrowDataType::Int32, array.len());
863            primitive::nested_array_to_page::<i32, i32>(&array, options, type_, nested)
864        },
865        // Map empty struct to boolean array with same validity.
866        Struct(fs) if fs.is_empty() => {
867            let array = BooleanArray::new(
868                ArrowDataType::Boolean,
869                Bitmap::new_zeroed(array.len()),
870                array.validity().cloned(),
871            );
872            boolean::nested_array_to_page(&array, options, type_, nested)
873        },
874        Boolean => {
875            let array = array.as_any().downcast_ref().unwrap();
876            boolean::nested_array_to_page(array, options, type_, nested)
877        },
878        LargeUtf8 => {
879            let array =
880                polars_compute::cast::cast(array, &LargeBinary, Default::default()).unwrap();
881            let array = array.as_any().downcast_ref().unwrap();
882            binary::nested_array_to_page::<i64>(array, options, type_, nested)
883        },
884        LargeBinary => {
885            let array = array.as_any().downcast_ref().unwrap();
886            binary::nested_array_to_page::<i64>(array, options, type_, nested)
887        },
888        BinaryView => {
889            let array = array.as_any().downcast_ref().unwrap();
890            binview::nested_array_to_page(array, options, type_, nested)
891        },
892        Utf8View => {
893            let array = polars_compute::cast::cast(array, &BinaryView, Default::default()).unwrap();
894            let array = array.as_any().downcast_ref().unwrap();
895            binview::nested_array_to_page(array, options, type_, nested)
896        },
897        UInt8 => {
898            let array = array.as_any().downcast_ref().unwrap();
899            primitive::nested_array_to_page::<u8, i32>(array, options, type_, nested)
900        },
901        UInt16 => {
902            let array = array.as_any().downcast_ref().unwrap();
903            primitive::nested_array_to_page::<u16, i32>(array, options, type_, nested)
904        },
905        UInt32 => {
906            let array = array.as_any().downcast_ref().unwrap();
907            primitive::nested_array_to_page::<u32, i32>(array, options, type_, nested)
908        },
909        UInt64 => {
910            let array = array.as_any().downcast_ref().unwrap();
911            primitive::nested_array_to_page::<u64, i64>(array, options, type_, nested)
912        },
913        Int8 => {
914            let array = array.as_any().downcast_ref().unwrap();
915            primitive::nested_array_to_page::<i8, i32>(array, options, type_, nested)
916        },
917        Int16 => {
918            let array = array.as_any().downcast_ref().unwrap();
919            primitive::nested_array_to_page::<i16, i32>(array, options, type_, nested)
920        },
921        Int32 | Date32 | Time32(_) => {
922            let array = array.as_any().downcast_ref().unwrap();
923            primitive::nested_array_to_page::<i32, i32>(array, options, type_, nested)
924        },
925        Int64 | Date64 | Time64(_) | Timestamp(_, _) | Duration(_) => {
926            let array = array.as_any().downcast_ref().unwrap();
927            primitive::nested_array_to_page::<i64, i64>(array, options, type_, nested)
928        },
929        Float16 => {
930            let array: &PrimitiveArray<pf16> = array.as_any().downcast_ref().unwrap();
931            let statistics = options
932                .has_statistics()
933                .then(|| build_statistics_float16(array, type_.clone(), &options.statistics));
934            let array = FixedSizeBinaryArray::new(
935                ArrowDataType::FixedSizeBinary(2),
936                array.values().clone().try_transmute().unwrap(),
937                array.validity().cloned(),
938            );
939            fixed_size_binary::nested_array_to_page(&array, options, type_, nested, statistics)
940        },
941        Float32 => {
942            let array = array.as_any().downcast_ref().unwrap();
943            primitive::nested_array_to_page::<f32, f32>(array, options, type_, nested)
944        },
945        Float64 => {
946            let array = array.as_any().downcast_ref().unwrap();
947            primitive::nested_array_to_page::<f64, f64>(array, options, type_, nested)
948        },
949        Decimal(precision, _) => {
950            let precision = *precision;
951            let array = array
952                .as_any()
953                .downcast_ref::<PrimitiveArray<i128>>()
954                .unwrap();
955            if precision <= 9 {
956                let values = array
957                    .values()
958                    .iter()
959                    .map(|x| *x as i32)
960                    .collect::<Vec<_>>()
961                    .into();
962
963                let array = PrimitiveArray::<i32>::new(
964                    ArrowDataType::Int32,
965                    values,
966                    array.validity().cloned(),
967                );
968                primitive::nested_array_to_page::<i32, i32>(&array, options, type_, nested)
969            } else if precision <= 18 {
970                let values = array
971                    .values()
972                    .iter()
973                    .map(|x| *x as i64)
974                    .collect::<Vec<_>>()
975                    .into();
976
977                let array = PrimitiveArray::<i64>::new(
978                    ArrowDataType::Int64,
979                    values,
980                    array.validity().cloned(),
981                );
982                primitive::nested_array_to_page::<i64, i64>(&array, options, type_, nested)
983            } else {
984                let size = decimal_length_from_precision(precision);
985
986                let statistics = if options.has_statistics() {
987                    let stats = fixed_size_binary::build_statistics_decimal(
988                        array,
989                        type_.clone(),
990                        size,
991                        &options.statistics,
992                    );
993                    Some(stats)
994                } else {
995                    None
996                };
997
998                let mut values = Vec::<u8>::with_capacity(size * array.len());
999                array.values().iter().for_each(|x| {
1000                    let bytes = &x.to_be_bytes()[16 - size..];
1001                    values.extend_from_slice(bytes)
1002                });
1003                let array = FixedSizeBinaryArray::new(
1004                    ArrowDataType::FixedSizeBinary(size),
1005                    values.into(),
1006                    array.validity().cloned(),
1007                );
1008                fixed_size_binary::nested_array_to_page(&array, options, type_, nested, statistics)
1009            }
1010        },
1011        Decimal256(precision, _) => {
1012            let precision = *precision;
1013            let array = array
1014                .as_any()
1015                .downcast_ref::<PrimitiveArray<i256>>()
1016                .unwrap();
1017            if precision <= 9 {
1018                let values = array
1019                    .values()
1020                    .iter()
1021                    .map(|x| x.0.as_i32())
1022                    .collect::<Vec<_>>()
1023                    .into();
1024
1025                let array = PrimitiveArray::<i32>::new(
1026                    ArrowDataType::Int32,
1027                    values,
1028                    array.validity().cloned(),
1029                );
1030                primitive::nested_array_to_page::<i32, i32>(&array, options, type_, nested)
1031            } else if precision <= 18 {
1032                let values = array
1033                    .values()
1034                    .iter()
1035                    .map(|x| x.0.as_i64())
1036                    .collect::<Vec<_>>()
1037                    .into();
1038
1039                let array = PrimitiveArray::<i64>::new(
1040                    ArrowDataType::Int64,
1041                    values,
1042                    array.validity().cloned(),
1043                );
1044                primitive::nested_array_to_page::<i64, i64>(&array, options, type_, nested)
1045            } else if precision <= 38 {
1046                let size = decimal_length_from_precision(precision);
1047                let statistics = if options.has_statistics() {
1048                    let stats = fixed_size_binary::build_statistics_decimal256_with_i128(
1049                        array,
1050                        type_.clone(),
1051                        size,
1052                        &options.statistics,
1053                    );
1054                    Some(stats)
1055                } else {
1056                    None
1057                };
1058
1059                let mut values = Vec::<u8>::with_capacity(size * array.len());
1060                array.values().iter().for_each(|x| {
1061                    let bytes = &x.0.low().to_be_bytes()[16 - size..];
1062                    values.extend_from_slice(bytes)
1063                });
1064                let array = FixedSizeBinaryArray::new(
1065                    ArrowDataType::FixedSizeBinary(size),
1066                    values.into(),
1067                    array.validity().cloned(),
1068                );
1069                fixed_size_binary::nested_array_to_page(&array, options, type_, nested, statistics)
1070            } else {
1071                let size = 32;
1072                let array = array
1073                    .as_any()
1074                    .downcast_ref::<PrimitiveArray<i256>>()
1075                    .unwrap();
1076                let statistics = if options.has_statistics() {
1077                    let stats = fixed_size_binary::build_statistics_decimal256(
1078                        array,
1079                        type_.clone(),
1080                        size,
1081                        &options.statistics,
1082                    );
1083                    Some(stats)
1084                } else {
1085                    None
1086                };
1087                let mut values = Vec::<u8>::with_capacity(size * array.len());
1088                array.values().iter().for_each(|x| {
1089                    let bytes = &x.to_be_bytes();
1090                    values.extend_from_slice(bytes)
1091                });
1092                let array = FixedSizeBinaryArray::new(
1093                    ArrowDataType::FixedSizeBinary(size),
1094                    values.into(),
1095                    array.validity().cloned(),
1096                );
1097
1098                fixed_size_binary::nested_array_to_page(&array, options, type_, nested, statistics)
1099            }
1100        },
1101        Int128 => {
1102            let array: &PrimitiveArray<i128> = array.as_any().downcast_ref().unwrap();
1103            // Can't write min/max statistics for signed 128-bit integer, see #25965.
1104            let mut no_mm_options = options;
1105            no_mm_options.statistics.min_value = false;
1106            no_mm_options.statistics.max_value = false;
1107            let statistics = if no_mm_options.has_statistics() {
1108                let stats = fixed_size_binary::build_statistics_decimal(
1109                    array,
1110                    type_.clone(),
1111                    16,
1112                    &no_mm_options.statistics,
1113                );
1114                Some(stats)
1115            } else {
1116                None
1117            };
1118            let array = FixedSizeBinaryArray::new(
1119                ArrowDataType::FixedSizeBinary(16),
1120                array.values().clone().try_transmute().unwrap(),
1121                array.validity().cloned(),
1122            );
1123            fixed_size_binary::nested_array_to_page(
1124                &array,
1125                no_mm_options,
1126                type_,
1127                nested,
1128                statistics,
1129            )
1130        },
1131        UInt128 => {
1132            let array: &PrimitiveArray<u128> = array.as_any().downcast_ref().unwrap();
1133            let statistics = if options.has_statistics() {
1134                let stats = fixed_size_binary::build_statistics_decimal(
1135                    array,
1136                    type_.clone(),
1137                    16,
1138                    &options.statistics,
1139                );
1140                Some(stats)
1141            } else {
1142                None
1143            };
1144            let array = FixedSizeBinaryArray::new(
1145                ArrowDataType::FixedSizeBinary(16),
1146                array.values().clone().try_transmute().unwrap(),
1147                array.validity().cloned(),
1148            );
1149            fixed_size_binary::nested_array_to_page(&array, options, type_, nested, statistics)
1150        },
1151        other => polars_bail!(nyi = "Writing nested parquet pages for data type {other:?}"),
1152    }
1153    .map(Page::Data)
1154}
1155
1156fn get_encodings_recursive(dtype: &ArrowDataType, encodings: &mut Vec<Encoding>) {
1157    use arrow::datatypes::PhysicalType::*;
1158    match dtype.to_physical_type() {
1159        Null | Boolean | Primitive(_) | Binary | FixedSizeBinary | LargeBinary | Utf8
1160        | Dictionary(_) | LargeUtf8 | BinaryView | Utf8View => {
1161            encodings.push(get_primitive_dtype_encoding(dtype))
1162        },
1163        List | FixedSizeList | LargeList => {
1164            let a = dtype.to_storage();
1165            if let ArrowDataType::List(inner) = a {
1166                get_encodings_recursive(&inner.dtype, encodings)
1167            } else if let ArrowDataType::LargeList(inner) = a {
1168                get_encodings_recursive(&inner.dtype, encodings)
1169            } else if let ArrowDataType::FixedSizeList(inner, _) = a {
1170                get_encodings_recursive(&inner.dtype, encodings)
1171            } else {
1172                unreachable!()
1173            }
1174        },
1175        Struct => {
1176            if let ArrowDataType::Struct(fields) = dtype.to_storage() {
1177                if fields.is_empty() {
1178                    // 0-field struct writes as a boolean column representing outer validity.
1179                    encodings.push(Encoding::Rle)
1180                }
1181
1182                for field in fields {
1183                    get_encodings_recursive(&field.dtype, encodings)
1184                }
1185            } else {
1186                unreachable!()
1187            }
1188        },
1189        Map => {
1190            if let ArrowDataType::Map(field, _) = dtype.to_storage() {
1191                if let ArrowDataType::Struct(fields) = field.dtype.to_storage() {
1192                    for field in fields {
1193                        get_encodings_recursive(&field.dtype, encodings)
1194                    }
1195                } else {
1196                    unreachable!()
1197                }
1198            } else {
1199                unreachable!()
1200            }
1201        },
1202        Union => todo!(),
1203    }
1204}
1205
1206/// Transverses the `dtype` up to its (parquet) columns and returns a vector of
1207/// items based on `map`.
1208///
1209/// This is used to assign an [`Encoding`] to every parquet column based on the columns' type (see example)
1210pub fn get_dtype_encoding(dtype: &ArrowDataType) -> Vec<Encoding> {
1211    let mut encodings = vec![];
1212    get_encodings_recursive(dtype, &mut encodings);
1213    encodings
1214}
1215
1216fn get_primitive_dtype_encoding(dtype: &ArrowDataType) -> Encoding {
1217    match dtype.to_physical_type() {
1218        PhysicalType::Dictionary(_)
1219        | PhysicalType::LargeBinary
1220        | PhysicalType::LargeUtf8
1221        | PhysicalType::Utf8View
1222        | PhysicalType::BinaryView
1223        | PhysicalType::Primitive(_) => Encoding::RleDictionary,
1224        // remaining is plain
1225        _ => Encoding::Plain,
1226    }
1227}