blob: d43816f75b0ed0cbf029e2ee7226b3675d5af157 [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
use crate::error::{Result, _plan_err};
use arrow::{
array::{new_null_array, Array, ArrayRef, StructArray},
compute::{cast_with_options, CastOptions},
datatypes::{DataType::Struct, Field, FieldRef},
};
use std::sync::Arc;
/// Cast a struct column to match target struct fields, handling nested structs recursively.
///
/// This function implements struct-to-struct casting with the assumption that **structs should
/// always be allowed to cast to other structs**. However, the source column must already be
/// a struct type - non-struct sources will result in an error.
///
/// ## Field Matching Strategy
/// - **By Name**: Source struct fields are matched to target fields by name (case-sensitive)
/// - **Type Adaptation**: When a matching field is found, it is recursively cast to the target field's type
/// - **Missing Fields**: Target fields not present in the source are filled with null values
/// - **Extra Fields**: Source fields not present in the target are ignored
///
/// ## Nested Struct Handling
/// - Nested structs are handled recursively using the same casting rules
/// - Each level of nesting follows the same field matching and null-filling strategy
/// - This allows for complex struct transformations while maintaining data integrity
///
/// # Arguments
/// * `source_col` - The source array to cast (must be a struct array)
/// * `target_fields` - The target struct field definitions to cast to
///
/// # Returns
/// A `Result<ArrayRef>` containing the cast struct array
///
/// # Errors
/// Returns a `DataFusionError::Plan` if the source column is not a struct type
fn cast_struct_column(
source_col: &ArrayRef,
target_fields: &[Arc<Field>],
cast_options: &CastOptions,
) -> Result<ArrayRef> {
if let Some(source_struct) = source_col.as_any().downcast_ref::<StructArray>() {
validate_struct_compatibility(source_struct.fields(), target_fields)?;
let mut fields: Vec<Arc<Field>> = Vec::with_capacity(target_fields.len());
let mut arrays: Vec<ArrayRef> = Vec::with_capacity(target_fields.len());
let num_rows = source_col.len();
for target_child_field in target_fields {
fields.push(Arc::clone(target_child_field));
match source_struct.column_by_name(target_child_field.name()) {
Some(source_child_col) => {
let adapted_child =
cast_column(source_child_col, target_child_field, cast_options)
.map_err(|e| {
e.context(format!(
"While casting struct field '{}'",
target_child_field.name()
))
})?;
arrays.push(adapted_child);
}
None => {
arrays.push(new_null_array(target_child_field.data_type(), num_rows));
}
}
}
let struct_array =
StructArray::new(fields.into(), arrays, source_struct.nulls().cloned());
Ok(Arc::new(struct_array))
} else {
// Return error if source is not a struct type
_plan_err!(
"Cannot cast column of type {} to struct type. Source must be a struct to cast to struct.",
source_col.data_type()
)
}
}
/// Cast a column to match the target field type, with special handling for nested structs.
///
/// This function serves as the main entry point for column casting operations. For struct
/// types, it enforces that **only struct columns can be cast to struct types**.
///
/// ## Casting Behavior
/// - **Struct Types**: Delegates to `cast_struct_column` for struct-to-struct casting only
/// - **Non-Struct Types**: Uses Arrow's standard `cast` function for primitive type conversions
///
/// ## Cast Options
/// The `cast_options` argument controls how Arrow handles values that cannot be represented
/// in the target type. When `safe` is `false` (DataFusion's default) the cast will return an
/// error if such a value is encountered. Setting `safe` to `true` instead produces `NULL`
/// for out-of-range or otherwise invalid values. The options also allow customizing how
/// temporal values are formatted when cast to strings.
///
/// ```
/// use arrow::array::{ArrayRef, Int64Array};
/// use arrow::compute::CastOptions;
/// use arrow::datatypes::{DataType, Field};
/// use datafusion_common::nested_struct::cast_column;
/// use std::sync::Arc;
///
/// let source: ArrayRef = Arc::new(Int64Array::from(vec![1, i64::MAX]));
/// let target = Field::new("ints", DataType::Int32, true);
/// // Permit lossy conversions by producing NULL on overflow instead of erroring
/// let options = CastOptions {
/// safe: true,
/// ..Default::default()
/// };
/// let result = cast_column(&source, &target, &options).unwrap();
/// assert!(result.is_null(1));
/// ```
///
/// ## Struct Casting Requirements
/// The struct casting logic requires that the source column must already be a struct type.
/// This makes the function useful for:
/// - Schema evolution scenarios where struct layouts change over time
/// - Data migration between different struct schemas
/// - Type-safe data processing pipelines that maintain struct type integrity
///
/// # Arguments
/// * `source_col` - The source array to cast
/// * `target_field` - The target field definition (including type and metadata)
/// * `cast_options` - Options that govern strictness and formatting of the cast
///
/// # Returns
/// A `Result<ArrayRef>` containing the cast array
///
/// # Errors
/// Returns an error if:
/// - Attempting to cast a non-struct column to a struct type
/// - Arrow's cast function fails for non-struct types
/// - Memory allocation fails during struct construction
/// - Invalid data type combinations are encountered
pub fn cast_column(
source_col: &ArrayRef,
target_field: &Field,
cast_options: &CastOptions,
) -> Result<ArrayRef> {
match target_field.data_type() {
Struct(target_fields) => {
cast_struct_column(source_col, target_fields, cast_options)
}
_ => Ok(cast_with_options(
source_col,
target_field.data_type(),
cast_options,
)?),
}
}
/// Validates compatibility between source and target struct fields for casting operations.
///
/// This function implements comprehensive struct compatibility checking by examining:
/// - Field name matching between source and target structs
/// - Type castability for each matching field (including recursive struct validation)
/// - Proper handling of missing fields (target fields not in source are allowed - filled with nulls)
/// - Proper handling of extra fields (source fields not in target are allowed - ignored)
///
/// # Compatibility Rules
/// - **Field Matching**: Fields are matched by name (case-sensitive)
/// - **Missing Target Fields**: Allowed - will be filled with null values during casting
/// - **Extra Source Fields**: Allowed - will be ignored during casting
/// - **Type Compatibility**: Each matching field must be castable using Arrow's type system
/// - **Nested Structs**: Recursively validates nested struct compatibility
///
/// # Arguments
/// * `source_fields` - Fields from the source struct type
/// * `target_fields` - Fields from the target struct type
///
/// # Returns
/// * `Ok(())` if the structs are compatible for casting
/// * `Err(DataFusionError)` with detailed error message if incompatible
///
/// # Examples
/// ```text
/// // Compatible: source has extra field, target has missing field
/// // Source: {a: i32, b: string, c: f64}
/// // Target: {a: i64, d: bool}
/// // Result: Ok(()) - 'a' can cast i32->i64, 'b','c' ignored, 'd' filled with nulls
///
/// // Incompatible: matching field has incompatible types
/// // Source: {a: string}
/// // Target: {a: binary}
/// // Result: Err(...) - string cannot cast to binary
/// ```
pub fn validate_struct_compatibility(
source_fields: &[FieldRef],
target_fields: &[FieldRef],
) -> Result<()> {
// Check compatibility for each target field
for target_field in target_fields {
// Look for matching field in source by name
if let Some(source_field) = source_fields
.iter()
.find(|f| f.name() == target_field.name())
{
// Ensure nullability is compatible. It is invalid to cast a nullable
// source field to a non-nullable target field as this may discard
// null values.
if source_field.is_nullable() && !target_field.is_nullable() {
return _plan_err!(
"Cannot cast nullable struct field '{}' to non-nullable field",
target_field.name()
);
}
// Check if the matching field types are compatible
match (source_field.data_type(), target_field.data_type()) {
// Recursively validate nested structs
(Struct(source_nested), Struct(target_nested)) => {
validate_struct_compatibility(source_nested, target_nested)?;
}
// For non-struct types, use the existing castability check
_ => {
if !arrow::compute::can_cast_types(
source_field.data_type(),
target_field.data_type(),
) {
return _plan_err!(
"Cannot cast struct field '{}' from type {} to type {}",
target_field.name(),
source_field.data_type(),
target_field.data_type()
);
}
}
}
}
// Missing fields in source are OK - they'll be filled with nulls
}
// Extra fields in source are OK - they'll be ignored
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::format::DEFAULT_CAST_OPTIONS;
use arrow::{
array::{
BinaryArray, Int32Array, Int32Builder, Int64Array, ListArray, MapArray,
MapBuilder, StringArray, StringBuilder,
},
buffer::NullBuffer,
datatypes::{DataType, Field, FieldRef, Int32Type},
};
/// Macro to extract and downcast a column from a StructArray
macro_rules! get_column_as {
($struct_array:expr, $column_name:expr, $array_type:ty) => {
$struct_array
.column_by_name($column_name)
.unwrap()
.as_any()
.downcast_ref::<$array_type>()
.unwrap()
};
}
fn field(name: &str, data_type: DataType) -> Field {
Field::new(name, data_type, true)
}
fn non_null_field(name: &str, data_type: DataType) -> Field {
Field::new(name, data_type, false)
}
fn arc_field(name: &str, data_type: DataType) -> FieldRef {
Arc::new(field(name, data_type))
}
fn struct_type(fields: Vec<Field>) -> DataType {
Struct(fields.into())
}
fn struct_field(name: &str, fields: Vec<Field>) -> Field {
field(name, struct_type(fields))
}
fn arc_struct_field(name: &str, fields: Vec<Field>) -> FieldRef {
Arc::new(struct_field(name, fields))
}
#[test]
fn test_cast_simple_column() {
let source = Arc::new(Int32Array::from(vec![1, 2, 3])) as ArrayRef;
let target_field = field("ints", DataType::Int64);
let result = cast_column(&source, &target_field, &DEFAULT_CAST_OPTIONS).unwrap();
let result = result.as_any().downcast_ref::<Int64Array>().unwrap();
assert_eq!(result.len(), 3);
assert_eq!(result.value(0), 1);
assert_eq!(result.value(1), 2);
assert_eq!(result.value(2), 3);
}
#[test]
fn test_cast_column_with_options() {
let source = Arc::new(Int64Array::from(vec![1, i64::MAX])) as ArrayRef;
let target_field = field("ints", DataType::Int32);
let safe_opts = CastOptions {
// safe: false - return Err for failure
safe: false,
..DEFAULT_CAST_OPTIONS
};
assert!(cast_column(&source, &target_field, &safe_opts).is_err());
let unsafe_opts = CastOptions {
// safe: true - return Null for failure
safe: true,
..DEFAULT_CAST_OPTIONS
};
let result = cast_column(&source, &target_field, &unsafe_opts).unwrap();
let result = result.as_any().downcast_ref::<Int32Array>().unwrap();
assert_eq!(result.value(0), 1);
assert!(result.is_null(1));
}
#[test]
fn test_cast_struct_with_missing_field() {
let a_array = Arc::new(Int32Array::from(vec![1, 2])) as ArrayRef;
let source_struct = StructArray::from(vec![(
arc_field("a", DataType::Int32),
Arc::clone(&a_array),
)]);
let source_col = Arc::new(source_struct) as ArrayRef;
let target_field = struct_field(
"s",
vec![field("a", DataType::Int32), field("b", DataType::Utf8)],
);
let result =
cast_column(&source_col, &target_field, &DEFAULT_CAST_OPTIONS).unwrap();
let struct_array = result.as_any().downcast_ref::<StructArray>().unwrap();
assert_eq!(struct_array.fields().len(), 2);
let a_result = get_column_as!(&struct_array, "a", Int32Array);
assert_eq!(a_result.value(0), 1);
assert_eq!(a_result.value(1), 2);
let b_result = get_column_as!(&struct_array, "b", StringArray);
assert_eq!(b_result.len(), 2);
assert!(b_result.is_null(0));
assert!(b_result.is_null(1));
}
#[test]
fn test_cast_struct_source_not_struct() {
let source = Arc::new(Int32Array::from(vec![10, 20])) as ArrayRef;
let target_field = struct_field("s", vec![field("a", DataType::Int32)]);
let result = cast_column(&source, &target_field, &DEFAULT_CAST_OPTIONS);
assert!(result.is_err());
let error_msg = result.unwrap_err().to_string();
assert!(error_msg.contains("Cannot cast column of type"));
assert!(error_msg.contains("to struct type"));
assert!(error_msg.contains("Source must be a struct"));
}
#[test]
fn test_cast_struct_incompatible_child_type() {
let a_array = Arc::new(BinaryArray::from(vec![
Some(b"a".as_ref()),
Some(b"b".as_ref()),
])) as ArrayRef;
let source_struct =
StructArray::from(vec![(arc_field("a", DataType::Binary), a_array)]);
let source_col = Arc::new(source_struct) as ArrayRef;
let target_field = struct_field("s", vec![field("a", DataType::Int32)]);
let result = cast_column(&source_col, &target_field, &DEFAULT_CAST_OPTIONS);
assert!(result.is_err());
let error_msg = result.unwrap_err().to_string();
assert!(error_msg.contains("Cannot cast struct field 'a'"));
}
#[test]
fn test_validate_struct_compatibility_incompatible_types() {
// Source struct: {field1: Binary, field2: String}
let source_fields = vec![
arc_field("field1", DataType::Binary),
arc_field("field2", DataType::Utf8),
];
// Target struct: {field1: Int32}
let target_fields = vec![arc_field("field1", DataType::Int32)];
let result = validate_struct_compatibility(&source_fields, &target_fields);
assert!(result.is_err());
let error_msg = result.unwrap_err().to_string();
assert!(error_msg.contains("Cannot cast struct field 'field1'"));
assert!(error_msg.contains("Binary"));
assert!(error_msg.contains("Int32"));
}
#[test]
fn test_validate_struct_compatibility_compatible_types() {
// Source struct: {field1: Int32, field2: String}
let source_fields = vec![
arc_field("field1", DataType::Int32),
arc_field("field2", DataType::Utf8),
];
// Target struct: {field1: Int64} (Int32 can cast to Int64)
let target_fields = vec![arc_field("field1", DataType::Int64)];
let result = validate_struct_compatibility(&source_fields, &target_fields);
assert!(result.is_ok());
}
#[test]
fn test_validate_struct_compatibility_missing_field_in_source() {
// Source struct: {field2: String} (missing field1)
let source_fields = vec![arc_field("field2", DataType::Utf8)];
// Target struct: {field1: Int32}
let target_fields = vec![arc_field("field1", DataType::Int32)];
// Should be OK - missing fields will be filled with nulls
let result = validate_struct_compatibility(&source_fields, &target_fields);
assert!(result.is_ok());
}
#[test]
fn test_validate_struct_compatibility_additional_field_in_source() {
// Source struct: {field1: Int32, field2: String} (extra field2)
let source_fields = vec![
arc_field("field1", DataType::Int32),
arc_field("field2", DataType::Utf8),
];
// Target struct: {field1: Int32}
let target_fields = vec![arc_field("field1", DataType::Int32)];
// Should be OK - extra fields in source are ignored
let result = validate_struct_compatibility(&source_fields, &target_fields);
assert!(result.is_ok());
}
#[test]
fn test_cast_struct_parent_nulls_retained() {
let a_array = Arc::new(Int32Array::from(vec![Some(1), Some(2)])) as ArrayRef;
let fields = vec![arc_field("a", DataType::Int32)];
let nulls = Some(NullBuffer::from(vec![true, false]));
let source_struct = StructArray::new(fields.clone().into(), vec![a_array], nulls);
let source_col = Arc::new(source_struct) as ArrayRef;
let target_field = struct_field("s", vec![field("a", DataType::Int64)]);
let result =
cast_column(&source_col, &target_field, &DEFAULT_CAST_OPTIONS).unwrap();
let struct_array = result.as_any().downcast_ref::<StructArray>().unwrap();
assert_eq!(struct_array.null_count(), 1);
assert!(struct_array.is_valid(0));
assert!(struct_array.is_null(1));
let a_result = get_column_as!(&struct_array, "a", Int64Array);
assert_eq!(a_result.value(0), 1);
assert_eq!(a_result.value(1), 2);
}
#[test]
fn test_validate_struct_compatibility_nullable_to_non_nullable() {
// Source struct: {field1: Int32 nullable}
let source_fields = vec![arc_field("field1", DataType::Int32)];
// Target struct: {field1: Int32 non-nullable}
let target_fields = vec![Arc::new(non_null_field("field1", DataType::Int32))];
let result = validate_struct_compatibility(&source_fields, &target_fields);
assert!(result.is_err());
let error_msg = result.unwrap_err().to_string();
assert!(error_msg.contains("field1"));
assert!(error_msg.contains("non-nullable"));
}
#[test]
fn test_validate_struct_compatibility_non_nullable_to_nullable() {
// Source struct: {field1: Int32 non-nullable}
let source_fields = vec![Arc::new(non_null_field("field1", DataType::Int32))];
// Target struct: {field1: Int32 nullable}
let target_fields = vec![arc_field("field1", DataType::Int32)];
let result = validate_struct_compatibility(&source_fields, &target_fields);
assert!(result.is_ok());
}
#[test]
fn test_validate_struct_compatibility_nested_nullable_to_non_nullable() {
// Source struct: {field1: {nested: Int32 nullable}}
let source_fields = vec![Arc::new(non_null_field(
"field1",
struct_type(vec![field("nested", DataType::Int32)]),
))];
// Target struct: {field1: {nested: Int32 non-nullable}}
let target_fields = vec![Arc::new(non_null_field(
"field1",
struct_type(vec![non_null_field("nested", DataType::Int32)]),
))];
let result = validate_struct_compatibility(&source_fields, &target_fields);
assert!(result.is_err());
let error_msg = result.unwrap_err().to_string();
assert!(error_msg.contains("nested"));
assert!(error_msg.contains("non-nullable"));
}
#[test]
fn test_cast_nested_struct_with_extra_and_missing_fields() {
// Source inner struct has fields a, b, extra
let a = Arc::new(Int32Array::from(vec![Some(1), None])) as ArrayRef;
let b = Arc::new(Int32Array::from(vec![Some(2), Some(3)])) as ArrayRef;
let extra = Arc::new(Int32Array::from(vec![Some(9), Some(10)])) as ArrayRef;
let inner = StructArray::from(vec![
(arc_field("a", DataType::Int32), a),
(arc_field("b", DataType::Int32), b),
(arc_field("extra", DataType::Int32), extra),
]);
let source_struct = StructArray::from(vec![(
arc_struct_field(
"inner",
vec![
field("a", DataType::Int32),
field("b", DataType::Int32),
field("extra", DataType::Int32),
],
),
Arc::new(inner) as ArrayRef,
)]);
let source_col = Arc::new(source_struct) as ArrayRef;
// Target inner struct reorders fields, adds "missing", and drops "extra"
let target_field = struct_field(
"outer",
vec![struct_field(
"inner",
vec![
field("b", DataType::Int64),
field("a", DataType::Int32),
field("missing", DataType::Int32),
],
)],
);
let result =
cast_column(&source_col, &target_field, &DEFAULT_CAST_OPTIONS).unwrap();
let outer = result.as_any().downcast_ref::<StructArray>().unwrap();
let inner = get_column_as!(&outer, "inner", StructArray);
assert_eq!(inner.fields().len(), 3);
let b = get_column_as!(inner, "b", Int64Array);
assert_eq!(b.value(0), 2);
assert_eq!(b.value(1), 3);
assert!(!b.is_null(0));
assert!(!b.is_null(1));
let a = get_column_as!(inner, "a", Int32Array);
assert_eq!(a.value(0), 1);
assert!(a.is_null(1));
let missing = get_column_as!(inner, "missing", Int32Array);
assert!(missing.is_null(0));
assert!(missing.is_null(1));
}
#[test]
fn test_cast_struct_with_array_and_map_fields() {
// Array field with second row null
let arr_array = Arc::new(ListArray::from_iter_primitive::<Int32Type, _, _>(vec![
Some(vec![Some(1), Some(2)]),
None,
])) as ArrayRef;
// Map field with second row null
let string_builder = StringBuilder::new();
let int_builder = Int32Builder::new();
let mut map_builder = MapBuilder::new(None, string_builder, int_builder);
map_builder.keys().append_value("a");
map_builder.values().append_value(1);
map_builder.append(true).unwrap();
map_builder.append(false).unwrap();
let map_array = Arc::new(map_builder.finish()) as ArrayRef;
let source_struct = StructArray::from(vec![
(
arc_field(
"arr",
DataType::List(Arc::new(field("item", DataType::Int32))),
),
arr_array,
),
(
arc_field(
"map",
DataType::Map(
Arc::new(non_null_field(
"entries",
struct_type(vec![
non_null_field("keys", DataType::Utf8),
field("values", DataType::Int32),
]),
)),
false,
),
),
map_array,
),
]);
let source_col = Arc::new(source_struct) as ArrayRef;
let target_field = struct_field(
"s",
vec![
field(
"arr",
DataType::List(Arc::new(field("item", DataType::Int32))),
),
field(
"map",
DataType::Map(
Arc::new(non_null_field(
"entries",
struct_type(vec![
non_null_field("keys", DataType::Utf8),
field("values", DataType::Int32),
]),
)),
false,
),
),
],
);
let result =
cast_column(&source_col, &target_field, &DEFAULT_CAST_OPTIONS).unwrap();
let struct_array = result.as_any().downcast_ref::<StructArray>().unwrap();
let arr = get_column_as!(&struct_array, "arr", ListArray);
assert!(!arr.is_null(0));
assert!(arr.is_null(1));
let arr0 = arr.value(0);
let values = arr0.as_any().downcast_ref::<Int32Array>().unwrap();
assert_eq!(values.value(0), 1);
assert_eq!(values.value(1), 2);
let map = get_column_as!(&struct_array, "map", MapArray);
assert!(!map.is_null(0));
assert!(map.is_null(1));
let map0 = map.value(0);
let entries = map0.as_any().downcast_ref::<StructArray>().unwrap();
let keys = get_column_as!(entries, "keys", StringArray);
let vals = get_column_as!(entries, "values", Int32Array);
assert_eq!(keys.value(0), "a");
assert_eq!(vals.value(0), 1);
}
#[test]
fn test_cast_struct_field_order_differs() {
let a = Arc::new(Int32Array::from(vec![Some(1), Some(2)])) as ArrayRef;
let b = Arc::new(Int32Array::from(vec![Some(3), None])) as ArrayRef;
let source_struct = StructArray::from(vec![
(arc_field("a", DataType::Int32), a),
(arc_field("b", DataType::Int32), b),
]);
let source_col = Arc::new(source_struct) as ArrayRef;
let target_field = struct_field(
"s",
vec![field("b", DataType::Int64), field("a", DataType::Int32)],
);
let result =
cast_column(&source_col, &target_field, &DEFAULT_CAST_OPTIONS).unwrap();
let struct_array = result.as_any().downcast_ref::<StructArray>().unwrap();
let b_col = get_column_as!(&struct_array, "b", Int64Array);
assert_eq!(b_col.value(0), 3);
assert!(b_col.is_null(1));
let a_col = get_column_as!(&struct_array, "a", Int32Array);
assert_eq!(a_col.value(0), 1);
assert_eq!(a_col.value(1), 2);
}
}