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


##########
native/spark-expr/src/math_funcs/modulo_expr.rs:
##########
@@ -441,4 +520,126 @@ mod tests {
             verify_result(modulo_expr, batch, fail_on_error, 
Some(expected_result));
         })
     }
+
+    fn run_float_modulo<T: ArrowPrimitiveType>(

Review Comment:
   All six new float tests go through `run_float_modulo`, which builds both 
operands as `Column` expressions, so `spark_modulo` always sees 
`(ColumnarValue::Array, ColumnarValue::Array)`. The `Scalar/Scalar`, 
`Array/Scalar`, and `Scalar/Array` match arms in 
`check_float_remainder_by_zero` are not exercised by any of these tests.
   
   Could you add a case that passes at least one operand as a `Literal`, the 
way `test_division_by_zero_with_complex_int_expr` does with 
`Literal::new(ScalarValue::Int32(Some(0)))`?



##########
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:
   Thanks for tracking down the root cause here. The `mod_wrapping` vs 
`mod_checked` split in arrow-arith explains exactly why the existing error 
branch was unreachable for floats.
   
   `check_float_remainder_by_zero` and its helpers (`float_array_has_zero`, 
`scan_float_array`) reimplement something arrow-rs already provides. 
`ArrowNativeTypeOp::mod_checked` for `f32`/`f64` already returns `DivideByZero` 
when the divisor `is_zero()`, and computes `self % rhs` otherwise, so `-0.0` is 
handled the same way there as it is in this hand-written check.
   
   `checked_arithmetic.rs` already takes this route for the equivalent 
float-divide-by-zero problem: it calls 
`arrow::compute::kernels::arity::try_binary` with `div_checked` instead of 
relying on `arrow::compute::kernels::numeric::div`, because that kernel has the 
same wrapping-only behavior for floats that `rem()` has here. Would 
`try_binary` with `mod_checked` work for this fix too? Since `mod_checked` for 
floats can only fail with `DivideByZero` (there is no overflow case for 
remainder), any `Err` from that call maps directly to 
`remainder_by_zero_error()`. `try_binary` also only invokes the closure on rows 
where both operands are valid, so the null-dividend skip logic in 
`float_array_has_zero` and `scan_float_array` would not be needed. This would 
also remove the extra full pass over the divisor array that the current code 
does before `rem()` runs its own pass, on every ANSI-mode float remainder call, 
including the common case with no zero divisor.
   
   Rough sketch of what this could look like in place of 
`check_float_remainder_by_zero`, `float_array_has_zero`, and `scan_float_array` 
(not compiled, just to make the suggestion concrete):
   
   ```rust
   if fail_on_error && matches!(right_data_type, DataType::Float32 | 
DataType::Float64) {
       return checked_float_modulo(lhs, rhs, &right_data_type);
   }
   
   match apply(lhs, rhs, rem) {
       Ok(result) => Ok(result),
       Err(e) if e.to_string().contains("Divide by zero") && fail_on_error => {
           Err(remainder_by_zero_error().into())
       }
       Err(e) => Err(e),
   }
   ```
   
   ```rust
   fn checked_float_modulo(
       lhs: &ColumnarValue,
       rhs: &ColumnarValue,
       data_type: &DataType,
   ) -> Result<ColumnarValue> {
       let len = match (lhs, rhs) {
           (ColumnarValue::Array(a), _) | (_, ColumnarValue::Array(a)) => 
a.len(),
           _ => 1,
       };
       let l_arr = match lhs {
           ColumnarValue::Array(a) => Arc::clone(a),
           ColumnarValue::Scalar(s) => s.to_array_of_size(len)?,
       };
       let r_arr = match rhs {
           ColumnarValue::Array(a) => Arc::clone(a),
           ColumnarValue::Scalar(s) => s.to_array_of_size(len)?,
       };
   
       let result: ArrayRef = match data_type {
           DataType::Float32 => Arc::new(
               try_binary::<_, _, _, Float32Type>(
                   l_arr.as_primitive::<Float32Type>(),
                   r_arr.as_primitive::<Float32Type>(),
                   |l, r| l.mod_checked(r),
               )
               .map_err(|_| remainder_by_zero_error())?,
           ),
           DataType::Float64 => Arc::new(
               try_binary::<_, _, _, Float64Type>(
                   l_arr.as_primitive::<Float64Type>(),
                   r_arr.as_primitive::<Float64Type>(),
                   |l, r| l.mod_checked(r),
               )
               .map_err(|_| remainder_by_zero_error())?,
           ),
           _ => unreachable!(),
       };
       Ok(ColumnarValue::Array(result))
   }
   ```
   
   Needs `use arrow::array::{ArrowNativeTypeOp, AsArray}` and `use 
arrow::compute::kernels::arity::try_binary`, both already imported in 
`checked_arithmetic.rs` for the same purpose.



##########
spark/src/test/resources/sql-tests/expressions/math/arithmetic_ansi.sql:
##########
@@ -147,6 +147,37 @@ SELECT c div d FROM ansi_div_zero
 query expect_error(BY_ZERO)
 SELECT c % d FROM ansi_div_zero
 
+-- ============================================================================
+-- Float/Double remainder by zero (issue #5067)
+-- Spark 4.1 throws REMAINDER_BY_ZERO for floating-point x % 0 as well.
+-- ============================================================================
+
+statement
+CREATE TABLE ansi_float_div_zero(a float, b float, c double, d double) USING 
parquet
+
+statement
+INSERT INTO ansi_float_div_zero VALUES (3.0, 0.0, 3.0, 0.0), (1.0, -0.0, 1.0, 
-0.0)
+
+-- float column % 0 should throw
+query expect_error(BY_ZERO)
+SELECT a % b FROM ansi_float_div_zero WHERE b = 0.0
+
+-- double column % 0 should throw
+query expect_error(BY_ZERO)
+SELECT c % d FROM ansi_float_div_zero WHERE d = 0.0
+
+-- literal double % 0.0 should throw
+query expect_error(BY_ZERO)
+SELECT CAST(1.0 AS DOUBLE) % CAST(0.0 AS DOUBLE)

Review Comment:
   This expression is fully literal, so Spark's own `ConstantFolding` rule 
evaluates and throws on it during optimization, before the query reaches 
Comet's native `spark_modulo`. The same is true for the `-0.0` and float cases 
on lines 175 and 179. This mirrors an existing pattern already in this file 
(`SELECT 1 % 0` above), so it is not a new issue, but it means these three 
cases do not exercise this PR's new code.
   
   Could you add a case like `SELECT a % 0.0 FROM ansi_float_div_zero` (column 
dividend, literal zero divisor)? That query is not foldable, since `a` is a 
column, so it would actually reach Comet's native path at execution time and 
cover the `Array/Scalar` branch that the Rust unit tests also miss.



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