Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: modulo op with negative zero divisor produces Nan #585

Open
wants to merge 22 commits into
base: main
Choose a base branch
from

Conversation

vaibhawvipul
Copy link
Contributor

@vaibhawvipul vaibhawvipul commented Jun 18, 2024

Which issue does this PR close?

Closes #521 and #665.

Rationale for this change

What changes are included in this PR?

Improves compatibility with apache spark.

How are these changes tested?

Added a relevant test case.

@vaibhawvipul vaibhawvipul marked this pull request as ready for review June 21, 2024 07:41
Copy link
Contributor

@comphead comphead left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

imho it has to be fixed on DataFusion side. I can see it returns NaN

> select 1 % -0.0;
+------------------------+
| Int64(1) % Float64(-0) |
+------------------------+
| NaN                    |
+------------------------+

PG, Trino fails on such query,
DuckDB, Spark returns NULL

I'll start a ticket in DF, if community supports to go with NULL, so no action needed for this ticket, if they decide to fail the query we might need some custom handling

@comphead
Copy link
Contributor

Filed apache/datafusion#11051 lets see

@vaibhawvipul
Copy link
Contributor Author

imho it has to be fixed on DataFusion side. I can see it returns NaN

> select 1 % -0.0;
+------------------------+
| Int64(1) % Float64(-0) |
+------------------------+
| NaN                    |
+------------------------+

PG, Trino fails on such query, DuckDB, Spark returns NULL

I'll start a ticket in DF, if community supports to go with NULL, so no action needed for this ticket, if they decide to fail the query we might need some custom handling

Make sense. I will keep my PR as it then until we get some resolution on the datafusion ticket.

Thank you @comphead

@kazuyukitanimura
Copy link
Contributor

kazuyukitanimura commented Jun 21, 2024

The direct reason why NaN is produced by dividing by -0.0 is that EqualTo condition is failing in nullIfWhenPrimitive() of QueryPlanSerde

DataFusion changed their behavior several versions ago, to fail for dividing by zero. The DataFusion community recommendation to keep old behavior was to put if condition such as nullIfWhenPrimitive().

I would say we should do something like

if (expression.dataType == DoubleType) {
    If(Or(EqualTo(expression, zero), EqualTo(expression, negZero)), Literal.create(null, expression.dataType), expression)
} else {
    If(EqualTo(expression, zero), Literal.create(null, expression.dataType), expression)
}

If is also supported natively, so it won't be too much overhead (at least that was the case last time I tried). I would hesitate to re-implement whole operator only for negative zero case. I feel we should reuse DataFusion as much as possible

@comphead comphead marked this pull request as draft June 21, 2024 21:48
@vaibhawvipul vaibhawvipul marked this pull request as ready for review June 22, 2024 02:44
@vaibhawvipul
Copy link
Contributor Author

vaibhawvipul commented Jun 22, 2024

The direct reason why NaN is produced by dividing by -0.0 is that EqualTo condition is failing in nullIfWhenPrimitive() of QueryPlanSerde

DataFusion changed their behavior several versions ago, to fail for dividing by zero. The DataFusion community recommendation to keep old behavior was to put if condition such as nullIfWhenPrimitive().

I would say we should do something like

if (expression.dataType == DoubleType) {
    If(Or(EqualTo(expression, zero), EqualTo(expression, negZero)), Literal.create(null, expression.dataType), expression)
} else {
    If(EqualTo(expression, zero), Literal.create(null, expression.dataType), expression)
}

If is also supported natively, so it won't be too much overhead (at least that was the case last time I tried). I would hesitate to re-implement whole operator only for negative zero case. I feel we should reuse DataFusion as much as possible

makes a lot of sense. I have made a fix as per your comments.

please review @kazuyukitanimura

@vaibhawvipul
Copy link
Contributor Author

can someone please re-run the CI? It seems like a task failed because of network error.

cc @kazuyukitanimura @comphead

Copy link
Contributor

@kazuyukitanimura kazuyukitanimura left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @vaibhawvipul

@vaibhawvipul
Copy link
Contributor Author

@kazuyukitanimura ready for review

(-1.0, 0.0),
(0.0, -1.0)),
"t") {
checkSparkAnswerAndOperator("SELECT _1 == _2 FROM t")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may need to test != and <=>as well, but can be a separate PR

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure, I will create a different PR for it.

@vaibhawvipul
Copy link
Contributor Author

@kazuyukitanimura ready for review.

@codecov-commenter
Copy link

codecov-commenter commented Jun 30, 2024

Codecov Report

Attention: Patch coverage is 52.08333% with 23 lines in your changes missing coverage. Please review.

Project coverage is 34.08%. Comparing base (2fc45a2) to head (6e843da).
Report is 20 commits behind head on main.

Files Patch % Lines
.../scala/org/apache/comet/serde/QueryPlanSerde.scala 52.08% 9 Missing and 14 partials ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##               main     #585      +/-   ##
============================================
- Coverage     34.13%   34.08%   -0.06%     
+ Complexity      809      796      -13     
============================================
  Files           106      108       +2     
  Lines         38586    38767     +181     
  Branches       8566     8581      +15     
============================================
+ Hits          13172    13212      +40     
- Misses        22674    22799     +125     
- Partials       2740     2756      +16     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@vaibhawvipul
Copy link
Contributor Author

@kazuyukitanimura could you please review once? CI passed.

@kazuyukitanimura
Copy link
Contributor

@vaibhawvipul Sorry, I got busy with something else. I may be unable to review or work on OSS a week or two.

builder.setRight(rightExpr.get)
val leftExpr =
if (left.dataType == DoubleType || left.dataType == FloatType) {
exprToProtoInternal(If(EqualTo(left, negZeroLeft), leftZero, left), inputs)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm exprToProtoInternal recursively gets executed and EqualTo(left, negZeroLeft) will match the above case-match and gets converted at

              case (_, `negZeroRight`) =>
                 return buildEqualExpr(
                   exprToProtoInternal(left, inputs),
                   exprToProtoInternal(Abs(right).child, inputs))

??

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm still trying to understand how this recursion works...

@vaibhawvipul
Copy link
Contributor Author

@andygrove / @kazuyukitanimura can we please get CI triggered? and also a review?

case (_, `negZeroRight`) =>
return buildEqualExpr(
exprToProtoInternal(left, inputs),
exprToProtoInternal(Abs(right).child, inputs))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mind explaining why we need Abs(right).child? I thought Abs(right).child == right

Comment on lines +1034 to +1035
// also return none if one side is nullable and the other is not
if ((left.nullable && !right.nullable) &&
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the reason of the left.nullable && !right.nullable requirement?

builder.setRight(rightExpr.get)
val leftExpr =
if (left.dataType == DoubleType || left.dataType == FloatType) {
exprToProtoInternal(If(EqualTo(left, negZeroLeft), leftZero, left), inputs)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm still trying to understand how this recursion works...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Modulus can produce incorrect results in some cases
4 participants