dongjoon-hyun commented on code in PR #59334:
URL: https://github.com/apache/spark/pull/59334#discussion_r4252551518
##########
sql/api/src/main/scala/org/apache/spark/sql/types/Decimal.scala:
##########
@@ -635,26 +635,38 @@ object Decimal {
}
}
- private def numDigitsInIntegralPart(bigDecimal: JavaBigDecimal): Int =
- bigDecimal.precision - bigDecimal.scale
+ // Computed in Long since `precision - scale` can overflow Int, e.g. for
"1e2147483647".
+ private def numDigitsInIntegralPart(bigDecimal: JavaBigDecimal): Long =
+ bigDecimal.precision.toLong - bigDecimal.scale
private def stringToJavaBigDecimal(str: UTF8String): JavaBigDecimal = {
// According the benchmark test, `s.toString.trim` is much faster than
`s.trim.toString`.
// Please refer to https://github.com/apache/spark/pull/26640
new JavaBigDecimal(str.toString.trim)
}
+ /**
+ * Converts a `JavaBigDecimal` parsed from a string to `Decimal`, or returns
null if no decimal
+ * type can hold it. We fast fail because constructing a very large
JavaBigDecimal to Decimal is
+ * very slow. For example: Decimal("6.0790316E+25569151")
+ */
+ private def stringBigDecimalToDecimal(bigDecimal: JavaBigDecimal): Decimal =
{
+ if (SqlApiConf.get.allowNegativeScaleOfDecimalEnabled) {
Review Comment:
This reads `SqlApiConf.get.allowNegativeScaleOfDecimalEnabled` before
anything else, so every call pays for a conf lookup. On master and at the
previous commit, the `&&` order read the conf only when
`numDigitsInIntegralPart(bigDecimal) > DecimalType.MAX_PRECISION`, that is,
only for out-of-range strings.
This is a per-row path: `Cast` calls the helper per row (L1380/L1385
interpreted, L1900/L1912 codegen), and so does the protobuf uint64-to-decimal
converter. The lookup isn't cheap there:
- On executors, a normal task has no `existingConf` (only `SQLExecutionRDD`
sets one), so `SQLConf.get` returns `new ReadOnlySQLConf(TaskContext.get())`.
That builds two `ConfigReader`s with their maps and providers, then looks the
key up in the task's local properties, once per row.
- On the driver (for example `ConvertToLocalRelation` or constant folding),
`SQLConf.get` calls `SparkContext.getActive`, which takes
`SPARK_CONTEXT_CONSTRUCTOR_LOCK` on every call.
I traced this in the code and didn't benchmark it. This path has been tuned
for speed before, though (the `s.toString.trim` note in
`stringToJavaBigDecimal` above, and the fast-fail itself).
How about reading the conf only on the two rare paths that need it? For
example:
```scala
private def stringBigDecimalToDecimal(bigDecimal: JavaBigDecimal): Decimal
= {
if (bigDecimal.signum == 0) {
// Zero fits any decimal type whatever its exponent, e.g. "0e39". Drop
a negative scale so
// that the precision does not grow with the exponent.
if (bigDecimal.scale < 0 &&
!SqlApiConf.get.allowNegativeScaleOfDecimalEnabled) {
Decimal(bigDecimal.setScale(0))
} else {
Decimal(bigDecimal)
}
} else if (numDigitsInIntegralPart(bigDecimal) >
DecimalType.MAX_PRECISION &&
!SqlApiConf.get.allowNegativeScaleOfDecimalEnabled) {
null
} else {
Decimal(bigDecimal)
}
}
```
This gives the same results as the current helper for every input, and an
in-range string never touches the conf.
##########
sql/api/src/main/scala/org/apache/spark/sql/types/Decimal.scala:
##########
@@ -635,26 +635,38 @@ object Decimal {
}
}
- private def numDigitsInIntegralPart(bigDecimal: JavaBigDecimal): Int =
- bigDecimal.precision - bigDecimal.scale
+ // Computed in Long since `precision - scale` can overflow Int, e.g. for
"1e2147483647".
+ private def numDigitsInIntegralPart(bigDecimal: JavaBigDecimal): Long =
+ bigDecimal.precision.toLong - bigDecimal.scale
private def stringToJavaBigDecimal(str: UTF8String): JavaBigDecimal = {
// According the benchmark test, `s.toString.trim` is much faster than
`s.trim.toString`.
// Please refer to https://github.com/apache/spark/pull/26640
new JavaBigDecimal(str.toString.trim)
}
+ /**
+ * Converts a `JavaBigDecimal` parsed from a string to `Decimal`, or returns
null if no decimal
+ * type can hold it. We fast fail because constructing a very large
JavaBigDecimal to Decimal is
+ * very slow. For example: Decimal("6.0790316E+25569151")
+ */
+ private def stringBigDecimalToDecimal(bigDecimal: JavaBigDecimal): Decimal =
{
+ if (SqlApiConf.get.allowNegativeScaleOfDecimalEnabled) {
+ Decimal(bigDecimal)
+ } else if (bigDecimal.signum == 0) {
Review Comment:
The precision that grows with the exponent comes from
`Decimal.set(BigDecimal)` (L150-152, `this._precision = decimal.precision -
decimal.scale`), so the other `BigDecimal` entry points still have it. From the
code, `Literal(new java.math.BigDecimal("0E+39"))` (literals.scala L83-85)
builds `Decimal(d)` with precision 40. `DecimalType.fromDecimal` then throws
`DECIMAL_PRECISION_EXCEEDS_MAX_PRECISION`, while
`Literal(java.math.BigDecimal.ZERO)` works. With `0E+2147483647`, the precision
is `-2147483648`, and `DecimalType` fails with
`DECIMAL_SCALE_EXCEEDS_PRECISION`.
I know SPARK-60132 tracks the other entry points, so this doesn't need to
block this PR. If `set(BigDecimal)` handled zero itself (precision 1 and scale
0 for a zero with a negative scale when negative scales are not allowed), every
entry point would be covered, and this branch would only need to skip the
fast-fail. Could you add the zero case to SPARK-60132 so it isn't lost?
##########
sql/api/src/main/scala/org/apache/spark/sql/types/Decimal.scala:
##########
@@ -635,26 +635,38 @@ object Decimal {
}
}
- private def numDigitsInIntegralPart(bigDecimal: JavaBigDecimal): Int =
- bigDecimal.precision - bigDecimal.scale
+ // Computed in Long since `precision - scale` can overflow Int, e.g. for
"1e2147483647".
+ private def numDigitsInIntegralPart(bigDecimal: JavaBigDecimal): Long =
+ bigDecimal.precision.toLong - bigDecimal.scale
private def stringToJavaBigDecimal(str: UTF8String): JavaBigDecimal = {
// According the benchmark test, `s.toString.trim` is much faster than
`s.trim.toString`.
// Please refer to https://github.com/apache/spark/pull/26640
new JavaBigDecimal(str.toString.trim)
}
+ /**
+ * Converts a `JavaBigDecimal` parsed from a string to `Decimal`, or returns
null if no decimal
+ * type can hold it. We fast fail because constructing a very large
JavaBigDecimal to Decimal is
+ * very slow. For example: Decimal("6.0790316E+25569151")
+ */
+ private def stringBigDecimalToDecimal(bigDecimal: JavaBigDecimal): Decimal =
{
+ if (SqlApiConf.get.allowNegativeScaleOfDecimalEnabled) {
+ Decimal(bigDecimal)
+ } else if (bigDecimal.signum == 0) {
+ // Zero fits any decimal type whatever its exponent, e.g. "0e39". Drop a
negative scale so
+ // that the precision does not grow with the exponent.
Review Comment:
nit: This keeps a non-negative scale as it is, so a zero with a large
negative exponent still gets a precision that grows with the exponent:
```
0e-50: bd.precision=1 bd.scale=50 -> Decimal._precision=50
_scale=50
0e-2147483647: bd.precision=1 bd.scale=2147483647 ->
Decimal._precision=2147483647 _scale=2147483647
```
(I checked this with `java.math.BigDecimal` on JDK 21, mirroring this branch
and `Decimal.set`.) `Cast` still returns `0.00` because `BigDecimal.setScale`
returns a zero right away, so nothing fails. But this comment says the
precision no longer grows with the exponent, and the test comment says
"whatever its exponent", while both only cover positive exponents.
Could you either cap the scale for zero, e.g.
`math.min(math.max(bigDecimal.scale, 0), DecimalType.MAX_SCALE)`, or narrow the
comment to positive exponents?
##########
sql/catalyst/src/test/scala/org/apache/spark/sql/types/DecimalSuite.scala:
##########
@@ -446,6 +446,29 @@ class DecimalSuite extends SparkFunSuite with
PrivateMethodTester with SQLHelper
}
}
+ test("SPARK-60118: UTF8String to Decimal with exponent near Int.MaxValue") {
+ // The number of integral digits (precision - scale) of these values
overflows Int.
+ Seq("1e2147483647", "-1E+2147483647", "12e2147483647",
+ "1.5e2147483647").foreach { string =>
+ assert(Decimal.fromString(UTF8String.fromString(string)) === null)
+ checkError(
+ exception = intercept[SparkArithmeticException](
+ Decimal.fromStringANSI(UTF8String.fromString(string))),
+ condition = "NUMERIC_OUT_OF_SUPPORTED_RANGE",
+ parameters = Map("value" -> string))
+ }
+ // Zero fits any decimal type whatever its exponent.
+ Seq("0e39", "-0E+100", "0e2147483647").foreach { string =>
Review Comment:
The helper checks the legacy conf before the zero branch. So under
`spark.sql.legacy.allowNegativeScaleOfDecimal=true`, a zero such as `'0e39'`
keeps its negative scale (precision 1, scale -39) and is not normalized. I
think that's intended, but no test covers it, so reordering the branches (as in
the suggestion at `Decimal.scala` L654) could change it silently.
How about a legacy block like the one in the SPARK-37451 test above? For
example:
```scala
withSQLConf(SQLConf.LEGACY_ALLOW_NEGATIVE_SCALE_OF_DECIMAL_ENABLED.key
-> "true") {
Seq(Decimal.fromString(UTF8String.fromString("0e39")),
Decimal.fromStringANSI(UTF8String.fromString("0e39"))).foreach { d =>
assert(d === Decimal(0))
assert(d.scale === -39)
}
}
```
Also, the `'0.00'` check at L469 runs only through `fromString`. It could
cover `fromStringANSI` too, like the loop above.
##########
sql/api/src/main/scala/org/apache/spark/sql/types/Decimal.scala:
##########
@@ -635,26 +635,38 @@ object Decimal {
}
}
- private def numDigitsInIntegralPart(bigDecimal: JavaBigDecimal): Int =
- bigDecimal.precision - bigDecimal.scale
+ // Computed in Long since `precision - scale` can overflow Int, e.g. for
"1e2147483647".
+ private def numDigitsInIntegralPart(bigDecimal: JavaBigDecimal): Long =
+ bigDecimal.precision.toLong - bigDecimal.scale
private def stringToJavaBigDecimal(str: UTF8String): JavaBigDecimal = {
// According the benchmark test, `s.toString.trim` is much faster than
`s.trim.toString`.
// Please refer to https://github.com/apache/spark/pull/26640
new JavaBigDecimal(str.toString.trim)
}
+ /**
+ * Converts a `JavaBigDecimal` parsed from a string to `Decimal`, or returns
null if no decimal
Review Comment:
nit: The helper returns `null` only when the integral part has more than
`MAX_PRECISION` digits and negative scales are not allowed. A value whose scale
no decimal type can hold, such as `'1e-50'` (precision 50, scale 50), comes
back as a non-null `Decimal`, and `changePrecision` deals with it later.
How about something like "or returns null if its integral part has more than
`DecimalType.MAX_PRECISION` digits"? Then a new caller that skips
`changePrecision` won't take a non-null result for a valid decimal.
##########
sql/api/src/main/scala/org/apache/spark/sql/types/Decimal.scala:
##########
@@ -635,26 +635,38 @@ object Decimal {
}
}
- private def numDigitsInIntegralPart(bigDecimal: JavaBigDecimal): Int =
- bigDecimal.precision - bigDecimal.scale
+ // Computed in Long since `precision - scale` can overflow Int, e.g. for
"1e2147483647".
+ private def numDigitsInIntegralPart(bigDecimal: JavaBigDecimal): Long =
+ bigDecimal.precision.toLong - bigDecimal.scale
private def stringToJavaBigDecimal(str: UTF8String): JavaBigDecimal = {
// According the benchmark test, `s.toString.trim` is much faster than
`s.trim.toString`.
// Please refer to https://github.com/apache/spark/pull/26640
new JavaBigDecimal(str.toString.trim)
}
+ /**
+ * Converts a `JavaBigDecimal` parsed from a string to `Decimal`, or returns
null if no decimal
+ * type can hold it. We fast fail because constructing a very large
JavaBigDecimal to Decimal is
+ * very slow. For example: Decimal("6.0790316E+25569151")
+ */
+ private def stringBigDecimalToDecimal(bigDecimal: JavaBigDecimal): Decimal =
{
+ if (SqlApiConf.get.allowNegativeScaleOfDecimalEnabled) {
+ Decimal(bigDecimal)
Review Comment:
nit: This branch is the same as the last `else` branch. The conf matters
only for a zero with a negative scale and for an out-of-range value. So the
four branches could be the two conditions in the suggestion at L654, which also
keeps the conf off the common path.
--
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]