Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 86 additions & 1 deletion datafusion/common/src/scalar/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Copy link
Copy Markdown
Contributor

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 type Decimal128(3,3), the result in the natural scale will be 0.001:

("0.001", ScalarValue::Decimal128(Some(1), 3, 3)),

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)?

Copy link
Copy Markdown
Contributor Author

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.

}
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:?}\""
Expand All @@ -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)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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:?}\""
Expand All @@ -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:?}\""
Expand Down Expand Up @@ -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()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

to_usize returns None on overflow.
shouldn't we return None when checked_sub overflows too?

} 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() )

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

remove

l.checked_sub(*r)?.checked_abs()?.to_usize()
Comment thread
findepi marked this conversation as resolved.
} else {
None
}
}
_ => None,
}
}
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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 {
Expand Down
137 changes: 92 additions & 45 deletions datafusion/optimizer/src/simplify_expressions/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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] = [

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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.

i128 and i256 pow are not hardware-backed (with i256 introducing non-trivial low/high logic), so it's probably better to precompute lists via a const function.

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 {
Expand Down Expand Up @@ -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,
}
}
Expand All @@ -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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why were powers of 10 precomputed?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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

i128::pow and i256::pow have logarithmic complexity depending on the argument (scale in our case), which is usually low. The precomputed array lookup is surely done in constant time.

My other idea about const function to precalculate this array works only for i128 since its methods are consts, which is not the case for arrow-buffer's i256. So, the const function cannot be written without tinkering with from_parts manipulations.

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
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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,
                )));
            }
        })
    });
}
pow-lookup-table        time:   [159.16 ns 161.86 ns 166.03 ns]
                        change: [-0.9307% +0.0846% +1.1886%] (p = 0.90 > 0.05)
                        No change in performance detected.
Found 10 outliers among 100 measurements (10.00%)
  5 (5.00%) low mild
  2 (2.00%) high mild
  3 (3.00%) high severe

pow-with-calculation    time:   [673.14 ns 674.23 ns 675.36 ns]
                        change: [-0.3838% -0.1634% +0.0709%] (p = 0.18 > 0.05)
                        No change in performance detected.
Found 4 outliers among 100 measurements (4.00%)
  1 (1.00%) low mild
  3 (3.00%) high mild

.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,
}
Expand Down Expand Up @@ -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
))));
}
}