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


##########
native/spark-expr/src/conversion_funcs/boolean.rs:
##########
@@ -32,28 +32,32 @@ pub fn cast_boolean_to_decimal(
     array: &ArrayRef,
     precision: u8,
     scale: i8,
+    eval_mode: EvalMode,
 ) -> SparkResult<ArrayRef> {
     let bool_array = array.as_boolean();
     let scaled_val = 10_i128.pow(scale as u32);
+
+    // Spark's Cast uses `nullOnOverflow = !ansiEnabled`: legacy/try return 
NULL
+    // on overflow, only ANSI raises. `false` maps to 0 which always fits, so
+    // overflow only happens for `true` when 10^scale exceeds `precision`.
+    let overflows = !is_validate_decimal_precision(scaled_val, precision);
+    if overflows && eval_mode == EvalMode::Ansi {
+        return Err(crate::error::decimal_overflow_error(
+            scaled_val, precision, scale,
+        ));
+    }
+
     let result: Decimal128Array = bool_array
         .iter()
-        .map(|v| v.map(|b| if b { scaled_val } else { 0 }))
+        .map(|v| match v {
+            Some(false) => Some(0),
+            Some(true) if !overflows => Some(scaled_val),
+            _ => None,
+        })
         .collect();
-
-    // Convert Arrow decimal overflow errors to SparkError
     let decimal_array = result
         .with_precision_and_scale(precision, scale)
-        .map_err(|e| {
-            if matches!(e, arrow::error::ArrowError::InvalidArgumentError(_))
-                && e.to_string().contains("too large to store in a Decimal128")
-            {
-                // Use the scaled value as it's the only non-zero value that 
could overflow
-                crate::error::decimal_overflow_error(scaled_val, precision, 
scale)
-            } else {
-                SparkError::Arrow(Arc::new(e))
-            }
-        })?;
-
+        .map_err(|e| SparkError::Arrow(Arc::new(e)))?;
     Ok(Arc::new(decimal_array))
 }

Review Comment:
   `is_validate_decimal_precision` from arrow-rs (`arrow-data/src/decimal.rs`, 
re-exported via `arrow::datatypes`) is the right existing kernel for the 
precision check and is used consistently with the same import already in 
`numeric.rs`. There is no arrow-rs or DataFusion cast kernel that already 
implements Spark's `nullOnOverflow = !ansiEnabled` semantics for decimal casts, 
since that is Spark-specific behavior rather than a general Arrow cast concept, 
so a hand-written check here is expected and this part does not need to change. 
Dropping the old string-matching error detection (`e.to_string().contains("too 
large to store in a Decimal128")`) in favor of the `eval_mode` precheck removes 
a fragile pattern and is worth keeping as is.



##########
spark/src/test/scala/org/apache/comet/CometCastSuite.scala:
##########
@@ -189,6 +189,18 @@ class CometCastSuite extends CometTestBase with 
AdaptiveSparkPlanHelper {
     castTest(generateBools(), DataTypes.createDecimalType(30, 0))
   }
 
+  test("cast BooleanType to DecimalType where 10^scale overflows precision") {
+    // Regression for https://github.com/apache/datafusion-comet/issues/5068.
+    // `true` scales to 10^scale, which does not fit in 
DECIMAL(1,1)/(2,2)/(3,3).
+    // Spark returns NULL in legacy/try mode and only throws under ANSI.
+    Seq(
+      DataTypes.createDecimalType(1, 1),
+      DataTypes.createDecimalType(2, 2),
+      DataTypes.createDecimalType(3, 3)).foreach { dt =>
+      castTest(generateBools(), dt)
+    }
+  }

Review Comment:
   The new test relies on `castTest`'s default `testAnsi = true` to exercise 
the ANSI overflow path, but nothing in the test documents that this is 
intentional or names the ANSI behavior being checked, unlike the comment block 
above it which only describes the legacy/try semantics. Add a line noting that 
`castTest`'s ANSI branch, enabled by default, is what exercises the 
`EvalMode::Ansi` throw path added in `boolean.rs`, since a reader scanning this 
test in isolation cannot otherwise tell that ANSI is covered here at all.



##########
native/spark-expr/src/conversion_funcs/boolean.rs:
##########
@@ -32,28 +32,32 @@ pub fn cast_boolean_to_decimal(
     array: &ArrayRef,
     precision: u8,
     scale: i8,
+    eval_mode: EvalMode,
 ) -> SparkResult<ArrayRef> {
     let bool_array = array.as_boolean();
     let scaled_val = 10_i128.pow(scale as u32);
+
+    // Spark's Cast uses `nullOnOverflow = !ansiEnabled`: legacy/try return 
NULL
+    // on overflow, only ANSI raises. `false` maps to 0 which always fits, so
+    // overflow only happens for `true` when 10^scale exceeds `precision`.
+    let overflows = !is_validate_decimal_precision(scaled_val, precision);
+    if overflows && eval_mode == EvalMode::Ansi {
+        return Err(crate::error::decimal_overflow_error(
+            scaled_val, precision, scale,
+        ));

Review Comment:
   The ANSI error reports the wrong value. `decimal_overflow_error(scaled_val, 
precision, scale)` passes the already-scaled internal representation 
(`10_i128.pow(scale)`, e.g. 10 for `Decimal(1,1)`). Spark's actual path for 
boolean-to-decimal ANSI overflow is `Cast.scala` calling `toPrecision(if (b) 
Decimal.ONE else Decimal.ZERO, target, ...)`, and `Decimal.toPrecision` on 
failure calls `DataTypeErrors.cannotChangeDecimalPrecisionError(this, 
precision, scale, context)` where `this` is `Decimal.ONE`, i.e. the unscaled 
logical value 1. `Decimal.ONE.toPlainString` is `"1"`, so Spark's message is `1 
cannot be represented as Decimal(1, 1)....`. On the JVM side, 
`ShimSparkErrorConverter` 
(`spark/src/main/spark-3.4/org/apache/spark/sql/comet/shims/ShimSparkErrorConverter.scala:107-113`,
 same in spark-3.5) reconstructs `Decimal(params("value").toString)` and calls 
the same `cannotChangeDecimalPrecisionError`, so whatever string Comet puts in 
`value` is parsed back as a `Decimal` verbatim. Si
 nce Comet passes `"10"`, the reconstructed message is `10 cannot be 
represented as Decimal(1, 1)...`, which does not match Spark's `1 cannot be 
represented as Decimal(1, 1)...`.
   This mismatch predates this PR (the old `map_err` branch also passed 
`scaled_val` into `decimal_overflow_error`), but it was never exercised by any 
test until this PR added the `CometCastSuite` regression test that now runs 
`true` values through `DECIMAL(1,1)/(2,2)/(3,3)` under ANSI. On the `spark-3.4` 
and `spark-3.5` profiles, `castTest`'s ANSI branch asserts `cometMessage == 
sparkMessage` exactly 
(`spark/src/test/scala/org/apache/comet/CometCastSuite.scala:2431`), so the new 
test will fail the exact-message comparison on those profiles. On 
`spark-4.0`/`spark-4.1` it will not be caught, because the `isSpark40Plus` 
branch only compares `cometMessage.substring(0, min(40, cometMessage.length))` 
as a prefix (`CometCastSuite.scala:2426-2428`), and 40 characters only covers 
the `[NUMERIC_VALUE_OUT_OF_RANGE.WITH_SUGGESTION] ` tag, not the value.
   Fix: pass the pre-scale logical value, matching the convention used by 
`cast_int_to_decimal128_internal` 
(`native/spark-expr/src/conversion_funcs/numeric.rs:750-754`), which reports 
the original face-value input, not the post-multiplier value. For boolean, that 
is `1` for `true`: replace `decimal_overflow_error(scaled_val, precision, 
scale)` with `decimal_overflow_error(1, precision, scale)`.



##########
native/spark-expr/src/conversion_funcs/boolean.rs:
##########
@@ -32,28 +32,32 @@ pub fn cast_boolean_to_decimal(
     array: &ArrayRef,
     precision: u8,
     scale: i8,
+    eval_mode: EvalMode,
 ) -> SparkResult<ArrayRef> {
     let bool_array = array.as_boolean();
     let scaled_val = 10_i128.pow(scale as u32);
+
+    // Spark's Cast uses `nullOnOverflow = !ansiEnabled`: legacy/try return 
NULL
+    // on overflow, only ANSI raises. `false` maps to 0 which always fits, so
+    // overflow only happens for `true` when 10^scale exceeds `precision`.
+    let overflows = !is_validate_decimal_precision(scaled_val, precision);
+    if overflows && eval_mode == EvalMode::Ansi {
+        return Err(crate::error::decimal_overflow_error(
+            scaled_val, precision, scale,
+        ));
+    }
+
     let result: Decimal128Array = bool_array
         .iter()
-        .map(|v| v.map(|b| if b { scaled_val } else { 0 }))
+        .map(|v| match v {
+            Some(false) => Some(0),
+            Some(true) if !overflows => Some(scaled_val),
+            _ => None,
+        })
         .collect();
-
-    // Convert Arrow decimal overflow errors to SparkError
     let decimal_array = result
         .with_precision_and_scale(precision, scale)
-        .map_err(|e| {
-            if matches!(e, arrow::error::ArrowError::InvalidArgumentError(_))
-                && e.to_string().contains("too large to store in a Decimal128")

Review Comment:
   This is the third hand-written copy of the same decision shape: check 
`is_validate_decimal_precision`, then match `eval_mode` to decide null vs 
`decimal_overflow_error`. The other two copies are 
`native/spark-expr/src/conversion_funcs/numeric.rs:744-772` 
(`cast_int_to_decimal128_internal`, both the `checked_mul` overflow branch and 
the post-multiply precision-check branch) and `numeric.rs:915-937` 
(`cast_floating_point_to_decimal128`'s ANSI rescan). Each function's scan 
strategy should stay different: boolean's domain is 2-valued so a single 
precheck outside the loop is correct and should not be forced into a per-row 
loop like the int/float paths. But the branch body itself, "precision check 
fails, Ansi errors with `decimal_overflow_error(value, precision, scale)`, else 
null", is identical in shape across all three sites and has already drifted 
once: boolean passes the scaled value while int/float pass the face value (see 
the finding above). Extract a small helper, e.g. `fn decima
 l_overflow_result(value: i128, precision: u8, scale: i8, eval_mode: EvalMode) 
-> SparkResult<Option<i128>>` in `numeric.rs` or a shared `decimal.rs`, and 
have all three call sites call it with the correct pre-scale `value` argument. 
This forces the value convention to be decided once instead of letting it 
diverge a third time.



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