Skip to content

Commit a4ffd00

Browse files
committed
do not allow leading 'interval' in the interval literal string
1 parent 8cf76f8 commit a4ffd00

File tree

16 files changed

+246
-200
lines changed

16 files changed

+246
-200
lines changed

common/unsafe/src/main/java/org/apache/spark/unsafe/types/CalendarInterval.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ public int hashCode() {
8181

8282
@Override
8383
public String toString() {
84-
StringBuilder sb = new StringBuilder("interval");
84+
StringBuilder sb = new StringBuilder();
8585

8686
if (months != 0) {
8787
appendUnit(sb, months / 12, "year");
@@ -98,18 +98,19 @@ public String toString() {
9898
rest %= MICROS_PER_MINUTE;
9999
if (rest != 0) {
100100
String s = BigDecimal.valueOf(rest, 6).stripTrailingZeros().toPlainString();
101-
sb.append(' ').append(s).append(" seconds");
101+
sb.append(s).append(" seconds ");
102102
}
103103
} else if (months == 0 && days == 0) {
104-
sb.append(" 0 microseconds");
104+
sb.append("0 seconds ");
105105
}
106106

107+
sb.setLength(sb.length() - 1);
107108
return sb.toString();
108109
}
109110

110111
private void appendUnit(StringBuilder sb, long value, String unit) {
111112
if (value != 0) {
112-
sb.append(' ').append(value).append(' ').append(unit).append('s');
113+
sb.append(value).append(' ').append(unit).append('s').append(' ');
113114
}
114115
}
115116
}

docs/sql-migration-guide.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,8 @@ license: |
220220

221221
- Since Spark 3.0, the interval literal syntax does not allow multiple from-to units anymore. For example, `SELECT INTERVAL '1-1' YEAR TO MONTH '2-2' YEAR TO MONTH'` throws parser exception.
222222

223+
- Since Spark 3.0, when casting string to interval type, strings with leading "interval" such as "interval 1 day" will be treated as invalid and the cast returns null. In Spark version 2.4 and earlier, the leading "interval" is allowed and required. To allow the leading "interval", you can set `spark.sql.legacy.allowIntervalPrefixInCast` to true.
224+
223225
## Upgrading from Spark SQL 2.4 to 2.4.1
224226

225227
- The value of `spark.executor.heartbeatInterval`, when specified without units like "30" rather than "30s", was

sql/catalyst/src/main/antlr4/org/apache/spark/sql/catalyst/parser/SqlBase.g4

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ singleTableSchema
8080
;
8181

8282
singleInterval
83-
: INTERVAL? multiUnitsInterval EOF
83+
: multiUnitsInterval EOF
8484
;
8585

8686
statement

sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/Cast.scala

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,9 @@ abstract class CastBase extends UnaryExpression with TimeZoneAwareExpression wit
465465

466466
// IntervalConverter
467467
private[this] def castToInterval(from: DataType): Any => Any = from match {
468+
case StringType if SQLConf.get.getConf(SQLConf.LEGACY_ALLOW_INTERVAL_PREFIX_IN_CAST) =>
469+
buildCast[UTF8String](_, s => IntervalUtils.legacyCastStringToInterval(s.toString))
470+
468471
case StringType =>
469472
buildCast[UTF8String](_, s => IntervalUtils.safeFromString(s.toString))
470473
}
@@ -1214,10 +1217,15 @@ abstract class CastBase extends UnaryExpression with TimeZoneAwareExpression wit
12141217
private[this] def castToIntervalCode(from: DataType): CastFunction = from match {
12151218
case StringType =>
12161219
val util = IntervalUtils.getClass.getCanonicalName.stripSuffix("$")
1220+
val func = if (SQLConf.get.getConf(SQLConf.LEGACY_ALLOW_INTERVAL_PREFIX_IN_CAST)) {
1221+
"legacyCastStringToInterval"
1222+
} else {
1223+
"safeFromString"
1224+
}
12171225
(c, evPrim, evNull) =>
1218-
code"""$evPrim = $util.safeFromString($c.toString());
1219-
if(${evPrim} == null) {
1220-
${evNull} = true;
1226+
code"""$evPrim = $util.$func($c.toString());
1227+
if($evPrim == null) {
1228+
$evNull = true;
12211229
}
12221230
""".stripMargin
12231231

sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/util/IntervalUtils.scala

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,4 +365,13 @@ object IntervalUtils {
365365
def isNegative(interval: CalendarInterval, daysPerMonth: Int = 31): Boolean = {
366366
getDuration(interval, TimeUnit.MICROSECONDS, daysPerMonth) < 0
367367
}
368+
369+
def legacyCastStringToInterval(str: String): CalendarInterval = {
370+
val trimmed = str.trim
371+
if (trimmed.regionMatches(true, 0, "interval", 0, 8)) {
372+
safeFromString(trimmed.drop(8))
373+
} else {
374+
safeFromString(trimmed)
375+
}
376+
}
368377
}

sql/catalyst/src/main/scala/org/apache/spark/sql/internal/SQLConf.scala

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2042,6 +2042,14 @@ object SQLConf {
20422042
.booleanConf
20432043
.createWithDefault(true)
20442044

2045+
val LEGACY_ALLOW_INTERVAL_PREFIX_IN_CAST =
2046+
buildConf("spark.sql.legacy.allowIntervalPrefixInCast")
2047+
.doc("When true, it's allowed to have the 'interval' prefix in the string that is being " +
2048+
"casted to interval type. For example, `CAST('interval 1 day' AS INTERVAL)` returns " +
2049+
"null if this config is set to false.")
2050+
.booleanConf
2051+
.createWithDefault(false)
2052+
20452053
val ADDITIONAL_REMOTE_REPOSITORIES =
20462054
buildConf("spark.sql.additionalRemoteRepositories")
20472055
.doc("A comma-delimited string config of the optional additional remote Maven mirror " +

sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/CastSuite.scala

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -664,14 +664,21 @@ abstract class CastSuiteBase extends SparkFunSuite with ExpressionEvalHelper {
664664
import org.apache.spark.unsafe.types.CalendarInterval
665665

666666
checkEvaluation(Cast(Literal(""), CalendarIntervalType), null)
667-
checkEvaluation(Cast(Literal("interval -3 month 1 day 7 hours"), CalendarIntervalType),
667+
checkEvaluation(Cast(Literal("-3 month 1 day 7 hours"), CalendarIntervalType),
668668
new CalendarInterval(-3, 1, 7 * CalendarInterval.MICROS_PER_HOUR))
669+
checkEvaluation(Cast(Literal("interval -3 month 1 day 7 hours"), CalendarIntervalType), null)
670+
671+
withSQLConf(SQLConf.LEGACY_ALLOW_INTERVAL_PREFIX_IN_CAST.key -> "true") {
672+
checkEvaluation(Cast(Literal("interval -3 month 1 day 7 hours"), CalendarIntervalType),
673+
new CalendarInterval(-3, 1, 7 * CalendarInterval.MICROS_PER_HOUR))
674+
checkEvaluation(Cast(Literal("INTERVAL 1 Second 1 microsecond"), CalendarIntervalType),
675+
new CalendarInterval(0, 0, 1000001))
676+
}
677+
669678
checkEvaluation(Cast(Literal.create(
670679
new CalendarInterval(15, 9, -3 * CalendarInterval.MICROS_PER_HOUR), CalendarIntervalType),
671680
StringType),
672-
"interval 1 years 3 months 9 days -3 hours")
673-
checkEvaluation(Cast(Literal("INTERVAL 1 Second 1 microsecond"), CalendarIntervalType),
674-
new CalendarInterval(0, 0, 1000001))
681+
"1 years 3 months 9 days -3 hours")
675682
checkEvaluation(Cast(Literal("1 MONTH 1 Microsecond"), CalendarIntervalType),
676683
new CalendarInterval(1, 0, 1))
677684
}

sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/util/IntervalUtilsSuite.scala

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ class IntervalUtilsSuite extends SparkFunSuite {
5050
}
5151
}
5252

53-
for (input <- Seq("interval", "interval1 day", "foo", "foo 1 day")) {
53+
for (input <- Seq("interval", "interval 1 day", "interval1 day", "foo", "foo 1 day")) {
5454
try {
5555
fromString(input)
5656
fail("Expected to throw an exception for the invalid input")
@@ -82,13 +82,11 @@ class IntervalUtilsSuite extends SparkFunSuite {
8282

8383
private def testSingleUnit(
8484
unit: String, number: Int, months: Int, days: Int, microseconds: Long): Unit = {
85-
for (prefix <- Seq("interval ", "")) {
86-
val input1 = prefix + number + " " + unit
87-
val input2 = prefix + number + " " + unit + "s"
88-
val result = new CalendarInterval(months, days, microseconds)
89-
assert(fromString(input1) == result)
90-
assert(fromString(input2) == result)
91-
}
85+
val input1 = number + " " + unit
86+
val input2 = number + " " + unit + "s"
87+
val result = new CalendarInterval(months, days, microseconds)
88+
assert(fromString(input1) == result)
89+
assert(fromString(input2) == result)
9290
}
9391

9492
test("from year-month string") {
@@ -186,4 +184,11 @@ class IntervalUtilsSuite extends SparkFunSuite {
186184
assert(!isNegative("1 year -360 days", 31))
187185
assert(!isNegative("-1 year 380 days", 31))
188186
}
187+
188+
test("legacyCastStringToInterval") {
189+
for (input <- Seq("inTERval 1 year", "1 year")) {
190+
val result = new CalendarInterval(12, 0, 0)
191+
assert(IntervalUtils.legacyCastStringToInterval(input) == result)
192+
}
193+
}
189194
}

sql/core/src/test/resources/sql-tests/inputs/cast.sql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,5 +58,5 @@ DESC FUNCTION EXTENDED boolean;
5858
-- TODO: migrate all cast tests here.
5959

6060
-- cast string to interval and interval to string
61-
SELECT CAST('interval 3 month 1 hour' AS interval);
61+
SELECT CAST('3 month 1 hour' AS interval);
6262
SELECT CAST(interval 3 month 1 hour AS string);

sql/core/src/test/resources/sql-tests/inputs/literals.sql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,6 @@ SELECT 3.14, -3.14, 3.14e8, 3.14e-8, -3.14e8, -3.14e-8, 3.14e+8, 3.14E8, 3.14E-8
123123
select map(1, interval 1 day, 2, interval 3 week);
124124

125125
-- typed interval expression
126-
select interval 'interval 3 year 1 hour';
127126
select interval '3 year 1 hour';
128127

129128
-- typed integer expression
@@ -133,6 +132,7 @@ select integer '2147483648';
133132

134133
-- malformed interval literal
135134
select interval;
135+
select interval 'interval 3 year 1 hour';
136136
select interval 1 fake_unit;
137137
select interval 1 year to month;
138138
select interval '1' year to second;

0 commit comments

Comments
 (0)