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

github-merge-queue[bot] pushed a commit to branch 
gh-readonly-queue/main/pr-5227-a8fc789b3cb8546592299c736e8a89a69adfb0b5
in repository https://gitbox.apache.org/repos/asf/datafusion-comet.git

commit 971380971064082215744f4fc4d86177bdc416c7
Author: Han-Yin Chang <[email protected]>
AuthorDate: Thu Sep 24 00:43:03 2026 +0000

    fix: preserve map field metadata and honor target sorted flag in 
cast_map_to_map (#5227)
    
    cast_map_to_map rebuilt the entries, key and value fields with Field::new, 
which dropped field metadata and target nullability, so data_type() did not 
equal the requested target. serde.rs runs map key and value fields through 
with_parquet_field_id, so a target really can carry PARQUET:field_id. It also 
used the source sorted flag for the result rather than the target's.
    
    Delegate the rename-only case to arrow's cast, which clones the target 
entries field rather than rebuilding it. Build the hand-built case with the 
target sorted flag and target fields, and use try_new instead of new. Guard the 
entries field count before indexing, since that indexing happens before try_new 
and would otherwise panic on a 0 or 1 field target.
    
    Co-authored-by: Claude Opus 5 <[email protected]>
---
 native/spark-expr/src/conversion_funcs/cast.rs | 616 ++++++++++++++++++++++---
 1 file changed, 545 insertions(+), 71 deletions(-)

diff --git a/native/spark-expr/src/conversion_funcs/cast.rs 
b/native/spark-expr/src/conversion_funcs/cast.rs
index 7a9b93ff16..45c3ea678c 100644
--- a/native/spark-expr/src/conversion_funcs/cast.rs
+++ b/native/spark-expr/src/conversion_funcs/cast.rs
@@ -45,8 +45,7 @@ use arrow::array::builder::{GenericStringBuilder, 
StringBuilder};
 use arrow::array::{
     new_null_array, BinaryBuilder, GenericByteArray, ListArray, MapArray, 
StringArray, StructArray,
 };
-use arrow::datatypes::{DataType, Schema};
-use arrow::datatypes::{Field, Fields, GenericBinaryType};
+use arrow::datatypes::{DataType, GenericBinaryType, Schema};
 use arrow::error::ArrowError;
 use arrow::{
     array::{
@@ -490,8 +489,21 @@ fn cast_struct_to_struct(
     }
 }
 
-/// Cast between map types, handling field name differences between Parquet 
("key_value")
-/// and Spark ("entries") while preserving the map's structure.
+/// Cast between map types, including the relabel case where the child types 
match and only
+/// field names, nullability or metadata differ.
+///
+/// - Rename-only (unchanged key/value types and sort order): delegate to 
arrow's `cast`, which
+///   relabels to the target fields and preserves their metadata with no value 
transformation.
+/// - Otherwise (a child type or the sort flag differs): recurse with Comet's 
`cast_array` for a
+///   changed child and hand-build the result with the target sort flag. 
`try_new` is used so a
+///   malformed target returns `Err` rather than panicking.
+///
+/// Either way the result `data_type()` equals `to_type`.
+///
+/// The target sort flag is copied, not re-derived. `serde.rs` builds every 
map type with
+/// `sorted = false` and the planner propagates that unchanged, so a target 
asking for
+/// `sorted = true` does not arise today. A producer that started emitting one 
would need the flag
+/// recomputed here, because casting the key type can reorder keys.
 fn cast_map_to_map(
     array: &ArrayRef,
     from_type: &DataType,
@@ -506,75 +518,84 @@ fn cast_map_to_map(
     match (from_type, to_type) {
         (
             DataType::Map(from_entries_field, from_sorted),
-            DataType::Map(to_entries_field, _to_sorted),
+            DataType::Map(to_entries_field, to_sorted),
         ) => {
-            // Get the struct types for entries
-            let from_struct_type = from_entries_field.data_type();
-            let to_struct_type = to_entries_field.data_type();
-
-            match (from_struct_type, to_struct_type) {
-                (DataType::Struct(from_fields), DataType::Struct(to_fields)) 
=> {
-                    // Get the key and value types
-                    let from_key_type = from_fields[0].data_type();
-                    let from_value_type = from_fields[1].data_type();
-                    let to_key_type = to_fields[0].data_type();
-                    let to_value_type = to_fields[1].data_type();
-
-                    // Cast keys if needed
-                    let keys = map_array.keys();
-                    let cast_keys = if from_key_type != to_key_type {
-                        cast_array(Arc::clone(keys), to_key_type, 
cast_options)?
-                    } else {
-                        Arc::clone(keys)
-                    };
-
-                    // Cast values if needed
-                    let values = map_array.values();
-                    let cast_values = if from_value_type != to_value_type {
-                        cast_array(Arc::clone(values), to_value_type, 
cast_options)?
-                    } else {
-                        Arc::clone(values)
-                    };
-
-                    // Build the new entries struct with the target field names
-                    let new_key_field = Arc::new(Field::new(
-                        to_fields[0].name(),
-                        to_key_type.clone(),
-                        to_fields[0].is_nullable(),
-                    ));
-                    let new_value_field = Arc::new(Field::new(
-                        to_fields[1].name(),
-                        to_value_type.clone(),
-                        to_fields[1].is_nullable(),
-                    ));
-
-                    let struct_fields = Fields::from(vec![new_key_field, 
new_value_field]);
-                    let entries_struct =
-                        StructArray::new(struct_fields, vec![cast_keys, 
cast_values], None);
-
-                    // Create the new map field with the target name
-                    let new_entries_field = Arc::new(Field::new(
-                        to_entries_field.name(),
-                        DataType::Struct(entries_struct.fields().clone()),
-                        to_entries_field.is_nullable(),
-                    ));
-
-                    // Build the new MapArray
-                    let new_map = MapArray::new(
-                        new_entries_field,
-                        map_array.offsets().clone(),
-                        entries_struct,
-                        map_array.nulls().cloned(),
-                        *from_sorted,
-                    );
-
-                    Ok(Arc::new(new_map))
-                }
-                _ => Err(DataFusionError::Internal(format!(
-                    "Map entries must be structs, got {:?} and {:?}",
-                    from_struct_type, to_struct_type
-                ))),
+            let (from_fields, to_fields) =
+                match (from_entries_field.data_type(), 
to_entries_field.data_type()) {
+                    (DataType::Struct(f), DataType::Struct(t)) => (f, t),
+                    (from_struct_type, to_struct_type) => {
+                        return Err(DataFusionError::Internal(format!(
+                            "Map entries must be structs, got 
{from_struct_type:?} and \
+                             {to_struct_type:?}"
+                        )))
+                    }
+                };
+            // Both field lists are indexed below. This guard is load-bearing 
for a target with 0
+            // or 1 fields, which would otherwise panic on that indexing. A 
target with 3 or more is
+            // already rejected without it, by `MapArray::try_new`'s 
entries-type check on the
+            // delegated path and by `StructArray::try_new` on the hand-built 
one, so there the guard
+            // only makes the error clearer.
+            if to_fields.len() != 2 {
+                return Err(DataFusionError::Internal(format!(
+                    "Map entries struct in the cast target must have exactly 2 
fields \
+                     (key, value), got {}",
+                    to_fields.len()
+                )));
+            }
+            // No matching guard on `from_fields`: a `MapArray` from arrow's 
safe constructors
+            // cannot have any other entries field count. `MapArray::try_new` 
rejects a declared
+            // entries field whose type differs from the entries array, 
`try_new_from_array_data`
+            // requires a two-field entries struct, and 
`ArrayData::validate_child_data` requires
+            // matching child types.
+            let key_type_unchanged = from_fields[0].data_type() == 
to_fields[0].data_type();
+            let value_type_unchanged = from_fields[1].data_type() == 
to_fields[1].data_type();
+
+            // Rename-only path: the key and value types and the sort order 
are unchanged, so only
+            // the field labels, nullability and metadata differ. Delegate to 
arrow's cast, whose
+            // map arm requires matching sort flags and relabels to the target 
fields, preserving
+            // their metadata and values. The hand-built path below produces 
the same array for
+            // this case, so this reuses arrow's kernel rather than behaving 
differently.
+            // `test_cast_map_to_map_both_paths_agree` pins that equivalence.
+            if key_type_unchanged && value_type_unchanged && from_sorted == 
to_sorted {
+                // The eval mode cannot matter here. Both child types are 
unchanged, so arrow casts
+                // each child with `from_type == to_type` and returns it 
untouched without ever
+                // reading `safe`. Use the shared static rather than threading 
the mode through, so
+                // this does not read as if the mode changed the result.
+                return Ok(cast_with_options(array, to_type, &CAST_OPTIONS)?);
             }
+
+            // Otherwise a child type or the sort flag differs. Recurse with 
Comet's Spark-compatible
+            // casts for the changed children and hand-build the result 
carrying the target sort flag.
+            // `try_new` reports a malformed target as `Err` rather than 
panicking.
+            let keys = map_array.keys();
+            let cast_keys = if key_type_unchanged {
+                Arc::clone(keys)
+            } else {
+                cast_array(Arc::clone(keys), to_fields[0].data_type(), 
cast_options)?
+            };
+            let values = map_array.values();
+            let cast_values = if value_type_unchanged {
+                Arc::clone(values)
+            } else {
+                cast_array(Arc::clone(values), to_fields[1].data_type(), 
cast_options)?
+            };
+
+            // `None` rather than the source entries null buffer, which is 
always absent.
+            // `MapArray::try_new` rejects entries carrying any null, 
`StructArray::try_new`
+            // discards an all-valid null buffer, and the `ArrayData` route 
normalizes one to
+            // `None` in `ArrayDataBuilder::build` before validation is even 
reached. So
+            // `entries().nulls()` is `None` however the `MapArray` was built. 
Reading it back
+            // would suggest a null buffer can survive here when none can 
exist.
+            let entries_struct =
+                StructArray::try_new(to_fields.clone(), vec![cast_keys, 
cast_values], None)?;
+            let new_map = MapArray::try_new(
+                Arc::clone(to_entries_field),
+                map_array.offsets().clone(),
+                entries_struct,
+                map_array.nulls().cloned(),
+                *to_sorted,
+            )?;
+            Ok(Arc::new(new_map))
         }
         _ => unreachable!("cast_map_to_map called with non-Map types"),
     }
@@ -1237,4 +1258,457 @@ mod tests {
         assert_eq!(3, values.null_count());
         assert!(values.iter().all(|value| value.is_none()));
     }
+    fn legacy_opts() -> SparkCastOptions {
+        SparkCastOptions::new(EvalMode::Legacy, "UTC", false)
+    }
+
+    /// Build a `Map<Utf8, Int32>` MapArray (Parquet-style "key_value" field 
names).
+    fn build_str_i32_map(
+        keys: Vec<&str>,
+        values: Vec<Option<i32>>,
+        offsets: Vec<i32>,
+        map_nulls: Option<arrow::buffer::NullBuffer>,
+        sorted: bool,
+    ) -> MapArray {
+        use arrow::array::{Int32Array, StringArray};
+        let key_field = Arc::new(Field::new("key_value_key", DataType::Utf8, 
false));
+        let value_field = Arc::new(Field::new("key_value_value", 
DataType::Int32, true));
+        let entries_fields = Fields::from(vec![key_field, value_field]);
+        let ks = Arc::new(StringArray::from(keys)) as ArrayRef;
+        let vs = Arc::new(Int32Array::from(values)) as ArrayRef;
+        // Entries nulls are always None: MapArray::new rejects entries 
carrying any null.
+        let entries_struct = StructArray::new(entries_fields, vec![ks, vs], 
None);
+        let entries_field = Arc::new(Field::new(
+            "key_value",
+            DataType::Struct(entries_struct.fields().clone()),
+            false,
+        ));
+        MapArray::new(
+            entries_field,
+            OffsetBuffer::<i32>::new(offsets.into()),
+            entries_struct,
+            map_nulls,
+            sorted,
+        )
+    }
+
+    /// Build a target `Map<Utf8, val_type>` type ("entries"/"key"/"value" 
Spark-style names).
+    fn build_to_map_type(val_type: DataType, val_nullable: bool, sorted: bool) 
-> DataType {
+        let to_key = Arc::new(Field::new("key", DataType::Utf8, false));
+        let to_val = Arc::new(Field::new("value", val_type, val_nullable));
+        let entries = Arc::new(Field::new(
+            "entries",
+            DataType::Struct(Fields::from(vec![to_key, to_val])),
+            false,
+        ));
+        DataType::Map(entries, sorted)
+    }
+
+    /// Assert which branch of `cast_map_to_map` a (from, to) type pair 
selects, by checking the
+    /// three inputs the branch condition is built from.
+    fn assert_map_child_types_and_sort(
+        from_type: &DataType,
+        to_type: &DataType,
+        expect_key_unchanged: bool,
+        expect_value_unchanged: bool,
+        expect_same_sort: bool,
+    ) {
+        let children = |t: &DataType| match t {
+            DataType::Map(entries, sorted) => match entries.data_type() {
+                DataType::Struct(f) => {
+                    assert_eq!(f.len(), 2, "map entries must be (key, value)");
+                    (f[0].data_type().clone(), f[1].data_type().clone(), 
*sorted)
+                }
+                other => panic!("map entries must be a struct, got {other:?}"),
+            },
+            other => panic!("expected a Map type, got {other:?}"),
+        };
+        let (from_key, from_val, from_sorted) = children(from_type);
+        let (to_key, to_val, to_sorted) = children(to_type);
+        assert_eq!(
+            from_key == to_key,
+            expect_key_unchanged,
+            "key type unchanged: {from_key:?} vs {to_key:?}"
+        );
+        assert_eq!(
+            from_val == to_val,
+            expect_value_unchanged,
+            "value type unchanged: {from_val:?} vs {to_val:?}"
+        );
+        assert_eq!(
+            from_sorted == to_sorted,
+            expect_same_sort,
+            "sort flag unchanged: {from_sorted} vs {to_sorted}"
+        );
+    }
+
+    fn sorted_src(from_sorted: bool) -> ArrayRef {
+        Arc::new(build_str_i32_map(
+            vec!["a", "b", "c"],
+            vec![Some(1), Some(2), Some(3)],
+            vec![0, 3],
+            None,
+            from_sorted,
+        )) as ArrayRef
+    }
+
+    #[test]
+    fn test_cast_map_to_map_sorted_true_to_false() {
+        // Downgrade a sorted map to unsorted: allowed, result carries the 
target (false) flag.
+        let to_type = build_to_map_type(DataType::Int32, true, false);
+        let casted = cast_array(sorted_src(true), &to_type, 
&legacy_opts()).unwrap();
+        assert_eq!(casted.data_type(), &to_type);
+        match casted.data_type() {
+            DataType::Map(_, is_sorted) => assert!(!*is_sorted),
+            _ => panic!("Expected Map DataType"),
+        }
+    }
+
+    #[test]
+    fn test_cast_map_to_map_preserves_metadata_and_child_type_casts() {
+        use arrow::array::{Int32Array, Int64Array, StringArray};
+        use std::collections::HashMap;
+
+        let key_field = Arc::new(Field::new("key_value_key", DataType::Utf8, 
false));
+        let value_field = Arc::new(Field::new("key_value_value", 
DataType::Int32, true));
+        let entries_fields = Fields::from(vec![key_field, value_field]);
+
+        let keys = Arc::new(StringArray::from(vec!["k1", "k2"]));
+        let values = Arc::new(Int32Array::from(vec![10, 20]));
+        let entries_struct = StructArray::new(entries_fields, vec![keys, 
values], None);
+
+        let from_entries_field = Arc::new(Field::new(
+            "key_value",
+            DataType::Struct(entries_struct.fields().clone()),
+            false,
+        ));
+        let map_array = Arc::new(MapArray::new(
+            from_entries_field,
+            OffsetBuffer::<i32>::new(vec![0, 2].into()),
+            entries_struct,
+            None,
+            false,
+        )) as ArrayRef;
+
+        // `serde.rs` attaches the Parquet field id to the map key and value 
fields, so a rebuild
+        // with `Field::new` drops it. Use the real metadata key 
(`PARQUET_FIELD_ID_META_KEY` in
+        // `parquet::arrow`, whose value is "PARQUET:field_id") so this pins 
that consequence and
+        // not just metadata in general.
+        let field_id_key = "PARQUET:field_id";
+        let to_key_field = Arc::new(
+            Field::new("key", DataType::Utf8, false)
+                .with_metadata(HashMap::from([(field_id_key.to_string(), 
"7".to_string())])),
+        );
+        // The source value field is nullable and the target's is not, so the 
result type carries a
+        // nullability delta as well as a type change.
+        let to_value_field = Arc::new(
+            Field::new("value", DataType::Int64, false)
+                .with_metadata(HashMap::from([(field_id_key.to_string(), 
"8".to_string())])),
+        );
+        // Metadata on the entries field itself, not only on the key field. 
This is the level the
+        // production change fixed by reusing `to_entries_field` rather than 
rebuilding it with
+        // `Field::new`, and the value type change below routes this through 
the hand-built path.
+        let mut entries_meta = HashMap::new();
+        entries_meta.insert("tag".to_string(), "map_entries_meta".to_string());
+        let to_entries_field = Arc::new(
+            Field::new(
+                "entries",
+                DataType::Struct(Fields::from(vec![to_key_field, 
to_value_field])),
+                false,
+            )
+            .with_metadata(entries_meta),
+        );
+        let to_type = DataType::Map(to_entries_field, false);
+
+        let casted = cast_array(
+            map_array,
+            &to_type,
+            &SparkCastOptions::new(EvalMode::Legacy, "UTC", false),
+        )
+        .unwrap();
+
+        let casted_map = casted.as_any().downcast_ref::<MapArray>().unwrap();
+        // Result type is exactly the requested target type (incl. field 
metadata).
+        assert_eq!(casted_map.data_type(), &to_type);
+        // Assert the Parquet field ids survive on both the key and the value 
field
+        assert_eq!(
+            casted_map.entries().fields()[0]
+                .metadata()
+                .get(field_id_key),
+            Some(&"7".to_string())
+        );
+        assert_eq!(
+            casted_map.entries().fields()[1]
+                .metadata()
+                .get(field_id_key),
+            Some(&"8".to_string())
+        );
+        assert!(!casted_map.entries().fields()[1].is_nullable());
+        // Assert entries field metadata is preserved, which is what reusing 
`to_entries_field` fixed
+        match casted_map.data_type() {
+            DataType::Map(entries_field, _) => assert_eq!(
+                entries_field.metadata().get("tag"),
+                Some(&"map_entries_meta".to_string())
+            ),
+            other => panic!("expected a Map type, got {other:?}"),
+        }
+
+        // Assert child values were cast from Int32 to Int64
+        let casted_values = casted_map
+            .values()
+            .as_any()
+            .downcast_ref::<Int64Array>()
+            .unwrap();
+        assert_eq!(casted_values.value(0), 10i64);
+        assert_eq!(casted_values.value(1), 20i64);
+    }
+
+    #[test]
+    fn test_cast_map_to_map_rename_only_preserves_values_offsets_and_nulls() {
+        use arrow::array::{Int32Array, StringArray};
+        use arrow::buffer::NullBuffer;
+        use std::collections::HashMap;
+
+        // Key/value types unchanged and sort order unchanged -> the 
rename-only path (arrow
+        // cast) is used. Three rows including a null row so offsets and map 
nulls are non-trivial.
+        let map_nulls = NullBuffer::from(vec![true, false, true]);
+        let src = build_str_i32_map(
+            vec!["a", "b", "c"],
+            vec![Some(1), Some(2), Some(3)],
+            vec![0, 2, 2, 3],
+            Some(map_nulls.clone()),
+            false,
+        );
+        let src_offsets: Vec<i32> = src.offsets().as_ref().to_vec();
+        let map_array = Arc::new(src) as ArrayRef;
+
+        // Complete target schema: renamed entries/key/value fields, outer + 
key metadata, same
+        // (unchanged) child types, same sort flag.
+        let mut outer_meta = HashMap::new();
+        outer_meta.insert("outer".to_string(), "entries_meta".to_string());
+        let mut key_meta = HashMap::new();
+        key_meta.insert("k".to_string(), "kmeta".to_string());
+        let to_key = Arc::new(Field::new("key", DataType::Utf8, 
false).with_metadata(key_meta));
+        let to_val = Arc::new(Field::new("value", DataType::Int32, true));
+        let to_entries = Arc::new(
+            Field::new(
+                "entries",
+                DataType::Struct(Fields::from(vec![to_key, to_val])),
+                false,
+            )
+            .with_metadata(outer_meta),
+        );
+        let to_type = DataType::Map(Arc::clone(&to_entries), false);
+
+        let casted = cast_array(map_array, &to_type, &legacy_opts()).unwrap();
+        let casted_map = casted.as_any().downcast_ref::<MapArray>().unwrap();
+
+        // The complete target schema is reproduced exactly (field names, 
metadata, nullability, sort).
+        assert_eq!(casted_map.data_type(), &to_type);
+        // Map-level nulls and offsets are unchanged by the relabel.
+        assert_eq!(casted_map.nulls(), Some(&map_nulls));
+        assert!(casted_map.is_null(1));
+        assert_eq!(casted_map.offsets().as_ref(), src_offsets.as_slice());
+        // Keys and values are unchanged by the relabel.
+        let keys = casted_map
+            .keys()
+            .as_any()
+            .downcast_ref::<StringArray>()
+            .unwrap();
+        let vals = casted_map
+            .values()
+            .as_any()
+            .downcast_ref::<Int32Array>()
+            .unwrap();
+        assert_eq!(
+            (0..3).map(|i| keys.value(i)).collect::<Vec<_>>(),
+            vec!["a", "b", "c"]
+        );
+        assert_eq!(vals.values(), &[1, 2, 3]);
+    }
+
+    #[test]
+    fn test_cast_map_to_map_both_paths_agree() {
+        use arrow::array::{Int32Array, StringArray};
+        // Two independent implementations reach the same target type, so pin 
that they agree.
+        // The only difference between the two inputs is the source `sorted` 
flag, which is not
+        // part of the output type: equal flags select the arrow delegation, 
differing flags
+        // select the hand-built path. Everything else (values, offsets, 
map-level nulls) matches,
+        // so the two results must be identical.
+        let rows = |sorted: bool| {
+            Arc::new(build_str_i32_map(
+                vec!["a", "b", "c", "d"],
+                vec![Some(1), None, Some(3), Some(4)],
+                vec![0, 1, 3, 3, 4],
+                Some(arrow::buffer::NullBuffer::from(vec![
+                    true, true, false, true,
+                ])),
+                sorted,
+            )) as ArrayRef
+        };
+        // Slice off the first row so both paths see a non-zero offset window 
and a null row.
+        let unsorted_src = rows(false).slice(1, 3);
+        let sorted_src = rows(true).slice(1, 3);
+        let to_type = build_to_map_type(DataType::Int32, true, false);
+
+        // Fast path: child types and sort flag all unchanged.
+        assert_map_child_types_and_sort(unsorted_src.data_type(), &to_type, 
true, true, true);
+        // Hand-built path: child types unchanged but the sort flag differs 
(true -> false).
+        assert_map_child_types_and_sort(sorted_src.data_type(), &to_type, 
true, true, false);
+
+        let fast = cast_array(unsorted_src, &to_type, &legacy_opts()).unwrap();
+        let hand_built = cast_array(sorted_src, &to_type, 
&legacy_opts()).unwrap();
+
+        assert_eq!(fast.data_type(), &to_type);
+        assert_eq!(hand_built.data_type(), &to_type);
+        assert_eq!(fast.to_data(), hand_built.to_data());
+
+        // Assert the shared result is actually right, so agreement on a wrong 
value cannot pass.
+        let m = fast.as_any().downcast_ref::<MapArray>().unwrap();
+        assert_eq!(m.len(), 3);
+        assert!(m.is_valid(0) && !m.is_valid(1) && m.is_valid(2));
+        let keys = m.keys().as_any().downcast_ref::<StringArray>().unwrap();
+        let vals = m.values().as_any().downcast_ref::<Int32Array>().unwrap();
+        let o = m.offsets();
+        let (start, end) = (o[0] as usize, o[3] as usize);
+        let got_keys: Vec<&str> = (start..end).map(|i| 
keys.value(i)).collect();
+        let got_vals: Vec<Option<i32>> = (start..end)
+            .map(|i| (!vals.is_null(i)).then(|| vals.value(i)))
+            .collect();
+        assert_eq!(got_keys, vec!["b", "c", "d"]);
+        assert_eq!(got_vals, vec![None, Some(3), Some(4)]);
+    }
+
+    #[test]
+    fn test_cast_map_to_map_casts_key_and_value() {
+        use arrow::array::{Int32Array, Int64Array};
+        use std::collections::HashMap;
+        // Source Map<Int32, Int32> -> target Map<Int64, Int64>: both key and 
value are cast.
+        let key_field = Arc::new(Field::new("key_value_key", DataType::Int32, 
false));
+        let value_field = Arc::new(Field::new("key_value_value", 
DataType::Int32, true));
+        let entries_fields = Fields::from(vec![key_field, value_field]);
+        let ks = Arc::new(Int32Array::from(vec![1, 2])) as ArrayRef;
+        let vs = Arc::new(Int32Array::from(vec![10, 20])) as ArrayRef;
+        let entries_struct = StructArray::new(entries_fields, vec![ks, vs], 
None);
+        let entries_field = Arc::new(Field::new(
+            "key_value",
+            DataType::Struct(entries_struct.fields().clone()),
+            false,
+        ));
+        let src = Arc::new(MapArray::new(
+            entries_field,
+            OffsetBuffer::<i32>::new(vec![0, 2].into()),
+            entries_struct,
+            None,
+            false,
+        )) as ArrayRef;
+
+        // The target key carries metadata so this pins the metadata as well 
as the cast. Without
+        // it a plain type change is reproduced exactly by rebuilding the 
fields with
+        // `Field::new`, and the test would pass against the unfixed function.
+        let to_key = Arc::new(Field::new("key", DataType::Int64, 
false).with_metadata(
+            HashMap::from([("PARQUET:field_id".to_string(), "7".to_string())]),
+        ));
+        let to_val = Arc::new(Field::new("value", DataType::Int64, true));
+        let to_entries = Arc::new(Field::new(
+            "entries",
+            DataType::Struct(Fields::from(vec![to_key, to_val])),
+            false,
+        ));
+        let to_type = DataType::Map(Arc::clone(&to_entries), false);
+
+        let casted = cast_array(src, &to_type, &legacy_opts()).unwrap();
+        assert_eq!(casted.data_type(), &to_type);
+        let m = casted.as_any().downcast_ref::<MapArray>().unwrap();
+        let keys = m.keys().as_any().downcast_ref::<Int64Array>().unwrap();
+        let vals = m.values().as_any().downcast_ref::<Int64Array>().unwrap();
+        assert_eq!(keys.values(), &[1i64, 2]);
+        assert_eq!(vals.values(), &[10i64, 20]);
+    }
+
+    #[test]
+    fn test_cast_map_to_map_malformed_target_returns_err_without_panic() {
+        // Source value has a NULL; the target changes the value type (Int32 
-> Int64) AND declares
+        // it NON-nullable. The type change routes through the hand-built 
child-cast path, and the
+        // resulting null in a non-nullable field makes `StructArray::try_new` 
return Err (no panic).
+        let src = Arc::new(build_str_i32_map(
+            vec!["a", "b"],
+            vec![Some(1), None],
+            vec![0, 2],
+            None,
+            false,
+        )) as ArrayRef;
+        let to_type = build_to_map_type(DataType::Int64, false, false);
+        let err = cast_array(src, &to_type, &legacy_opts()).unwrap_err();
+        assert!(
+            err.to_string()
+                .contains(r#"Found unmasked nulls for non-nullable StructArray 
field "value""#),
+            "a null in a non-nullable target child must fail in 
StructArray::try_new, got: {err}"
+        );
+    }
+
+    fn one_row_src() -> ArrayRef {
+        Arc::new(build_str_i32_map(
+            vec!["a"],
+            vec![Some(1)],
+            vec![0, 1],
+            None,
+            false,
+        )) as ArrayRef
+    }
+
+    fn map_target_with_entry_fields(fields: Vec<Arc<Field>>) -> DataType {
+        let entries = Arc::new(Field::new(
+            "entries",
+            DataType::Struct(Fields::from(fields)),
+            false,
+        ));
+        DataType::Map(entries, false)
+    }
+
+    fn entry_field(n: &str, t: DataType) -> Arc<Field> {
+        Arc::new(Field::new(n, t, false))
+    }
+
+    // A target whose entries struct is not exactly (key, value) must hit the 
field-count guard. The
+    // 0- and 1-field cases would otherwise panic on the `[0]`/`[1]` indexing. 
A 3-field target is
+    // rejected downstream even without the guard, so these assert the guard's 
own message rather
+    // than bare `is_err`, which an unrelated failure would satisfy just as 
well.
+    // Split per field-count so a panic in one case cannot hide the others.
+    #[test]
+    fn test_cast_map_to_map_zero_entry_fields_errs() {
+        let to_type = map_target_with_entry_fields(vec![]);
+        let err = cast_array(one_row_src(), &to_type, 
&legacy_opts()).unwrap_err();
+        assert!(
+            err.to_string()
+                .contains("Map entries struct in the cast target must have 
exactly 2 fields"),
+            "0 entry fields must hit the target field-count guard, got: {err}"
+        );
+    }
+
+    #[test]
+    fn test_cast_map_to_map_one_entry_field_errs() {
+        let to_type = map_target_with_entry_fields(vec![entry_field("key", 
DataType::Utf8)]);
+        let err = cast_array(one_row_src(), &to_type, 
&legacy_opts()).unwrap_err();
+        assert!(
+            err.to_string()
+                .contains("Map entries struct in the cast target must have 
exactly 2 fields"),
+            "1 entry field must hit the target field-count guard, got: {err}"
+        );
+    }
+
+    #[test]
+    fn test_cast_map_to_map_three_entry_fields_errs() {
+        let to_type = map_target_with_entry_fields(vec![
+            entry_field("key", DataType::Utf8),
+            entry_field("value", DataType::Int32),
+            entry_field("extra", DataType::Int32),
+        ]);
+        let err = cast_array(one_row_src(), &to_type, 
&legacy_opts()).unwrap_err();
+        assert!(
+            err.to_string()
+                .contains("Map entries struct in the cast target must have 
exactly 2 fields"),
+            "3 entry fields must hit the target field-count guard, got: {err}"
+        );
+    }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to