spark-test commented on code in PR #60188:
URL: https://github.com/apache/spark/pull/60188#discussion_r4500500771
##########
sql/api/src/main/scala/org/apache/spark/sql/types/Decimal.scala:
##########
@@ -558,14 +558,34 @@ final class Decimal extends Ordered[Decimal] with
Serializable {
def %(that: Decimal): Decimal =
if (that.isZero) null
- else Decimal(toJavaBigDecimal.remainder(that.toJavaBigDecimal,
MATH_CONTEXT))
+ else {
+ val (a, b) = (toJavaBigDecimal, that.toJavaBigDecimal)
+ Decimal(a.remainder(b, integralQuotientContext(a, b)))
+ }
def quot(that: Decimal): Decimal =
if (that.isZero) null
- else Decimal(toJavaBigDecimal.divideToIntegralValue(that.toJavaBigDecimal,
MATH_CONTEXT))
+ else {
+ val (a, b) = (toJavaBigDecimal, that.toJavaBigDecimal)
+ Decimal(a.divideToIntegralValue(b, integralQuotientContext(a, b)))
+ }
def remainder(that: Decimal): Decimal = this % that
+ // `BigDecimal.remainder` and `divideToIntegralValue` raise "Division
impossible" when the
+ // integral part of the quotient has more digits than the `MathContext`
holds, e.g. 41 digits
+ // for DECIMAL(38, 0) and DECIMAL(3, 3). Returns `MATH_CONTEXT`, or a larger
context that holds
+ // the integral part of `a / b`: |a| < 10^(a.precision - a.scale) and
+ // |b| >= 10^(b.precision - b.scale - 1).
+ // This is a private method of the class, not of `object Decimal`: the class
calling a private
+ // member of the object makes it public in `Decimal$` and changes the
computed
+ // serialVersionUID of `Decimal$`, which
`UdfSerialization.auditedTransitions` pins.
+ private def integralQuotientContext(a: JavaBigDecimal, b: JavaBigDecimal):
MathContext = {
+ val digits = (a.precision.toLong - a.scale) - (b.precision.toLong -
b.scale) + 1
+ if (digits <= MATH_CONTEXT.getPrecision) MATH_CONTEXT
+ else new MathContext(digits.toInt, RoundingMode.DOWN)
Review Comment:
**Non-blocking (P2):** With
`spark.sql.legacy.allowNegativeScaleOfDecimal=true`, a negative operand scale
feeds `digits` directly, and this context has no upper limit. For `1E+100000BD
% 1BD`, `digits` is 100,001, although the integral quotient has a single
significant digit. The JDK computes all of those quotient digits and then
strips the trailing zeros one at a time.
I ran the same `BigDecimal` calls on JDK 21.0.8, comparing the base branch's
`MATH_CONTEXT` with this helper. Both return the same values where both succeed:
- `1E+10000BD % 1BD`: 57 ms, against under 1 ms on the base branch
- `1E+100000BD % 1BD`: 3.6 s
- `1E+1000000BD % 1BD`: no answer within 150 s, so a query that used to
finish can hang during constant folding or on every row
- `1E+999999999BD % 1BD`: now raises `ArithmeticException: BigInteger would
overflow supported range` instead of returning 0
- `1E+2147483636BD % 3E-38BD`: the bound exceeds `Int.MaxValue`, so
`digits.toInt` wraps negative and `new MathContext` raises
`IllegalArgumentException: Digits < 0`, where the base branch raised
`ArithmeticException`
So under this mode, some calls that did not raise before now fail, and
others become far slower. This is the kind of extreme negative-scale cost and
`BigInteger` overflow that SPARK-60119 recently removed from `changePrecision`.
With scales 0 to 38 the bound stays at or below 76, so this part is specific to
the legacy mode.
See **Shared repair plan 1** in the review body.
##########
sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/ArithmeticExpressionSuite.scala:
##########
@@ -776,6 +776,18 @@ class ArithmeticExpressionSuite extends SparkFunSuite with
ExpressionEvalHelper
}
}
+ test("SPARK-61050: Remainder, Pmod and IntegralDivide of decimals with a
41-digit quotient") {
Review Comment:
**Nit (P3):** This covers the widened-context branch at one width only: a
41-digit quotient. The randomized SPARK-45786 test never reaches the branch,
because `makeNum` always yields |divisor| >= 0.1, which keeps the bound at 39
or below. Two plausible regressions would leave the suite green while supported
inputs raise `Division impossible` again:
- An off-by-one threshold such as `digits <= 40` breaks 38 nines `% 0.07`,
which has a 40-digit quotient and remainder 0.02.
- A fixed 41-digit context breaks 38 nines `% 7E-38` as `DECIMAL(38, 38)`,
which has a 76-digit quotient and remainder 2E-38. That is the widest case for
precision-38 operands with non-negative scales.
Both inputs raise on the base branch and return those remainders with this
change (I checked both on JDK 21). Asserting them next to the 41-digit case
would pin both edges.
**Verification:**
- **Behavior:** Remainder and Pmod return 0.02 for 38 nines by 0.07 and
2E-38 for 38 nines by 7E-38 in LEGACY and ANSI modes on interpreted and
code-generated evaluation. IntegralDivide returns NULL in LEGACY and raises
NUMERIC_VALUE_OUT_OF_RANGE in ANSI for both inputs.
- **Regression:** Keeping the 39-digit context when the bound is 40 makes
the 40-digit case fail with the raw Division impossible exception. Using a
fixed 41-digit context makes the 76-digit case fail. Both cases pass with the
PR's context selection, and with the fix that keeps the 39-digit context until
it raises.
--
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]