-
Notifications
You must be signed in to change notification settings - Fork 29.3k
[SPARK-39470][SQL] Support cast of ANSI intervals to decimals #36857
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
Changes from 5 commits
ade9160
d992ca5
3160dd5
679571d
d6fc899
77818ff
90fb367
654b2d3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -31,7 +31,7 @@ import org.apache.spark.sql.catalyst.util._ | |
| import org.apache.spark.sql.catalyst.util.DateTimeConstants._ | ||
| import org.apache.spark.sql.catalyst.util.DateTimeUtils._ | ||
| import org.apache.spark.sql.catalyst.util.IntervalStringStyles.ANSI_STYLE | ||
| import org.apache.spark.sql.catalyst.util.IntervalUtils.{dayTimeIntervalToByte, dayTimeIntervalToInt, dayTimeIntervalToLong, dayTimeIntervalToShort, yearMonthIntervalToByte, yearMonthIntervalToInt, yearMonthIntervalToShort} | ||
| import org.apache.spark.sql.catalyst.util.IntervalUtils.{dayTimeIntervalToByte, dayTimeIntervalToDecimal, dayTimeIntervalToInt, dayTimeIntervalToLong, dayTimeIntervalToShort, yearMonthIntervalToByte, yearMonthIntervalToInt, yearMonthIntervalToShort} | ||
| import org.apache.spark.sql.errors.QueryExecutionErrors | ||
| import org.apache.spark.sql.internal.SQLConf | ||
| import org.apache.spark.sql.types._ | ||
|
|
@@ -110,7 +110,7 @@ object Cast { | |
| case (StringType, _: CalendarIntervalType) => true | ||
| case (StringType, _: AnsiIntervalType) => true | ||
|
|
||
| case (_: AnsiIntervalType, _: IntegralType) => true | ||
| case (_: AnsiIntervalType, _: IntegralType | _: DecimalType) => true | ||
|
|
||
| case (_: DayTimeIntervalType, _: DayTimeIntervalType) => true | ||
| case (_: YearMonthIntervalType, _: YearMonthIntervalType) => true | ||
|
|
@@ -194,8 +194,7 @@ object Cast { | |
|
|
||
| case (_: DayTimeIntervalType, _: DayTimeIntervalType) => true | ||
| case (_: YearMonthIntervalType, _: YearMonthIntervalType) => true | ||
| case (_: DayTimeIntervalType, _: IntegralType) => true | ||
| case (_: YearMonthIntervalType, _: IntegralType) => true | ||
| case (_: AnsiIntervalType, _: IntegralType | _: DecimalType) => true | ||
|
|
||
| case (StringType, _: NumericType) => true | ||
| case (BooleanType, _: NumericType) => true | ||
|
|
@@ -1015,6 +1014,11 @@ case class Cast( | |
| } catch { | ||
| case _: NumberFormatException => null | ||
| } | ||
| case x: DayTimeIntervalType => | ||
| buildCast[Long](_, dt => changePrecision(dayTimeIntervalToDecimal(dt, x.endField), target)) | ||
| case x: YearMonthIntervalType => | ||
| buildCast[Int](_, ym => | ||
| changePrecision(Decimal(yearMonthIntervalToInt(ym, x.startField, x.endField)), target)) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. shall we consider ansi mode here? what if the decimal precision is too small, e.g.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As we did before, we don't consider non-ANSI mode for ANSI intervals. So, we are in ANSI mode always for ANSI intervals.
I think
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yea let's add some negative tests
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added a test
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I will take confusion on what is scale and what is precision to my grave.... We can round/truncate, but not modulo/wrap. PS: I loathe that cannot-change-decimal-precision error. In most cases we should just call it an overflow.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I plan to do that.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Though, changing of scale/precision while keeping the same value is different thing from changing the value and get overflow. In any case, I am not going to change decimal logic (errors) here in the PR. |
||
| } | ||
|
|
||
| // DoubleConverter | ||
|
|
@@ -1605,6 +1609,22 @@ case class Cast( | |
| $evNull = true; | ||
| } | ||
| """ | ||
| case x: DayTimeIntervalType => | ||
| (c, evPrim, evNull) => | ||
| val u = IntervalUtils.getClass.getCanonicalName.stripSuffix("$") | ||
| code""" | ||
| Decimal $tmp = $u.dayTimeIntervalToDecimal($c, (byte)${x.endField}); | ||
| ${changePrecision(tmp, target, evPrim, evNull, canNullSafeCast, ctx)} | ||
| """ | ||
| case x: YearMonthIntervalType => | ||
| (c, evPrim, evNull) => | ||
| val u = IntervalUtils.getClass.getCanonicalName.stripSuffix("$") | ||
| val tmpYm = ctx.freshVariable("tmpYm", classOf[Int]) | ||
| code""" | ||
| int $tmpYm = $u.yearMonthIntervalToInt($c, (byte)${x.startField}, (byte)${x.endField}); | ||
| Decimal $tmp = Decimal.apply($tmpYm); | ||
| ${changePrecision(tmp, target, evPrim, evNull, canNullSafeCast, ctx)} | ||
| """ | ||
| } | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1272,4 +1272,37 @@ abstract class CastSuiteBase extends SparkFunSuite with ExpressionEvalHelper { | |
| "to restore the behavior before Spark 3.0.")) | ||
| } | ||
| } | ||
|
|
||
| test("cast ANSI intervals to decimals") { | ||
| Seq( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need examples with rounding:
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, we don't round and don't loose info. See the last check that I added: Both of your cases are the same, actually - total number of digits is greater than 3 or 4.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is different. My examples test sucess. Yours tests a failure. Obviously any number>= 10 and <100 cannot be even approximated in a single digit. |
||
| (Duration.ZERO, DayTimeIntervalType(DAY), DecimalType(10, 3)) -> Decimal(0, 10, 3), | ||
| (Duration.ofHours(-1), DayTimeIntervalType(HOUR), DecimalType(10, 1)) -> Decimal(-10, 10, 1), | ||
| (Duration.ofMinutes(1), DayTimeIntervalType(MINUTE), DecimalType(8, 2)) -> Decimal(100, 8, 2), | ||
| (Duration.ofSeconds(59), DayTimeIntervalType(SECOND), DecimalType(6, 0)) -> Decimal(59, 6, 0), | ||
| (Duration.ofSeconds(-60).minusMillis(1), DayTimeIntervalType(SECOND), | ||
| DecimalType(10, 3)) -> Decimal(-60.001, 10, 3), | ||
| (Duration.ZERO, DayTimeIntervalType(DAY, SECOND), DecimalType(10, 6)) -> Decimal(0, 10, 6), | ||
| (Duration.ofHours(-23).minusMinutes(59).minusSeconds(59).minusNanos(123456000), | ||
| DayTimeIntervalType(HOUR, SECOND), DecimalType(18, 6)) -> Decimal(-86399.123456, 18, 6), | ||
| (Period.ZERO, YearMonthIntervalType(YEAR), DecimalType(5, 2)) -> Decimal(0, 5, 2), | ||
| (Period.ofMonths(-1), YearMonthIntervalType(MONTH), | ||
| DecimalType(8, 0)) -> Decimal(-1, 8, 0), | ||
| (Period.ofYears(-1).minusMonths(1), YearMonthIntervalType(YEAR, MONTH), | ||
| DecimalType(8, 3)) -> Decimal(-13000, 8, 3) | ||
| ).foreach { case ((duration, intervalType, targetType), expected) => | ||
| checkEvaluation( | ||
| Cast(Literal.create(duration, intervalType), targetType), | ||
| expected) | ||
| } | ||
|
|
||
| dayTimeIntervalTypes.foreach { it => | ||
| checkConsistencyBetweenInterpretedAndCodegen((child: Expression) => | ||
| Cast(child, DecimalType.USER_DEFAULT), it) | ||
| } | ||
|
|
||
| yearMonthIntervalTypes.foreach { it => | ||
| checkConsistencyBetweenInterpretedAndCodegen((child: Expression) => | ||
| Cast(child, DecimalType.USER_DEFAULT), it) | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@MaxGekk as you said I think it makes sense to always go with ANSI behavior in new features. Does
changePrecisiontake a bool flag likenullOnOverflow?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, it checks the SQL config actually, passed to the
Castexpression:Probably, it makes sense to incapsulate the logic:
spark/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/Cast.scala
Lines 993 to 1000 in a4f96af
Also cc @gengliangwang
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah always go with ANSI behavior in new features. Either adding a new parameter on
changePrecisionor adding a new method are fine.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's add a new parameter then.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have added new parameter already.