This is an automated email from the ASF dual-hosted git repository.
MaxGekk pushed a commit to branch branch-4.x
in repository https://gitbox.apache.org/repos/asf/spark.git
The following commit(s) were added to refs/heads/branch-4.x by this push:
new 70b343025a5a [SPARK-57588][SQL] Report the actual target precision in
CAST_INVALID_INPUT when casting strings to TIME(p)
70b343025a5a is described below
commit 70b343025a5a917760dd383c6d589cfe571c0f02
Author: Maxim Gekk <[email protected]>
AuthorDate: Mon Jun 22 21:24:27 2026 +0200
[SPARK-57588][SQL] Report the actual target precision in CAST_INVALID_INPUT
when casting strings to TIME(p)
### What changes were proposed in this pull request?
When a string cannot be parsed while casting to `TIME(p)` in ANSI mode, the
`CAST_INVALID_INPUT` error always reported the target type as `"TIME(6)"`,
regardless of the actual target precision. This happened because
`SparkDateTimeUtils.stringToTimeAnsi` hardcoded the default `TimeType()`
(precision 6) when constructing the error, and the cast's target precision
never reached the error builder.
This PR:
- Adds a precision-aware overload `stringToTimeAnsi(s, precision, context)`
in `SparkDateTimeUtils` that reports `TimeType(precision)` in the error. The
existing no-arg/default overload is kept (delegating to
`TimeType.DEFAULT_PRECISION`) for non-cast callers such as
`DefaultTimeFormatter.parse`.
- Updates `Cast.castToTime` (interpreted) and `Cast.castToTimeCode`
(codegen) to pass the cast's target `to.precision`.
This mirrors the existing nanosecond `TIMESTAMP` casts, which already pass
the real target type, e.g. `invalidInputInCastToDatetimeError(s,
TimestampLTZNanosType(precision), ...)`.
### Why are the changes needed?
The `CAST_INVALID_INPUT` error message was inaccurate for any TIME
precision other than 6. For example:
```sql
SET spark.sql.ansi.enabled=true;
SELECT CAST('not a time' AS TIME(2));
```
reported `targetType = "TIME(6)"` instead of the requested `"TIME(2)"`.
### Does this PR introduce _any_ user-facing change?
Yes. With ANSI mode enabled, `CAST('x' AS TIME(p))` on invalid input now
reports `targetType = "TIME(p)"` (the requested precision) instead of always
`"TIME(6)"`. This is a change only within the unreleased master/branch lines.
### How was this patch tested?
- Extended `DateTimeUtilsSuite."stringToTimeAnsi"` to assert the error
reports the requested precision across the supported range
`[TimeType.MIN_PRECISION, TimeType.MAX_PRECISION]`, in addition to the existing
default `TIME(6)` cases.
- Added `CastWithAnsiOnSuite."SPARK-57588: cast invalid string input to
time reports the target precision"`, which exercises both the interpreted and
codegen paths via the standard cast eval harness.
Ran `build/sbt "catalyst/testOnly *DateTimeUtilsSuite
*CastWithAnsiOnSuite"` (199 tests passed) and scalastyle on `sql-api` and
`catalyst` (0 errors).
### Was this patch authored or co-authored using generative AI tooling?
Generated-by: Cursor (Claude Opus 4.8)
Closes #56641 from MaxGekk/time-fix-CAST_INVALID_INPUT.
Authored-by: Maxim Gekk <[email protected]>
Signed-off-by: Max Gekk <[email protected]>
(cherry picked from commit ecaa9cf9509d25e1866ca0461842391e654efa89)
Signed-off-by: Max Gekk <[email protected]>
---
.../spark/sql/catalyst/util/SparkDateTimeUtils.scala | 7 +++++--
.../apache/spark/sql/catalyst/expressions/Cast.scala | 5 +++--
.../sql/catalyst/expressions/CastWithAnsiOnSuite.scala | 9 +++++++++
.../spark/sql/catalyst/util/DateTimeUtilsSuite.scala | 17 ++++++++++++++++-
4 files changed, 33 insertions(+), 5 deletions(-)
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 430b04d8f3df..5abc80e7c6a1 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
@@ -1123,9 +1123,12 @@ trait SparkDateTimeUtils {
}
}
- def stringToTimeAnsi(s: UTF8String, context: QueryContext = null): Long = {
+ def stringToTimeAnsi(s: UTF8String, context: QueryContext = null): Long =
+ stringToTimeAnsi(s, TimeType.DEFAULT_PRECISION, context)
+
+ def stringToTimeAnsi(s: UTF8String, precision: Int, context: QueryContext):
Long = {
stringToTime(s).getOrElse {
- throw ExecutionErrors.invalidInputInCastToDatetimeError(s, TimeType(),
context)
+ throw ExecutionErrors.invalidInputInCastToDatetimeError(s,
TimeType(precision), context)
}
}
diff --git
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/Cast.scala
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/Cast.scala
index 62bbd65863e8..d12f46ecee9f 100644
---
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/Cast.scala
+++
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/Cast.scala
@@ -966,7 +966,7 @@ case class Cast(
case _: StringType =>
if (ansiEnabled) {
buildCast[UTF8String](_, s => DateTimeUtils.truncateTimeToPrecision(
- DateTimeUtils.stringToTimeAnsi(s, getContextOrNull()), to.precision))
+ DateTimeUtils.stringToTimeAnsi(s, to.precision, getContextOrNull()),
to.precision))
} else {
buildCast[UTF8String](_, s => DateTimeUtils.stringToTime(s)
.map(DateTimeUtils.truncateTimeToPrecision(_, to.precision)).orNull)
@@ -1673,7 +1673,8 @@ case class Cast(
val errorContext = getContextOrNullCode(ctx)
code"""
$evPrim = $dateTimeUtilsCls.truncateTimeToPrecision(
- $dateTimeUtilsCls.stringToTimeAnsi($c, $errorContext),
${to.precision});
+ $dateTimeUtilsCls.stringToTimeAnsi($c, ${to.precision},
$errorContext),
+ ${to.precision});
"""
} else {
code"""
diff --git
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/CastWithAnsiOnSuite.scala
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/CastWithAnsiOnSuite.scala
index ce7850d8c9c1..ef3e1e6eca9b 100644
---
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/CastWithAnsiOnSuite.scala
+++
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/CastWithAnsiOnSuite.scala
@@ -868,6 +868,15 @@ class CastWithAnsiOnSuite extends CastSuiteBase with
QueryErrorsBase {
}
}
+ test("SPARK-57588: cast invalid string input to time reports the target
precision") {
+ for (precision <- TimeType.MIN_PRECISION to TimeType.MAX_PRECISION) {
+ val invalidInput = "not a time"
+ checkExceptionInExpression[DateTimeException](
+ cast(invalidInput, TimeType(precision)),
+ castErrMsg(invalidInput, TimeType(precision)))
+ }
+ }
+
test("SPARK-52620: cast time to decimal with insufficient precision (ANSI
on)") {
// Create a time that will overflow Decimal(2, 0): 23:59:59 = 86399
seconds.
val largeTime = Literal.create(LocalTime.of(23, 59, 59, 123456000),
TimeType(6))
diff --git
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/util/DateTimeUtilsSuite.scala
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/util/DateTimeUtilsSuite.scala
index 0a6c9123a20e..1f594667fce3 100644
---
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/util/DateTimeUtilsSuite.scala
+++
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/util/DateTimeUtilsSuite.scala
@@ -34,8 +34,8 @@ import org.apache.spark.sql.catalyst.util.DateTimeUtils._
import
org.apache.spark.sql.catalyst.util.RebaseDateTime.rebaseJulianToGregorianMicros
import org.apache.spark.sql.errors.DataTypeErrors.toSQLConf
import org.apache.spark.sql.internal.SqlApiConf
+import org.apache.spark.sql.types.{Decimal, TimeType}
import org.apache.spark.sql.types.DayTimeIntervalType.{HOUR, SECOND}
-import org.apache.spark.sql.types.Decimal
import org.apache.spark.unsafe.types.{CalendarInterval, TimestampNanosVal,
UTF8String}
class DateTimeUtilsSuite extends SparkFunSuite with Matchers with SQLHelper {
@@ -1505,6 +1505,21 @@ class DateTimeUtilsSuite extends SparkFunSuite with
Matchers with SQLHelper {
"targetType" -> "\"TIME(6)\"",
"ansiConfig" -> "\"spark.sql.ansi.enabled\""))
}
+
+ // The error must report the requested target precision, not the default
TIME(6).
+ for (precision <- TimeType.MIN_PRECISION to TimeType.MAX_PRECISION) {
+ val invalidTime = "not a time"
+ checkError(
+ exception = intercept[SparkDateTimeException] {
+ stringToTimeAnsi(UTF8String.fromString(invalidTime), precision, null)
+ },
+ condition = "CAST_INVALID_INPUT",
+ parameters = Map(
+ "expression" -> s"'$invalidTime'",
+ "sourceType" -> "\"STRING\"",
+ "targetType" -> s""""TIME($precision)"""",
+ "ansiConfig" -> "\"spark.sql.ansi.enabled\""))
+ }
}
test("timeToMicros") {
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]