-
Notifications
You must be signed in to change notification settings - Fork 1k
Fix broken decimal->decimal casting with large scale reduction #8580
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -188,11 +188,19 @@ where | |
| // [99999] -> [99] + 1 = [100], a cast to Decimal(2, 0) would not be possible | ||
| let is_infallible_cast = (input_precision as i8) - delta_scale < (output_precision as i8); | ||
|
|
||
| let div = I::Native::from_decimal(10_i128) | ||
| .unwrap() | ||
| .pow_checked(delta_scale as u32)?; | ||
| // delta_scale is guaranteed to be > 0, but may also be larger than I::MAX_PRECISION. If so, the | ||
| // scale change divides out more digits than the input has precision and the result of the cast | ||
| // is always zero. For example, if we try to apply delta_scale=10 a decimal32 value, the largest | ||
| // possible result is 999999999/10000000000 = 0.0999999999, which rounds to zero. Smaller values | ||
| // (e.g. 1/10000000000) or larger delta_scale (e.g. 999999999/10000000000000) produce even | ||
| // smaller results, which also round to zero. In that case, just return an array of zeros. | ||
| let Some(max) = I::MAX_FOR_EACH_PRECISION.get(delta_scale as usize) else { | ||
| let zeros = vec![O::Native::ZERO; array.len()]; | ||
| return Ok(PrimitiveArray::new(zeros.into(), array.nulls().cloned())); | ||
| }; | ||
|
|
||
| let half = div.div_wrapping(I::Native::from_usize(2).unwrap()); | ||
| let div = max.add_wrapping(I::Native::ONE); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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::ONE.add_wrapping(I::Native::ONE)); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Opportunistic cleanup: compute 2 as |
||
| let half_neg = half.neg_wrapping(); | ||
|
|
||
| let f = |x: I::Native| { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3084,6 +3084,32 @@ mod tests { | |
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_cast_decimal32_to_decimal32_large_scale_reduction() { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I verified this test fails without the code in this PR: |
||
| let array = vec![Some(-999999999), Some(0), Some(999999999), None]; | ||
| let array = create_decimal32_array(array, 9, 3).unwrap(); | ||
|
|
||
| // Divide out all digits of precision -- rounding could still produce +/- 1 | ||
| let output_type = DataType::Decimal32(9, -6); | ||
| assert!(can_cast_types(array.data_type(), &output_type)); | ||
| generate_cast_test_case!( | ||
| &array, | ||
| Decimal32Array, | ||
| &output_type, | ||
| vec![Some(-1), Some(0), Some(1), None] | ||
| ); | ||
|
|
||
| // Divide out more digits than we have precision -- all-zero result | ||
| let output_type = DataType::Decimal32(9, -7); | ||
| assert!(can_cast_types(array.data_type(), &output_type)); | ||
| generate_cast_test_case!( | ||
| &array, | ||
| Decimal32Array, | ||
| &output_type, | ||
| vec![Some(0), Some(0), Some(0), None] | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_cast_decimal64_to_decimal64_overflow() { | ||
| let input_type = DataType::Decimal64(18, 3); | ||
|
|
@@ -3106,6 +3132,37 @@ mod tests { | |
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_cast_decimal64_to_decimal64_large_scale_reduction() { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i agree this seems adequate |
||
| let array = vec![ | ||
| Some(-999999999999999999), | ||
| Some(0), | ||
| Some(999999999999999999), | ||
| None, | ||
| ]; | ||
| let array = create_decimal64_array(array, 18, 3).unwrap(); | ||
|
|
||
| // Divide out all digits of precision -- rounding could still produce +/- 1 | ||
| let output_type = DataType::Decimal64(18, -15); | ||
| assert!(can_cast_types(array.data_type(), &output_type)); | ||
| generate_cast_test_case!( | ||
| &array, | ||
| Decimal64Array, | ||
| &output_type, | ||
| vec![Some(-1), Some(0), Some(1), None] | ||
| ); | ||
|
|
||
| // Divide out more digits than we have precision -- all-zero result | ||
| let output_type = DataType::Decimal64(18, -16); | ||
| assert!(can_cast_types(array.data_type(), &output_type)); | ||
| generate_cast_test_case!( | ||
| &array, | ||
| Decimal64Array, | ||
| &output_type, | ||
| vec![Some(0), Some(0), Some(0), None] | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_cast_floating_to_decimals() { | ||
| for output_type in [ | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
opportunistic cleanup
There was a problem hiding this comment.
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 👍