andygrove commented on code in PR #5081:
URL: https://github.com/apache/datafusion-comet/pull/5081#discussion_r3684143284


##########
native/spark-expr/src/math_funcs/modulo_expr.rs:
##########
@@ -63,6 +71,77 @@ pub fn spark_modulo(args: &[ColumnarValue], fail_on_error: 
bool) -> Result<Colum
     }
 }
 
+/// Returns an error if any row has a non-null dividend paired with a zero 
divisor. Only
+/// meant to be called with floating point operands — Spark treats `-0.0` as 
zero here,
+/// which falls out naturally from IEEE 754 equality.
+fn check_float_remainder_by_zero(lhs: &ColumnarValue, rhs: &ColumnarValue) -> 
Result<()> {
+    let is_zero_float_scalar = |sv: &ScalarValue| match sv {
+        ScalarValue::Float32(Some(v)) => *v == 0.0,
+        ScalarValue::Float64(Some(v)) => *v == 0.0,
+        _ => false,
+    };
+
+    match (lhs, rhs) {
+        (ColumnarValue::Scalar(l), ColumnarValue::Scalar(r)) => {
+            if !l.is_null() && is_zero_float_scalar(r) {
+                return Err(remainder_by_zero_error().into());
+            }
+        }
+        (ColumnarValue::Array(l_arr), ColumnarValue::Scalar(r)) => {
+            if is_zero_float_scalar(r) && l_arr.null_count() != l_arr.len() {
+                return Err(remainder_by_zero_error().into());
+            }
+        }
+        (ColumnarValue::Scalar(l), ColumnarValue::Array(r_arr)) => {
+            if !l.is_null() && float_array_has_zero(r_arr, None) {
+                return Err(remainder_by_zero_error().into());
+            }
+        }
+        (ColumnarValue::Array(l_arr), ColumnarValue::Array(r_arr)) => {
+            if float_array_has_zero(r_arr, Some(l_arr.as_ref())) {
+                return Err(remainder_by_zero_error().into());
+            }
+        }
+    }
+    Ok(())
+}
+
+/// Returns true if `divisor` contains any non-null zero. When `dividend_mask` 
is provided,
+/// positions where `dividend_mask` is null are skipped (they will produce 
null results and
+/// must not raise an error).
+fn float_array_has_zero(divisor: &ArrayRef, dividend_mask: Option<&dyn Array>) 
-> bool {
+    match divisor.data_type() {
+        DataType::Float32 => scan_float_array::<Float32Type>(divisor, 
dividend_mask),
+        DataType::Float64 => scan_float_array::<Float64Type>(divisor, 
dividend_mask),
+        _ => false,
+    }
+}
+
+fn scan_float_array<T: ArrowPrimitiveType>(
+    divisor: &ArrayRef,
+    dividend_mask: Option<&dyn Array>,
+) -> bool
+where
+    T::Native: Default + PartialEq,
+{
+    let arr = divisor.as_primitive::<T>();
+    let zero = T::Native::default();
+    for i in 0..arr.len() {
+        if arr.is_null(i) {
+            continue;
+        }
+        if let Some(mask) = dividend_mask {
+            if mask.is_null(i) {
+                continue;
+            }
+        }
+        if arr.value(i) == zero {
+            return true;
+        }
+    }
+    false
+}

Review Comment:
   Good call — this is much better. Done in 6c815fea.
   
   I confirmed both halves of the suggestion against the pinned arrow version 
before switching:
   
   - arrow-array 59.1.0 `src/arithmetic.rs:353` — float `mod_checked` is `if 
rhs.is_zero() { Err(DivideByZero) } else { Ok(self % rhs) }`, and `is_zero` for 
floats is `self == $zero`, so `-0.0` errors just like the hand-written check 
did.
   - arrow-arith 59.1.0 `src/arity.rs:283` — when either input has nulls, 
`try_binary` runs the closure under `nulls.try_for_each_valid_idx`, so it only 
touches rows valid in both operands. That makes the null-dividend skip logic 
unnecessary, as you said.
   
   So `check_float_remainder_by_zero`, `float_array_has_zero`, and 
`scan_float_array` are all gone (~70 lines), along with the extra pass over the 
divisor array that ran ahead of `rem` on every ANSI-mode float remainder.
   
   Two deviations from your sketch:
   
   1. The sketch always returns `ColumnarValue::Array`. DataFusion's `apply` 
returns a `Scalar` for scalar/scalar operands (it calls 
`ScalarValue::try_from_array(array, 0)` in that arm), and returning a length-1 
array when the batch has N rows would be wrong, so I convert back to a scalar 
in that case. `test_modulo_both_literals_non_zero_divisor_float64_ansi` pins 
that down.
   
   2. I added a `left_data_type == right_data_type` guard on the branch. Spark 
casts `Remainder` operands to a common type so this should always hold, but 
without it a mismatched pair would panic in `as_primitive::<Float64Type>()`; 
with it, the pair falls through to `rem`, which returns an error rather than 
silently producing NaN.
   
   I also matched specifically on `ArrowError::DivideByZero` rather than 
mapping every error to `remainder_by_zero_error()`, mirroring `checked_binary` 
in `checked_arithmetic.rs`, so a length mismatch can't surface as 
`REMAINDER_BY_ZERO`.



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