Varun Bhandary created SPARK-60202:
--------------------------------------

             Summary: Preserve floating-point boundaries in scaled floor and 
ceil
                 Key: SPARK-60202
                 URL: https://issues.apache.org/jira/browse/SPARK-60202
             Project: Spark
          Issue Type: Bug
          Components: SQL
    Affects Versions: 4.1.2, 4.0.2
            Reporter: Varun Bhandary


h3. Problem

The two-argument {{floor}} and {{ceil}} can return a value on the wrong side of 
their floating-point input. They implicitly convert Double to 
{{decimal(30,15)}} and Float to {{decimal(14,7)}} before applying directional 
rounding. The conversion can round across the very boundary that floor/ceil 
should preserve.

h3. Reproduction

For example, with explicit Double inputs:

{code:sql}
SELECT
  floor(CAST('0.9999999999999999' AS DOUBLE)) AS unary_floor,
  floor(CAST('0.9999999999999999' AS DOUBLE), 0) AS scaled_floor,
  ceil(CAST('0.9000000000000001' AS DOUBLE), 1) AS scaled_ceil;
{code}

The current results are {{(0, 1, 0.9)}}. The expected numerical results are 
{{(0, 0, 1.0)}}. The unary result remains a long and the scaled results remain 
decimals; this report does not propose making their types identical.

|| Explicit Double expression || Current || Expected ||
| {{floor(0.8999999999999999, 1)}} | 0.9 | 0.8 |
| {{floor(1.7999999999999998, 1)}} | 1.8 | 1.7 |
| {{floor(0.9999999999999999, 0)}} | 1 | 0 |
| {{ceil(0.9000000000000001, 1)}} | 0.9 | 1.0 |

This affects directional rounding and interval/bucket boundaries, including 
values obtained from calculations rather than SQL literals. Runtime-column 
regressions reproduce the problem without relying on constant folding.

h3. Cause and proposed correction

The cause is in Catalyst's [RoundFloor and RoundCeil 
implementation|https://github.com/apache/spark/blob/f04e6d60ebf9131fd83f306627697370ef52a309/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/mathExpressions.scala]:
 both require decimal input, so analysis inserts the lossy cast before 
rounding. This is distinct from ordinary binary floating-point representation 
error: the supplied Double values are already on the correct side of the 
boundary, but the intermediate cast moves them across it.

The proposed correction is local to scaled floor/ceil: accept Float/Double 
directly and defer rounding to the requested output scale. Preserve existing 
decimal result precision/scale, explicit user casts, integral/decimal input 
behavior, and established null, non-finite and ANSI overflow behavior. Do not 
change global numeric-to-decimal coercion.

Float results can also change. For example, {{3.1415f}} represents 
{{3.141499996185302734375}}, so floor at scale 4 should produce {{3.1414}}, not 
{{3.1415}}. The proposal does not expand the existing maximum output scales or 
promise exact binary arithmetic for every input; it retains Spark's existing 
Double-to-Decimal conversion convention while removing the premature scale 
reduction.

h3. Related work

The decimal return type is intentional, as discussed in 
[SPARK-38604|https://issues.apache.org/jira/browse/SPARK-38604] and [the 
original scale 
implementation|https://github.com/apache/spark/pull/34729#discussion_r789493919].
 [SPARK-59373|https://issues.apache.org/jira/browse/SPARK-59373] clarified that 
contract without changing behavior. This precision problem is separate from the 
overflow work in 
[SPARK-58052|https://issues.apache.org/jira/browse/SPARK-58052].

h3. Validation

Recorded Catalyst reproductions cover released 4.0.2 and 4.1.2. On master 
{{f04e6d60ebf9131fd83f306627697370ef52a309}}, two new regressions fail before 
the local fix and pass afterward. Four relevant suites pass (242 tests), 
including SQL runtime columns with code generation enabled/disabled and ANSI 
enabled/disabled. Local builds required a session-only Netty 4.2.18 override 
because the configured proxy rejects master's 4.2.19 artifacts as "version too 
new"; validation with the exact upstream dependency set remains outstanding.




--
This message was sent by Atlassian Jira
(v8.20.10#820010)

---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to