comphead commented on code in PR #6953:
URL: https://github.com/apache/datafusion-comet/pull/6953#discussion_r4274983783


##########
native/spark-expr/src/conversion_funcs/numeric.rs:
##########
@@ -617,46 +565,27 @@ macro_rules! cast_decimal_to_int32_up {
             .downcast_ref::<Decimal128Array>()
             .expect("Expected a Decimal128ArrayType");
 
-        let output_array = match $eval_mode {
-            EvalMode::Ansi => cast_array
-                .iter()
-                .map(|value| match value {
-                    Some(value) => {
-                        let divisor = 10_i128.pow($scale as u32);
-                        let truncated = value / divisor;
-                        let is_overflow = truncated.abs() > 
$max_dest_val.into();
-                        if is_overflow {
-                            return Err(cast_overflow(
-                                &format!(
-                                    "{}BD",
-                                    format_decimal_str(
-                                        &value.to_string(),
-                                        $precision as usize,
-                                        $scale
-                                    )
-                                ),
-                                &format!("DECIMAL({},{})", $precision, $scale),
-                                $dest_type_str,
-                            ));
-                        }
-                        Ok(Some(truncated as $rust_dest_type))
-                    }
-                    None => Ok(None),
-                })
-                .collect::<Result<$dest_array_type, _>>()?,
-            _ => cast_array
-                .iter()
-                .map(|value| match value {
-                    Some(value) => {
-                        let divisor = 10_i128.pow($scale as u32);
-                        let truncated = value / divisor;
-                        Ok::<Option<$rust_dest_type>, SparkError>(Some(
-                            truncated as $rust_dest_type,
-                        ))
-                    }
-                    None => Ok(None),
-                })
-                .collect::<Result<$dest_array_type, _>>()?,
+        let divisor = 10_i128.pow($scale as u32);
+        // The legacy conversion cannot fail, so `unary` maps the values 
buffer in one pass and
+        // keeps the null buffer. `try_unary` only visits valid slots, so an 
out-of-range value
+        // under a null cannot raise a spurious overflow error.
+        let output_array: $dest_array_type = match $eval_mode {
+            EvalMode::Ansi => cast_array.try_unary(|value| {
+                let truncated = value / divisor;
+                let is_overflow = truncated.abs() > $max_dest_val.into();
+                if is_overflow {
+                    return Err(cast_overflow(
+                        &format!(
+                            "{}BD",
+                            format_decimal_str(&value.to_string(), $precision 
as usize, $scale)
+                        ),
+                        &format!("DECIMAL({},{})", $precision, $scale),
+                        $dest_type_str,
+                    ));
+                }
+                Ok(truncated as $rust_dest_type)
+            })?,
+            _ => cast_array.unary(|value| (value / divisor) as 
$rust_dest_type),

Review Comment:
   Thanks, this is fixed in 0ab5db9138. I followed andygrove's suggestion below 
instead of a null-only fast path. A negative scale already panics on main 
(debug build) as soon as a batch has a non-null value, so the fast path would 
only have covered the all-null half.
   
   `CometCast.canCastFromDecimal` now returns `Unsupported` for a 
negative-scale decimal with a `ByteType`, `ShortType`, `IntegerType` or 
`LongType` target in every eval mode. Spark's own cast runs through the codegen 
dispatcher, so the hoisted divisor is never computed for such a column. The 
cast docs have a short section on it.
   
   The new test in `CometNativeCastSuite` casts a negative-scale column twice, 
once with values and once all null, to the four integral targets. It also 
asserts the `Unsupported` result for the three eval modes. One thing I ran into 
while writing it: Spark's `ConvertToLocalRelation` folds 
`Project(LocalRelation)` on the driver, so by default the query never reaches 
the Comet cast and the answer check cannot fail. The test excludes that rule. 
With the rule excluded and the guard removed, it fails with the native panic 
`attempt to multiply with overflow`.
   
   Locally I ran clippy, the 116 `conversion_funcs` unit tests, and this test 
together with the existing negative-scale cast test. I only reproduced the 
debug build panic. I did not run a release build, so the divide by zero in your 
report is not re-checked here. The guard keeps the cast out of the native 
kernels, so it should not depend on the build profile.
   



##########
native/spark-expr/src/conversion_funcs/numeric.rs:
##########
@@ -540,62 +513,37 @@ macro_rules! cast_decimal_to_int16_down {
             .downcast_ref::<Decimal128Array>()
             .expect("Expected a Decimal128ArrayType");
 
-        let output_array = match $eval_mode {
-            EvalMode::Ansi => cast_array
-                .iter()
-                .map(|value| match value {
-                    Some(value) => {
-                        let divisor = 10_i128.pow($scale as u32);
-                        let truncated = value / divisor;
-                        let is_overflow = truncated.abs() > i32::MAX.into();
-                        if is_overflow {
-                            return Err(cast_overflow(
-                                &format!(
-                                    "{}BD",
-                                    format_decimal_str(
-                                        &value.to_string(),
-                                        $precision as usize,
-                                        $scale
-                                    )
-                                ),
-                                &format!("DECIMAL({},{})", $precision, $scale),
-                                $dest_type_str,
-                            ));
-                        }
-                        let i32_value = truncated as i32;
-                        <$rust_dest_type>::try_from(i32_value)
-                            .map_err(|_| {
-                                cast_overflow(
-                                    &format!(
-                                        "{}BD",
-                                        format_decimal_str(
-                                            &value.to_string(),
-                                            $precision as usize,
-                                            $scale
-                                        )
-                                    ),
-                                    &format!("DECIMAL({},{})", $precision, 
$scale),
-                                    $dest_type_str,
-                                )
-                            })
-                            .map(Some)
-                    }
-                    None => Ok(None),
-                })
-                .collect::<Result<$dest_array_type, _>>()?,
-            _ => cast_array
-                .iter()
-                .map(|value| match value {
-                    Some(value) => {
-                        let divisor = 10_i128.pow($scale as u32);
-                        let i32_value = (value / divisor) as i32;
-                        Ok::<Option<$rust_dest_type>, SparkError>(Some(
-                            i32_value as $rust_dest_type,
-                        ))
-                    }
-                    None => Ok(None),
+        let divisor = 10_i128.pow($scale as u32);
+        // The legacy conversion cannot fail, so `unary` maps the values 
buffer in one pass and
+        // keeps the null buffer. `try_unary` only visits valid slots, so an 
out-of-range value
+        // under a null cannot raise a spurious overflow error.
+        let output_array: $dest_array_type = match $eval_mode {
+            EvalMode::Ansi => cast_array.try_unary(|value| {
+                let truncated = value / divisor;
+                let is_overflow = truncated.abs() > i32::MAX.into();
+                if is_overflow {
+                    return Err(cast_overflow(
+                        &format!(
+                            "{}BD",
+                            format_decimal_str(&value.to_string(), $precision 
as usize, $scale)
+                        ),
+                        &format!("DECIMAL({},{})", $precision, $scale),
+                        $dest_type_str,
+                    ));
+                }
+                let i32_value = truncated as i32;
+                <$rust_dest_type>::try_from(i32_value).map_err(|_| {
+                    cast_overflow(
+                        &format!(
+                            "{}BD",
+                            format_decimal_str(&value.to_string(), $precision 
as usize, $scale)
+                        ),
+                        &format!("DECIMAL({},{})", $precision, $scale),
+                        $dest_type_str,
+                    )
                 })
-                .collect::<Result<$dest_array_type, _>>()?,
+            })?,
+            _ => cast_array.unary(|value| (value / divisor) as i32 as 
$rust_dest_type),

Review Comment:
   Done in 0ab5db9138. The two legacy decimal arms now use `try_unary` with a 
closure that always returns `Ok`, and a comment says why. The float arms stay 
on `unary`.
   
   My numbers for `decimal(18,2)` to `i32` at 8192 rows (criterion, M3 Max, 
bench profile): with no nulls it is 47.5 µs on main, 18.6 µs with `unary` and 
16.2 µs with `try_unary`. At 10% nulls it is 58.5 µs on main and 18.4 µs with 
either, so I see no difference there. I did not measure 50% or all-null 
batches, so I cannot confirm your 10.1 µs and 0.4 µs figures.
   



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