Repository navigation
feat(spark): support the scale argument in Spark ceil - #24643
dhruvdavest07 wants to merge 1 commit into
Conversation
ceil(value, scale) rounds up to a given number of decimal places. Only the one-argument form was implemented, and the file carried a TODO pointing at this issue. Spark builds the two-argument form as RoundCeil, declared over (DecimalType, IntegerType) with a foldable scale, so the value is a decimal and the scale has to be a constant. The result type comes straight from Spark's RoundBase::dataType: scale < 0 Decimal128(min(max(p - s + 1, -scale + 1), 38), 0) scale >= 0 Decimal128(min(p - s + 1 + min(s, scale), 38), min(s, scale)) The p - s + 1 term is the integral part gaining a digit, since rounding up can carry: ceil(9.9, 0) is 10. Both formulas reproduce the PySpark 3.5.5 results already recorded in ceil.slt, which is where the expected values in the enabled tests come from. Those two cases were commented out with a TODO and are now live. The scale is read through return_field_from_args, since the result type depends on its value rather than just its type. A non-constant or null scale is a planning error, which is what Spark does too. Only decimal values take a scale, matching RoundCeil's declared input types. A scale on any other type is rejected rather than guessed at; Spark casts those to decimal first and that coercion is a separate piece of work. Fixes apache#21560
|
Nice to see this one picked up @dhruvdavest07. I have been working in 1. The scale parser diverges from
Some(ScalarValue::Int32(Some(scale))) => *scale,
...
_ => return plan_err!("Function ceil requires a constant integer scale argument"),So 2. There is approved prior art you may not have seen. #21710 implements the same issue (#21560) and was approved after 11 rounds of review. Its author commented "untake" on 30 August and it has since gone conflicting, so this PR is the live one, but the review feedback on it is worth harvesting rather than rediscovering. Relevant to point 1: #21710 lifted 3. The scale path is decimal-only.
other => return plan_err!("Function ceil does not support a scale argument for {other:?}"),#21710 had For what it is worth, I like the Disclosure: written with AI assistance, reviewed by me before posting. |
|
@dhruvdavest07 Thanks for picking this up, and for the thorough description. I built this branch locally (head 410eaf4) and compared it against PySpark 4.2.0 with ANSI on and off. The normal decimal cases I tested match Spark, values and result types, including negative scales, a scale larger than the input scale, column input and NULLs. The ceil unit tests and ceil.slt pass locally. Note that CI is still awaiting workflow approval. One correctness issue: overflow produces silent wrong results.
Spark raises NUMERIC_VALUE_OUT_OF_RANGE for all three, in both ANSI modes. The cause is the wrapping arithmetic (pow_wrapping in the decimal128_ceil helper, pow_wrapping and mul_wrapping in decimal128_ceil_to_scale) plus no check of the result against the output precision. Could these use checked arithmetic, validate the result, and return an error? Tests for these cases would be great. A smaller gap on the scale argument: it has to be a bare literal. ceiling(x, CAST(3 AS INT)) and ceiling(x, 1+2) fail with "Function ceil requires a constant integer scale argument", but both are foldable constants that Spark accepts. On point 1 of the earlier comment: Spark rejects ceil(x, CAST(2 AS BIGINT)), and TINYINT and NULL scales too, so I would not widen the accepted types. In this PR that example fails because it is a cast, and 3::int fails the same way. On the decimal-only scope, which the description explains: keeping it out of this PR seems right to me. In case it helps the follow-up, Spark's numeric-to-decimal mappings are in DecimalType.forType: Int is decimal(10,0), Long is decimal(20,0), Float is decimal(14,7), Double is decimal(30,15). The results I measured are consistent with that: ceil(5, -1) = 10 decimal(11,0), ceil(CAST(5 AS BIGINT), 1) = 5 decimal(21,0), ceil(3.5D, 0) = 4 decimal(16,0), ceil(CAST(3.14159 AS FLOAT), 2) = 3.15 decimal(10,2). On the "something a reviewer should decide" question: I confirmed ceil(x, 3) is still rejected in the planner on current main, for both CEIL and FLOOR. A follow-up issue covering both seems cleaner than folding planner changes into this PR. Not my call, just one reviewer's view. @comphead this PR targets your #21560 . Could you approve the CI workflows when you get a chance? |
|
Thanks @dhruvdavest07 |
|
Thank you for opening this pull request! Reviewer note: cargo-semver-checks reported the current version number is not SemVer-compatible with the changes in this pull request (compared against the base branch). Details |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #24643 +/- ##
==========================================
- Coverage 81.43% 81.43% -0.01%
==========================================
Files 1118 1118
Lines 399414 399535 +121
Branches 399414 399535 +121
==========================================
+ Hits 325281 325364 +83
- Misses 55142 55176 +34
- Partials 18991 18995 +4 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Marked as draft after 14 days without activity: failing CI checks. Please address these items and mark Ready for review when ready. |
|
Thanks for the contribution! I am closing this under DataFusion's AI Dump policy. This has the hallmarks of AI code that was not reviewed by the author: a very long description with test pass counts, sentence-style test names, and no reply to the review comments left in September. If you would like to continue, please open a focused PR in your own words that you have reviewed and understand, and we are happy to help with review. |
Which issue does this PR close?
Closes #21560.
Rationale for this change
ceil(value, scale)rounds up to a given number of decimal places. Only the one-argumentform was implemented, and
ceil.rscarried a TODO pointing at this issue, with the twomatching cases in
ceil.sltcommented out.Those commented-out cases already carried their PySpark 3.5.5 results, so the expected values
here are recorded Spark output rather than anything I decided.
What changes are included in this PR?
SparkCeilgains a two-argument form.Spark builds it as
RoundCeil, which declaresinputTypes = Seq(DecimalType, IntegerType)and extends
RoundBasewithRoundingMode.CEILING. So the value is a decimal and the scalehas to be a foldable constant. The result type is
RoundBase::dataType:ceil_scaled_precisionandceil_scaled_scaleare that, transcribed. Thep - s + 1term isthe integral part gaining a digit, because rounding up can carry:
ceil(9.9, 0)is10.Checked against the two recorded PySpark results:
ceil(3.1411::decimal(5,4), 3)decimal(5,3)3.142Decimal128(5, 3),3.142ceil(3.1411::decimal(5,4), -3)decimal(4,0)1000Decimal128(4, 0),1000The result type depends on the scale's value, not just its type, so it is read through
return_field_from_argsviascalar_arguments. A non-constant or null scale is a planningerror, which is what Spark reports as
NON_FOLDABLE_INPUT.Only decimal values accept a scale, matching
RoundCeil's declared input types. A scale onany other type is rejected rather than guessed at. Spark casts those to decimal first, and
picking a target precision for that cast is a separate decision I did not want to make
silently.
The one-argument path is untouched.
Are these changes tested?
Yes.
The two previously commented-out
ceil.sltcases are enabled and pass with the recordedPySpark values. Six unit tests in
ceil.rscover the two reference cases, rounding towardpositive infinity for negatives, a scale at or beyond the input scale being a no-op, the carry
case, precision capped at 38, and precision never dropping below 1.
285 tests pass in
datafusion-spark, and all 60 files underspark/mathpass.cargo fmt --checkandcargo clippy --all-targetsare clean on the crate.I ran the enabled
.sltcases against the unmodifiedceil.rsfirst: both fail withFunction 'ceil' expects 1 arguments but received 2, so they are exercising the new path.Something a reviewer should decide
One case in the
.sltis spelledceilingrather thanceil, and that is not cosmetic.sqlparser recognises
CEIL(<expr>, <unsigned literal>)as the SQL-standard scale form andhands it to the planner as
CeilFloorKind::Scale.datafusion/sql/src/expr/mod.rs:619rejectsthat with
CEIL with scale is not supported, before any function is resolved. A negativescale does not match that production, so
ceil(x, -3)reaches the function whileceil(x, 3)does not. The
ceilingalias parses as an ordinary function call either way.So after this PR, in a Spark context:
ceil(x, -3)worksceiling(x, 3)worksceil(x, 3)still errors in the plannerRouting
CeilFloorKind::Scaleto a two-argumentceilcall would fix the asymmetry, but itchanges core SQL planning for every session, and in plain DataFusion
ceiltakes one argument,so the error would become a signature mismatch rather than today's clearer message. That is a
call for maintainers rather than something to slip into this PR, and
FLOORhas the sameshape. Happy to send it as a follow-up if you want it, or to fold it in here if you would
rather it land together.