-
Notifications
You must be signed in to change notification settings - Fork 343
perf: track decimal overflow without rescanning results #5044
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
1104c78
e190704
bc72ccc
52fabf5
07944c8
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 |
|---|---|---|
|
|
@@ -29,6 +29,7 @@ use arrow::record_batch::RecordBatch; | |
| use datafusion::common::Result; | ||
| use datafusion::logical_expr::ColumnarValue; | ||
| use datafusion::physical_expr::PhysicalExpr; | ||
| use std::cell::Cell; | ||
| use std::fmt::{Display, Formatter}; | ||
| use std::hash::Hash; | ||
| use std::sync::Arc; | ||
|
|
@@ -214,6 +215,7 @@ impl PhysicalExpr for WideDecimalBinaryExpr { | |
|
|
||
| let bound = max_for_precision(p_out); | ||
| let neg_bound = i256::ZERO.wrapping_sub(bound); | ||
| let overflowed = Cell::new(false); | ||
|
|
||
| let result: Decimal128Array = match op { | ||
| WideDecimalOp::Add | WideDecimalOp::Subtract => { | ||
|
|
@@ -249,7 +251,7 @@ impl PhysicalExpr for WideDecimalBinaryExpr { | |
| } else { | ||
| raw | ||
| }; | ||
| check_overflow_and_convert(result, bound, neg_bound, eval_mode) | ||
| check_overflow_and_convert(result, bound, neg_bound, eval_mode, &overflowed) | ||
| })? | ||
| } | ||
| WideDecimalOp::Multiply => { | ||
|
|
@@ -276,12 +278,12 @@ impl PhysicalExpr for WideDecimalBinaryExpr { | |
| } else { | ||
| raw | ||
| }; | ||
| check_overflow_and_convert(result, bound, neg_bound, eval_mode) | ||
| check_overflow_and_convert(result, bound, neg_bound, eval_mode, &overflowed) | ||
| })? | ||
| } | ||
| }; | ||
|
|
||
| let result = if eval_mode != EvalMode::Ansi { | ||
| let result = if overflowed.get() { | ||
| result.null_if_overflow_precision(p_out) | ||
| } else { | ||
| result | ||
|
|
@@ -329,20 +331,22 @@ impl PhysicalExpr for WideDecimalBinaryExpr { | |
| } | ||
|
|
||
| /// Check if the i256 result fits in the output precision. In Ansi mode, return an error | ||
| /// on overflow. In Legacy/Try mode, return i128::MAX as a sentinel value that will be | ||
| /// nullified by `null_if_overflow_precision`. | ||
| /// on overflow. In Legacy/Try mode, record the overflow and return i128::MAX as a sentinel | ||
| /// value that will be nullified by `null_if_overflow_precision`. | ||
| #[inline] | ||
| fn check_overflow_and_convert( | ||
| result: i256, | ||
| bound: i256, | ||
| neg_bound: i256, | ||
| eval_mode: EvalMode, | ||
| overflowed: &Cell<bool>, | ||
| ) -> Result<i128, ArrowError> { | ||
| if result > bound || result < neg_bound { | ||
| if eval_mode == EvalMode::Ansi { | ||
| return Err(ArrowError::ComputeError("Arithmetic overflow".to_string())); | ||
| } | ||
| // Sentinel value — will be nullified by null_if_overflow_precision | ||
| overflowed.set(true); | ||
| Ok(i128::MAX) | ||
| } else { | ||
| Ok(result.to_i128().unwrap()) | ||
|
|
@@ -506,6 +510,23 @@ mod tests { | |
| assert!(arr.is_null(0)); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_overflow_with_nulls_legacy_mode() { | ||
|
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. Good test, and I like that Since the guard now also governs the Scalar x Scalar path, it might be worth adding a companion to
Member
Author
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. added. thanks! |
||
| let batch = make_batch( | ||
| vec![Some(4), Some(5), None], | ||
| 38, | ||
| 0, | ||
| vec![Some(5), Some(5), Some(1)], | ||
| 38, | ||
| 0, | ||
| ); | ||
| let result = eval_expr(&batch, WideDecimalOp::Add, 1, 0, EvalMode::Legacy).unwrap(); | ||
| let arr = result.as_primitive::<Decimal128Type>(); | ||
| assert_eq!(arr.value(0), 9); | ||
| assert!(arr.is_null(1)); | ||
| assert!(arr.is_null(2)); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_overflow_ansi_mode_returns_error() { | ||
| let batch = make_batch(vec![Some(5)], 38, 0, vec![Some(5)], 38, 0); | ||
|
|
@@ -620,6 +641,28 @@ mod tests { | |
| } | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_scalar_scalar_overflow_returns_null_scalar() { | ||
| use datafusion::common::ScalarValue; | ||
| use datafusion::physical_expr::expressions::Literal; | ||
|
|
||
| let value = ScalarValue::Decimal128(Some(5), 38, 0); | ||
| let expr = WideDecimalBinaryExpr::new( | ||
| Arc::new(Literal::new(value.clone())), | ||
| Arc::new(Literal::new(value)), | ||
| WideDecimalOp::Multiply, | ||
| 1, | ||
| 0, | ||
| EvalMode::Legacy, | ||
| ); | ||
| let batch = RecordBatch::new_empty(Arc::new(Schema::empty())); | ||
|
|
||
| assert!(matches!( | ||
| expr.evaluate(&batch).unwrap(), | ||
| ColumnarValue::Scalar(ScalarValue::Decimal128(None, 1, 0)) | ||
| )); | ||
| } | ||
|
|
||
| /// Companion test: when at least one input is an Array, the result must remain an Array. | ||
| /// Guards against over-eager scalar-unwrapping in the fix. | ||
| #[test] | ||
|
|
||
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.
The two overflow shapes both end up with an overflow at index 0, since
0 % 17 == 0and0 % 2 == 0. That meanscontainsreturns on the very first element in both cases, so the sparse and dense benches never exercise a scan longer than one element.Could we add a shape where the only overflow sits at the last index? That is the case where the guard costs the most, because you pay the full 8192-element scan and then still pay the masking pass on top. It would be good to see a number for it. Right now the "No significant change" results for the sparse and dense shapes are really measuring the index-0 case, so they do not tell us much about the overflow path.
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.
added. thanks!