This is an automated email from the ASF dual-hosted git repository.
Jefffrey pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-rs.git
The following commit(s) were added to refs/heads/main by this push:
new bc2db0f5fc fix: remove stale Interval -> Int64 arm from can_cast_types
(#10939)
bc2db0f5fc is described below
commit bc2db0f5fce8780c6805e7d37ef78019cbeb3753
Author: Dylan Pulver <[email protected]>
AuthorDate: Tue Sep 15 06:44:02 2026 +0200
fix: remove stale Interval -> Int64 arm from can_cast_types (#10939)
# Which issue does this PR close?
- Closes #10938.
# Rationale for this change
`can_cast_types` promised `Interval(YearMonth) -> Int64` and
`Interval(DayTime) -> Int64`; `cast_with_options` has no such arm and
errors. #5769 removed the cast side deliberately and left the
`can_cast_types` arm behind, so this restores the intended state rather
than adding a cast. The user-visible effect is in
`cast/union.rs::resolve_child_array`, which picks the first union child
`can_cast_types` accepts: a union with an interval child and a `Utf8`
child failed to cast to `Int64` even though the `Utf8` child casts fine.
If you would rather have `Interval -> Int64` actually implemented, this
is the wrong direction and I am happy to close it — that would be a
feature rather than a fix.
# What changes are included in this PR?
The `(Interval(_), Int64)` arm is removed from `can_cast_types`.
`arrow/tests/array_cast.rs::get_all_types()` gets `Int64` back: it
currently reads `Int8, Int16, Int32, UInt64, UInt8, UInt16, UInt32,
UInt64`, so `Int64` was never used as a cast target. Line 568 of that
file has the intended sequence.
# Are these changes tested?
Matched pair, `cargo test -p arrow --features="chrono-tz prettyprint"
--test array_cast test_can_cast_types`: passes on unmodified main; with
only `Int64` restored and no source change it fails with `from
Interval(YearMonth) to Int64 but can_cast_types reported true`; with
both changes it passes. Restoring `Int64` exposed that one mismatch and
no others.
Two unit tests added in `arrow-cast`:
`test_can_cast_interval_to_int64_matches_cast` asserts `can_cast_types
== cast(..).is_ok()` for all three interval units rather than hard
coding the answer, and
`test_cast_union_to_int64_skips_uncastable_interval_child` covers the
union path.
Reverting the source with the tests in place fails both of them and the
integration test. A half fix (`YearMonth => false`, `DayTime => true`)
still passes the union test but fails the enumerating one on
`Interval(DayTime)`, which is why the enumeration is there —
`test_can_cast_types` panics on the first mismatch and would only ever
have shown `YearMonth`.
`cargo fmt --all -- --check` clean, `cargo clippy -p arrow-cast
--all-targets -- -D warnings` clean, `cargo test -p arrow-cast` 379
passed + 11 doc tests, `--test array_cast` 10 passed. rustc 1.97.1,
matching `rust-toolchain.toml`.
Not tested: full workspace, miri, `force_validate`, `--all-features`,
benchmarks. My sweep for other stale arms of this class was
dispatch-level over null arrays, so value-level cast bugs were not
covered.
# Are there any user-facing changes?
`can_cast_types(Interval(YearMonth) | Interval(DayTime), Int64)` returns
`false` instead of `true`. Nothing could have relied on the `true`,
since the cast always failed. Casting a union to `Int64` now succeeds in
cases that previously errored.
---
AI disclosure, per CONTRIBUTING: the investigation, the patch and the
tests were produced with AI assistance (Claude Opus 5). The measurements
quoted above were run against this branch.
---------
Co-authored-by: Dylan Pulver <[email protected]>
---
arrow-cast/src/cast/mod.rs | 58 ++++++++++++++++++++++++++++++++++++++++------
arrow/tests/array_cast.rs | 2 +-
2 files changed, 52 insertions(+), 8 deletions(-)
diff --git a/arrow-cast/src/cast/mod.rs b/arrow-cast/src/cast/mod.rs
index 7ba864336e..03abb14d27 100644
--- a/arrow-cast/src/cast/mod.rs
+++ b/arrow-cast/src/cast/mod.rs
@@ -321,13 +321,7 @@ 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
- }
- }
+ // No `(Interval(_), Int64)` arm: `cast_with_options` implements no
such cast.
(Int32, Interval(to_type)) => match to_type {
YearMonth => true,
DayTime => false,
@@ -12450,6 +12444,56 @@ mod tests {
assert_eq!(casted_array.value(0), IntervalMonthDayNano::new(0, 123,
0));
}
+ #[test]
+ fn test_can_cast_interval_to_int64_matches_cast() {
+ // Assert the two agree for every interval unit, rather than hard
coding the answer.
+ let arrays: Vec<ArrayRef> = 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 `can_cast_types`
accepts, so an
+ // over-permissive answer made it pick the interval child instead of
the `Utf8` one.
+ 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::<Int64Type>();
+ 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 82ff7c3a04..896980dd36 100644
--- a/arrow/tests/array_cast.rs
+++ b/arrow/tests/array_cast.rs
@@ -507,7 +507,7 @@ fn get_all_types() -> Vec<DataType> {
Int8,
Int16,
Int32,
- UInt64,
+ Int64,
UInt8,
UInt16,
UInt32,