-
Notifications
You must be signed in to change notification settings - Fork 342
refactor: use arity helper for Int to Decimal128 reinterpretation #5193
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
base: main
Are you sure you want to change the base?
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 | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -17,10 +17,12 @@ | |||||||||||||||||
|
|
||||||||||||||||||
| use crate::error::decimal_overflow_error; | ||||||||||||||||||
| use crate::math_funcs::utils::get_precision_scale; | ||||||||||||||||||
| use arrow::compute::kernels::arity::try_unary; | ||||||||||||||||||
| use arrow::datatypes::DataType; | ||||||||||||||||||
| use arrow::error::ArrowError; | ||||||||||||||||||
| use arrow::{ | ||||||||||||||||||
| array::{AsArray, Decimal128Builder}, | ||||||||||||||||||
| datatypes::{validate_decimal_precision, Int64Type}, | ||||||||||||||||||
| array::{AsArray, Decimal128Array}, | ||||||||||||||||||
| datatypes::{validate_decimal_precision, Decimal128Type, Int64Type}, | ||||||||||||||||||
| }; | ||||||||||||||||||
| use datafusion::common::{internal_err, DataFusionError, Result as DataFusionResult, ScalarValue}; | ||||||||||||||||||
| use datafusion::physical_plan::ColumnarValue; | ||||||||||||||||||
|
|
@@ -45,14 +47,41 @@ pub fn spark_make_decimal( | |||||||||||||||||
| ColumnarValue::Array(a) => match a.data_type() { | ||||||||||||||||||
| DataType::Int64 => { | ||||||||||||||||||
| let arr = a.as_primitive::<Int64Type>(); | ||||||||||||||||||
| let mut result = Decimal128Builder::new(); | ||||||||||||||||||
| for v in arr.into_iter() { | ||||||||||||||||||
| result.append_option(long_to_decimal(v, precision, scale, fail_on_error)?) | ||||||||||||||||||
| } | ||||||||||||||||||
| let result_type = DataType::Decimal128(precision, scale); | ||||||||||||||||||
| // The Int64 is already the unscaled Decimal128 value, so we only reinterpret | ||||||||||||||||||
| // the bits (an Arrow Int64->Decimal cast would rescale the value). Both arity | ||||||||||||||||||
| // helpers reuse the input null buffer and only invoke the closure on valid rows. | ||||||||||||||||||
| let result: Decimal128Array = if fail_on_error { | ||||||||||||||||||
|
Member
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. A question on the helper choice here. Arrow's own docs on I benchmarked the three shapes on 8192-row Int64 batches at
Your version is a genuine 1.3x to 1.9x win over what is there now. The Would you be up for trying that shape and seeing whether you reproduce the gain? The scan still finds the first offending value, so your new ordering test should keep passing. If you would rather stay with Either way, would you mind adding a |
||||||||||||||||||
| // ANSI mode: overflow is a hard error. `try_unary` surfaces the closure's | ||||||||||||||||||
| // ArrowError; unwrap the ExternalError back to DataFusionError::External so | ||||||||||||||||||
| // the ANSI error variant (not a generic ArrowError) is preserved. | ||||||||||||||||||
| try_unary::<Int64Type, _, Decimal128Type>(arr, |v| { | ||||||||||||||||||
|
Member
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.
arr.try_unary::<_, Decimal128Type, DataFusionError>(|v| {
let v = v as i128;
validate_decimal_precision(v, precision, scale)
.map(|()| v)
.map_err(|_| {
DataFusionError::External(Box::new(decimal_overflow_error(v, precision, scale)))
})
})?I compiled and ran this and it produces the identical error, same variant and same |
||||||||||||||||||
| let v = v as i128; | ||||||||||||||||||
| validate_decimal_precision(v, precision, scale) | ||||||||||||||||||
| .map(|()| v) | ||||||||||||||||||
| .map_err(|_| { | ||||||||||||||||||
| ArrowError::ExternalError(Box::new(decimal_overflow_error( | ||||||||||||||||||
| v, precision, scale, | ||||||||||||||||||
| ))) | ||||||||||||||||||
| }) | ||||||||||||||||||
| }) | ||||||||||||||||||
| .map_err(|e| match e { | ||||||||||||||||||
| ArrowError::ExternalError(inner) => DataFusionError::External(inner), | ||||||||||||||||||
| other => DataFusionError::from(other), | ||||||||||||||||||
| })? | ||||||||||||||||||
| } else { | ||||||||||||||||||
| // Non-ANSI: overflow becomes null. `unary_opt` applies the closure only to | ||||||||||||||||||
| // valid rows and marks a row null wherever the closure returns None. | ||||||||||||||||||
| arr.unary_opt::<_, Decimal128Type>(|v| { | ||||||||||||||||||
|
Member
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.
#[inline]
fn to_unscaled(v: i64, precision: u8, scale: i8) -> Option<i128> {
let v = v as i128;
validate_decimal_precision(v, precision, scale).ok().map(|()| v)
}Then the non-ANSI array path becomes |
||||||||||||||||||
| let v = v as i128; | ||||||||||||||||||
| validate_decimal_precision(v, precision, scale) | ||||||||||||||||||
| .ok() | ||||||||||||||||||
| .map(|()| v) | ||||||||||||||||||
| }) | ||||||||||||||||||
| }; | ||||||||||||||||||
|
|
||||||||||||||||||
| Ok(ColumnarValue::Array(Arc::new( | ||||||||||||||||||
| result.finish().with_data_type(result_type), | ||||||||||||||||||
| result.with_data_type(result_type), | ||||||||||||||||||
| ))) | ||||||||||||||||||
| } | ||||||||||||||||||
| av => internal_err!("Expected Int64 but found {av:?}"), | ||||||||||||||||||
|
|
@@ -106,6 +135,30 @@ mod tests { | |||||||||||||||||
| ); | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| #[test] | ||||||||||||||||||
| fn test_array_overflow_reports_first_offending_value() { | ||||||||||||||||||
| // Two distinct overflowing values with a valid value in front. Both the original | ||||||||||||||||||
| // per-row `?` loop and `try_unary` walk rows in index order, so the reported error | ||||||||||||||||||
| // must reference the FIRST overflow (111111), not the later one (222222). This locks | ||||||||||||||||||
| // the ordering semantics rather than merely "some error occurred". | ||||||||||||||||||
| let args = [ColumnarValue::Array(Arc::new(Int64Array::from(vec![ | ||||||||||||||||||
| Some(99), | ||||||||||||||||||
| Some(111111), | ||||||||||||||||||
| Some(222222), | ||||||||||||||||||
| ])))]; | ||||||||||||||||||
| let err = spark_make_decimal(&args, &DataType::Decimal128(3, 0), true) | ||||||||||||||||||
| .expect_err("overflow should error when fail_on_error is set"); | ||||||||||||||||||
| let msg = err.to_string(); | ||||||||||||||||||
| assert!( | ||||||||||||||||||
| msg.contains("111111"), | ||||||||||||||||||
| "should report first overflow: {msg}" | ||||||||||||||||||
| ); | ||||||||||||||||||
| assert!( | ||||||||||||||||||
| !msg.contains("222222"), | ||||||||||||||||||
| "should not report later overflow: {msg}" | ||||||||||||||||||
| ); | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| #[test] | ||||||||||||||||||
| fn test_array_overflow_nulls_when_not_fail_on_error() { | ||||||||||||||||||
| let result = spark_make_decimal(&overflow_args(), &DataType::Decimal128(3, 0), false) | ||||||||||||||||||
|
|
||||||||||||||||||
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.
This reads well, and the note about an Arrow cast rescaling the value is worth having in the code. I confirmed that dropping the
downcast_ref+ok_or_elseis safe, since the match is onarray.data_type(), so nothing new can panic here.The substance of the change is null-buffer handling, and
test_convert_int32_to_decimal128andtest_convert_int64_to_decimal128both use all-valid arrays, so that is the one thing the tests do not exercise. Could you add a null to those arrays? A sliced input would be good too, followingtest_map_data_conversion_sliced_maparrayfurther down the file. I checked both cases by hand and they behave correctly, so this is just about locking the behaviour in.