Skip to main content

datafusion_common/
nested_struct.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18use crate::error::{_plan_err, Result};
19use arrow::{
20    array::{
21        Array, ArrayRef, AsArray, DictionaryArray, FixedSizeListArray, GenericListArray,
22        GenericListViewArray, RecordBatch, StructArray, UnionArray, downcast_integer,
23        make_array, new_null_array,
24    },
25    buffer::NullBuffer,
26    compute::{CastOptions, can_cast_types, cast_with_options},
27    datatypes::{
28        DataType, DataType::Struct, Field, FieldRef, SchemaRef, UnionFields, UnionMode,
29    },
30};
31use std::{collections::HashSet, sync::Arc};
32
33/// Cast a struct column to match target struct fields, handling nested structs recursively.
34///
35/// This function implements struct-to-struct casting with the assumption that **structs should
36/// always be allowed to cast to other structs**. However, the source column must already be
37/// a struct type - non-struct sources will result in an error.
38///
39/// ## Field Matching Strategy
40/// - **By Name**: Source struct fields are matched to target fields by name (case-sensitive)
41/// - **No Positional Mapping**: Structs with no overlapping field names are rejected
42/// - **Type Adaptation**: When a matching field is found, it is recursively cast to the target field's type
43/// - **Missing Fields**: Target fields not present in the source are filled with null values
44/// - **Extra Fields**: Source fields not present in the target are ignored
45///
46/// ## Nested Struct Handling
47/// - Nested structs are handled recursively using the same casting rules
48/// - Each level of nesting follows the same field matching and null-filling strategy
49/// - This allows for complex struct transformations while maintaining data integrity
50///
51/// # Arguments
52/// * `source_col` - The source array to cast (must be a struct array)
53/// * `target_fields` - The target struct field definitions to cast to
54///
55/// # Returns
56/// A `Result<ArrayRef>` containing the cast struct array
57///
58/// # Errors
59/// Returns a `DataFusionError::Plan` if the source column is not a struct type
60fn cast_struct_column(
61    source_col: &ArrayRef,
62    target_fields: &[Arc<Field>],
63    cast_options: &CastOptions,
64) -> Result<ArrayRef> {
65    if source_col.data_type() == &DataType::Null {
66        return Ok(new_null_array(
67            &Struct(target_fields.to_vec().into()),
68            source_col.len(),
69        ));
70    }
71
72    if let Some(source_struct) = source_col.as_any().downcast_ref::<StructArray>() {
73        let source_fields = source_struct.fields();
74        validate_struct_compatibility(source_fields, target_fields)?;
75
76        if !source_col.is_empty() && source_col.null_count() == source_col.len() {
77            return Ok(new_null_array(
78                &Struct(target_fields.to_vec().into()),
79                source_col.len(),
80            ));
81        }
82
83        let mut fields: Vec<Arc<Field>> = Vec::with_capacity(target_fields.len());
84        let mut arrays: Vec<ArrayRef> = Vec::with_capacity(target_fields.len());
85        let num_rows = source_col.len();
86
87        // Iterate target fields and pick source child by name when present.
88        for target_child_field in target_fields.iter() {
89            fields.push(Arc::clone(target_child_field));
90
91            let source_child_opt =
92                source_struct.column_by_name(target_child_field.name());
93
94            match source_child_opt {
95                Some(source_child_col) => {
96                    let adapted_child = cast_column(
97                        source_child_col,
98                        target_child_field.data_type(),
99                        cast_options,
100                    )
101                    .map_err(|e| {
102                        e.context(format!(
103                            "While casting struct field '{}'",
104                            target_child_field.name()
105                        ))
106                    })?;
107                    arrays.push(adapted_child);
108                }
109                None => {
110                    arrays.push(new_null_array(target_child_field.data_type(), num_rows));
111                }
112            }
113        }
114
115        let struct_array =
116            StructArray::new(fields.into(), arrays, source_struct.nulls().cloned());
117        Ok(Arc::new(struct_array))
118    } else {
119        // Return error if source is not a struct type
120        _plan_err!(
121            "Cannot cast column of type {} to struct type. Source must be a struct to cast to struct.",
122            source_col.data_type()
123        )
124    }
125}
126
127/// Cast a union column to match target union fields, handling child fields recursively.
128///
129/// ## Casting Behavior
130/// - Preserves union mode (sparse or dense). Incompatible modes are rejected.
131/// - Requires exact matching union type ID sets (order may differ).
132/// - Recursively adapts each matching child array using `cast_column`.
133/// - Preserves row-level `type_ids` and dense `offsets` buffers without copying primitive data.
134fn cast_union_column(
135    source_col: &ArrayRef,
136    source_fields: &UnionFields,
137    source_mode: &UnionMode,
138    target_fields: &UnionFields,
139    target_mode: &UnionMode,
140    cast_options: &CastOptions,
141) -> Result<ArrayRef> {
142    validate_union_schema_compatibility(
143        source_fields,
144        source_mode,
145        target_fields,
146        target_mode,
147    )?;
148
149    let source_union = source_col
150        .as_any()
151        .downcast_ref::<UnionArray>()
152        .ok_or_else(|| {
153            crate::error::DataFusionError::Plan(format!(
154                "Expected UnionArray for Union data type, got {}",
155                source_col.data_type()
156            ))
157        })?;
158
159    let mut children = Vec::with_capacity(target_fields.len());
160
161    for (target_type_id, target_field) in target_fields.iter() {
162        let source_child = source_union.child(target_type_id);
163
164        children.push(
165            cast_column(source_child, target_field.data_type(), cast_options).map_err(
166                |e| {
167                    e.context(format!(
168                        "While adapting Union child type ID {target_type_id} ('{}')",
169                        target_field.name()
170                    ))
171                },
172            )?,
173        );
174    }
175
176    Ok(Arc::new(UnionArray::try_new(
177        target_fields.clone(),
178        source_union.type_ids().clone(),
179        source_union.offsets().cloned(),
180        children,
181    )?))
182}
183
184/// Cast a column to match the target field type, with special handling for nested structs.
185///
186/// This function serves as the main entry point for column casting operations. For struct
187/// types, it enforces that **only struct columns can be cast to struct types**.
188///
189/// ## Casting Behavior
190/// - **Struct Types**: Delegates to `cast_struct_column` for struct-to-struct casting only
191/// - **Non-Struct Types**: Uses Arrow's standard `cast` function for primitive type conversions
192///
193/// ## Cast Options
194/// The `cast_options` argument controls how Arrow handles values that cannot be represented
195/// in the target type. When `safe` is `false` (DataFusion's default) the cast will return an
196/// error if such a value is encountered. Setting `safe` to `true` instead produces `NULL`
197/// for out-of-range or otherwise invalid values. The options also allow customizing how
198/// temporal values are formatted when cast to strings.
199///
200/// ```
201/// use arrow::array::{ArrayRef, Int64Array};
202/// use arrow::compute::CastOptions;
203/// use arrow::datatypes::DataType;
204/// use datafusion_common::nested_struct::cast_column;
205/// use std::sync::Arc;
206///
207/// let source: ArrayRef = Arc::new(Int64Array::from(vec![1, i64::MAX]));
208/// // Permit lossy conversions by producing NULL on overflow instead of erroring
209/// let options = CastOptions {
210///     safe: true,
211///     ..Default::default()
212/// };
213/// let result = cast_column(&source, &DataType::Int32, &options).unwrap();
214/// assert!(result.is_null(1));
215/// ```
216///
217/// ## Struct Casting Requirements
218/// The struct casting logic requires that the source column must already be a struct type.
219/// This makes the function useful for:
220/// - Schema evolution scenarios where struct layouts change over time
221/// - Data migration between different struct schemas
222/// - Type-safe data processing pipelines that maintain struct type integrity
223///
224/// # Arguments
225/// * `source_col` - The source array to cast
226/// * `target_type` - The target data type to cast to
227/// * `cast_options` - Options that govern strictness and formatting of the cast
228///
229/// # Returns
230/// A `Result<ArrayRef>` containing the cast array
231///
232/// # Errors
233/// Returns an error if:
234/// - Attempting to cast a non-struct column to a struct type
235/// - Arrow's cast function fails for non-struct types
236/// - Memory allocation fails during struct construction
237/// - Invalid data type combinations are encountered
238pub fn cast_column(
239    source_col: &ArrayRef,
240    target_type: &DataType,
241    cast_options: &CastOptions,
242) -> Result<ArrayRef> {
243    match (source_col.data_type(), target_type) {
244        (_, Struct(target_fields)) => {
245            cast_struct_column(source_col, target_fields, cast_options)
246        }
247        (DataType::List(_), DataType::List(target_inner)) => {
248            cast_list_column::<i32>(source_col, target_inner, cast_options)
249        }
250        (DataType::LargeList(_), DataType::LargeList(target_inner)) => {
251            cast_list_column::<i64>(source_col, target_inner, cast_options)
252        }
253        (
254            DataType::FixedSizeList(_, source_list_size),
255            DataType::FixedSizeList(target_inner, target_list_size),
256        ) if source_list_size == target_list_size => cast_fixed_size_list_column(
257            source_col,
258            target_inner,
259            *target_list_size,
260            cast_options,
261        ),
262        (DataType::ListView(_), DataType::ListView(target_inner)) => {
263            cast_list_view_column::<i32>(source_col, target_inner, cast_options)
264        }
265        (DataType::LargeListView(_), DataType::LargeListView(target_inner)) => {
266            cast_list_view_column::<i64>(source_col, target_inner, cast_options)
267        }
268        (
269            DataType::Dictionary(source_key_type, _),
270            DataType::Dictionary(target_key_type, target_value_type),
271        ) => cast_dictionary_column(
272            source_col,
273            source_key_type,
274            target_key_type,
275            target_value_type,
276            cast_options,
277        ),
278        (
279            DataType::Union(source_fields, source_mode),
280            DataType::Union(target_fields, target_mode),
281        ) => cast_union_column(
282            source_col,
283            source_fields,
284            source_mode,
285            target_fields,
286            target_mode,
287            cast_options,
288        ),
289        _ => Ok(cast_with_options(source_col, target_type, cast_options)?),
290    }
291}
292
293fn cast_list_column<O: arrow::array::OffsetSizeTrait>(
294    source_col: &ArrayRef,
295    target_inner_field: &FieldRef,
296    cast_options: &CastOptions,
297) -> Result<ArrayRef> {
298    let source_list = source_col.as_list::<O>();
299
300    let cast_values = cast_column(
301        source_list.values(),
302        target_inner_field.data_type(),
303        cast_options,
304    )?;
305
306    let result = GenericListArray::<O>::new(
307        Arc::clone(target_inner_field),
308        source_list.offsets().clone(),
309        cast_values,
310        source_list.nulls().cloned(),
311    );
312    Ok(Arc::new(result))
313}
314
315fn cast_list_view_column<O: arrow::array::OffsetSizeTrait>(
316    source_col: &ArrayRef,
317    target_inner_field: &FieldRef,
318    cast_options: &CastOptions,
319) -> Result<ArrayRef> {
320    let source_list = source_col.as_list_view::<O>();
321
322    let cast_values = cast_column(
323        source_list.values(),
324        target_inner_field.data_type(),
325        cast_options,
326    )?;
327
328    let result = GenericListViewArray::<O>::try_new(
329        Arc::clone(target_inner_field),
330        source_list.offsets().clone(),
331        source_list.sizes().clone(),
332        cast_values,
333        source_list.nulls().cloned(),
334    )?;
335    Ok(Arc::new(result))
336}
337
338fn cast_fixed_size_list_column(
339    source_col: &ArrayRef,
340    target_inner_field: &FieldRef,
341    target_list_size: i32,
342    cast_options: &CastOptions,
343) -> Result<ArrayRef> {
344    let source_list = source_col.as_fixed_size_list();
345
346    let source_values = source_list.values();
347    let target_type = target_inner_field.data_type();
348
349    let cast_values = match cast_column(source_values, target_type, cast_options) {
350        Ok(cast_values) => cast_values,
351        Err(error) => match cast_fixed_size_list_values_with_parent_nulls(
352            source_values,
353            target_type,
354            cast_options,
355            source_list.nulls(),
356            target_list_size,
357        ) {
358            Some(masked_cast) => masked_cast?,
359            None => return Err(error),
360        },
361    };
362
363    Ok(Arc::new(FixedSizeListArray::try_new(
364        Arc::clone(target_inner_field),
365        target_list_size,
366        cast_values,
367        source_list.nulls().cloned(),
368    )?))
369}
370
371fn cast_fixed_size_list_values_with_parent_nulls(
372    source_values: &ArrayRef,
373    target_type: &DataType,
374    cast_options: &CastOptions,
375    parent_nulls: Option<&NullBuffer>,
376    list_size: i32,
377) -> Option<Result<ArrayRef>> {
378    let parent_nulls = parent_nulls.filter(|nulls| nulls.null_count() > 0)?;
379
380    // FixedSizeList stores child slots for null parent lists. Those child
381    // values are semantically hidden, but recursive casts still inspect them.
382    let hidden_child_nulls = parent_nulls.expand(list_size as usize);
383    let masked_values = mask_array_values(source_values, &hidden_child_nulls);
384    Some(masked_values.and_then(|values| cast_column(&values, target_type, cast_options)))
385}
386
387fn mask_array_values(
388    values: &ArrayRef,
389    additional_nulls: &NullBuffer,
390) -> Result<ArrayRef> {
391    let nulls = NullBuffer::union(values.nulls(), Some(additional_nulls));
392
393    if let Some(struct_array) = values.as_any().downcast_ref::<StructArray>() {
394        let struct_nulls = nulls
395            .as_ref()
396            .expect("additional nulls always produce nulls");
397        let arrays = struct_array
398            .columns()
399            .iter()
400            .map(|child| mask_array_values(child, struct_nulls))
401            .collect::<Result<Vec<_>>>()?;
402        return Ok(Arc::new(StructArray::new(
403            struct_array.fields().clone(),
404            arrays,
405            nulls,
406        )));
407    }
408
409    Ok(make_array(
410        values.to_data().into_builder().nulls(nulls).build()?,
411    ))
412}
413
414fn cast_dictionary_column(
415    source_col: &ArrayRef,
416    source_key_type: &DataType,
417    target_key_type: &DataType,
418    target_value_type: &DataType,
419    cast_options: &CastOptions,
420) -> Result<ArrayRef> {
421    // Dispatch on source key type to access keys/values, then recursively
422    // cast values. Rebuild with the source key type first.
423    macro_rules! cast_dict_values {
424        ($t:ty) => {{
425            let source_dict = source_col
426                .as_any()
427                .downcast_ref::<DictionaryArray<$t>>()
428                .expect("downcast must succeed");
429            let cast_values =
430                cast_column(source_dict.values(), target_value_type, cast_options)?;
431            Ok(Arc::new(DictionaryArray::<$t>::new(
432                source_dict.keys().clone(),
433                cast_values,
434            )) as ArrayRef)
435        }};
436    }
437
438    let result: Result<ArrayRef> = downcast_integer! {
439        source_key_type => (cast_dict_values),
440        k => _plan_err!("Unsupported dictionary key type: {k}")
441    };
442    let result = result?;
443
444    // If key types differ, delegate key casting to Arrow.
445    if source_key_type != target_key_type {
446        let target_dict_type = DataType::Dictionary(
447            Box::new(target_key_type.clone()),
448            Box::new(target_value_type.clone()),
449        );
450        Ok(cast_with_options(&result, &target_dict_type, cast_options)?)
451    } else {
452        Ok(result)
453    }
454}
455
456/// Validates compatibility between source and target struct fields for casting operations.
457///
458/// This function implements comprehensive struct compatibility checking by examining:
459/// - Field name matching between source and target structs
460/// - Type castability for each matching field (including recursive struct validation)
461/// - Proper handling of missing fields (target fields not in source are allowed - filled with nulls)
462/// - Proper handling of extra fields (source fields not in target are allowed - ignored)
463///
464/// # Compatibility Rules
465/// - **Field Matching**: Fields are matched by name (case-sensitive)
466/// - **Missing Target Fields**: Allowed - will be filled with null values during casting
467/// - **Extra Source Fields**: Allowed - will be ignored during casting
468/// - **Type Compatibility**: Each matching field must be castable using Arrow's type system
469/// - **Nested Structs**: Recursively validates nested struct compatibility
470///
471/// # Arguments
472/// * `source_fields` - Fields from the source struct type
473/// * `target_fields` - Fields from the target struct type
474///
475/// # Returns
476/// * `Ok(())` if the structs are compatible for casting
477/// * `Err(DataFusionError)` with detailed error message if incompatible
478///
479/// # Examples
480/// ```text
481/// // Compatible: source has extra field, target has missing field
482/// // Source: {a: i32, b: string, c: f64}
483/// // Target: {a: i64, d: bool}
484/// // Result: Ok(()) - 'a' can cast i32->i64, 'b','c' ignored, 'd' filled with nulls
485///
486/// // Incompatible: matching field has incompatible types
487/// // Source: {a: string}
488/// // Target: {a: binary}
489/// // Result: Err(...) - string cannot cast to binary
490/// ```
491///
492pub fn validate_struct_compatibility(
493    source_fields: &[FieldRef],
494    target_fields: &[FieldRef],
495) -> Result<()> {
496    let has_overlap = has_one_of_more_common_fields(source_fields, target_fields);
497    if !has_overlap {
498        return _plan_err!(
499            "Cannot cast struct with {} fields to {} fields because there is no field name overlap",
500            source_fields.len(),
501            target_fields.len()
502        );
503    }
504
505    // Check compatibility for each target field
506    for target_field in target_fields {
507        // Look for matching field in source by name
508        if let Some(source_field) = source_fields
509            .iter()
510            .find(|f| f.name() == target_field.name())
511        {
512            validate_field_compatibility(source_field, target_field)?;
513        } else {
514            // Target field is missing from source
515            // If it's non-nullable, we cannot fill it with NULL
516            if !target_field.is_nullable() {
517                return _plan_err!(
518                    "Cannot cast struct: target field '{}' is non-nullable but missing from source. \
519                     Cannot fill with NULL.",
520                    target_field.name()
521                );
522            }
523        }
524    }
525
526    // Extra fields in source are OK - they'll be ignored
527    Ok(())
528}
529
530fn validate_field_compatibility(
531    source_field: &Field,
532    target_field: &Field,
533) -> Result<()> {
534    if source_field.data_type() == &DataType::Null {
535        // Validate that target allows nulls before returning early.
536        // It is invalid to cast a NULL source field to a non-nullable target field.
537        if !target_field.is_nullable() {
538            return _plan_err!(
539                "Cannot cast NULL struct field '{}' to non-nullable field '{}'",
540                source_field.name(),
541                target_field.name()
542            );
543        }
544        return Ok(());
545    }
546
547    // Ensure nullability is compatible. It is invalid to cast a nullable
548    // source field to a non-nullable target field as this may discard
549    // null values.
550    if source_field.is_nullable() && !target_field.is_nullable() {
551        return _plan_err!(
552            "Cannot cast nullable struct field '{}' to non-nullable field",
553            target_field.name()
554        );
555    }
556
557    validate_data_type_compatibility(
558        target_field.name(),
559        source_field.data_type(),
560        target_field.data_type(),
561    )
562}
563
564fn validate_union_schema_compatibility(
565    source_fields: &UnionFields,
566    source_mode: &UnionMode,
567    target_fields: &UnionFields,
568    target_mode: &UnionMode,
569) -> Result<()> {
570    if source_mode != target_mode {
571        return _plan_err!(
572            "Cannot adapt Union from mode {source_mode:?} to {target_mode:?}"
573        );
574    }
575
576    // This adapter is for schema conformance, not general Union variant-set evolution.
577    if source_fields.len() != target_fields.len() {
578        return _plan_err!(
579            "Cannot adapt Union schema with different field sets:              source has {} fields, target has {}",
580            source_fields.len(),
581            target_fields.len()
582        );
583    }
584
585    for (target_type_id, target_field) in target_fields.iter() {
586        let Some((_, source_field)) = source_fields
587            .iter()
588            .find(|(source_type_id, _)| *source_type_id == target_type_id)
589        else {
590            return _plan_err!(
591                "Cannot adapt Union schema: target type ID {target_type_id}                  ('{}') is missing from source",
592                target_field.name()
593            );
594        };
595
596        if !target_field.contains(source_field) {
597            return _plan_err!(
598                "Cannot adapt Union child with type ID {target_type_id}:                  source field {source_field} is not contained by target field {target_field}"
599            );
600        }
601    }
602
603    Ok(())
604}
605
606/// Validates that `source_type` can be cast to `target_type`, recursively
607/// handling container types that wrap structs.
608pub fn validate_data_type_compatibility(
609    field_name: &str,
610    source_type: &DataType,
611    target_type: &DataType,
612) -> Result<()> {
613    match (source_type, target_type) {
614        (Struct(source_nested), Struct(target_nested)) => {
615            validate_struct_compatibility(source_nested, target_nested)?;
616        }
617        (
618            DataType::FixedSizeList(s, source_list_size),
619            DataType::FixedSizeList(t, target_list_size),
620        ) if source_list_size == target_list_size => {
621            validate_field_compatibility(s, t)?;
622        }
623        (DataType::List(s), DataType::List(t))
624        | (DataType::LargeList(s), DataType::LargeList(t))
625        | (DataType::ListView(s), DataType::ListView(t))
626        | (DataType::LargeListView(s), DataType::LargeListView(t)) => {
627            validate_field_compatibility(s, t)?;
628        }
629        (DataType::Dictionary(s_key, s_val), DataType::Dictionary(t_key, t_val)) => {
630            if !can_cast_types(s_key, t_key) {
631                return _plan_err!(
632                    "Cannot cast dictionary key type {} to {} for field '{}'",
633                    s_key,
634                    t_key,
635                    field_name
636                );
637            }
638            validate_data_type_compatibility(field_name, s_val, t_val)?;
639        }
640        (
641            DataType::Union(source_fields, source_mode),
642            DataType::Union(target_fields, target_mode),
643        ) => {
644            validate_union_schema_compatibility(
645                source_fields,
646                source_mode,
647                target_fields,
648                target_mode,
649            )?;
650        }
651        _ => {
652            if !can_cast_types(source_type, target_type) {
653                return _plan_err!(
654                    "Cannot cast struct field '{}' from type {} to type {}",
655                    field_name,
656                    source_type,
657                    target_type
658                );
659            }
660        }
661    }
662    Ok(())
663}
664
665/// Returns true if casting from `source_type` to `target_type` requires
666/// name-based nested struct casting logic, rather than Arrow's standard cast.
667///
668/// This is the case when both types are struct types, or both are the same
669/// container type (List, LargeList, equal-width FixedSizeList, ListView,
670/// LargeListView, Dictionary) wrapping types that recursively contain structs.
671///
672/// Use this predicate at both planning time (to decide whether to apply struct
673/// compatibility validation) and execution time (to decide whether to route
674/// through [`cast_column`] instead of Arrow's generic cast).
675pub fn requires_nested_struct_cast(
676    source_type: &DataType,
677    target_type: &DataType,
678) -> bool {
679    match (source_type, target_type) {
680        (Struct(_), Struct(_)) => true,
681        (
682            DataType::FixedSizeList(s, source_list_size),
683            DataType::FixedSizeList(t, target_list_size),
684        ) if source_list_size == target_list_size => {
685            requires_nested_struct_cast(s.data_type(), t.data_type())
686        }
687        (DataType::List(s), DataType::List(t))
688        | (DataType::LargeList(s), DataType::LargeList(t))
689        | (DataType::ListView(s), DataType::ListView(t))
690        | (DataType::LargeListView(s), DataType::LargeListView(t)) => {
691            requires_nested_struct_cast(s.data_type(), t.data_type())
692        }
693        (DataType::Dictionary(_, s_val), DataType::Dictionary(_, t_val)) => {
694            requires_nested_struct_cast(s_val, t_val)
695        }
696        _ => false,
697    }
698}
699
700/// Check if two field lists have at least one common field by name.
701///
702/// This is useful for validating struct compatibility when casting between structs,
703/// ensuring that source and target fields have overlapping names.
704pub fn has_one_of_more_common_fields(
705    source_fields: &[FieldRef],
706    target_fields: &[FieldRef],
707) -> bool {
708    let source_names: HashSet<&str> = source_fields
709        .iter()
710        .map(|field| field.name().as_str())
711        .collect();
712    target_fields
713        .iter()
714        .any(|field| source_names.contains(field.name().as_str()))
715}
716
717#[cfg(test)]
718mod tests {
719    use super::*;
720    use crate::{assert_contains, format::DEFAULT_CAST_OPTIONS};
721    use arrow::{
722        array::{
723            BinaryArray, FixedSizeListArray, Int32Array, Int32Builder, Int64Array,
724            ListArray, ListViewArray, MapArray, MapBuilder, NullArray, StringArray,
725            StringBuilder,
726        },
727        buffer::{NullBuffer, ScalarBuffer},
728        datatypes::{DataType, Field, FieldRef, Int32Type},
729    };
730    /// Macro to extract and downcast a column from a StructArray
731    macro_rules! get_column_as {
732        ($struct_array:expr, $column_name:expr, $array_type:ty) => {
733            $struct_array
734                .column_by_name($column_name)
735                .unwrap()
736                .as_any()
737                .downcast_ref::<$array_type>()
738                .unwrap()
739        };
740    }
741
742    fn field(name: &str, data_type: DataType) -> Field {
743        Field::new(name, data_type, true)
744    }
745
746    fn non_null_field(name: &str, data_type: DataType) -> Field {
747        Field::new(name, data_type, false)
748    }
749
750    fn arc_field(name: &str, data_type: DataType) -> FieldRef {
751        Arc::new(field(name, data_type))
752    }
753
754    fn struct_type(fields: Vec<Field>) -> DataType {
755        Struct(fields.into())
756    }
757
758    fn struct_field(name: &str, fields: Vec<Field>) -> Field {
759        field(name, struct_type(fields))
760    }
761
762    fn arc_struct_field(name: &str, fields: Vec<Field>) -> FieldRef {
763        Arc::new(struct_field(name, fields))
764    }
765
766    #[test]
767    fn test_cast_simple_column() {
768        let source = Arc::new(Int32Array::from(vec![1, 2, 3])) as ArrayRef;
769        let target_field = field("ints", DataType::Int64);
770        let result =
771            cast_column(&source, target_field.data_type(), &DEFAULT_CAST_OPTIONS)
772                .unwrap();
773        let result = result.as_any().downcast_ref::<Int64Array>().unwrap();
774        assert_eq!(result.len(), 3);
775        assert_eq!(result.value(0), 1);
776        assert_eq!(result.value(1), 2);
777        assert_eq!(result.value(2), 3);
778    }
779
780    #[test]
781    fn test_cast_column_with_options() {
782        let source = Arc::new(Int64Array::from(vec![1, i64::MAX])) as ArrayRef;
783        let target_field = field("ints", DataType::Int32);
784
785        let safe_opts = CastOptions {
786            // safe: false - return Err for failure
787            safe: false,
788            ..DEFAULT_CAST_OPTIONS
789        };
790        assert!(cast_column(&source, target_field.data_type(), &safe_opts).is_err());
791
792        let unsafe_opts = CastOptions {
793            // safe: true - return Null for failure
794            safe: true,
795            ..DEFAULT_CAST_OPTIONS
796        };
797        let result =
798            cast_column(&source, target_field.data_type(), &unsafe_opts).unwrap();
799        let result = result.as_any().downcast_ref::<Int32Array>().unwrap();
800        assert_eq!(result.value(0), 1);
801        assert!(result.is_null(1));
802    }
803
804    #[test]
805    fn test_cast_struct_with_missing_field() {
806        let a_array = Arc::new(Int32Array::from(vec![1, 2])) as ArrayRef;
807        let source_struct = StructArray::from(vec![(
808            arc_field("a", DataType::Int32),
809            Arc::clone(&a_array),
810        )]);
811        let source_col = Arc::new(source_struct) as ArrayRef;
812
813        let target_field = struct_field(
814            "s",
815            vec![field("a", DataType::Int32), field("b", DataType::Utf8)],
816        );
817
818        let result =
819            cast_column(&source_col, target_field.data_type(), &DEFAULT_CAST_OPTIONS)
820                .unwrap();
821        let struct_array = result.as_any().downcast_ref::<StructArray>().unwrap();
822        assert_eq!(struct_array.fields().len(), 2);
823        let a_result = get_column_as!(&struct_array, "a", Int32Array);
824        assert_eq!(a_result.value(0), 1);
825        assert_eq!(a_result.value(1), 2);
826
827        let b_result = get_column_as!(&struct_array, "b", StringArray);
828        assert_eq!(b_result.len(), 2);
829        assert!(b_result.is_null(0));
830        assert!(b_result.is_null(1));
831    }
832
833    #[test]
834    fn test_cast_struct_source_not_struct() {
835        let source = Arc::new(Int32Array::from(vec![10, 20])) as ArrayRef;
836        let target_field = struct_field("s", vec![field("a", DataType::Int32)]);
837
838        let result =
839            cast_column(&source, target_field.data_type(), &DEFAULT_CAST_OPTIONS);
840        assert!(result.is_err());
841        let error_msg = result.unwrap_err().to_string();
842        assert!(error_msg.contains("Cannot cast column of type"));
843        assert!(error_msg.contains("to struct type"));
844        assert!(error_msg.contains("Source must be a struct"));
845    }
846
847    #[test]
848    fn test_cast_struct_incompatible_child_type() {
849        let a_array = Arc::new(BinaryArray::from(vec![
850            Some(b"a".as_ref()),
851            Some(b"b".as_ref()),
852        ])) as ArrayRef;
853        let source_struct =
854            StructArray::from(vec![(arc_field("a", DataType::Binary), a_array)]);
855        let source_col = Arc::new(source_struct) as ArrayRef;
856
857        let target_field = struct_field("s", vec![field("a", DataType::Int32)]);
858
859        let result =
860            cast_column(&source_col, target_field.data_type(), &DEFAULT_CAST_OPTIONS);
861        assert!(result.is_err());
862        let error_msg = result.unwrap_err().to_string();
863        assert!(error_msg.contains("Cannot cast struct field 'a'"));
864    }
865
866    #[test]
867    fn test_validate_struct_compatibility_incompatible_types() {
868        // Source struct: {field1: Binary, field2: String}
869        let source_fields = vec![
870            arc_field("field1", DataType::Binary),
871            arc_field("field2", DataType::Utf8),
872        ];
873
874        // Target struct: {field1: Int32}
875        let target_fields = vec![arc_field("field1", DataType::Int32)];
876
877        let result = validate_struct_compatibility(&source_fields, &target_fields);
878        assert!(result.is_err());
879        let error_msg = result.unwrap_err().to_string();
880        assert!(error_msg.contains("Cannot cast struct field 'field1'"));
881        assert!(error_msg.contains("Binary"));
882        assert!(error_msg.contains("Int32"));
883    }
884
885    #[test]
886    fn test_validate_struct_compatibility_compatible_types() {
887        // Source struct: {field1: Int32, field2: String}
888        let source_fields = vec![
889            arc_field("field1", DataType::Int32),
890            arc_field("field2", DataType::Utf8),
891        ];
892
893        // Target struct: {field1: Int64} (Int32 can cast to Int64)
894        let target_fields = vec![arc_field("field1", DataType::Int64)];
895
896        let result = validate_struct_compatibility(&source_fields, &target_fields);
897        assert!(result.is_ok());
898    }
899
900    #[test]
901    fn test_validate_struct_compatibility_missing_field_in_source() {
902        // Source struct: {field1: Int32} (missing field2)
903        let source_fields = vec![arc_field("field1", DataType::Int32)];
904
905        // Target struct: {field1: Int32, field2: Utf8}
906        let target_fields = vec![
907            arc_field("field1", DataType::Int32),
908            arc_field("field2", DataType::Utf8),
909        ];
910
911        // Should be OK - missing fields will be filled with nulls
912        let result = validate_struct_compatibility(&source_fields, &target_fields);
913        assert!(result.is_ok());
914    }
915
916    #[test]
917    fn test_validate_struct_compatibility_additional_field_in_source() {
918        // Source struct: {field1: Int32, field2: String} (extra field2)
919        let source_fields = vec![
920            arc_field("field1", DataType::Int32),
921            arc_field("field2", DataType::Utf8),
922        ];
923
924        // Target struct: {field1: Int32}
925        let target_fields = vec![arc_field("field1", DataType::Int32)];
926
927        // Should be OK - extra fields in source are ignored
928        let result = validate_struct_compatibility(&source_fields, &target_fields);
929        assert!(result.is_ok());
930    }
931
932    #[test]
933    fn test_validate_struct_compatibility_no_overlap_mismatch_len() {
934        let source_fields = vec![
935            arc_field("left", DataType::Int32),
936            arc_field("right", DataType::Int32),
937        ];
938        let target_fields = vec![arc_field("alpha", DataType::Int32)];
939
940        let result = validate_struct_compatibility(&source_fields, &target_fields);
941        assert!(result.is_err());
942        let error_msg = result.unwrap_err().to_string();
943        assert_contains!(error_msg, "no field name overlap");
944    }
945
946    #[test]
947    fn test_cast_struct_parent_nulls_retained() {
948        let a_array = Arc::new(Int32Array::from(vec![Some(1), Some(2)])) as ArrayRef;
949        let fields = vec![arc_field("a", DataType::Int32)];
950        let nulls = Some(NullBuffer::from(vec![true, false]));
951        let source_struct = StructArray::new(fields.clone().into(), vec![a_array], nulls);
952        let source_col = Arc::new(source_struct) as ArrayRef;
953
954        let target_field = struct_field("s", vec![field("a", DataType::Int64)]);
955
956        let result =
957            cast_column(&source_col, target_field.data_type(), &DEFAULT_CAST_OPTIONS)
958                .unwrap();
959        let struct_array = result.as_any().downcast_ref::<StructArray>().unwrap();
960        assert_eq!(struct_array.null_count(), 1);
961        assert!(struct_array.is_valid(0));
962        assert!(struct_array.is_null(1));
963
964        let a_result = get_column_as!(&struct_array, "a", Int64Array);
965        assert_eq!(a_result.value(0), 1);
966        assert_eq!(a_result.value(1), 2);
967    }
968
969    #[test]
970    fn test_validate_struct_compatibility_nullable_to_non_nullable() {
971        // Source struct: {field1: Int32 nullable}
972        let source_fields = vec![arc_field("field1", DataType::Int32)];
973
974        // Target struct: {field1: Int32 non-nullable}
975        let target_fields = vec![Arc::new(non_null_field("field1", DataType::Int32))];
976
977        let result = validate_struct_compatibility(&source_fields, &target_fields);
978        assert!(result.is_err());
979        let error_msg = result.unwrap_err().to_string();
980        assert!(error_msg.contains("field1"));
981        assert!(error_msg.contains("non-nullable"));
982    }
983
984    #[test]
985    fn test_validate_struct_compatibility_non_nullable_to_nullable() {
986        // Source struct: {field1: Int32 non-nullable}
987        let source_fields = vec![Arc::new(non_null_field("field1", DataType::Int32))];
988
989        // Target struct: {field1: Int32 nullable}
990        let target_fields = vec![arc_field("field1", DataType::Int32)];
991
992        let result = validate_struct_compatibility(&source_fields, &target_fields);
993        assert!(result.is_ok());
994    }
995
996    #[test]
997    fn test_validate_struct_compatibility_nested_nullable_to_non_nullable() {
998        // Source struct: {field1: {nested: Int32 nullable}}
999        let source_fields = vec![Arc::new(non_null_field(
1000            "field1",
1001            struct_type(vec![field("nested", DataType::Int32)]),
1002        ))];
1003
1004        // Target struct: {field1: {nested: Int32 non-nullable}}
1005        let target_fields = vec![Arc::new(non_null_field(
1006            "field1",
1007            struct_type(vec![non_null_field("nested", DataType::Int32)]),
1008        ))];
1009
1010        let result = validate_struct_compatibility(&source_fields, &target_fields);
1011        assert!(result.is_err());
1012        let error_msg = result.unwrap_err().to_string();
1013        assert!(error_msg.contains("nested"));
1014        assert!(error_msg.contains("non-nullable"));
1015    }
1016
1017    #[test]
1018    fn test_validate_struct_compatibility_by_name() {
1019        // Source struct: {field1: Int32, field2: String}
1020        let source_fields = vec![
1021            arc_field("field1", DataType::Int32),
1022            arc_field("field2", DataType::Utf8),
1023        ];
1024
1025        // Target struct: {field2: String, field1: Int64}
1026        let target_fields = vec![
1027            arc_field("field2", DataType::Utf8),
1028            arc_field("field1", DataType::Int64),
1029        ];
1030
1031        let result = validate_struct_compatibility(&source_fields, &target_fields);
1032        assert!(result.is_ok());
1033    }
1034
1035    #[test]
1036    fn test_validate_struct_compatibility_by_name_with_type_mismatch() {
1037        // Source struct: {field1: Binary}
1038        let source_fields = vec![arc_field("field1", DataType::Binary)];
1039
1040        // Target struct: {field1: Int32} (incompatible type)
1041        let target_fields = vec![arc_field("field1", DataType::Int32)];
1042
1043        let result = validate_struct_compatibility(&source_fields, &target_fields);
1044        assert!(result.is_err());
1045        let error_msg = result.unwrap_err().to_string();
1046        assert_contains!(
1047            error_msg,
1048            "Cannot cast struct field 'field1' from type Binary to type Int32"
1049        );
1050    }
1051
1052    #[test]
1053    fn test_validate_struct_compatibility_no_overlap_equal_len() {
1054        let source_fields = vec![
1055            arc_field("left", DataType::Int32),
1056            arc_field("right", DataType::Utf8),
1057        ];
1058
1059        let target_fields = vec![
1060            arc_field("alpha", DataType::Int32),
1061            arc_field("beta", DataType::Utf8),
1062        ];
1063
1064        let result = validate_struct_compatibility(&source_fields, &target_fields);
1065        assert!(result.is_err());
1066        let error_msg = result.unwrap_err().to_string();
1067        assert_contains!(error_msg, "no field name overlap");
1068    }
1069
1070    #[test]
1071    fn test_validate_struct_compatibility_mixed_name_overlap() {
1072        // Source struct: {a: Int32, b: String, extra: Boolean}
1073        let source_fields = vec![
1074            arc_field("a", DataType::Int32),
1075            arc_field("b", DataType::Utf8),
1076            arc_field("extra", DataType::Boolean),
1077        ];
1078
1079        // Target struct: {b: String, a: Int64, c: Float32}
1080        // Name overlap with a and b, missing c (nullable)
1081        let target_fields = vec![
1082            arc_field("b", DataType::Utf8),
1083            arc_field("a", DataType::Int64),
1084            arc_field("c", DataType::Float32),
1085        ];
1086
1087        let result = validate_struct_compatibility(&source_fields, &target_fields);
1088        assert!(result.is_ok());
1089    }
1090
1091    #[test]
1092    fn test_validate_struct_compatibility_by_name_missing_required_field() {
1093        // Source struct: {field1: Int32} (missing field2)
1094        let source_fields = vec![arc_field("field1", DataType::Int32)];
1095
1096        // Target struct: {field1: Int32, field2: Int32 non-nullable}
1097        let target_fields = vec![
1098            arc_field("field1", DataType::Int32),
1099            Arc::new(non_null_field("field2", DataType::Int32)),
1100        ];
1101
1102        let result = validate_struct_compatibility(&source_fields, &target_fields);
1103        assert!(result.is_err());
1104        let error_msg = result.unwrap_err().to_string();
1105        assert_contains!(
1106            error_msg,
1107            "Cannot cast struct: target field 'field2' is non-nullable but missing from source. Cannot fill with NULL."
1108        );
1109    }
1110
1111    #[test]
1112    fn test_validate_struct_compatibility_partial_name_overlap_with_count_mismatch() {
1113        // Source struct: {a: Int32} (only one field)
1114        let source_fields = vec![arc_field("a", DataType::Int32)];
1115
1116        // Target struct: {a: Int32, b: String} (two fields, but 'a' overlaps)
1117        let target_fields = vec![
1118            arc_field("a", DataType::Int32),
1119            arc_field("b", DataType::Utf8),
1120        ];
1121
1122        // This should succeed - partial overlap means by-name mapping
1123        // and missing field 'b' is nullable
1124        let result = validate_struct_compatibility(&source_fields, &target_fields);
1125        assert!(result.is_ok());
1126    }
1127
1128    #[test]
1129    fn test_cast_nested_struct_with_extra_and_missing_fields() {
1130        // Source inner struct has fields a, b, extra
1131        let a = Arc::new(Int32Array::from(vec![Some(1), None])) as ArrayRef;
1132        let b = Arc::new(Int32Array::from(vec![Some(2), Some(3)])) as ArrayRef;
1133        let extra = Arc::new(Int32Array::from(vec![Some(9), Some(10)])) as ArrayRef;
1134
1135        let inner = StructArray::from(vec![
1136            (arc_field("a", DataType::Int32), a),
1137            (arc_field("b", DataType::Int32), b),
1138            (arc_field("extra", DataType::Int32), extra),
1139        ]);
1140
1141        let source_struct = StructArray::from(vec![(
1142            arc_struct_field(
1143                "inner",
1144                vec![
1145                    field("a", DataType::Int32),
1146                    field("b", DataType::Int32),
1147                    field("extra", DataType::Int32),
1148                ],
1149            ),
1150            Arc::new(inner) as ArrayRef,
1151        )]);
1152        let source_col = Arc::new(source_struct) as ArrayRef;
1153
1154        // Target inner struct reorders fields, adds "missing", and drops "extra"
1155        let target_field = struct_field(
1156            "outer",
1157            vec![struct_field(
1158                "inner",
1159                vec![
1160                    field("b", DataType::Int64),
1161                    field("a", DataType::Int32),
1162                    field("missing", DataType::Int32),
1163                ],
1164            )],
1165        );
1166
1167        let result =
1168            cast_column(&source_col, target_field.data_type(), &DEFAULT_CAST_OPTIONS)
1169                .unwrap();
1170        let outer = result.as_any().downcast_ref::<StructArray>().unwrap();
1171        let inner = get_column_as!(&outer, "inner", StructArray);
1172        assert_eq!(inner.fields().len(), 3);
1173
1174        let b = get_column_as!(inner, "b", Int64Array);
1175        assert_eq!(b.value(0), 2);
1176        assert_eq!(b.value(1), 3);
1177        assert!(!b.is_null(0));
1178        assert!(!b.is_null(1));
1179
1180        let a = get_column_as!(inner, "a", Int32Array);
1181        assert_eq!(a.value(0), 1);
1182        assert!(a.is_null(1));
1183
1184        let missing = get_column_as!(inner, "missing", Int32Array);
1185        assert!(missing.is_null(0));
1186        assert!(missing.is_null(1));
1187    }
1188
1189    #[test]
1190    fn test_cast_null_struct_field_to_nested_struct() {
1191        let null_inner = Arc::new(NullArray::new(2)) as ArrayRef;
1192        let source_struct = StructArray::from(vec![(
1193            arc_field("inner", DataType::Null),
1194            Arc::clone(&null_inner),
1195        )]);
1196        let source_col = Arc::new(source_struct) as ArrayRef;
1197
1198        let target_field = struct_field(
1199            "outer",
1200            vec![struct_field("inner", vec![field("a", DataType::Int32)])],
1201        );
1202
1203        let result =
1204            cast_column(&source_col, target_field.data_type(), &DEFAULT_CAST_OPTIONS)
1205                .unwrap();
1206        let outer = result.as_any().downcast_ref::<StructArray>().unwrap();
1207        let inner = get_column_as!(&outer, "inner", StructArray);
1208        assert_eq!(inner.len(), 2);
1209        assert!(inner.is_null(0));
1210        assert!(inner.is_null(1));
1211
1212        let inner_a = get_column_as!(inner, "a", Int32Array);
1213        assert!(inner_a.is_null(0));
1214        assert!(inner_a.is_null(1));
1215    }
1216
1217    #[test]
1218    fn test_cast_struct_with_array_and_map_fields() {
1219        // Array field with second row null
1220        let arr_array = Arc::new(ListArray::from_iter_primitive::<Int32Type, _, _>(vec![
1221            Some(vec![Some(1), Some(2)]),
1222            None,
1223        ])) as ArrayRef;
1224
1225        // Map field with second row null
1226        let string_builder = StringBuilder::new();
1227        let int_builder = Int32Builder::new();
1228        let mut map_builder = MapBuilder::new(None, string_builder, int_builder);
1229        map_builder.keys().append_value("a");
1230        map_builder.values().append_value(1);
1231        map_builder.append(true).unwrap();
1232        map_builder.append(false).unwrap();
1233        let map_array = Arc::new(map_builder.finish()) as ArrayRef;
1234
1235        let source_struct = StructArray::from(vec![
1236            (
1237                arc_field(
1238                    "arr",
1239                    DataType::List(Arc::new(field("item", DataType::Int32))),
1240                ),
1241                arr_array,
1242            ),
1243            (
1244                arc_field(
1245                    "map",
1246                    DataType::Map(
1247                        Arc::new(non_null_field(
1248                            "entries",
1249                            struct_type(vec![
1250                                non_null_field("keys", DataType::Utf8),
1251                                field("values", DataType::Int32),
1252                            ]),
1253                        )),
1254                        false,
1255                    ),
1256                ),
1257                map_array,
1258            ),
1259        ]);
1260        let source_col = Arc::new(source_struct) as ArrayRef;
1261
1262        let target_field = struct_field(
1263            "s",
1264            vec![
1265                field(
1266                    "arr",
1267                    DataType::List(Arc::new(field("item", DataType::Int32))),
1268                ),
1269                field(
1270                    "map",
1271                    DataType::Map(
1272                        Arc::new(non_null_field(
1273                            "entries",
1274                            struct_type(vec![
1275                                non_null_field("keys", DataType::Utf8),
1276                                field("values", DataType::Int32),
1277                            ]),
1278                        )),
1279                        false,
1280                    ),
1281                ),
1282            ],
1283        );
1284
1285        let result =
1286            cast_column(&source_col, target_field.data_type(), &DEFAULT_CAST_OPTIONS)
1287                .unwrap();
1288        let struct_array = result.as_any().downcast_ref::<StructArray>().unwrap();
1289
1290        let arr = get_column_as!(&struct_array, "arr", ListArray);
1291        assert!(!arr.is_null(0));
1292        assert!(arr.is_null(1));
1293        let arr0 = arr.value(0);
1294        let values = arr0.as_any().downcast_ref::<Int32Array>().unwrap();
1295        assert_eq!(values.value(0), 1);
1296        assert_eq!(values.value(1), 2);
1297
1298        let map = get_column_as!(&struct_array, "map", MapArray);
1299        assert!(!map.is_null(0));
1300        assert!(map.is_null(1));
1301        let map0 = map.value(0);
1302        let entries = map0.as_any().downcast_ref::<StructArray>().unwrap();
1303        let keys = get_column_as!(entries, "keys", StringArray);
1304        let vals = get_column_as!(entries, "values", Int32Array);
1305        assert_eq!(keys.value(0), "a");
1306        assert_eq!(vals.value(0), 1);
1307    }
1308
1309    #[test]
1310    fn test_cast_struct_field_order_differs() {
1311        let a = Arc::new(Int32Array::from(vec![Some(1), Some(2)])) as ArrayRef;
1312        let b = Arc::new(Int32Array::from(vec![Some(3), None])) as ArrayRef;
1313
1314        let source_struct = StructArray::from(vec![
1315            (arc_field("a", DataType::Int32), a),
1316            (arc_field("b", DataType::Int32), b),
1317        ]);
1318        let source_col = Arc::new(source_struct) as ArrayRef;
1319
1320        let target_field = struct_field(
1321            "s",
1322            vec![field("b", DataType::Int64), field("a", DataType::Int32)],
1323        );
1324
1325        let result =
1326            cast_column(&source_col, target_field.data_type(), &DEFAULT_CAST_OPTIONS)
1327                .unwrap();
1328        let struct_array = result.as_any().downcast_ref::<StructArray>().unwrap();
1329
1330        let b_col = get_column_as!(&struct_array, "b", Int64Array);
1331        assert_eq!(b_col.value(0), 3);
1332        assert!(b_col.is_null(1));
1333
1334        let a_col = get_column_as!(&struct_array, "a", Int32Array);
1335        assert_eq!(a_col.value(0), 1);
1336        assert_eq!(a_col.value(1), 2);
1337    }
1338
1339    #[test]
1340    fn test_cast_struct_no_overlap_rejected() {
1341        let first = Arc::new(Int32Array::from(vec![Some(10), Some(20)])) as ArrayRef;
1342        let second =
1343            Arc::new(StringArray::from(vec![Some("alpha"), Some("beta")])) as ArrayRef;
1344
1345        let source_struct = StructArray::from(vec![
1346            (arc_field("left", DataType::Int32), first),
1347            (arc_field("right", DataType::Utf8), second),
1348        ]);
1349        let source_col = Arc::new(source_struct) as ArrayRef;
1350
1351        let target_field = struct_field(
1352            "s",
1353            vec![field("a", DataType::Int64), field("b", DataType::Utf8)],
1354        );
1355
1356        let result =
1357            cast_column(&source_col, target_field.data_type(), &DEFAULT_CAST_OPTIONS);
1358        assert!(result.is_err());
1359        let error_msg = result.unwrap_err().to_string();
1360        assert_contains!(error_msg, "no field name overlap");
1361    }
1362
1363    #[test]
1364    fn test_cast_struct_missing_non_nullable_field_fails() {
1365        // Source has only field 'a'
1366        let a = Arc::new(Int32Array::from(vec![Some(1), Some(2)])) as ArrayRef;
1367        let source_struct = StructArray::from(vec![(arc_field("a", DataType::Int32), a)]);
1368        let source_col = Arc::new(source_struct) as ArrayRef;
1369
1370        // Target has fields 'a' (nullable) and 'b' (non-nullable)
1371        let target_field = struct_field(
1372            "s",
1373            vec![
1374                field("a", DataType::Int32),
1375                non_null_field("b", DataType::Int32),
1376            ],
1377        );
1378
1379        // Should fail because 'b' is non-nullable but missing from source
1380        let result =
1381            cast_column(&source_col, target_field.data_type(), &DEFAULT_CAST_OPTIONS);
1382        assert!(result.is_err());
1383        let err = result.unwrap_err();
1384        assert!(
1385            err.to_string()
1386                .contains("target field 'b' is non-nullable but missing from source"),
1387            "Unexpected error: {err}"
1388        );
1389    }
1390
1391    #[test]
1392    fn test_cast_struct_missing_nullable_field_succeeds() {
1393        // Source has only field 'a'
1394        let a = Arc::new(Int32Array::from(vec![Some(1), Some(2)])) as ArrayRef;
1395        let source_struct = StructArray::from(vec![(arc_field("a", DataType::Int32), a)]);
1396        let source_col = Arc::new(source_struct) as ArrayRef;
1397
1398        // Target has fields 'a' and 'b' (both nullable)
1399        let target_field = struct_field(
1400            "s",
1401            vec![field("a", DataType::Int32), field("b", DataType::Int32)],
1402        );
1403
1404        // Should succeed - 'b' is nullable so can be filled with NULL
1405        let result =
1406            cast_column(&source_col, target_field.data_type(), &DEFAULT_CAST_OPTIONS)
1407                .unwrap();
1408        let struct_array = result.as_any().downcast_ref::<StructArray>().unwrap();
1409
1410        let a_col = get_column_as!(&struct_array, "a", Int32Array);
1411        assert_eq!(a_col.value(0), 1);
1412        assert_eq!(a_col.value(1), 2);
1413
1414        let b_col = get_column_as!(&struct_array, "b", Int32Array);
1415        assert!(b_col.is_null(0));
1416        assert!(b_col.is_null(1));
1417    }
1418
1419    #[test]
1420    fn test_validate_dictionary_value_evolution() {
1421        let source_inner = struct_type(vec![field("a", DataType::Int32)]);
1422        let target_inner = struct_type(vec![
1423            field("a", DataType::Int32),
1424            field("b", DataType::Utf8),
1425        ]);
1426        let source =
1427            DataType::Dictionary(Box::new(DataType::Int32), Box::new(source_inner));
1428        let target =
1429            DataType::Dictionary(Box::new(DataType::Int32), Box::new(target_inner));
1430        assert!(validate_data_type_compatibility("col", &source, &target).is_ok());
1431    }
1432
1433    #[test]
1434    fn test_cast_dictionary_struct_value() {
1435        // Build a Dictionary<Int32, Struct{a: Int32}> and cast to
1436        // Dictionary<Int32, Struct{a: Int64, b: Utf8}> (field added, type widened).
1437        let struct_arr = StructArray::from(vec![(
1438            arc_field("a", DataType::Int32),
1439            Arc::new(Int32Array::from(vec![10, 20])) as ArrayRef,
1440        )]);
1441        // keys: [0, null, 1] mapping into the 2-element struct values array.
1442        let keys = Int32Array::from(vec![Some(0), None, Some(1)]);
1443        let source_dict = DictionaryArray::<Int32Type>::new(keys, Arc::new(struct_arr));
1444        let source_col: ArrayRef = Arc::new(source_dict);
1445
1446        let target_type = DataType::Dictionary(
1447            Box::new(DataType::Int32),
1448            Box::new(struct_type(vec![
1449                field("a", DataType::Int64),
1450                field("b", DataType::Utf8),
1451            ])),
1452        );
1453
1454        let result =
1455            cast_column(&source_col, &target_type, &DEFAULT_CAST_OPTIONS).unwrap();
1456        let result_dict = result
1457            .as_any()
1458            .downcast_ref::<DictionaryArray<Int32Type>>()
1459            .unwrap();
1460
1461        assert!(result_dict.is_valid(0));
1462        assert!(result_dict.is_null(1));
1463        assert!(result_dict.is_valid(2));
1464
1465        let struct_values = result_dict
1466            .values()
1467            .as_any()
1468            .downcast_ref::<StructArray>()
1469            .unwrap();
1470        let a_col = get_column_as!(&struct_values, "a", Int64Array);
1471        assert_eq!(a_col.values(), &[10, 20]);
1472        let b_col = get_column_as!(&struct_values, "b", StringArray);
1473        assert!(b_col.iter().all(|v| v.is_none()));
1474    }
1475
1476    #[test]
1477    fn test_cast_list_view_struct() {
1478        // Build a ListView<Struct{a: Int32}> and cast to
1479        // ListView<Struct{a: Int64, b: Utf8}>.
1480        let struct_arr = StructArray::from(vec![(
1481            arc_field("a", DataType::Int32),
1482            Arc::new(Int32Array::from(vec![1, 2, 3])) as ArrayRef,
1483        )]);
1484
1485        let source_field =
1486            arc_field("item", struct_type(vec![field("a", DataType::Int32)]));
1487        let target_field = arc_field(
1488            "item",
1489            struct_type(vec![
1490                field("a", DataType::Int64),
1491                field("b", DataType::Utf8),
1492            ]),
1493        );
1494
1495        // Two list-view entries: [0..2] and [2..3]
1496        let list_view = ListViewArray::new(
1497            source_field,
1498            ScalarBuffer::from(vec![0i32, 2]),
1499            ScalarBuffer::from(vec![2i32, 1]),
1500            Arc::new(struct_arr),
1501            None,
1502        );
1503        let source_col: ArrayRef = Arc::new(list_view);
1504
1505        let target_type = DataType::ListView(target_field);
1506
1507        let result =
1508            cast_column(&source_col, &target_type, &DEFAULT_CAST_OPTIONS).unwrap();
1509        let result_lv = result.as_any().downcast_ref::<ListViewArray>().unwrap();
1510        assert_eq!(result_lv.len(), 2);
1511
1512        let struct_values = result_lv
1513            .values()
1514            .as_any()
1515            .downcast_ref::<StructArray>()
1516            .unwrap();
1517        let a_col = get_column_as!(&struct_values, "a", Int64Array);
1518        assert_eq!(a_col.values(), &[1, 2, 3]);
1519        let b_col = get_column_as!(&struct_values, "b", StringArray);
1520        assert!(b_col.iter().all(|v| v.is_none()));
1521    }
1522
1523    fn fixed_size_list_struct_field(fields: Vec<(&str, DataType)>) -> FieldRef {
1524        arc_field(
1525            "item",
1526            struct_type(
1527                fields
1528                    .into_iter()
1529                    .map(|(name, data_type)| field(name, data_type))
1530                    .collect(),
1531            ),
1532        )
1533    }
1534
1535    fn create_fixed_size_list_test_fields(
1536        source_struct_fields: Vec<(&str, DataType)>,
1537        target_struct_fields: Vec<(&str, DataType)>,
1538    ) -> (FieldRef, FieldRef) {
1539        (
1540            fixed_size_list_struct_field(source_struct_fields),
1541            fixed_size_list_struct_field(target_struct_fields),
1542        )
1543    }
1544
1545    fn fixed_size_list_struct_values(
1546        array: &ArrayRef,
1547    ) -> (&FixedSizeListArray, &StructArray) {
1548        let list = array.as_any().downcast_ref::<FixedSizeListArray>().unwrap();
1549        let values = list
1550            .values()
1551            .as_any()
1552            .downcast_ref::<StructArray>()
1553            .unwrap();
1554        (list, values)
1555    }
1556
1557    #[test]
1558    fn test_cast_fixed_size_list_struct() {
1559        let struct_arr = StructArray::from(vec![(
1560            arc_field("a", DataType::Int32),
1561            Arc::new(Int32Array::from(vec![1, 2, 3, 4])) as ArrayRef,
1562        )]);
1563
1564        let (source_field, target_field) = create_fixed_size_list_test_fields(
1565            vec![("a", DataType::Int32)],
1566            vec![("a", DataType::Int64), ("b", DataType::Utf8)],
1567        );
1568        let source_col: ArrayRef = Arc::new(FixedSizeListArray::new(
1569            source_field,
1570            2,
1571            Arc::new(struct_arr),
1572            Some(NullBuffer::from(vec![true, false])),
1573        ));
1574        let target_type = DataType::FixedSizeList(target_field, 2);
1575
1576        let result =
1577            cast_column(&source_col, &target_type, &DEFAULT_CAST_OPTIONS).unwrap();
1578        let (result_list, struct_values) = fixed_size_list_struct_values(&result);
1579        assert_eq!(result_list.len(), 2);
1580        assert!(result_list.is_valid(0));
1581        assert!(result_list.is_null(1));
1582        let a_col = get_column_as!(&struct_values, "a", Int64Array);
1583        assert_eq!(a_col.values(), &[1, 2, 3, 4]);
1584        let b_col = get_column_as!(&struct_values, "b", StringArray);
1585        assert!(b_col.iter().all(|v| v.is_none()));
1586    }
1587
1588    #[test]
1589    fn test_validate_fixed_size_list_struct_compatibility() {
1590        let (source_field, target_field) = create_fixed_size_list_test_fields(
1591            vec![("a", DataType::Int32)],
1592            vec![("a", DataType::Int64), ("b", DataType::Utf8)],
1593        );
1594        let source = DataType::FixedSizeList(source_field, 2);
1595        let target = DataType::FixedSizeList(target_field, 2);
1596
1597        assert!(requires_nested_struct_cast(&source, &target));
1598        assert!(validate_data_type_compatibility("col", &source, &target).is_ok());
1599    }
1600
1601    #[test]
1602    fn test_validate_fixed_size_list_struct_missing_non_nullable_field_rejected() {
1603        let (source_field, _) = create_fixed_size_list_test_fields(
1604            vec![("a", DataType::Int32)],
1605            vec![("a", DataType::Int64), ("b", DataType::Utf8)],
1606        );
1607        let source = DataType::FixedSizeList(source_field, 2);
1608        let target = DataType::FixedSizeList(
1609            arc_field(
1610                "item",
1611                struct_type(vec![
1612                    field("a", DataType::Int32),
1613                    non_null_field("b", DataType::Utf8),
1614                ]),
1615            ),
1616            2,
1617        );
1618
1619        let error = validate_data_type_compatibility("col", &source, &target)
1620            .unwrap_err()
1621            .to_string();
1622        assert_contains!(
1623            error,
1624            "target field 'b' is non-nullable but missing from source"
1625        );
1626    }
1627
1628    #[test]
1629    fn test_fixed_size_list_struct_size_mismatch_rejected() {
1630        let source_field = fixed_size_list_struct_field(vec![("a", DataType::Int32)]);
1631        let target_field = Arc::clone(&source_field);
1632        let source_type = DataType::FixedSizeList(Arc::clone(&source_field), 2);
1633        let target_type = DataType::FixedSizeList(target_field, 3);
1634
1635        let validation_error =
1636            validate_data_type_compatibility("col", &source_type, &target_type)
1637                .unwrap_err()
1638                .to_string();
1639        assert_contains!(validation_error, "Cannot cast struct field 'col'");
1640
1641        let struct_arr = StructArray::from(vec![(
1642            arc_field("a", DataType::Int32),
1643            Arc::new(Int32Array::from(vec![1, 2])) as ArrayRef,
1644        )]);
1645        let source_col: ArrayRef = Arc::new(FixedSizeListArray::new(
1646            source_field,
1647            2,
1648            Arc::new(struct_arr),
1649            None,
1650        ));
1651
1652        let runtime_error = cast_column(&source_col, &target_type, &DEFAULT_CAST_OPTIONS)
1653            .unwrap_err()
1654            .to_string();
1655        assert_contains!(
1656            runtime_error,
1657            "cannot cast fixed-size-list to fixed-size-list with different size"
1658        );
1659    }
1660
1661    #[test]
1662    fn test_cast_fixed_size_list_struct_all_null() {
1663        let (source_field, target_field) = create_fixed_size_list_test_fields(
1664            vec![("a", DataType::Int32)],
1665            vec![("a", DataType::Int64), ("b", DataType::Utf8)],
1666        );
1667        let source_col: ArrayRef =
1668            Arc::new(FixedSizeListArray::new_null(source_field, 2, 2));
1669        let target_type = DataType::FixedSizeList(target_field, 2);
1670
1671        let result =
1672            cast_column(&source_col, &target_type, &DEFAULT_CAST_OPTIONS).unwrap();
1673        let (result_list, struct_values) = fixed_size_list_struct_values(&result);
1674        assert_eq!(result_list.null_count(), 2);
1675        let a_col = get_column_as!(&struct_values, "a", Int64Array);
1676        let b_col = get_column_as!(&struct_values, "b", StringArray);
1677        assert!(a_col.iter().all(|v| v.is_none()));
1678        assert!(b_col.iter().all(|v| v.is_none()));
1679    }
1680
1681    #[test]
1682    fn test_fixed_size_list_struct_planner_runtime_parity_on_incompatible_type() {
1683        let source_field =
1684            arc_field("item", struct_type(vec![field("a", DataType::Binary)]));
1685        let target_field =
1686            arc_field("item", struct_type(vec![field("a", DataType::Int32)]));
1687        let source_type = DataType::FixedSizeList(Arc::clone(&source_field), 2);
1688        let target_type = DataType::FixedSizeList(target_field, 2);
1689        let validation_error =
1690            validate_data_type_compatibility("col", &source_type, &target_type)
1691                .unwrap_err()
1692                .to_string();
1693        assert_contains!(validation_error, "Cannot cast struct field 'a'");
1694
1695        let struct_arr = StructArray::from(vec![(
1696            arc_field("a", DataType::Binary),
1697            Arc::new(BinaryArray::from(vec![
1698                Some(b"x".as_ref()),
1699                Some(b"y".as_ref()),
1700            ])) as ArrayRef,
1701        )]);
1702        let source_col: ArrayRef = Arc::new(FixedSizeListArray::new(
1703            source_field,
1704            2,
1705            Arc::new(struct_arr),
1706            None,
1707        ));
1708
1709        let runtime_error = cast_column(&source_col, &target_type, &DEFAULT_CAST_OPTIONS)
1710            .unwrap_err()
1711            .to_string();
1712        assert_contains!(runtime_error, "Cannot cast struct field 'a'");
1713    }
1714
1715    #[test]
1716    fn test_cast_fixed_size_list_struct_missing_non_nullable_field_runtime_rejected() {
1717        let source_field =
1718            arc_field("item", struct_type(vec![field("a", DataType::Int32)]));
1719        let target_field = arc_field(
1720            "item",
1721            struct_type(vec![
1722                field("a", DataType::Int32),
1723                non_null_field("b", DataType::Utf8),
1724            ]),
1725        );
1726        let source_col: ArrayRef =
1727            Arc::new(FixedSizeListArray::new_null(source_field, 2, 1));
1728        let target_type = DataType::FixedSizeList(target_field, 2);
1729
1730        let error = cast_column(&source_col, &target_type, &DEFAULT_CAST_OPTIONS)
1731            .unwrap_err()
1732            .to_string();
1733        assert_contains!(
1734            error,
1735            "target field 'b' is non-nullable but missing from source"
1736        );
1737    }
1738
1739    #[test]
1740    fn test_cast_fixed_size_list_returns_error_for_non_nullable_child() {
1741        let source_field = Arc::new(Field::new("item", DataType::Int32, true));
1742        let target_field = Arc::new(Field::new("item", DataType::Int32, false));
1743        let source_col: ArrayRef = Arc::new(FixedSizeListArray::new(
1744            source_field,
1745            2,
1746            Arc::new(Int32Array::from(vec![None, Some(1)])),
1747            None,
1748        ));
1749        let target_type = DataType::FixedSizeList(target_field, 2);
1750
1751        let error = cast_column(&source_col, &target_type, &DEFAULT_CAST_OPTIONS)
1752            .unwrap_err()
1753            .to_string();
1754        assert_contains!(error, "Found unmasked nulls for non-nullable");
1755    }
1756
1757    #[test]
1758    fn test_cast_sliced_fixed_size_list_struct_ignores_hidden_child_values() {
1759        let source_field =
1760            arc_field("item", struct_type(vec![field("a", DataType::Utf8)]));
1761        let target_field =
1762            arc_field("item", struct_type(vec![field("a", DataType::Int32)]));
1763        let struct_arr = StructArray::from(vec![(
1764            arc_field("a", DataType::Utf8),
1765            Arc::new(StringArray::from(vec![
1766                "0", "0", "not_int", "also_bad", "1", "2",
1767            ])) as ArrayRef,
1768        )]);
1769        let source_col: ArrayRef = Arc::new(
1770            FixedSizeListArray::new(
1771                source_field,
1772                2,
1773                Arc::new(struct_arr),
1774                Some(NullBuffer::from(vec![true, false, true])),
1775            )
1776            .slice(1, 2),
1777        );
1778        let target_type = DataType::FixedSizeList(target_field, 2);
1779
1780        let result =
1781            cast_column(&source_col, &target_type, &DEFAULT_CAST_OPTIONS).unwrap();
1782        let (result_list, struct_values) = fixed_size_list_struct_values(&result);
1783        assert!(result_list.is_null(0));
1784        assert!(result_list.is_valid(1));
1785        let a_col = get_column_as!(&struct_values, "a", Int32Array);
1786        assert!(a_col.is_null(0));
1787        assert!(a_col.is_null(1));
1788        assert_eq!(a_col.value(2), 1);
1789        assert_eq!(a_col.value(3), 2);
1790    }
1791
1792    #[test]
1793    fn test_requires_nested_struct_cast() {
1794        let s1 = struct_type(vec![field("a", DataType::Int32)]);
1795        let s2 = struct_type(vec![field("a", DataType::Int64)]);
1796
1797        assert!(requires_nested_struct_cast(&s1, &s2));
1798        assert!(requires_nested_struct_cast(
1799            &DataType::List(arc_field("item", s1.clone())),
1800            &DataType::List(arc_field("item", s2.clone())),
1801        ));
1802        assert!(requires_nested_struct_cast(
1803            &DataType::Dictionary(Box::new(DataType::Int32), Box::new(s1.clone())),
1804            &DataType::Dictionary(Box::new(DataType::Int32), Box::new(s2.clone())),
1805        ));
1806        assert!(requires_nested_struct_cast(
1807            &DataType::ListView(arc_field("item", s1.clone())),
1808            &DataType::ListView(arc_field("item", s2.clone())),
1809        ));
1810        assert!(requires_nested_struct_cast(
1811            &DataType::FixedSizeList(arc_field("item", s1), 2),
1812            &DataType::FixedSizeList(arc_field("item", s2), 2),
1813        ));
1814
1815        // Non-struct types should return false.
1816        assert!(!requires_nested_struct_cast(
1817            &DataType::Int32,
1818            &DataType::Int64
1819        ));
1820        assert!(!requires_nested_struct_cast(
1821            &DataType::List(arc_field("item", DataType::Int32)),
1822            &DataType::List(arc_field("item", DataType::Int64)),
1823        ));
1824        assert!(!requires_nested_struct_cast(
1825            &DataType::FixedSizeList(arc_field("item", DataType::Int32), 2),
1826            &DataType::FixedSizeList(arc_field("item", DataType::Int64), 2),
1827        ));
1828    }
1829}
1830
1831/// Adapts a `RecordBatch` to conform to `target_schema`, verifying that each target field
1832/// type contains the incoming column data type (as verified by [`arrow::datatypes::DataType::contains`])
1833/// and transforms the metadata/types of differing columns to match `target_schema`
1834/// without copying primitive buffer data.
1835///
1836/// If `batch` has an incompatible column count or incompatible column data types,
1837/// an error is returned.
1838pub fn adapt_batch_to_schema(
1839    batch: RecordBatch,
1840    target_schema: &SchemaRef,
1841) -> Result<RecordBatch> {
1842    if Arc::ptr_eq(batch.schema_ref(), target_schema)
1843        || batch.schema().as_ref() == target_schema.as_ref()
1844    {
1845        return Ok(batch);
1846    }
1847
1848    if batch.num_columns() != target_schema.fields().len() {
1849        return _plan_err!(
1850            "Batch schema does not conform to expected schema (column count mismatch). Expected: {target_schema}, got: {}",
1851            batch.schema()
1852        );
1853    }
1854
1855    let mut columns = Vec::with_capacity(batch.num_columns());
1856    let mut needs_column_adaptation = false;
1857    let cast_options = CastOptions::default();
1858
1859    for (target_field, col) in target_schema.fields().iter().zip(batch.columns()) {
1860        if target_field.data_type() != col.data_type() {
1861            // If data types differ, verify that target_field's data type contains
1862            // the column's data type (e.g. stricter nested struct / list field nullability).
1863            if !target_field.data_type().contains(col.data_type()) {
1864                return _plan_err!(
1865                    "Batch column '{}' with type {} cannot be adapted to expected type {}",
1866                    target_field.name(),
1867                    col.data_type(),
1868                    target_field.data_type()
1869                );
1870            }
1871            needs_column_adaptation = true;
1872            let adapted_col = cast_column(col, target_field.data_type(), &cast_options)?;
1873            columns.push(adapted_col);
1874        } else {
1875            columns.push(Arc::clone(col));
1876        }
1877    }
1878
1879    if needs_column_adaptation {
1880        Ok(RecordBatch::try_new(Arc::clone(target_schema), columns)?)
1881    } else {
1882        // Schema differs only in top-level metadata or field nullability, while
1883        // column data types match exactly. Replace the schema on the batch.
1884        Ok(RecordBatch::try_new(
1885            Arc::clone(target_schema),
1886            batch.columns().to_vec(),
1887        )?)
1888    }
1889}
1890
1891#[cfg(test)]
1892mod adapt_schema_tests {
1893    use super::*;
1894    use arrow::array::{Int32Array, StringArray};
1895    use arrow::datatypes::{Field, Fields, Schema};
1896
1897    #[test]
1898    fn test_adapt_batch_to_schema_identical() -> Result<()> {
1899        let schema = Arc::new(Schema::new(vec![
1900            Field::new("a", DataType::Int32, false),
1901            Field::new("b", DataType::Utf8, true),
1902        ]));
1903
1904        let a = Arc::new(Int32Array::from(vec![1, 2, 3])) as ArrayRef;
1905        let b = Arc::new(StringArray::from(vec![Some("x"), None, Some("z")])) as ArrayRef;
1906        let batch = RecordBatch::try_new(Arc::clone(&schema), vec![a, b])?;
1907
1908        let adapted = adapt_batch_to_schema(batch.clone(), &schema)?;
1909        assert_eq!(adapted, batch);
1910        Ok(())
1911    }
1912
1913    #[test]
1914    fn test_adapt_batch_to_schema_stricter_nested_struct() -> Result<()> {
1915        // Declared table schema: {a: Struct({x: Int32 (nullable), y: Utf8 (nullable)})}
1916        let declared_inner_fields = Fields::from(vec![
1917            Field::new("x", DataType::Int32, true),
1918            Field::new("y", DataType::Utf8, true),
1919        ]);
1920        let declared_schema = Arc::new(Schema::new(vec![Field::new(
1921            "a",
1922            Struct(declared_inner_fields),
1923            false,
1924        )]));
1925
1926        // Runtime batch schema: {a: Struct({x: Int32 (NON-nullable), y: Utf8 (NON-nullable)})}
1927        let runtime_inner_fields = Fields::from(vec![
1928            Field::new("x", DataType::Int32, false),
1929            Field::new("y", DataType::Utf8, false),
1930        ]);
1931        let runtime_schema = Arc::new(Schema::new(vec![Field::new(
1932            "a",
1933            Struct(runtime_inner_fields.clone()),
1934            false,
1935        )]));
1936
1937        let x = Arc::new(Int32Array::from(vec![1, 2, 3])) as ArrayRef;
1938        let y = Arc::new(StringArray::from(vec!["x", "y", "z"])) as ArrayRef;
1939        let struct_array =
1940            Arc::new(StructArray::new(runtime_inner_fields, vec![x, y], None))
1941                as ArrayRef;
1942        let batch = RecordBatch::try_new(runtime_schema, vec![struct_array])?;
1943
1944        let adapted = adapt_batch_to_schema(batch, &declared_schema)?;
1945        assert_eq!(adapted.schema().as_ref(), declared_schema.as_ref());
1946        assert_eq!(adapted.num_rows(), 3);
1947
1948        // Verify nested fields now have the declared nullability
1949        let Struct(fields) = adapted.column(0).data_type() else {
1950            panic!("expected struct");
1951        };
1952        assert!(fields[0].is_nullable());
1953        assert!(fields[1].is_nullable());
1954        Ok(())
1955    }
1956
1957    #[test]
1958    fn test_adapt_batch_to_schema_top_level_nullability_only() -> Result<()> {
1959        // Declared schema has nullable column 'a', runtime batch has non-nullable 'a'
1960        let declared_schema =
1961            Arc::new(Schema::new(vec![Field::new("a", DataType::Int32, true)]));
1962        let runtime_schema =
1963            Arc::new(Schema::new(vec![Field::new("a", DataType::Int32, false)]));
1964
1965        let a = Arc::new(Int32Array::from(vec![1, 2, 3])) as ArrayRef;
1966        let batch = RecordBatch::try_new(runtime_schema, vec![a])?;
1967
1968        let adapted = adapt_batch_to_schema(batch, &declared_schema)?;
1969        assert_eq!(adapted.schema().as_ref(), declared_schema.as_ref());
1970        assert!(adapted.schema().field(0).is_nullable());
1971        Ok(())
1972    }
1973
1974    #[test]
1975    fn test_adapt_batch_to_schema_null_into_non_nullable_rejected() {
1976        // Declared schema is non-nullable, but runtime batch is nullable
1977        let declared_schema =
1978            Arc::new(Schema::new(vec![Field::new("a", DataType::Int32, false)]));
1979        let runtime_schema =
1980            Arc::new(Schema::new(vec![Field::new("a", DataType::Int32, true)]));
1981
1982        let a = Arc::new(Int32Array::from(vec![Some(1), None, Some(3)])) as ArrayRef;
1983        let batch = RecordBatch::try_new(runtime_schema, vec![a]).unwrap();
1984
1985        // Must reject because nullable is not contained by non-nullable
1986        let result = adapt_batch_to_schema(batch, &declared_schema);
1987        assert!(result.is_err());
1988    }
1989
1990    #[test]
1991    fn test_adapt_batch_to_schema_incompatible_type_rejected() {
1992        let declared_schema =
1993            Arc::new(Schema::new(vec![Field::new("a", DataType::Int32, true)]));
1994        let runtime_schema =
1995            Arc::new(Schema::new(vec![Field::new("a", DataType::Utf8, true)]));
1996
1997        let a = Arc::new(StringArray::from(vec!["1", "2"])) as ArrayRef;
1998        let batch = RecordBatch::try_new(runtime_schema, vec![a]).unwrap();
1999
2000        let result = adapt_batch_to_schema(batch, &declared_schema);
2001        assert!(result.is_err());
2002    }
2003
2004    fn test_two_field_union(nullable: bool) -> UnionFields {
2005        UnionFields::try_new(
2006            vec![0, 1],
2007            vec![
2008                Field::new("value", DataType::Int32, nullable),
2009                Field::new("str", DataType::Utf8, nullable),
2010            ],
2011        )
2012        .unwrap()
2013    }
2014
2015    #[test]
2016    fn test_adapt_batch_to_schema_stricter_sparse_union() -> Result<()> {
2017        use arrow::array::UnionArray;
2018        use arrow::buffer::ScalarBuffer;
2019        use arrow::datatypes::UnionMode;
2020
2021        let target_union_fields = test_two_field_union(true);
2022        let declared_schema = Arc::new(Schema::new(vec![Field::new(
2023            "u",
2024            DataType::Union(target_union_fields, UnionMode::Sparse),
2025            false,
2026        )]));
2027
2028        let source_union_fields = test_two_field_union(false);
2029        let runtime_schema = Arc::new(Schema::new(vec![Field::new(
2030            "u",
2031            DataType::Union(source_union_fields.clone(), UnionMode::Sparse),
2032            false,
2033        )]));
2034
2035        let int_array: ArrayRef = Arc::new(Int32Array::from(vec![10, 20, 30]));
2036        let str_array: ArrayRef =
2037            Arc::new(StringArray::from(vec!["hello", "world", "!"]));
2038        let type_ids = [0, 0, 1].into_iter().collect::<ScalarBuffer<i8>>();
2039        let source_union = UnionArray::try_new(
2040            source_union_fields,
2041            type_ids,
2042            None,
2043            vec![int_array, str_array],
2044        )?;
2045        let batch = RecordBatch::try_new(runtime_schema, vec![Arc::new(source_union)])?;
2046
2047        let adapted = adapt_batch_to_schema(batch, &declared_schema)?;
2048        assert_eq!(adapted.schema().as_ref(), declared_schema.as_ref());
2049
2050        let adapted_union = adapted
2051            .column(0)
2052            .as_any()
2053            .downcast_ref::<UnionArray>()
2054            .unwrap();
2055        let DataType::Union(fields, mode) = adapted_union.data_type() else {
2056            panic!("expected union");
2057        };
2058        assert_eq!(*mode, UnionMode::Sparse);
2059        assert!(fields.iter().all(|(_, f)| f.is_nullable()));
2060
2061        Ok(())
2062    }
2063
2064    #[test]
2065    fn test_adapt_batch_to_schema_stricter_dense_union() -> Result<()> {
2066        use arrow::array::UnionArray;
2067        use arrow::buffer::ScalarBuffer;
2068        use arrow::datatypes::UnionMode;
2069
2070        let target_union_fields = test_two_field_union(true);
2071        let declared_schema = Arc::new(Schema::new(vec![Field::new(
2072            "u",
2073            DataType::Union(target_union_fields, UnionMode::Dense),
2074            false,
2075        )]));
2076
2077        let source_union_fields = test_two_field_union(false);
2078        let runtime_schema = Arc::new(Schema::new(vec![Field::new(
2079            "u",
2080            DataType::Union(source_union_fields.clone(), UnionMode::Dense),
2081            false,
2082        )]));
2083
2084        let int_array: ArrayRef = Arc::new(Int32Array::from(vec![10, 30]));
2085        let str_array: ArrayRef = Arc::new(StringArray::from(vec!["hello"]));
2086        let type_ids = [0, 1, 0].into_iter().collect::<ScalarBuffer<i8>>();
2087        let offsets = [0, 0, 1].into_iter().collect::<ScalarBuffer<i32>>();
2088        let source_union = UnionArray::try_new(
2089            source_union_fields,
2090            type_ids,
2091            Some(offsets),
2092            vec![int_array, str_array],
2093        )?;
2094        let batch = RecordBatch::try_new(runtime_schema, vec![Arc::new(source_union)])?;
2095
2096        let adapted = adapt_batch_to_schema(batch, &declared_schema)?;
2097        assert_eq!(adapted.schema().as_ref(), declared_schema.as_ref());
2098
2099        let adapted_union = adapted
2100            .column(0)
2101            .as_any()
2102            .downcast_ref::<UnionArray>()
2103            .unwrap();
2104        let DataType::Union(fields, mode) = adapted_union.data_type() else {
2105            panic!("expected union");
2106        };
2107        assert_eq!(*mode, UnionMode::Dense);
2108        assert!(fields.iter().all(|(_, f)| f.is_nullable()));
2109
2110        Ok(())
2111    }
2112
2113    #[test]
2114    fn test_adapt_batch_to_schema_union_reordered_and_non_contiguous_type_ids()
2115    -> Result<()> {
2116        use arrow::array::UnionArray;
2117        use arrow::buffer::ScalarBuffer;
2118        use arrow::datatypes::UnionMode;
2119
2120        let target_union_fields = UnionFields::try_new(
2121            vec![3, 1],
2122            vec![
2123                Field::new("str", DataType::Utf8, true),
2124                Field::new("int", DataType::Int32, true),
2125            ],
2126        )?;
2127        let declared_schema = Arc::new(Schema::new(vec![Field::new(
2128            "u",
2129            DataType::Union(target_union_fields, UnionMode::Dense),
2130            false,
2131        )]));
2132
2133        let source_union_fields = UnionFields::try_new(
2134            vec![1, 3],
2135            vec![
2136                Field::new("int", DataType::Int32, false),
2137                Field::new("str", DataType::Utf8, false),
2138            ],
2139        )?;
2140        let source_schema = Arc::new(Schema::new(vec![Field::new(
2141            "u",
2142            DataType::Union(source_union_fields.clone(), UnionMode::Dense),
2143            false,
2144        )]));
2145
2146        let int_array: ArrayRef = Arc::new(Int32Array::from(vec![10, 30]));
2147        let str_array: ArrayRef = Arc::new(StringArray::from(vec!["b"]));
2148        let type_ids = [1, 3, 1].into_iter().collect::<ScalarBuffer<i8>>();
2149        let offsets = [0, 0, 1].into_iter().collect::<ScalarBuffer<i32>>();
2150        let source_union = UnionArray::try_new(
2151            source_union_fields,
2152            type_ids.clone(),
2153            Some(offsets.clone()),
2154            vec![int_array, str_array],
2155        )?;
2156
2157        let source_batch =
2158            RecordBatch::try_new(source_schema, vec![Arc::new(source_union)])?;
2159
2160        let adapted = adapt_batch_to_schema(source_batch, &declared_schema)?;
2161        assert_eq!(adapted.schema().as_ref(), declared_schema.as_ref());
2162        let adapted_union = adapted
2163            .column(0)
2164            .as_any()
2165            .downcast_ref::<UnionArray>()
2166            .unwrap();
2167        assert_eq!(
2168            adapted_union.data_type(),
2169            declared_schema.field(0).data_type()
2170        );
2171        assert_eq!(adapted_union.type_ids(), &type_ids);
2172        assert_eq!(adapted_union.offsets(), Some(&offsets));
2173
2174        // Child 1 is int, Child 3 is str (accessed by type ID)
2175        let int_child = adapted_union
2176            .child(1)
2177            .as_any()
2178            .downcast_ref::<Int32Array>()
2179            .unwrap();
2180        let str_child = adapted_union
2181            .child(3)
2182            .as_any()
2183            .downcast_ref::<StringArray>()
2184            .unwrap();
2185
2186        // Row 0: type_id 1 -> int value 10
2187        assert_eq!(adapted_union.type_id(0), 1);
2188        assert_eq!(int_child.value(adapted_union.value_offset(0)), 10);
2189
2190        // Row 1: type_id 3 -> str value "b"
2191        assert_eq!(adapted_union.type_id(1), 3);
2192        assert_eq!(str_child.value(adapted_union.value_offset(1)), "b");
2193
2194        // Row 2: type_id 1 -> int value 30
2195        assert_eq!(adapted_union.type_id(2), 1);
2196        assert_eq!(int_child.value(adapted_union.value_offset(2)), 30);
2197
2198        Ok(())
2199    }
2200
2201    #[test]
2202    fn test_adapt_batch_to_schema_union_nested_struct() -> Result<()> {
2203        use arrow::array::UnionArray;
2204        use arrow::buffer::ScalarBuffer;
2205        use arrow::datatypes::{UnionFields, UnionMode};
2206
2207        let target_struct_fields = vec![Field::new("x", DataType::Int32, true)];
2208        let target_union_fields = UnionFields::try_new(
2209            vec![0],
2210            vec![Field::new("s", Struct(target_struct_fields.into()), true)],
2211        )?;
2212        let declared_schema = Arc::new(Schema::new(vec![Field::new(
2213            "u",
2214            DataType::Union(target_union_fields, UnionMode::Dense),
2215            false,
2216        )]));
2217
2218        let source_struct_fields = vec![Field::new("x", DataType::Int32, false)];
2219        let source_union_fields = UnionFields::try_new(
2220            vec![0],
2221            vec![Field::new("s", Struct(source_struct_fields.into()), false)],
2222        )?;
2223        let source_schema = Arc::new(Schema::new(vec![Field::new(
2224            "u",
2225            DataType::Union(source_union_fields.clone(), UnionMode::Dense),
2226            false,
2227        )]));
2228
2229        let struct_child: ArrayRef = Arc::new(StructArray::new(
2230            vec![Field::new("x", DataType::Int32, false)].into(),
2231            vec![Arc::new(Int32Array::from(vec![1, 2]))],
2232            None,
2233        ));
2234        let type_ids = [0, 0].into_iter().collect::<ScalarBuffer<i8>>();
2235        let offsets = [0, 1].into_iter().collect::<ScalarBuffer<i32>>();
2236        let source_union = UnionArray::try_new(
2237            source_union_fields,
2238            type_ids.clone(),
2239            Some(offsets.clone()),
2240            vec![struct_child],
2241        )?;
2242
2243        let source_batch =
2244            RecordBatch::try_new(source_schema, vec![Arc::new(source_union)])?;
2245
2246        let adapted = adapt_batch_to_schema(source_batch, &declared_schema)?;
2247        assert_eq!(adapted.schema().as_ref(), declared_schema.as_ref());
2248        let adapted_union = adapted
2249            .column(0)
2250            .as_any()
2251            .downcast_ref::<UnionArray>()
2252            .unwrap();
2253        let adapted_child = adapted_union.child(0);
2254        let struct_arr = adapted_child
2255            .as_any()
2256            .downcast_ref::<StructArray>()
2257            .unwrap();
2258        assert!(struct_arr.fields()[0].is_nullable());
2259        Ok(())
2260    }
2261
2262    #[test]
2263    fn test_adapt_batch_to_schema_union_incompatible_mode_rejected() {
2264        use arrow::array::UnionArray;
2265        use arrow::buffer::ScalarBuffer;
2266        use arrow::datatypes::UnionMode;
2267
2268        let declared_schema = Arc::new(Schema::new(vec![Field::new(
2269            "u",
2270            DataType::Union(test_two_field_union(true), UnionMode::Dense),
2271            false,
2272        )]));
2273        let source_schema = Arc::new(Schema::new(vec![Field::new(
2274            "u",
2275            DataType::Union(test_two_field_union(false), UnionMode::Sparse),
2276            false,
2277        )]));
2278
2279        let int_array: ArrayRef = Arc::new(Int32Array::from(vec![10, 20]));
2280        let str_array: ArrayRef = Arc::new(StringArray::from(vec!["a", "b"]));
2281        let type_ids = [0, 0].into_iter().collect::<ScalarBuffer<i8>>();
2282        let source_union = UnionArray::try_new(
2283            test_two_field_union(false),
2284            type_ids,
2285            None,
2286            vec![int_array, str_array],
2287        )
2288        .unwrap();
2289
2290        let source_batch =
2291            RecordBatch::try_new(source_schema, vec![Arc::new(source_union)]).unwrap();
2292
2293        let res = adapt_batch_to_schema(source_batch, &declared_schema);
2294        assert!(res.is_err());
2295    }
2296
2297    #[test]
2298    fn test_adapt_batch_to_schema_union_field_set_mismatch_rejected() {
2299        use arrow::array::UnionArray;
2300        use arrow::buffer::ScalarBuffer;
2301        use arrow::datatypes::{UnionFields, UnionMode};
2302
2303        // Target has type ID [0]
2304        let target_union_fields = UnionFields::try_new(
2305            vec![0],
2306            vec![Field::new("value", DataType::Int32, true)],
2307        )
2308        .unwrap();
2309        let declared_schema = Arc::new(Schema::new(vec![Field::new(
2310            "u",
2311            DataType::Union(target_union_fields, UnionMode::Sparse),
2312            false,
2313        )]));
2314
2315        // Source has type IDs [0, 1] (where ID 0 is compatible)
2316        let source_union_fields = UnionFields::try_new(
2317            vec![0, 1],
2318            vec![
2319                Field::new("value", DataType::Int32, false),
2320                Field::new("extra", DataType::Utf8, false),
2321            ],
2322        )
2323        .unwrap();
2324        let source_schema = Arc::new(Schema::new(vec![Field::new(
2325            "u",
2326            DataType::Union(source_union_fields.clone(), UnionMode::Sparse),
2327            false,
2328        )]));
2329
2330        let int_array: ArrayRef = Arc::new(Int32Array::from(vec![10, 20]));
2331        let str_array: ArrayRef = Arc::new(StringArray::from(vec!["a", "b"]));
2332        let type_ids = [0, 1].into_iter().collect::<ScalarBuffer<i8>>();
2333        let source_union = UnionArray::try_new(
2334            source_union_fields,
2335            type_ids,
2336            None,
2337            vec![int_array, str_array],
2338        )
2339        .unwrap();
2340
2341        let source_batch =
2342            RecordBatch::try_new(source_schema, vec![Arc::new(source_union)]).unwrap();
2343
2344        let res = adapt_batch_to_schema(source_batch, &declared_schema);
2345        assert!(res.is_err());
2346        let err = res.unwrap_err().to_string();
2347        assert!(
2348            err.contains("different field sets")
2349                || err.contains("cannot be adapted to expected type"),
2350            "unexpected error message: {err}"
2351        );
2352    }
2353
2354    #[test]
2355    fn test_validate_data_type_compatibility_union() {
2356        use arrow::datatypes::{UnionFields, UnionMode};
2357
2358        let target_type = DataType::Union(test_two_field_union(true), UnionMode::Dense);
2359
2360        // Compatible: exact same type IDs in different order with stricter nullability
2361        let reordered_source_fields = UnionFields::try_new(
2362            vec![1, 0],
2363            vec![
2364                Field::new("str", DataType::Utf8, false),
2365                Field::new("value", DataType::Int32, false),
2366            ],
2367        )
2368        .unwrap();
2369        let source_type = DataType::Union(reordered_source_fields, UnionMode::Dense);
2370        assert!(
2371            validate_data_type_compatibility("u", &source_type, &target_type).is_ok()
2372        );
2373
2374        // Incompatible: mismatched mode
2375        let sparse_source_type =
2376            DataType::Union(test_two_field_union(false), UnionMode::Sparse);
2377        assert!(
2378            validate_data_type_compatibility("u", &sparse_source_type, &target_type)
2379                .is_err()
2380        );
2381
2382        // Incompatible: field-set mismatch (extra source ID 2)
2383        let extra_id_source = DataType::Union(
2384            UnionFields::try_new(
2385                vec![0, 1, 2],
2386                vec![
2387                    Field::new("value", DataType::Int32, false),
2388                    Field::new("str", DataType::Utf8, false),
2389                    Field::new("extra", DataType::Int32, false),
2390                ],
2391            )
2392            .unwrap(),
2393            UnionMode::Dense,
2394        );
2395        assert!(
2396            validate_data_type_compatibility("u", &extra_id_source, &target_type)
2397                .is_err()
2398        );
2399
2400        // Incompatible: field-set mismatch (missing source ID 1)
2401        let missing_id_source = DataType::Union(
2402            UnionFields::try_new(
2403                vec![0],
2404                vec![Field::new("value", DataType::Int32, false)],
2405            )
2406            .unwrap(),
2407            UnionMode::Dense,
2408        );
2409        assert!(
2410            validate_data_type_compatibility("u", &missing_id_source, &target_type)
2411                .is_err()
2412        );
2413    }
2414}