From c94d453b537364feeaecfc26e4157e461fe99519 Mon Sep 17 00:00:00 2001 From: SEPURI-SAI-KRISHNA Date: Mon, 21 Sep 2026 14:30:02 +0530 Subject: [PATCH 1/2] [SPARK-59686][SQL] Clamp the rounding scale in round and bround round and bround passed the user supplied scale straight to BigDecimal.setScale, which throws once the magnitude passes roughly 1e9. Neighbouring scales return the result fine, so a well defined answer became an error: round(1.5, -10000000) is 0.0 while round(1.5, -1000000000) raised java.lang.ArithmeticException. On the ANSI integral path the exception was wrapped by MathUtils.withOverflow and surfaced as a spurious ARITHMETIC_OVERFLOW, even though nothing overflowed and the result is representable. Everywhere else, including the integral types in non-ANSI mode, the raw java.lang.ArithmeticException reached the user. Clamp the scale where it is read. _scale is a single protected lazy val that every type branch uses and that codegen interpolates, so clamping there covers interpreted and generated code for all numeric types. The clamp cannot change a result: no finite value has more than 309 integral digits, and the exact decimal expansion of a finite double needs at most 1074 fractional digits. It also stops -_scale in dataType from overflowing for a scale of Int.MinValue. --- .../expressions/mathExpressions.scala | 16 ++++++++++- .../expressions/MathExpressionsSuite.scala | 27 +++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/mathExpressions.scala b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/mathExpressions.scala index 0aebdc0223144..94f3baaed8527 100644 --- a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/mathExpressions.scala +++ b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/mathExpressions.scala @@ -1687,6 +1687,13 @@ case class Logarithm(left: Expression, right: Expression) * @param mode rounding mode (e.g. HALF_UP, HALF_EVEN) * @param modeStr rounding mode string name (e.g. "ROUND_HALF_UP", "ROUND_HALF_EVEN") */ +object RoundBase { + // Beyond this magnitude a rounding scale cannot change the result for any Spark numeric type, + // since 1074 exceeds both the fractional digits of the smallest subnormal double and the + // integral digits of the largest finite double. + val MAX_SCALE: Int = 1100 +} + abstract class RoundBase(child: Expression, scale: Expression, mode: BigDecimal.RoundingMode.Value, modeStr: String) extends BinaryExpression with Serializable with ImplicitCastInputTypes with SupportQueryContext { @@ -1743,7 +1750,14 @@ abstract class RoundBase(child: Expression, scale: Expression, // avoid unnecessary `child` evaluation in both codegen and non-codegen eval // by checking if scaleV == null as well. private lazy val scaleV: Any = scale.eval(EmptyRow) - protected lazy val _scale: Int = scaleV.asInstanceOf[Int] + // The requested scale is clamped before it reaches `BigDecimal.setScale`, which throws for + // magnitudes beyond roughly 1e9. Clamping cannot change a result: no finite value has more + // than 309 integral digits, so every scale at or below `-MAX_SCALE` rounds to zero, and the + // exact decimal expansion of a finite double needs at most 1074 fractional digits, so every + // scale at or above `MAX_SCALE` leaves the value unchanged. It also keeps `-_scale` in + // `dataType` from overflowing for a scale of `Int.MinValue`. + protected lazy val _scale: Int = + math.max(-RoundBase.MAX_SCALE, math.min(RoundBase.MAX_SCALE, scaleV.asInstanceOf[Int])) override def initQueryContext(): Option[QueryContext] = { if (ansiEnabled) { diff --git a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/MathExpressionsSuite.scala b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/MathExpressionsSuite.scala index 761f5268d4562..3a1a1246a6139 100644 --- a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/MathExpressionsSuite.scala +++ b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/MathExpressionsSuite.scala @@ -748,6 +748,33 @@ class MathExpressionsSuite extends SparkFunSuite with ExpressionEvalHelper { checkEvaluation(Truncate(Literal(1.23), Literal.create(null, IntegerType)), null) } + test("round/bround with an extreme scale") { + // `BigDecimal.setScale` throws for scales beyond roughly 1e9, so before clamping these + // raised a raw java.lang.ArithmeticException, or a spurious ARITHMETIC_OVERFLOW on the + // ANSI integral path, even though neighbouring scales return the result below. + Seq(Int.MinValue, Int.MinValue + 1, -1000000000, -10000000).foreach { scale => + checkEvaluation(Round(Literal(1.5d), Literal(scale)), 0.0d) + checkEvaluation(BRound(Literal(1.5d), Literal(scale)), 0.0d) + checkEvaluation(Round(Literal(1.5f), Literal(scale)), 0.0f) + checkEvaluation(Round(Literal(1L), Literal(scale)), 0L) + checkEvaluation(Round(Literal(1, IntegerType), Literal(scale)), 0) + checkEvaluation(Round(Literal(1L), Literal(scale), ansiEnabled = true), 0L) + } + // A scale past the digits a value carries leaves it unchanged. + Seq(Int.MaxValue, Int.MaxValue - 1, 1000000000, 10000000).foreach { scale => + checkEvaluation(Round(Literal(1.5d), Literal(scale)), 1.5d) + checkEvaluation(BRound(Literal(1.5d), Literal(scale)), 1.5d) + checkEvaluation(Round(Literal(1.5f), Literal(scale)), 1.5f) + checkEvaluation(Round(Literal(1L), Literal(scale)), 1L) + checkEvaluation(Round(Literal(1L), Literal(scale), ansiEnabled = true), 1L) + } + // The smallest subnormal has the longest exact decimal expansion, so it is the value most + // at risk from clamping the scale. + checkEvaluation(Round(Literal(Double.MinPositiveValue), Literal(Int.MaxValue)), + Double.MinPositiveValue) + checkEvaluation(Round(Literal(Double.MaxValue), Literal(Int.MinValue)), 0.0d) + } + test("round/bround/floor/ceil") { val scales = -6 to 6 val doublePi: Double = math.Pi From e8b08a1bb8e990bdd13f98d841982537273deff7 Mon Sep 17 00:00:00 2001 From: SEPURI-SAI-KRISHNA Date: Mon, 21 Sep 2026 14:41:10 +0530 Subject: [PATCH 2/2] Trigger CI