blob: 8320d1480064f40e368f6aa1b852dd891ebb17f5 [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
use crate::arithmetic_overflow_error;
use crate::math_funcs::utils::{get_precision_scale, make_decimal_array, make_decimal_scalar};
use arrow::array::{Array, ArrowNativeTypeOp};
use arrow::array::{Int16Array, Int32Array, Int64Array, Int8Array};
use arrow::datatypes::DataType;
use arrow::error::ArrowError;
use datafusion::common::{exec_err, internal_err, DataFusionError, ScalarValue};
use datafusion::physical_plan::ColumnarValue;
use std::{cmp::min, sync::Arc};
macro_rules! integer_round {
($X:expr, $DIV:expr, $HALF:expr, $FAIL_ON_ERROR:expr) => {{
let rem = $X % $DIV;
if rem <= -$HALF {
if $FAIL_ON_ERROR {
($X - rem).sub_checked($DIV).map_err(|_| {
ArrowError::ComputeError(arithmetic_overflow_error("integer").to_string())
})
} else {
Ok(($X - rem).sub_wrapping($DIV))
}
} else if rem >= $HALF {
if $FAIL_ON_ERROR {
($X - rem).add_checked($DIV).map_err(|_| {
ArrowError::ComputeError(arithmetic_overflow_error("integer").to_string())
})
} else {
Ok(($X - rem).add_wrapping($DIV))
}
} else {
if $FAIL_ON_ERROR {
$X.sub_checked(rem).map_err(|_| {
ArrowError::ComputeError(arithmetic_overflow_error("integer").to_string())
})
} else {
Ok($X.sub_wrapping(rem))
}
}
}};
}
// Round a single native integer when `10^(-point)` does not fit in the native
// integer type but still fits in i128. The caller has already excluded the
// case where `div` fits the native type, so `|x| <= NATIVE::MAX < div`, which
// makes `x % div == x`: no division is needed here, and the result is either
// `0` (when `|x| < half`) or `sign(x) * div` — the latter always overflows the
// native type. Under ANSI we throw, under legacy we wrap by truncation
// (matches `BigDecimal.longValue`'s low-64-bit semantics).
macro_rules! integer_round_widened {
($X:expr, $DIV:expr, $HALF:expr, $NATIVE:ty, $FAIL_ON_ERROR:expr) => {{
let x128 = $X as i128;
debug_assert!(
x128 > -$DIV && x128 < $DIV,
"integer_round_widened! requires div to overflow the native type"
);
if x128 > -$HALF && x128 < $HALF {
Ok(0 as $NATIVE)
} else if $FAIL_ON_ERROR {
Err(ArrowError::ComputeError(
arithmetic_overflow_error("integer").to_string(),
))
} else {
Ok((if x128 >= $HALF { $DIV } else { -$DIV }) as $NATIVE)
}
}};
}
// Lift an `ArrowError` from the rounding macros into a `DataFusionError`,
// returning early from the enclosing function. Used by `round_integer_scalar!`.
macro_rules! round_scalar_result {
($RESULT:expr) => {
match $RESULT {
Ok(v) => Some(v),
Err(e) => {
return Err(DataFusionError::ArrowError(
Box::from(e),
Some(DataFusionError::get_back_trace()),
))
}
}
};
}
macro_rules! round_integer_array {
($ARRAY:expr, $POINT:expr, $TYPE:ty, $NATIVE:ty, $FAIL_ON_ERROR:expr) => {{
let array = $ARRAY.as_any().downcast_ref::<$TYPE>().unwrap();
let ten: $NATIVE = 10;
let point_abs = (-(*$POINT)) as u32;
let result: $TYPE = if let Some(div) = ten.checked_pow(point_abs) {
let half = div / 2;
arrow::compute::kernels::arity::try_unary(array, |x| {
integer_round!(x, div, half, $FAIL_ON_ERROR)
})?
} else if let Some(div) = 10_i128.checked_pow(point_abs) {
let half = div / 2;
arrow::compute::kernels::arity::try_unary(array, |x| {
integer_round_widened!(x, div, half, $NATIVE, $FAIL_ON_ERROR)
})?
} else {
// Even i128 cannot hold 10^(-point); every bounded native
// integer rounds to 0.
arrow::compute::kernels::arity::try_unary(array, |_| Ok(0))?
};
Ok(ColumnarValue::Array(Arc::new(result)))
}};
}
macro_rules! round_integer_scalar {
($SCALAR:expr, $POINT:expr, $TYPE:expr, $NATIVE:ty, $FAIL_ON_ERROR:expr) => {{
let ten: $NATIVE = 10;
let point_abs = (-(*$POINT)) as u32;
let scalar_opt = match $SCALAR {
None => None,
Some(x) => {
if let Some(div) = ten.checked_pow(point_abs) {
let half = div / 2;
round_scalar_result!(integer_round!(*x, div, half, $FAIL_ON_ERROR))
} else if let Some(div) = 10_i128.checked_pow(point_abs) {
let half = div / 2;
round_scalar_result!(integer_round_widened!(
*x,
div,
half,
$NATIVE,
$FAIL_ON_ERROR
))
} else {
// Even i128 cannot hold 10^(-point); every bounded native
// integer rounds to 0.
Some(0)
}
}
};
Ok(ColumnarValue::Scalar($TYPE(scalar_opt)))
}};
}
/// `round` function that simulates Spark `round` expression
///
/// Float and double are deliberately absent: Spark rounds them through a `BigDecimal` built from
/// `java.lang.Double.toString()`, which no native kernel reproduces, so `CometRound` reports those
/// inputs as `Unsupported` and routes them to the JVM codegen dispatcher instead. They never reach
/// this function, and fall through to the catch-all error below if they somehow do.
pub fn spark_round(
args: &[ColumnarValue],
data_type: &DataType,
fail_on_error: bool,
) -> Result<ColumnarValue, DataFusionError> {
let value = &args[0];
let point = &args[1];
let ColumnarValue::Scalar(ScalarValue::Int64(Some(point))) = point else {
return internal_err!("Invalid point argument for Round(): {:#?}", point);
};
match value {
ColumnarValue::Array(array) => match array.data_type() {
DataType::Int64 if *point < 0 => {
round_integer_array!(array, point, Int64Array, i64, fail_on_error)
}
DataType::Int32 if *point < 0 => {
round_integer_array!(array, point, Int32Array, i32, fail_on_error)
}
DataType::Int16 if *point < 0 => {
round_integer_array!(array, point, Int16Array, i16, fail_on_error)
}
DataType::Int8 if *point < 0 => {
round_integer_array!(array, point, Int8Array, i8, fail_on_error)
}
DataType::Decimal128(_, scale) if *scale >= 0 => {
let f = decimal_round_f(scale, point);
let (precision, scale) = get_precision_scale(data_type);
make_decimal_array(array, precision, scale, &f)
}
dt => exec_err!("Not supported datatype for ROUND: {dt}"),
},
ColumnarValue::Scalar(a) => match a {
ScalarValue::Int64(a) if *point < 0 => {
round_integer_scalar!(a, point, ScalarValue::Int64, i64, fail_on_error)
}
ScalarValue::Int32(a) if *point < 0 => {
round_integer_scalar!(a, point, ScalarValue::Int32, i32, fail_on_error)
}
ScalarValue::Int16(a) if *point < 0 => {
round_integer_scalar!(a, point, ScalarValue::Int16, i16, fail_on_error)
}
ScalarValue::Int8(a) if *point < 0 => {
round_integer_scalar!(a, point, ScalarValue::Int8, i8, fail_on_error)
}
ScalarValue::Decimal128(a, _, scale) if *scale >= 0 => {
let f = decimal_round_f(scale, point);
let (precision, scale) = get_precision_scale(data_type);
make_decimal_scalar(a, precision, scale, &f)
}
dt => exec_err!("Not supported datatype for ROUND: {dt}"),
},
}
}
// Spark uses BigDecimal. See RoundBase implementation in Spark. Instead, we do the same by
// 1) add the half of divisor, 2) round down by division, 3) adjust precision by multiplication
#[inline]
fn decimal_round_f(scale: &i8, point: &i64) -> Box<dyn Fn(i128) -> i128> {
if *point < 0 {
if let Some(div) = 10_i128.checked_pow((-(*point) as u32) + (*scale as u32)) {
let half = div / 2;
let mul = 10_i128.pow_wrapping((-(*point)) as u32);
// i128 can hold 39 digits of a base 10 number, adding half will not cause overflow
Box::new(move |x: i128| (x + x.signum() * half) / div * mul)
} else {
Box::new(move |_: i128| 0)
}
} else {
let div = 10_i128.pow_wrapping((*scale as u32) - min(*scale as u32, *point as u32));
let half = div / 2;
Box::new(move |x: i128| (x + x.signum() * half) / div)
}
}
#[cfg(test)]
mod test {
use std::sync::Arc;
use crate::spark_round;
use arrow::array::Int64Array;
use arrow::datatypes::DataType;
use datafusion::common::cast::as_int64_array;
use datafusion::common::{Result, ScalarValue};
use datafusion::physical_plan::ColumnarValue;
// Regression tests for https://github.com/apache/datafusion-comet/issues/5070:
// round(Int64, scale) where `10^(-scale)` does not fit in i64. For scale=-19,
// values with |x| >= 5e18 round to sign(x)*1e19, which does not fit in a long:
// Spark throws under ANSI and wraps (low-order 64 bits) under legacy.
// 1e19 truncated to the low 64 bits, matching `BigDecimal.longValue`. Note
// this value is *negative* (-8446744073709551616): 1e19 exceeds i64::MAX, so
// reinterpreting its low 64 bits as a signed long flips the sign. Rounding
// -5e18 down to -1e19 therefore wraps to a positive value.
const WRAPPED_1E19: i64 = 10_000_000_000_000_000_000u64 as i64;
const WRAPPED_MINUS_1E19: i64 = -WRAPPED_1E19;
fn assert_round_int64_ansi_overflows(value: ColumnarValue) {
let args = vec![value, ColumnarValue::Scalar(ScalarValue::Int64(Some(-19)))];
let err = spark_round(&args, &DataType::Int64, true).unwrap_err();
assert!(
err.to_string().to_ascii_lowercase().contains("overflow"),
"expected arithmetic overflow error, got: {err}"
);
}
#[test]
fn test_round_int64_negative_scale_overflow_ansi() {
// ±5e18 rounds away from zero to ±1e19, which overflows i64.
for value in [5_000_000_000_000_000_000i64, -5_000_000_000_000_000_000i64] {
assert_round_int64_ansi_overflows(ColumnarValue::Array(Arc::new(Int64Array::from(
vec![value],
))));
}
}
#[test]
fn test_round_int64_negative_scale_overflow_ansi_scalar() {
for value in [5_000_000_000_000_000_000i64, -5_000_000_000_000_000_000i64] {
assert_round_int64_ansi_overflows(ColumnarValue::Scalar(ScalarValue::Int64(Some(
value,
))));
}
}
#[test]
fn test_round_int64_negative_scale_overflow_legacy() -> Result<()> {
// Under legacy mode, ±1e19 wraps to its low-order 64 bits.
let args = vec![
ColumnarValue::Array(Arc::new(Int64Array::from(vec![
5_000_000_000_000_000_000i64,
-5_000_000_000_000_000_000i64,
4_999_999_999_999_999_999i64,
0i64,
i64::MAX,
i64::MIN,
]))),
ColumnarValue::Scalar(ScalarValue::Int64(Some(-19))),
];
let ColumnarValue::Array(result) = spark_round(&args, &DataType::Int64, false)? else {
unreachable!()
};
let longs = as_int64_array(&result)?;
let expected = Int64Array::from(vec![
WRAPPED_1E19,
WRAPPED_MINUS_1E19,
0i64,
0i64,
WRAPPED_1E19,
WRAPPED_MINUS_1E19,
]);
assert_eq!(longs, &expected);
Ok(())
}
#[test]
fn test_round_int64_negative_scale_below_threshold() -> Result<()> {
// scale=-20: threshold is 5e19, which exceeds i64::MAX, so every long
// rounds to 0 under both ANSI and legacy.
let arr = Int64Array::from(vec![i64::MAX, i64::MIN, 0, 1_000_000_000_000_000_000]);
for fail_on_error in [false, true] {
let args = vec![
ColumnarValue::Array(Arc::new(arr.clone())),
ColumnarValue::Scalar(ScalarValue::Int64(Some(-20))),
];
let ColumnarValue::Array(result) = spark_round(&args, &DataType::Int64, fail_on_error)?
else {
unreachable!()
};
let longs = as_int64_array(&result)?;
assert_eq!(longs, &Int64Array::from(vec![0i64; arr.len()]));
}
Ok(())
}
#[test]
fn test_round_int64_scale_below_i128_range() -> Result<()> {
// scale=-40: 10^40 does not fit in i128 either, so `checked_pow` returns
// None and the fallback returns 0 for every long, under both modes.
let arr = Int64Array::from(vec![i64::MAX, i64::MIN, 0, 5_000_000_000_000_000_000]);
for fail_on_error in [false, true] {
let args = vec![
ColumnarValue::Array(Arc::new(arr.clone())),
ColumnarValue::Scalar(ScalarValue::Int64(Some(-40))),
];
let ColumnarValue::Array(result) = spark_round(&args, &DataType::Int64, fail_on_error)?
else {
unreachable!()
};
let longs = as_int64_array(&result)?;
assert_eq!(longs, &Int64Array::from(vec![0i64; arr.len()]));
let scalar_args = vec![
ColumnarValue::Scalar(ScalarValue::Int64(Some(5_000_000_000_000_000_000i64))),
ColumnarValue::Scalar(ScalarValue::Int64(Some(-40))),
];
let ColumnarValue::Scalar(ScalarValue::Int64(result)) =
spark_round(&scalar_args, &DataType::Int64, fail_on_error)?
else {
unreachable!()
};
assert_eq!(result, Some(0));
}
Ok(())
}
#[test]
fn test_round_int64_negative_scale_null_scalar() -> Result<()> {
// A null long stays null in every scale band: 10^(-scale) fits in i64
// (-9), only in i128 (-19), or in neither (-40).
for point in [-9i64, -19, -40] {
for fail_on_error in [false, true] {
let args = vec![
ColumnarValue::Scalar(ScalarValue::Int64(None)),
ColumnarValue::Scalar(ScalarValue::Int64(Some(point))),
];
let ColumnarValue::Scalar(ScalarValue::Int64(result)) =
spark_round(&args, &DataType::Int64, fail_on_error)?
else {
unreachable!()
};
assert_eq!(result, None, "scale={point}, ansi={fail_on_error}");
}
}
Ok(())
}
#[test]
fn test_round_int64_negative_scale_legacy_scalar() -> Result<()> {
let args = vec![
ColumnarValue::Scalar(ScalarValue::Int64(Some(5_000_000_000_000_000_000i64))),
ColumnarValue::Scalar(ScalarValue::Int64(Some(-19))),
];
let ColumnarValue::Scalar(ScalarValue::Int64(Some(result))) =
spark_round(&args, &DataType::Int64, false)?
else {
unreachable!()
};
assert_eq!(result, WRAPPED_1E19);
Ok(())
}
}