Repository navigation
Conversation
… result is known from the magnitude
### What changes were proposed in this pull request?
In the BigDecimal branch of `Decimal.changePrecision`, when the scale change exceeds `DecimalType.MAX_PRECISION`, the value's number of integral digits (`precision - scale`, computed in Long) is checked before calling `setScale`:
- If the value is nonzero and has more integral digits than the target type allows, return `false` (overflow) directly. Rounding cannot bring it back into range.
- If the value is below 0.01 ulp of the target scale, the result depends only on the sign and the rounding mode, so a same-sign value of 0.01 ulp is rescaled instead. This gives the same result for every rounding mode.
Otherwise the remaining scale change is bounded by the number of digits of the value, so `setScale` is unchanged. The extra check costs one Int comparison when the scale change is small, which is the common case.
The Long branch of `changePrecision` now computes the scale change in Long. With negative scales allowed it can exceed the Int range and wrap, so `POW_10(diff)` threw `ArrayIndexOutOfBoundsException`, e.g. for a compact `1.00` changed to `DECIMAL(1, -2147483647)`. It now takes the existing rounding path or switches to BigDecimal.
`NUMERIC_VALUE_OUT_OF_RANGE.WITH_SUGGESTION` now renders a decimal whose scale is below -1000 (only possible under `spark.sql.legacy.allowNegativeScaleOfDecimal`) with `toString` instead of `toPlainString`, since the plain form of e.g. `1E+2147483647` does not fit in a String. Other values are rendered as before. `QueryExecutionErrors.cannotChangeDecimalPrecisionError`, an identical copy, now delegates to `DataTypeErrors.cannotChangeDecimalPrecisionError`.
### Why are the changes needed?
`setScale` takes time that grows with the scale change, and throws once 10^change exceeds BigInteger's range or the change exceeds the Int range:
```sql
SELECT CAST('1e-2147483647' AS DECIMAL(10,2));
-- java.lang.ArithmeticException: BigInteger would overflow supported range (LEGACY, ANSI and TRY)
SELECT CAST('1e-100000000' AS DECIMAL(10,2));
-- 0.00, after ~40s
SET spark.sql.legacy.allowNegativeScaleOfDecimal=true;
SELECT CAST('1e2147483647' AS DECIMAL(10,2));
-- java.lang.ArithmeticException: Underflow
```
This is the small-side counterpart of the fast-fail added in SPARK-35841/SPARK-37451. With negative scales allowed, `Decimal.fromString` intentionally skips that fast-fail, so huge values reach `changePrecision` too.
### Does this PR introduce _any_ user-facing change?
Yes.
- Casting a string whose value is far below the target scale's ulp now returns the rounded value (e.g. 0.00) instead of throwing `ArithmeticException`, and returns quickly.
- With `spark.sql.legacy.allowNegativeScaleOfDecimal=true`, casting a string with a huge exponent returns NULL (non-ANSI, try_cast) or throws `NUMERIC_VALUE_OUT_OF_RANGE` (ANSI) instead of `ArithmeticException: Underflow`. In that error, a value whose scale is below -1000 (e.g. `1E+2147483647`) is shown in scientific notation, since its plain form is too long to build. Other values are shown as before.
- With `spark.sql.legacy.allowNegativeScaleOfDecimal=true`, changing a decimal stored as a long (precision up to 18, e.g. after an `UnsafeRow` round trip) by a scale change beyond the Int range no longer throws `ArrayIndexOutOfBoundsException`, and gives the same result as the same value stored as a BigDecimal.
No input that previously produced a value changes its result.
### How was this patch tested?
New tests in `DecimalSuite`, `CastSuiteBase` (so they run in `CastWithAnsiOffSuite`, `CastWithAnsiOnSuite` and `TryCastSuite`), `CastWithAnsiOffSuite` and `CastWithAnsiOnSuite`, covering interpreted and codegen evaluation. All of them fail without the fix. The ANSI test also checks the `value` parameter of the error, in scientific notation for `1e2147483647` and in plain notation for `1e40`. `DecimalSuite` also checks that compact (long) inputs, including scale changes beyond the Int range, round like the same values in BigDecimal form, and that values around the bounds of both shortcuts, with short and 81-digit mantissas, under every supported rounding mode and with and without negative scales, round exactly like `BigDecimal.setScale`.
### Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Claude Opus 5.5)
This pull request and its description were written by Isaac.
Closes apache#59335 from viirya/SPARK-60119.
Authored-by: Liang-Chi Hsieh <[email protected]>
Signed-off-by: Dongjoon Hyun <[email protected]>
(cherry picked from commit 9c3a51f)
HyukjinKwon
approved these changes
Oct 16, 2026
viirya
added a commit
that referenced
this pull request
Oct 16, 2026
…n the result is known from the magnitude ### What changes were proposed in this pull request? Backport of #59335 to branch-4.0. The cherry-pick conflicted only on `CastUtils.java`, which does not exist on this branch: `Cast` calls `QueryExecutionErrors.cannotChangeDecimalPrecisionError` directly in both the interpreted and generated code. So instead of removing that method and switching `CastUtils`, this backport keeps it and makes it delegate to `DataTypeErrors.cannotChangeDecimalPrecisionError`, which gives the same error message. The rest of the change is identical to #59335. In the BigDecimal branch of `Decimal.changePrecision`, when the scale change exceeds `DecimalType.MAX_PRECISION`, the value's number of integral digits (`precision - scale`, computed in Long) is checked before calling `setScale`: - If the value is nonzero and has more integral digits than the target type allows, return `false` (overflow) directly. Rounding cannot bring it back into range. - If the value is below 0.01 ulp of the target scale, the result depends only on the sign and the rounding mode, so a same-sign value of 0.01 ulp is rescaled instead. This gives the same result for every rounding mode. The Long branch of `changePrecision` now computes the scale change in Long, so it no longer wraps and throws `ArrayIndexOutOfBoundsException` from `POW_10(diff)` when negative scales are allowed. `NUMERIC_VALUE_OUT_OF_RANGE.WITH_SUGGESTION` now renders a decimal whose scale is below -1000 (only possible under `spark.sql.legacy.allowNegativeScaleOfDecimal`) with `toString` instead of `toPlainString`, since the plain form of e.g. `1E+2147483647` does not fit in a String. Other values are rendered as before. `QueryExecutionErrors.cannotChangeDecimalPrecisionError`, an identical copy, now delegates to `DataTypeErrors.cannotChangeDecimalPrecisionError`. ### Why are the changes needed? `setScale` takes time that grows with the scale change, and throws once 10^change exceeds BigInteger's range or the change exceeds the Int range: ```sql SELECT CAST('1e-2147483647' AS DECIMAL(10,2)); -- java.lang.ArithmeticException: BigInteger would overflow supported range (LEGACY, ANSI and TRY) SELECT CAST('1e-100000000' AS DECIMAL(10,2)); -- 0.00, after ~40s SET spark.sql.legacy.allowNegativeScaleOfDecimal=true; SELECT CAST('1e2147483647' AS DECIMAL(10,2)); -- java.lang.ArithmeticException: Underflow ``` This branch already has the related fix SPARK-60118, so backporting this keeps decimal casts consistent with master. ### Does this PR introduce _any_ user-facing change? Yes, the same as #59335: - Casting a string whose value is far below the target scale's ulp now returns the rounded value (e.g. 0.00) instead of throwing `ArithmeticException`, and returns quickly. - With `spark.sql.legacy.allowNegativeScaleOfDecimal=true`, casting a string with a huge exponent returns NULL (non-ANSI, try_cast) or throws `NUMERIC_VALUE_OUT_OF_RANGE` (ANSI) instead of `ArithmeticException: Underflow`, and a decimal stored as a long no longer throws `ArrayIndexOutOfBoundsException` for a scale change beyond the Int range. In that error, a value whose scale is below -1000 is shown in scientific notation. No input that previously produced a value changes its result. ### How was this patch tested? The tests from #59335 in `DecimalSuite`, `CastSuiteBase`, `CastWithAnsiOnSuite` and `CastWithAnsiOffSuite` (also run through `TryCastSuite`). On this branch with JDK 17, {'4.3': '512 tests across `DecimalSuite`, `CastWithAnsiOnSuite`, `CastWithAnsiOffSuite`, `TryCastSuite`, `DecimalPrecisionSuite`, `DecimalExpressionSuite` and `MathExpressionsSuite`, plus `UdfSerializationSuite` (16 tests)', '4.2': '410 tests across `DecimalSuite`, `CastWithAnsiOnSuite`, `CastWithAnsiOffSuite`, `TryCastSuite`, `DecimalPrecisionSuite`, `DecimalExpressionSuite` and `MathExpressionsSuite`', '4.1': '403 tests across `DecimalSuite`, `CastWithAnsiOnSuite`, `CastWithAnsiOffSuite`, `TryCastSuite`, `DecimalPrecisionSuite`, `DecimalExpressionSuite` and `MathExpressionsSuite`', '4.0': '354 tests across `DecimalSuite`, `CastWithAnsiOnSuite`, `CastWithAnsiOffSuite`, `TryCastSuite`, `DecimalPrecisionSuite`, `DecimalExpressionSuite` and `MathExpressionsSuite`'} pass, and scalastyle (`sql-api`, `catalyst`, `catalyst` tests) and the `sql/api` scalafmt check pass. ### Was this patch authored or co-authored using generative AI tooling? Generated-by: Claude Code (Claude Opus 5.5) Closes #59515 from viirya/SPARK-60119-4.0. Authored-by: Liang-Chi Hsieh <[email protected]> Signed-off-by: Liang-Chi Hsieh <[email protected]>
Member
Author
|
Merge Summary:
Posted by |
Member
Author
|
Thanks @HyukjinKwon ! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changes were proposed in this pull request?
Backport of #59335 to branch-4.0.
The cherry-pick conflicted only on
CastUtils.java, which does not exist on this branch:CastcallsQueryExecutionErrors.cannotChangeDecimalPrecisionErrordirectly in both the interpreted and generated code. So instead of removing that method and switchingCastUtils, this backport keeps it and makes it delegate toDataTypeErrors.cannotChangeDecimalPrecisionError, which gives the same error message. The rest of the change is identical to #59335.In the BigDecimal branch of
Decimal.changePrecision, when the scale change exceedsDecimalType.MAX_PRECISION, the value's number of integral digits (precision - scale, computed in Long) is checked before callingsetScale:false(overflow) directly. Rounding cannot bring it back into range.The Long branch of
changePrecisionnow computes the scale change in Long, so it no longer wraps and throwsArrayIndexOutOfBoundsExceptionfromPOW_10(diff)when negative scales are allowed.NUMERIC_VALUE_OUT_OF_RANGE.WITH_SUGGESTIONnow renders a decimal whose scale is below -1000 (only possible underspark.sql.legacy.allowNegativeScaleOfDecimal) withtoStringinstead oftoPlainString, since the plain form of e.g.1E+2147483647does not fit in a String. Other values are rendered as before.QueryExecutionErrors.cannotChangeDecimalPrecisionError, an identical copy, now delegates toDataTypeErrors.cannotChangeDecimalPrecisionError.Why are the changes needed?
setScaletakes time that grows with the scale change, and throws once 10^change exceeds BigInteger's range or the change exceeds the Int range:This branch already has the related fix SPARK-60118, so backporting this keeps decimal casts consistent with master.
Does this PR introduce any user-facing change?
Yes, the same as #59335:
ArithmeticException, and returns quickly.spark.sql.legacy.allowNegativeScaleOfDecimal=true, casting a string with a huge exponent returns NULL (non-ANSI, try_cast) or throwsNUMERIC_VALUE_OUT_OF_RANGE(ANSI) instead ofArithmeticException: Underflow, and a decimal stored as a long no longer throwsArrayIndexOutOfBoundsExceptionfor a scale change beyond the Int range. In that error, a value whose scale is below -1000 is shown in scientific notation.No input that previously produced a value changes its result.
How was this patch tested?
The tests from #59335 in
DecimalSuite,CastSuiteBase,CastWithAnsiOnSuiteandCastWithAnsiOffSuite(also run throughTryCastSuite). On this branch with JDK 17, {'4.3': '512 tests acrossDecimalSuite,CastWithAnsiOnSuite,CastWithAnsiOffSuite,TryCastSuite,DecimalPrecisionSuite,DecimalExpressionSuiteandMathExpressionsSuite, plusUdfSerializationSuite(16 tests)', '4.2': '410 tests acrossDecimalSuite,CastWithAnsiOnSuite,CastWithAnsiOffSuite,TryCastSuite,DecimalPrecisionSuite,DecimalExpressionSuiteandMathExpressionsSuite', '4.1': '403 tests acrossDecimalSuite,CastWithAnsiOnSuite,CastWithAnsiOffSuite,TryCastSuite,DecimalPrecisionSuite,DecimalExpressionSuiteandMathExpressionsSuite', '4.0': '354 tests acrossDecimalSuite,CastWithAnsiOnSuite,CastWithAnsiOffSuite,TryCastSuite,DecimalPrecisionSuite,DecimalExpressionSuiteandMathExpressionsSuite'} pass, and scalastyle (sql-api,catalyst,catalysttests) and thesql/apiscalafmt check pass.Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Claude Opus 5.5)