Skip to main content

parquet/arrow/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: See the NOTICE file distributed with this work for additional information regarding copyright ownership
3// SPDX-FileContributor: Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements
4
5//! API for reading/writing Arrow [`RecordBatch`]es and [`Array`]s to/from
6//! Parquet Files.
7//!
8//! See the [crate-level documentation](crate) for more details on other APIs
9//!
10//! # Schema Conversion
11//!
12//! These APIs ensure that data in Arrow [`RecordBatch`]es written to Parquet are
13//! read back as [`RecordBatch`]es with the exact same types and values.
14//!
15//! Parquet and Arrow have different type systems, and there is not
16//! always a one to one mapping between the systems. For example, data
17//! stored as a Parquet [`BYTE_ARRAY`] can be read as either an Arrow
18//! [`BinaryViewArray`] or [`BinaryArray`].
19//!
20//! To recover the original Arrow types, the writers in this module add a "hint" to
21//! the metadata in the [`ARROW_SCHEMA_META_KEY`] key which records the original Arrow
22//! schema. The metadata hint follows the same convention as arrow-cpp based
23//! implementations such as `pyarrow`. The reader looks for the schema hint in the
24//! metadata to determine Arrow types, and if it is not present, infers the Arrow schema
25//! from the Parquet schema.
26//!
27//! In situations where the embedded Arrow schema is not compatible with the Parquet
28//! schema, the Parquet schema takes precedence and no error is raised.
29//! See [#1663](https://github.com/apache/arrow-rs/issues/1663)
30//!
31//! You can also control the type conversion process in more detail using:
32//!
33//! * [`ArrowSchemaConverter`] control the conversion of Arrow types to Parquet
34//!   types.
35//!
36//! * [`ArrowReaderOptions::with_schema`] to explicitly specify your own Arrow schema hint
37//!   to use when reading Parquet, overriding any metadata that may be present.
38//!
39//! [`RecordBatch`]: arrow_array::RecordBatch
40//! [`Array`]: arrow_array::Array
41//! [`BYTE_ARRAY`]: crate::basic::Type::BYTE_ARRAY
42//! [`BinaryViewArray`]: arrow_array::BinaryViewArray
43//! [`BinaryArray`]: arrow_array::BinaryArray
44//! [`ArrowReaderOptions::with_schema`]: arrow_reader::ArrowReaderOptions::with_schema
45//!
46//! # Example: Writing Arrow `RecordBatch` to Parquet file
47//!
48//!```rust
49//! # use arrow_array::{Int32Array, ArrayRef};
50//! # use arrow_array::RecordBatch;
51//! # use parquet::arrow::arrow_writer::ArrowWriter;
52//! # use parquet::file::properties::WriterProperties;
53//! # use tempfile::tempfile;
54//! # use std::sync::Arc;
55//! # use parquet::basic::Compression;
56//! let ids = Int32Array::from(vec![1, 2, 3, 4]);
57//! let vals = Int32Array::from(vec![5, 6, 7, 8]);
58//! let batch = RecordBatch::try_from_iter(vec![
59//!   ("id", Arc::new(ids) as ArrayRef),
60//!   ("val", Arc::new(vals) as ArrayRef),
61//! ]).unwrap();
62//!
63//! let file = tempfile().unwrap();
64//!
65//! // WriterProperties can be used to set Parquet file options
66//! let props = WriterProperties::builder()
67//!     .set_compression(Compression::SNAPPY)
68//!     .build();
69//!
70//! let mut writer = ArrowWriter::try_new(file, batch.schema(), Some(props)).unwrap();
71//!
72//! writer.write(&batch).expect("Writing batch");
73//!
74//! // writer must be closed to write footer
75//! writer.close().unwrap();
76//! ```
77//!
78//! # Example: Reading Parquet file into Arrow `RecordBatch`
79//!
80//! ```rust
81//! # use std::fs::File;
82//! # use parquet::arrow::arrow_reader::ParquetRecordBatchReaderBuilder;
83//! # use std::sync::Arc;
84//! # use arrow_array::Int32Array;
85//! # use arrow::datatypes::{DataType, Field, Schema};
86//! # use arrow_array::RecordBatch;
87//! # use parquet::arrow::arrow_writer::ArrowWriter;
88//! #
89//! # let ids = Int32Array::from(vec![1, 2, 3, 4]);
90//! # let schema = Arc::new(Schema::new(vec![
91//! #     Field::new("id", DataType::Int32, false),
92//! # ]));
93//! #
94//! # let file = File::create("data.parquet").unwrap();
95//! #
96//! # let batch = RecordBatch::try_new(Arc::clone(&schema), vec![Arc::new(ids)]).unwrap();
97//! # let batches = vec![batch];
98//! #
99//! # let mut writer = ArrowWriter::try_new(file, Arc::clone(&schema), None).unwrap();
100//! #
101//! # for batch in batches {
102//! #     writer.write(&batch).expect("Writing batch");
103//! # }
104//! # writer.close().unwrap();
105//! #
106//! let file = File::open("data.parquet").unwrap();
107//!
108//! let builder = ParquetRecordBatchReaderBuilder::try_new(file).unwrap();
109//! println!("Converted arrow schema is: {}", builder.schema());
110//!
111//! let mut reader = builder.build().unwrap();
112//!
113//! let record_batch = reader.next().unwrap().unwrap();
114//!
115//! println!("Read {} records.", record_batch.num_rows());
116//! ```
117//!
118//! # Example: Reading non-uniformly encrypted parquet file into arrow record batch
119//!
120//! Note: This requires the experimental `encryption` feature to be enabled at compile time.
121//!
122#![cfg_attr(feature = "encryption", doc = "```rust")]
123#![cfg_attr(not(feature = "encryption"), doc = "```ignore")]
124//! # use arrow_array::{Int32Array, ArrayRef};
125//! # use arrow_array::{types, RecordBatch};
126//! # use parquet::arrow::arrow_reader::{
127//! #     ArrowReaderMetadata, ArrowReaderOptions, ParquetRecordBatchReaderBuilder,
128//! # };
129//! # use arrow_array::cast::AsArray;
130//! # use parquet::file::metadata::ParquetMetaData;
131//! # use tempfile::tempfile;
132//! # use std::fs::File;
133//! # use parquet::encryption::decrypt::FileDecryptionProperties;
134//! # let test_data = arrow::util::test_util::parquet_test_data();
135//! # let path = format!("{test_data}/encrypt_columns_and_footer.parquet.encrypted");
136//! #
137//! let file = File::open(path).unwrap();
138//!
139//! // Define the AES encryption keys required for decrypting the footer metadata
140//! // and column-specific data. If only a footer key is used then it is assumed that the
141//! // file uses uniform encryption and all columns are encrypted with the footer key.
142//! // If any column keys are specified, other columns without a key provided are assumed
143//! // to be unencrypted
144//! let footer_key = "0123456789012345".as_bytes(); // Keys are 128 bits (16 bytes)
145//! let column_1_key = "1234567890123450".as_bytes();
146//! let column_2_key = "1234567890123451".as_bytes();
147//!
148//! let decryption_properties = FileDecryptionProperties::builder(footer_key.to_vec())
149//!     .with_column_key("double_field", column_1_key.to_vec())
150//!     .with_column_key("float_field", column_2_key.to_vec())
151//!     .build()
152//!     .unwrap();
153//!
154//! let options = ArrowReaderOptions::default()
155//!  .with_file_decryption_properties(decryption_properties);
156//! let reader_metadata = ArrowReaderMetadata::load(&file, options.clone()).unwrap();
157//! let file_metadata = reader_metadata.metadata().file_metadata();
158//! assert_eq!(50, file_metadata.num_rows());
159//!
160//! let mut reader = ParquetRecordBatchReaderBuilder::try_new_with_options(file, options)
161//!   .unwrap()
162//!   .build()
163//!   .unwrap();
164//!
165//! let record_batch = reader.next().unwrap().unwrap();
166//! assert_eq!(50, record_batch.num_rows());
167//! ```
168
169// Keep these module declarations explicit so rustfmt discovers their source files.
170// See the comment in the crate root.
171#[cfg(feature = "experimental")]
172#[doc(hidden)]
173pub mod array_reader;
174#[cfg(not(feature = "experimental"))]
175mod array_reader;
176// Re-exported (beyond the `experimental` feature) so `file::metadata::dictionary`
177// can PLAIN-decode a raw dictionary page without duplicating this logic.
178pub(crate) use array_reader::ByteArrayDecoderPlain;
179pub mod arrow_reader;
180pub mod arrow_writer;
181pub mod bloom_filter;
182mod buffer;
183#[cfg(feature = "file_experimental")]
184pub mod file_type;
185pub(crate) use buffer::offset_buffer::OffsetBuffer;
186mod decoder;
187
188#[cfg(feature = "async")]
189pub mod async_reader;
190#[cfg(feature = "async")]
191pub mod async_writer;
192
193pub mod push_decoder;
194
195mod in_memory_row_group;
196mod record_reader;
197
198#[cfg(feature = "experimental")]
199#[doc(hidden)]
200pub mod schema;
201#[cfg(not(feature = "experimental"))]
202mod schema;
203
204use std::fmt::Debug;
205
206pub use self::arrow_writer::ArrowWriter;
207#[cfg(feature = "async")]
208pub use self::async_reader::ParquetRecordBatchStreamBuilder;
209#[cfg(feature = "async")]
210pub use self::async_writer::AsyncArrowWriter;
211use crate::schema::types::SchemaDescriptor;
212use arrow_schema::{FieldRef, Schema};
213
214pub use self::schema::{
215    ArrowSchemaConverter, FieldLevels, add_encoded_arrow_schema_to_metadata, encode_arrow_schema,
216    parquet_to_arrow_field_levels, parquet_to_arrow_field_levels_with_virtual,
217    parquet_to_arrow_schema, parquet_to_arrow_schema_by_columns, virtual_type::*,
218};
219
220/// Schema metadata key used to store serialized Arrow schema
221///
222/// The Arrow schema is encoded using the Arrow IPC format, and then base64
223/// encoded. This is the same format used by arrow-cpp systems, such as pyarrow.
224pub const ARROW_SCHEMA_META_KEY: &str = "ARROW:schema";
225
226/// The value of this metadata key, if present on [`Field::metadata`], will be used
227/// to populate [`BasicTypeInfo::id`]
228///
229/// [`Field::metadata`]: arrow_schema::Field::metadata
230/// [`BasicTypeInfo::id`]: crate::schema::types::BasicTypeInfo::id
231pub const PARQUET_FIELD_ID_META_KEY: &str = "PARQUET:field_id";
232
233/// A [`ProjectionMask`] identifies a set of columns within a potentially nested schema to project
234///
235/// In particular, a [`ProjectionMask`] can be constructed from a list of leaf column indices
236/// or root column indices where:
237///
238/// * Root columns are the direct children of the root schema, enumerated in order
239/// * Leaf columns are the child-less leaves of the schema as enumerated by a depth-first search
240///
241/// For example, the schema
242///
243/// ```ignore
244/// message schema {
245///   REQUIRED boolean         leaf_1;
246///   REQUIRED GROUP group {
247///     OPTIONAL int32 leaf_2;
248///     OPTIONAL int64 leaf_3;
249///   }
250/// }
251/// ```
252///
253/// Has roots `["leaf_1", "group"]` and leaves `["leaf_1", "leaf_2", "leaf_3"]`
254///
255/// For non-nested schemas, i.e. those containing only primitive columns, the root
256/// and leaves are the same
257///
258#[derive(Debug, Clone, PartialEq, Eq)]
259pub struct ProjectionMask {
260    /// If `Some`, a leaf column should be included if the value at
261    /// the corresponding index is true
262    ///
263    /// If `None`, all columns should be included
264    ///
265    /// # Examples
266    ///
267    /// Given the original parquet schema with leaf columns is `[a, b, c, d]`
268    ///
269    /// A mask of `[true, false, true, false]` will result in a schema 2
270    /// elements long:
271    /// * `fields[0]`: `a`
272    /// * `fields[1]`: `c`
273    ///
274    /// A mask of `None` will result in a schema 4 elements long:
275    /// * `fields[0]`: `a`
276    /// * `fields[1]`: `b`
277    /// * `fields[2]`: `c`
278    /// * `fields[3]`: `d`
279    mask: Option<Vec<bool>>,
280}
281
282impl ProjectionMask {
283    /// Create a [`ProjectionMask`] which selects all columns
284    pub fn all() -> Self {
285        Self { mask: None }
286    }
287
288    /// Create a [`ProjectionMask`] which selects no columns
289    pub fn none(len: usize) -> Self {
290        Self {
291            mask: Some(vec![false; len]),
292        }
293    }
294
295    /// Create a [`ProjectionMask`] which selects only the specified leaf columns
296    ///
297    /// Note: repeated or out of order indices will not impact the final mask
298    ///
299    /// i.e. `[0, 1, 2]` will construct the same mask as `[1, 0, 0, 2]`
300    pub fn leaves(schema: &SchemaDescriptor, indices: impl IntoIterator<Item = usize>) -> Self {
301        let mut mask = vec![false; schema.num_columns()];
302        for leaf_idx in indices {
303            mask[leaf_idx] = true;
304        }
305        Self { mask: Some(mask) }
306    }
307
308    /// Create a [`ProjectionMask`] which selects only the specified root columns
309    ///
310    /// Note: repeated or out of order indices will not impact the final mask
311    ///
312    /// i.e. `[0, 1, 2]` will construct the same mask as `[1, 0, 0, 2]`
313    pub fn roots(schema: &SchemaDescriptor, indices: impl IntoIterator<Item = usize>) -> Self {
314        let num_root_columns = schema.root_schema().get_fields().len();
315        let mut root_mask = vec![false; num_root_columns];
316        for root_idx in indices {
317            root_mask[root_idx] = true;
318        }
319
320        let mask = (0..schema.num_columns())
321            .map(|leaf_idx| {
322                let root_idx = schema.get_column_root_idx(leaf_idx);
323                root_mask[root_idx]
324            })
325            .collect();
326
327        Self { mask: Some(mask) }
328    }
329
330    /// Create a [`ProjectionMask`] which selects only the named columns
331    ///
332    /// All leaf columns that fall below a given name will be selected. For example, given
333    /// the schema
334    /// ```ignore
335    /// message schema {
336    ///   OPTIONAL group a (MAP) {
337    ///     REPEATED group key_value {
338    ///       REQUIRED BYTE_ARRAY key (UTF8);  // leaf index 0
339    ///       OPTIONAL group value (MAP) {
340    ///         REPEATED group key_value {
341    ///           REQUIRED INT32 key;          // leaf index 1
342    ///           REQUIRED BOOLEAN value;      // leaf index 2
343    ///         }
344    ///       }
345    ///     }
346    ///   }
347    ///   REQUIRED INT32 b;                    // leaf index 3
348    ///   REQUIRED DOUBLE c;                   // leaf index 4
349    /// }
350    /// ```
351    /// `["a.key_value.value", "c"]` would return leaf columns 1, 2, and 4. `["a"]` would return
352    /// columns 0, 1, and 2.
353    ///
354    /// Note: repeated or out of order indices will not impact the final mask.
355    ///
356    /// i.e. `["b", "c"]` will construct the same mask as `["c", "b", "c"]`.
357    ///
358    /// Also, this will not produce the desired results if a column contains a '.' in its name.
359    /// Use [`Self::leaves`] or [`Self::roots`] in that case.
360    pub fn columns<'a>(
361        schema: &SchemaDescriptor,
362        names: impl IntoIterator<Item = &'a str>,
363    ) -> Self {
364        let mut mask = vec![false; schema.num_columns()];
365        for name in names {
366            let name_path: Vec<&str> = name.split('.').collect();
367            for (idx, col) in schema.columns().iter().enumerate() {
368                let path = col.path().parts();
369                // searching for "a.b.c" cannot match "a.b"
370                if name_path.len() > path.len() {
371                    continue;
372                }
373                // now path >= name_path, so check that each element in name_path matches
374                if name_path.iter().zip(path.iter()).all(|(a, b)| a == b) {
375                    mask[idx] = true;
376                }
377            }
378        }
379
380        Self { mask: Some(mask) }
381    }
382
383    /// Returns true if the leaf column `leaf_idx` is included by the mask
384    pub fn leaf_included(&self, leaf_idx: usize) -> bool {
385        self.mask.as_ref().map(|m| m[leaf_idx]).unwrap_or(true)
386    }
387
388    /// Union two projection masks
389    ///
390    /// Example:
391    /// ```text
392    /// mask1 = [true, false, true]
393    /// mask2 = [false, true, true]
394    /// union(mask1, mask2) = [true, true, true]
395    /// ```
396    pub fn union(&mut self, other: &Self) {
397        match (self.mask.as_ref(), other.mask.as_ref()) {
398            (None, _) | (_, None) => self.mask = None,
399            (Some(a), Some(b)) => {
400                debug_assert_eq!(a.len(), b.len());
401                let mask = a.iter().zip(b.iter()).map(|(&a, &b)| a || b).collect();
402                self.mask = Some(mask);
403            }
404        }
405    }
406
407    /// Intersect two projection masks
408    ///
409    /// Example:
410    /// ```text
411    /// mask1 = [true, false, true]
412    /// mask2 = [false, true, true]
413    /// intersect(mask1, mask2) = [false, false, true]
414    /// ```
415    pub fn intersect(&mut self, other: &Self) {
416        match (self.mask.as_ref(), other.mask.as_ref()) {
417            (None, _) => self.mask.clone_from(&other.mask),
418            (_, None) => {}
419            (Some(a), Some(b)) => {
420                debug_assert_eq!(a.len(), b.len());
421                let mask = a.iter().zip(b.iter()).map(|(&a, &b)| a && b).collect();
422                self.mask = Some(mask);
423            }
424        }
425    }
426
427    /// Return a new [`ProjectionMask`] that excludes any leaf columns that are
428    /// part of a nested type, such as struct, list, or map
429    ///
430    /// If there are no non-nested columns in the mask, returns `None`
431    pub(crate) fn without_nested_types(&self, schema: &SchemaDescriptor) -> Option<Self> {
432        let num_leaves = schema.num_columns();
433
434        // Count how many leaves each root column has
435        let num_roots = schema.root_schema().get_fields().len();
436        let mut root_leaf_counts = vec![0usize; num_roots];
437        for leaf_idx in 0..num_leaves {
438            let root_idx = schema.get_column_root_idx(leaf_idx);
439            root_leaf_counts[root_idx] += 1;
440        }
441
442        // Cache only top-level primitive columns.
443        // Even a one-leaf group is nested; caching it drops parent def levels.
444        let mut included_leaves = Vec::new();
445        for leaf_idx in 0..num_leaves {
446            if self.leaf_included(leaf_idx) {
447                let root = schema.get_column_root(leaf_idx);
448                let root_idx = schema.get_column_root_idx(leaf_idx);
449                if root_leaf_counts[root_idx] == 1 && root.is_primitive() {
450                    included_leaves.push(leaf_idx);
451                }
452            }
453        }
454
455        if included_leaves.is_empty() {
456            None
457        } else {
458            Some(ProjectionMask::leaves(schema, included_leaves))
459        }
460    }
461}
462
463/// Lookups up the parquet column by name
464///
465/// Returns the parquet column index and the corresponding arrow field
466pub fn parquet_column<'a>(
467    parquet_schema: &SchemaDescriptor,
468    arrow_schema: &'a Schema,
469    name: &str,
470) -> Option<(usize, &'a FieldRef)> {
471    let (root_idx, field) = arrow_schema.fields.find(name)?;
472    if field.data_type().is_nested() {
473        // Nested fields are not supported and require non-trivial logic
474        // to correctly walk the parquet schema accounting for the
475        // logical type rules - <https://github.com/apache/parquet-format/blob/master/LogicalTypes.md>
476        //
477        // For example a ListArray could correspond to anything from 1 to 3 levels
478        // in the parquet schema
479        return None;
480    }
481
482    // This could be made more efficient (#TBD)
483    let parquet_idx = (0..parquet_schema.columns().len())
484        .find(|x| parquet_schema.get_column_root_idx(*x) == root_idx)?;
485    Some((parquet_idx, field))
486}
487
488#[cfg(test)]
489mod test {
490    use crate::arrow::ArrowWriter;
491    use crate::file::metadata::{
492        PageIndexPolicy, ParquetMetaData, ParquetMetaDataOptions, ParquetMetaDataReader,
493        ParquetMetaDataWriter,
494    };
495    use crate::file::properties::{EnabledStatistics, WriterProperties};
496    use crate::schema::parser::parse_message_type;
497    use crate::schema::types::SchemaDescriptor;
498    use arrow_array::{ArrayRef, Int32Array, RecordBatch};
499    use bytes::Bytes;
500    use std::sync::Arc;
501
502    use super::ProjectionMask;
503
504    #[test]
505    // Reproducer for https://github.com/apache/arrow-rs/issues/6464
506    fn test_metadata_read_write_partial_offset() {
507        let parquet_bytes = create_parquet_file();
508
509        // read the metadata from the file WITHOUT the page index structures
510        let options = ParquetMetaDataOptions::new().with_encoding_stats_as_mask(false);
511        let original_metadata = ParquetMetaDataReader::new()
512            .with_metadata_options(Some(options))
513            .parse_and_finish(&parquet_bytes)
514            .unwrap();
515
516        // this should error because the page indexes are not present, but have offsets specified
517        let metadata_bytes = metadata_to_bytes(&original_metadata);
518        let options = ParquetMetaDataOptions::new().with_encoding_stats_as_mask(false);
519        let err = ParquetMetaDataReader::new()
520            .with_metadata_options(Some(options))
521            .with_page_index_policy(PageIndexPolicy::Required) // there are no page indexes in the metadata
522            .parse_and_finish(&metadata_bytes)
523            .err()
524            .unwrap();
525        assert_eq!(
526            err.to_string(),
527            "EOF: Parquet file too small. Page index range 82..115 overlaps with file metadata 0..356"
528        );
529    }
530
531    #[test]
532    fn test_metadata_read_write_roundtrip() {
533        let parquet_bytes = create_parquet_file();
534
535        // read the metadata from the file
536        let options = ParquetMetaDataOptions::new().with_encoding_stats_as_mask(false);
537        let original_metadata = ParquetMetaDataReader::new()
538            .with_metadata_options(Some(options))
539            .parse_and_finish(&parquet_bytes)
540            .unwrap();
541
542        // read metadata back from the serialized bytes and ensure it is the same
543        let metadata_bytes = metadata_to_bytes(&original_metadata);
544        assert_ne!(
545            metadata_bytes.len(),
546            parquet_bytes.len(),
547            "metadata is subset of parquet"
548        );
549
550        let options = ParquetMetaDataOptions::new().with_encoding_stats_as_mask(false);
551        let roundtrip_metadata = ParquetMetaDataReader::new()
552            .with_metadata_options(Some(options))
553            .parse_and_finish(&metadata_bytes)
554            .unwrap();
555
556        assert_eq!(original_metadata, roundtrip_metadata);
557    }
558
559    #[test]
560    #[cfg_attr(miri, ignore)] // Takes too long
561    fn test_metadata_read_write_roundtrip_page_index() {
562        let parquet_bytes = create_parquet_file();
563
564        // read the metadata from the file including the page index structures
565        // (which are stored elsewhere in the footer)
566        let options = ParquetMetaDataOptions::new().with_encoding_stats_as_mask(false);
567        let original_metadata = ParquetMetaDataReader::new()
568            .with_metadata_options(Some(options))
569            .with_page_index_policy(PageIndexPolicy::Required)
570            .parse_and_finish(&parquet_bytes)
571            .unwrap();
572
573        // read metadata back from the serialized bytes and ensure it is the same
574        let metadata_bytes = metadata_to_bytes(&original_metadata);
575        let options = ParquetMetaDataOptions::new().with_encoding_stats_as_mask(false);
576        let roundtrip_metadata = ParquetMetaDataReader::new()
577            .with_metadata_options(Some(options))
578            .with_page_index_policy(PageIndexPolicy::Required)
579            .parse_and_finish(&metadata_bytes)
580            .unwrap();
581
582        // Need to normalize the metadata first to remove offsets in data
583        let original_metadata = normalize_locations(original_metadata);
584        let roundtrip_metadata = normalize_locations(roundtrip_metadata);
585        assert_eq!(
586            format!("{original_metadata:#?}"),
587            format!("{roundtrip_metadata:#?}")
588        );
589        assert_eq!(original_metadata, roundtrip_metadata);
590    }
591
592    #[test]
593    fn test_metadata_read_write_roundtrip_missing_page_index() {
594        let parquet_bytes = create_parquet_file();
595
596        // read the metadata from the file but skip the page indexes
597        let options = ParquetMetaDataOptions::new().with_encoding_stats_as_mask(false);
598        let original_metadata = ParquetMetaDataReader::new()
599            .with_metadata_options(Some(options))
600            .with_page_index_policy(PageIndexPolicy::Skip)
601            .parse_and_finish(&parquet_bytes)
602            .unwrap();
603
604        // metadata_to_bytes_no_page_idx should zero out the page index locations. if they aren't
605        // then reading metadata_bytes will fail with EOF
606        let metadata_bytes = metadata_to_bytes_no_page_idx(&original_metadata);
607        let options = ParquetMetaDataOptions::new().with_encoding_stats_as_mask(false);
608        let roundtrip_metadata = ParquetMetaDataReader::new()
609            .with_metadata_options(Some(options))
610            .with_page_index_policy(PageIndexPolicy::Optional)
611            .parse_and_finish(&metadata_bytes)
612            .expect("page index locations should have been cleared");
613
614        assert!(roundtrip_metadata.page_index().is_none());
615    }
616
617    #[test]
618    fn test_metadata_read_write_roundtrip_offset_index_only() {
619        // `Chunk` statistics: the file has an offset index, but no column index
620        let array: ArrayRef = Arc::new(Int32Array::from(vec![1, 2, 3]));
621        let batch = RecordBatch::try_from_iter(vec![("id", array)]).unwrap();
622        let props = WriterProperties::builder()
623            .set_statistics_enabled(EnabledStatistics::Chunk)
624            .build();
625        let mut buf = vec![];
626        let mut writer = ArrowWriter::try_new(&mut buf, batch.schema(), Some(props)).unwrap();
627        writer.write(&batch).unwrap();
628        writer.close().unwrap();
629
630        let read = |bytes: &Bytes| {
631            let options = ParquetMetaDataOptions::new().with_encoding_stats_as_mask(false);
632            ParquetMetaDataReader::new()
633                .with_metadata_options(Some(options))
634                .with_page_index_policy(PageIndexPolicy::Optional)
635                .parse_and_finish(bytes)
636                .unwrap()
637        };
638        let original = read(&Bytes::from(buf));
639        let page_index = original.page_index().unwrap();
640        assert!(!page_index.has_column_indexes() && page_index.has_offset_indexes());
641        let roundtrip = read(&metadata_to_bytes(&original));
642        assert_eq!(
643            normalize_locations(original),
644            normalize_locations(roundtrip)
645        );
646    }
647
648    #[test]
649    fn test_metadata_read_write_roundtrip_custom_page_index() {
650        use crate::file::metadata::page_index::PageIndexProvider;
651        use crate::file::page_index::{
652            column_index::ColumnIndexMetaData, offset_index::OffsetIndexMetaData,
653        };
654
655        /// A custom provider that forwards to another provider
656        #[derive(Debug)]
657        struct Forward(Arc<dyn PageIndexProvider>);
658        impl PageIndexProvider for Forward {
659            fn has_offset_indexes(&self) -> bool {
660                self.0.has_offset_indexes()
661            }
662            fn has_column_indexes(&self) -> bool {
663                self.0.has_column_indexes()
664            }
665            fn column_index(&self, rg: usize, col: usize) -> Option<&ColumnIndexMetaData> {
666                self.0.column_index(rg, col)
667            }
668            fn offset_index(&self, rg: usize, col: usize) -> Option<&OffsetIndexMetaData> {
669                self.0.offset_index(rg, col)
670            }
671            fn as_any(&self) -> &dyn std::any::Any {
672                self
673            }
674        }
675
676        let read = |bytes: &Bytes| {
677            let options = ParquetMetaDataOptions::new().with_encoding_stats_as_mask(false);
678            ParquetMetaDataReader::new()
679                .with_metadata_options(Some(options))
680                .with_page_index_policy(PageIndexPolicy::Required)
681                .parse_and_finish(bytes)
682                .unwrap()
683        };
684        let original = read(&create_parquet_file());
685        let provider = Forward(original.page_index().unwrap().clone());
686        let custom = original
687            .clone()
688            .into_builder()
689            .set_page_index(Some(Arc::new(provider)))
690            .build();
691        let roundtrip = read(&metadata_to_bytes(&custom));
692        assert_eq!(
693            normalize_locations(original),
694            normalize_locations(roundtrip)
695        );
696    }
697
698    /// Sets the page index offset locations in the metadata to `None`
699    ///
700    /// This is because the offsets are used to find the relative location of the index
701    /// structures, and thus differ depending on how the structures are stored.
702    fn normalize_locations(metadata: ParquetMetaData) -> ParquetMetaData {
703        let mut metadata_builder = metadata.into_builder();
704        for rg in metadata_builder.take_row_groups() {
705            let mut rg_builder = rg.into_builder();
706            for col in rg_builder.take_columns() {
707                rg_builder = rg_builder.add_column_metadata(
708                    col.into_builder()
709                        .set_offset_index_offset(None)
710                        .set_index_page_offset(None)
711                        .set_column_index_offset(None)
712                        .build()
713                        .unwrap(),
714                );
715            }
716            let rg = rg_builder.build().unwrap();
717            metadata_builder = metadata_builder.add_row_group(rg);
718        }
719        metadata_builder.build()
720    }
721
722    /// Write a parquet filed into an in memory buffer
723    fn create_parquet_file() -> Bytes {
724        let mut buf = vec![];
725        let data = vec![100, 200, 201, 300, 102, 33];
726        let array: ArrayRef = Arc::new(Int32Array::from(data));
727        let batch = RecordBatch::try_from_iter(vec![("id", array)]).unwrap();
728        let props = WriterProperties::builder()
729            .set_statistics_enabled(EnabledStatistics::Page)
730            .set_write_page_header_statistics(true)
731            .build();
732
733        let mut writer = ArrowWriter::try_new(&mut buf, batch.schema(), Some(props)).unwrap();
734        writer.write(&batch).unwrap();
735        writer.finish().unwrap();
736        drop(writer);
737
738        Bytes::from(buf)
739    }
740
741    /// Serializes `ParquetMetaData` into a memory buffer, using `ParquetMetadataWriter
742    fn metadata_to_bytes(metadata: &ParquetMetaData) -> Bytes {
743        let mut buf = vec![];
744        ParquetMetaDataWriter::new(&mut buf, metadata)
745            .finish()
746            .unwrap();
747        Bytes::from(buf)
748    }
749
750    // like metadata_to_bytes, but do not preserve page index location info
751    fn metadata_to_bytes_no_page_idx(metadata: &ParquetMetaData) -> Bytes {
752        let mut buf = vec![];
753        ParquetMetaDataWriter::new(&mut buf, metadata)
754            .with_preserve_page_index_locations(false)
755            .finish()
756            .unwrap();
757        Bytes::from(buf)
758    }
759
760    #[test]
761    fn test_mask_from_column_names() {
762        let schema = parse_schema(
763            "
764            message test_schema {
765                OPTIONAL group a (MAP) {
766                    REPEATED group key_value {
767                        REQUIRED BYTE_ARRAY key (UTF8);
768                        OPTIONAL group value (MAP) {
769                            REPEATED group key_value {
770                                REQUIRED INT32 key;
771                                REQUIRED BOOLEAN value;
772                            }
773                        }
774                    }
775                }
776                REQUIRED INT32 b;
777                REQUIRED DOUBLE c;
778            }
779            ",
780        );
781
782        let mask = ProjectionMask::columns(&schema, ["foo", "bar"]);
783        assert_eq!(mask.mask.unwrap(), vec![false; 5]);
784
785        let mask = ProjectionMask::columns(&schema, []);
786        assert_eq!(mask.mask.unwrap(), vec![false; 5]);
787
788        let mask = ProjectionMask::columns(&schema, ["a", "c"]);
789        assert_eq!(mask.mask.unwrap(), [true, true, true, false, true]);
790
791        let mask = ProjectionMask::columns(&schema, ["a.key_value.key", "c"]);
792        assert_eq!(mask.mask.unwrap(), [true, false, false, false, true]);
793
794        let mask = ProjectionMask::columns(&schema, ["a.key_value.value", "b"]);
795        assert_eq!(mask.mask.unwrap(), [false, true, true, true, false]);
796
797        let schema = parse_schema(
798            "
799            message test_schema {
800                OPTIONAL group a (LIST) {
801                    REPEATED group list {
802                        OPTIONAL group element (LIST) {
803                            REPEATED group list {
804                                OPTIONAL group element (LIST) {
805                                    REPEATED group list {
806                                        OPTIONAL BYTE_ARRAY element (UTF8);
807                                    }
808                                }
809                            }
810                        }
811                    }
812                }
813                REQUIRED INT32 b;
814            }
815            ",
816        );
817
818        let mask = ProjectionMask::columns(&schema, ["a", "b"]);
819        assert_eq!(mask.mask.unwrap(), [true, true]);
820
821        let mask = ProjectionMask::columns(&schema, ["a.list.element", "b"]);
822        assert_eq!(mask.mask.unwrap(), [true, true]);
823
824        let mask =
825            ProjectionMask::columns(&schema, ["a.list.element.list.element.list.element", "b"]);
826        assert_eq!(mask.mask.unwrap(), [true, true]);
827
828        let mask = ProjectionMask::columns(&schema, ["b"]);
829        assert_eq!(mask.mask.unwrap(), [false, true]);
830
831        let schema = parse_schema(
832            "
833            message test_schema {
834                OPTIONAL INT32 a;
835                OPTIONAL INT32 b;
836                OPTIONAL INT32 c;
837                OPTIONAL INT32 d;
838                OPTIONAL INT32 e;
839            }
840            ",
841        );
842
843        let mask = ProjectionMask::columns(&schema, ["a", "b"]);
844        assert_eq!(mask.mask.unwrap(), [true, true, false, false, false]);
845
846        let mask = ProjectionMask::columns(&schema, ["d", "b", "d"]);
847        assert_eq!(mask.mask.unwrap(), [false, true, false, true, false]);
848
849        let schema = parse_schema(
850            "
851            message test_schema {
852                OPTIONAL INT32 a;
853                OPTIONAL INT32 b;
854                OPTIONAL INT32 a;
855                OPTIONAL INT32 d;
856                OPTIONAL INT32 e;
857            }
858            ",
859        );
860
861        let mask = ProjectionMask::columns(&schema, ["a", "e"]);
862        assert_eq!(mask.mask.unwrap(), [true, false, true, false, true]);
863
864        let schema = parse_schema(
865            "
866            message test_schema {
867                OPTIONAL INT32 a;
868                OPTIONAL INT32 aa;
869            }
870            ",
871        );
872
873        let mask = ProjectionMask::columns(&schema, ["a"]);
874        assert_eq!(mask.mask.unwrap(), [true, false]);
875    }
876
877    #[test]
878    fn test_projection_mask_union() {
879        let mut mask1 = ProjectionMask {
880            mask: Some(vec![true, false, true]),
881        };
882        let mask2 = ProjectionMask {
883            mask: Some(vec![false, true, true]),
884        };
885        mask1.union(&mask2);
886        assert_eq!(mask1.mask, Some(vec![true, true, true]));
887
888        let mut mask1 = ProjectionMask { mask: None };
889        let mask2 = ProjectionMask {
890            mask: Some(vec![false, true, true]),
891        };
892        mask1.union(&mask2);
893        assert_eq!(mask1.mask, None);
894
895        let mut mask1 = ProjectionMask {
896            mask: Some(vec![true, false, true]),
897        };
898        let mask2 = ProjectionMask { mask: None };
899        mask1.union(&mask2);
900        assert_eq!(mask1.mask, None);
901
902        let mut mask1 = ProjectionMask { mask: None };
903        let mask2 = ProjectionMask { mask: None };
904        mask1.union(&mask2);
905        assert_eq!(mask1.mask, None);
906    }
907
908    #[test]
909    fn test_projection_mask_intersect() {
910        let mut mask1 = ProjectionMask {
911            mask: Some(vec![true, false, true]),
912        };
913        let mask2 = ProjectionMask {
914            mask: Some(vec![false, true, true]),
915        };
916        mask1.intersect(&mask2);
917        assert_eq!(mask1.mask, Some(vec![false, false, true]));
918
919        let mut mask1 = ProjectionMask { mask: None };
920        let mask2 = ProjectionMask {
921            mask: Some(vec![false, true, true]),
922        };
923        mask1.intersect(&mask2);
924        assert_eq!(mask1.mask, Some(vec![false, true, true]));
925
926        let mut mask1 = ProjectionMask {
927            mask: Some(vec![true, false, true]),
928        };
929        let mask2 = ProjectionMask { mask: None };
930        mask1.intersect(&mask2);
931        assert_eq!(mask1.mask, Some(vec![true, false, true]));
932
933        let mut mask1 = ProjectionMask { mask: None };
934        let mask2 = ProjectionMask { mask: None };
935        mask1.intersect(&mask2);
936        assert_eq!(mask1.mask, None);
937    }
938
939    #[test]
940    fn test_projection_mask_without_nested_no_nested() {
941        // Schema with no nested types
942        let schema = parse_schema(
943            "
944            message test_schema {
945                OPTIONAL INT32 a;
946                OPTIONAL INT32 b;
947                REQUIRED DOUBLE d;
948            }
949            ",
950        );
951
952        let mask = ProjectionMask::all();
953        // All columns are non-nested, but without_nested_types returns a new mask
954        assert_eq!(
955            Some(ProjectionMask::leaves(&schema, [0, 1, 2])),
956            mask.without_nested_types(&schema)
957        );
958
959        // select b, c
960        let mask = ProjectionMask::leaves(&schema, [1, 2]);
961        assert_eq!(Some(mask.clone()), mask.without_nested_types(&schema));
962    }
963
964    #[test]
965    fn test_projection_mask_without_nested_nested() {
966        // Schema with nested types (structs)
967        let schema = parse_schema(
968            "
969            message test_schema {
970                OPTIONAL INT32 a;
971                OPTIONAL group b {
972                    REQUIRED INT32 b1;
973                    OPTIONAL INT64 b2;
974                }
975                OPTIONAL group c (LIST) {
976                    REPEATED group list {
977                        OPTIONAL INT32 element;
978                    }
979                }
980                REQUIRED DOUBLE d;
981            }
982            ",
983        );
984
985        // all leaves --> a, d
986        let mask = ProjectionMask::all();
987        assert_eq!(
988            Some(ProjectionMask::leaves(&schema, [0, 4])),
989            mask.without_nested_types(&schema)
990        );
991
992        // b1 --> empty (it is nested)
993        let mask = ProjectionMask::leaves(&schema, [1]);
994        assert_eq!(None, mask.without_nested_types(&schema));
995
996        // b2, d --> d
997        let mask = ProjectionMask::leaves(&schema, [1, 4]);
998        assert_eq!(
999            Some(ProjectionMask::leaves(&schema, [4])),
1000            mask.without_nested_types(&schema)
1001        );
1002
1003        // element --> empty (it is nested)
1004        let mask = ProjectionMask::leaves(&schema, [3]);
1005        assert_eq!(None, mask.without_nested_types(&schema));
1006    }
1007
1008    #[test]
1009    fn test_projection_mask_without_nested_map_only() {
1010        // Example from https://github.com/apache/parquet-format/blob/master/LogicalTypes.md
1011        let schema = parse_schema(
1012            "
1013            message test_schema {
1014                required group my_map (MAP) {
1015                    repeated group key_value {
1016                        required binary key (STRING);
1017                        optional int32 value;
1018                    }
1019                }
1020            }
1021            ",
1022        );
1023
1024        let mask = ProjectionMask::all();
1025        assert_eq!(None, mask.without_nested_types(&schema));
1026
1027        // key --> empty (it is nested)
1028        let mask = ProjectionMask::leaves(&schema, [0]);
1029        assert_eq!(None, mask.without_nested_types(&schema));
1030
1031        // value --> empty (it is nested)
1032        let mask = ProjectionMask::leaves(&schema, [1]);
1033        assert_eq!(None, mask.without_nested_types(&schema));
1034    }
1035
1036    #[test]
1037    fn test_projection_mask_without_nested_map_with_non_nested() {
1038        // Example from https://github.com/apache/parquet-format/blob/master/LogicalTypes.md
1039        // with an additional non-nested field
1040        let schema = parse_schema(
1041            "
1042            message test_schema {
1043                REQUIRED INT32 a;
1044                required group my_map (MAP) {
1045                    repeated group key_value {
1046                        required binary key (STRING);
1047                        optional int32 value;
1048                    }
1049                }
1050                REQUIRED INT32 b;
1051            }
1052            ",
1053        );
1054
1055        // all leaves --> a, b which are the only non nested ones
1056        let mask = ProjectionMask::all();
1057        assert_eq!(
1058            Some(ProjectionMask::leaves(&schema, [0, 3])),
1059            mask.without_nested_types(&schema)
1060        );
1061
1062        // key, value, b --> b (the only non-nested one)
1063        let mask = ProjectionMask::leaves(&schema, [1, 2, 3]);
1064        assert_eq!(
1065            Some(ProjectionMask::leaves(&schema, [3])),
1066            mask.without_nested_types(&schema)
1067        );
1068
1069        // key, value --> NONE
1070        let mask = ProjectionMask::leaves(&schema, [1, 2]);
1071        assert_eq!(None, mask.without_nested_types(&schema));
1072    }
1073
1074    #[test]
1075    fn test_projection_mask_without_nested_deeply_nested() {
1076        // Map of Maps
1077        let schema = parse_schema(
1078            "
1079            message test_schema {
1080                OPTIONAL group a (MAP) {
1081                    REPEATED group key_value {
1082                        REQUIRED BYTE_ARRAY key (UTF8);
1083                        OPTIONAL group value (MAP) {
1084                            REPEATED group key_value {
1085                                REQUIRED INT32 key;
1086                                REQUIRED BOOLEAN value;
1087                            }
1088                        }
1089                    }
1090                }
1091                REQUIRED INT32 b;
1092                REQUIRED DOUBLE c;
1093            ",
1094        );
1095
1096        let mask = ProjectionMask::all();
1097        assert_eq!(
1098            Some(ProjectionMask::leaves(&schema, [3, 4])),
1099            mask.without_nested_types(&schema)
1100        );
1101
1102        // (first) key, c --> c (the only non-nested one)
1103        let mask = ProjectionMask::leaves(&schema, [0, 4]);
1104        assert_eq!(
1105            Some(ProjectionMask::leaves(&schema, [4])),
1106            mask.without_nested_types(&schema)
1107        );
1108
1109        // (second) key, value, b --> b (the only non-nested one)
1110        let mask = ProjectionMask::leaves(&schema, [1, 2, 3]);
1111        assert_eq!(
1112            Some(ProjectionMask::leaves(&schema, [3])),
1113            mask.without_nested_types(&schema)
1114        );
1115
1116        // key --> NONE (the only non-nested one)
1117        let mask = ProjectionMask::leaves(&schema, [0]);
1118        assert_eq!(None, mask.without_nested_types(&schema));
1119    }
1120
1121    #[test]
1122    fn test_projection_mask_without_nested_list() {
1123        // Example from https://github.com/apache/parquet-format/blob/master/LogicalTypes.md#lists
1124        let schema = parse_schema(
1125            "
1126            message test_schema {
1127                required group my_list (LIST) {
1128                    repeated group list {
1129                        optional binary element (STRING);
1130                    }
1131                }
1132                REQUIRED INT32 b;
1133            }
1134            ",
1135        );
1136
1137        let mask = ProjectionMask::all();
1138        assert_eq!(
1139            Some(ProjectionMask::leaves(&schema, [1])),
1140            mask.without_nested_types(&schema),
1141        );
1142
1143        // element --> empty (it is nested)
1144        let mask = ProjectionMask::leaves(&schema, [0]);
1145        assert_eq!(None, mask.without_nested_types(&schema));
1146
1147        // element, b --> b (it is nested)
1148        let mask = ProjectionMask::leaves(&schema, [0, 1]);
1149        assert_eq!(
1150            Some(ProjectionMask::leaves(&schema, [1])),
1151            mask.without_nested_types(&schema),
1152        );
1153    }
1154
1155    #[test]
1156    fn test_projection_mask_without_nested_single_leaf_struct() {
1157        // Regression: a single-leaf struct is still nested.
1158        let schema = parse_schema(
1159            "
1160            message test_schema {
1161                OPTIONAL group address {
1162                    REQUIRED BYTE_ARRAY street (UTF8);
1163                }
1164                REQUIRED INT32 id;
1165            }
1166            ",
1167        );
1168
1169        // street -> empty; root is a struct
1170        let mask = ProjectionMask::leaves(&schema, [0]);
1171        assert_eq!(None, mask.without_nested_types(&schema));
1172
1173        // street, id --> id only
1174        let mask = ProjectionMask::leaves(&schema, [0, 1]);
1175        assert_eq!(
1176            Some(ProjectionMask::leaves(&schema, [1])),
1177            mask.without_nested_types(&schema)
1178        );
1179
1180        // all --> id only
1181        let mask = ProjectionMask::all();
1182        assert_eq!(
1183            Some(ProjectionMask::leaves(&schema, [1])),
1184            mask.without_nested_types(&schema)
1185        );
1186    }
1187
1188    /// Converts a schema string into a `SchemaDescriptor`
1189    fn parse_schema(schema: &str) -> SchemaDescriptor {
1190        let parquet_group_type = parse_message_type(schema).unwrap();
1191        SchemaDescriptor::new(Arc::new(parquet_group_type))
1192    }
1193}