diff --git a/common/utils/src/main/resources/error/error-conditions.json b/common/utils/src/main/resources/error/error-conditions.json index 12666fe4ff629..35d7dc4be9d56 100644 --- a/common/utils/src/main/resources/error/error-conditions.json +++ b/common/utils/src/main/resources/error/error-conditions.json @@ -3106,6 +3106,12 @@ ], "sqlState" : "42K0F" }, + "INVALID_TIMEZONE" : { + "message" : [ + "The timezone: is invalid. The timezone must be either a region-based zone ID or a zone offset. Region IDs must have the form 'area/city', such as 'America/Los_Angeles'. Zone offsets must be in the format '(+|-)HH', '(+|-)HH:mm’ or '(+|-)HH:mm:ss', e.g '-08' , '+01:00' or '-13:33:33', and must be in the range from -18:00 to +18:00. 'Z' and 'UTC' are accepted as synonyms for '+00:00'." + ], + "sqlState" : "22009" + }, "INVALID_TIME_TRAVEL_SPEC" : { "message" : [ "Cannot specify both version and timestamp when time travelling the table." diff --git a/common/utils/src/main/scala/org/apache/spark/SparkException.scala b/common/utils/src/main/scala/org/apache/spark/SparkException.scala index 398cb1fad6726..fcaee787fd8d3 100644 --- a/common/utils/src/main/scala/org/apache/spark/SparkException.scala +++ b/common/utils/src/main/scala/org/apache/spark/SparkException.scala @@ -306,8 +306,9 @@ private[spark] class SparkDateTimeException private( message: String, errorClass: Option[String], messageParameters: Map[String, String], - context: Array[QueryContext]) - extends DateTimeException(message) with SparkThrowable { + context: Array[QueryContext], + cause: Option[Throwable]) + extends DateTimeException(message, cause.orNull) with SparkThrowable { def this( errorClass: String, @@ -318,7 +319,23 @@ private[spark] class SparkDateTimeException private( SparkThrowableHelper.getMessage(errorClass, messageParameters, summary), Option(errorClass), messageParameters, - context + context, + cause = None + ) + } + + def this( + errorClass: String, + messageParameters: Map[String, String], + context: Array[QueryContext], + summary: String, + cause: Option[Throwable]) = { + this( + SparkThrowableHelper.getMessage(errorClass, messageParameters, summary), + Option(errorClass), + messageParameters, + context, + cause.orElse(None) ) } diff --git a/sql/api/src/main/scala/org/apache/spark/sql/catalyst/util/SparkDateTimeUtils.scala b/sql/api/src/main/scala/org/apache/spark/sql/catalyst/util/SparkDateTimeUtils.scala index 4e94bc6617357..4d05f9079548c 100644 --- a/sql/api/src/main/scala/org/apache/spark/sql/catalyst/util/SparkDateTimeUtils.scala +++ b/sql/api/src/main/scala/org/apache/spark/sql/catalyst/util/SparkDateTimeUtils.scala @@ -41,12 +41,16 @@ trait SparkDateTimeUtils { final val singleMinuteTz = Pattern.compile("(\\+|\\-)(\\d\\d):(\\d)$") def getZoneId(timeZoneId: String): ZoneId = { - // To support the (+|-)h:mm format because it was supported before Spark 3.0. - var formattedZoneId = singleHourTz.matcher(timeZoneId).replaceFirst("$10$2:") - // To support the (+|-)hh:m format because it was supported before Spark 3.0. - formattedZoneId = singleMinuteTz.matcher(formattedZoneId).replaceFirst("$1$2:0$3") - - ZoneId.of(formattedZoneId, ZoneId.SHORT_IDS) + try { + // To support the (+|-)h:mm format because it was supported before Spark 3.0. + var formattedZoneId = singleHourTz.matcher(timeZoneId).replaceFirst("$10$2:") + // To support the (+|-)hh:m format because it was supported before Spark 3.0. + formattedZoneId = singleMinuteTz.matcher(formattedZoneId).replaceFirst("$1$2:0$3") + ZoneId.of(formattedZoneId, ZoneId.SHORT_IDS) + } catch { + case e: java.time.DateTimeException => + throw ExecutionErrors.zoneOffsetError(timeZoneId, e) + } } def getTimeZone(timeZoneId: String): TimeZone = TimeZone.getTimeZone(getZoneId(timeZoneId)) diff --git a/sql/api/src/main/scala/org/apache/spark/sql/errors/ExecutionErrors.scala b/sql/api/src/main/scala/org/apache/spark/sql/errors/ExecutionErrors.scala index 698a7b096e1a5..3527a10496862 100644 --- a/sql/api/src/main/scala/org/apache/spark/sql/errors/ExecutionErrors.scala +++ b/sql/api/src/main/scala/org/apache/spark/sql/errors/ExecutionErrors.scala @@ -238,6 +238,17 @@ private[sql] trait ExecutionErrors extends DataTypeErrorsBase { "encoderType" -> encoder.getClass.getName, "docroot" -> SparkBuildInfo.spark_doc_root)) } + + def zoneOffsetError( + timeZone: String, + e: java.time.DateTimeException): SparkDateTimeException = { + new SparkDateTimeException( + errorClass = "INVALID_TIMEZONE", + messageParameters = Map("timeZone" -> timeZone), + context = Array.empty, + summary = "", + cause = Some(e)) + } } private[sql] object ExecutionErrors extends ExecutionErrors diff --git a/sql/core/src/test/scala/org/apache/spark/sql/errors/QueryExecutionAnsiErrorsSuite.scala b/sql/core/src/test/scala/org/apache/spark/sql/errors/QueryExecutionAnsiErrorsSuite.scala index ec92e0b700e31..2e0983fe0319c 100644 --- a/sql/core/src/test/scala/org/apache/spark/sql/errors/QueryExecutionAnsiErrorsSuite.scala +++ b/sql/core/src/test/scala/org/apache/spark/sql/errors/QueryExecutionAnsiErrorsSuite.scala @@ -391,4 +391,14 @@ class QueryExecutionAnsiErrorsSuite extends QueryTest } } } + + test("SPARK-49773: INVALID_TIMEZONE for bad timezone") { + checkError( + exception = intercept[SparkDateTimeException] { + sql("select make_timestamp(1, 2, 28, 23, 1, 1, -100)").collect() + }, + condition = "INVALID_TIMEZONE", + parameters = Map("timeZone" -> "-100") + ) + } }