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

github-merge-queue[bot] pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg-rust.git


The following commit(s) were added to refs/heads/main by this push:
     new 200ee6593 fix(arrow): map Iceberg Binary to Arrow Binary instead of 
LargeBinary (#2710)
200ee6593 is described below

commit 200ee6593dee84654a3b98aed724c7d094aa18cf
Author: Liang-Chi Hsieh <[email protected]>
AuthorDate: Tue Nov 3 22:57:48 2026 +0000

    fix(arrow): map Iceberg Binary to Arrow Binary instead of LargeBinary 
(#2710)
    
    * fix(arrow): map Iceberg Binary to Arrow Binary instead of LargeBinary
    
    `type_to_arrow_type` mapped `PrimitiveType::Binary` to 
`DataType::LargeBinary`,
    which is inconsistent with both the Java implementation
    (`ArrowSchemaUtil` maps Iceberg Binary to Arrow `Binary`) and parquet-rs,
    which reads an unannotated Parquet `BYTE_ARRAY` column as `DataType::Binary`
    by default. The mismatch forces an unnecessary cast when reading Binary
    columns and diverges from the rest of the ecosystem.
    
    Map `PrimitiveType::Binary` to `DataType::Binary`. The reverse conversion
    (`arrow_schema_to_schema`), the arrow-to-literal path, and the bucket
    transform already accept both `Binary` and `LargeBinary`, so externally
    produced `LargeBinary` columns keep working.
    
    Closes #2698
    
    * fix(writer): accept LargeBinary equality columns after the Binary mapping 
change
    
    With Iceberg Binary now mapped to Arrow Binary, the equality delete
    projector builds its output batch against a Binary schema while reusing
    the input arrays as-is, so batches carrying LargeBinaryArray equality
    columns (accepted before) failed RecordBatch::try_new. Cast LargeBinary
    to Binary in RecordBatchProjector::project_batch and add a regression
    test. Also drop redundant `.as_slice()` in test fixtures.
    
    Co-authored-by: Isaac <[email protected]>
    
    * fix(arrow): accept LargeBinary source columns in partition value 
calculation
    
    PartitionValueCalculator builds its result struct against the partition
    Arrow type, which now uses Binary for Iceberg binary fields, while the
    identity transform passes the projected LargeBinary column through, so
    StructArray::try_new failed. Move the LargeBinary -> Binary cast from
    RecordBatchProjector::project_batch into project_column so both callers
    get columns matching the projected schema. This also lets truncate
    partitions on binary fields accept LargeBinary input.
    
    Co-authored-by: Isaac <[email protected]>
    
    ---------
    
    Co-authored-by: Isaac <[email protected]>
---
 .../src/arrow/partition_value_calculator.rs        | 64 +++++++++++++++++-
 crates/iceberg/src/arrow/record_batch_projector.rs | 18 ++++-
 crates/iceberg/src/arrow/schema.rs                 |  9 +--
 .../writer/base_writer/equality_delete_writer.rs   | 76 +++++++++++++++++++++-
 .../src/writer/file_writer/parquet_writer.rs       |  2 +-
 5 files changed, 160 insertions(+), 9 deletions(-)

diff --git a/crates/iceberg/src/arrow/partition_value_calculator.rs 
b/crates/iceberg/src/arrow/partition_value_calculator.rs
index c8c917779..142de42cd 100644
--- a/crates/iceberg/src/arrow/partition_value_calculator.rs
+++ b/crates/iceberg/src/arrow/partition_value_calculator.rs
@@ -163,7 +163,7 @@ impl PartitionValueCalculator {
 mod tests {
     use std::sync::Arc;
 
-    use arrow_array::{Int32Array, RecordBatch, StringArray};
+    use arrow_array::{BinaryArray, Int32Array, LargeBinaryArray, RecordBatch, 
StringArray};
     use arrow_schema::{Field, Schema as ArrowSchema};
 
     use super::*;
@@ -220,6 +220,68 @@ mod tests {
         assert_eq!(id_partition.value(2), 30);
     }
 
+    #[test]
+    fn test_partition_calculator_binary_partition_accepts_large_binary_input() 
{
+        let table_schema = Schema::builder()
+            .with_schema_id(0)
+            .with_fields(vec![
+                NestedField::required(1, "id", 
Type::Primitive(PrimitiveType::Int)).into(),
+                NestedField::optional(2, "key", 
Type::Primitive(PrimitiveType::Binary)).into(),
+            ])
+            .build()
+            .unwrap();
+
+        let partition_spec = 
PartitionSpecBuilder::new(Arc::new(table_schema.clone()))
+            .add_partition_field("key", "key_identity", Transform::Identity)
+            .unwrap()
+            .add_partition_field("key", "key_trunc", Transform::Truncate(2))
+            .unwrap()
+            .build()
+            .unwrap();
+        let calculator = PartitionValueCalculator::try_new(&partition_spec, 
&table_schema).unwrap();
+
+        let arrow_schema = Arc::new(ArrowSchema::new(vec![
+            Field::new("id", DataType::Int32, false),
+            Field::new("key", DataType::LargeBinary, true),
+        ]));
+        let batch = RecordBatch::try_new(arrow_schema, vec![
+            Arc::new(Int32Array::from(vec![1, 2, 3])),
+            Arc::new(LargeBinaryArray::from_opt_vec(vec![
+                Some(b"abc"),
+                None,
+                Some(b"z"),
+            ])),
+        ])
+        .unwrap();
+
+        let result = calculator.calculate(&batch).unwrap();
+
+        let DataType::Struct(expected_fields) = 
calculator.partition_arrow_type() else {
+            unreachable!()
+        };
+        let expected = StructArray::try_new(
+            expected_fields.clone(),
+            vec![
+                Arc::new(BinaryArray::from_opt_vec(vec![
+                    Some(b"abc"),
+                    None,
+                    Some(b"z"),
+                ])),
+                Arc::new(BinaryArray::from_opt_vec(vec![
+                    Some(b"ab"),
+                    None,
+                    Some(b"z"),
+                ])),
+            ],
+            None,
+        )
+        .unwrap();
+        assert_eq!(
+            result.as_any().downcast_ref::<StructArray>().unwrap(),
+            &expected
+        );
+    }
+
     #[test]
     fn test_partition_calculator_unpartitioned_error() {
         let table_schema = Schema::builder()
diff --git a/crates/iceberg/src/arrow/record_batch_projector.rs 
b/crates/iceberg/src/arrow/record_batch_projector.rs
index 823ef3188..ca559933f 100644
--- a/crates/iceberg/src/arrow/record_batch_projector.rs
+++ b/crates/iceberg/src/arrow/record_batch_projector.rs
@@ -19,6 +19,7 @@ use std::sync::Arc;
 
 use arrow_array::{Array, ArrayRef, RecordBatch, StructArray, make_array};
 use arrow_buffer::NullBuffer;
+use arrow_cast::cast;
 use arrow_schema::{DataType, Field, FieldRef, Fields, Schema, SchemaRef};
 
 use crate::arrow::schema::{schema_to_arrow_schema, 
try_get_field_id_from_metadata};
@@ -160,8 +161,21 @@ impl RecordBatchProjector {
     /// Do projection with columns
     pub fn project_column(&self, batch: &[ArrayRef]) -> Result<Vec<ArrayRef>> {
         let mut columns = Vec::with_capacity(self.field_indices.len());
-        for index_vec in &self.field_indices {
-            columns.push(Self::get_column_by_field_index(batch, index_vec)?);
+        for (index_vec, field) in self
+            .field_indices
+            .iter()
+            .zip(self.projected_schema.fields())
+        {
+            let column = Self::get_column_by_field_index(batch, index_vec)?;
+            let column = match (column.data_type(), field.data_type()) {
+                (DataType::LargeBinary, DataType::Binary) => cast(&column, 
&DataType::Binary)
+                    .map_err(|err| {
+                        invalid_data!("Failed to cast column {} to Binary", 
field.name())
+                            .with_source(err)
+                    })?,
+                _ => column,
+            };
+            columns.push(column);
         }
         Ok(columns)
     }
diff --git a/crates/iceberg/src/arrow/schema.rs 
b/crates/iceberg/src/arrow/schema.rs
index 91b8d687a..24fc93f03 100644
--- a/crates/iceberg/src/arrow/schema.rs
+++ b/crates/iceberg/src/arrow/schema.rs
@@ -890,7 +890,8 @@ impl SchemaVisitor for ToArrowSchemaConverter {
                     .map(DataType::FixedSizeBinary)
                     .unwrap_or(DataType::LargeBinary),
             )),
-            PrimitiveType::Binary | PrimitiveType::Geometry(_) | 
PrimitiveType::Geography(_) => {
+            PrimitiveType::Binary => 
Ok(ArrowSchemaOrFieldOrType::Type(DataType::Binary)),
+            PrimitiveType::Geometry(_) | PrimitiveType::Geography(_) => {
                 Ok(ArrowSchemaOrFieldOrType::Type(DataType::LargeBinary))
             }
         }
@@ -2015,8 +2016,8 @@ mod tests {
                 false,
                 "12",
             ),
-            simple_field("l", DataType::LargeBinary, false, "13"),
-            simple_field("o", DataType::LargeBinary, false, "22"),
+            simple_field("l", DataType::Binary, false, "13"),
+            simple_field("o", DataType::Binary, false, "22"),
             simple_field("m", DataType::FixedSizeBinary(10), false, "11"),
             simple_field(
                 "list",
@@ -2044,7 +2045,7 @@ mod tests {
                 "fixed_list",
                 DataType::List(Arc::new(simple_field(
                     "element",
-                    DataType::LargeBinary,
+                    DataType::Binary,
                     false,
                     "26",
                 ))),
diff --git a/crates/iceberg/src/writer/base_writer/equality_delete_writer.rs 
b/crates/iceberg/src/writer/base_writer/equality_delete_writer.rs
index 84c10b176..f62016add 100644
--- a/crates/iceberg/src/writer/base_writer/equality_delete_writer.rs
+++ b/crates/iceberg/src/writer/base_writer/equality_delete_writer.rs
@@ -763,7 +763,7 @@ mod test {
             )
             .unwrap(),
         ) as ArrayRef;
-        let col13 = Arc::new(arrow_array::LargeBinaryArray::from_opt_vec(vec![
+        let col13 = Arc::new(arrow_array::BinaryArray::from_opt_vec(vec![
             Some(b"one"),
             Some(b""),
             Some(b"zzzz"),
@@ -785,6 +785,80 @@ mod test {
         Ok(())
     }
 
+    #[tokio::test]
+    async fn test_equality_delete_accepts_large_binary_input() -> Result<(), 
anyhow::Error> {
+        let temp_dir = TempDir::new().unwrap();
+        let file_io = FileIO::new_with_fs();
+        let location_gen = DefaultLocationGenerator::with_data_location(
+            temp_dir.path().to_str().unwrap().to_string(),
+        );
+        let file_name_gen =
+            DefaultFileNameGenerator::new("test".to_string(), None, 
DataFileFormat::Parquet);
+
+        let schema = Arc::new(
+            Schema::builder()
+                .with_schema_id(1)
+                .with_fields(vec![
+                    NestedField::required(1, "id", 
Type::Primitive(PrimitiveType::Int)).into(),
+                    NestedField::optional(2, "key", 
Type::Primitive(PrimitiveType::Binary)).into(),
+                ])
+                .build()
+                .unwrap(),
+        );
+        let config = EqualityDeleteWriterConfig::new(vec![2], 
schema.clone()).unwrap();
+        let delete_arrow_schema = config.projected_arrow_schema_ref().clone();
+        let delete_schema = 
arrow_schema_to_schema(&delete_arrow_schema).unwrap();
+
+        let pb =
+            ParquetWriterBuilder::new(WriterProperties::builder().build(), 
Arc::new(delete_schema));
+        let rolling_writer_builder = 
RollingFileWriterBuilder::new_with_default_file_size(
+            pb,
+            file_io.clone(),
+            location_gen,
+            file_name_gen,
+        );
+        let mut equality_delete_writer =
+            EqualityDeleteFileWriterBuilder::new(rolling_writer_builder, 
config)
+                .build(None)
+                .await?;
+
+        let input_schema = Arc::new(arrow_schema::Schema::new(vec![
+            Field::new("id", DataType::Int32, 
false).with_metadata(HashMap::from([(
+                PARQUET_FIELD_ID_META_KEY.to_string(),
+                "1".to_string(),
+            )])),
+            Field::new("key", DataType::LargeBinary, 
true).with_metadata(HashMap::from([(
+                PARQUET_FIELD_ID_META_KEY.to_string(),
+                "2".to_string(),
+            )])),
+        ]));
+        let to_write = RecordBatch::try_new(input_schema, vec![
+            Arc::new(Int32Array::from(vec![1, 2, 3])) as ArrayRef,
+            Arc::new(arrow_array::LargeBinaryArray::from_opt_vec(vec![
+                Some(b"one"),
+                None,
+                Some(b"zzzz"),
+            ])) as ArrayRef,
+        ])
+        .unwrap();
+        equality_delete_writer.write(to_write).await?;
+        let res = equality_delete_writer.close().await?;
+        assert_eq!(res.len(), 1);
+
+        let expected = RecordBatch::try_new(delete_arrow_schema, vec![Arc::new(
+            arrow_array::BinaryArray::from_opt_vec(vec![Some(b"one"), None, 
Some(b"zzzz")]),
+        ) as ArrayRef])
+        .unwrap();
+        check_parquet_data_file_with_equality_delete_write(
+            &file_io,
+            &res.into_iter().next().unwrap(),
+            &expected,
+        )
+        .await;
+
+        Ok(())
+    }
+
     #[tokio::test]
     async fn test_equality_delete_with_nullable_field() -> Result<(), 
anyhow::Error> {
         // prepare data
diff --git a/crates/iceberg/src/writer/file_writer/parquet_writer.rs 
b/crates/iceberg/src/writer/file_writer/parquet_writer.rs
index 1470b310b..485a94200 100644
--- a/crates/iceberg/src/writer/file_writer/parquet_writer.rs
+++ b/crates/iceberg/src/writer/file_writer/parquet_writer.rs
@@ -1377,7 +1377,7 @@ mod tests {
             None,
             Some("d"),
         ])) as ArrayRef;
-        let col6 = Arc::new(arrow_array::LargeBinaryArray::from_opt_vec(vec![
+        let col6 = Arc::new(arrow_array::BinaryArray::from_opt_vec(vec![
             Some(b"one"),
             None,
             Some(b""),

Reply via email to