This is an automated email from the ASF dual-hosted git repository.

etseidl pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-rs.git


The following commit(s) were added to refs/heads/main by this push:
     new 0d4f469d9b Allow reading and writing more than 32k Parquet row groups 
(#10149)
0d4f469d9b is described below

commit 0d4f469d9b12b1dc838ca326b5b18eb9e46dec74
Author: Ed Seidl <[email protected]>
AuthorDate: Mon Aug 3 11:43:22 2026 -0700

    Allow reading and writing more than 32k Parquet row groups (#10149)
    
    # Which issue does this PR close?
    
    - Closes #10129.
    
    # Rationale for this change
    The parquet crate will error if more than 32767 row groups are present
    in the file. This is a limit imposed on write when encryption is in use,
    but there is no other limit on the number of row groups beyond that
    imposed by the Thrift compact protocol.
    
    # What changes are included in this PR?
    This changes the `ordinal` field of the `RowGroupMetaData` from an `i16`
    to `i32`. This allows reading up to the maximum number of row groups
    allowed by Thrift. On write, the `ordinal` on the RowGroup will not be
    written if more than 32k row groups are present.
    
    # Are these changes tested?
    Yes
    
    # Are there any user-facing changes?
    Yes, `RowGroupMetaData::ordinal` now returns `Option<i32>` and
    `RowGroupMetaDataBuilder::set_ordinal` takes `Option<i32>`.
---
 parquet/benches/metadata.rs                       |  2 +-
 parquet/src/arrow/array_reader/row_group_index.rs |  4 +-
 parquet/src/arrow/array_reader/row_number.rs      |  4 +-
 parquet/src/file/metadata/mod.rs                  |  6 +-
 parquet/src/file/metadata/thrift/mod.rs           | 21 ++++---
 parquet/src/file/writer.rs                        | 73 ++++++++++++++++++++---
 parquet/src/parquet_thrift.rs                     | 17 ++++++
 7 files changed, 101 insertions(+), 26 deletions(-)

diff --git a/parquet/benches/metadata.rs b/parquet/benches/metadata.rs
index b6e04580c1..24ac685131 100644
--- a/parquet/benches/metadata.rs
+++ b/parquet/benches/metadata.rs
@@ -124,7 +124,7 @@ fn encoded_meta(is_nullable: bool, has_lists: bool, 
write_path_in_schema: bool)
                 .set_column_metadata(columns)
                 .set_total_byte_size(rng.random_range(1..2000000000))
                 .set_num_rows(rng.random_range(1..10000000000))
-                .set_ordinal(i as i16)
+                .set_ordinal(i as i32)
                 .build()
                 .unwrap()
         })
diff --git a/parquet/src/arrow/array_reader/row_group_index.rs 
b/parquet/src/arrow/array_reader/row_group_index.rs
index b4c296c145..032f1931dc 100644
--- a/parquet/src/arrow/array_reader/row_group_index.rs
+++ b/parquet/src/arrow/array_reader/row_group_index.rs
@@ -90,7 +90,7 @@ impl RowGroupIndexReader {
         // general path: many row groups
         // builds a mapping from ordinal to row group index
         // this is O(n) where n is the total number of row groups in the file
-        let ordinal_to_index: HashMap<i16, i64> =
+        let ordinal_to_index: HashMap<i32, i64> =
             
HashMap::from_iter(parquet_metadata.row_groups().iter().enumerate().filter_map(
                 |(row_group_index, rg)| {
                     rg.ordinal()
@@ -215,7 +215,7 @@ mod tests {
         Arc::new(SchemaDescriptor::new(Arc::new(schema)))
     }
 
-    fn create_test_parquet_metadata(row_groups: Vec<(i16, i64)>) -> 
ParquetMetaData {
+    fn create_test_parquet_metadata(row_groups: Vec<(i32, i64)>) -> 
ParquetMetaData {
         let schema_descr = create_test_schema();
 
         let mut row_group_metas = vec![];
diff --git a/parquet/src/arrow/array_reader/row_number.rs 
b/parquet/src/arrow/array_reader/row_number.rs
index def88641a1..3ac55c0b0e 100644
--- a/parquet/src/arrow/array_reader/row_number.rs
+++ b/parquet/src/arrow/array_reader/row_number.rs
@@ -45,7 +45,7 @@ impl RowNumberReader {
     ) -> Result<Self> {
         // Pass 1: Build a map from ordinal to first_row_index
         // This is O(M) where M is the total number of row groups in the file
-        let mut ordinal_to_offset: HashMap<i16, i64> = HashMap::new();
+        let mut ordinal_to_offset: HashMap<i32, i64> = HashMap::new();
         let mut first_row_index: i64 = 0;
 
         for rg in parquet_metadata.row_groups() {
@@ -181,7 +181,7 @@ mod tests {
         Arc::new(SchemaDescriptor::new(Arc::new(schema)))
     }
 
-    fn create_test_parquet_metadata(row_groups: Vec<(i16, i64)>) -> 
ParquetMetaData {
+    fn create_test_parquet_metadata(row_groups: Vec<(i32, i64)>) -> 
ParquetMetaData {
         let schema_descr = create_test_schema();
 
         let mut row_group_metas = vec![];
diff --git a/parquet/src/file/metadata/mod.rs b/parquet/src/file/metadata/mod.rs
index cf0eb6d27f..874449d992 100644
--- a/parquet/src/file/metadata/mod.rs
+++ b/parquet/src/file/metadata/mod.rs
@@ -636,7 +636,7 @@ pub struct RowGroupMetaData {
     /// We can't infer from file offset of first column since there may empty 
columns in row group.
     file_offset: Option<i64>,
     /// Ordinal position of this row group in file
-    ordinal: Option<i16>,
+    ordinal: Option<i32>,
 }
 
 impl RowGroupMetaData {
@@ -700,7 +700,7 @@ impl RowGroupMetaData {
     /// For example if this is the first row group in the file, this will 
return 0.
     /// If this is the second row group in the file, this will return 1.
     #[inline(always)]
-    pub fn ordinal(&self) -> Option<i16> {
+    pub fn ordinal(&self) -> Option<i32> {
         self.ordinal
     }
 
@@ -773,7 +773,7 @@ impl RowGroupMetaDataBuilder {
     }
 
     /// Sets ordinal for this row group.
-    pub fn set_ordinal(mut self, value: i16) -> Self {
+    pub fn set_ordinal(mut self, value: i32) -> Self {
         self.0.ordinal = Some(value);
         self
     }
diff --git a/parquet/src/file/metadata/thrift/mod.rs 
b/parquet/src/file/metadata/thrift/mod.rs
index 29b7dffc07..fdf0bf786a 100644
--- a/parquet/src/file/metadata/thrift/mod.rs
+++ b/parquet/src/file/metadata/thrift/mod.rs
@@ -712,7 +712,7 @@ fn read_row_group(
             }
             // 6: we don't expose total_compressed_size
             7 => {
-                row_group.ordinal = Some(i16::read_thrift(&mut *prot)?);
+                row_group.ordinal = Some(i16::read_thrift(&mut *prot)? as i32);
             }
             _ => {
                 prot.skip(field_ident.field_type)?;
@@ -838,11 +838,6 @@ pub(crate) fn parquet_metadata_from_bytes(
                 // Read row groups and handle ordinal assignment
                 let mut assigner = OrdinalAssigner::new();
                 for ordinal in 0..list_ident.size {
-                    let ordinal: i16 = ordinal.try_into().map_err(|_| {
-                        ParquetError::General(format!(
-                            "Row group ordinal {ordinal} exceeds i16 max 
value",
-                        ))
-                    })?;
                     let rg = read_row_group(&mut prot, schema_descr, options)?;
                     rg_vec.push(assigner.ensure(ordinal, rg)?);
                 }
@@ -962,7 +957,7 @@ impl OrdinalAssigner {
     ///    groups must also not have ordinals.
     fn ensure(
         &mut self,
-        actual_ordinal: i16,
+        actual_ordinal: i32,
         mut rg: RowGroupMetaData,
     ) -> Result<RowGroupMetaData> {
         let rg_has_ordinal = rg.ordinal.is_some();
@@ -1461,6 +1456,8 @@ impl<'a> WriteThrift for FileMeta<'a> {
     #[allow(unused_assignments)]
     fn write_thrift<W: Write>(&self, writer: &mut 
ThriftCompactOutputProtocol<W>) -> Result<()> {
         writer.set_write_path_in_schema(self.write_path_in_schema);
+        // only write ordinal if all values will fit in an i16
+        writer.set_write_row_group_ordinal(self.row_groups.len() <= i16::MAX 
as usize);
 
         self.file_metadata
             .version
@@ -1621,8 +1618,14 @@ impl WriteThrift for RowGroupMetaData {
         last_field_id = self
             .compressed_size()
             .write_thrift_field(writer, 6, last_field_id)?;
-        if let Some(ordinal) = self.ordinal() {
-            ordinal.write_thrift_field(writer, 7, last_field_id)?;
+
+        // write ordinal if it will fit in an i16
+        if writer.write_row_group_ordinal() {
+            if let Some(ordinal) = self.ordinal() {
+                if let Ok(ordinal) = i16::try_from(ordinal) {
+                    ordinal.write_thrift_field(writer, 7, last_field_id)?;
+                }
+            }
         }
         writer.write_struct_end()
     }
diff --git a/parquet/src/file/writer.rs b/parquet/src/file/writer.rs
index cb94483200..dbd1a48f70 100644
--- a/parquet/src/file/writer.rs
+++ b/parquet/src/file/writer.rs
@@ -228,8 +228,8 @@ impl<W: Write + Send> SerializedFileWriter<W> {
 
     /// Creates new row group from this file writer.
     ///
-    /// Note: Parquet files are limited to at most 2^15 row groups in a file; 
and row groups must
-    /// be written sequentially.
+    /// Note: Parquet files are limited to at most 2^31 row groups in a file. 
If encryption is
+    /// enabled, this is reduced to 2^15, and row groups must be written 
sequentially.
     ///
     /// Every time the next row group is requested, the previous row group must
     /// be finalised and closed using the [`SerializedRowGroupWriter::close`]
@@ -238,14 +238,25 @@ impl<W: Write + Send> SerializedFileWriter<W> {
         self.assert_previous_writer_closed()?;
         let ordinal = self.row_group_index;
 
-        let ordinal: i16 = ordinal.try_into().map_err(|_| {
+        // Thrift cannot encode lists with more than i32::MAX elements
+        let ordinal: i32 = ordinal.try_into().map_err(|_| {
             ParquetError::General(format!(
                 "Parquet does not support more than {} row groups per file 
(currently: {})",
-                i16::MAX,
+                i32::MAX,
                 ordinal
             ))
         })?;
 
+        // If encryption is enabled, the max is 32767
+        #[cfg(feature = "encryption")]
+        if self.file_encryptor.is_some() && ordinal > i16::MAX as i32 {
+            return Err(ParquetError::General(format!(
+                "Parquet with encryption does not support more than {} row 
groups per file (currently: {})",
+                i16::MAX,
+                ordinal
+            )));
+        }
+
         self.row_group_index = self
             .row_group_index
             .checked_add(1)
@@ -472,7 +483,7 @@ fn write_bloom_filters<W: Write + Send>(
     // iter each column
     // write bloom filter to the file
 
-    let row_group_idx: u16 = row_group
+    let row_group_idx: u32 = row_group
         .ordinal()
         .expect("Missing row group ordinal")
         .try_into()
@@ -525,7 +536,7 @@ pub struct SerializedRowGroupWriter<'a, W: Write> {
     bloom_filters: Vec<Option<Sbbf>>,
     column_indexes: Vec<Option<ColumnIndexMetaData>>,
     offset_indexes: Vec<Option<OffsetIndexMetaData>>,
-    row_group_index: i16,
+    row_group_index: i32,
     file_offset: i64,
     on_close: Option<OnCloseRowGroup<'a, W>>,
     #[cfg(feature = "encryption")]
@@ -545,7 +556,7 @@ impl<'a, W: Write + Send> SerializedRowGroupWriter<'a, W> {
         schema_descr: SchemaDescPtr,
         properties: WriterPropertiesPtr,
         buf: &'a mut TrackedWrite<W>,
-        row_group_index: i16,
+        row_group_index: i32,
         on_close: Option<OnCloseRowGroup<'a, W>>,
     ) -> Self {
         let num_columns = schema_descr.num_columns();
@@ -1813,7 +1824,7 @@ mod tests {
             let last_group = row_group_writer.close().unwrap();
             let flushed = file_writer.flushed_row_groups();
             assert_eq!(flushed.len(), idx + 1);
-            assert_eq!(Some(idx as i16), last_group.ordinal());
+            assert_eq!(Some(idx as i32), last_group.ordinal());
             assert_eq!(Some(row_group_file_offset as i64), 
last_group.file_offset());
             assert_eq!(&flushed[idx], last_group.as_ref());
         }
@@ -2292,6 +2303,7 @@ mod tests {
         assert_eq!(page_sizes[0], unenc_size);
     }
 
+    #[cfg(feature = "encryption")]
     #[test]
     fn test_too_many_rowgroups() {
         let message_type = "
@@ -2301,10 +2313,17 @@ mod tests {
         ";
         let schema = Arc::new(parse_message_type(message_type).unwrap());
         let file: File = tempfile::tempfile().unwrap();
+
+        const AES_128_FOOTER_KEY: &[u8; 16] = b"0123456789012345"; // 128bit/16
+        let footer_key = AES_128_FOOTER_KEY;
+        let file_encryption_properties = 
FileEncryptionProperties::builder(footer_key.to_vec())
+            .build()
+            .unwrap();
         let props = Arc::new(
             WriterProperties::builder()
                 .set_statistics_enabled(EnabledStatistics::None)
                 .set_max_row_group_row_count(Some(1))
+                .with_file_encryption_properties(file_encryption_properties)
                 .build(),
         );
         let mut writer = SerializedFileWriter::new(&file, schema, 
props).unwrap();
@@ -2322,7 +2341,7 @@ mod tests {
                     assert_eq!(i, 0x8000);
                     assert_eq!(
                         e.to_string(),
-                        "Parquet error: Parquet does not support more than 
32767 row groups per file (currently: 32768)"
+                        "Parquet error: Parquet with encryption does not 
support more than 32767 row groups per file (currently: 32768)"
                     );
                 }
             }
@@ -2330,6 +2349,42 @@ mod tests {
         writer.close().unwrap();
     }
 
+    #[test]
+    fn test_32k_rowgroups() {
+        let message_type = "
+            message test_schema {
+                REQUIRED BYTE_ARRAY a (UTF8);
+            }
+        ";
+        let schema = Arc::new(parse_message_type(message_type).unwrap());
+        let file: File = tempfile::tempfile().unwrap();
+        let props = Arc::new(
+            WriterProperties::builder()
+                .set_statistics_enabled(EnabledStatistics::None)
+                .set_max_row_group_row_count(Some(1))
+                .build(),
+        );
+        let mut writer = SerializedFileWriter::new(&file, schema, 
props).unwrap();
+
+        // Create 32k + 1 empty rowgroups. No row group ordinals should be 
written (but we can't
+        // test for that).
+        for _ in 0..0x8001 {
+            let mut row_group_writer = writer.next_row_group().unwrap();
+            let col_writer = row_group_writer.next_column().unwrap().unwrap();
+            col_writer.close().unwrap();
+            row_group_writer.close().unwrap();
+        }
+        writer.close().unwrap();
+
+        // Parse the written metadata and check that ordinals were replaced.
+        let reader = SerializedFileReader::new(file).unwrap();
+        let metadata = reader.metadata();
+
+        for (i, rg) in metadata.row_groups().iter().enumerate() {
+            assert_eq!(i as i32, rg.ordinal().unwrap());
+        }
+    }
+
     #[test]
     fn test_size_statistics_with_repetition_and_nulls() {
         let message_type = "
diff --git a/parquet/src/parquet_thrift.rs b/parquet/src/parquet_thrift.rs
index cc6390b392..218194e0da 100644
--- a/parquet/src/parquet_thrift.rs
+++ b/parquet/src/parquet_thrift.rs
@@ -754,6 +754,7 @@ pub(crate) fn validate_list_type(expected: ElementType, 
got: &ListIdentifier) ->
 pub(crate) struct ThriftCompactOutputProtocol<W: Write> {
     writer: W,
     write_path_in_schema: bool,
+    write_rg_ordinal: bool,
 }
 
 impl<W: Write> ThriftCompactOutputProtocol<W> {
@@ -762,6 +763,7 @@ impl<W: Write> ThriftCompactOutputProtocol<W> {
         Self {
             writer,
             write_path_in_schema: true,
+            write_rg_ordinal: true,
         }
     }
 
@@ -778,6 +780,21 @@ impl<W: Write> ThriftCompactOutputProtocol<W> {
         self.write_path_in_schema
     }
 
+    /// Control the writing of the `ordinal` element of the `RowGroup` struct.
+    ///
+    /// The Thrift `ordinal` field on the `RowGroup` struct is `i16`, but the
+    /// Thrift compact protocol allows for up to 2^31 elements in a list. If
+    /// more than 2^15 row groups are to be written, this can be set to `false`
+    /// to prevent writing the ordinal for some row groups but not others.
+    pub(crate) fn set_write_row_group_ordinal(&mut self, val: bool) {
+        self.write_rg_ordinal = val;
+    }
+
+    /// Indicate whether or not to emit `ordinal`.
+    pub(crate) fn write_row_group_ordinal(&self) -> bool {
+        self.write_rg_ordinal
+    }
+
     /// Write a single byte to the output stream.
     fn write_byte(&mut self, b: u8) -> Result<()> {
         self.writer.write_all(&[b])?;

Reply via email to