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 adc8717d [core] Extend Arrow input support for row-level updates 
(#1149)
adc8717d is described below

commit adc8717d8e413c8f4a11c5f79a5265c365cbe769
Author: Jingsong Lee <[email protected]>
AuthorDate: Sat Oct 17 22:59:39 2026 +0800

    [core] Extend Arrow input support for row-level updates (#1149)
---
 bindings/python/src/arrow_input.rs                 | 350 ++++++++++++
 bindings/python/src/lib.rs                         |   1 +
 bindings/python/src/update_assignment.rs           |  10 +-
 bindings/python/src/write.rs                       |   4 +-
 crates/paimon/src/arrow/evolution_cast.rs          |  77 +--
 crates/paimon/src/arrow/mod.rs                     |   2 +-
 crates/paimon/src/table/key_normalization.rs       |  28 -
 crates/paimon/src/table/update_input.rs            | 199 +++++--
 .../src/table/update_input/container_tests.rs      | 240 +++++++++
 crates/paimon/src/table/update_input/decimal.rs    | 239 ++++++++
 crates/paimon/src/table/update_input/encoded.rs    | 599 +++++++++++++++++++++
 .../paimon/src/table/update_input/scalar_tests.rs  | 207 +++++++
 12 files changed, 1832 insertions(+), 124 deletions(-)

diff --git a/bindings/python/src/arrow_input.rs 
b/bindings/python/src/arrow_input.rs
new file mode 100644
index 00000000..18cb5427
--- /dev/null
+++ b/bindings/python/src/arrow_input.rs
@@ -0,0 +1,350 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+//! Preserve C Data offsets before Arrow Rust materializes sparse Union arrays.
+
+use std::sync::Arc;
+
+use arrow::array::{make_array, ArrayData, ArrayRef, RecordBatch, 
RecordBatchOptions};
+use arrow::datatypes::{DataType, Schema, UnionMode};
+use arrow::error::ArrowError;
+use arrow::pyarrow::FromPyArrow;
+use pyo3::exceptions::PyValueError;
+use pyo3::prelude::*;
+
+fn contains_sparse_union(data_type: &DataType) -> bool {
+    match data_type {
+        DataType::Union(_, UnionMode::Sparse) => true,
+        DataType::Union(fields, _) => fields
+            .iter()
+            .any(|(_, f)| contains_sparse_union(f.data_type())),
+        DataType::Struct(fields) => fields.iter().any(|f| 
contains_sparse_union(f.data_type())),
+        DataType::List(f)
+        | DataType::LargeList(f)
+        | DataType::FixedSizeList(f, _)
+        | DataType::ListView(f)
+        | DataType::LargeListView(f)
+        | DataType::Map(f, _) => contains_sparse_union(f.data_type()),
+        DataType::Dictionary(_, values) => contains_sparse_union(values),
+        DataType::RunEndEncoded(_, values) => 
contains_sparse_union(values.data_type()),
+        _ => false,
+    }
+}
+
+pub(crate) fn array_from_pyarrow(value: &Bound<'_, PyAny>) -> 
PyResult<ArrayRef> {
+    let mut data = ArrayData::from_pyarrow_bound(value)?;
+    // Foreign buffers may be unaligned; normalize only after making them
+    // valid for Arrow Rust's typed access and ArrayData builder validation.
+    data.align_buffers();
+    let data = normalize_sparse_unions(data).map_err(|e| 
PyValueError::new_err(e.to_string()))?;
+    Ok(make_array(data))
+}
+
+pub(crate) fn batch_from_pyarrow(value: &Bound<'_, PyAny>) -> 
PyResult<RecordBatch> {
+    let schema = 
Arc::new(Schema::from_pyarrow_bound(&value.getattr("schema")?)?);
+    if !schema
+        .fields()
+        .iter()
+        .any(|f| contains_sparse_union(f.data_type()))
+    {
+        return RecordBatch::from_pyarrow_bound(value);
+    }
+    // The standard batch importer materializes its children before we can fix
+    // Union offsets. Import each column's C Data representation first instead.
+    let columns = value
+        .getattr("columns")?
+        .try_iter()?
+        .map(|column| array_from_pyarrow(&column?))
+        .collect::<PyResult<Vec<_>>>()?;
+    let rows = value.getattr("num_rows")?.extract::<usize>()?;
+    RecordBatch::try_new_with_options(
+        schema,
+        columns,
+        &RecordBatchOptions::default().with_row_count(Some(rows)),
+    )
+    .map_err(|e| PyValueError::new_err(e.to_string()))
+}
+
+fn normalize_sparse_unions(data: ArrayData) -> Result<ArrayData, ArrowError> {
+    if !contains_sparse_union(data.data_type()) {
+        return Ok(data);
+    }
+    normalize_imported_data(data)
+}
+
+fn normalize_imported_data(data: ArrayData) -> Result<ArrayData, ArrowError> {
+    let len = data.len();
+    normalize_imported_slice(data, 0, len)
+}
+
+fn normalize_imported_slice(
+    data: ArrayData,
+    relative_offset: usize,
+    len: usize,
+) -> Result<ArrayData, ArrowError> {
+    // Once a parent is sliced, normalize every descendant, including ordinary
+    // STRUCT siblings and Union children. Arrow's STRUCT materializer uses
+    // ArrayData::slice, which can otherwise apply nested offsets twice.
+    let (data_type, _, nulls, offset, mut buffers, children) = 
data.into_parts();
+    let nulls = nulls.map(|n| n.slice(relative_offset, len));
+    let mut offset = offset + relative_offset;
+    // Defer validation until both parent and children share their coordinates.
+    // An intermediate ArrayData slice would validate a sliced parent's NULLs
+    // against unsliced non-nullable children, or propagate STRUCT offsets 
twice.
+    let children = match &data_type {
+        DataType::Struct(_) | DataType::Union(_, UnionMode::Sparse) => {
+            if matches!(data_type, DataType::Union(_, UnionMode::Sparse)) {
+                // UnionArray::from(ArrayData) applies the parent offset to 
type
+                // IDs but not sparse children. Canonicalize both before that.
+                buffers[0] = buffers[0].slice_with_length(offset, len);
+            }
+            let children = children
+                .into_iter()
+                .map(|c| normalize_imported_slice(c, offset, len))
+                .collect::<Result<Vec<_>, _>>()?;
+            offset = 0;
+            children
+        }
+        DataType::FixedSizeList(_, width) => {
+            let width = *width as usize;
+            let children = children
+                .into_iter()
+                .map(|c| normalize_imported_slice(c, offset * width, len * 
width))
+                .collect::<Result<Vec<_>, _>>()?;
+            offset = 0;
+            children
+        }
+        _ => children
+            .into_iter()
+            .map(normalize_imported_data)
+            .collect::<Result<Vec<_>, _>>()?,
+    };
+    ArrayData::builder(data_type)
+        .len(len)
+        .offset(offset)
+        .nulls(nulls)
+        .buffers(buffers)
+        .child_data(children)
+        .build()
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use arrow::array::{
+        Array, FixedSizeListArray, Int32Array, StringArray, StructArray, 
UnionArray,
+    };
+    use arrow::buffer::{NullBuffer, ScalarBuffer};
+    use arrow::datatypes::{Field, UnionFields};
+
+    fn input() -> ArrayData {
+        UnionArray::try_new(
+            UnionFields::try_new(
+                [7, 42],
+                [
+                    Field::new("n", DataType::Int32, true),
+                    Field::new("s", DataType::Utf8, true),
+                ],
+            )
+            .unwrap(),
+            ScalarBuffer::from(vec![42, 7, 7, 42]),
+            None,
+            vec![
+                Arc::new(Int32Array::from(vec![0, 1, 2, 0])),
+                Arc::new(StringArray::from(vec!["hidden"; 4])),
+            ],
+        )
+        .unwrap()
+        .to_data()
+    }
+
+    fn assert_values(data: ArrayData) {
+        let array = UnionArray::from(data);
+        assert_eq!(array.type_ids().as_ref(), &[7, 7]);
+        assert_eq!(
+            array.child(7).to_data(),
+            Int32Array::from(vec![1, 2]).to_data()
+        );
+    }
+
+    #[test]
+    fn c_data_sparse_union_slice_rebases_children_and_type_ids() {
+        let raw = input().into_builder().offset(1).len(2).build().unwrap();
+        assert_values(normalize_sparse_unions(raw).unwrap());
+    }
+
+    #[test]
+    fn c_data_parent_offsets_are_applied_before_union_materialization() {
+        let child = input();
+        let rows = ArrayData::builder(DataType::Struct(
+            vec![Field::new("u", child.data_type().clone(), true)].into(),
+        ))
+        .offset(1)
+        .len(2)
+        .child_data(vec![child.clone()])
+        .build()
+        .unwrap();
+        let rows = StructArray::from(normalize_sparse_unions(rows).unwrap());
+        assert_values(rows.column(0).to_data());
+        // Nested STRUCT children are still in the original C Data coordinate
+        // system; the outer slice must not be propagated into them twice.
+        let nested = ArrayData::builder(DataType::Struct(
+            vec![Field::new("u", child.data_type().clone(), true)].into(),
+        ))
+        .len(4)
+        .child_data(vec![child.clone()])
+        .build()
+        .unwrap();
+        let outer = ArrayData::builder(DataType::Struct(
+            vec![Field::new("row", nested.data_type().clone(), true)].into(),
+        ))
+        .offset(1)
+        .len(2)
+        .child_data(vec![nested])
+        .build()
+        .unwrap();
+        let outer = StructArray::from(normalize_sparse_unions(outer).unwrap());
+        let nested = outer
+            .column(0)
+            .as_any()
+            .downcast_ref::<StructArray>()
+            .unwrap();
+        assert_values(nested.column(0).to_data());
+        let lists = ArrayData::builder(DataType::FixedSizeList(
+            Arc::new(Field::new("item", child.data_type().clone(), true)),
+            1,
+        ))
+        .offset(1)
+        .len(2)
+        .child_data(vec![child])
+        .build()
+        .unwrap();
+        let lists = 
FixedSizeListArray::from(normalize_sparse_unions(lists).unwrap());
+        assert_values(lists.values().to_data());
+    }
+
+    #[test]
+    fn c_data_slices_normalize_ordinary_nested_union_children_and_siblings() {
+        let leaf = Int32Array::from(vec![0, 1, 2, 3]).to_data();
+        let inner = ArrayData::builder(DataType::Struct(
+            vec![Field::new("b", DataType::Int32, true)].into(),
+        ))
+        .len(4)
+        .child_data(vec![leaf])
+        .build()
+        .unwrap();
+        let ordinary = ArrayData::builder(DataType::Struct(
+            vec![Field::new("a", inner.data_type().clone(), true)].into(),
+        ))
+        .len(4)
+        .child_data(vec![inner])
+        .build()
+        .unwrap();
+        let sibling = ArrayData::builder(DataType::Struct(
+            vec![
+                Field::new("u", input().data_type().clone(), true),
+                Field::new("ordinary", ordinary.data_type().clone(), true),
+            ]
+            .into(),
+        ))
+        .len(2)
+        .offset(1)
+        .child_data(vec![input(), ordinary.clone()])
+        .build()
+        .unwrap();
+        let sibling = 
StructArray::from(normalize_sparse_unions(sibling).unwrap());
+        let expected = Int32Array::from(vec![1, 2]).to_data();
+        let assert_ordinary = |data: ArrayData| {
+            let outer = StructArray::from(data);
+            let inner = outer
+                .column(0)
+                .as_any()
+                .downcast_ref::<StructArray>()
+                .unwrap();
+            assert_eq!(inner.column(0).to_data(), expected);
+        };
+        assert_ordinary(sibling.column(1).to_data());
+        let union = ArrayData::builder(DataType::Union(
+            UnionFields::try_new(
+                [7],
+                [Field::new("ordinary", ordinary.data_type().clone(), true)],
+            )
+            .unwrap(),
+            UnionMode::Sparse,
+        ))
+        .len(2)
+        .offset(1)
+        .buffers(vec![ScalarBuffer::from(vec![7_i8; 4]).into_inner()])
+        .child_data(vec![ordinary])
+        .build()
+        .unwrap();
+        let union = UnionArray::from(normalize_sparse_unions(union).unwrap());
+        assert_ordinary(union.child(7).to_data());
+    }
+
+    #[test]
+    fn c_data_nullability_is_validated_after_parent_and_children_are_sliced() {
+        let numbers = Int32Array::from(vec![None, Some(1), Some(2), None]);
+        let parents = NullBuffer::from(vec![false, true, true, false]);
+        let rows = ArrayData::builder(DataType::Struct(
+            vec![Field::new("n", DataType::Int32, false)].into(),
+        ))
+        .len(4)
+        .nulls(Some(parents.clone()))
+        .child_data(vec![numbers.to_data()])
+        .build()
+        .unwrap();
+        let lists = ArrayData::builder(DataType::FixedSizeList(
+            Arc::new(Field::new("item", DataType::Int32, false)),
+            1,
+        ))
+        .len(4)
+        .nulls(Some(parents))
+        .child_data(vec![numbers.to_data()])
+        .build()
+        .unwrap();
+        for ordinary in [rows, lists] {
+            // Both children are initially valid in their complete C Data
+            // coordinates. The nested parent's NULL masks must move together.
+            let input = ArrayData::builder(DataType::Struct(
+                vec![
+                    Field::new("u", input().data_type().clone(), true),
+                    Field::new("ordinary", ordinary.data_type().clone(), true),
+                ]
+                .into(),
+            ))
+            .len(2)
+            .offset(1)
+            .child_data(vec![input(), ordinary])
+            .build()
+            .unwrap();
+            let result = 
StructArray::from(normalize_sparse_unions(input).unwrap());
+            let ordinary = result.column(1);
+            assert_eq!(ordinary.null_count(), 0);
+            let values = if let Some(row) = 
ordinary.as_any().downcast_ref::<StructArray>() {
+                row.column(0)
+            } else {
+                ordinary
+                    .as_any()
+                    .downcast_ref::<FixedSizeListArray>()
+                    .unwrap()
+                    .values()
+            };
+            assert_eq!(values.to_data(), Int32Array::from(vec![1, 
2]).to_data());
+        }
+    }
+}
diff --git a/bindings/python/src/lib.rs b/bindings/python/src/lib.rs
index 72d17d42..d579ee28 100644
--- a/bindings/python/src/lib.rs
+++ b/bindings/python/src/lib.rs
@@ -17,6 +17,7 @@
 
 use pyo3::prelude::*;
 
+mod arrow_input;
 mod blob;
 mod blob_uri_reader;
 mod catalog_version;
diff --git a/bindings/python/src/update_assignment.rs 
b/bindings/python/src/update_assignment.rs
index d0cc3134..290d9560 100644
--- a/bindings/python/src/update_assignment.rs
+++ b/bindings/python/src/update_assignment.rs
@@ -17,8 +17,8 @@
 
 //! Python object and callback conversion for core update assignments.
 
-use arrow::array::{make_array, ArrayData, ArrayRef};
-use arrow::pyarrow::{FromPyArrow, ToPyArrow};
+use arrow::array::ArrayRef;
+use arrow::pyarrow::ToPyArrow;
 use paimon::spec::DataField;
 use paimon::table::UpdateAssignment;
 use pyo3::exceptions::PyValueError;
@@ -41,12 +41,12 @@ fn bridge_error(error: PyErr, target: &CallbackError) -> 
paimon::Error {
 fn arrow_arrays(py: Python<'_>, value: &Bound<'_, PyAny>) -> 
PyResult<Vec<ArrayRef>> {
     let pa = py.import("pyarrow")?;
     if value.is_instance(&pa.getattr("Array")?)? {
-        Ok(vec![make_array(ArrayData::from_pyarrow_bound(value)?)])
+        Ok(vec![crate::arrow_input::array_from_pyarrow(value)?])
     } else if value.is_instance(&pa.getattr("ChunkedArray")?)? {
         value
             .getattr("chunks")?
             .try_iter()?
-            .map(|chunk| 
Ok(make_array(ArrayData::from_pyarrow_bound(&chunk?)?)))
+            .map(|chunk| crate::arrow_input::array_from_pyarrow(&chunk?))
             .collect()
     } else {
         Err(PyValueError::new_err(
@@ -121,7 +121,7 @@ pub(crate) fn from_python(
                                 (PyList::new(py, [scalar])?,),
                                 Some(&kwargs),
                             )?;
-                            
Ok(make_array(ArrayData::from_pyarrow_bound(&one)?))
+                            crate::arrow_input::array_from_pyarrow(&one)
                         })
                     })
                     .map_err(|cause| bridge_error(cause, &error))
diff --git a/bindings/python/src/write.rs b/bindings/python/src/write.rs
index 312990d0..f9a1716a 100644
--- a/bindings/python/src/write.rs
+++ b/bindings/python/src/write.rs
@@ -540,7 +540,7 @@ fn arrow_table_batches(table: &Bound<'_, PyAny>) -> 
PyResult<Vec<RecordBatch>> {
     let mut batches = table
         .call_method0("to_batches")?
         .try_iter()?
-        .map(|batch| RecordBatch::from_pyarrow_bound(&batch?))
+        .map(|batch| crate::arrow_input::batch_from_pyarrow(&batch?))
         .collect::<PyResult<Vec<_>>>()?;
     // Preserve empty input schema so core validates columns even with no rows.
     if batches.is_empty() {
@@ -559,7 +559,7 @@ fn arrow_upsert_batches(input: &Bound<'_, PyAny>) -> 
PyResult<Vec<RecordBatch>>
     }
     input
         .try_iter()?
-        .map(|batch| RecordBatch::from_pyarrow_bound(&batch?))
+        .map(|batch| crate::arrow_input::batch_from_pyarrow(&batch?))
         .collect()
 }
 
diff --git a/crates/paimon/src/arrow/evolution_cast.rs 
b/crates/paimon/src/arrow/evolution_cast.rs
index 5e85e110..991932f0 100644
--- a/crates/paimon/src/arrow/evolution_cast.rs
+++ b/crates/paimon/src/arrow/evolution_cast.rs
@@ -131,45 +131,56 @@ pub(crate) fn cast_numeric(
             let strings = super::evolution_decimal::decimal_to_string(source)?;
             return Ok(Some(super::apply_declared_width(&strings, to)));
         }
-        // DecimalUtils.doubleValue divides the compact long by its scale;
-        // only wide decimals use BigDecimal.doubleValue. FLOAT rounds that
-        // double rather than the decimal coefficient directly.
-        let values: Vec<Option<f64>> = if decimal.precision() <= 18 {
-            let divisor = 10_f64.powi(decimal.scale() as i32);
-            source
-                .as_primitive::<Decimal128Type>()
-                .iter()
-                .map(|value| value.map(|value| value as f64 / divisor))
-                .collect()
-        } else {
-            let strings = super::evolution_decimal::decimal_to_string(source)?;
-            strings
-                .as_string::<i32>()
-                .iter()
-                .map(|value| {
-                    value
-                        .map(|value| {
-                            value
-                                .parse::<f64>()
-                                .map_err(|_| invalid_numeric("Invalid DECIMAL 
value"))
-                        })
-                        .transpose()
-                })
-                .collect::<crate::Result<Vec<_>>>()?
-        };
-        return Ok(Some(match target {
-            ArrowType::Float32 => Arc::new(Float32Array::from_iter(
-                values.into_iter().map(|v| v.map(|v| v as f32)),
-            )) as ArrayRef,
-            ArrowType::Float64 => Arc::new(Float64Array::from(values)) as 
ArrayRef,
-            _ => unreachable!("validated DECIMAL numeric target"),
-        }));
+        return cast_decimal_to_floating(source, decimal.precision(), 
decimal.scale(), &target)
+            .map(Some);
     }
     arrow_cast::cast(source, &target)
         .map(Some)
         .map_err(|error| invalid_numeric(error.to_string()))
 }
 
+/// Java DECIMAL conversion shared by schema reads and external update inputs.
+pub(crate) fn cast_decimal_to_floating(
+    source: &ArrayRef,
+    precision: u32,
+    scale: u32,
+    target: &ArrowType,
+) -> crate::Result<ArrayRef> {
+    // DecimalUtils.doubleValue divides the compact long by its scale;
+    // only wide decimals use BigDecimal.doubleValue. FLOAT rounds that
+    // double rather than the decimal coefficient directly.
+    let values: Vec<Option<f64>> = if precision <= 18 {
+        let divisor = 10_f64.powi(scale as i32);
+        source
+            .as_primitive::<Decimal128Type>()
+            .iter()
+            .map(|value| value.map(|value| value as f64 / divisor))
+            .collect()
+    } else {
+        let strings = super::evolution_decimal::decimal_to_string(source)?;
+        strings
+            .as_string::<i32>()
+            .iter()
+            .map(|value| {
+                value
+                    .map(|value| {
+                        value
+                            .parse::<f64>()
+                            .map_err(|_| invalid_numeric("Invalid DECIMAL 
value"))
+                    })
+                    .transpose()
+            })
+            .collect::<crate::Result<Vec<_>>>()?
+    };
+    Ok(match target {
+        ArrowType::Float32 => Arc::new(Float32Array::from_iter(
+            values.into_iter().map(|v| v.map(|v| v as f32)),
+        )) as ArrayRef,
+        ArrowType::Float64 => Arc::new(Float64Array::from(values)) as ArrayRef,
+        _ => unreachable!("validated DECIMAL numeric target"),
+    })
+}
+
 fn invalid_numeric(message: impl Into<String>) -> crate::Error {
     crate::Error::DataInvalid {
         message: message.into(),
diff --git a/crates/paimon/src/arrow/mod.rs b/crates/paimon/src/arrow/mod.rs
index 7a057c0a..fddf9f19 100644
--- a/crates/paimon/src/arrow/mod.rs
+++ b/crates/paimon/src/arrow/mod.rs
@@ -26,7 +26,7 @@ mod cast_completion_java_test;
 mod cast_text;
 pub(crate) use cast_text::parse_timestamp as parse_java_timestamp;
 mod evolution_cast;
-pub(crate) use evolution_cast::{cast_numeric, supports_numeric_cast};
+pub(crate) use evolution_cast::{cast_decimal_to_floating, cast_numeric, 
supports_numeric_cast};
 mod evolution_decimal;
 mod evolution_string;
 mod evolution_temporal;
diff --git a/crates/paimon/src/table/key_normalization.rs 
b/crates/paimon/src/table/key_normalization.rs
index 3ea04dc5..50b78dc8 100644
--- a/crates/paimon/src/table/key_normalization.rs
+++ b/crates/paimon/src/table/key_normalization.rs
@@ -36,34 +36,6 @@ pub(super) fn normalize_match_key(
         return Ok(new_null_array(expected, decoded.len()));
     }
     let source = decoded.data_type();
-    if source.is_decimal() && matches!(expected, DataType::Decimal128(..)) {
-        let source_scale = match source {
-            DataType::Decimal32(_, scale)
-            | DataType::Decimal64(_, scale)
-            | DataType::Decimal128(_, scale)
-            | DataType::Decimal256(_, scale) => *scale,
-            _ => unreachable!(),
-        };
-        let DataType::Decimal128(_, target_scale) = expected else {
-            unreachable!()
-        };
-        let difference = i16::from(*target_scale) - i16::from(source_scale);
-        let text_fallback = || {
-            // Arrow's decimal rescaler can reject a large scale difference
-            // before inspecting values, even for zero and NULL. Reuse the
-            // exact text parser as a fallback; it still rejects nonzero values
-            // that exceed precision or lose fractional digits.
-            let text = cast_update_value(&decoded, &DataType::Utf8, 
CastMode::Assignment)?;
-            cast_update_value(&text, expected, CastMode::Assignment)
-        };
-        // Arrow computes scale differences in i8 and uses a Decimal128 power
-        // table. Skip that kernel before either can overflow.
-        return if !(-38..=38).contains(&difference) {
-            text_fallback()
-        } else {
-            cast_update_value(&decoded, expected, 
CastMode::Assignment).or_else(|_| text_fallback())
-        };
-    }
     let exact_family = source.is_integer() && expected.is_integer()
         || source.is_decimal() && expected.is_decimal()
         || matches!(
diff --git a/crates/paimon/src/table/update_input.rs 
b/crates/paimon/src/table/update_input.rs
index f6a809db..38f1fccc 100644
--- a/crates/paimon/src/table/update_input.rs
+++ b/crates/paimon/src/table/update_input.rs
@@ -28,6 +28,9 @@ use arrow_array::{
 use arrow_buffer::OffsetBuffer;
 use arrow_schema::{DataType, Schema, TimeUnit};
 
+mod decimal;
+mod encoded;
+
 /// PyPaimon distinguishes evaluated assignments (safe casts) from row-ID
 /// input (numeric coercion). Both use the same encodings, strings and nested
 /// traversal; callers select the contract explicitly.
@@ -133,27 +136,37 @@ pub(super) fn cast_update_value(
     if mode == CastMode::RowUpdate {
         cast_values(array, target, CastMode::Assignment).or_else(|_| {
             validate_map_constructor_input(array, target)?;
-            let decoded = decode_dictionary(array)?;
-            // Python converts lists of pairs to tuples only for a top-level 
MAP.
-            if let DataType::Map(field, sorted) = target {
-                if matches!(
-                    decoded.data_type(),
-                    DataType::List(_)
-                        | DataType::LargeList(_)
-                        | DataType::ListView(_)
-                        | DataType::LargeListView(_)
-                        | DataType::FixedSizeList(..)
-                ) {
-                    return constructor_map(&decoded, field, *sorted);
-                }
-            }
-            cast_values(array, target, CastMode::Constructor)
+            construct_update_value(array, target)
         })
     } else {
         cast_values(array, target, mode)
     }
 }
 
+/// The root MAP constructor converts list pairs to tuples. Preserve that
+/// context through encoding wrappers, but not through ROW/ARRAY children.
+fn construct_update_value(array: &ArrayRef, target: &DataType) -> 
crate::Result<ArrayRef> {
+    if matches!(array.data_type(), DataType::Dictionary(..)) {
+        return encoded::construct_dictionary(array, target, 
construct_update_value);
+    }
+    if matches!(array.data_type(), DataType::Union(..)) {
+        return encoded::construct_union(array, target, construct_update_value);
+    }
+    if let DataType::Map(field, sorted) = target {
+        if matches!(
+            array.data_type(),
+            DataType::List(_)
+                | DataType::LargeList(_)
+                | DataType::ListView(_)
+                | DataType::LargeListView(_)
+                | DataType::FixedSizeList(..)
+        ) {
+            return constructor_map(array, field, *sorted);
+        }
+    }
+    cast_values(array, target, CastMode::Constructor)
+}
+
 /// Cast logical values recursively, so dictionary encoding and nesting cannot
 /// bypass primitive validation. The final outer cast only converts the layout.
 fn cast_values(array: &ArrayRef, target: &DataType, mode: CastMode) -> 
crate::Result<ArrayRef> {
@@ -164,8 +177,18 @@ fn cast_values(array: &ArrayRef, target: &DataType, mode: 
CastMode) -> crate::Re
         return Ok(array.clone());
     }
     if matches!(array.data_type(), DataType::Dictionary(..)) {
+        if mode == CastMode::Constructor {
+            return encoded::construct_dictionary(array, target, |array, 
target| {
+                cast_values(array, target, CastMode::Constructor)
+            });
+        }
         return cast_values(&decode_dictionary(array)?, target, mode);
     }
+    if mode == CastMode::Constructor && matches!(array.data_type(), 
DataType::Union(..)) {
+        return encoded::construct_union(array, target, |array, target| {
+            cast_values(array, target, CastMode::Constructor)
+        });
+    }
     if let DataType::ListView(field) | DataType::LargeListView(field) = 
array.data_type() {
         // Convert only the offsets/layout first. Casting children here would
         // bypass the recursive precision and formatting rules below.
@@ -209,10 +232,7 @@ fn cast_values(array: &ArrayRef, target: &DataType, mode: 
CastMode) -> crate::Re
             let columns = fields
                 .iter()
                 .map(|field| match input.column_by_name(field.name()) {
-                    Some(column) => {
-                        let column = constructor_child(column, input.nulls(), 
1, mode)?;
-                        cast_values(&column, field.data_type(), mode)
-                    }
+                    Some(column) => cast_child(column, field.data_type(), 
input.nulls(), 1, mode),
                     None => Ok(new_null_array(field.data_type(), input.len())),
                 })
                 .collect::<crate::Result<Vec<_>>>()?;
@@ -242,9 +262,13 @@ fn cast_values(array: &ArrayRef, target: &DataType, mode: 
CastMode) -> crate::Re
             )?),
             DataType::FixedSizeList(_, size) => {
                 let input = 
array.as_any().downcast_ref::<FixedSizeListArray>().unwrap();
-                let values =
-                    constructor_child(input.values(), input.nulls(), *size as 
usize, mode)?;
-                let values = cast_values(&values, field.data_type(), mode)?;
+                let values = cast_child(
+                    input.values(),
+                    field.data_type(),
+                    input.nulls(),
+                    *size as usize,
+                    mode,
+                )?;
                 Some(Arc::new(
                     FixedSizeListArray::try_new_with_length(
                         field.clone(),
@@ -256,35 +280,32 @@ fn cast_values(array: &ArrayRef, target: &DataType, mode: 
CastMode) -> crate::Re
                     .map_err(|error| invalid(error.to_string()))?,
                 ))
             }
+            DataType::Map(..) => {
+                let input = array.as_any().downcast_ref::<MapArray>().unwrap();
+                let DataType::Struct(fields) = field.data_type() else {
+                    return Err(invalid(
+                        "Map entries can only be cast to two-field ROW 
elements",
+                    ));
+                };
+                let (entries, offsets) = cast_map_entries(input, fields, 
mode)?;
+                Some(Arc::new(
+                    ListArray::try_new(
+                        field.clone(),
+                        offsets,
+                        Arc::new(entries),
+                        input.nulls().cloned(),
+                    )
+                    .map_err(|error| invalid(error.to_string()))?,
+                ))
+            }
             _ => None,
         },
         (DataType::Map(_, _), DataType::Map(field, sorted)) => {
             let input = array.as_any().downcast_ref::<MapArray>().unwrap();
-            let offsets = input.value_offsets();
-            let start = offsets[0];
-            let length = offsets[offsets.len() - 1] - start;
-            let (offsets, indices) = constructor_offsets(offsets, 
input.nulls(), mode);
-            let length = indices.as_ref().map_or(length as usize, |i| i.len());
             let DataType::Struct(fields) = field.data_type() else {
                 return Err(invalid("Map entries must have a struct type"));
             };
-            // Map fields may use custom names. Key/value roles are positional,
-            // unlike ROW fields, so never match these two children by name.
-            let columns = [input.keys(), input.values()]
-                .into_iter()
-                .zip(fields)
-                .map(|(values, field)| {
-                    let values = match &indices {
-                        Some(indices) => 
arrow_select::take::take(values.as_ref(), indices, None)
-                            .map_err(|error| invalid(error.to_string()))?,
-                        None => values.slice(start as usize, length),
-                    };
-                    cast_values(&values, field.data_type(), mode)
-                })
-                .collect::<crate::Result<Vec<_>>>()?;
-            let entries = StructArray::try_new_with_length(fields.clone(), 
columns, None, length)
-                .map_err(|error| invalid(error.to_string()))?;
-            let offsets = OffsetBuffer::new(offsets.into());
+            let (entries, offsets) = cast_map_entries(input, fields, mode)?;
             Some(Arc::new(
                 MapArray::try_new(
                     field.clone(),
@@ -323,6 +344,41 @@ fn cast_values(array: &ArrayRef, target: &DataType, mode: 
CastMode) -> crate::Re
     cast_primitive(array, target, mode)
 }
 
+/// Arrow represents a MAP as a list of key/value ROWs. Adapt these children
+/// positionally for both MAP and ARRAY<ROW> inputs, retaining recursive value
+/// conversion and the whole-column constructor's visibility rules.
+fn cast_map_entries(
+    input: &MapArray,
+    fields: &arrow_schema::Fields,
+    mode: CastMode,
+) -> crate::Result<(StructArray, OffsetBuffer<i32>)> {
+    if fields.len() != 2 {
+        return Err(invalid("Map entries require exactly two ROW fields"));
+    }
+    let offsets = input.value_offsets();
+    let start = offsets[0];
+    let length = offsets[offsets.len() - 1] - start;
+    let (offsets, indices) = constructor_offsets(offsets, input.nulls(), mode);
+    let length = indices.as_ref().map_or(length as usize, |i| i.len());
+    // MAP field names are aliases. Their key/value roles do not depend on the
+    // source names or the target ROW names, unlike a normal ROW-to-ROW cast.
+    let columns = [input.keys(), input.values()]
+        .into_iter()
+        .zip(fields)
+        .map(|(values, field)| {
+            let values = match &indices {
+                Some(indices) => arrow_select::take::take(values.as_ref(), 
indices, None)
+                    .map_err(|error| invalid(error.to_string()))?,
+                None => values.slice(start as usize, length),
+            };
+            cast_values(&values, field.data_type(), mode)
+        })
+        .collect::<crate::Result<Vec<_>>>()?;
+    let entries = StructArray::try_new_with_length(fields.clone(), columns, 
None, length)
+        .map_err(|error| invalid(error.to_string()))?;
+    Ok((entries, OffsetBuffer::new(offsets.into())))
+}
+
 // PyPaimon's row-ID fallback reconstructs Python dicts only after a 
whole-column
 // safe cast fails. At the top level it rejects ambiguous schema-less null 
keys.
 fn validate_map_constructor_input(array: &ArrayRef, target: &DataType) -> 
crate::Result<()> {
@@ -406,13 +462,12 @@ fn constructor_struct_from_map(
                 .collect::<crate::Result<Vec<_>>>()?;
             // Missing fields, NULL parents and trailing entries do not 
participate
             // in conversion, matching PyArrow's ordered pair constructor for 
ROW.
-            let values = arrow_select::take::take(
-                input.values().as_ref(),
+            encoded::construct_selected(
+                input.values(),
                 &arrow_array::UInt64Array::from(indices),
-                None,
+                field.data_type(),
+                |array, target| cast_values(array, target, 
CastMode::Constructor),
             )
-            .map_err(|error| invalid(error.to_string()))?;
-            cast_values(&values, field.data_type(), CastMode::Constructor)
         })
         .collect::<crate::Result<Vec<_>>>()?;
     Ok(Arc::new(
@@ -451,8 +506,13 @@ fn constructor_map(
             .columns()
             .iter()
             .map(|column| {
-                let visible = constructor_child(column, input.nulls(), 1, 
CastMode::Constructor)?;
-                cast_values(&visible, fields[1].data_type(), 
CastMode::Constructor)
+                cast_child(
+                    column,
+                    fields[1].data_type(),
+                    input.nulls(),
+                    1,
+                    CastMode::Constructor,
+                )
             })
             .collect::<crate::Result<Vec<_>>>()?;
         let values = if columns.is_empty() {
@@ -580,21 +640,23 @@ fn cast_list<O: OffsetSizeTrait>(
 
 // Rebuilding Python values discards children hidden by a null parent. Safe
 // casts deliberately retain them, since Arrow C++ validates their values too.
-fn constructor_child(
+fn cast_child(
     array: &ArrayRef,
+    target: &DataType,
     nulls: Option<&arrow_buffer::NullBuffer>,
     width: usize,
     mode: CastMode,
 ) -> crate::Result<ArrayRef> {
     if mode != CastMode::Constructor || nulls.is_none_or(|n| n.null_count() == 
0) {
-        return Ok(array.clone());
+        return cast_values(array, target, mode);
     }
     let nulls = nulls.unwrap();
     let indices = arrow_array::UInt64Array::from_iter(
         (0..array.len()).map(|i| nulls.is_valid(i / width).then_some(i as 
u64)),
     );
-    arrow_select::take::take(array.as_ref(), &indices, None)
-        .map_err(|error| invalid(error.to_string()))
+    encoded::construct_selected(array, &indices, target, |array, target| {
+        cast_values(array, target, CastMode::Constructor)
+    })
 }
 
 fn constructor_offsets<O: OffsetSizeTrait>(
@@ -706,7 +768,22 @@ fn cast_to_string(array: &ArrayRef, target: &DataType) -> 
crate::Result<ArrayRef
         safe: false,
         ..Default::default()
     };
+    if matches!(array.data_type(), DataType::FixedSizeBinary(_)) {
+        // Fixed and variable width byte arrays carry the same UTF-8 values.
+        // Arrow Rust lacks the direct fixed-binary to STRING kernel.
+        let binary = 
super::write_batch_normalize::normalize_write_array(array, &DataType::Binary)?;
+        return cast_to_string(&binary, target);
+    }
     let floats: Option<StringArray> = match array.data_type() {
+        DataType::Float16 => Some(
+            array
+                .as_any()
+                .downcast_ref::<arrow_array::Float16Array>()
+                .unwrap()
+                .iter()
+                .map(|value| value.map(|value| format_float(value.to_f64())))
+                .collect(),
+        ),
         DataType::Float32 => Some(
             array
                 .as_any()
@@ -1365,6 +1442,12 @@ fn cast_primitive(array: &ArrayRef, target: &DataType, 
mode: CastMode) -> crate:
             return cast_float_decimal(array, *precision, *scale);
         }
     }
+    if source.is_decimal() && matches!(target, DataType::Decimal128(..)) {
+        return decimal::cast_exact_decimal(array, target);
+    }
+    if source.is_decimal() && matches!(target, DataType::Float32 | 
DataType::Float64) {
+        return decimal::cast_to_floating(array, target);
+    }
     if mode == CastMode::Constructor {
         if let Some(converted) = coerce_temporal(array, target)? {
             return Ok(converted);
@@ -1467,6 +1550,12 @@ fn cast_primitive(array: &ArrayRef, target: &DataType, 
mode: CastMode) -> crate:
     Ok(converted)
 }
 
+#[cfg(test)]
+mod container_tests;
+
+#[cfg(test)]
+mod scalar_tests;
+
 #[cfg(test)]
 mod tests {
     use super::*;
diff --git a/crates/paimon/src/table/update_input/container_tests.rs 
b/crates/paimon/src/table/update_input/container_tests.rs
new file mode 100644
index 00000000..85dd4186
--- /dev/null
+++ b/crates/paimon/src/table/update_input/container_tests.rs
@@ -0,0 +1,240 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use super::*;
+use arrow_array::{types::Int8Type, DictionaryArray, Int32Array, Int8Array};
+use arrow_buffer::NullBuffer;
+use arrow_schema::Field;
+
+fn map(keys: ArrayRef, values: ArrayRef, lengths: &[usize], valid: &[bool]) -> 
ArrayRef {
+    let entries = StructArray::from(vec![
+        (
+            Arc::new(Field::new("encoded_key", keys.data_type().clone(), 
false)),
+            keys,
+        ),
+        (
+            Arc::new(Field::new(
+                "encoded_value",
+                values.data_type().clone(),
+                true,
+            )),
+            values,
+        ),
+    ]);
+    Arc::new(MapArray::new(
+        Arc::new(Field::new("pairs", entries.data_type().clone(), false)),
+        OffsetBuffer::from_lengths(lengths.iter().copied()),
+        entries,
+        Some(NullBuffer::from(valid.to_vec())),
+        false,
+    ))
+}
+
+fn pair_type(key: DataType, value: DataType) -> DataType {
+    DataType::Struct(
+        vec![
+            Field::new("label", key, true),
+            Field::new("amount", value, true),
+        ]
+        .into(),
+    )
+}
+
+#[test]
+fn map_to_array_rows_preserves_aliases_slices_empty_and_null_values() {
+    let input = map(
+        Arc::new(StringArray::from(vec!["unused", "a", "b", "z"])),
+        Arc::new(Int32Array::from(vec![Some(-1), Some(1), None, Some(4)])),
+        &[1, 2, 0, 0, 1],
+        &[true, true, false, true, true],
+    )
+    .slice(1, 4);
+    let fields = pair_type(DataType::Utf8, DataType::Int64);
+    let item = Arc::new(Field::new("element", fields.clone(), false));
+    for target in [
+        DataType::List(item.clone()),
+        DataType::LargeList(item.clone()),
+    ] {
+        let output = cast_update_value(&input, &target, 
CastMode::Assignment).unwrap();
+        assert_eq!(output.data_type(), &target);
+        assert!(output.is_null(1));
+        // Normalize only the container to inspect both offset widths.
+        let output = arrow_cast::cast(output.as_ref(), 
&DataType::List(item.clone())).unwrap();
+        let output = output.as_any().downcast_ref::<ListArray>().unwrap();
+        assert_eq!(output.value_offsets(), &[0, 2, 2, 2, 3]);
+        let entries = output
+            .values()
+            .as_any()
+            .downcast_ref::<StructArray>()
+            .unwrap();
+        assert_eq!(entries.data_type(), &fields);
+        assert_eq!(
+            entries.column(0).to_data(),
+            StringArray::from(vec!["a", "b", "z"]).to_data()
+        );
+        assert_eq!(
+            entries.column(1).to_data(),
+            Int64Array::from(vec![Some(1), None, Some(4)]).to_data()
+        );
+    }
+}
+
+#[test]
+fn map_to_array_recurses_through_rows_lists_and_dictionaries() {
+    let dictionary: ArrayRef = Arc::new(
+        DictionaryArray::<Int8Type>::try_new(
+            Int8Array::from(vec![0, 1]),
+            Arc::new(StringArray::from(vec!["42", "7"])),
+        )
+        .unwrap(),
+    );
+    let input = map(
+        Arc::new(StringArray::from(vec!["a", "b"])),
+        dictionary,
+        &[1, 1],
+        &[true, true],
+    );
+    let target = DataType::List(Arc::new(Field::new(
+        "item",
+        pair_type(DataType::Utf8, DataType::Int32),
+        true,
+    )));
+    let expected = cast_update_value(&input, &target, 
CastMode::Assignment).unwrap();
+    let outer_row: ArrayRef = Arc::new(StructArray::from(vec![(
+        Arc::new(Field::new("nested", input.data_type().clone(), true)),
+        input.clone(),
+    )]));
+    let result = cast_update_value(
+        &outer_row,
+        &DataType::Struct(vec![Field::new("nested", target.clone(), 
true)].into()),
+        CastMode::Assignment,
+    )
+    .unwrap();
+    assert_eq!(
+        result
+            .as_any()
+            .downcast_ref::<StructArray>()
+            .unwrap()
+            .column(0)
+            .to_data(),
+        expected.to_data()
+    );
+    let outer_list: ArrayRef = Arc::new(ListArray::new(
+        Arc::new(Field::new("item", input.data_type().clone(), true)),
+        OffsetBuffer::from_lengths([2]),
+        input.clone(),
+        None,
+    ));
+    let result = cast_update_value(
+        &outer_list,
+        &DataType::List(Arc::new(Field::new("item", target.clone(), true))),
+        CastMode::Assignment,
+    )
+    .unwrap();
+    assert_eq!(
+        result
+            .as_any()
+            .downcast_ref::<ListArray>()
+            .unwrap()
+            .values()
+            .to_data(),
+        expected.to_data()
+    );
+    let encoded: ArrayRef =
+        Arc::new(DictionaryArray::<Int8Type>::try_new(Int8Array::from(vec![0, 
1]), input).unwrap());
+    assert_eq!(
+        cast_update_value(&encoded, &target, CastMode::Assignment)
+            .unwrap()
+            .to_data(),
+        expected.to_data()
+    );
+}
+
+#[test]
+fn map_to_array_keeps_whole_column_constructor_visibility() {
+    let input = map(
+        Arc::new(StringArray::from(vec!["a", "hidden"])),
+        Arc::new(Int64Array::from(vec![7, i64::MAX])),
+        &[1, 1, 0],
+        &[true, false, true],
+    );
+    let target = DataType::List(Arc::new(Field::new(
+        "item",
+        pair_type(DataType::Utf8, DataType::Int32),
+        true,
+    )));
+    assert!(cast_update_value(&input, &target, CastMode::Assignment).is_err());
+    let output = cast_update_value(&input, &target, 
CastMode::RowUpdate).unwrap();
+    let output = output.as_any().downcast_ref::<ListArray>().unwrap();
+    assert_eq!(output.value_offsets(), &[0, 1, 1, 1]);
+    assert!(output.is_null(1));
+    let entries = output
+        .values()
+        .as_any()
+        .downcast_ref::<StructArray>()
+        .unwrap();
+    assert_eq!(
+        entries.column(1).to_data(),
+        Int32Array::from(vec![7]).to_data()
+    );
+    let visible = map(
+        Arc::new(StringArray::from(vec!["bad"])),
+        Arc::new(Int64Array::from(vec![i64::MAX])),
+        &[1],
+        &[true],
+    );
+    assert!(cast_update_value(&visible, &target, 
CastMode::RowUpdate).is_err());
+}
+
+#[test]
+fn map_entries_require_two_fields_and_actual_child_nullability() {
+    let input = map(
+        Arc::new(StringArray::from(vec!["a"])),
+        Arc::new(Int32Array::from(vec![7])),
+        &[1],
+        &[true],
+    );
+    for size in [0, 1, 3] {
+        let fields = (0..size)
+            .map(|i| Field::new(format!("f{i}"), DataType::Utf8, true))
+            .collect::<Vec<_>>();
+        let target = DataType::List(Arc::new(Field::new(
+            "item",
+            DataType::Struct(fields.into()),
+            true,
+        )));
+        assert!(cast_update_value(&input, &target, 
CastMode::Assignment).is_err());
+    }
+    let fields = vec![
+        Field::new("k", DataType::Utf8, false),
+        Field::new("v", DataType::Int32, false),
+    ];
+    let target = DataType::List(Arc::new(Field::new(
+        "item",
+        DataType::Struct(fields.into()),
+        false,
+    )));
+    // A nullable source value FIELD with non-null actual values can be cast.
+    assert!(cast_update_value(&input, &target, CastMode::Assignment).is_ok());
+    let null_value = map(
+        Arc::new(StringArray::from(vec!["a"])),
+        Arc::new(Int32Array::from(vec![None])),
+        &[1],
+        &[true],
+    );
+    assert!(cast_update_value(&null_value, &target, 
CastMode::Assignment).is_err());
+}
diff --git a/crates/paimon/src/table/update_input/decimal.rs 
b/crates/paimon/src/table/update_input/decimal.rs
new file mode 100644
index 00000000..609d47e7
--- /dev/null
+++ b/crates/paimon/src/table/update_input/decimal.rs
@@ -0,0 +1,239 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+//! Exact DECIMAL input rescaling shared by updates and logical match keys.
+
+use arrow_array::{cast::AsArray, ArrayRef, Decimal128Array, Float32Array, 
Float64Array};
+use arrow_schema::DataType;
+use std::sync::Arc;
+
+use super::{invalid, parse_decimal_text};
+
+fn precision_and_scale(array: &ArrayRef) -> (u8, i8) {
+    match array.data_type() {
+        DataType::Decimal32(p, s)
+        | DataType::Decimal64(p, s)
+        | DataType::Decimal128(p, s)
+        | DataType::Decimal256(p, s) => (*p, *s),
+        _ => unreachable!("validated DECIMAL input"),
+    }
+}
+
+fn map_decimal<T>(
+    array: &ArrayRef,
+    parse: impl Fn(&str) -> crate::Result<T>,
+) -> crate::Result<Vec<Option<T>>> {
+    let (_, scale) = precision_and_scale(array);
+    // Preserve the full coefficient. A generic display can clip digits to the
+    // declared source precision for foreign buffers containing more digits.
+    macro_rules! values {
+        ($ty:ty) => {
+            array
+                .as_primitive::<$ty>()
+                .iter()
+                .map(|value| {
+                    value
+                        .map(|value| parse(&format!("{value}e{}", 
-i16::from(scale))))
+                        .transpose()
+                })
+                .collect()
+        };
+    }
+    match array.data_type() {
+        DataType::Decimal32(..) => values!(arrow_array::types::Decimal32Type),
+        DataType::Decimal64(..) => values!(arrow_array::types::Decimal64Type),
+        DataType::Decimal128(..) => 
values!(arrow_array::types::Decimal128Type),
+        DataType::Decimal256(..) => 
values!(arrow_array::types::Decimal256Type),
+        _ => unreachable!("validated DECIMAL input"),
+    }
+}
+
+pub(super) fn cast_to_floating(array: &ArrayRef, target: &DataType) -> 
crate::Result<ArrayRef> {
+    let (precision, scale) = precision_and_scale(array);
+    if precision <= 38 && scale >= 0 && scale as u8 <= precision {
+        let canonical = cast_exact_decimal(array, 
&DataType::Decimal128(precision, scale))?;
+        return crate::arrow::cast_decimal_to_floating(
+            &canonical,
+            u32::from(precision),
+            scale as u32,
+            target,
+        );
+    }
+    // External Arrow decimals can exceed Paimon's logical precision or use a
+    // negative scale. They still update FLOAT/DOUBLE columns via the exact
+    // decimal value, without first rounding an oversized coefficient to f64.
+    let values = map_decimal(array, |text| {
+        text.parse::<f64>()
+            .map_err(|_| invalid("Invalid decimal floating assignment"))
+    })?;
+    Ok(match target {
+        DataType::Float32 => Arc::new(Float32Array::from_iter(
+            values.into_iter().map(|v| v.map(|v| v as f32)),
+        )) as ArrayRef,
+        DataType::Float64 => Arc::new(Float64Array::from(values)),
+        _ => unreachable!("validated DECIMAL floating target"),
+    })
+}
+
+pub(super) fn cast_exact_decimal(array: &ArrayRef, target: &DataType) -> 
crate::Result<ArrayRef> {
+    let (_, source_scale) = precision_and_scale(array);
+    let DataType::Decimal128(target_precision, target_scale) = target else {
+        unreachable!("validated DECIMAL target")
+    };
+    let text_fallback = || {
+        // A zero or NULL can cross scales beyond the rescaler's power table.
+        // The exact text parser still rejects fractional loss and overflow.
+        let values = map_decimal(array, |text| {
+            parse_decimal_text(text, *target_precision, *target_scale)
+        })?;
+        Decimal128Array::from(values)
+            .with_precision_and_scale(*target_precision, *target_scale)
+            .map(|array| Arc::new(array) as ArrayRef)
+            .map_err(|error| invalid(error.to_string()))
+    };
+    let difference = i16::from(*target_scale) - i16::from(source_scale);
+    if !(-38..=38).contains(&difference) {
+        // Arrow subtracts scales in i8 and uses a Decimal128 power table.
+        // Check before entering the kernel, including its reverse cast.
+        return text_fallback();
+    }
+    let options = arrow_cast::CastOptions {
+        safe: false,
+        ..Default::default()
+    };
+    let exact = || {
+        let converted = arrow_cast::cast_with_options(array.as_ref(), target, 
&options)
+            .map_err(|error| invalid(error.to_string()))?;
+        let restored =
+            arrow_cast::cast_with_options(converted.as_ref(), 
array.data_type(), &options)
+                .map_err(|error| invalid(error.to_string()))?;
+        if restored.to_data() != array.to_data() {
+            return Err(invalid("Decimal assignment would lose scale 
precision"));
+        }
+        Ok(converted)
+    };
+    exact().or_else(|_| text_fallback())
+}
+
+#[cfg(test)]
+mod tests {
+    use super::super::{cast_update_value, CastMode};
+    use super::*;
+    use arrow_array::{Array, Decimal128Array, Decimal256Array, Decimal32Array, 
Decimal64Array};
+    use arrow_buffer::i256;
+    use std::sync::Arc;
+
+    fn decimal(data_type: &DataType, value: i128) -> ArrayRef {
+        match data_type {
+            DataType::Decimal32(p, s) => Arc::new(
+                Decimal32Array::from(vec![Some(value as i32), None])
+                    .with_precision_and_scale(*p, *s)
+                    .unwrap(),
+            ),
+            DataType::Decimal64(p, s) => Arc::new(
+                Decimal64Array::from(vec![Some(value as i64), None])
+                    .with_precision_and_scale(*p, *s)
+                    .unwrap(),
+            ),
+            DataType::Decimal128(p, s) => Arc::new(
+                Decimal128Array::from(vec![Some(value), None])
+                    .with_precision_and_scale(*p, *s)
+                    .unwrap(),
+            ),
+            DataType::Decimal256(p, s) => Arc::new(
+                Decimal256Array::from(vec![Some(i256::from_i128(value)), None])
+                    .with_precision_and_scale(*p, *s)
+                    .unwrap(),
+            ),
+            _ => unreachable!(),
+        }
+    }
+
+    #[test]
+    fn zero_and_null_cross_scale_and_width_boundaries_without_overflow() {
+        for (source, target) in [
+            (DataType::Decimal32(9, -128), DataType::Decimal128(38, 38)),
+            (DataType::Decimal64(18, -90), DataType::Decimal128(38, 38)),
+            (DataType::Decimal128(38, -2), DataType::Decimal128(38, 38)),
+            (DataType::Decimal128(38, 38), DataType::Decimal128(20, 0)),
+            (DataType::Decimal256(76, -128), DataType::Decimal128(20, 2)),
+            (DataType::Decimal256(60, -40), DataType::Decimal128(20, 2)),
+            (DataType::Decimal256(76, 76), DataType::Decimal128(38, 38)),
+        ] {
+            for mode in [CastMode::Assignment, CastMode::RowUpdate] {
+                let output = cast_update_value(&decimal(&source, 0), &target, 
mode).unwrap();
+                assert_eq!(
+                    output.to_data(),
+                    decimal(&target, 0).to_data(),
+                    "{source:?} -> {target:?}"
+                );
+                assert!(
+                    cast_update_value(&decimal(&source, 1), &target, 
mode).is_err(),
+                    "{source:?} -> {target:?}"
+                );
+            }
+        }
+    }
+
+    #[test]
+    fn exact_rescaling_preserves_precision_and_nulls_for_every_input_width() {
+        for source in [
+            DataType::Decimal32(9, 3),
+            DataType::Decimal64(18, 3),
+            DataType::Decimal128(38, 3),
+            DataType::Decimal256(76, 3),
+        ] {
+            let target = DataType::Decimal128(10, 2);
+            let output =
+                cast_update_value(&decimal(&source, -1250), &target, 
CastMode::Assignment).unwrap();
+            assert_eq!(output.to_data(), decimal(&target, -125).to_data());
+            assert!(
+                cast_update_value(&decimal(&source, -1251), &target, 
CastMode::Assignment).is_err()
+            );
+            assert!(cast_update_value(
+                &decimal(&source, 1250),
+                &DataType::Decimal128(2, 2),
+                CastMode::Assignment
+            )
+            .is_err());
+        }
+    }
+
+    #[test]
+    fn floating_input_keeps_coefficients_wider_than_decimal128() {
+        let coefficient = format!("1508079088755887087797858531882816082{}", 
"0".repeat(38))
+            .parse::<i256>()
+            .unwrap();
+        let source: ArrayRef = Arc::new(
+            Decimal256Array::from(vec![Some(coefficient), None, 
Some(-coefficient)])
+                .with_precision_and_scale(76, 39)
+                .unwrap(),
+        );
+        for mode in [CastMode::Assignment, CastMode::RowUpdate] {
+            let result = cast_update_value(&source, &DataType::Float64, 
mode).unwrap();
+            assert_eq!(
+                result.to_data(),
+                Float64Array::from(vec![
+                    Some(1.508079088755887e35),
+                    None,
+                    Some(-1.508079088755887e35),
+                ])
+                .to_data()
+            );
+        }
+    }
+}
diff --git a/crates/paimon/src/table/update_input/encoded.rs 
b/crates/paimon/src/table/update_input/encoded.rs
new file mode 100644
index 00000000..4bb99363
--- /dev/null
+++ b/crates/paimon/src/table/update_input/encoded.rs
@@ -0,0 +1,599 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+//! Rebuild selected encoded values after the whole-column safe cast has 
failed.
+
+use arrow_array::cast::AsArray;
+use arrow_array::{Array, ArrayRef, UInt64Array, UnionArray};
+use arrow_schema::DataType;
+use indexmap::IndexMap;
+
+use super::invalid;
+
+/// Decode references before constructor conversion, then restore NULL keys.
+/// Unlike taking a nullable index directly from a Union, taking from the typed
+/// output can represent NULL without assigning it an unrelated active type.
+pub(super) fn construct_dictionary(
+    array: &ArrayRef,
+    target: &DataType,
+    convert: fn(&ArrayRef, &DataType) -> crate::Result<ArrayRef>,
+) -> crate::Result<ArrayRef> {
+    let dictionary = array.as_any_dictionary();
+    let keys = arrow_cast::cast(dictionary.keys(), &DataType::UInt64)
+        .map_err(|error| invalid(error.to_string()))?;
+    let keys = keys.as_any().downcast_ref::<UInt64Array>().unwrap();
+    construct_selected(dictionary.values(), keys, target, convert)
+}
+
+/// Restore missing values only after conversion to a type with a validity
+/// bitmap. Union inputs cannot represent a missing selection independently of
+/// their active child, including below NULL parents or absent ROW fields.
+pub(super) fn construct_selected(
+    array: &ArrayRef,
+    indices: &UInt64Array,
+    target: &DataType,
+    convert: fn(&ArrayRef, &DataType) -> crate::Result<ArrayRef>,
+) -> crate::Result<ArrayRef> {
+    let mut selected = Vec::new();
+    let restore = indices
+        .iter()
+        .map(|key| {
+            key.map(|key| {
+                let index = selected.len() as u64;
+                selected.push(key);
+                index
+            })
+        })
+        .collect::<Vec<_>>();
+    if selected.is_empty() {
+        return Ok(arrow_array::new_null_array(target, indices.len()));
+    }
+    let values = if let Some(union) = 
array.as_any().downcast_ref::<UnionArray>() {
+        // Taking a sparse Union would also take every inactive child. Select
+        // its active values directly, including through dictionary wrappers.
+        construct_union_rows(
+            union,
+            selected.into_iter().map(|row| row as usize),
+            target,
+            convert,
+        )?
+    } else {
+        let values = arrow_select::take::take(array.as_ref(), 
&UInt64Array::from(selected), None)
+            .map_err(|error| invalid(error.to_string()))?;
+        convert(&values, target)?
+    };
+    arrow_select::take::take(values.as_ref(), &UInt64Array::from(restore), 
None)
+        .map_err(|error| invalid(error.to_string()))
+}
+
+/// Only active children participate in the Python-value constructor. A sparse
+/// Union can contain invalid values at inactive positions; a dense Union can
+/// select any child offset repeatedly or in a different order.
+pub(super) fn construct_union(
+    array: &ArrayRef,
+    target: &DataType,
+    convert: fn(&ArrayRef, &DataType) -> crate::Result<ArrayRef>,
+) -> crate::Result<ArrayRef> {
+    let input = array.as_any().downcast_ref::<UnionArray>().unwrap();
+    construct_union_rows(input, 0..input.len(), target, convert)
+}
+
+fn construct_union_rows(
+    input: &UnionArray,
+    rows: impl ExactSizeIterator<Item = usize>,
+    target: &DataType,
+    convert: fn(&ArrayRef, &DataType) -> crate::Result<ArrayRef>,
+) -> crate::Result<ArrayRef> {
+    if rows.len() == 0 {
+        return Ok(arrow_array::new_empty_array(target));
+    }
+    let mut selected = IndexMap::<i8, Vec<u64>>::new();
+    let mut positions = Vec::with_capacity(rows.len());
+    for row in rows {
+        let entry = selected.entry(input.type_id(row));
+        let child = entry.index();
+        let offsets = entry.or_default();
+        positions.push((child, offsets.len()));
+        offsets.push(input.value_offset(row) as u64);
+    }
+    let values = selected
+        .into_iter()
+        .map(|(id, offsets)| {
+            let selected = arrow_select::take::take(
+                input.child(id).as_ref(),
+                &UInt64Array::from(offsets),
+                None,
+            )
+            .map_err(|error| invalid(error.to_string()))?;
+            convert(&selected, target)
+        })
+        .collect::<crate::Result<Vec<_>>>()?;
+    let values = values
+        .iter()
+        .map(|array| array.as_ref())
+        .collect::<Vec<_>>();
+    arrow_select::interleave::interleave(&values, &positions)
+        .map_err(|error| invalid(error.to_string()))
+}
+
+#[cfg(test)]
+mod tests {
+    use super::super::{cast_update_value, CastMode};
+    use super::*;
+    use arrow_array::{
+        types::Int8Type, DictionaryArray, FixedSizeListArray, Float64Array, 
Int32Array, Int64Array,
+        Int8Array, ListArray, MapArray, NullArray, StringArray, StructArray,
+    };
+    use arrow_buffer::{NullBuffer, OffsetBuffer, ScalarBuffer};
+    use arrow_schema::{Field, UnionFields};
+    use std::sync::Arc;
+
+    fn numeric_union(dense: bool) -> ArrayRef {
+        let fields = UnionFields::try_new(
+            [7, 42, 99],
+            [
+                Field::new("small", DataType::Int32, true),
+                Field::new("large", DataType::Int64, true),
+                Field::new("unused", DataType::Utf8, true),
+            ],
+        )
+        .unwrap();
+        let (left, right, offsets) = if dense {
+            (
+                vec![Some(-1), None, Some(2)],
+                vec![Some(3), Some(1), Some(i64::MAX)],
+                Some(ScalarBuffer::from(vec![0, 1, 2, 0, 1, 2])),
+            )
+        } else {
+            (
+                vec![Some(-1), Some(0), Some(2), Some(0), None, Some(0)],
+                vec![
+                    Some(i64::MAX),
+                    Some(1),
+                    Some(i64::MAX),
+                    Some(3),
+                    Some(i64::MAX),
+                    Some(i64::MAX),
+                ],
+                None,
+            )
+        };
+        Arc::new(
+            UnionArray::try_new(
+                fields,
+                ScalarBuffer::from(vec![7, 42, 7, 42, 7, 42]),
+                offsets,
+                vec![
+                    Arc::new(Int32Array::from(left)),
+                    Arc::new(Int64Array::from(right)),
+                    Arc::new(StringArray::from(vec![
+                        "invalid";
+                        if dense { 1 } else { 6 }
+                    ])),
+                ],
+            )
+            .unwrap(),
+        )
+    }
+
+    #[test]
+    fn active_values_keep_slices_nulls_and_nonstandard_type_ids() {
+        for dense in [true, false] {
+            let input = numeric_union(dense);
+            let slice = input.slice(1, 4);
+            assert_eq!(slice.null_count(), 0);
+            assert!(cast_update_value(&slice, &DataType::Int32, 
CastMode::Assignment).is_err());
+            let result = cast_update_value(&slice, &DataType::Int32, 
CastMode::RowUpdate).unwrap();
+            assert_eq!(
+                result.to_data(),
+                Int32Array::from(vec![Some(1), Some(2), Some(3), 
None]).to_data()
+            );
+            // The same wide value must fail as soon as its type/offset is 
active.
+            assert!(cast_update_value(&input, &DataType::Int32, 
CastMode::RowUpdate).is_err());
+            assert_eq!(
+                cast_update_value(&slice.slice(0, 0), &DataType::Int32, 
CastMode::RowUpdate)
+                    .unwrap()
+                    .len(),
+                0
+            );
+        }
+    }
+
+    #[test]
+    fn dense_offsets_can_repeat_and_reorder_child_values() {
+        let input = numeric_union(true);
+        let fields = match input.data_type() {
+            DataType::Union(fields, _) => fields.clone(),
+            _ => unreachable!(),
+        };
+        let input = input.as_any().downcast_ref::<UnionArray>().unwrap();
+        let reordered: ArrayRef = Arc::new(
+            UnionArray::try_new(
+                fields,
+                ScalarBuffer::from(vec![7, 7, 42, 7]),
+                Some(ScalarBuffer::from(vec![2, 0, 1, 2])),
+                vec![
+                    input.child(7).clone(),
+                    input.child(42).clone(),
+                    input.child(99).clone(),
+                ],
+            )
+            .unwrap(),
+        );
+        assert_eq!(
+            cast_update_value(&reordered, &DataType::Int32, 
CastMode::RowUpdate)
+                .unwrap()
+                .to_data(),
+            Int32Array::from(vec![2, -1, 1, 2]).to_data(),
+        );
+    }
+
+    #[test]
+    fn every_selected_child_uses_constructor_semantics() {
+        let fields = UnionFields::try_new(
+            [3, 8],
+            [
+                Field::new("number", DataType::Float64, true),
+                Field::new("text", DataType::Utf8, true),
+            ],
+        )
+        .unwrap();
+        let input: ArrayRef = Arc::new(
+            UnionArray::try_new(
+                fields,
+                ScalarBuffer::from(vec![3, 8]),
+                Some(ScalarBuffer::from(vec![0, 0])),
+                vec![
+                    Arc::new(Float64Array::from(vec![1.5])),
+                    Arc::new(StringArray::from(vec!["42"])),
+                ],
+            )
+            .unwrap(),
+        );
+        assert_eq!(
+            cast_update_value(&input.slice(0, 1), &DataType::Int32, 
CastMode::RowUpdate)
+                .unwrap()
+                .to_data(),
+            Int32Array::from(vec![1]).to_data(),
+        );
+        // A numeric text child must not get a fresh safe-cast attempt after
+        // the Union's whole-column safe cast has already failed.
+        assert!(cast_update_value(&input, &DataType::Int32, 
CastMode::RowUpdate).is_err());
+        assert!(
+            cast_update_value(&input.slice(0, 1), &DataType::Utf8, 
CastMode::RowUpdate).is_err()
+        );
+    }
+
+    #[test]
+    fn constructors_recurse_through_rows_and_arrays() {
+        for dense in [true, false] {
+            let input = numeric_union(dense).slice(1, 4);
+            let rows: ArrayRef = Arc::new(StructArray::from(vec![(
+                Arc::new(Field::new("n", input.data_type().clone(), true)),
+                input.clone(),
+            )]));
+            let target = DataType::Struct(vec![Field::new("n", 
DataType::Int32, true)].into());
+            let output = cast_update_value(&rows, &target, 
CastMode::RowUpdate).unwrap();
+            let expected = Int32Array::from(vec![Some(1), Some(2), Some(3), 
None]);
+            assert_eq!(
+                output
+                    .as_any()
+                    .downcast_ref::<StructArray>()
+                    .unwrap()
+                    .column(0)
+                    .to_data(),
+                expected.to_data()
+            );
+            let lists: ArrayRef = Arc::new(ListArray::new(
+                Arc::new(Field::new("item", input.data_type().clone(), true)),
+                OffsetBuffer::from_lengths([2, 2]),
+                input,
+                None,
+            ));
+            let target = DataType::List(Arc::new(Field::new("item", 
DataType::Int32, true)));
+            let output = cast_update_value(&lists, &target, 
CastMode::RowUpdate).unwrap();
+            assert_eq!(
+                output
+                    .as_any()
+                    .downcast_ref::<ListArray>()
+                    .unwrap()
+                    .values()
+                    .to_data(),
+                expected.to_data()
+            );
+        }
+    }
+
+    fn pair_union() -> ArrayRef {
+        let pairs: ArrayRef = Arc::new(ListArray::new(
+            Arc::new(Field::new("item", DataType::Int32, true)),
+            OffsetBuffer::from_lengths([2]),
+            Arc::new(Int32Array::from(vec![1, 2])),
+            None,
+        ));
+        let pairs: ArrayRef = Arc::new(ListArray::new(
+            Arc::new(Field::new("item", pairs.data_type().clone(), true)),
+            OffsetBuffer::from_lengths([1]),
+            pairs,
+            None,
+        ));
+        Arc::new(
+            UnionArray::try_new(
+                UnionFields::try_new(
+                    [7, 42],
+                    [
+                        Field::new("pairs", pairs.data_type().clone(), true),
+                        Field::new("empty", DataType::Null, true),
+                    ],
+                )
+                .unwrap(),
+                ScalarBuffer::from(vec![7, 42]),
+                Some(ScalarBuffer::from(vec![0, 0])),
+                vec![pairs, Arc::new(NullArray::new(1))],
+            )
+            .unwrap(),
+        )
+    }
+
+    fn map_type(key: DataType) -> DataType {
+        DataType::Map(
+            Arc::new(Field::new(
+                "entries",
+                DataType::Struct(
+                    vec![
+                        Field::new("key", key, false),
+                        Field::new("value", DataType::Int64, true),
+                    ]
+                    .into(),
+                ),
+                false,
+            )),
+            false,
+        )
+    }
+
+    #[test]
+    fn map_pair_conversion_is_root_only_through_encoding_wrappers() {
+        let input = pair_union();
+        let target = map_type(DataType::Int32);
+        let output = cast_update_value(&input, &target, 
CastMode::RowUpdate).unwrap();
+        let output = output.as_any().downcast_ref::<MapArray>().unwrap();
+        assert_eq!(output.value_offsets(), &[0, 1, 1]);
+        assert!(output.is_null(1));
+        assert_eq!(
+            output.values().to_data(),
+            Int64Array::from(vec![2]).to_data()
+        );
+        let encoded: ArrayRef = Arc::new(
+            DictionaryArray::<Int8Type>::try_new(Int8Array::from(vec![0, 1]), 
input.clone())
+                .unwrap(),
+        );
+        assert_eq!(
+            cast_update_value(&encoded, &target, CastMode::RowUpdate)
+                .unwrap()
+                .to_data(),
+            output.to_data()
+        );
+        let nested: ArrayRef = Arc::new(StructArray::from(vec![(
+            Arc::new(Field::new("m", input.data_type().clone(), true)),
+            input,
+        )]));
+        let target = DataType::Struct(vec![Field::new("m", target, 
true)].into());
+        assert!(cast_update_value(&nested, &target, 
CastMode::RowUpdate).is_err());
+    }
+
+    #[test]
+    fn dictionary_union_preserves_null_indices_and_active_null_values() {
+        for dense in [true, false] {
+            let values = numeric_union(dense).slice(1, 4);
+            let input: ArrayRef = Arc::new(
+                DictionaryArray::<Int8Type>::try_new(
+                    Int8Array::from(vec![Some(3), None, Some(0), Some(1)]),
+                    values.clone(),
+                )
+                .unwrap(),
+            );
+            let output = cast_update_value(&input, &DataType::Int32, 
CastMode::RowUpdate).unwrap();
+            assert_eq!(
+                output.to_data(),
+                Int32Array::from(vec![None, None, Some(1), Some(2)]).to_data()
+            );
+            let input: ArrayRef = Arc::new(
+                
DictionaryArray::<Int8Type>::try_new(Int8Array::from(vec![None, None]), values)
+                    .unwrap(),
+            );
+            assert_eq!(
+                cast_update_value(&input, &DataType::Int32, 
CastMode::RowUpdate)
+                    .unwrap()
+                    .null_count(),
+                2
+            );
+        }
+        let input: ArrayRef = Arc::new(
+            DictionaryArray::<Int8Type>::try_new(
+                Int8Array::from(vec![None, Some(0), Some(1), None]),
+                pair_union(),
+            )
+            .unwrap(),
+        );
+        let output =
+            cast_update_value(&input, &map_type(DataType::Int32), 
CastMode::RowUpdate).unwrap();
+        let output = output.as_any().downcast_ref::<MapArray>().unwrap();
+        assert_eq!(output.value_offsets(), &[0, 0, 1, 1, 1]);
+        assert_eq!(output.null_count(), 3);
+    }
+
+    #[test]
+    fn union_dict_values_allow_explicit_null_map_values() {
+        let row: ArrayRef = Arc::new(StructArray::from(vec![
+            (
+                Arc::new(Field::new("a", DataType::Int32, true)),
+                Arc::new(Int32Array::from(vec![Some(1)])) as ArrayRef,
+            ),
+            (
+                Arc::new(Field::new("b", DataType::Int32, true)),
+                Arc::new(Int32Array::from(vec![None])) as ArrayRef,
+            ),
+        ]));
+        let target = map_type(DataType::Utf8);
+        assert!(cast_update_value(&row, &target, 
CastMode::RowUpdate).is_err());
+        let input: ArrayRef = Arc::new(
+            UnionArray::try_new(
+                UnionFields::try_new([42], [Field::new("row", 
row.data_type().clone(), true)])
+                    .unwrap(),
+                ScalarBuffer::from(vec![42]),
+                Some(ScalarBuffer::from(vec![0])),
+                vec![row],
+            )
+            .unwrap(),
+        );
+        let output = cast_update_value(&input, &target, 
CastMode::RowUpdate).unwrap();
+        let output = output.as_any().downcast_ref::<MapArray>().unwrap();
+        assert_eq!(
+            output.keys().to_data(),
+            StringArray::from(vec!["a", "b"]).to_data()
+        );
+        assert_eq!(
+            output.values().to_data(),
+            Int64Array::from(vec![Some(1), None]).to_data()
+        );
+    }
+
+    #[test]
+    fn null_parents_hide_union_values_before_constructor_conversion() {
+        for dense in [false, true] {
+            let input: ArrayRef = Arc::new(
+                UnionArray::try_new(
+                    UnionFields::try_new(
+                        [7, 42],
+                        [
+                            Field::new("number", DataType::Int32, true),
+                            Field::new("text", DataType::Utf8, true),
+                        ],
+                    )
+                    .unwrap(),
+                    ScalarBuffer::from(vec![42, 7, 7, 42]),
+                    dense.then(|| ScalarBuffer::from(vec![0, 0, 1, 1])),
+                    if dense {
+                        vec![
+                            Arc::new(Int32Array::from(vec![1, 2])) as ArrayRef,
+                            Arc::new(StringArray::from(vec!["hidden", "also 
hidden"])),
+                        ]
+                    } else {
+                        vec![
+                            Arc::new(Int32Array::from(vec![0, 1, 2, 0])) as 
ArrayRef,
+                            Arc::new(StringArray::from(vec!["hidden"; 4])),
+                        ]
+                    },
+                )
+                .unwrap(),
+            );
+            let dictionary: ArrayRef = Arc::new(
+                DictionaryArray::<Int8Type>::try_new(
+                    Int8Array::from(vec![0, 1, 2, 3]),
+                    input.clone(),
+                )
+                .unwrap(),
+            );
+            for input in [input, dictionary] {
+                let parents = NullBuffer::from(vec![false, true, true, false]);
+                let rows: ArrayRef = Arc::new(StructArray::new(
+                    vec![Field::new("n", input.data_type().clone(), 
true)].into(),
+                    vec![input.clone()],
+                    Some(parents.clone()),
+                ));
+                let target = DataType::Struct(vec![Field::new("n", 
DataType::Int32, true)].into());
+                assert!(cast_update_value(&rows, &target, 
CastMode::Assignment).is_err());
+                let output = cast_update_value(&rows, &target, 
CastMode::RowUpdate).unwrap();
+                let output = output.as_struct();
+                assert_eq!(output.nulls(), Some(&parents));
+                assert_eq!(
+                    output.column(0).to_data(),
+                    Int32Array::from(vec![None, Some(1), Some(2), 
None]).to_data()
+                );
+
+                let lists: ArrayRef = Arc::new(FixedSizeListArray::new(
+                    Arc::new(Field::new("item", input.data_type().clone(), 
true)),
+                    1,
+                    input,
+                    Some(parents.clone()),
+                ));
+                let target = DataType::List(Arc::new(Field::new("item", 
DataType::Int32, true)));
+                let output = cast_update_value(&lists, &target, 
CastMode::RowUpdate).unwrap();
+                let output = output.as_list::<i32>();
+                assert_eq!(output.nulls(), Some(&parents));
+                assert_eq!(
+                    output.value(1).to_data(),
+                    Int32Array::from(vec![1]).to_data()
+                );
+                assert_eq!(
+                    output.value(2).to_data(),
+                    Int32Array::from(vec![2]).to_data()
+                );
+            }
+        }
+    }
+
+    #[test]
+    fn map_union_values_leave_absent_row_fields_null() {
+        for dense in [false, true] {
+            let values: ArrayRef = Arc::new(
+                UnionArray::try_new(
+                    UnionFields::try_new([7], [Field::new("number", 
DataType::Int32, true)])
+                        .unwrap(),
+                    ScalarBuffer::from(vec![7; 4]),
+                    dense.then(|| ScalarBuffer::from(vec![0, 1, 2, 3])),
+                    vec![Arc::new(Int32Array::from(vec![1, 2, 3, 4]))],
+                )
+                .unwrap(),
+            );
+            let entries = StructArray::new(
+                vec![
+                    Field::new("key", DataType::Utf8, false),
+                    Field::new("value", values.data_type().clone(), true),
+                ]
+                .into(),
+                vec![Arc::new(StringArray::from(vec!["n"; 4])), values],
+                None,
+            );
+            let input: ArrayRef = Arc::new(MapArray::new(
+                Arc::new(Field::new("entries", entries.data_type().clone(), 
false)),
+                OffsetBuffer::from_lengths([1; 4]),
+                entries,
+                None,
+                false,
+            ));
+            let target = DataType::Struct(
+                vec![
+                    Field::new("n", DataType::Int32, true),
+                    Field::new("v", DataType::Utf8, true),
+                ]
+                .into(),
+            );
+            let output = cast_update_value(&input, &target, 
CastMode::RowUpdate).unwrap();
+            let output = output.as_struct();
+            assert_eq!(
+                output.column(0).to_data(),
+                Int32Array::from(vec![1, 2, 3, 4]).to_data()
+            );
+            assert_eq!(
+                output.column(1).to_data(),
+                StringArray::from(vec![None::<&str>; 4]).to_data()
+            );
+        }
+    }
+}
diff --git a/crates/paimon/src/table/update_input/scalar_tests.rs 
b/crates/paimon/src/table/update_input/scalar_tests.rs
new file mode 100644
index 00000000..56f06deb
--- /dev/null
+++ b/crates/paimon/src/table/update_input/scalar_tests.rs
@@ -0,0 +1,207 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use super::*;
+use arrow_array::{Decimal128Array, Decimal256Array, FixedSizeBinaryArray};
+use arrow_buffer::{Buffer, NullBuffer};
+use arrow_schema::Field;
+
+#[test]
+fn fixed_binary_strings_keep_utf8_nulls_slices_and_empty_values() {
+    let input: ArrayRef = Arc::new(
+        FixedSizeBinaryArray::try_from_sparse_iter_with_size(
+            [
+                Some(b"old".as_slice()),
+                Some(b"a\0b".as_slice()),
+                None,
+                Some("中".as_bytes()),
+            ]
+            .into_iter(),
+            3,
+        )
+        .unwrap(),
+    );
+    let input = input.slice(1, 3);
+    for target in [DataType::Utf8, DataType::LargeUtf8] {
+        for mode in [CastMode::Assignment, CastMode::RowUpdate] {
+            let actual = cast_update_value(&input, &target, mode).unwrap();
+            let expected = arrow_cast::cast(
+                &StringArray::from(vec![Some("a\0b"), None, Some("中")]),
+                &target,
+            )
+            .unwrap();
+            assert_eq!(actual.to_data(), expected.to_data());
+        }
+    }
+    let empty: ArrayRef = Arc::new(
+        FixedSizeBinaryArray::try_new(
+            0,
+            Buffer::from_vec(Vec::<u8>::new()),
+            Some(NullBuffer::from(vec![true, false, true])),
+        )
+        .unwrap(),
+    );
+    assert_eq!(
+        cast_update_value(&empty, &DataType::Utf8, CastMode::Assignment)
+            .unwrap()
+            .to_data(),
+        StringArray::from(vec![Some(""), None, Some("")]).to_data()
+    );
+    let invalid: ArrayRef =
+        
Arc::new(FixedSizeBinaryArray::try_from_iter([b"\xffab".as_slice()].into_iter()).unwrap());
+    assert!(cast_update_value(&invalid, &DataType::Utf8, 
CastMode::RowUpdate).is_err());
+    // The adaptation is for text, not a new binary-to-numeric cast.
+    let digits: ArrayRef =
+        
Arc::new(FixedSizeBinaryArray::try_from_iter([b"123".as_slice()].into_iter()).unwrap());
+    assert!(cast_update_value(&digits, &DataType::Int32, 
CastMode::RowUpdate).is_err());
+}
+
+#[test]
+fn half_float_text_uses_all_significant_digits() {
+    let input = Float64Array::from(vec![
+        Some(0.1),
+        None,
+        Some(1.5),
+        Some(-3.25),
+        Some(65504.0),
+        Some(2_f64.powi(-24)),
+    ]);
+    let input = arrow_cast::cast(&input, &DataType::Float16).unwrap();
+    let expected = StringArray::from(vec![
+        Some("0.0999755859375"),
+        None,
+        Some("1.5"),
+        Some("-3.25"),
+        Some("65504"),
+        Some("5.960464477539063e-8"),
+    ]);
+    for mode in [CastMode::Assignment, CastMode::RowUpdate] {
+        assert_eq!(
+            cast_update_value(&input, &DataType::Utf8, mode)
+                .unwrap()
+                .to_data(),
+            expected.to_data()
+        );
+    }
+}
+
+#[test]
+fn scalar_encodings_recurse_inside_containers() {
+    let input: ArrayRef = Arc::new(
+        FixedSizeBinaryArray::try_from_iter([b"abc".as_slice(), 
"中".as_bytes()].into_iter())
+            .unwrap(),
+    );
+    let rows: ArrayRef = Arc::new(StructArray::from(vec![(
+        Arc::new(Field::new("s", input.data_type().clone(), true)),
+        input.clone(),
+    )]));
+    let target = DataType::Struct(vec![Field::new("s", DataType::Utf8, 
true)].into());
+    let output = cast_update_value(&rows, &target, 
CastMode::Assignment).unwrap();
+    let expected = StringArray::from(vec!["abc", "中"]);
+    assert_eq!(
+        output
+            .as_any()
+            .downcast_ref::<StructArray>()
+            .unwrap()
+            .column(0)
+            .to_data(),
+        expected.to_data()
+    );
+    let lists: ArrayRef = Arc::new(ListArray::new(
+        Arc::new(Field::new("item", input.data_type().clone(), true)),
+        OffsetBuffer::from_lengths([2]),
+        input,
+        None,
+    ));
+    let target = DataType::List(Arc::new(Field::new("item", DataType::Utf8, 
true)));
+    let output = cast_update_value(&lists, &target, 
CastMode::Assignment).unwrap();
+    assert_eq!(
+        output
+            .as_any()
+            .downcast_ref::<ListArray>()
+            .unwrap()
+            .values()
+            .to_data(),
+        expected.to_data()
+    );
+}
+
+#[test]
+fn wide_decimal_floating_values_use_java_big_decimal_rounding() {
+    let coefficient = "1508079088755887087797858531882816082"
+        .parse::<i128>()
+        .unwrap();
+    let source: ArrayRef = Arc::new(
+        Decimal128Array::from(vec![Some(coefficient), None, 
Some(-coefficient)])
+            .with_precision_and_scale(38, 1)
+            .unwrap(),
+    );
+    let expected = [
+        Some(1.508079088755887e35),
+        None,
+        Some(-1.508079088755887e35),
+    ];
+    let mut sources = vec![source];
+    for precision in [38, 39, 76] {
+        let wide: ArrayRef = Arc::new(
+            Decimal256Array::from_iter(
+                [Some(coefficient), None, Some(-coefficient)]
+                    .into_iter()
+                    .map(|value| value.map(arrow_buffer::i256::from_i128)),
+            )
+            .with_precision_and_scale(precision, 1)
+            .unwrap(),
+        );
+        sources.push(wide);
+    }
+    for source in sources {
+        for mode in [CastMode::Assignment, CastMode::RowUpdate] {
+            let output = cast_update_value(&source, &DataType::Float64, 
mode).unwrap();
+            assert_eq!(
+                output.to_data(),
+                Float64Array::from(expected.to_vec()).to_data()
+            );
+            let output = cast_update_value(&source, &DataType::Float32, 
mode).unwrap();
+            assert_eq!(
+                output.to_data(),
+                Float32Array::from_iter(expected.into_iter().map(|v| v.map(|v| 
v as f32)))
+                    .to_data()
+            );
+        }
+    }
+}
+
+#[test]
+fn compact_decimal_floating_values_keep_java_long_division() {
+    let coefficient = 9_007_199_254_740_993_i128;
+    let source: ArrayRef = Arc::new(
+        Decimal128Array::from(vec![Some(coefficient), None])
+            .with_precision_and_scale(18, 3)
+            .unwrap(),
+    );
+    let expected = coefficient as f64 / 1000.0;
+    let output = cast_update_value(&source, &DataType::Float64, 
CastMode::Assignment).unwrap();
+    assert_eq!(
+        output.to_data(),
+        Float64Array::from(vec![Some(expected), None]).to_data()
+    );
+    let output = cast_update_value(&source, &DataType::Float32, 
CastMode::Assignment).unwrap();
+    assert_eq!(
+        output.to_data(),
+        Float32Array::from(vec![Some(expected as f32), None]).to_data()
+    );
+}

Reply via email to