-
Notifications
You must be signed in to change notification settings - Fork 29.3k
[SPARK-31710][SQL] Fail casting numeric to timestamp by default #28593
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 65 commits
bb1efa2
1459d5b
88c40fe
8d1deee
df22083
a02c6c7
e61c484
d99cc30
0436611
0a1a6a5
ed80c84
bae99b1
39da7be
39ca87c
0189b91
2c29c5b
fd3efa2
69198c6
c3b3c89
38f6e64
43f9b4e
852d730
2e55be3
7637256
87cada7
13ae816
4abd8d3
b13316f
50cc0cf
22638e4
b1dda01
38f0e76
e478c90
7348917
e50e5cb
2344e1f
c3546eb
c7cf8b9
5f138af
77a339a
3a3b93e
a6b4f74
1b52fd6
293fa9f
9cf87e8
cb21d5d
726371c
8ed4fc4
96c197c
3d1819d
be88f01
fd7815a
e14c4f9
7cb0a54
7ad82da
00ed960
664277e
6d9eecb
b8b2919
eeb0a61
409c821
ec2cf54
3fd6d02
fd677c9
108dfea
47f210c
860500d
4c7ddb4
b43a140
1d5c750
7b26d0c
c7f2a9b
8a5e9be
93b1f63
a6a9bd4
a5b5474
4fecf9b
f995f46
c8d5aa5
b4f4d53
f4556a4
f4da70b
7132fca
7ffcde2
f84caa1
6d56a83
5716f40
64acff3
d26bd34
9545c9f
01f42e6
e29409d
183d418
1764c76
74eb6ca
5f06714
8fe1960
ffa3079
bc4b62c
08aee30
12b4239
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 |
|---|---|---|
|
|
@@ -59,7 +59,8 @@ object Cast { | |
| case (StringType, TimestampType) => true | ||
| case (BooleanType, TimestampType) => true | ||
| case (DateType, TimestampType) => true | ||
| case (_: NumericType, TimestampType) => true | ||
| case (_: NumericType, TimestampType) => | ||
|
GuoPhilipse marked this conversation as resolved.
|
||
| SQLConf.get.getConf(SQLConf.LEGACY_ALLOW_CAST_NUMERIC_TO_TIMESTAMP) | ||
|
|
||
| case (StringType, DateType) => true | ||
| case (TimestampType, DateType) => true | ||
|
|
@@ -266,7 +267,15 @@ abstract class CastBase extends UnaryExpression with TimeZoneAwareExpression wit | |
| TypeCheckResult.TypeCheckSuccess | ||
| } else { | ||
| TypeCheckResult.TypeCheckFailure( | ||
| s"cannot cast ${child.dataType.catalogString} to ${dataType.catalogString}") | ||
| if (child.dataType.isInstanceOf[NumericType] && dataType.isInstanceOf[TimestampType]) { | ||
|
Member
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. btw, we need the check here? I think its okay just to update our migration guide.
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. it's better to let users know why it's wrong and how to fix, in the error message. |
||
| s"cannot cast ${child.dataType.catalogString} to ${dataType.catalogString}," + | ||
| "you can enable the casting by setting " + | ||
| s"${SQLConf.LEGACY_ALLOW_CAST_NUMERIC_TO_TIMESTAMP.key} to true," + | ||
| "but we strongly recommand using function " + | ||
|
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. recommand -> recommend ? |
||
| "TIMESTAMP_SECONDS/TIMESTAMP_MILLIS/TIMESTAMP_MICROS instead." | ||
| } else { | ||
| s"cannot cast ${child.dataType.catalogString} to ${dataType.catalogString}" | ||
| }) | ||
| } | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -411,19 +411,48 @@ abstract class NumberToTimestampBase extends UnaryExpression | |
|
|
||
| protected def upScaleFactor: Long | ||
|
|
||
| override def inputTypes: Seq[AbstractDataType] = Seq(IntegralType) | ||
| override def inputTypes: Seq[AbstractDataType] = Seq(NumericType) | ||
|
|
||
| override def dataType: DataType = TimestampType | ||
|
|
||
| override def nullSafeEval(input: Any): Any = { | ||
| Math.multiplyExact(input.asInstanceOf[Number].longValue(), upScaleFactor) | ||
| child.dataType match { | ||
| case ByteType | ShortType | IntegerType | LongType => | ||
| Math.multiplyExact(input.asInstanceOf[Number].longValue(), upScaleFactor).longValue() | ||
| case DecimalType() => | ||
|
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 shouldn't support these fraction types. Users should add manual cast if they want to convert fraction numbers to timestamp.
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. @cloud-fan i plan to use a legacy config for those test cases who used casting fraction to timestamp,so we can keep compatibility with previous tests. |
||
| (input.asInstanceOf[Decimal].toBigDecimal * upScaleFactor).longValue() | ||
| case FloatType => | ||
| if (input.asInstanceOf[Float].isNaN || input.asInstanceOf[Float].isInfinite) null | ||
| (input.asInstanceOf[Float] * upScaleFactor).longValue() | ||
| case DoubleType => | ||
| if (input.asInstanceOf[Double].isNaN || input.asInstanceOf[Double].isInfinite) null | ||
| (input.asInstanceOf[Double] * upScaleFactor).longValue() | ||
| } | ||
| } | ||
|
|
||
| override protected def doGenCode(ctx: CodegenContext, ev: ExprCode): ExprCode = { | ||
| if (upScaleFactor == 1) { | ||
| defineCodeGen(ctx, ev, c => c) | ||
| } else { | ||
| defineCodeGen(ctx, ev, c => s"java.lang.Math.multiplyExact($c, ${upScaleFactor}L)") | ||
| child.dataType match { | ||
| case ByteType | ShortType | IntegerType | LongType => | ||
| defineCodeGen(ctx, ev, c => | ||
| s"java.lang.Math.multiplyExact($c, ${upScaleFactor}L)") | ||
| case DecimalType() => | ||
| val block = inline"new java.math.BigDecimal($upScaleFactor)" | ||
| defineCodeGen(ctx, ev, c => | ||
| s"($c.toBigDecimal().bigDecimal().multiply($block)).longValue()") | ||
| case FloatType => | ||
| val caf = child.eval().asInstanceOf[Float] | ||
| if (caf.isNaN || caf.isInfinite) ExprCode.forNullValue(LongType) | ||
| else defineCodeGen(ctx, ev, c => | ||
| s"(long)($c * ${upScaleFactor})") | ||
| case DoubleType => | ||
| val cad = child.eval().asInstanceOf[Double] | ||
| if (cad.isNaN || cad.isInfinite) ExprCode.forNullValue(LongType) | ||
| else defineCodeGen(ctx, ev, c => | ||
| s"(long)($c * ${upScaleFactor})") | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
@@ -1642,7 +1671,7 @@ case class ParseToDate(left: Expression, format: Option[Expression], child: Expr | |
|
|
||
| def this(left: Expression, format: Expression) { | ||
| this(left, Option(format), | ||
| Cast(Cast(UnixTimestamp(left, format), TimestampType), DateType)) | ||
| Cast(SecondsToTimestamp(UnixTimestamp(left, format)), DateType)) | ||
| } | ||
|
|
||
| def this(left: Expression) = { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2595,6 +2595,15 @@ object SQLConf { | |
| .checkValue(_ > 0, "The timeout value must be positive") | ||
| .createWithDefault(10L) | ||
|
|
||
| val LEGACY_ALLOW_CAST_NUMERIC_TO_TIMESTAMP = | ||
| buildConf("spark.sql.legacy.allowCastNumericToTimestamp") | ||
| .internal() | ||
| .doc("When true, allow cast numeric to timestamp, but for integral numbers," + | ||
| "Hive treats it as milliseconds, Spark SQL treats n as seconds") | ||
|
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. Do we need to mention hive here ? Can we not just state the spark behavior ? Secondly not sure what is N here ? |
||
| .version("3.1.0") | ||
| .booleanConf | ||
| .createWithDefault(false) | ||
|
|
||
| /** | ||
| * Holds information about keys that have been deprecated. | ||
| * | ||
|
|
@@ -3176,6 +3185,10 @@ class SQLConf extends Serializable with Logging { | |
|
|
||
| def integerGroupingIdEnabled: Boolean = getConf(SQLConf.LEGACY_INTEGER_GROUPING_ID) | ||
|
|
||
| def legacyAllowCastNumericToTimestamp: Boolean = | ||
| getConf(SQLConf.LEGACY_ALLOW_CAST_NUMERIC_TO_TIMESTAMP) | ||
|
|
||
|
|
||
| /** ********************** SQLConf functionality methods ************ */ | ||
|
|
||
| /** Set Spark SQL configuration properties. */ | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -50,7 +50,17 @@ abstract class CastSuiteBase extends SparkFunSuite with ExpressionEvalHelper { | |
| } | ||
|
|
||
| protected def checkNullCast(from: DataType, to: DataType): Unit = { | ||
| checkEvaluation(cast(Literal.create(null, from), to, UTC_OPT), null) | ||
| withSQLConf(SQLConf.LEGACY_ALLOW_CAST_NUMERIC_TO_TIMESTAMP.key -> "true") { | ||
| checkEvaluation(cast(Literal.create(null, from), to, UTC_OPT), null) | ||
| } | ||
|
|
||
| withSQLConf(SQLConf.LEGACY_ALLOW_CAST_NUMERIC_TO_TIMESTAMP.key -> "false") { | ||
|
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 can remove this code block, as we already have a test case for invalid casting numeric to timestamp.
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. Good catch,I have removed it. |
||
| if (from.isInstanceOf[NumericType] && to.isInstanceOf[TimestampType]) { | ||
| assert(!cast(Literal.create(null, from), to, UTC_OPT).resolved) | ||
| } else { | ||
| checkEvaluation(cast(Literal.create(null, from), to, UTC_OPT), null) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| test("null cast") { | ||
|
|
@@ -239,7 +249,13 @@ abstract class CastSuiteBase extends SparkFunSuite with ExpressionEvalHelper { | |
| checkCast(1.5, 1.5f) | ||
| checkCast(1.5, "1.5") | ||
|
|
||
| checkEvaluation(cast(cast(1.toDouble, TimestampType), DoubleType), 1.toDouble) | ||
| withSQLConf(SQLConf.LEGACY_ALLOW_CAST_NUMERIC_TO_TIMESTAMP.key -> "true") { | ||
| checkEvaluation(cast(cast(1.toDouble, TimestampType), DoubleType), 1.toDouble) | ||
| } | ||
|
|
||
| withSQLConf(SQLConf.LEGACY_ALLOW_CAST_NUMERIC_TO_TIMESTAMP.key -> "false") { | ||
| assert(!cast(cast(1.toDouble, TimestampType), DoubleType).resolved) | ||
| } | ||
| } | ||
|
|
||
| test("cast from string") { | ||
|
|
@@ -305,17 +321,34 @@ abstract class CastSuiteBase extends SparkFunSuite with ExpressionEvalHelper { | |
| checkEvaluation(cast(cast(cast(cast( | ||
| cast(cast("5", ByteType), ShortType), IntegerType), FloatType), DoubleType), LongType), | ||
| 5.toLong) | ||
| checkEvaluation( | ||
| cast(cast(cast(cast(cast(cast("5", ByteType), TimestampType), | ||
| DecimalType.SYSTEM_DEFAULT), LongType), StringType), ShortType), | ||
| 5.toShort) | ||
| checkEvaluation( | ||
| cast(cast(cast(cast(cast(cast("5", TimestampType, UTC_OPT), ByteType), | ||
| DecimalType.SYSTEM_DEFAULT), LongType), StringType), ShortType), | ||
| null) | ||
| checkEvaluation(cast(cast(cast(cast(cast(cast("5", DecimalType.SYSTEM_DEFAULT), | ||
| ByteType), TimestampType), LongType), StringType), ShortType), | ||
| 5.toShort) | ||
|
|
||
| withSQLConf(SQLConf.LEGACY_ALLOW_CAST_NUMERIC_TO_TIMESTAMP.key -> "true") { | ||
| checkEvaluation( | ||
| cast(cast(cast(cast(cast(cast("5", ByteType), TimestampType), | ||
| DecimalType.SYSTEM_DEFAULT), LongType), StringType), ShortType), | ||
| 5.toShort) | ||
| checkEvaluation( | ||
| cast(cast(cast(cast(cast(cast("5", TimestampType, UTC_OPT), ByteType), | ||
| DecimalType.SYSTEM_DEFAULT), LongType), StringType), ShortType), | ||
| null) | ||
| checkEvaluation(cast(cast(cast(cast(cast(cast("5", DecimalType.SYSTEM_DEFAULT), | ||
| ByteType), TimestampType), LongType), StringType), ShortType), | ||
| 5.toShort) | ||
| } | ||
|
|
||
| withSQLConf(SQLConf.LEGACY_ALLOW_CAST_NUMERIC_TO_TIMESTAMP.key -> "false") { | ||
| assert(!cast(cast(cast(cast(cast(cast("5", ByteType), TimestampType), | ||
| DecimalType.SYSTEM_DEFAULT), LongType), StringType), ShortType).resolved) | ||
|
|
||
| checkEvaluation( | ||
| cast(cast(cast(cast(cast(cast("5", TimestampType, UTC_OPT), ByteType), | ||
| DecimalType.SYSTEM_DEFAULT), LongType), StringType), ShortType), | ||
| null) | ||
|
|
||
| assert(!cast(cast(cast(cast(cast(cast("5", DecimalType.SYSTEM_DEFAULT), | ||
| ByteType), TimestampType), LongType), StringType), ShortType).resolved) | ||
|
|
||
| } | ||
|
|
||
| checkEvaluation(cast("23", DoubleType), 23d) | ||
| checkEvaluation(cast("23", IntegerType), 23) | ||
|
|
@@ -376,29 +409,46 @@ abstract class CastSuiteBase extends SparkFunSuite with ExpressionEvalHelper { | |
| checkEvaluation(cast(ts, LongType), 15.toLong) | ||
| checkEvaluation(cast(ts, FloatType), 15.003f) | ||
| checkEvaluation(cast(ts, DoubleType), 15.003) | ||
| checkEvaluation(cast(cast(tss, ShortType), TimestampType), | ||
| DateTimeUtils.fromJavaTimestamp(ts) * MILLIS_PER_SECOND) | ||
| checkEvaluation(cast(cast(tss, IntegerType), TimestampType), | ||
| DateTimeUtils.fromJavaTimestamp(ts) * MILLIS_PER_SECOND) | ||
| checkEvaluation(cast(cast(tss, LongType), TimestampType), | ||
| DateTimeUtils.fromJavaTimestamp(ts) * MILLIS_PER_SECOND) | ||
| checkEvaluation( | ||
| cast(cast(millis.toFloat / MILLIS_PER_SECOND, TimestampType), FloatType), | ||
| millis.toFloat / MILLIS_PER_SECOND) | ||
| checkEvaluation( | ||
| cast(cast(millis.toDouble / MILLIS_PER_SECOND, TimestampType), DoubleType), | ||
| millis.toDouble / MILLIS_PER_SECOND) | ||
| checkEvaluation( | ||
| cast(cast(Decimal(1), TimestampType), DecimalType.SYSTEM_DEFAULT), | ||
| Decimal(1)) | ||
|
|
||
| // A test for higher precision than millis | ||
| checkEvaluation(cast(cast(0.000001, TimestampType), DoubleType), 0.000001) | ||
| withSQLConf(SQLConf.LEGACY_ALLOW_CAST_NUMERIC_TO_TIMESTAMP.key -> "true") { | ||
| checkEvaluation(cast(cast(tss, ShortType), TimestampType), | ||
| DateTimeUtils.fromJavaTimestamp(ts) * MILLIS_PER_SECOND) | ||
| checkEvaluation(cast(cast(tss, IntegerType), TimestampType), | ||
| DateTimeUtils.fromJavaTimestamp(ts) * MILLIS_PER_SECOND) | ||
| checkEvaluation(cast(cast(tss, LongType), TimestampType), | ||
| DateTimeUtils.fromJavaTimestamp(ts) * MILLIS_PER_SECOND) | ||
| checkEvaluation( | ||
| cast(cast(millis.toFloat / MILLIS_PER_SECOND, TimestampType), FloatType), | ||
| millis.toFloat / MILLIS_PER_SECOND) | ||
| checkEvaluation( | ||
| cast(cast(millis.toDouble / MILLIS_PER_SECOND, TimestampType), DoubleType), | ||
| millis.toDouble / MILLIS_PER_SECOND) | ||
| checkEvaluation( | ||
| cast(cast(Decimal(1), TimestampType), DecimalType.SYSTEM_DEFAULT), | ||
| Decimal(1)) | ||
|
|
||
| // A test for higher precision than millis | ||
| checkEvaluation(cast(cast(0.000001, TimestampType), DoubleType), 0.000001) | ||
|
|
||
| checkEvaluation(cast(Double.NaN, TimestampType), null) | ||
| checkEvaluation(cast(1.0 / 0.0, TimestampType), null) | ||
| checkEvaluation(cast(Float.NaN, TimestampType), null) | ||
| checkEvaluation(cast(1.0f / 0.0f, TimestampType), null) | ||
| } | ||
|
|
||
| checkEvaluation(cast(Double.NaN, TimestampType), null) | ||
| checkEvaluation(cast(1.0 / 0.0, TimestampType), null) | ||
| checkEvaluation(cast(Float.NaN, TimestampType), null) | ||
| checkEvaluation(cast(1.0f / 0.0f, TimestampType), null) | ||
| withSQLConf(SQLConf.LEGACY_ALLOW_CAST_NUMERIC_TO_TIMESTAMP.key -> "false") { | ||
| assert(!cast(cast(tss, ShortType), TimestampType).resolved) | ||
|
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. can we have a dedicated test case to explicitly test the new cast type check? Then we don't need to repeat the existing test cases with the config set to false.
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. ah we already have. Then let's just remove these duplicated test cases.
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. NIce, have removed the duplicated ones |
||
| assert(!cast(cast(tss, IntegerType), TimestampType).resolved) | ||
| assert(!cast(cast(tss, LongType), TimestampType).resolved) | ||
| assert(!cast(cast(millis.toFloat / MILLIS_PER_SECOND, TimestampType), FloatType).resolved) | ||
| assert(!cast(cast(millis.toDouble / MILLIS_PER_SECOND, TimestampType), DoubleType).resolved) | ||
| assert(!cast(cast(Decimal(1), TimestampType), DecimalType.SYSTEM_DEFAULT).resolved) | ||
| assert(!cast(cast(0.000001, TimestampType), DoubleType).resolved) | ||
| assert(!cast(Double.NaN, TimestampType).resolved) | ||
| assert(!cast(1.0 / 0.0, TimestampType).resolved) | ||
| assert(!cast(Float.NaN, TimestampType).resolved) | ||
| assert(!cast(1.0f / 0.0f, TimestampType).resolved) | ||
| } | ||
| } | ||
|
|
||
| test("cast from array") { | ||
|
|
@@ -1026,8 +1076,16 @@ class CastSuite extends CastSuiteBase { | |
|
|
||
| test("cast from int 2") { | ||
| checkEvaluation(cast(1, LongType), 1.toLong) | ||
| checkEvaluation(cast(cast(1000, TimestampType), LongType), 1000.toLong) | ||
| checkEvaluation(cast(cast(-1200, TimestampType), LongType), -1200.toLong) | ||
|
|
||
| withSQLConf(SQLConf.LEGACY_ALLOW_CAST_NUMERIC_TO_TIMESTAMP.key -> "true") { | ||
| checkEvaluation(cast(cast(1000, TimestampType), LongType), 1000.toLong) | ||
| checkEvaluation(cast(cast(-1200, TimestampType), LongType), -1200.toLong) | ||
| } | ||
|
|
||
| withSQLConf(SQLConf.LEGACY_ALLOW_CAST_NUMERIC_TO_TIMESTAMP.key -> "false") { | ||
| assert(!cast(cast(1000, TimestampType), LongType).resolved) | ||
| assert(!cast(cast(-1200, TimestampType), LongType).resolved) | ||
| } | ||
|
|
||
| checkEvaluation(cast(123, DecimalType.USER_DEFAULT), Decimal(123)) | ||
| checkEvaluation(cast(123, DecimalType(3, 0)), Decimal(123)) | ||
|
|
@@ -1310,6 +1368,20 @@ class CastSuite extends CastSuiteBase { | |
| checkEvaluation(cast(negativeTs, LongType), expectedSecs) | ||
| } | ||
| } | ||
|
|
||
| test("SPARK-31710:fail casting from numeric to timestamp by default") { | ||
| Seq(true, false).foreach { enable => | ||
| withSQLConf(SQLConf.LEGACY_ALLOW_CAST_NUMERIC_TO_TIMESTAMP.key -> enable.toString) { | ||
| assert(cast(2.toByte, TimestampType).resolved == enable) | ||
| assert(cast(10.toShort, TimestampType).resolved == enable) | ||
| assert(cast(3, TimestampType).resolved == enable) | ||
| assert(cast(10L, TimestampType).resolved == enable) | ||
| assert(cast(Decimal(1.2), TimestampType).resolved == enable) | ||
| assert(cast(1.7f, TimestampType).resolved == enable) | ||
| assert(cast(2.3d, TimestampType).resolved == enable) | ||
| } | ||
| } | ||
| } | ||
|
GuoPhilipse marked this conversation as resolved.
|
||
| } | ||
|
|
||
| /** | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.