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


##########
native/spark-expr/src/math_funcs/internal/checkoverflow.rs:
##########
@@ -119,8 +119,24 @@ impl PhysicalExpr for CheckOverflow {
 
                 let decimal_array = 
as_primitive_array::<Decimal128Type>(&array);
 
-                let casted_array = if self.fail_on_error {
-                    // Returning error if overflow - convert decimal overflow 
to SparkError
+                // Fast path shared by both ANSI and non-ANSI: 
`is_valid_decimal_precision` is a
+                // small, inlined bounds check and `all` short-circuits at the 
first overflow. When
+                // nothing overflows (the common shape for decimal arithmetic 
in TPC-DS) we reuse the
+                // input buffers via `to_data()`, which only clones cheap Arc 
metadata. This avoids
+                // the heavier per-value `validate_decimal_precision` scan 
(ANSI) or the allocating
+                // `null_if_overflow_precision` (non-ANSI) below.
+                let no_overflow = decimal_array

Review Comment:
   On an ANSI batch that contains an overflow, the code now runs:
   
   1. the fast-path `all(is_valid_decimal_precision)` scan (`:128-131`), which 
returns `false`,
   2. `validate_decimal_precision(*precision)` (`:140-141`), a full second scan 
that finds the overflow and produces the error, and
   3. inside the `map_err`, a third scan `decimal_array.iter().find(...)` 
(`:146-158`) to locate the first offending value for the Spark error.
   
   Scans 2 and 3 are redundant. The fast-path scan at `:128` already knows an 
overflow exists, and `is_valid_decimal_precision` already tells you which value 
is the first offender. You can drop `validate_decimal_precision` entirely and 
build the error from a single `find`:
   
   ```rust
   } else if self.fail_on_error {
       // ANSI: fast path already proved an overflow exists. Find the first 
offending
       // value and raise the precise Spark error. Only runs on the aborting 
error path.
       let overflow_value = decimal_array
           .iter()
           .flatten()
           .find(|v| !Decimal128Type::is_valid_decimal_precision(*v, 
*precision))
           .unwrap_or(0);
       let spark_error =
           crate::error::decimal_overflow_error(overflow_value, *precision, 
*scale);
       return Err(match &self.query_context {
           Some(ctx) => DataFusionError::External(Box::new(
               crate::SparkErrorWithContext::with_context(spark_error, 
Arc::clone(ctx)),
           )),
           None => DataFusionError::External(Box::new(spark_error)),
       });
   }
   ```
   
   This removes the whole `validate_decimal_precision` call, the 
string-matching on `"too large to store in a Decimal128"` (which is brittle 
against arrow-rs wording changes, and also fails to catch the `"too small"` 
underflow branch at `arrow-data/src/decimal.rs:1160`), and the unreachable 
`Internal` error at `:177-179`. The PR says the error path "aborts the query 
anyway" so cost does not matter, but the simpler version is also more correct: 
the current `find` at `:146-158` calls the three-arg 
`Decimal128Type::validate_decimal_precision(val, precision, scale)` and matches 
only the "too large" string, so a value that overflows on the negative side 
reaches the `.unwrap_or(0)` fallback and reports value `0` in the error. Using 
`is_valid_decimal_precision` in the `find` fixes that.



##########
native/spark-expr/src/math_funcs/internal/checkoverflow.rs:
##########
@@ -381,4 +402,79 @@ mod tests {
             other => panic!("unexpected: {other:?}"),
         }
     }
+
+    // --- array path ---
+
+    fn array_batch(values: Vec<Option<i128>>, in_precision: u8, scale: i8) -> 
RecordBatch {

Review Comment:
   Every new array test uses a positive overflow value (`1000` against 
precision 3). The `MIN_DECIMAL128_FOR_EACH_PRECISION` lower bound is never 
exercised. This matters because the existing ANSI error-formatting path only 
string-matches `"too large"` (see finding 1), so a negative overflow is a real 
untested branch. Add a legacy case that nulls a large-negative value and an 
ANSI case that errors on one:
   
   ```rust
   #[test]
   fn test_array_negative_overflow_nulled_legacy() {
       let batch = array_batch(vec![Some(-1000), Some(5)], 38, 0);
       let out = eval_array(&array_check_overflow(3, 0, false), &batch);
       assert_eq!(out.iter().collect::<Vec<_>>(), vec![None, Some(5)]);
   }
   ```



##########
native/spark-expr/src/math_funcs/internal/checkoverflow.rs:
##########
@@ -381,4 +402,79 @@ mod tests {
             other => panic!("unexpected: {other:?}"),
         }
     }
+
+    // --- array path ---
+
+    fn array_batch(values: Vec<Option<i128>>, in_precision: u8, scale: i8) -> 
RecordBatch {
+        let arr = values
+            .into_iter()
+            .collect::<Decimal128Array>()
+            .with_precision_and_scale(in_precision, scale)
+            .unwrap();
+        let schema = Schema::new(vec![Field::new("d", arr.data_type().clone(), 
true)]);
+        RecordBatch::try_new(Arc::new(schema), vec![Arc::new(arr)]).unwrap()
+    }
+
+    fn array_check_overflow(target_precision: u8, scale: i8, fail_on_error: 
bool) -> CheckOverflow {

Review Comment:
   The review brief asks for all-overflow, all-null, and boundary-precision 
coverage. Current tests cover no-overflow, single-overflow, and mixed-null, but 
not:
   
   - an all-null batch (the fast path takes `flatten().all(...)` which returns 
`true` on an empty iterator, so all-null must reuse the input and preserve the 
null mask - worth pinning),
   - an all-overflow batch in legacy mode (every slot nulled) and ANSI mode 
(errors),
   - a boundary value that exactly equals `MAX_FOR_EACH_PRECISION[precision]` 
(for example `Some(999)` at precision 3 is already the max and passes; add 
`Some(9999)` at precision 3 to confirm it is treated as overflow, since 
off-by-one on the bound is the classic failure).
   
   These are cheap to add and directly guard the fast-path condition.



##########
native/spark-expr/src/math_funcs/internal/checkoverflow.rs:
##########
@@ -119,8 +119,24 @@ impl PhysicalExpr for CheckOverflow {
 
                 let decimal_array = 
as_primitive_array::<Decimal128Type>(&array);
 
-                let casted_array = if self.fail_on_error {
-                    // Returning error if overflow - convert decimal overflow 
to SparkError
+                // Fast path shared by both ANSI and non-ANSI: 
`is_valid_decimal_precision` is a
+                // small, inlined bounds check and `all` short-circuits at the 
first overflow. When
+                // nothing overflows (the common shape for decimal arithmetic 
in TPC-DS) we reuse the
+                // input buffers via `to_data()`, which only clones cheap Arc 
metadata. This avoids
+                // the heavier per-value `validate_decimal_precision` scan 
(ANSI) or the allocating
+                // `null_if_overflow_precision` (non-ANSI) below.
+                let no_overflow = decimal_array
+                    .iter()
+                    .flatten()
+                    .all(|v| Decimal128Type::is_valid_decimal_precision(v, 
*precision));

Review Comment:
   #4937 detects overflow with `all(is_valid_decimal_precision)` over the raw 
input values. #4938 detects it with `result.values().contains(&i128::MAX)` over 
a sentinel that `try_unary` already wrote. The two do not and cannot share a 
helper, because in `DecimalRescaleCheckOverflow` the rescale pass has already 
folded the overflow decision into the sentinel, so re-deriving it from the 
original values would mean redoing the rescale. That is a defensible split, not 
a flaw. What is missing is the shared decision both PRs encode: "run the 
null-masking / error pass only when an overflow is actually present." If you 
want reuse, extract the non-ANSI tail both files share:
   
   ```rust
   // returns the input array unchanged when nothing overflows, else the 
null-masked array
   fn null_if_any_overflow(arr: &Decimal128Array, precision: u8, any_overflow: 
bool) -> Decimal128Array {
       if any_overflow { arr.null_if_overflow_precision(precision) } else { 
arr.clone() }
   }
   ```
   
   This is optional given the different detection inputs, but I would rather 
see one named concept than two ad hoc `if` shapes drifting apart over time. At 
minimum, land both PRs together so a reviewer can see the pattern is 
intentional.



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