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

JingsongLi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/paimon-rust.git


The following commit(s) were added to refs/heads/main by this push:
     new 107b3a60 [core] Align Parquet writer row group and page sizing options 
with Java (#1072)
107b3a60 is described below

commit 107b3a60bfeb315119f542d141e3bc7fb85fe444
Author: Dapeng Sun(孙大鹏) <[email protected]>
AuthorDate: Thu Oct 8 20:58:32 2026 +0800

    [core] Align Parquet writer row group and page sizing options with Java 
(#1072)
---
 crates/paimon/src/arrow/format/parquet.rs | 319 +++++++++++++++++++++++++++++-
 crates/paimon/src/table/table_write.rs    | 207 +++++++++++++++++++
 docs/src/getting-started.md               |  16 ++
 3 files changed, 534 insertions(+), 8 deletions(-)

diff --git a/crates/paimon/src/arrow/format/parquet.rs 
b/crates/paimon/src/arrow/format/parquet.rs
index 48132c27..cbc434a4 100644
--- a/crates/paimon/src/arrow/format/parquet.rs
+++ b/crates/paimon/src/arrow/format/parquet.rs
@@ -288,6 +288,7 @@ pub(crate) struct ParquetFormatWriter {
     write_fields: Option<Vec<DataField>>,
     stats_modes: Option<Vec<MetadataStatsMode>>,
     stats_dense_store: bool,
+    max_row_group_bytes: Option<usize>,
 }
 
 pub(crate) struct ParquetWriterFactory {
@@ -370,6 +371,7 @@ impl ParquetFormatWriter {
         let core_options = CoreOptions::new(format_options);
         let page_index_enabled = 
core_options.parquet_write_page_index_enabled()?;
         let props = parquet_writer_properties(codec, page_index_enabled, 
format_options)?;
+        let max_row_group_bytes = props.max_row_group_bytes();
         let async_write = output.async_writer().await?;
         let input_schema = schema;
         let schema = timestamp_millis_schema(&input_schema);
@@ -384,8 +386,19 @@ impl ParquetFormatWriter {
             write_fields: write_fields.map(|fields| fields.to_vec()),
             stats_modes,
             stats_dense_store: core_options.metadata_stats_dense_store(),
+            max_row_group_bytes,
         })
     }
+
+    async fn write_physical(&mut self, batch: &RecordBatch) -> 
crate::Result<()> {
+        self.inner
+            .write(batch)
+            .await
+            .map_err(|e| crate::Error::DataInvalid {
+                message: format!("Failed to write parquet batch: {e}"),
+                source: None,
+            })
+    }
 }
 
 fn create_parquet_arrow_writer(
@@ -445,7 +458,134 @@ fn parquet_writer_properties(
             );
         }
     }
-    Ok(props.build())
+    Ok(parquet_sizing_properties(props, options)?.build())
+}
+
+/// Java's parquet writer sizing keys; `file.block-size` overrides 
`parquet.block.size` as in
+/// Java's `ParquetFileFormat`.
+fn parquet_sizing_properties(
+    mut props: parquet::file::properties::WriterPropertiesBuilder,
+    options: &HashMap<String, String>,
+) -> crate::Result<parquet::file::properties::WriterPropertiesBuilder> {
+    const BLOCK_SIZE: &str = "parquet.block.size";
+    const PAGE_SIZE: &str = "parquet.page.size";
+    const DICTIONARY_PAGE_SIZE: &str = "parquet.dictionary.page.size";
+    const PAGE_ROW_COUNT_LIMIT: &str = "parquet.page.row.count.limit";
+    const PAGE_SIZE_ROW_CHECK_MIN: &str = "parquet.page.size.row.check.min";
+    let positive = |key: &str| -> crate::Result<Option<usize>> {
+        options
+            .get(key)
+            .map(|value| {
+                value
+                    .trim()
+                    .parse::<usize>()
+                    .ok()
+                    .filter(|parsed| *parsed > 0)
+                    .ok_or_else(|| crate::Error::ConfigInvalid {
+                        message: format!("Option '{key}' must be a positive 
integer, got: {value}"),
+                    })
+            })
+            .transpose()
+    };
+    let file_block_size = CoreOptions::new(options)
+        .file_block_size()?
+        .map(|bytes| {
+            usize::try_from(bytes)
+                .ok()
+                .filter(|bytes| *bytes > 0)
+                .ok_or_else(|| crate::Error::ConfigInvalid {
+                    message: format!("Option 'file.block-size' must be 
positive, got: {bytes}"),
+                })
+        })
+        .transpose()?;
+    let block_size = match file_block_size {
+        Some(bytes) => Some(bytes),
+        None => positive(BLOCK_SIZE)?,
+    };
+    if let Some(bytes) = block_size {
+        props = props.set_max_row_group_bytes(Some(bytes));
+    }
+    if let Some(bytes) = positive(PAGE_SIZE)? {
+        props = props.set_data_page_size_limit(bytes);
+    }
+    if let Some(bytes) = positive(DICTIONARY_PAGE_SIZE)? {
+        props = props.set_dictionary_page_size_limit(bytes);
+    }
+    // parquet-rs checks page limits between write batches, so the batch 
bounds the check interval.
+    let row_check = positive(PAGE_SIZE_ROW_CHECK_MIN)?;
+    let page_rows = positive(PAGE_ROW_COUNT_LIMIT)?;
+    if let Some(rows) = page_rows {
+        props = props.set_data_page_row_count_limit(rows);
+    }
+    if row_check.is_some() || page_rows.is_some() {
+        let limit =
+            
page_rows.unwrap_or(parquet::file::properties::DEFAULT_DATA_PAGE_ROW_COUNT_LIMIT);
+        let batch = 
row_check.unwrap_or(parquet::file::properties::DEFAULT_WRITE_BATCH_SIZE);
+        props = props.set_write_batch_size(batch.min(limit));
+    }
+    Ok(props)
+}
+
+/// Bytes the rows of a (possibly sliced) batch occupy; unlike 
`get_array_memory_size`, list
+/// values and dictionaries outside the slice are not counted.
+fn batch_slice_bytes(batch: &RecordBatch) -> usize {
+    batch
+        .columns()
+        .iter()
+        .map(|column| array_slice_bytes(column.as_ref()))
+        .sum()
+}
+
+fn array_slice_bytes(array: &dyn arrow_array::Array) -> usize {
+    use arrow_array::cast::AsArray;
+    use arrow_array::{Array, ArrayRef};
+    use arrow_schema::DataType as ArrowDataType;
+    let nulls = array.nulls().map_or(0, |nulls| nulls.len().div_ceil(8));
+    let list_bytes = |values: &ArrayRef, start: usize, end: usize, 
offset_width: usize| {
+        offset_width * (array.len() + 1)
+            + nulls
+            + array_slice_bytes(values.slice(start, end - start).as_ref())
+    };
+    match array.data_type() {
+        ArrowDataType::List(_) => {
+            let list = array.as_list::<i32>();
+            let offsets = list.value_offsets();
+            let (start, end) = (offsets[0] as usize, offsets[list.len()] as 
usize);
+            list_bytes(list.values(), start, end, 4)
+        }
+        ArrowDataType::LargeList(_) => {
+            let list = array.as_list::<i64>();
+            let offsets = list.value_offsets();
+            let (start, end) = (offsets[0] as usize, offsets[list.len()] as 
usize);
+            list_bytes(list.values(), start, end, 8)
+        }
+        ArrowDataType::Map(_, _) => {
+            let map = array.as_map();
+            let offsets = map.value_offsets();
+            let (start, end) = (offsets[0] as usize, offsets[map.len()] as 
usize);
+            let entries: ArrayRef = Arc::new(map.entries().clone());
+            list_bytes(&entries, start, end, 4)
+        }
+        ArrowDataType::Struct(_) => {
+            nulls
+                + array
+                    .as_struct()
+                    .columns()
+                    .iter()
+                    .map(|column| array_slice_bytes(column.as_ref()))
+                    .sum::<usize>()
+        }
+        ArrowDataType::Dictionary(_, _) => {
+            let dictionary = array.as_any_dictionary();
+            let values = dictionary.values();
+            let value_bytes = array_slice_bytes(values.as_ref());
+            array_slice_bytes(dictionary.keys()) + value_bytes * array.len() / 
values.len().max(1)
+        }
+        _ => array
+            .to_data()
+            .get_slice_memory_size()
+            .unwrap_or_else(|_| array.get_array_memory_size()),
+    }
 }
 
 /// Every `file.compression` value a parquet write accepts.
@@ -524,13 +664,29 @@ impl FormatFileWriter for ParquetFormatWriter {
             physical_batch = cast_record_batch_to_schema(batch, &self.schema)?;
             &physical_batch
         };
-        self.inner
-            .write(batch)
-            .await
-            .map_err(|e| crate::Error::DataInvalid {
-                message: format!("Failed to write parquet batch: {e}"),
-                source: None,
-            })
+        let Some(limit) = self.max_row_group_bytes else {
+            return self.write_physical(batch).await;
+        };
+        // parquet-rs splits by bytes only once a row group holds rows, so 
write slices of at
+        // most a quarter of the limit, measured on their own rows, and close 
the group here.
+        let target = (limit / 4).max(1);
+        let rows = batch.num_rows();
+        let guess = (target / 
batch_slice_bytes(batch).div_ceil(rows.max(1)).max(1)).max(1);
+        let mut offset = 0;
+        while offset < rows {
+            let mut len = guess.min(rows - offset);
+            let mut slice = batch.slice(offset, len);
+            while len > 1 && batch_slice_bytes(&slice) > target {
+                len /= 2;
+                slice = batch.slice(offset, len);
+            }
+            self.write_physical(&slice).await?;
+            offset += len;
+            if self.inner.in_progress_size() >= limit {
+                self.flush().await?;
+            }
+        }
+        Ok(())
     }
 
     fn num_bytes(&self) -> usize {
@@ -4176,6 +4332,7 @@ mod tests {
                 write_fields: None,
                 stats_modes: None,
                 stats_dense_store: false,
+                max_row_group_bytes: None,
             }),
             Some(&resources),
         );
@@ -4339,6 +4496,152 @@ mod tests {
         }
     }
 
+    #[test]
+    fn test_parquet_writer_sizing_options() {
+        let defaults =
+            parquet_writer_properties(Compression::UNCOMPRESSED, true, 
&HashMap::new()).unwrap();
+        assert_eq!(defaults.max_row_group_bytes(), None);
+        assert_eq!(
+            defaults.write_batch_size(),
+            parquet::file::properties::DEFAULT_WRITE_BATCH_SIZE
+        );
+        let mut options = HashMap::from([
+            ("parquet.block.size".to_owned(), "1048576".to_owned()),
+            ("parquet.page.size".to_owned(), "65536".to_owned()),
+            (
+                "parquet.dictionary.page.size".to_owned(),
+                "32768".to_owned(),
+            ),
+            ("parquet.page.row.count.limit".to_owned(), "500".to_owned()),
+        ]);
+        let props = parquet_writer_properties(Compression::UNCOMPRESSED, true, 
&options).unwrap();
+        assert_eq!(props.max_row_group_bytes(), Some(1_048_576));
+        assert_eq!(props.data_page_size_limit(), 65_536);
+        assert_eq!(props.dictionary_page_size_limit(), 32_768);
+        assert_eq!(props.data_page_row_count_limit(), 500);
+        assert_eq!(props.write_batch_size(), 500);
+
+        options.insert("file.block-size".to_owned(), "2 mb".to_owned());
+        options.insert("parquet.block.size".to_owned(), "128mb".to_owned());
+        options.insert(
+            "parquet.page.size.row.check.min".to_owned(),
+            "100".to_owned(),
+        );
+        let props = parquet_writer_properties(Compression::UNCOMPRESSED, true, 
&options).unwrap();
+        assert_eq!(props.max_row_group_bytes(), Some(2 * 1024 * 1024));
+        assert_eq!(props.write_batch_size(), 100);
+
+        // Without an explicit page row limit, the default one still bounds 
the check interval.
+        let options = HashMap::from([(
+            "parquet.page.size.row.check.min".to_owned(),
+            "65536".to_owned(),
+        )]);
+        let props = parquet_writer_properties(Compression::UNCOMPRESSED, true, 
&options).unwrap();
+        assert_eq!(
+            props.data_page_row_count_limit(),
+            parquet::file::properties::DEFAULT_DATA_PAGE_ROW_COUNT_LIMIT
+        );
+        assert_eq!(
+            props.write_batch_size(),
+            parquet::file::properties::DEFAULT_DATA_PAGE_ROW_COUNT_LIMIT
+        );
+    }
+
+    #[tokio::test]
+    async fn test_parquet_writer_invalid_sizing_options_create_no_file() {
+        for (key, value) in [
+            ("parquet.block.size", "0"),
+            ("parquet.block.size", "128mb"),
+            ("parquet.page.size", "-1"),
+            ("parquet.dictionary.page.size", "x"),
+            ("parquet.page.row.count.limit", "0"),
+            ("parquet.page.size.row.check.min", ""),
+            ("file.block-size", "0 b"),
+            ("file.block-size", "invalid"),
+        ] {
+            let file_io = FileIOBuilder::new("memory").build().unwrap();
+            let path = "memory:/invalid_sizing.parquet";
+            let options = HashMap::from([(key.to_owned(), value.to_owned())]);
+            let result = ParquetFormatWriter::new(
+                &file_io.new_output(path).unwrap(),
+                writer_arrow_schema(),
+                "zstd",
+                1,
+                None,
+                &options,
+            )
+            .await;
+            assert!(
+                matches!(result, Err(crate::Error::ConfigInvalid { .. })),
+                "{key}={value}"
+            );
+            assert!(!file_io.exists(path).await.unwrap());
+        }
+    }
+
+    #[tokio::test]
+    async fn test_parquet_writer_sizing_options_shape_the_file() {
+        use parquet::arrow::arrow_reader::{ArrowReaderOptions, 
ParquetRecordBatchReaderBuilder};
+
+        let file_io = FileIOBuilder::new("memory").build().unwrap();
+        let path = "memory:/sizing_options.parquet";
+        let options = HashMap::from([
+            ("parquet.enable.dictionary".to_owned(), "false".to_owned()),
+            ("parquet.block.size".to_owned(), "16384".to_owned()),
+            ("parquet.page.row.count.limit".to_owned(), "100".to_owned()),
+        ]);
+        let schema = writer_arrow_schema();
+        let mut writer = ParquetFormatWriter::new(
+            &file_io.new_output(path).unwrap(),
+            schema.clone(),
+            "none",
+            1,
+            None,
+            &options,
+        )
+        .await
+        .unwrap();
+        for chunk in 0..40 {
+            let ids = (chunk * 100..(chunk + 1) * 100).collect::<Vec<i32>>();
+            let values = ids.iter().map(|id| id.wrapping_mul(7919)).collect();
+            writer
+                .write(&writer_test_batch(&schema, ids, values))
+                .await
+                .unwrap();
+        }
+        Box::new(writer).close().await.unwrap();
+
+        let bytes = file_io.new_input(path).unwrap().read().await.unwrap();
+        let reader = ParquetRecordBatchReaderBuilder::try_new_with_options(
+            bytes,
+            
ArrowReaderOptions::new().with_page_index_policy(PageIndexPolicy::Required),
+        )
+        .unwrap();
+        let metadata = reader.metadata().clone();
+        assert!(metadata.num_row_groups() > 1);
+        let offsets = metadata.offset_index().expect("page index");
+        for (row_group, columns) in metadata.row_groups().iter().zip(offsets) {
+            assert!(row_group.compressed_size() <= 2 * 16384);
+            for column in columns {
+                let firsts = column
+                    .page_locations()
+                    .iter()
+                    .map(|page| page.first_row_index)
+                    .chain(std::iter::once(row_group.num_rows()))
+                    .collect::<Vec<_>>();
+                // parquet-rs checks the limit between write batches, so a 
split batch can add one batch.
+                assert!(firsts.windows(2).all(|pair| pair[1] - pair[0] < 200));
+                assert!(firsts.len() > (row_group.num_rows() / 100) as usize);
+            }
+        }
+        let rows = reader
+            .build()
+            .unwrap()
+            .map(|batch| batch.unwrap().num_rows())
+            .sum::<usize>();
+        assert_eq!(rows, 4000);
+    }
+
     #[tokio::test]
     async fn test_parquet_writer_write_and_close() {
         let file_io = FileIOBuilder::new("memory").build().unwrap();
diff --git a/crates/paimon/src/table/table_write.rs 
b/crates/paimon/src/table/table_write.rs
index 4e8032be..59ed4a4c 100644
--- a/crates/paimon/src/table/table_write.rs
+++ b/crates/paimon/src/table/table_write.rs
@@ -2301,6 +2301,213 @@ pub(in crate::table) mod tests {
         assert_eq!(snapshot.total_record_count(), Some(3));
     }
 
+    fn sizing_table(
+        file_io: &FileIO,
+        table_path: &str,
+        primary_key: bool,
+        options: &[(&str, &str)],
+    ) -> Table {
+        let mut schema = Schema::builder()
+            .column("id", DataType::Int(IntType::new()))
+            .column("name", DataType::VarChar(VarCharType::string_type()))
+            .option("file.format", "parquet")
+            .option("file.compression", "none")
+            .option("parquet.enable.dictionary", "false");
+        for (key, value) in options {
+            schema = schema.option(*key, *value);
+        }
+        if primary_key {
+            schema = schema.primary_key(["id"]).option("bucket", "1");
+        }
+        Table::new(
+            file_io.clone(),
+            Identifier::new("default", "test_table"),
+            table_path.to_string(),
+            TableSchema::new(0, &schema.build().unwrap()),
+            None,
+        )
+    }
+
+    fn sizing_batch(names: Vec<String>) -> RecordBatch {
+        let schema = Arc::new(ArrowSchema::new(vec![
+            ArrowField::new("id", ArrowDataType::Int32, false),
+            ArrowField::new("name", ArrowDataType::Utf8, false),
+        ]));
+        let ids = (0..names.len() as i32).collect::<Vec<_>>();
+        RecordBatch::try_new(
+            schema,
+            vec![
+                Arc::new(Int32Array::from(ids)),
+                Arc::new(StringArray::from(names)),
+            ],
+        )
+        .unwrap()
+    }
+
+    /// Writes one batch, returns the footers (with page indexes) of the new 
files, then
+    /// commits and checks that every row reads back.
+    async fn write_sizing_batch(
+        file_io: &FileIO,
+        table: &Table,
+        batch: &RecordBatch,
+    ) -> Vec<(String, parquet::file::metadata::ParquetMetaData)> {
+        use parquet::arrow::arrow_reader::{ArrowReaderOptions, 
ParquetRecordBatchReaderBuilder};
+        use parquet::file::metadata::PageIndexPolicy;
+
+        let mut table_write = TableWrite::new(table, 
"test-user".to_string()).unwrap();
+        table_write.write_arrow_batch(batch).await.unwrap();
+        let messages = table_write.prepare_commit().await.unwrap();
+        let mut footers = Vec::new();
+        for message in &messages {
+            for file in &message.new_files {
+                let path = format!(
+                    "{}/{}/{}",
+                    table.location(),
+                    bucket_dir_name(message.bucket),
+                    file.file_name
+                );
+                let bytes = 
file_io.new_input(&path).unwrap().read().await.unwrap();
+                let metadata = 
ParquetRecordBatchReaderBuilder::try_new_with_options(
+                    bytes,
+                    
ArrowReaderOptions::new().with_page_index_policy(PageIndexPolicy::Required),
+                )
+                .unwrap()
+                .metadata()
+                .as_ref()
+                .clone();
+                footers.push((path, metadata));
+            }
+        }
+        TableCommit::new(table.clone(), "test-user".to_string())
+            .commit(messages)
+            .await
+            .unwrap();
+        let read_builder = table.new_read_builder();
+        let plan = read_builder.new_scan().plan().await.unwrap();
+        let batches: Vec<RecordBatch> = futures::TryStreamExt::try_collect(
+            read_builder
+                .new_read()
+                .unwrap()
+                .to_arrow(plan.splits())
+                .unwrap(),
+        )
+        .await
+        .unwrap();
+        let mut read_ids = batches
+            .iter()
+            .flat_map(|batch| {
+                let ids = batch
+                    .column_by_name("id")
+                    .unwrap()
+                    .as_any()
+                    .downcast_ref::<Int32Array>()
+                    .unwrap();
+                ids.values().to_vec()
+            })
+            .collect::<Vec<_>>();
+        read_ids.sort_unstable();
+        assert_eq!(read_ids, (0..batch.num_rows() as i32).collect::<Vec<_>>());
+        footers
+    }
+
+    #[tokio::test]
+    async fn test_parquet_block_size_bounds_row_groups_of_one_large_batch() {
+        for primary_key in [false, true] {
+            let file_io = test_file_io();
+            let table_path = 
format!("memory:/test_parquet_block_size_{primary_key}");
+            setup_dirs(&file_io, &table_path).await;
+            // parquet.block.size is shadowed by file.block-size as in Java, 
so it is never parsed.
+            let table = sizing_table(
+                &file_io,
+                &table_path,
+                primary_key,
+                &[
+                    ("file.block-size", "16 kb"),
+                    ("parquet.block.size", "128mb"),
+                ],
+            );
+            let names = (0..65_536).map(|id| format!("{:08}", id * 
7919)).collect();
+            for (path, metadata) in write_sizing_batch(&file_io, &table, 
&sizing_batch(names)).await
+            {
+                assert!(metadata.num_row_groups() > 1, "{path}");
+                for row_group in metadata.row_groups() {
+                    assert!(row_group.compressed_size() <= 2 * 16 * 1024, 
"{path}");
+                }
+            }
+        }
+    }
+
+    #[tokio::test]
+    async fn test_parquet_block_size_bounds_row_groups_of_skewed_rows() {
+        const LIMIT: i64 = 1024 * 1024;
+        for primary_key in [false, true] {
+            let file_io = test_file_io();
+            let table_path = 
format!("memory:/test_parquet_block_size_skewed_{primary_key}");
+            setup_dirs(&file_io, &table_path).await;
+            let table = sizing_table(
+                &file_io,
+                &table_path,
+                primary_key,
+                &[("file.block-size", "1 mb")],
+            );
+            // The first rows hold nearly all bytes, so the batch average says 
little about them.
+            let names = (0..65_536)
+                .map(|id| {
+                    if id < 1_000 {
+                        "x".repeat(8 * 1024)
+                    } else {
+                        "y".to_string()
+                    }
+                })
+                .collect();
+            for (path, metadata) in write_sizing_batch(&file_io, &table, 
&sizing_batch(names)).await
+            {
+                assert!(metadata.num_row_groups() > 1, "{path}");
+                for row_group in metadata.row_groups() {
+                    assert!(
+                        row_group.compressed_size() <= LIMIT * 3 / 2,
+                        "{path}: {}",
+                        row_group.compressed_size()
+                    );
+                }
+            }
+        }
+    }
+
+    #[tokio::test]
+    async fn test_parquet_row_check_interval_keeps_default_page_row_limit() {
+        let file_io = test_file_io();
+        let table_path = "memory:/test_parquet_row_check_interval";
+        setup_dirs(&file_io, table_path).await;
+        let table = sizing_table(
+            &file_io,
+            table_path,
+            false,
+            &[("parquet.page.size.row.check.min", "65536")],
+        );
+        let names = (0..131_072).map(|id| (id % 10).to_string()).collect();
+        let default_limit = 
parquet::file::properties::DEFAULT_DATA_PAGE_ROW_COUNT_LIMIT as i64;
+        for (path, metadata) in write_sizing_batch(&file_io, &table, 
&sizing_batch(names)).await {
+            let offsets = metadata.offset_index().expect("page index");
+            for (row_group, columns) in 
metadata.row_groups().iter().zip(offsets) {
+                for column in columns {
+                    let firsts = column
+                        .page_locations()
+                        .iter()
+                        .map(|page| page.first_row_index)
+                        .chain(std::iter::once(row_group.num_rows()))
+                        .collect::<Vec<_>>();
+                    assert!(
+                        firsts
+                            .windows(2)
+                            .all(|pair| pair[1] - pair[0] <= default_limit),
+                        "{path}: {firsts:?}"
+                    );
+                }
+            }
+        }
+    }
+
     #[tokio::test]
     async fn test_append_write_populates_value_stats() {
         let file_io = test_file_io();
diff --git a/docs/src/getting-started.md b/docs/src/getting-started.md
index 8a417b2b..85e6ecc5 100644
--- a/docs/src/getting-started.md
+++ b/docs/src/getting-started.md
@@ -132,6 +132,22 @@ Dictionary encoding is enabled by default. Disable it for 
all columns with
 Paths use dot-separated physical Parquet leaves, not logical MAP keys.
 These options affect new writes only.
 
+## Parquet Row Groups and Pages
+
+The Parquet writer accepts Java's sizing keys as plain integers:
+`parquet.block.size` (row group bytes), `parquet.page.size`,
+`parquet.dictionary.page.size`, `parquet.page.row.count.limit` and
+`parquet.page.size.row.check.min`. As in Java, `file.block-size` (for example
+`32 mb`) overrides `parquet.block.size`. Unset keys keep the parquet-rs
+defaults: no row group byte limit with at most 1,048,576 rows, 1 MiB data and
+dictionary pages, and 20,000 rows per page. parquet-rs checks page limits 
between
+write batches of `parquet.page.size.row.check.min` rows (1,024 by default, and
+never more than the effective page row limit), so a page can exceed its row 
limit
+by up to one batch. With a row group byte limit, the writer passes rows in 
slices
+of at most a quarter of the limit and closes the row group once its encoded 
size
+reaches the limit, so large or skewed batches stay close to it. These options
+affect new writes only.
+
 ## Parquet Page Pruning
 
 Parquet Page Index pruning is enabled by default. Set

Reply via email to