0lai0 commented on code in PR #5050:
URL: https://github.com/apache/datafusion-comet/pull/5050#discussion_r4307675772
##########
spark/src/test/scala/org/apache/comet/CometExpressionSuite.scala:
##########
@@ -1486,6 +1487,111 @@ class CometExpressionSuite extends CometTestBase with
AdaptiveSparkPlanHelper {
}
}
+ test("division on negative-scale decimal reports unsupported via
getSupportLevel") {
+ // Issue #5013: `spark_decimal_div` computes its scale-alignment exponents
in `u32`, so a
+ // negative scale wraps to roughly 4.29e9 and the BigInt branch attempts
an unbounded
+ // allocation. Only division is affected. The other arithmetic operators
scale-align by
+ // `10^(max_scale - s)`, which is never negative, so they stay native and
are pinned by
+ // "safe ops on negative-scale decimal run natively" below. Constructing
the expressions
+ // requires allowNegativeScaleOfDecimal so DecimalType(_, s<0) passes
Spark's own check.
+ withSQLConf("spark.sql.legacy.allowNegativeScaleOfDecimal" -> "true") {
+ Seq(DecimalType(10, -1), DecimalType(20, -5)).foreach { negScale =>
+ val posDec = DecimalType(10, 0)
+ val negAttr = AttributeReference("n", negScale)()
+ val posAttr = AttributeReference("p", posDec)()
+ val expected =
Unsupported(Some(CometDivide.negScaleDecimalArithmeticReason))
+
+ assert(CometDivide.getSupportLevel(Divide(negAttr, posAttr)) ==
expected)
+ assert(CometIntegralDivide.getSupportLevel(IntegralDivide(negAttr,
posAttr)) == expected)
+ // Guard also fires when negative scale is only on the right operand.
+ assert(CometDivide.getSupportLevel(Divide(posAttr, negAttr)) ==
expected)
+
+ // The unguarded operators must not report unsupported for the same
operands.
+ assert(CometAdd.getSupportLevel(Add(negAttr, posAttr)) == Compatible())
+ assert(CometSubtract.getSupportLevel(Subtract(negAttr, posAttr)) ==
Compatible())
+ assert(CometRemainder.getSupportLevel(Remainder(negAttr, posAttr)) ==
Compatible())
+ assert(CometMultiply.getSupportLevel(Multiply(negAttr, posAttr)) ==
Compatible())
+ }
+ }
+ }
+
+ test("division on negative-scale decimal falls back and returns correct
results") {
+ // End-to-end proof for issue #5013 that the previously-panicking queries
now complete.
+ // `checkSparkAnswerAndFallbackReason` rather than `checkSparkAnswer`:
these five serdes do
+ // not mix in `CodegenDispatchFallback`, so `Unsupported` falls the whole
projection back to
+ // Spark, and asserting the reason is what proves the guard fired. A plain
answer comparison
+ // would pass whether or not it did.
+ // ConvertToLocalRelation is excluded so the arithmetic actually runs on
the plan rather
+ // than being folded at plan time (#4789). `DecimalType(_, s<0)` must be
constructed under
+ // allowNegativeScaleOfDecimal=true because the case class initializer
reads the flag, so
+ // wrap everything (including the type value) in one block.
+ withSQLConf(
+ "spark.sql.legacy.allowNegativeScaleOfDecimal" -> "true",
+ CometConf.COMET_EXEC_LOCAL_TABLE_SCAN_ENABLED.key -> "true",
+ SQLConf.OPTIMIZER_EXCLUDED_RULES.key ->
+ "org.apache.spark.sql.catalyst.optimizer.ConvertToLocalRelation") {
+ // Cover a narrow and a wide negative-scale type: at precision 20 the
combined operand
+ // width crosses DECIMAL128_MAX_PRECISION, which routes the operation to
+ // `WideDecimalBinaryExpr` / `decimal_div`'s BigInt path rather than the
narrow arrow
+ // kernels. `unit` is the smallest magnitude representable at that scale.
+ val negScaleCases = Seq((DecimalType(10, -1), 10), (DecimalType(20, -5),
100000))
+ negScaleCases.foreach { case (negScaleType, unit) =>
+ // Build the neg-scale column via string -> Decimal(neg), a known-safe
cast, so the
+ // source construction does not depend on any of the guards under test.
+ val strs = Seq(10 * unit, 20 * unit, 30 * unit).map(_.toString)
+ val v = strs.toDF("s").select(col("s").cast(negScaleType).as("v"))
+ Seq("v / cast(3 as decimal(10, 0))", "v div cast(3 as decimal(10,
0))").foreach { e =>
+ checkSparkAnswerAndFallbackReason(
+ v.selectExpr(e),
+ CometDivide.negScaleDecimalArithmeticReason)
+ }
+ }
+ }
+ }
+
+ test("safe ops on negative-scale decimal run natively (regression pin)") {
+ // Multiply and UnaryMinus on negative-scale decimals don't scale-align an
integer and
+ // ran natively at the time #5013 was fixed. The arithmetic guard
intentionally does
+ // not cover them. If a future change ever adds scale-alignment to
Multiply or a
+ // decimal codepath is added to UnaryMinus that does the multiply-by-10^n
trick,
+ // these assertions will start returning a Comet fallback plan and this
test will
+ // catch it before it reaches production as a silent-wrap or debug panic.
+ withSQLConf(
+ "spark.sql.legacy.allowNegativeScaleOfDecimal" -> "true",
+ CometConf.COMET_EXEC_LOCAL_TABLE_SCAN_ENABLED.key -> "true",
+ SQLConf.OPTIMIZER_EXCLUDED_RULES.key ->
+ "org.apache.spark.sql.catalyst.optimizer.ConvertToLocalRelation") {
+ // The wide case matters most for `v * v`: Decimal(20,-5) *
Decimal(20,-5) has
+ // p1 + p2 >= DECIMAL128_MAX_PRECISION, so it takes
`WideDecimalBinaryExpr`'s multiply
+ // branch (which does compute a scale adjustment against the output
precision/scale)
+ // and its result type is Decimal(38,-10) -- a negative *output* scale,
not just
+ // negative inputs.
+ val negScaleCases = Seq((DecimalType(10, -1), 10), (DecimalType(20, -5),
100000))
+ negScaleCases.foreach { case (negScaleType, unit) =>
+ val strs = Seq(10 * unit, 20 * unit, 30 * unit).map(_.toString)
+ val v = strs.toDF("s").select(col("s").cast(negScaleType).as("v"))
+ // Same-scale Multiply - no alignment needed, must stay native.
+ checkSparkAnswerAndOperator(v.selectExpr("v * v"))
+ checkSparkAnswerAndOperator(v.selectExpr("v * cast(2 as int)"))
+ // UnaryMinus on Decimal(neg) - no alignment, must stay native.
+ checkSparkAnswerAndOperator(v.selectExpr("-v"))
+ // Add, Subtract and Remainder scale-align by `10^(max_scale - s)`,
which is never
+ // negative. They were guarded in an earlier revision of this PR
because the `u8`
+ // underflow in the two width tests made them fail. Those are
rewritten in `i16` here,
+ // so they run natively again. If either rewrite is reverted these
assertions fail.
Review Comment:
You are right that the Scala assertions cannot pin the rewrites with
`overflow-checks = false`. I have reworded that comment rather than leaving it
claiming something it cannot do.
Three tests added, which do run with overflow checks on:
- `test_decimal128_add_sub_width_handles_negative_scale`, covering the
negative-scale cases plus a few non-negative ones to pin the unchanged behavior
- `test_modulo_decimal_negative_scale`, next to `test_modulo_basic_decimal`
- `test_decimal_div_rejects_negative_scale`, next to
`test_decimal_divide_by_zero_returns_spark_error`, covering all three positions
a negative scale can appear in
On sharing the calculation, I did not do it quite as suggested, because the
two sites use different boundaries: `create_binary_expr_with_options` tests `>=
DECIMAL128_MAX_PRECISION` and `create_modulo_expr` tests `>`. A shared
predicate would have changed one of them. So the shared piece is the
calculation, `decimal128_add_sub_width(p1, s1, p2, s2) -> i16`, and each site
keeps its own comparison. That still leaves one copy of the expression and
makes it testable.
--
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]