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 078c0f17be perf(arrow-cast): optimize parsing of decimals from strings 
(#10668)
078c0f17be is described below

commit 078c0f17be9d1c2e202b87d39a9a84e4b0258ba1
Author: Neil Conway <[email protected]>
AuthorDate: Mon Aug 17 21:18:42 2026 -0400

    perf(arrow-cast): optimize parsing of decimals from strings (#10668)
    
    # Which issue does this PR close?
    
    - Closes #10664
    - Closes #10665
    - Closes #10666
    
    # Rationale for this change
    
    The previous implementation of `parse_string_to_decimal_native` used a
    string-manipulation approach: it trimmed the input string, split it
    based on `"."`, parsed both halves with `i256::from_string`, converted
    the result back to a string with `format!`, and parsed that _again_ with
    `i256::from_string`.
    
    Instead, we use a single pass over the input bytes. A simple state
    machine walks over the input digits, accumulating a running sum. This
    avoids all of the string manipulation and heap allocation of the
    previous approach.
    
    We further optimize this by accumulating the running sum in a `u64`, and
    then periodically folding that partial value into the running `decimal`
    value (we do this often enough that there is no risk of overflowing the
    `u64`). That trades a bit of redundant computation for doing more work
    in `u64` and less work in `decimal`; based on benchmarking, this is a
    clear win.
    
    Finally, we don't need to accumulate digits from the suffix of the
    string. Values beyond the target type's scale don't contribute to the
    result value; only the first such digit influences rounding behavior.
    
    This new approach also fixes two correctness bugs (#10664 and #10665) in
    the previous implementation.
    
    **Benchmarks** (M4 Max)
    
    Parser microbenchmarks (arrow-cast/benches/parse_decimal.rs):
    
    - string decimal128 integer: ~111 ns → 15.8 ns, −85.8%
    - string decimal128 exact scale: ~109 ns → 15.2 ns, −86.0%
    - string decimal128 padded scale: ~116 ns → 15.3 ns, −86.8%
    - string decimal128 rounded scale: ~175 ns → 15.7 ns, −91.0%
    - string decimal128 signed: 201.8 ns → 15.7 ns, −92.2%
    - string decimal128 38 digits: 116.8 ns → 27.2 ns, −76.8%
    - string decimal256 76 digits: 174.4 ns → 61.9 ns, −64.5%
    - string decimal256 rounded scale: 440.0 ns → 62.3 ns, −85.8%
    
    End-to-end cast kernel (arrow/benches/cast_kernels.rs, 512-row string
    array, safe mode):
    
    - cast string to decimal128(38, 3): 558 µs → 68.4 µs, −87.0%
    - cast string to decimal256(76, 3): 519 µs → 80.1 µs, −84.4%
    
    # What changes are included in this PR?
    
    * Rewrite `parse_string_to_decimal_native` as described above
    * Add benchmark coverage
    * Add unit tests
    * Clarify / extend comments on accepted syntax and rounding behavior
    * Replace a `Vec` + `unsafe` with `PrimitiveBuilder`, which saves an
    allocation
    * Improve error message reporting
    
    # Are these changes tested?
    
    Yes; existing tests pass, and new tests have been added. I also checked
    the new implementation against a naive oracle built using the
    [num-bigint](https://docs.rs/num-bigint/latest/num_bigint/) crate; the
    new implementation was consistent with `num-bigint` for 120M randomly
    generated inputs.
    
    # Are there any user-facing changes?
    
    No, aside from fixed bugs.
    
    # AI usage
    
    Iterated with the help of Claude Fable; I reviewed and understand the
    resulting code.
    
    ---------
    
    Co-authored-by: Jeffrey Vo <[email protected]>
---
 arrow-cast/benches/parse_decimal.rs |  40 +++-
 arrow-cast/src/cast/decimal.rs      | 442 +++++++++++++++++++++++++++---------
 arrow-cast/src/cast/mod.rs          |   6 +-
 arrow/benches/cast_kernels.rs       |   5 +
 4 files changed, 386 insertions(+), 107 deletions(-)

diff --git a/arrow-cast/benches/parse_decimal.rs 
b/arrow-cast/benches/parse_decimal.rs
index 28364b733c..93089363bb 100644
--- a/arrow-cast/benches/parse_decimal.rs
+++ b/arrow-cast/benches/parse_decimal.rs
@@ -15,7 +15,8 @@
 // specific language governing permissions and limitations
 // under the License.
 
-use arrow_array::types::Decimal256Type;
+use arrow_array::types::{Decimal128Type, Decimal256Type};
+use arrow_cast::cast::parse_string_to_decimal_native;
 use arrow_cast::parse::parse_decimal;
 use criterion::*;
 use std::hint;
@@ -51,6 +52,43 @@ fn criterion_benchmark(c: &mut Criterion) {
             b.iter(|| parse_decimal::<Decimal256Type>(d, 20, 3).unwrap());
         });
     }
+
+    let string_decimals = [
+        ("string decimal128 integer", "12345678912345678", 3),
+        ("string decimal128 exact scale", "12345678912345.123", 3),
+        ("string decimal128 padded scale", "12345678912345.1", 6),
+        ("string decimal128 rounded scale", "12345678912345.1235", 3),
+        ("string decimal128 signed", "-12345678912345.1235", 3),
+        (
+            "string decimal128 38 digits",
+            "99999999999999999999999999999999999999",
+            0,
+        ),
+        (
+            "string decimal256 76 digits",
+            
"9999999999999999999999999999999999999999999999999999999999999999999999999999",
+            0,
+        ),
+        (
+            "string decimal256 rounded scale",
+            
"999999999999999999999999999999999999999999999999999999999999999999999.9995",
+            3,
+        ),
+    ];
+
+    for (name, decimal, scale) in string_decimals {
+        let d = hint::black_box(decimal);
+        let scale = hint::black_box(scale);
+        if name.contains("decimal256") {
+            c.bench_function(name, |b| {
+                b.iter(|| parse_string_to_decimal_native::<Decimal256Type>(d, 
scale).unwrap());
+            });
+        } else {
+            c.bench_function(name, |b| {
+                b.iter(|| parse_string_to_decimal_native::<Decimal128Type>(d, 
scale).unwrap());
+            });
+        }
+    }
 }
 
 criterion_group!(benches, criterion_benchmark);
diff --git a/arrow-cast/src/cast/decimal.rs b/arrow-cast/src/cast/decimal.rs
index f9ee1c563a..847076e48b 100644
--- a/arrow-cast/src/cast/decimal.rs
+++ b/arrow-cast/src/cast/decimal.rs
@@ -529,8 +529,18 @@ where
     )?))
 }
 
-/// Parses given string to specified decimal native (i128/i256) based on given
-/// scale. Returns an `Err` if it cannot parse given string.
+/// Parses the given string as a decimal with the given scale, returning the
+/// unscaled representation in the decimal type's native integer (e.g. `i32`
+/// for `Decimal32Type`, `i256` for `Decimal256Type`).
+///
+/// The input is an optionally signed (`+`/`-`) sequence of digits containing
+/// at most one decimal point, with optional surrounding whitespace. Fractional
+/// digits beyond `scale` do not appear in the result but round it half away
+/// from zero (e.g. `"1.005"` at scale 2 parses as `101`).
+///
+/// Returns an error if the input is not a valid decimal string, or if the
+/// scaled and rounded value overflows the native type. The caller is
+/// responsible for validating the result against a precision.
 pub fn parse_string_to_decimal_native<T: DecimalType>(
     value_str: &str,
     scale: usize,
@@ -539,100 +549,160 @@ where
     T::Native: DecimalCast + ArrowNativeTypeOp,
 {
     let value_str = value_str.trim();
-    let parts: Vec<&str> = value_str.split('.').collect();
-    if parts.len() > 2 {
-        return Err(ArrowError::InvalidArgumentError(format!(
-            "Invalid decimal format: {value_str:?}"
-        )));
-    }
+    let bytes = value_str.as_bytes();
 
-    let (negative, first_part) = if parts[0].is_empty() {
-        (false, parts[0])
-    } else {
-        match parts[0].as_bytes()[0] {
-            b'-' => (true, &parts[0][1..]),
-            b'+' => (false, &parts[0][1..]),
-            _ => (false, parts[0]),
+    let mut index = 0;
+    let negative = match bytes.first() {
+        Some(b'-') => {
+            index += 1;
+            true
+        }
+        Some(b'+') => {
+            index += 1;
+            false
         }
+        _ => false,
     };
 
-    let integers = first_part;
-    let decimals = if parts.len() == 2 { parts[1] } else { "" };
+    let mut value = T::Native::ZERO;
+    let mut chunk = 0_u64;
+    let mut chunk_len = 0_usize;
+    let mut saw_digit = false;
+    let mut saw_point = false;
+    let mut fractionals = 0_usize;
+    let mut first_discarded_digit = None;
+
+    while let Some(&b) = bytes.get(index) {
+        match b {
+            b'0'..=b'9' => {
+                saw_digit = true;
+                let digit = b - b'0';
+                if saw_point {
+                    if fractionals == scale {
+                        first_discarded_digit.get_or_insert(digit);
+                        index += 1;
+                        continue;
+                    }
+                    fractionals += 1;
+                }
 
-    if integers.is_empty() && decimals.is_empty() {
-        return Err(ArrowError::InvalidArgumentError(format!(
-            "Invalid decimal format: {value_str:?}"
-        )));
+                // Cannot overflow: the chunk is folded into `value` before it
+                // exceeds MAX_CHUNK_DIGITS digits, all of which fit in a u64
+                chunk = chunk * 10 + digit as u64;
+                chunk_len += 1;
+                if chunk_len == MAX_CHUNK_DIGITS {
+                    value = fold_decimal_chunk::<T>(value, chunk, chunk_len, 
negative, value_str)?;
+                    chunk = 0;
+                    chunk_len = 0;
+                }
+            }
+            b'.' if !saw_point => saw_point = true,
+            _ => {
+                return Err(ArrowError::InvalidArgumentError(format!(
+                    "Invalid decimal format: {value_str:?}"
+                )));
+            }
+        }
+        index += 1;
     }
 
-    if !integers.is_empty() && !integers.as_bytes()[0].is_ascii_digit() {
-        return Err(ArrowError::InvalidArgumentError(format!(
-            "Invalid decimal format: {value_str:?}"
-        )));
+    if chunk_len > 0 {
+        value = fold_decimal_chunk::<T>(value, chunk, chunk_len, negative, 
value_str)?;
     }
 
-    if !decimals.is_empty() && !decimals.as_bytes()[0].is_ascii_digit() {
+    if !saw_digit {
         return Err(ArrowError::InvalidArgumentError(format!(
             "Invalid decimal format: {value_str:?}"
         )));
     }
 
-    // Adjust decimal based on scale
-    let mut number_decimals = if decimals.len() > scale {
-        let decimal_number = i256::from_string(decimals).ok_or_else(|| {
-            ArrowError::InvalidArgumentError(format!("Cannot parse decimal 
format: {value_str}"))
-        })?;
-
-        let div = i256::from_i128(10_i128).pow_checked((decimals.len() - 
scale) as u32)?;
-
-        let half = div.div_wrapping(i256::from_i128(2));
-        let half_neg = half.neg_wrapping();
-
-        let d = decimal_number.div_wrapping(div);
-        let r = decimal_number.mod_wrapping(div);
-
-        // Round result
-        let adjusted = match decimal_number >= i256::ZERO {
-            true if r >= half => d.add_wrapping(i256::ONE),
-            false if r <= half_neg => d.sub_wrapping(i256::ONE),
-            _ => d,
-        };
+    // Scale the value up to the target scale. Skipped for zero, where 
computing
+    // 10^(scale - fractionals) could overflow the native type even though the
+    // result (zero) is always representable.
+    if fractionals < scale && !value.is_zero() {
+        value = value
+            .mul_checked(decimal_pow::<T>(scale - fractionals, value_str)?)
+            .map_err(|_| decimal_parse_overflow::<T>(value_str))?;
+    }
 
-        let integers = if !integers.is_empty() {
-            i256::from_string(integers)
-                .ok_or_else(|| {
-                    ArrowError::InvalidArgumentError(format!(
-                        "Cannot parse decimal format: {value_str}"
-                    ))
-                })
-                .map(|v| 
v.mul_wrapping(i256::from_i128(10_i128).pow_wrapping(scale as u32)))?
+    if first_discarded_digit.is_some_and(|digit| digit >= 5) {
+        value = if negative {
+            value.sub_checked(T::Native::ONE)
         } else {
-            i256::ZERO
-        };
+            value.add_checked(T::Native::ONE)
+        }
+        .map_err(|_| decimal_parse_overflow::<T>(value_str))?;
+    }
 
-        format!("{}", integers.add_wrapping(adjusted))
-    } else {
-        let padding = if scale > decimals.len() { scale } else { 0 };
+    Ok(value)
+}
 
-        let decimals = format!("{decimals:0<padding$}");
-        format!("{integers}{decimals}")
+/// The maximum number of decimal digits a u64 can accumulate without
+/// overflowing: every 19-digit number fits in a u64.
+const MAX_CHUNK_DIGITS: usize = 19;
+
+/// Folds a chunk of up to [`MAX_CHUNK_DIGITS`] digits into `value`, producing
+/// `value * 10^chunk_len + chunk` (`chunk` is negated first when parsing a
+/// negative number).
+#[inline]
+fn fold_decimal_chunk<T: DecimalType>(
+    value: T::Native,
+    chunk: u64,
+    chunk_len: usize,
+    negative: bool,
+    value_str: &str,
+) -> Result<T::Native, ArrowError>
+where
+    T::Native: DecimalCast + ArrowNativeTypeOp,
+{
+    // Negate before narrowing to the native type so that a chunk with the
+    // magnitude of the native type's minimum value (e.g. "2147483648" for
+    // Decimal32) remains representable.
+    let signed_chunk = if negative {
+        -(chunk as i128)
+    } else {
+        chunk as i128
     };
-
-    if negative {
-        number_decimals.insert(0, '-');
+    let chunk = T::Native::from_decimal(signed_chunk)
+        .ok_or_else(|| decimal_parse_overflow::<T>(value_str))?;
+
+    // When `value` is zero the multiply would be a no-op; skipping it avoids
+    // computing 10^chunk_len, which can overflow a narrow native type even
+    // though the result (the chunk itself) is representable.
+    if value.is_zero() {
+        return Ok(chunk);
     }
 
-    let value = i256::from_string(number_decimals.as_str()).ok_or_else(|| {
-        ArrowError::InvalidArgumentError(format!(
-            "Cannot convert {} to {}: Overflow",
-            value_str,
-            T::PREFIX
-        ))
-    })?;
+    value
+        .mul_checked(decimal_pow::<T>(chunk_len, value_str)?)
+        .map_err(|_| decimal_parse_overflow::<T>(value_str))?
+        .add_checked(chunk)
+        .map_err(|_| decimal_parse_overflow::<T>(value_str))
+}
+
+/// Returns `10^exp` as a `T::Native`, or an overflow error if the result does
+/// not fit in the native type.
+#[inline]
+fn decimal_pow<T: DecimalType>(exp: usize, value_str: &str) -> 
Result<T::Native, ArrowError>
+where
+    T::Native: ArrowNativeTypeOp,
+{
+    // T::MAX_FOR_EACH_PRECISION[k] holds 10^k - 1, so adding one yields 10^k
+    // without computing a power at runtime. Exponents beyond the table always
+    // overflow: the native type cannot hold 10^(MAX_PRECISION + 1).
+    let max = T::MAX_FOR_EACH_PRECISION
+        .get(exp)
+        .ok_or_else(|| decimal_parse_overflow::<T>(value_str))?;
+    Ok(max.add_wrapping(T::Native::ONE))
+}
 
-    T::Native::from_decimal(value).ok_or_else(|| {
-        ArrowError::InvalidArgumentError(format!("Cannot convert {} to {}", 
value_str, T::PREFIX))
-    })
+#[inline]
+fn decimal_parse_overflow<T: DecimalType>(value_str: &str) -> ArrowError {
+    ArrowError::InvalidArgumentError(format!(
+        "Cannot convert {} to {}: Overflow",
+        value_str,
+        T::PREFIX
+    ))
 }
 
 pub(crate) fn generic_string_to_decimal_cast<'a, T, S>(
@@ -652,40 +722,35 @@ where
                 .and_then(|v| T::is_valid_decimal_precision(v, 
precision).then_some(v))
         });
         // Benefit:
-        //     20% performance improvement
+        //     15-19% faster than appending to a PrimitiveBuilder (measured
+        //     with the cast_kernels string-to-decimal benchmarks)
         // Soundness:
-        //     The iterator is trustedLen because it comes from an 
`StringArray`.
+        //     The iterator is trustedLen because it comes from a 
`StringArray`.
         Ok(unsafe {
             PrimitiveArray::<T>::from_trusted_len_iter(iter)
                 .with_precision_and_scale(precision, scale)?
         })
     } else {
-        let vec = from
-            .iter()
-            .map(|v| {
-                v.map(|v| {
-                    parse_string_to_decimal_native::<T>(v, scale as usize)
-                        .map_err(|_| {
+        let mut builder = PrimitiveBuilder::<T>::with_capacity(from.len());
+        for v in from.iter() {
+            match v {
+                Some(v) => {
+                    let v = parse_string_to_decimal_native::<T>(v, scale as 
usize)
+                        .map_err(|e| {
                             ArrowError::CastError(format!(
-                                "Cannot cast string '{v}' to value of {} type",
-                                T::DATA_TYPE,
+                                "Cannot cast string '{v}' to value of 
{}({precision}, {scale}) type: {e}",
+                                T::PREFIX,
                             ))
                         })
                         .and_then(|v| {
                             T::validate_decimal_precision(v, precision, 
scale).map(|()| v)
-                        })
-                })
-                .transpose()
-            })
-            .collect::<Result<Vec<_>, _>>()?;
-        // Benefit:
-        //     20% performance improvement
-        // Soundness:
-        //     The iterator is trustedLen because it comes from an 
`StringArray`.
-        Ok(unsafe {
-            PrimitiveArray::<T>::from_trusted_len_iter(vec.iter())
-                .with_precision_and_scale(precision, scale)?
-        })
+                        })?;
+                    builder.append_value(v);
+                }
+                None => builder.append_null(),
+            }
+        }
+        builder.finish().with_precision_and_scale(precision, scale)
     }
 }
 
@@ -981,18 +1046,189 @@ mod tests {
             parse_string_to_decimal_native::<Decimal128Type>("123.4567891", 
5)?,
             12345679_i128
         );
+        Ok(())
+    }
+
+    #[test]
+    fn test_parse_string_to_decimal_native_integer_widths() -> Result<(), 
ArrowError> {
+        assert_eq!(
+            parse_string_to_decimal_native::<Decimal32Type>("123.45", 2)?,
+            12_345_i32
+        );
+        assert_eq!(
+            parse_string_to_decimal_native::<Decimal32Type>("-2147483648", 0)?,
+            i32::MIN
+        );
+        assert!(parse_string_to_decimal_native::<Decimal32Type>("2147483648", 
0).is_err());
 
-        for value in ["", " ", ".", "+", "-", "+.", "-."] {
+        assert_eq!(
+            parse_string_to_decimal_native::<Decimal64Type>("123.45", 2)?,
+            12_345_i64
+        );
+        assert_eq!(
+            
parse_string_to_decimal_native::<Decimal64Type>("-9223372036854775808", 0)?,
+            i64::MIN
+        );
+        
assert!(parse_string_to_decimal_native::<Decimal64Type>("9223372036854775808", 
0).is_err());
+
+        assert_eq!(
+            
parse_string_to_decimal_native::<Decimal128Type>(&i128::MAX.to_string(), 0)?,
+            i128::MAX
+        );
+        assert_eq!(
+            
parse_string_to_decimal_native::<Decimal128Type>(&i128::MIN.to_string(), 0)?,
+            i128::MIN
+        );
+
+        assert_eq!(
+            
parse_string_to_decimal_native::<Decimal256Type>(&i256::MAX.to_string(), 0)?,
+            i256::MAX
+        );
+        assert_eq!(
+            
parse_string_to_decimal_native::<Decimal256Type>(&i256::MIN.to_string(), 0)?,
+            i256::MIN
+        );
+        Ok(())
+    }
+
+    #[test]
+    fn test_parse_string_to_decimal_native_rounding_and_padding() -> 
Result<(), ArrowError> {
+        assert_eq!(
+            parse_string_to_decimal_native::<Decimal128Type>("12.", 2)?,
+            1_200_i128
+        );
+        assert_eq!(
+            parse_string_to_decimal_native::<Decimal128Type>(".12", 2)?,
+            12_i128
+        );
+        assert_eq!(
+            parse_string_to_decimal_native::<Decimal128Type>("+.12", 2)?,
+            12_i128
+        );
+        assert_eq!(
+            parse_string_to_decimal_native::<Decimal128Type>("-.12", 2)?,
+            -12_i128
+        );
+        assert_eq!(
+            parse_string_to_decimal_native::<Decimal128Type>(".5", 0)?,
+            1_i128
+        );
+        assert_eq!(
+            parse_string_to_decimal_native::<Decimal128Type>("-.5", 0)?,
+            -1_i128
+        );
+        assert_eq!(
+            parse_string_to_decimal_native::<Decimal128Type>("1.234", 2)?,
+            123_i128
+        );
+        assert_eq!(
+            parse_string_to_decimal_native::<Decimal128Type>("1.235", 2)?,
+            124_i128
+        );
+        assert_eq!(
+            parse_string_to_decimal_native::<Decimal128Type>("-1.234", 2)?,
+            -123_i128
+        );
+        assert_eq!(
+            parse_string_to_decimal_native::<Decimal128Type>("-1.235", 2)?,
+            -124_i128
+        );
+        assert_eq!(
+            parse_string_to_decimal_native::<Decimal128Type>("-0.004", 2)?,
+            0_i128
+        );
+        assert_eq!(
+            parse_string_to_decimal_native::<Decimal128Type>("-0.005", 2)?,
+            -1_i128
+        );
+        Ok(())
+    }
+
+    #[test]
+    fn test_parse_string_to_decimal_native_rounding_overflow() {
+        
assert!(parse_string_to_decimal_native::<Decimal32Type>("2147483647.5", 
0).is_err());
+        
assert!(parse_string_to_decimal_native::<Decimal32Type>("-2147483648.5", 
0).is_err());
+
+        assert!(
+            parse_string_to_decimal_native::<Decimal128Type>(&format!("{}.5", 
i128::MAX), 0)
+                .is_err()
+        );
+        assert!(
+            parse_string_to_decimal_native::<Decimal128Type>(&format!("{}.5", 
i128::MIN), 0)
+                .is_err()
+        );
+
+        assert!(
+            parse_string_to_decimal_native::<Decimal256Type>(&format!("{}.5", 
i256::MAX), 0)
+                .is_err()
+        );
+        assert!(
+            parse_string_to_decimal_native::<Decimal256Type>(&format!("{}.5", 
i256::MIN), 0)
+                .is_err()
+        );
+    }
+
+    #[test]
+    fn test_parse_string_to_decimal_native_overflow_not_wrapped() {
+        // The unscaled value (integer digits scaled by 10^21) far exceeds the
+        // i256 range, so this must report overflow rather than wrapping to an
+        // arbitrary (possibly in-range) value
+        let input = format!("{}.12345678901234567890123", "7".repeat(71));
+        assert!(parse_string_to_decimal_native::<Decimal256Type>(&input, 
21).is_err());
+    }
+
+    #[test]
+    fn test_parse_string_to_decimal_native_long_fraction() -> Result<(), 
ArrowError> {
+        // Fractional parts longer than any native integer type parse fine;
+        // digits beyond the scale only matter for rounding
+        assert_eq!(
+            parse_string_to_decimal_native::<Decimal128Type>(&format!(".{}", 
"1".repeat(100)), 4)?,
+            1_111_i128
+        );
+        assert_eq!(
+            parse_string_to_decimal_native::<Decimal64Type>(&format!(".{}", 
"5".repeat(100)), 4)?,
+            5_556_i64
+        );
+        Ok(())
+    }
+
+    #[test]
+    fn test_parse_string_to_decimal_native_zero_with_large_scale() -> 
Result<(), ArrowError> {
+        // 10^scale overflows the native type, but zero is still representable
+        assert_eq!(
+            parse_string_to_decimal_native::<Decimal32Type>("0", 10)?,
+            0_i32
+        );
+        assert_eq!(
+            parse_string_to_decimal_native::<Decimal32Type>("-0.0", 10)?,
+            0_i32
+        );
+        assert_eq!(
+            parse_string_to_decimal_native::<Decimal64Type>("0", 20)?,
+            0_i64
+        );
+        assert_eq!(
+            parse_string_to_decimal_native::<Decimal128Type>("0", 40)?,
+            0_i128
+        );
+        assert!(parse_string_to_decimal_native::<Decimal32Type>("1", 
10).is_err());
+        Ok(())
+    }
+
+    #[test]
+    fn test_parse_string_to_decimal_native_invalid_syntax() {
+        for input in [
+            "", " ", ".", "+", "-", "+.", "-.", "1.2.3", "1e2", "1.-2", "--1",
+        ] {
             assert!(
-                parse_string_to_decimal_native::<Decimal128Type>(value, 
2).is_err(),
-                "expected {value:?} to fail parsing as Decimal128"
+                parse_string_to_decimal_native::<Decimal128Type>(input, 
2).is_err(),
+                "expected {input:?} to fail parsing as Decimal128"
             );
             assert!(
-                parse_string_to_decimal_native::<Decimal256Type>(value, 
2).is_err(),
-                "expected {value:?} to fail parsing as Decimal256"
+                parse_string_to_decimal_native::<Decimal256Type>(input, 
2).is_err(),
+                "expected {input:?} to fail parsing as Decimal256"
             );
         }
-        Ok(())
     }
 
     #[test]
diff --git a/arrow-cast/src/cast/mod.rs b/arrow-cast/src/cast/mod.rs
index e698293dd4..da783419e0 100644
--- a/arrow-cast/src/cast/mod.rs
+++ b/arrow-cast/src/cast/mod.rs
@@ -11326,7 +11326,7 @@ mod tests {
         assert!(
             casted_err
                 .to_string()
-                .contains("Cannot cast string '4.4.5' to value of 
Decimal128(38, 10) type")
+                .contains("Cannot cast string '4.4.5' to value of 
Decimal128(38, 2) type")
         );
 
         let str_array = StringArray::from(vec![". 0.123"]);
@@ -11335,7 +11335,7 @@ mod tests {
         assert!(
             casted_err
                 .to_string()
-                .contains("Cannot cast string '. 0.123' to value of 
Decimal128(38, 10) type")
+                .contains("Cannot cast string '. 0.123' to value of 
Decimal128(38, 2) type")
         );
 
         let str_array = StringArray::from(vec![""]);
@@ -11344,7 +11344,7 @@ mod tests {
         assert!(
             casted_err
                 .to_string()
-                .contains("Cannot cast string '' to value of Decimal128(38, 
10) type")
+                .contains("Cannot cast string '' to value of Decimal128(38, 2) 
type")
         );
     }
 
diff --git a/arrow/benches/cast_kernels.rs b/arrow/benches/cast_kernels.rs
index 18acbe2d6f..9d76cce1b3 100644
--- a/arrow/benches/cast_kernels.rs
+++ b/arrow/benches/cast_kernels.rs
@@ -450,6 +450,11 @@ fn add_benchmark(c: &mut Criterion) {
         string_float_array_normal,
         DataType::Decimal128(38, 3)
     );
+    benchmark_cast!(
+        "cast string to decimal256(76, 3)",
+        string_float_array_normal,
+        DataType::Decimal256(76, 3)
+    );
 
     // cast float64 to decimals
     benchmark_cast!(

Reply via email to