This is an automated email from the ASF dual-hosted git repository. github-merge-queue[bot] pushed a commit to branch gh-readonly-queue/main/pr-11626-78f334d9e1682770732f100f23313c91c23fc465 in repository https://gitbox.apache.org/repos/asf/arrow-rs.git
commit dad78c3243db92fefd57b8b4f3cf65901f874263 Author: Matt Butrovich <[email protected]> AuthorDate: Sat Oct 17 06:58:30 2026 +0000 fix(arrow-cast): bump atoi to 3.1.1 to fix Int16 parse overflow (#11626) # Which issue does this PR close? - Closes #11591 # Rationale for this change atoi 3.1.1 fixes pacman82/atoi-rs#54, which caused the bug in #11591. This is a regression in 60.0.0. #10219 bumped atoi from 2.0.0 to 3.1.0, and 59.3.1 still uses atoi 2.0.0. The integer parser in `arrow-cast/src/parse.rs` is otherwise unchanged between 59.3.1 and `main`, and the new tests pass on 59.3.1. This makes the fix a candidate for a 60.x backport. # What changes are included in this PR? The minimum atoi version in `arrow-cast` is now 3.1.1. # Are these changes tested? Yes. `test_parse_integer_bounds` in `arrow-cast/src/parse.rs` parses each integer type's bounds, one past each bound, and the all-nines values on either side of each bound, with and without leading whitespace. `test_cast_string_to_integral_overflow` in `arrow-cast/src/cast/mod.rs` now includes `"-40000"` cast to `Int16`. Both tests fail with atoi 3.1.0. # Are there any significant user-facing changes? No. ## AI Disclosure I wrote this PR with help from Claude but understand and support the core approach, implementation, and test coverage. --- Cargo.lock | 4 ++-- arrow-cast/Cargo.toml | 2 +- arrow-cast/src/cast/mod.rs | 10 ++++++++-- arrow-cast/src/parse.rs | 42 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 53 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 90152321ad..045c6a552f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -609,9 +609,9 @@ checksum = "f2d85fd3d291fabcc40c7232c92c280ec1754fd7b5d7ea769f143222143e179a" [[package]] name = "atoi" -version = "3.1.0" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7a8bbe9949e43a1edaa043038c68703b04774156afdfb62ba2cef5bf93d67be" +checksum = "d12c37257553c19e9aee6cba77814fdc9e07844abfc8b3a2c377cd920ee5a993" dependencies = [ "num-traits", ] diff --git a/arrow-cast/Cargo.toml b/arrow-cast/Cargo.toml index 2d1364e525..25c1ae358d 100644 --- a/arrow-cast/Cargo.toml +++ b/arrow-cast/Cargo.toml @@ -48,7 +48,7 @@ chrono = { workspace = true } half = { version = "2.1", default-features = false } num-traits = { version = "0.2.19", default-features = false, features = ["std"] } lexical-core = { version = "1.0", default-features = false, features = ["write-integers", "write-floats", "parse-integers", "parse-floats"] } -atoi = "3.1.0" +atoi = "3.1.1" comfy-table = { version = "8", optional = true, default-features = false } base64 = "0.23" ryu = "1.0.16" diff --git a/arrow-cast/src/cast/mod.rs b/arrow-cast/src/cast/mod.rs index 94de6548f4..bfebdcdfe7 100644 --- a/arrow-cast/src/cast/mod.rs +++ b/arrow-cast/src/cast/mod.rs @@ -4728,6 +4728,7 @@ mod tests { Some("123"), Some("-123"), Some("86374"), + Some("-40000"), None, ])) as ArrayRef; @@ -4736,8 +4737,13 @@ mod tests { format_options: FormatOptions::default(), }; let res = cast_with_options(&str, &DataType::Int16, &options).expect("should cast to i16"); - let expected = - Arc::new(Int16Array::from(vec![Some(123), Some(-123), None, None])) as ArrayRef; + let expected = Arc::new(Int16Array::from(vec![ + Some(123), + Some(-123), + None, + None, + None, + ])) as ArrayRef; assert_eq!(&res, &expected); } diff --git a/arrow-cast/src/parse.rs b/arrow-cast/src/parse.rs index 384eba2007..a571368d63 100644 --- a/arrow-cast/src/parse.rs +++ b/arrow-cast/src/parse.rs @@ -3631,6 +3631,48 @@ mod tests { assert_eq!(Date32Type::parse(""), None); } + #[test] + fn test_parse_integer_bounds() { + // The all-nines cases have as many digits as a bound but lie past it. atoi 3.1.0 + // parsed -99999 as i16 without an overflow check (#11591). + macro_rules! check { + ($t:ty) => {{ + type Native = <$t as ArrowPrimitiveType>::Native; + let (min, max) = (i128::from(Native::MIN), i128::from(Native::MAX)); + let nines = 10_i128.pow(max.ilog10() + 1) - 1; + for v in [ + min, + max, + min - 1, + max + 1, + -nines, + nines, + -nines - 1, + nines + 1, + ] { + let expected = Native::try_from(v).ok(); + // The leading space takes the path that trims before parsing + for s in [v.to_string(), format!(" {v}")] { + assert_eq!( + <$t>::parse(&s), + expected, + "{s:?} as {}", + <$t as ArrowPrimitiveType>::DATA_TYPE + ); + } + } + }}; + } + check!(Int8Type); + check!(Int16Type); + check!(Int32Type); + check!(Int64Type); + check!(UInt8Type); + check!(UInt16Type); + check!(UInt32Type); + check!(UInt64Type); + } + #[test] fn test_parse_interval_month_day_nano_config() { let interval = parse_interval_month_day_nano_config(
