Skip to content

Conversation

@scovich
Copy link
Contributor

@scovich scovich commented Oct 9, 2025

Which issue does this PR close?

Rationale for this change

Bug fix

What changes are included in this PR?

Detect and directly handle large scale reductions, instead of failing on accident because the computed divisor overflows.

Also, replace the pow_checked call with a lookup into the (already existing) MAX_DECIMALXX_FOR_EACH_PRECISION array. This requires adding a new MAX_FOR_EACH_PRECISION constant to the DecimalType trait, but the corresponding arrays were already public so this seems ok?

Are these changes tested?

New unit tests exercise the scenario (and its boundary case). The tests fail without this fix.

Are there any user-facing changes?

New constant on the public DecimalType trait.

A class of decimal conversions that used to fail will now (correctly) produce zeros instead.

@github-actions github-actions bot added the arrow Changes to the arrow crate label Oct 9, 2025
@scovich
Copy link
Contributor Author

scovich commented Oct 9, 2025

CC @alamb -- not sure who the best reviewer might be?

native_type_op!(u16);
native_type_op!(u32);
native_type_op!(u64);
native_type_op!(i256, i256::ZERO, i256::ONE, i256::MIN, i256::MAX);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

opportunistic cleanup

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I verified that this form of the macro simply re-calls the same macro with the same arguments 👍

};

let half = div.div_wrapping(I::Native::from_usize(2).unwrap());
let div = max.add_wrapping(I::Native::ONE);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than paying an exponentiation, just look up the max value for that precision and add one.


let half = div.div_wrapping(I::Native::from_usize(2).unwrap());
let div = max.add_wrapping(I::Native::ONE);
let half = div.div_wrapping(I::Native::ONE.add_wrapping(I::Native::ONE));
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opportunistic cleanup: compute 2 as 1+1 (infallible) instead of converting from 2_usize (needs unwrap). It's fairly likely that the compiler emits the same code either way, tho, thanks to aggressive inlining.

}

#[test]
fn test_cast_decimal64_to_decimal64_large_scale_reduction() {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not obvious to me that we need this second version of the test, given that it's all generic code anyway.

I intentionally avoided adding cases for 128- and 256-bit decimals because IMO they add no value -- any problems in the constants should be caught by other tests, and two data points should suffice to confirm that the new code doesn't hide any size-specific assumptions.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i agree this seems adequate

Copy link
Contributor

@alamb alamb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks good to me -- thank you @scovich

native_type_op!(u16);
native_type_op!(u32);
native_type_op!(u64);
native_type_op!(i256, i256::ZERO, i256::ONE, i256::MIN, i256::MAX);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I verified that this form of the macro simply re-calls the same macro with the same arguments 👍

}

#[test]
fn test_cast_decimal32_to_decimal32_large_scale_reduction() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I verified this test fails without the code in this PR:

called `Result::unwrap()` on an `Err` value: ArithmeticOverflow("Overflow happened on: 10 ^ 10")
thread 'cast::tests::test_cast_decimal32_to_decimal32_large_scale_reduction' panicked at arrow-cast/src/cast/mod.rs:3105:9:
called `Result::unwrap()` on an `Err` value: ArithmeticOverflow("Overflow happened on: 10 ^ 10")
stack backtrace:

}

#[test]
fn test_cast_decimal64_to_decimal64_large_scale_reduction() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i agree this seems adequate

Copy link
Contributor

@liamzwbao liamzwbao left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! thanks for the fix

@alamb alamb merged commit d90faef into apache:main Oct 10, 2025
26 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

arrow Changes to the arrow crate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Decimal -> Decimal cast wrongly fails for large scale reduction

3 participants