This is an automated email from the ASF dual-hosted git repository.

uros-b 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 a660811c7d82 [SPARK-57211][SQL] Cast strings to 
TIMESTAMP_NTZ(p)/TIMESTAMP_LTZ(p)
a660811c7d82 is described below

commit a660811c7d822dd1a06b4b9cb1dd1f2b32d86eb6
Author: Maxim Gekk <[email protected]>
AuthorDate: Wed Jun 3 15:07:02 2026 +0200

    [SPARK-57211][SQL] Cast strings to TIMESTAMP_NTZ(p)/TIMESTAMP_LTZ(p)
    
    ### What changes were proposed in this pull request?
    
    This PR wires `Cast` to support casting `StringType` to the 
nanosecond-capable timestamp types `TimestampNTZNanosType(p)` and 
`TimestampLTZNanosType(p)` with fractional-seconds precision `p` in `[7, 9]`, 
on both the interpreted and codegen paths and across all eval modes (`LEGACY`, 
`ANSI`, `TRY`):
    
    - `CAST(<string> AS TIMESTAMP_NTZ(p))`
    - `CAST(<string> AS TIMESTAMP_LTZ(p))`
    
    Concretely, in `Cast.scala`:
    - Add `StringType -> TimestampNTZNanosType(p)` / `TimestampLTZNanosType(p)` 
arms to `canCast` and `canAnsiCast`. Try-cast is covered automatically 
(`canTryCast` delegates to `canAnsiCast`, and `canUseLegacyCastForTryCast` 
already matches `(StringType, DatetimeType)`, which the nanos types extend).
    - Add `(StringType, TimestampLTZNanosType)` to `Cast.needsTimeZone`. The 
NTZ string is zone-independent, mirroring the micro `TIMESTAMP_NTZ` cast.
    - Add interpreted `castToTimestampLTZNanos` / `castToTimestampNTZNanos` and 
matching codegen, dispatched from `castInternal` / `nullSafeCastFunction` with 
the precision taken from the target type. The result is a `TimestampNanosVal` 
(or `null` in legacy/try mode on malformed input).
    - The NTZ cast adopts `allowTimeZone = true` to match the existing micro 
`TIMESTAMP_NTZ` string cast, and resolves the `TODO(SPARK-57032)` left on 
`stringToTimestampNTZNanosAnsi`.
    
    This reuses the parse entry points added in SPARK-57032 on 
`SparkDateTimeUtils` (inherited by `DateTimeUtils`), which already return a 
normalized `TimestampNanosVal` and apply per-precision truncation, so no 
separate normalization module is required for the string path.
    
    Existing preview gating is unchanged: `Cast.checkInputDataTypes` calls 
`TypeUtils.failUnsupportedDataType`, which throws `FEATURE_NOT_ENABLED` when 
`spark.sql.timestampNanosTypes.enabled` is off.
    
    ### Why are the changes needed?
    
    This is a sub-task of 
[SPARK-56822](https://issues.apache.org/jira/browse/SPARK-56822) (SPIP: 
Timestamps with nanosecond precision).
    
    The logical types, the `TIMESTAMP_NTZ(p)` / `TIMESTAMP_LTZ(p)` SQL syntax, 
the physical row value `TimestampNanosVal`, and the string-to-nanos parse 
helpers all exist, but `Cast` had zero arms for the nanos types. As a result 
`CAST(s AS TIMESTAMP_NTZ(9))` failed type-check with `CAST_WITHOUT_SUGGESTION` 
even when the preview flag `spark.sql.timestampNanosTypes.enabled` was on. 
String ingestion is the most common entry point for these types and unblocks 
typed literals, filters, and CTA [...]
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes, but only when the preview flag `spark.sql.timestampNanosTypes.enabled` 
is enabled (it defaults to off in production). With the flag on, `CAST(<string> 
AS TIMESTAMP_NTZ(p))` and `CAST(<string> AS TIMESTAMP_LTZ(p))` for `p` in `[7, 
9]` now produce correct nanosecond values in `LEGACY`, `ANSI`, and `TRY` modes; 
previously they failed type-checking. With the flag off, the behavior is 
unchanged (`FEATURE_NOT_ENABLED`). Existing microsecond timestamp string casts 
are unchanged.
    
    ### How was this patch tested?
    
    - `CastSuiteBase`: success cases for both types over `p` in `[7, 9]` and a 
7-9 digit fractional corpus; LTZ parameterized over time zones, NTZ 
zone-independent (including a discarded zone suffix). Plus a flag-off guard 
asserting `FEATURE_NOT_ENABLED`.
    - `CastWithAnsiOnSuite`: malformed-input parse errors (`DateTimeException` 
/ `CAST_INVALID_INPUT`).
    - `CastWithAnsiOffSuite` / `TryCastSuite`: malformed input returns `NULL`.
    - Golden-file checks added to `cast.sql` (regenerated with 
`SPARK_GENERATE_GOLDEN_FILES=1`): positive cases assert the result type via 
`typeof` (the reverse direction, nanos -> string rendering, is not wired yet 
and is tracked under SPARK-57162); negative cases exercise the ANSI parse-error 
path (and `NULL` in non-ANSI mode).
    
    Verified locally:
    ```
    $ build/sbt 'catalyst/testOnly *CastSuite *CastWithAnsiOnSuite 
*CastWithAnsiOffSuite *TryCastSuite'
    $ build/sbt 'sql/testOnly org.apache.spark.sql.SQLQueryTestSuite -- -z 
cast.sql'
    $ ./dev/scalastyle
    ```
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    Generated-by: Cursor (Claude Opus 4.8)
    
    Closes #56288 from MaxGekk/nanos-cast-string.
    
    Authored-by: Maxim Gekk <[email protected]>
    Signed-off-by: Uros Bojanic <[email protected]>
    (cherry picked from commit 7d0a8cd314ac827b7298b1ceed89619892c6a4b2)
    Signed-off-by: Uros Bojanic <[email protected]>
---
 .../sql/catalyst/util/SparkDateTimeUtils.scala     |  7 +-
 .../spark/sql/catalyst/expressions/Cast.scala      | 95 +++++++++++++++++++++-
 .../sql/catalyst/expressions/CastSuiteBase.scala   | 49 ++++++++++-
 .../expressions/CastWithAnsiOffSuite.scala         | 12 +++
 .../catalyst/expressions/CastWithAnsiOnSuite.scala | 18 ++++
 .../sql-tests/analyzer-results/cast.sql.out        | 28 +++++++
 .../analyzer-results/nonansi/cast.sql.out          | 28 +++++++
 .../src/test/resources/sql-tests/inputs/cast.sql   |  9 ++
 .../test/resources/sql-tests/results/cast.sql.out  | 66 +++++++++++++++
 .../sql-tests/results/nonansi/cast.sql.out         | 32 ++++++++
 10 files changed, 339 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 d7200715f937..29f280fdd09c 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
@@ -946,9 +946,10 @@ trait SparkDateTimeUtils {
       s: UTF8String,
       precision: Int,
       context: QueryContext = null): TimestampNanosVal = {
-    // TODO(SPARK-57032): when this is wired to a user-facing CAST(... AS 
TIMESTAMP_NTZ(p)), the
-    // cast must decide `allowTimeZone` explicitly (per ANSI/legacy mode) 
instead of relying on
-    // the `true` default used here, which silently discards a zone suffix.
+    // CAST(... AS TIMESTAMP_NTZ(p)) intentionally uses `allowTimeZone = true` 
here, mirroring the
+    // micro `TIMESTAMP_NTZ` string cast 
(`stringToTimestampWithoutTimeZoneAnsi`): a zone suffix in
+    // the input is silently discarded rather than rejected. Callers that need 
strict NTZ rejection
+    // should call `stringToTimestampNTZNanos` directly with `allowTimeZone = 
false`.
     stringToTimestampNTZNanos(s, precision).getOrElse {
       throw ExecutionErrors.invalidInputInCastToDatetimeError(
         s,
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 ad3e22dc2257..a1935c739643 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
@@ -38,7 +38,7 @@ import 
org.apache.spark.sql.catalyst.util.IntervalUtils.{dayTimeIntervalToByte,
 import org.apache.spark.sql.errors.{QueryErrorsBase, QueryExecutionErrors}
 import org.apache.spark.sql.internal.SQLConf
 import org.apache.spark.sql.types._
-import org.apache.spark.unsafe.types.{BinaryView, UTF8String, VariantVal}
+import org.apache.spark.unsafe.types.{BinaryView, TimestampNanosVal, 
UTF8String, VariantVal}
 import org.apache.spark.unsafe.types.UTF8String.{IntWrapper, LongWrapper}
 import org.apache.spark.util.ArrayImplicits._
 
@@ -113,6 +113,9 @@ object Cast extends QueryErrorsBase {
     case (DateType, TimestampNTZType) => true
     case (TimestampType, TimestampNTZType) => true
 
+    case (_: StringType, _: TimestampNTZNanosType) => true
+    case (_: StringType, _: TimestampLTZNanosType) => true
+
     case (_: StringType, _: CalendarIntervalType) => true
     case (_: StringType, _: AnsiIntervalType) => true
 
@@ -248,6 +251,9 @@ object Cast extends QueryErrorsBase {
     case (DateType, TimestampNTZType) => true
     case (TimestampType, TimestampNTZType) => true
 
+    case (_: StringType, _: TimestampNTZNanosType) => true
+    case (_: StringType, _: TimestampLTZNanosType) => true
+
     case (_: StringType, DateType) => true
     case (_: StringType, _: TimeType) => true
     case (TimestampType, DateType) => true
@@ -335,6 +341,9 @@ object Cast extends QueryErrorsBase {
     case (TimestampType, DateType) => true
     case (TimestampType, TimestampNTZType) => true
     case (TimestampNTZType, TimestampType) => true
+    // NTZ string is zone-independent (mirroring micro TIMESTAMP_NTZ, which is 
not listed); only
+    // the LTZ string parse depends on the session time zone.
+    case (_: StringType, _: TimestampLTZNanosType) => true
     case (ArrayType(fromType, _), ArrayType(toType, _)) => 
needsTimeZone(fromType, toType)
     case (MapType(fromKey, fromValue, _), MapType(toKey, toValue, _)) =>
       needsTimeZone(fromKey, toKey) || needsTimeZone(fromValue, toValue)
@@ -786,6 +795,30 @@ case class Cast(
       buildCast[Long](_, ts => convertTz(ts, ZoneOffset.UTC, zoneId))
   }
 
+  private[this] def castToTimestampLTZNanos(
+      from: DataType,
+      precision: Int): Any => Any = from match {
+    case _: StringType =>
+      buildCast[UTF8String](_, utfs =>
+        if (ansiEnabled) {
+          DateTimeUtils.stringToTimestampLTZNanosAnsi(utfs, precision, zoneId, 
getContextOrNull())
+        } else {
+          DateTimeUtils.stringToTimestampLTZNanos(utfs, precision, 
zoneId).orNull
+        })
+  }
+
+  private[this] def castToTimestampNTZNanos(
+      from: DataType,
+      precision: Int): Any => Any = from match {
+    case _: StringType =>
+      buildCast[UTF8String](_, utfs =>
+        if (ansiEnabled) {
+          DateTimeUtils.stringToTimestampNTZNanosAnsi(utfs, precision, 
getContextOrNull())
+        } else {
+          DateTimeUtils.stringToTimestampNTZNanos(utfs, precision, 
allowTimeZone = true).orNull
+        })
+  }
+
   private[this] def decimalToTimestamp(d: Decimal): Long = {
     (d.toBigDecimal * MICROS_PER_SECOND).longValue
   }
@@ -1299,6 +1332,8 @@ case class Cast(
         case decimal: DecimalType => castToDecimal(from, decimal)
         case TimestampType => castToTimestamp(from)
         case TimestampNTZType => castToTimestampNTZ(from)
+        case t: TimestampNTZNanosType => castToTimestampNTZNanos(from, 
t.precision)
+        case t: TimestampLTZNanosType => castToTimestampLTZNanos(from, 
t.precision)
         case CalendarIntervalType => castToInterval(from)
         case it: DayTimeIntervalType => castToDayTimeInterval(from, it)
         case it: YearMonthIntervalType => castToYearMonthInterval(from, it)
@@ -1409,6 +1444,8 @@ case class Cast(
     case decimal: DecimalType => castToDecimalCode(from, decimal, ctx)
     case TimestampType => castToTimestampCode(from, ctx)
     case TimestampNTZType => castToTimestampNTZCode(from, ctx)
+    case t: TimestampNTZNanosType => castToTimestampNTZNanosCode(from, 
t.precision, ctx)
+    case t: TimestampLTZNanosType => castToTimestampLTZNanosCode(from, 
t.precision, ctx)
     case CalendarIntervalType => castToIntervalCode(from)
     case it: DayTimeIntervalType => castToDayTimeIntervalCode(from, it)
     case it: YearMonthIntervalType => castToYearMonthIntervalCode(from, it)
@@ -1772,6 +1809,62 @@ case class Cast(
         code"$evPrim = $dateTimeUtilsCls.convertTz($c, 
java.time.ZoneOffset.UTC, $zid);"
   }
 
+  private[this] def castToTimestampLTZNanosCode(
+      from: DataType,
+      precision: Int,
+      ctx: CodegenContext): CastFunction = from match {
+    case _: StringType =>
+      val zoneIdClass = classOf[ZoneId]
+      val zid = JavaCode.global(
+        ctx.addReferenceObj("zoneId", zoneId, zoneIdClass.getName),
+        zoneIdClass)
+      val tsOpt = ctx.freshVariable("tsOpt", 
classOf[Option[TimestampNanosVal]])
+      (c, evPrim, evNull) =>
+        if (ansiEnabled) {
+          val errorContext = getContextOrNullCode(ctx)
+          code"""
+            $evPrim = $dateTimeUtilsCls.stringToTimestampLTZNanosAnsi(
+              $c, $precision, $zid, $errorContext);
+           """
+        } else {
+          code"""
+            scala.Option<TimestampNanosVal> $tsOpt =
+              $dateTimeUtilsCls.stringToTimestampLTZNanos($c, $precision, 
$zid);
+            if ($tsOpt.isDefined()) {
+              $evPrim = (TimestampNanosVal) $tsOpt.get();
+            } else {
+              $evNull = true;
+            }
+           """
+        }
+  }
+
+  private[this] def castToTimestampNTZNanosCode(
+      from: DataType,
+      precision: Int,
+      ctx: CodegenContext): CastFunction = from match {
+    case _: StringType =>
+      val tsOpt = ctx.freshVariable("tsOpt", 
classOf[Option[TimestampNanosVal]])
+      (c, evPrim, evNull) =>
+        if (ansiEnabled) {
+          val errorContext = getContextOrNullCode(ctx)
+          code"""
+            $evPrim = $dateTimeUtilsCls.stringToTimestampNTZNanosAnsi(
+              $c, $precision, $errorContext);
+           """
+        } else {
+          code"""
+            scala.Option<TimestampNanosVal> $tsOpt =
+              $dateTimeUtilsCls.stringToTimestampNTZNanos($c, $precision, 
true);
+            if ($tsOpt.isDefined()) {
+              $evPrim = (TimestampNanosVal) $tsOpt.get();
+            } else {
+              $evNull = true;
+            }
+           """
+        }
+  }
+
   private[this] def castToIntervalCode(from: DataType): CastFunction = from 
match {
     case _: StringType =>
       val util = IntervalUtils.getClass.getCanonicalName.stripSuffix("$")
diff --git 
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/CastSuiteBase.scala
 
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/CastSuiteBase.scala
index e888432ef91e..b33045ad90a8 100644
--- 
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/CastSuiteBase.scala
+++ 
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/CastSuiteBase.scala
@@ -22,7 +22,7 @@ import java.time.{Duration, LocalDate, LocalDateTime, 
LocalTime, Period}
 import java.time.temporal.ChronoUnit
 import java.util.{Calendar, Locale, TimeZone}
 
-import org.apache.spark.{SparkFunSuite, SparkIllegalArgumentException}
+import org.apache.spark.{SparkException, SparkFunSuite, 
SparkIllegalArgumentException}
 import org.apache.spark.sql.Row
 import org.apache.spark.sql.catalyst.InternalRow
 import org.apache.spark.sql.catalyst.analysis.TypeCheckResult.DataTypeMismatch
@@ -33,6 +33,7 @@ import org.apache.spark.sql.catalyst.util.DateTimeTestUtils._
 import org.apache.spark.sql.catalyst.util.DateTimeUtils._
 import org.apache.spark.sql.catalyst.util.IntervalUtils
 import org.apache.spark.sql.catalyst.util.IntervalUtils.microsToDuration
+import org.apache.spark.sql.catalyst.util.TimestampNanosTestUtils._
 import org.apache.spark.sql.internal.SQLConf
 import org.apache.spark.sql.types._
 import org.apache.spark.sql.types.DataTypeTestUtils.{dayTimeIntervalTypes, 
yearMonthIntervalTypes}
@@ -1023,6 +1024,52 @@ abstract class CastSuiteBase extends SparkFunSuite with 
ExpressionEvalHelper {
       LocalDateTime.of(2021, 6, 17, 0, 0))
   }
 
+  test("SPARK-57211: cast string to timestamp_ltz with nanosecond precision") {
+    foreachNanosPrecision { precision =>
+      val truncate = nanoOfSecTruncator(precision)
+      outstandingZoneIds.foreach { zid =>
+        specialNanosTs.foreach { s =>
+          val ldt = 
parseSpecialNanosNTZ(s).withNano(truncate(parseSpecialNanosNTZ(s).getNano))
+          val expected = instantToNanosVal(ldt.atZone(zid).toInstant)
+          checkEvaluation(
+            cast(Literal(s), TimestampLTZNanosType(precision), 
Option(zid.getId)),
+            expected)
+        }
+      }
+    }
+  }
+
+  test("SPARK-57211: cast string to timestamp_ntz with nanosecond precision") {
+    foreachNanosPrecision { precision =>
+      val truncate = nanoOfSecTruncator(precision)
+      specialNanosTs.foreach { s =>
+        val ldt = 
parseSpecialNanosNTZ(s).withNano(truncate(parseSpecialNanosNTZ(s).getNano))
+        val expected = localDateTimeToNanosVal(ldt)
+        // NTZ result is independent of the session time zone.
+        checkEvaluation(cast(Literal(s), TimestampNTZNanosType(precision)), 
expected)
+        // A zone suffix is discarded (allowTimeZone = true), mirroring micro 
TIMESTAMP_NTZ.
+        checkEvaluation(cast(Literal(s + "Z"), 
TimestampNTZNanosType(precision)), expected)
+      }
+    }
+  }
+
+  test("SPARK-57211: nanosecond timestamp cast requires the preview flag") {
+    withSQLConf(SQLConf.TIMESTAMP_NANOS_TYPES_ENABLED.key -> "false") {
+      val expectedParams = Map(
+        "featureName" -> "Nanosecond-precision timestamp types",
+        "configKey" -> "spark.sql.timestampNanosTypes.enabled",
+        "configValue" -> "true")
+      Seq(TimestampNTZNanosType(9), TimestampLTZNanosType(9)).foreach { to =>
+        checkError(
+          exception = intercept[SparkException] {
+            cast(Literal("2020-01-01 00:00:00"), to, 
UTC_OPT).checkInputDataTypes()
+          },
+          condition = "FEATURE_NOT_ENABLED",
+          parameters = expectedParams)
+      }
+    }
+  }
+
   test("SPARK-35112: Cast string to day-time interval") {
     checkEvaluation(cast(Literal.create("0 0:0:0"), DayTimeIntervalType()), 0L)
     checkEvaluation(cast(Literal.create(" interval '0 0:0:0' Day TO second   
"),
diff --git 
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/CastWithAnsiOffSuite.scala
 
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/CastWithAnsiOffSuite.scala
index ec347a14a9a4..9f9a6f275a3f 100644
--- 
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/CastWithAnsiOffSuite.scala
+++ 
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/CastWithAnsiOffSuite.scala
@@ -27,6 +27,7 @@ import 
org.apache.spark.sql.catalyst.analysis.TypeCoercionSuite
 import org.apache.spark.sql.catalyst.expressions.aggregate.{CollectList, 
CollectSet}
 import org.apache.spark.sql.catalyst.util.DateTimeConstants._
 import org.apache.spark.sql.catalyst.util.DateTimeTestUtils._
+import 
org.apache.spark.sql.catalyst.util.TimestampNanosTestUtils.foreachNanosPrecision
 import org.apache.spark.sql.internal.SQLConf
 import org.apache.spark.sql.types._
 import org.apache.spark.sql.types.DayTimeIntervalType.{DAY, HOUR, MINUTE, 
SECOND}
@@ -56,6 +57,17 @@ class CastWithAnsiOffSuite extends CastSuiteBase {
     checkEvaluation(cast(123L, DecimalType(2, 0)), null)
   }
 
+  test("SPARK-57211: legacy mode cast malformed string to nanosecond timestamp 
returns null") {
+    Seq("123", "2015-03-18 123142", "2015-03-18X", "abdef").foreach { str =>
+      foreachNanosPrecision { precision =>
+        checkEvaluation(
+          cast(Literal(str), TimestampLTZNanosType(precision), UTC_OPT), null)
+        checkEvaluation(
+          cast(Literal(str), TimestampNTZNanosType(precision)), null)
+      }
+    }
+  }
+
   test("cast from int #2") {
     checkEvaluation(cast(cast(1000, TimestampType), LongType), 1000.toLong)
     checkEvaluation(cast(cast(-1200, TimestampType), LongType), -1200.toLong)
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 b76aec6d6ce0..ce7850d8c9c1 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
@@ -28,6 +28,7 @@ import 
org.apache.spark.sql.catalyst.analysis.TypeCheckResult.DataTypeMismatch
 import org.apache.spark.sql.catalyst.util.DateTimeConstants.MILLIS_PER_SECOND
 import org.apache.spark.sql.catalyst.util.DateTimeTestUtils
 import 
org.apache.spark.sql.catalyst.util.DateTimeTestUtils.{withDefaultTimeZone, UTC}
+import 
org.apache.spark.sql.catalyst.util.TimestampNanosTestUtils.foreachNanosPrecision
 import org.apache.spark.sql.errors.QueryErrorsBase
 import org.apache.spark.sql.internal.SQLConf
 import org.apache.spark.sql.types._
@@ -800,6 +801,23 @@ class CastWithAnsiOnSuite extends CastSuiteBase with 
QueryErrorsBase {
     }
   }
 
+  test("SPARK-57211: ANSI mode cast string to nanosecond timestamp with parse 
error") {
+    val invalidInputs = Seq(
+      "123", "2015-03-18 123142", "2015-03-18X", "2015/03/18", "abdef", 
"2015-031-8")
+    DateTimeTestUtils.outstandingZoneIds.foreach { zid =>
+      foreachNanosPrecision { precision =>
+        invalidInputs.foreach { str =>
+          checkExceptionInExpression[DateTimeException](
+            cast(Literal(str), TimestampLTZNanosType(precision), 
Option(zid.getId)),
+            castErrMsg(str, TimestampLTZNanosType(precision)))
+          checkExceptionInExpression[DateTimeException](
+            cast(Literal(str), TimestampNTZNanosType(precision)),
+            castErrMsg(str, TimestampNTZNanosType(precision)))
+        }
+      }
+    }
+  }
+
   test("ANSI mode: cast string to date with parse error") {
     DateTimeTestUtils.outstandingZoneIds.foreach { zid =>
       def checkCastWithParseError(str: String): Unit = {
diff --git 
a/sql/core/src/test/resources/sql-tests/analyzer-results/cast.sql.out 
b/sql/core/src/test/resources/sql-tests/analyzer-results/cast.sql.out
index 053d7af3df45..b077443a9f28 100644
--- a/sql/core/src/test/resources/sql-tests/analyzer-results/cast.sql.out
+++ b/sql/core/src/test/resources/sql-tests/analyzer-results/cast.sql.out
@@ -641,6 +641,34 @@ Project [cast(a as timestamp_ntz) AS CAST(a AS 
TIMESTAMP_NTZ)#x]
 +- OneRowRelation
 
 
+-- !query
+select typeof(cast('2022-01-01 00:00:00.123456789' as timestamp_ntz(9)))
+-- !query analysis
+Project [typeof(cast(2022-01-01 00:00:00.123456789 as timestamp_ntz(9))) AS 
typeof(CAST(2022-01-01 00:00:00.123456789 AS TIMESTAMP_NTZ(9)))#x]
++- OneRowRelation
+
+
+-- !query
+select typeof(cast('2022-01-01 00:00:00.123456789' as timestamp_ltz(7)))
+-- !query analysis
+Project [typeof(cast(2022-01-01 00:00:00.123456789 as timestamp_ltz(7))) AS 
typeof(CAST(2022-01-01 00:00:00.123456789 AS TIMESTAMP_LTZ(7)))#x]
++- OneRowRelation
+
+
+-- !query
+select cast('a' as timestamp_ntz(9)) is null
+-- !query analysis
+Project [isnull(cast(a as timestamp_ntz(9))) AS (CAST(a AS TIMESTAMP_NTZ(9)) 
IS NULL)#x]
++- OneRowRelation
+
+
+-- !query
+select cast('a' as timestamp_ltz(9)) is null
+-- !query analysis
+Project [isnull(cast(a as timestamp_ltz(9))) AS (CAST(a AS TIMESTAMP_LTZ(9)) 
IS NULL)#x]
++- OneRowRelation
+
+
 -- !query
 select cast(cast('inf' as double) as timestamp)
 -- !query analysis
diff --git 
a/sql/core/src/test/resources/sql-tests/analyzer-results/nonansi/cast.sql.out 
b/sql/core/src/test/resources/sql-tests/analyzer-results/nonansi/cast.sql.out
index 0113716bdf71..1255f2266629 100644
--- 
a/sql/core/src/test/resources/sql-tests/analyzer-results/nonansi/cast.sql.out
+++ 
b/sql/core/src/test/resources/sql-tests/analyzer-results/nonansi/cast.sql.out
@@ -505,6 +505,34 @@ Project [cast(a as timestamp_ntz) AS CAST(a AS 
TIMESTAMP_NTZ)#x]
 +- OneRowRelation
 
 
+-- !query
+select typeof(cast('2022-01-01 00:00:00.123456789' as timestamp_ntz(9)))
+-- !query analysis
+Project [typeof(cast(2022-01-01 00:00:00.123456789 as timestamp_ntz(9))) AS 
typeof(CAST(2022-01-01 00:00:00.123456789 AS TIMESTAMP_NTZ(9)))#x]
++- OneRowRelation
+
+
+-- !query
+select typeof(cast('2022-01-01 00:00:00.123456789' as timestamp_ltz(7)))
+-- !query analysis
+Project [typeof(cast(2022-01-01 00:00:00.123456789 as timestamp_ltz(7))) AS 
typeof(CAST(2022-01-01 00:00:00.123456789 AS TIMESTAMP_LTZ(7)))#x]
++- OneRowRelation
+
+
+-- !query
+select cast('a' as timestamp_ntz(9)) is null
+-- !query analysis
+Project [isnull(cast(a as timestamp_ntz(9))) AS (CAST(a AS TIMESTAMP_NTZ(9)) 
IS NULL)#x]
++- OneRowRelation
+
+
+-- !query
+select cast('a' as timestamp_ltz(9)) is null
+-- !query analysis
+Project [isnull(cast(a as timestamp_ltz(9))) AS (CAST(a AS TIMESTAMP_LTZ(9)) 
IS NULL)#x]
++- OneRowRelation
+
+
 -- !query
 select cast(cast('inf' as double) as timestamp)
 -- !query analysis
diff --git a/sql/core/src/test/resources/sql-tests/inputs/cast.sql 
b/sql/core/src/test/resources/sql-tests/inputs/cast.sql
index 9d191dff6702..5065e7c335e7 100644
--- a/sql/core/src/test/resources/sql-tests/inputs/cast.sql
+++ b/sql/core/src/test/resources/sql-tests/inputs/cast.sql
@@ -102,6 +102,15 @@ select cast('a' as timestamp);
 select cast('2022-01-01 00:00:00' as timestamp_ntz);
 select cast('a' as timestamp_ntz);
 
+-- SPARK-57211: cast string to nanosecond-precision timestamps 
TIMESTAMP_NTZ(p)/TIMESTAMP_LTZ(p).
+-- The reverse direction (nanos -> string) is not wired yet, so positive cases 
assert the result
+-- type via typeof. Negative cases exercise the ANSI parse-error path and use 
IS NULL so the result
+-- column stays non-nanos (a bare nanos result column is not yet serializable 
by JDBC/thrift).
+select typeof(cast('2022-01-01 00:00:00.123456789' as timestamp_ntz(9)));
+select typeof(cast('2022-01-01 00:00:00.123456789' as timestamp_ltz(7)));
+select cast('a' as timestamp_ntz(9)) is null;
+select cast('a' as timestamp_ltz(9)) is null;
+
 select cast(cast('inf' as double) as timestamp);
 select cast(cast('inf' as float) as timestamp);
 
diff --git a/sql/core/src/test/resources/sql-tests/results/cast.sql.out 
b/sql/core/src/test/resources/sql-tests/results/cast.sql.out
index ca2f739113f1..10b6f4526889 100644
--- a/sql/core/src/test/resources/sql-tests/results/cast.sql.out
+++ b/sql/core/src/test/resources/sql-tests/results/cast.sql.out
@@ -1288,6 +1288,72 @@ org.apache.spark.SparkDateTimeException
 }
 
 
+-- !query
+select typeof(cast('2022-01-01 00:00:00.123456789' as timestamp_ntz(9)))
+-- !query schema
+struct<typeof(CAST(2022-01-01 00:00:00.123456789 AS TIMESTAMP_NTZ(9))):string>
+-- !query output
+timestamp_ntz(9)
+
+
+-- !query
+select typeof(cast('2022-01-01 00:00:00.123456789' as timestamp_ltz(7)))
+-- !query schema
+struct<typeof(CAST(2022-01-01 00:00:00.123456789 AS TIMESTAMP_LTZ(7))):string>
+-- !query output
+timestamp_ltz(7)
+
+
+-- !query
+select cast('a' as timestamp_ntz(9)) is null
+-- !query schema
+struct<>
+-- !query output
+org.apache.spark.SparkDateTimeException
+{
+  "errorClass" : "CAST_INVALID_INPUT",
+  "sqlState" : "22018",
+  "messageParameters" : {
+    "ansiConfig" : "\"spark.sql.ansi.enabled\"",
+    "expression" : "'a'",
+    "sourceType" : "\"STRING\"",
+    "targetType" : "\"TIMESTAMP_NTZ(9)\""
+  },
+  "queryContext" : [ {
+    "objectType" : "",
+    "objectName" : "",
+    "startIndex" : 8,
+    "stopIndex" : 36,
+    "fragment" : "cast('a' as timestamp_ntz(9))"
+  } ]
+}
+
+
+-- !query
+select cast('a' as timestamp_ltz(9)) is null
+-- !query schema
+struct<>
+-- !query output
+org.apache.spark.SparkDateTimeException
+{
+  "errorClass" : "CAST_INVALID_INPUT",
+  "sqlState" : "22018",
+  "messageParameters" : {
+    "ansiConfig" : "\"spark.sql.ansi.enabled\"",
+    "expression" : "'a'",
+    "sourceType" : "\"STRING\"",
+    "targetType" : "\"TIMESTAMP_LTZ(9)\""
+  },
+  "queryContext" : [ {
+    "objectType" : "",
+    "objectName" : "",
+    "startIndex" : 8,
+    "stopIndex" : 36,
+    "fragment" : "cast('a' as timestamp_ltz(9))"
+  } ]
+}
+
+
 -- !query
 select cast(cast('inf' as double) as timestamp)
 -- !query schema
diff --git a/sql/core/src/test/resources/sql-tests/results/nonansi/cast.sql.out 
b/sql/core/src/test/resources/sql-tests/results/nonansi/cast.sql.out
index 64d7b3597055..2b73fe4e63da 100644
--- a/sql/core/src/test/resources/sql-tests/results/nonansi/cast.sql.out
+++ b/sql/core/src/test/resources/sql-tests/results/nonansi/cast.sql.out
@@ -584,6 +584,38 @@ struct<CAST(a AS TIMESTAMP_NTZ):timestamp_ntz>
 NULL
 
 
+-- !query
+select typeof(cast('2022-01-01 00:00:00.123456789' as timestamp_ntz(9)))
+-- !query schema
+struct<typeof(CAST(2022-01-01 00:00:00.123456789 AS TIMESTAMP_NTZ(9))):string>
+-- !query output
+timestamp_ntz(9)
+
+
+-- !query
+select typeof(cast('2022-01-01 00:00:00.123456789' as timestamp_ltz(7)))
+-- !query schema
+struct<typeof(CAST(2022-01-01 00:00:00.123456789 AS TIMESTAMP_LTZ(7))):string>
+-- !query output
+timestamp_ltz(7)
+
+
+-- !query
+select cast('a' as timestamp_ntz(9)) is null
+-- !query schema
+struct<(CAST(a AS TIMESTAMP_NTZ(9)) IS NULL):boolean>
+-- !query output
+true
+
+
+-- !query
+select cast('a' as timestamp_ltz(9)) is null
+-- !query schema
+struct<(CAST(a AS TIMESTAMP_LTZ(9)) IS NULL):boolean>
+-- !query output
+true
+
+
 -- !query
 select cast(cast('inf' as double) as timestamp)
 -- !query schema


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to