spark-test commented on code in PR #59889:
URL: https://github.com/apache/spark/pull/59889#discussion_r4406720191


##########
sql/catalyst/src/test/scala/org/apache/spark/sql/types/DecimalSuite.scala:
##########
@@ -245,6 +247,141 @@ class DecimalSuite extends SparkFunSuite with 
PrivateMethodTester with SQLHelper
     assert(Decimal(100) % Decimal(0) === null)
   }
 
+  Seq(
+    ("10000000000000000000000000000000000000", "0.0007", "0.0005"),
+    ("99999999999999999999999999999999999999", "0.0007", "0.0004"),
+    ("99999999999999999999999999999999999999", "7E-38", "2E-38"),
+    ("99999999999999999999999999999999999999", "1E-38", "0")
+  ).foreach { case (dividend, divisor, expected) =>
+    test(s"decimal remainder with a large integral quotient: $dividend % 
$divisor") {
+      val left = Decimal(dividend)
+      val right = Decimal(divisor)
+      val remainder = Decimal(expected)
+      assert(left % right == remainder)
+      assert(left % -right == remainder)
+      assert(-left % right == -remainder)
+      assert(-left % -right == -remainder)
+      assert((left % right).scale == remainder.scale)
+      assert(left % Decimal(0) == null)
+    }
+  }
+
+  Seq(
+    (38, "99999999999999999999999999999999999999", "7", "1", 38),
+    (39, "99999999999999999999999999999999999999", "0.7", "0.3", 39),
+    (40, "99999999999999999999999999999999999999", "0.07", "0.02", 40),
+    (40, "10000000000000000000000000000000000000", "0.07", "0.06", 39)
+  ).foreach { case (bound, dividend, divisor, expected, quotientDigits) =>
+    test(s"decimal remainder with quotient bound $bound and $quotientDigits 
actual digits") {
+      val left = Decimal(dividend)
+      val right = Decimal(divisor)
+      val quotient = 
left.toJavaBigDecimal.divideToIntegralValue(right.toJavaBigDecimal)
+      assert(quotient.toBigInteger.toString.length == quotientDigits)
+      for (leftSign <- Seq(1, -1); rightSign <- Seq(1, -1)) {
+        val signedLeft = if (leftSign < 0) -left else left
+        val signedRight = if (rightSign < 0) -right else right
+        val remainder = if (leftSign < 0) -Decimal(expected) else 
Decimal(expected)
+        assert((signedLeft % signedRight).toJavaBigDecimal == 
remainder.toJavaBigDecimal)
+      }
+    }
+  }
+
+  test("decimal remainder preserves zero quotients, trailing zeros and compact 
operands") {
+    Seq(
+      (Decimal("0.00"), Decimal("0.7"), Decimal("0.00")),
+      (Decimal("1.00"), Decimal("7.0"), Decimal("1.00")),
+      (Decimal(120000L, 6, 2), Decimal(70L, 2, 1), Decimal("3.00")),
+      // The 40-digit quotient is representable by the old context because it 
has trailing zeros.
+      (Decimal("10000000000000000000000000000000000000"), Decimal("0.01"), 
Decimal("0"))
+    ).foreach { case (left, right, expected) =>
+      assert((left % right).toJavaBigDecimal == expected.toJavaBigDecimal)
+      assert((-left % right).toJavaBigDecimal == (-expected).toJavaBigDecimal)
+      assert((left % -right).toJavaBigDecimal == expected.toJavaBigDecimal)
+      assert(left % Decimal(0) == null)
+    }
+  }
+
+  test("decimal remainder preserves negative and extreme scales") {
+    withSQLConf(SQLConf.LEGACY_ALLOW_NEGATIVE_SCALE_OF_DECIMAL_ENABLED.key -> 
"true") {
+      Seq(
+        (Decimal("123E+4"), Decimal("7E+2"), Decimal("1E+2")),
+        (Decimal("9E+39"), Decimal("7"), Decimal("5")),
+        (Decimal(3L, 1, Int.MinValue), Decimal(2L, 1, Int.MinValue),
+          Decimal(1L, 1, Int.MinValue)),
+        (Decimal(3L, 1, Int.MaxValue), Decimal(2L, 1, Int.MaxValue),
+          Decimal(1L, 1, Int.MaxValue)),
+        (Decimal(3L, 1, Int.MinValue), Decimal(2L, 1, Int.MinValue + 1),
+          Decimal(0L, 1, Int.MinValue + 1)),
+        (Decimal(3L, 1, Int.MaxValue), Decimal(2L, 1, Int.MaxValue - 1),
+          Decimal(3L, 1, Int.MaxValue)),
+        // These scale differences exceed Int's range, but their remainders 
are representable.
+        (Decimal(0L, 1, Int.MinValue), Decimal(2L, 1, Int.MaxValue), 
Decimal(0L, 1, -1)),
+        (Decimal(0L, 1, Int.MaxValue), Decimal(2L, 1, Int.MinValue),
+          Decimal(0L, 1, Int.MaxValue)),
+        (Decimal(1L, 1, Int.MaxValue), Decimal(2L, 1, Int.MinValue),
+          Decimal(1L, 1, Int.MaxValue))
+      ).foreach { case (left, right, expected) =>
+        assert((left % right).toJavaBigDecimal == expected.toJavaBigDecimal)
+        assert((-left % right).toJavaBigDecimal == 
(-expected).toJavaBigDecimal)
+        assert((left % -right).toJavaBigDecimal == expected.toJavaBigDecimal)
+      }
+    }
+  }
+
+  test("decimal remainder preserves huge trailing-zero quotients at extreme 
negative scales") {
+    withSQLConf(SQLConf.LEGACY_ALLOW_NEGATIVE_SCALE_OF_DECIMAL_ENABLED.key -> 
"true") {
+      for {
+        scale <- Seq(Int.MinValue, Int.MinValue + 1, Int.MinValue + 2)
+        leftSign <- Seq(1L, -1L)
+        rightSign <- Seq(1L, -1L)
+      } {
+        val left = Decimal(leftSign, 1, scale)
+        val right = Decimal(rightSign)
+        val result = left % right
+        assert(result.isZero)
+        assert(result.scale == scale)
+      }
+    }
+  }
+
+  test("decimal remainder propagates failure when both division attempts 
fail") {

Review Comment:
   **Nit (P3):** Test-only: `Decimal.%` itself behaves as the PR declares, but 
this test no longer exercises the path its title names. It was written for the 
first commit's unbounded exact retry. With the 76-digit bound, the estimate for 
`Decimal(1L, 1, Int.MinValue) % Decimal(1L, 1, Int.MaxValue)` is `1 - 
Int.MinValue + Int.MaxValue = 2^32`. `%` therefore rethrows the context failure 
at the bound and never calls the exact `remainder(divisor)`. The two direct 
intercepts above it only exercise JDK `BigDecimal`. So the test now duplicates 
the rethrow check in `decimal remainder bounds exact retries for legacy 
negative scales`, with a weaker assertion. Within the bound, practically sized 
operands cannot make the exact retry fail, so there is no double-failure path 
left to cover. The PR description's claim to cover "cases where both attempts 
fail" is stale for the same reason.
   
   There is also a coverage gap. Every rejection case in the new tests exceeds 
76 through dividend scale alone: this test, and the bound test, whose divisor 
`Decimal(3L)` has scale 0. Dropping or negating `+ divisor.scale()` in the 
estimate would leave every new test green. Yet that term sets where legacy 
negative-scale inputs are rejected. It also keeps out-of-range direct-API 
divisors such as `Decimal("3E-100000000")` away from an exact retry that would 
materialize a quotient of about 10^8 digits.
   
   Suggested change: replace this test with a case that is rejected only 
because of the divisor's scale. For example, `Decimal(1) % Decimal("3E-100")` 
has an estimate of 101; without the term it would return `1E-100`. As the 
legacy-scale bound test does, take the expected message from the 39-digit 
context remainder on the same operands, assert that `%` rethrows it, and title 
the test for that behavior.
   
   **Verification:**
   - **Regression:** With the divisor-scale term removed from or negated in the 
retry estimate, the new divisor-scale bound case fails because the remainder 
returns an exact value instead of rethrowing. With the term present, the case 
passes, and the rethrown message equals the message of the 39-digit context 
remainder for the same operands.
   - **Behavior:** The existing decimal remainder coverage still passes 
unchanged: wide-quotient results, preserved context-path results and scales, 
legacy and extreme scales, and the dividend-side bound cases.



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