Skip to content

[SPARK-28741][SQL]Optional mode: throw exceptions when casting to integers causes overflow #25461

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

Closed
wants to merge 24 commits into from

Conversation

gengliangwang
Copy link
Member

What changes were proposed in this pull request?

To follow ANSI SQL, we should support a configurable mode that throws exceptions when casting to integers causes overflow.
The behavior is similar to https://issues.apache.org/jira/browse/SPARK-26218, which throws exceptions on arithmetical operation overflow.
To unify it, the configuration is renamed from "spark.sql.arithmeticOperations.failOnOverFlow" to "spark.sql.failOnIntegerOverFlow"

How was this patch tested?

Unit test

@SparkQA
Copy link

SparkQA commented Aug 15, 2019

Test build #109151 has finished for PR 25461 at commit ce1e1b5.

  • This patch fails to build.
  • This patch merges cleanly.
  • This patch adds no public classes.

@gengliangwang
Copy link
Member Author

retest this please.

@HyukjinKwon
Copy link
Member

retest this please

@SparkQA
Copy link

SparkQA commented Aug 16, 2019

Test build #109156 has finished for PR 25461 at commit a9e7477.

  • This patch passes all tests.
  • This patch merges cleanly.
  • This patch adds no public classes.

@SparkQA
Copy link

SparkQA commented Aug 16, 2019

Test build #109178 has finished for PR 25461 at commit 0e8e7fb.

  • This patch fails due to an unknown error code, -9.
  • This patch merges cleanly.
  • This patch adds no public classes.

@gengliangwang
Copy link
Member Author

retest this please.

@gengliangwang
Copy link
Member Author

Also cc @cloud-fan @mgaido91

@SparkQA
Copy link

SparkQA commented Aug 16, 2019

Test build #109196 has started for PR 25461 at commit 0e8e7fb.

@SparkQA
Copy link

SparkQA commented Aug 16, 2019

Test build #109193 has finished for PR 25461 at commit 0e8e7fb.

  • This patch passes all tests.
  • This patch merges cleanly.
  • This patch adds no public classes.


private[this] def castDecimalToIntegerCode(
ctx: CodegenContext,
intType: String): CastFunction = {
Copy link
Contributor

Choose a reason for hiding this comment

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

intType? Do you mean inType?

Copy link
Member Author

Choose a reason for hiding this comment

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

It's actually integerType. But the full name makes some code longer than 100 characters.
I can change it if you think it is misleading.

case x: NumericType if failOnIntegerOverflow =>
b =>
val intValue = try {
x.exactNumeric.asInstanceOf[Numeric[Any]].toInt(b)
Copy link
Member

Choose a reason for hiding this comment

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

Why do you cast it into int once?

Copy link
Member Author

Choose a reason for hiding this comment

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

The trait Numeric doesn't have the method toInt. Before this code change, the value is also casted to int.

Copy link
Member

Choose a reason for hiding this comment

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

Ah, I see.

Copy link
Member

Choose a reason for hiding this comment

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

We cannot check the valid value range in a single place instead of the current two checks in line 520 and 525?

Copy link
Member Author

Choose a reason for hiding this comment

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

Well, we can do it by match it case by case. Then the code is a bit long. Casting to short/byte should be minor usage. Also, The previous code also cast to Int before cast to Short.

checkEvaluation(cast(Literal(value, TimestampType), LongType),
Math.floorDiv(value, MICROS_PER_SECOND))
}
checkEvaluation(cast(9223372036854775807.9f, LongType), 9223372036854775807L)
Copy link
Member

Choose a reason for hiding this comment

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

How about doing boundary tests like this?

checkEvaluation(cast(java.lang.Math.nextDown(9223372036854775807.9f), LongType), 9223372036854775807L)
--> non-overflow case
checkEvaluation(cast(java.lang.Math.nextUp(9223372036854775807.9f), LongType), 9223372036854775807L)
--> overflow case

Copy link
Member Author

Choose a reason for hiding this comment

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

Why would we do that?

scala> java.lang.Math.nextDown(9223372036854775807.9D) < 9223372036854775807.9D
res23: Boolean = true

Copy link
Member

Choose a reason for hiding this comment

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

Ah, its ok to do it like this instead;

checkEvaluation(cast(9223372036854775807.9f, LongType), 9223372036854775807L)
--> non-overflow case
checkEvaluation(cast(java.lang.Math.nextUp(9223372036854775807.9f), LongType), 9223372036854775807L)
--> overflow case

What I'm a little worried about is that 9223372036854775807.9f is implicitly truncated (to 9223372036854776000.0f?) by a compiler because it cannot be packed in the float IEEE754 format as you said before. So, IIUC the test is actually the same with cast(9223372036854776000.0f, LongType)?

What I understand is as follows(sorted by values desc) and is this correct?

          IEEE754 continuous float values
------------------------------------------
    overflow case: 9223373136366404000.0f <--- Math.nextUp(9223372036854775807.9f)
non-overflow case: 9223372036854776000.0f <--- 9223372036854775807.9f
non-overflow case: 9223371487098961900.0f <--- Math.nextDown(9223372036854775807.9f)

Copy link
Member Author

Choose a reason for hiding this comment

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

@maropu Yes, I think you are right

Copy link
Member

Choose a reason for hiding this comment

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

Thanks for the check!

@SparkQA
Copy link

SparkQA commented Aug 18, 2019

Test build #109297 has finished for PR 25461 at commit 721c4f2.

  • This patch fails Spark unit tests.
  • This patch merges cleanly.
  • This patch adds no public classes.

@gengliangwang
Copy link
Member Author

retest this please.

@SparkQA
Copy link

SparkQA commented Aug 18, 2019

Test build #109300 has finished for PR 25461 at commit 721c4f2.

  • This patch passes all tests.
  • This patch merges cleanly.
  • This patch adds no public classes.


test("Cast to byte with option FAIL_ON_INTEGER_OVERFLOW enabled") {
withSQLConf(SQLConf.FAIL_ON_INTEGER_OVERFLOW.key -> "true") {
testIntMaxAndMin(ByteType)
Copy link
Contributor

Choose a reason for hiding this comment

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

why we need to test cast int.max +1 to byte? I think it's good enough to test cast byte.max +1 to byte

Copy link
Member Author

@gengliangwang gengliangwang Aug 22, 2019

Choose a reason for hiding this comment

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

I think it is always good to have more test cases here if it doesn't increase the testing time by a few seconds.
For example,
If casting double to byte is implemented as:

val x = doubleValue.toShort
if (x.toByte == x) {
  x.toByte
} else {
  throw new ...
}

We can find that it is wrong with this test case, because

 (Int.MaxValue+1.0).toShort.toByte == (Int.MaxValue+1.0).toShort

is true.


test("Cast to short with option FAIL_ON_INTEGER_OVERFLOW enabled") {
withSQLConf(SQLConf.FAIL_ON_INTEGER_OVERFLOW.key -> "true") {
testIntMaxAndMin(ShortType)
Copy link
Contributor

Choose a reason for hiding this comment

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

ditto

}
checkEvaluation(cast(9223372036854775807.9f, LongType), 9223372036854775807L)
checkEvaluation(cast(-9223372036854775808.9f, LongType), -9223372036854775808L)
checkEvaluation(cast(9223372036854775807.9D, LongType), 9223372036854775807L)
Copy link
Contributor

Choose a reason for hiding this comment

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

how about checkEvaluation(cast(0.9D + Long.Max, LongType), Long.Max)?

@cloud-fan
Copy link
Contributor

LGTM except a few minor comments

@@ -474,8 +477,12 @@ case class Cast(child: Expression, dataType: DataType, timeZoneId: Option[String
buildCast[Boolean](_, b => if (b) 1 else 0)
case DateType =>
buildCast[Int](_, d => null)
case TimestampType if failOnIntegerOverflow =>
Copy link
Contributor

Choose a reason for hiding this comment

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

do we really need this? AFAIK a timestamp cannot overflow, can it?

Copy link
Member Author

Choose a reason for hiding this comment

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

It is possible in theory.

@@ -1182,6 +1233,78 @@ case class Cast(child: Expression, dataType: DataType, timeZoneId: Option[String
(c, evPrim, evNull) => code"$evPrim = $c != 0;"
}

private[this] def castTimestampToIntegerCode(
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
private[this] def castTimestampToIntegerCode(
private[this] def castTimestampToIntegralCode(

Copy link
Member Author

Choose a reason for hiding this comment

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

Integral is adjective. "Integral code" seems weird to me.
We can call it castTimestampToIntegralTypeCode if you insist. I didn't use it because the name is a bit long.

Copy link
Contributor

Choose a reason for hiding this comment

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

well, Integer is a specific data type, so I think this name is misleading...your suggested one is fine to me

(c, evPrim, evNull) => code"$evPrim = $c.to${integralType.capitalize}($failOnIntegerOverflow);"
}

private[this] def castIntegerToIntegerExactCode(integralType: String): CastFunction = {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
private[this] def castIntegerToIntegerExactCode(integralType: String): CastFunction = {
private[this] def castIntegerToIntegralExactCode(integralType: String): CastFunction = {

Copy link
Member

Choose a reason for hiding this comment

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

(oh, I didn't know this functionality, ```suggestion, cool)

(min.toString + typeIndicator, max.toString + typeIndicator)
}

private[this] def castFractionToIntegerExactCode(
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
private[this] def castFractionToIntegerExactCode(
private[this] def castFractionToIntegralExactCode(

* @throws ArithmeticException if checkOverflow is true and
* the decimal too big to fit in Long type.
*/
def toLong(checkOverflow: Boolean): Long = {
Copy link
Contributor

Choose a reason for hiding this comment

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

a lot of duplicated code doing this and an additional function call which may be avoided. Can't we just add a boolean with a default value to the existing functions?

Copy link
Member Author

Choose a reason for hiding this comment

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

Then all the other function calls toLong should be at least add parentheses as toLong(). Since Scala only allows Arity-0 https://docs.scala-lang.org/style/method-invocation.html#arity-0 to omit parentheses.
My two concerns here:

  1. The existing external code calling Decimal.toLong will fail
  2. The usage will be different from the trait Numeric

Copy link
Member Author

Choose a reason for hiding this comment

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

BTW, renaming to toLongExact won't accurate either. For example, toLongExact(1.1) should be false, while we are actually doing rounding in toLong.

Copy link
Contributor

Choose a reason for hiding this comment

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

for 2, anyway, the usage is already different and here we're not in a Numeric-like class. On 1, I am not sure it is a problem. Decimal is Unstable and this patch will go in Spark 3.0, so it is a major release (best place where to put a breaking change!). And the benefit to avoid extra function calls and a lot of duplicated code is worth the change IMHO.

cc @cloud-fan @maropu for their opinion on this too, but I feel quite strong about this.

Copy link
Member Author

Choose a reason for hiding this comment

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

Actually, I am not super comfortable with the code changes in Decimal.scala here.
I did this to address comments in #25461 (comment) .
How about just adding a new method roundToLong. The name is same as
https://github.com/google/guava/blob/master/guava/src/com/google/common/math/DoubleMath.java#L156 . So that we can leave toLong as it is.

Copy link
Contributor

Choose a reason for hiding this comment

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

We can't break public classes just to make it easier to write code in Spark.

Copy link
Contributor

@cloud-fan cloud-fan Aug 23, 2019

Choose a reason for hiding this comment

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

I think we can remove the additional function call by codegen. We can remove def toLong(checkOverflow: Boolean): Long, and in the codegen:

if (nullOnOverFlow) code"decimal.toLong" else code"decimal.roundToLong"

Copy link
Contributor

Choose a reason for hiding this comment

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

+1 for @cloud-fan 's suggestion. At least we remove the extra method call.

import scala.math.Ordering

import org.apache.spark.sql.types.Decimal.DecimalIsConflicted
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: I am wondering about moving it here...

Copy link
Member Author

Choose a reason for hiding this comment

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

Do you mean moving DecimalIsConflicted into numerics.scala? I think it is fine keeping it in Decimal.scala for now.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, I meant that. Not a big deal, just to have everything colocated. We can also do in another PR

@SparkQA
Copy link

SparkQA commented Aug 23, 2019

Test build #109596 has finished for PR 25461 at commit af48226.

  • This patch passes all tests.
  • This patch merges cleanly.
  • This patch adds no public classes.

@SparkQA
Copy link

SparkQA commented Aug 23, 2019

Test build #109601 has finished for PR 25461 at commit 977d509.

  • This patch passes all tests.
  • This patch merges cleanly.
  • This patch adds no public classes.

buildConf("spark.sql.arithmeticOperations.failOnOverFlow")
.doc("If it is set to true, all arithmetic operations on non-decimal fields throw an " +
val FAIL_ON_INTEGER_OVERFLOW =
buildConf("spark.sql.failOnIntegerOverFlow")
Copy link
Member

Choose a reason for hiding this comment

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

failOnIntegerOverFlow -> failOnIntegralTypeOverFlow? To me, Integer is a bit ambiguous.

@@ -258,6 +258,7 @@ case class Cast(child: Expression, dataType: DataType, timeZoneId: Option[String

private lazy val dateFormatter = DateFormatter()
private lazy val timestampFormatter = TimestampFormatter.getFractionFormatter(zoneId)
private val failOnIntegerOverflow = SQLConf.get.failOnIntegralTypeOverflow
Copy link
Contributor

Choose a reason for hiding this comment

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

to be consistent, shall we also rename it to failOnIntegralTypeOverflow?

Copy link
Member

@maropu maropu left a comment

Choose a reason for hiding this comment

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

Looks nice and I have no comment now except for the current left ones.

@SparkQA
Copy link

SparkQA commented Aug 23, 2019

Test build #109617 has finished for PR 25461 at commit f1c64e1.

  • This patch fails due to an unknown error code, -9.
  • This patch merges cleanly.
  • This patch adds no public classes.

@gengliangwang gengliangwang changed the title [SPARK-28741][SQL]Throw exceptions when casting to integers causes overflow [SPARK-28741][SQL]Optional mode: throw exceptions when casting to integers causes overflow Aug 23, 2019
@SparkQA
Copy link

SparkQA commented Aug 23, 2019

Test build #109628 has finished for PR 25461 at commit 3c49a9b.

  • This patch passes all tests.
  • This patch merges cleanly.
  • This patch adds no public classes.

@SparkQA
Copy link

SparkQA commented Aug 23, 2019

Test build #109635 has finished for PR 25461 at commit 86d3e8d.

  • This patch passes all tests.
  • This patch merges cleanly.
  • This patch adds no public classes.

@SparkQA
Copy link

SparkQA commented Aug 23, 2019

Test build #109636 has finished for PR 25461 at commit 9f4002c.

  • This patch passes all tests.
  • This patch merges cleanly.
  • This patch adds no public classes.

@cloud-fan
Copy link
Contributor

thanks, merging to master!

@cloud-fan cloud-fan closed this in 8258660 Aug 23, 2019
@gengliangwang
Copy link
Member Author

@cloud-fan @maropu @mgaido91 Thanks for the review!

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

Successfully merging this pull request may close these issues.

7 participants