-
Notifications
You must be signed in to change notification settings - Fork 2.3k
feat: Add ScalarValue::{new_one,new_zero,new_ten,distance} support for Decimal128 and Decimal256
#16831
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
feat: Add ScalarValue::{new_one,new_zero,new_ten,distance} support for Decimal128 and Decimal256
#16831
Changes from 3 commits
459f76d
bf3a35b
ba23e8c
15eb1b0
06551a7
c536f50
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 |
|---|---|---|
|
|
@@ -1382,6 +1382,12 @@ impl ScalarValue { | |
| DataType::Float16 => ScalarValue::Float16(Some(f16::from_f32(1.0))), | ||
| DataType::Float32 => ScalarValue::Float32(Some(1.0)), | ||
| DataType::Float64 => ScalarValue::Float64(Some(1.0)), | ||
| DataType::Decimal128(precision, scale) => { | ||
| ScalarValue::Decimal128(Some(1), *precision, *scale) | ||
| } | ||
| DataType::Decimal256(precision, scale) => { | ||
| ScalarValue::Decimal256(Some(i256::ONE), *precision, *scale) | ||
| } | ||
| _ => { | ||
| return _not_impl_err!( | ||
| "Can't create an one scalar from data_type \"{datatype:?}\"" | ||
|
|
@@ -1400,6 +1406,12 @@ impl ScalarValue { | |
| DataType::Float16 => ScalarValue::Float16(Some(f16::from_f32(-1.0))), | ||
| DataType::Float32 => ScalarValue::Float32(Some(-1.0)), | ||
| DataType::Float64 => ScalarValue::Float64(Some(-1.0)), | ||
| DataType::Decimal128(precision, scale) => { | ||
| ScalarValue::Decimal128(Some(-1), *precision, *scale) | ||
|
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. |
||
| } | ||
| DataType::Decimal256(precision, scale) => { | ||
| ScalarValue::Decimal256(Some(i256::MINUS_ONE), *precision, *scale) | ||
| } | ||
| _ => { | ||
| return _not_impl_err!( | ||
| "Can't create a negative one scalar from data_type \"{datatype:?}\"" | ||
|
|
@@ -1421,6 +1433,12 @@ impl ScalarValue { | |
| DataType::Float16 => ScalarValue::Float16(Some(f16::from_f32(10.0))), | ||
| DataType::Float32 => ScalarValue::Float32(Some(10.0)), | ||
| DataType::Float64 => ScalarValue::Float64(Some(10.0)), | ||
| DataType::Decimal128(precision, scale) => { | ||
| ScalarValue::Decimal128(Some(10), *precision, *scale) | ||
| } | ||
| DataType::Decimal256(precision, scale) => { | ||
| ScalarValue::Decimal256(Some(i256::from(10)), *precision, *scale) | ||
| } | ||
| _ => { | ||
| return _not_impl_err!( | ||
| "Can't create a ten scalar from data_type \"{datatype:?}\"" | ||
|
|
@@ -1790,6 +1808,27 @@ impl ScalarValue { | |
| (Self::Float64(Some(l)), Self::Float64(Some(r))) => { | ||
| Some((l - r).abs().round() as _) | ||
| } | ||
| ( | ||
| Self::Decimal128(Some(l), lprecision, lscale), | ||
| Self::Decimal128(Some(r), rprecision, rscale), | ||
| ) => { | ||
| if lprecision == rprecision && lscale == rscale { | ||
| l.checked_sub(*r)?.abs().to_usize() | ||
|
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. to_usize returns None on overflow. |
||
| } else { | ||
| None | ||
| } | ||
| } | ||
| ( | ||
| Self::Decimal256(Some(l), lprecision, lscale), | ||
| Self::Decimal256(Some(r), rprecision, rscale), | ||
| ) => { | ||
| if lprecision == rprecision && lscale == rscale { | ||
| // l.checked_sub(*r).and_then( |v| v.checked_abs() ).and_then(|v| v.to_usize() ) | ||
|
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. remove |
||
| l.checked_sub(*r)?.checked_abs()?.to_usize() | ||
|
findepi marked this conversation as resolved.
|
||
| } else { | ||
| None | ||
| } | ||
| } | ||
| _ => None, | ||
| } | ||
| } | ||
|
|
@@ -6946,6 +6985,26 @@ mod tests { | |
| ScalarValue::Float64(Some(-9.9)), | ||
| 5, | ||
| ), | ||
| ( | ||
| ScalarValue::Decimal128(Some(10), 1, 0), | ||
| ScalarValue::Decimal128(Some(5), 1, 0), | ||
| 5, | ||
| ), | ||
| ( | ||
| ScalarValue::Decimal128(Some(5), 1, 0), | ||
| ScalarValue::Decimal128(Some(10), 1, 0), | ||
| 5, | ||
| ), | ||
| ( | ||
| ScalarValue::Decimal256(Some(10.into()), 1, 0), | ||
| ScalarValue::Decimal256(Some(5.into()), 1, 0), | ||
| 5, | ||
| ), | ||
| ( | ||
| ScalarValue::Decimal256(Some(5.into()), 1, 0), | ||
| ScalarValue::Decimal256(Some(10.into()), 1, 0), | ||
| 5, | ||
| ), | ||
| ]; | ||
| for (lhs, rhs, expected) in cases.iter() { | ||
| let distance = lhs.distance(rhs).unwrap(); | ||
|
|
@@ -6994,7 +7053,33 @@ mod tests { | |
| (ScalarValue::Date64(Some(0)), ScalarValue::Date64(Some(1))), | ||
| ( | ||
| ScalarValue::Decimal128(Some(123), 5, 5), | ||
| ScalarValue::Decimal128(Some(120), 5, 5), | ||
| ScalarValue::Decimal128(Some(120), 5, 3), | ||
| ), | ||
| ( | ||
| ScalarValue::Decimal128(Some(123), 5, 5), | ||
| ScalarValue::Decimal128(Some(120), 3, 5), | ||
| ), | ||
| ( | ||
| ScalarValue::Decimal256(Some(123.into()), 5, 5), | ||
| ScalarValue::Decimal256(Some(120.into()), 3, 5), | ||
| ), | ||
| // Distance 2 * 2^50 is larger than usize | ||
| ( | ||
| ScalarValue::Decimal256( | ||
| Some(i256::from_parts(0, 2_i64.pow(50).into())), | ||
| 1, | ||
| 0, | ||
| ), | ||
| ScalarValue::Decimal256( | ||
| Some(i256::from_parts(0, (-(2_i64).pow(50)).into())), | ||
| 1, | ||
| 0, | ||
| ), | ||
| ), | ||
| // Distance overflow | ||
| ( | ||
| ScalarValue::Decimal256(Some(i256::from_parts(0, i128::MAX)), 1, 0), | ||
| ScalarValue::Decimal256(Some(i256::from_parts(0, -i128::MAX)), 1, 0), | ||
| ), | ||
| ]; | ||
| for (lhs, rhs) in cases { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,54 +17,14 @@ | |
|
|
||
| //! Utility functions for expression simplification | ||
|
|
||
| use arrow::datatypes::i256; | ||
| use datafusion_common::{internal_err, Result, ScalarValue}; | ||
| use datafusion_expr::{ | ||
| expr::{Between, BinaryExpr, InList}, | ||
| expr_fn::{and, bitwise_and, bitwise_or, or}, | ||
| Expr, Like, Operator, | ||
| }; | ||
|
|
||
| pub static POWS_OF_TEN: [i128; 38] = [ | ||
|
Contributor
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 guess this lookup table is used for performance? We can do some measurements to check if it's useful.
Contributor
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. Let me conduct some tests. It could also have been introduced for clarity, too.
|
||
| 1, | ||
| 10, | ||
| 100, | ||
| 1000, | ||
| 10000, | ||
| 100000, | ||
| 1000000, | ||
| 10000000, | ||
| 100000000, | ||
| 1000000000, | ||
| 10000000000, | ||
| 100000000000, | ||
| 1000000000000, | ||
| 10000000000000, | ||
| 100000000000000, | ||
| 1000000000000000, | ||
| 10000000000000000, | ||
| 100000000000000000, | ||
| 1000000000000000000, | ||
| 10000000000000000000, | ||
| 100000000000000000000, | ||
| 1000000000000000000000, | ||
| 10000000000000000000000, | ||
| 100000000000000000000000, | ||
| 1000000000000000000000000, | ||
| 10000000000000000000000000, | ||
| 100000000000000000000000000, | ||
| 1000000000000000000000000000, | ||
| 10000000000000000000000000000, | ||
| 100000000000000000000000000000, | ||
| 1000000000000000000000000000000, | ||
| 10000000000000000000000000000000, | ||
| 100000000000000000000000000000000, | ||
| 1000000000000000000000000000000000, | ||
| 10000000000000000000000000000000000, | ||
| 100000000000000000000000000000000000, | ||
| 1000000000000000000000000000000000000, | ||
| 10000000000000000000000000000000000000, | ||
| ]; | ||
|
|
||
| /// returns true if `needle` is found in a chain of search_op | ||
| /// expressions. Such as: (A AND B) AND C | ||
| fn expr_contains_inner(expr: &Expr, needle: &Expr, search_op: Operator) -> bool { | ||
|
|
@@ -150,6 +110,11 @@ pub fn is_zero(s: &Expr) -> bool { | |
| Expr::Literal(ScalarValue::Float32(Some(v)), _) if *v == 0. => true, | ||
| Expr::Literal(ScalarValue::Float64(Some(v)), _) if *v == 0. => true, | ||
| Expr::Literal(ScalarValue::Decimal128(Some(v), _p, _s), _) if *v == 0 => true, | ||
| Expr::Literal(ScalarValue::Decimal256(Some(v), _p, _s), _) | ||
| if *v == i256::ZERO => | ||
| { | ||
| true | ||
| } | ||
| _ => false, | ||
| } | ||
| } | ||
|
|
@@ -168,10 +133,17 @@ pub fn is_one(s: &Expr) -> bool { | |
| Expr::Literal(ScalarValue::Float64(Some(v)), _) if *v == 1. => true, | ||
| Expr::Literal(ScalarValue::Decimal128(Some(v), _p, s), _) => { | ||
| *s >= 0 | ||
| && POWS_OF_TEN | ||
|
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. Why were powers of 10 precomputed?
Contributor
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. I think the initial idea is to mirror Arrow's approach https://github.com/apache/arrow-rs/blob/123045cc766d42d1eb06ee8bb3f09e39ea995ddc/arrow-data/src/decimal.rs
My other idea about const function to precalculate this array works only for const fn calculate_pows_of_ten_decimal128() -> [i128; DECIMAL128_MAX_PRECISION as usize] {
let mut result = [0i128; DECIMAL128_MAX_PRECISION as usize];
result[0] = 1;
let mut i = 0;
while i <(DECIMAL128_MAX_PRECISION-1) as usize {
result[i+1] = result[i] * 10;
i += 1
}
result
}
Contributor
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. maybe since we don't have measurements one way or the other to justfy this change, we revert this change and keep the original approach? Other than this particular change, this PR looks good to me
Contributor
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. Rolled back to the original lookup map. The new calculation method is used only for Decimal256.
Contributor
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 have checked the lookup table approach is faster, perhaps it's better to implement such table in Arrow instead. fn bench_println(c: &mut Criterion) {
c.bench_function("pow-lookup-table", |b| {
b.iter(|| {
let precision = 30;
let max_scale = 25;
for s in 1..max_scale {
is_one(&lit(ScalarValue::Decimal128(
Some(i128::from(1)),
precision,
max_scale,
)));
}
})
});
// Decimal256 doesn't have a pre-computed power table
c.bench_function("pow-with-calculation", |b| {
b.iter(|| {
let precision = 30;
let max_scale = 25;
for s in 1..max_scale {
is_one(&lit(ScalarValue::Decimal256(
Some(i256::from(1)),
precision,
max_scale,
)));
}
})
});
} |
||
| .get(*s as usize) | ||
| .map(|x| x == v) | ||
| .unwrap_or_default() | ||
| && match i128::from(10).checked_pow(*s as u32) { | ||
| Some(res) => res == *v, | ||
| None => false, | ||
| } | ||
| } | ||
| Expr::Literal(ScalarValue::Decimal256(Some(v), _p, s), _) => { | ||
| *s >= 0 | ||
| && match i256::from(10).checked_pow(*s as u32) { | ||
| Some(res) => res == *v, | ||
| None => false, | ||
| } | ||
| } | ||
| _ => false, | ||
| } | ||
|
|
@@ -365,3 +337,78 @@ pub fn distribute_negation(expr: Expr) -> Expr { | |
| _ => Expr::Negative(Box::new(expr)), | ||
| } | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::{is_one, is_zero}; | ||
| use arrow::datatypes::i256; | ||
| use datafusion_common::ScalarValue; | ||
| use datafusion_expr::lit; | ||
|
|
||
| #[test] | ||
| fn test_is_zero() { | ||
| assert!(is_zero(&lit(ScalarValue::Int8(Some(0))))); | ||
| assert!(is_zero(&lit(ScalarValue::Float32(Some(0.0))))); | ||
| assert!(is_zero(&lit(ScalarValue::Decimal128( | ||
| Some(i128::from(0)), | ||
| 9, | ||
| 0 | ||
| )))); | ||
| assert!(is_zero(&lit(ScalarValue::Decimal128( | ||
| Some(i128::from(0)), | ||
| 9, | ||
| 5 | ||
| )))); | ||
| assert!(is_zero(&lit(ScalarValue::Decimal256( | ||
| Some(i256::ZERO), | ||
| 9, | ||
| 0 | ||
| )))); | ||
| assert!(is_zero(&lit(ScalarValue::Decimal256( | ||
| Some(i256::ZERO), | ||
| 9, | ||
| 5 | ||
| )))); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_is_one() { | ||
| assert!(is_one(&lit(ScalarValue::Int8(Some(1))))); | ||
| assert!(is_one(&lit(ScalarValue::Float32(Some(1.0))))); | ||
| assert!(is_one(&lit(ScalarValue::Decimal128( | ||
| Some(i128::from(1)), | ||
| 9, | ||
| 0 | ||
| )))); | ||
| assert!(is_one(&lit(ScalarValue::Decimal128( | ||
| Some(i128::from(10)), | ||
| 9, | ||
| 1 | ||
| )))); | ||
| assert!(is_one(&lit(ScalarValue::Decimal128( | ||
| Some(i128::from(100)), | ||
| 9, | ||
| 2 | ||
| )))); | ||
| assert!(is_one(&lit(ScalarValue::Decimal256( | ||
| Some(i256::from(1)), | ||
| 9, | ||
| 0 | ||
| )))); | ||
| assert!(is_one(&lit(ScalarValue::Decimal256( | ||
| Some(i256::from(10)), | ||
| 9, | ||
| 1 | ||
| )))); | ||
| assert!(is_one(&lit(ScalarValue::Decimal256( | ||
| Some(i256::from(100)), | ||
| 9, | ||
| 2 | ||
| )))); | ||
| assert!(!is_one(&lit(ScalarValue::Decimal256( | ||
| Some(i256::from(100)), | ||
| 9, | ||
| -1 | ||
| )))); | ||
| } | ||
| } | ||
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.
If we create a
new_one()for typeDecimal128(3,3), the result in the natural scale will be 0.001:datafusion/datafusion/sql/src/expr/value.rs
Line 467 in 3869857
I think this function is supposed to construct 1 in the natural scale? So in this example it should be converted to
Decimal128(Some(1000), 3, 3)?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 support for scale, input verification and some tests. It should match Arrow's decimal semantics now.