Skip to main content

parquet/arrow/
mod.rs

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