mbutrovich commented on code in PR #5082:
URL: https://github.com/apache/datafusion-comet/pull/5082#discussion_r3691532127


##########
native/spark-expr/src/math_funcs/round.rs:
##########


Review Comment:
   round.rs:89, 94, 111, 126
   
   `(-(*$POINT)) as u32` is written out twice in each macro: once for the 
native `checked_pow` and again for the `i128` `checked_pow`. Fix: compute it 
once, e.g. `let point_abs = (-(*$POINT)) as u32;` right after `let ten: $NATIVE 
= 10;`, and reuse it in both `checked_pow` calls in `round_integer_array!` and 
`round_integer_scalar!`.



##########
native/spark-expr/src/math_funcs/round.rs:
##########
@@ -93,6 +123,21 @@ macro_rules! round_integer_scalar {
                 None => None,
             };

Review Comment:
   round.rs:113-124, 128-139
   
   The native-fits and widened branches of `round_integer_scalar!` each repeat 
the identical `match ... { Ok(v) => Some(v), Err(e) => return 
Err(DataFusionError::ArrowError(Box::from(e), 
Some(DataFusionError::get_back_trace()))) }` boilerplate, differing only in 
which inner macro is invoked. Fix: factor this into a small helper macro, e.g. 
`unwrap_round_result!($RESULT)` expanding to that same match, and call 
`Some(unwrap_round_result!(integer_round!(...)))` / 
`Some(unwrap_round_result!(integer_round_widened!(...)))` in each branch.



##########
native/spark-expr/src/math_funcs/round.rs:
##########
@@ -59,6 +59,29 @@ macro_rules! integer_round {
     }};
 }
 
+// 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| < div` 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;
+        if x128 > -$HALF && x128 < $HALF {
+            Ok(0 as $NATIVE)
+        } else if $FAIL_ON_ERROR {
+            Err(ArrowError::ComputeError(
+                arithmetic_overflow_error("integer").to_string(),
+            ))
+        } else if x128 >= $HALF {
+            Ok($DIV as $NATIVE)
+        } else {
+            Ok((-$DIV) as $NATIVE)
+        }
+    }};
+}

Review Comment:
   The final two arms of `integer_round_widened!` (`x128 >= $HALF -> Ok($DIV as 
$NATIVE)`, else `Ok((-$DIV) as $NATIVE)`) differ only by sign, and both apply 
only once `$FAIL_ON_ERROR` has already been ruled out above. Fix: collapse to 
one arm: `Ok((if x128 >= $HALF { $DIV } else { -$DIV }) as $NATIVE)`. Same 
behavior, one fewer branch.



##########
native/spark-expr/src/math_funcs/round.rs:
##########
@@ -280,4 +325,102 @@ mod test {
         assert_eq!(result, 125.23);
         Ok(())
     }
+
+    // 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`.
+    const WRAPPED_POS_1E19: i64 = 10_000_000_000_000_000_000u64 as i64;
+    const WRAPPED_NEG_1E19: i64 = -WRAPPED_POS_1E19;

Review Comment:
   `const WRAPPED_POS_1E19: i64 = 10_000_000_000_000_000_000u64 as i64;` is 
named as if it holds a positive value, but the truncating cast actually 
produces a negative number: `1e19 mod 2^64` is less than `2^64` but greater 
than `i64::MAX`, so reinterpreting those bits as `i64` yields 
`-8446744073709551616`. The name implies the wrapped result of rounding `+5e18` 
up to `+1e19` stays positive, which is false and will mislead anyone reading 
the test without independently working out the two's-complement truncation. 
Fix: rename to something that does not assert a sign, e.g. `WRAPPED_1E19`, and 
add a comment stating the wrapped value is negative (`-8446744073709551616`) 
because `1e19` exceeds `i64::MAX`.



##########
native/spark-expr/src/math_funcs/round.rs:
##########
@@ -280,4 +325,102 @@ mod test {
         assert_eq!(result, 125.23);
         Ok(())
     }
+
+    // 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`.
+    const WRAPPED_POS_1E19: i64 = 10_000_000_000_000_000_000u64 as i64;
+    const WRAPPED_NEG_1E19: i64 = -WRAPPED_POS_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 up to 1e19, which overflows i64.
+        
assert_round_int64_ansi_overflows(ColumnarValue::Array(Arc::new(Int64Array::from(vec![
+            5_000_000_000_000_000_000i64,
+        ]))));
+    }
+
+    #[test]
+    fn test_round_int64_negative_scale_overflow_ansi_scalar() {
+        
assert_round_int64_ansi_overflows(ColumnarValue::Scalar(ScalarValue::Int64(Some(
+            5_000_000_000_000_000_000i64,
+        ))));
+    }

Review Comment:
   Both new ANSI tests (`test_round_int64_negative_scale_overflow_ansi`, 
`test_round_int64_negative_scale_overflow_ansi_scalar`) only exercise the 
positive-overflow case (`+5e18`). There is no ANSI-mode test for the negative 
side (`-5e18`), even though the legacy test at lines 362-390 covers both signs. 
Fix: extend `assert_round_int64_ansi_overflows` calls (or add a third test) to 
also assert ANSI overflow for `-5_000_000_000_000_000_000i64`.



##########
native/spark-expr/src/math_funcs/round.rs:
##########
@@ -68,7 +91,14 @@ macro_rules! round_integer_array {
             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)) as u32) {
+            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))?

Review Comment:
   The i128-overflow fallback (`scale <= -38` for `Int64`) is untouched by this 
PR but is now reached through a three-way branch instead of two, and the PR's 
own reasoning documents the boundary between "fits native" and "fits i128" 
carefully while leaving this third boundary undocumented by a test. The 
existing `test_round_int64_negative_scale_below_threshold` test (scale=-20) 
exercises the "fits i128 but rounds to 0" case, not the "doesn't even fit i128" 
case. Fix: add a test at `scale = -40` (or any scale where 
`10_i128.checked_pow` returns `None`) asserting the result is `0` under both 
modes, to pin down this boundary now that it sits behind a new middle branch.



##########
native/spark-expr/src/math_funcs/round.rs:
##########
@@ -59,6 +59,29 @@ macro_rules! integer_round {
     }};
 }
 
+// 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| < div` 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;
+        if x128 > -$HALF && x128 < $HALF {
+            Ok(0 as $NATIVE)
+        } else if $FAIL_ON_ERROR {
+            Err(ArrowError::ComputeError(
+                arithmetic_overflow_error("integer").to_string(),
+            ))
+        } else if x128 >= $HALF {
+            Ok($DIV as $NATIVE)
+        } else {
+            Ok((-$DIV) as $NATIVE)
+        }
+    }};
+}

Review Comment:
   `integer_round_widened!` is a second, separate rounding macro alongside 
`integer_round!`, correct only because of an unstated invariant: `div` 
overflowed the native type, so `|x| <= NATIVE::MAX < div`, hence `x % div == 
x`. That's a narrow algebraic shortcut for this specific overflow band, not the 
same mechanism `decimal_round_f` uses one function below it (lines 232-247), 
which always computes in `i128` regardless of how large `div` is and needs no 
special-cased middle branch. Fix: generalize `integer_round!`'s rem/half 
formula to operate in `i128` uniformly, with a single checked-or-wrapping 
downcast to `$NATIVE` at the end, replacing the current three-way dispatch 
(native-fits / i128-widened / i128-overflows) with one code path. This removes 
`integer_round_widened!` entirely and matches the pattern already established 
in this same file for decimals.



##########
native/spark-expr/src/math_funcs/round.rs:
##########
@@ -93,6 +123,21 @@ macro_rules! round_integer_scalar {
                 None => None,
             };
             Ok(ColumnarValue::Scalar($TYPE(scalar_opt)))
+        } else if let Some(div) = 10_i128.checked_pow((-(*$POINT)) as u32) {
+            let half = div / 2;
+            let scalar_opt = match $SCALAR {
+                Some(x) => match integer_round_widened!(*x, div, half, 
$NATIVE, $FAIL_ON_ERROR) {
+                    Ok(v) => Some(v),
+                    Err(e) => {
+                        return Err(DataFusionError::ArrowError(
+                            Box::from(e),
+                            Some(DataFusionError::get_back_trace()),
+                        ))
+                    }
+                },
+                None => None,
+            };
+            Ok(ColumnarValue::Scalar($TYPE(scalar_opt)))
         } else {
             Ok(ColumnarValue::Scalar($TYPE(Some(0))))
         }

Review Comment:
   This is the pre-existing catch-all branch of `round_integer_scalar!` for 
when even `i128` cannot hold `10^(-scale)` (roughly `scale <= -39`). It returns 
`Ok(ColumnarValue::Scalar($TYPE(Some(0))))` unconditionally, without checking 
whether `$SCALAR` was `None`. A null `Int64` scalar input at `scale <= -39` 
comes out as `Some(0)` instead of `None`. This predates the PR, but the PR 
inserts a new branch directly above it (lines 126-140) that gets this exact 
case right (`match $SCALAR { Some(x) => ..., None => None }`), so the 
inconsistency is now more visible, and it's a one-line fix while this exact 
function is already being reworked. Fix: match on `$SCALAR` here too, e.g. `let 
scalar_opt = match $SCALAR { Some(_) => Some(0), None => None }; 
Ok(ColumnarValue::Scalar($TYPE(scalar_opt)))`.



##########
native/spark-expr/src/math_funcs/round.rs:
##########
@@ -280,4 +325,102 @@ mod test {
         assert_eq!(result, 125.23);
         Ok(())
     }
+
+    // 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`.
+    const WRAPPED_POS_1E19: i64 = 10_000_000_000_000_000_000u64 as i64;
+    const WRAPPED_NEG_1E19: i64 = -WRAPPED_POS_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 up to 1e19, which overflows i64.
+        
assert_round_int64_ansi_overflows(ColumnarValue::Array(Arc::new(Int64Array::from(vec![
+            5_000_000_000_000_000_000i64,
+        ]))));
+    }
+
+    #[test]
+    fn test_round_int64_negative_scale_overflow_ansi_scalar() {
+        
assert_round_int64_ansi_overflows(ColumnarValue::Scalar(ScalarValue::Int64(Some(
+            5_000_000_000_000_000_000i64,
+        ))));
+    }
+
+    #[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_POS_1E19,
+            WRAPPED_NEG_1E19,
+            0i64,
+            0i64,
+            WRAPPED_POS_1E19,
+            WRAPPED_NEG_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_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_POS_1E19);
+        Ok(())
+    }

Review Comment:
   None of the six new tests pass a null `Int64` scalar through the widened 
path (`scale <= -19`) or the i128-overflow fallback (`scale <= -38`). A test 
with `ColumnarValue::Scalar(ScalarValue::Int64(None))` at `scale = -19` would 
confirm the widened-scalar branch preserves null (it does, by inspection of 
lines 126-140), and the same test at `scale = -40` would catch the 
null-handling bug at lines 141-143. Fix: add 
`test_round_int64_negative_scale_null_scalar` covering both scale bands.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to