diff --git a/arrow-cast/src/cast/mod.rs b/arrow-cast/src/cast/mod.rs index 459b47ac1ce0..c4233eb309ba 100644 --- a/arrow-cast/src/cast/mod.rs +++ b/arrow-cast/src/cast/mod.rs @@ -321,13 +321,10 @@ pub fn can_cast_types(from_type: &DataType, to_type: &DataType) -> bool { (_, Duration(_)) if from_type.is_numeric() => true, (Duration(_), _) if to_type.is_numeric() => true, (Duration(_), Duration(_)) => true, - (Interval(from_type), Int64) => { - match from_type { - YearMonth => true, - DayTime => true, - MonthDayNano => false, // Native type is i128 - } - } + // Note: there is deliberately no `(Interval(_), Int64)` arm. No interval unit has an + // unambiguous `i64` value (`YearMonth` is a count of months, `DayTime` packs days and + // milliseconds, `MonthDayNano` is 128 bits wide), and `cast_with_options` implements no + // such cast. Cast via `Duration` instead. (Int32, Interval(to_type)) => match to_type { YearMonth => true, DayTime => false, @@ -12451,6 +12448,60 @@ mod tests { assert_eq!(casted_array.value(0), IntervalMonthDayNano::new(0, 123, 0)); } + #[test] + fn test_can_cast_interval_to_int64_matches_cast() { + // `can_cast_types` used to report `true` for `Interval(YearMonth)` and + // `Interval(DayTime)` to `Int64` even though `cast_with_options` implements no such + // cast. Assert the two agree for every interval unit rather than hard coding the + // expected answer. + let arrays: Vec = vec![ + Arc::new(IntervalYearMonthArray::from(vec![12])), + Arc::new(IntervalDayTimeArray::from(vec![IntervalDayTime::new(1, 2)])), + Arc::new(IntervalMonthDayNanoArray::from(vec![ + IntervalMonthDayNano::new(1, 2, 3), + ])), + ]; + for array in arrays { + let from = array.data_type(); + assert_eq!( + can_cast_types(from, &DataType::Int64), + cast(&array, &DataType::Int64).is_ok(), + "can_cast_types disagrees with cast for {from} -> Int64" + ); + } + } + + #[test] + fn test_cast_union_to_int64_skips_uncastable_interval_child() { + // `resolve_child_array` picks the first child that `can_cast_types` accepts, so an + // over-permissive answer for `Interval -> Int64` made the union cast pick the interval + // child and fail, instead of the `Utf8` child that can actually be cast. + let fields = UnionFields::try_new( + [0, 1], + [ + Field::new("iv", DataType::Interval(IntervalUnit::YearMonth), true), + Field::new("s", DataType::Utf8, true), + ], + ) + .unwrap(); + let union = UnionArray::try_new( + fields, + vec![0, 1, 0].into(), + None, + vec![ + Arc::new(IntervalYearMonthArray::from(vec![Some(12), None, Some(24)])), + Arc::new(StringArray::from(vec![None, Some("77"), None])), + ], + ) + .unwrap(); + + let casted = cast(&union, &DataType::Int64).unwrap(); + let casted = casted.as_primitive::(); + assert!(casted.is_null(0)); + assert_eq!(casted.value(1), 77); + assert!(casted.is_null(2)); + } + #[test] fn test_cast_below_unixtimestamp() { let valid = StringArray::from(vec![ diff --git a/arrow/tests/array_cast.rs b/arrow/tests/array_cast.rs index 82ff7c3a04ea..896980dd36df 100644 --- a/arrow/tests/array_cast.rs +++ b/arrow/tests/array_cast.rs @@ -507,7 +507,7 @@ fn get_all_types() -> Vec { Int8, Int16, Int32, - UInt64, + Int64, UInt8, UInt16, UInt32,