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 b31140462cc2 [SPARK-57490][SQL] Support CAST between nanosecond 
timestamp types of different precision
b31140462cc2 is described below

commit b31140462cc22ef2b530a555c21ba06e230d5f55
Author: Maxim Gekk <[email protected]>
AuthorDate: Wed Jun 17 07:45:44 2026 +0200

    [SPARK-57490][SQL] Support CAST between nanosecond timestamp types of 
different precision
    
    ### What changes were proposed in this pull request?
    
    This PR adds support for `CAST` between same-family nanosecond-precision 
timestamp types of different precision:
    - `TIMESTAMP_NTZ(p1)` <-> `TIMESTAMP_NTZ(p2)`
    - `TIMESTAMP_LTZ(q1)` <-> `TIMESTAMP_LTZ(q2)`
    
    where the precisions are in `[7, 9]`.
    
    Both `TimestampNTZNanosType` and `TimestampLTZNanosType` share the same 
physical value `TimestampNanosVal(epochMicros, nanosWithinMicro)` (with 
`nanosWithinMicro` in `[0, 999]`). A cross-precision cast only re-floors the 
sub-microsecond part of the value; `epochMicros` and the time zone are 
untouched:
    - **Widening** (target precision >= source precision): lossless; the value 
is unchanged.
    - **Narrowing** (target precision < source precision): floors 
`nanosWithinMicro` toward the past to the target precision step (drops the 
lowest `9 - precision` sub-microsecond digits), consistent with the existing 
nanos -> micros narrowing rule.
    
    The store-assignment / up-cast contract follows the established micros <-> 
nanos precedent (SPARK-57293):
    - Widening (lossless) is allowed as an ANSI store assignment but is not an 
up-cast.
    - Narrowing (lossy) is explicit-CAST only (rejected by both up-cast and 
store assignment).
    - Equal precision is the identity cast.
    
    Concretely, this adds the `canCast` / `canAnsiCast` / `canANSIStoreAssign` 
rules, the interpreted and codegen eval paths in `Cast`, and a 
`DateTimeUtils.truncateTimestampNanosToPrecision` helper reused by both paths.
    
    In addition, this PR adds SQLQuery coverage in `cast.sql` for the SQL 
parser/typed-literal/`::` cast surface of cross-precision nanos casts, while 
avoiding value-semantics duplication with `CastSuite*`.
    
    ### Why are the changes needed?
    
    The nanosecond-capable timestamp types `TIMESTAMP_NTZ(p)` and 
`TIMESTAMP_LTZ(p)` (p in `[7, 9]`) are gated behind 
`spark.sql.timestampNanosTypes.enabled`. Casts between these types and their 
microsecond counterparts (`TIMESTAMP_NTZ` / `TIMESTAMP`), as well as to/from 
`DATE` and `STRING`, are already supported. However, casting between two 
nanosecond timestamps of the same family but different precision was not 
allowed: such a cast was absent from `Cast.canCast` / `Cast.canAnsiCast` an 
[...]
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes, but only behind the preview flag 
`spark.sql.timestampNanosTypes.enabled`. With the flag enabled, `CAST(... AS 
TIMESTAMP_NTZ(p))` / `CAST(... AS TIMESTAMP_LTZ(p))` from another nanosecond 
timestamp of the same family is now supported instead of failing type checking.
    
    ### How was this patch tested?
    
    Added unit tests in `CastSuiteBase` covering:
    - all NTZ/LTZ precision pairs (widening is lossless, narrowing floors the 
sub-microsecond digits), including pre-epoch (negative-epoch) values and nulls;
    - the store-assignment / up-cast contract for cross-precision pairs;
    - null-cast coverage for cross-precision pairs.
    
    Added SQLQuery tests in `cast.sql` (and regenerated goldens) for parser + 
typed-literal + `::` cross-precision cast paths.
    
    Ran:
    - `build/sbt 'catalyst/testOnly *CastSuite *CastWithAnsiOnSuite 
*CastWithAnsiOffSuite'` (355 tests passed)
    - `build/sbt "sql/testOnly org.apache.spark.sql.SQLQueryTestSuite -- -z 
cast.sql"` (6 tests passed)
    - `build/sbt "sql-api/scalastyle" "catalyst/scalastyle" 
"catalyst/Test/scalastyle"`
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    Generated-by: Cursor
    
    Closes #56544 from MaxGekk/nanos-cast-nanos.
    
    Authored-by: Maxim Gekk <[email protected]>
    Signed-off-by: Max Gekk <[email protected]>
    (cherry picked from commit ad886be8a84f6c7a333d57551bb270fdd103ad45)
    Signed-off-by: Max Gekk <[email protected]>
---
 .../sql/catalyst/util/SparkDateTimeUtils.scala     | 18 +++++
 .../spark/sql/catalyst/expressions/Cast.scala      | 24 ++++++
 .../sql/catalyst/expressions/CastSuiteBase.scala   | 92 ++++++++++++++++++++++
 .../sql-tests/analyzer-results/cast.sql.out        | 56 +++++++++++++
 .../analyzer-results/nonansi/cast.sql.out          | 56 +++++++++++++
 .../src/test/resources/sql-tests/inputs/cast.sql   | 14 ++++
 .../test/resources/sql-tests/results/cast.sql.out  | 64 +++++++++++++++
 .../sql-tests/results/nonansi/cast.sql.out         | 64 +++++++++++++++
 8 files changed, 388 insertions(+)

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 f570d47ec8a0..d85aa65e3454 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
@@ -232,6 +232,24 @@ trait SparkDateTimeUtils {
     }
   }
 
+  /**
+   * Floors a `TimestampNanosVal` to the given timestamp precision `p` in [7, 
9] by truncating its
+   * sub-microsecond `nanosWithinMicro` component (see 
[[truncateNanosWithinMicroToPrecision]]).
+   * `epochMicros` is left untouched, so this only ever drops trailing 
sub-microsecond digits.
+   *
+   * Used by the same-family cross-precision nanosecond cast paths 
(`TIMESTAMP_NTZ(p1) ->
+   * TIMESTAMP_NTZ(p2)` and `TIMESTAMP_LTZ(q1) -> TIMESTAMP_LTZ(q2)`): 
widening (`p2 >= p1`) is
+   * lossless and returns the input unchanged, while narrowing (`p2 < p1`) 
floors toward `-inf` to
+   * the target precision step.
+   */
+  def truncateTimestampNanosToPrecision(
+      v: TimestampNanosVal,
+      precision: Int): TimestampNanosVal = {
+    val truncated = 
truncateNanosWithinMicroToPrecision(v.nanosWithinMicro.toInt, precision)
+    if (truncated == v.nanosWithinMicro) v
+    else TimestampNanosVal.fromParts(v.epochMicros, truncated.toShort)
+  }
+
   /**
    * Converts a `java.time.LocalDateTime` into the composite `(epochMicros, 
nanosWithinMicro)`
    * pair used by `TimestampNTZNanosType(precision)` (interpreted at UTC). 
`epochMicros` comes
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 f3883047bed3..b29e6f5d953b 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
@@ -120,6 +120,9 @@ object Cast extends QueryErrorsBase {
     case (TimestampType, _: TimestampLTZNanosType) => true
     case (_: TimestampLTZNanosType, TimestampType) => true
 
+    case (_: TimestampNTZNanosType, _: TimestampNTZNanosType) => true
+    case (_: TimestampLTZNanosType, _: TimestampLTZNanosType) => true
+
     case (DateType, _: TimestampLTZNanosType) => true
     case (_: TimestampLTZNanosType, DateType) => true
     case (DateType, _: TimestampNTZNanosType) => true
@@ -267,6 +270,9 @@ object Cast extends QueryErrorsBase {
     case (TimestampType, _: TimestampLTZNanosType) => true
     case (_: TimestampLTZNanosType, TimestampType) => true
 
+    case (_: TimestampNTZNanosType, _: TimestampNTZNanosType) => true
+    case (_: TimestampLTZNanosType, _: TimestampLTZNanosType) => true
+
     case (DateType, _: TimestampLTZNanosType) => true
     case (_: TimestampLTZNanosType, DateType) => true
     case (DateType, _: TimestampNTZNanosType) => true
@@ -437,6 +443,12 @@ object Cast extends QueryErrorsBase {
     case (DateType, _: TimestampNTZNanosType) => false
     case (_: TimestampLTZNanosType, DateType) => false
     case (_: TimestampNTZNanosType, DateType) => false
+    // SPARK-57490: same-family cross-precision nanosecond casts: widening 
(e.g. TIMESTAMP_NTZ(7) ->
+    // TIMESTAMP_NTZ(9)) is lossless and allowed as a silent store assignment, 
while narrowing
+    // (e.g. (9) -> (7)) drops sub-microsecond digits and stays explicit-only. 
Equal precision is
+    // already handled by the `from == to` short-circuit above.
+    case (f: TimestampNTZNanosType, t: TimestampNTZNanosType) => f.precision 
<= t.precision
+    case (f: TimestampLTZNanosType, t: TimestampLTZNanosType) => f.precision 
<= t.precision
     case (_: DatetimeType, _: DatetimeType) => true
 
     case (ArrayType(fromType, fn), ArrayType(toType, tn)) =>
@@ -853,6 +865,9 @@ case class Cast(
         })
     case TimestampType =>
       buildCast[Long](_, m => TimestampNanosVal.fromParts(m, 0.toShort))
+    case _: TimestampLTZNanosType =>
+      buildCast[TimestampNanosVal](_, v =>
+        DateTimeUtils.truncateTimestampNanosToPrecision(v, precision))
     case DateType =>
       buildCast[Int](_, d => TimestampNanosVal.fromParts(daysToMicros(d, 
zoneId), 0.toShort))
   }
@@ -869,6 +884,9 @@ case class Cast(
         })
     case TimestampNTZType =>
       buildCast[Long](_, m => TimestampNanosVal.fromParts(m, 0.toShort))
+    case _: TimestampNTZNanosType =>
+      buildCast[TimestampNanosVal](_, v =>
+        DateTimeUtils.truncateTimestampNanosToPrecision(v, precision))
     case DateType =>
       buildCast[Int](_, d =>
         TimestampNanosVal.fromParts(daysToMicros(d, ZoneOffset.UTC), 
0.toShort))
@@ -1911,6 +1929,9 @@ case class Cast(
     case TimestampType =>
       (c, evPrim, evNull) =>
         code"$evPrim = TimestampNanosVal.fromParts($c, (short) 0);"
+    case _: TimestampLTZNanosType =>
+      (c, evPrim, evNull) =>
+        code"$evPrim = $dateTimeUtilsCls.truncateTimestampNanosToPrecision($c, 
$precision);"
     case DateType =>
       val zoneIdClass = classOf[ZoneId]
       val zid = JavaCode.global(
@@ -1948,6 +1969,9 @@ case class Cast(
     case TimestampNTZType =>
       (c, evPrim, evNull) =>
         code"$evPrim = TimestampNanosVal.fromParts($c, (short) 0);"
+    case _: TimestampNTZNanosType =>
+      (c, evPrim, evNull) =>
+        code"$evPrim = $dateTimeUtilsCls.truncateTimestampNanosToPrecision($c, 
$precision);"
     case DateType =>
       (c, evPrim, evNull) =>
         code"$evPrim = TimestampNanosVal.fromParts(" +
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 bb5484b4771d..896b2eddf3f6 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
@@ -101,6 +101,14 @@ abstract class CastSuiteBase extends SparkFunSuite with 
ExpressionEvalHelper {
       checkNullCast(DateType, TimestampLTZNanosType(p))
       checkNullCast(TimestampLTZNanosType(p), DateType)
     }
+    // Same-family cross-precision nanos casts.
+    for {
+      p1 <- TimestampNTZNanosType.MIN_PRECISION to 
TimestampNTZNanosType.MAX_PRECISION
+      p2 <- TimestampNTZNanosType.MIN_PRECISION to 
TimestampNTZNanosType.MAX_PRECISION
+    } {
+      checkNullCast(TimestampNTZNanosType(p1), TimestampNTZNanosType(p2))
+      checkNullCast(TimestampLTZNanosType(p1), TimestampLTZNanosType(p2))
+    }
     checkNullCast(StringType, BinaryType)
     checkNullCast(StringType, BooleanType)
     numericTypes.foreach(dt => checkNullCast(dt, BooleanType))
@@ -765,6 +773,24 @@ abstract class CastSuiteBase extends SparkFunSuite with 
ExpressionEvalHelper {
     }
   }
 
+  test("SPARK-57490: same-family cross-precision nanos store-assignment and 
up-cast contract") {
+    for {
+      p1 <- TimestampNTZNanosType.MIN_PRECISION to 
TimestampNTZNanosType.MAX_PRECISION
+      p2 <- TimestampNTZNanosType.MIN_PRECISION to 
TimestampNTZNanosType.MAX_PRECISION
+    } {
+      // Cross-precision nanos casts are never up-casts (only equal precision 
is, via from == to),
+      // matching the micros <-> nanos precedent above; STRICT store 
assignment rejects them.
+      assert(Cast.canUpCast(TimestampNTZNanosType(p1), 
TimestampNTZNanosType(p2)) == (p1 == p2))
+      assert(Cast.canUpCast(TimestampLTZNanosType(p1), 
TimestampLTZNanosType(p2)) == (p1 == p2))
+      // ANSI store assignment allows lossless widening (p1 <= p2) and equal 
precision, but blocks
+      // lossy narrowing (p1 > p2) to avoid silently dropping sub-microsecond 
digits.
+      assert(Cast.canANSIStoreAssign(TimestampNTZNanosType(p1), 
TimestampNTZNanosType(p2)) ==
+        (p1 <= p2))
+      assert(Cast.canANSIStoreAssign(TimestampLTZNanosType(p1), 
TimestampLTZNanosType(p2)) ==
+        (p1 <= p2))
+    }
+  }
+
   test("SPARK-40389: canUpCast: return false if casting decimal to integral 
types can cause" +
     " overflow") {
     Seq(ByteType, ShortType, IntegerType, LongType).foreach { integralType =>
@@ -1306,6 +1332,72 @@ abstract class CastSuiteBase extends SparkFunSuite with 
ExpressionEvalHelper {
     }
   }
 
+  // Floors a sub-microsecond nanos component (0..999) to the given timestamp 
precision in [7, 9],
+  // mirroring the production truncation rule used by the cross-precision cast.
+  private def floorNanosToPrecision(nanosWithinMicro: Int, precision: Int): 
Int = precision match {
+    case 7 => (nanosWithinMicro / 100) * 100
+    case 8 => (nanosWithinMicro / 10) * 10
+    case 9 => nanosWithinMicro
+  }
+
+  test("SPARK-57490: cast between timestamp_ntz nanos types of different 
precision") {
+    // Same physical (epochMicros, nanosWithinMicro) pair; only the 
sub-microsecond part is
+    // re-floored to the target precision. epochMicros never changes and no 
time zone is involved.
+    val micros = localDateTimeToNanosVal(LocalDateTime.of(2020, 1, 1, 12, 30, 
15, 123456000))
+      .epochMicros
+    val preEpochMicros =
+      localDateTimeToNanosVal(LocalDateTime.of(1969, 12, 31, 23, 59, 59, 
999999000)).epochMicros
+    for {
+      p1 <- TimestampNTZNanosType.MIN_PRECISION to 
TimestampNTZNanosType.MAX_PRECISION
+      p2 <- TimestampNTZNanosType.MIN_PRECISION to 
TimestampNTZNanosType.MAX_PRECISION
+    } {
+      // The source value is already valid at p1 (its sub-micro digits are 
floored to p1); casting
+      // to p2 re-floors to p2, so the net effect is flooring to min(p1, p2). 
Widening (p2 >= p1)
+      // is lossless, narrowing (p2 < p1) drops the extra sub-microsecond 
digits.
+      val srcNanos = floorNanosToPrecision(789, p1)
+      val expectedNanos = floorNanosToPrecision(srcNanos, p2)
+      checkEvaluation(
+        cast(Literal.create(nanosVal(micros, srcNanos), 
TimestampNTZNanosType(p1)),
+          TimestampNTZNanosType(p2)),
+        nanosVal(micros, expectedNanos))
+      // Narrowing floors toward the past for negative epochMicros (pre-1970): 
epochMicros stays
+      // put and only the sub-microsecond digits are dropped.
+      checkEvaluation(
+        cast(Literal.create(nanosVal(preEpochMicros, srcNanos), 
TimestampNTZNanosType(p1)),
+          TimestampNTZNanosType(p2)),
+        nanosVal(preEpochMicros, expectedNanos))
+      // Null input.
+      checkEvaluation(
+        cast(Literal.create(null, TimestampNTZNanosType(p1)), 
TimestampNTZNanosType(p2)), null)
+    }
+  }
+
+  test("SPARK-57490: cast between timestamp_ltz nanos types of different 
precision") {
+    // LTZ(p1) and LTZ(p2) share the same epoch-micros instant; no time zone 
conversion is
+    // involved, only re-flooring of the sub-microsecond part to the target 
precision.
+    val micros = instantToNanosVal(timestampLTZ(2020, 1, 1, 12, 30, 15, 
123456000)).epochMicros
+    val preEpochMicros =
+      instantToNanosVal(timestampLTZ(1969, 12, 31, 23, 59, 59, 
999999000)).epochMicros
+    for {
+      p1 <- TimestampLTZNanosType.MIN_PRECISION to 
TimestampLTZNanosType.MAX_PRECISION
+      p2 <- TimestampLTZNanosType.MIN_PRECISION to 
TimestampLTZNanosType.MAX_PRECISION
+    } {
+      val srcNanos = floorNanosToPrecision(789, p1)
+      val expectedNanos = floorNanosToPrecision(srcNanos, p2)
+      checkEvaluation(
+        cast(Literal.create(nanosVal(micros, srcNanos), 
TimestampLTZNanosType(p1)),
+          TimestampLTZNanosType(p2), UTC_OPT),
+        nanosVal(micros, expectedNanos))
+      checkEvaluation(
+        cast(Literal.create(nanosVal(preEpochMicros, srcNanos), 
TimestampLTZNanosType(p1)),
+          TimestampLTZNanosType(p2), UTC_OPT),
+        nanosVal(preEpochMicros, expectedNanos))
+      checkEvaluation(
+        cast(Literal.create(null, TimestampLTZNanosType(p1)), 
TimestampLTZNanosType(p2), UTC_OPT),
+        null)
+    }
+  }
+
   test("SPARK-57323: cast between date and timestamp_ntz with nanosecond 
precision") {
     // NTZ casts use a fixed UTC wall-clock grid, independent of the session 
time zone.
     val date = LocalDate.of(2020, 1, 1)
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 8af6e17e41c2..bf344eb08e05 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
@@ -816,6 +816,62 @@ Project [cast(cast(cast(2020-01-01 00:00:00.123456 as 
timestamp) as timestamp_lt
 +- OneRowRelation
 
 
+-- !query
+select typeof(timestamp_ntz'2020-01-01 00:00:00.123456789'::timestamp_ntz(7))
+-- !query analysis
+Project [typeof(cast(2020-01-01 00:00:00.123456789 as timestamp_ntz(7))) AS 
typeof(CAST(TIMESTAMP_NTZ '2020-01-01 00:00:00.123456789' AS 
TIMESTAMP_NTZ(7)))#x]
++- OneRowRelation
+
+
+-- !query
+select typeof(timestamp_ltz'2020-01-01 00:00:00.123456789'::timestamp_ltz(8))
+-- !query analysis
+Project [typeof(cast(2020-01-01 00:00:00.123456789 as timestamp_ltz(8))) AS 
typeof(CAST(TIMESTAMP_LTZ '2020-01-01 00:00:00.123456789' AS 
TIMESTAMP_LTZ(8)))#x]
++- OneRowRelation
+
+
+-- !query
+select timestamp_ntz'2020-01-01 00:00:00.123456789'::timestamp_ntz(7)
+-- !query analysis
+Project [cast(2020-01-01 00:00:00.123456789 as timestamp_ntz(7)) AS 
CAST(TIMESTAMP_NTZ '2020-01-01 00:00:00.123456789' AS TIMESTAMP_NTZ(7))#x]
++- OneRowRelation
+
+
+-- !query
+select timestamp_ltz'2020-01-01 00:00:00.123456789'::timestamp_ltz(8)
+-- !query analysis
+Project [cast(2020-01-01 00:00:00.123456789 as timestamp_ltz(8)) AS 
CAST(TIMESTAMP_LTZ '2020-01-01 00:00:00.123456789' AS TIMESTAMP_LTZ(8))#x]
++- OneRowRelation
+
+
+-- !query
+select timestamp_ntz'2020-01-01 
00:00:00.123456789'::timestamp_ntz(7)::timestamp_ntz(9)
+-- !query analysis
+Project [cast(cast(2020-01-01 00:00:00.123456789 as timestamp_ntz(7)) as 
timestamp_ntz(9)) AS CAST(CAST(TIMESTAMP_NTZ '2020-01-01 00:00:00.123456789' AS 
TIMESTAMP_NTZ(7)) AS TIMESTAMP_NTZ(9))#x]
++- OneRowRelation
+
+
+-- !query
+select timestamp_ltz'1960-01-01 
00:00:00.123456789'::timestamp_ltz(9)::timestamp_ltz(7)
+-- !query analysis
+Project [cast(cast(1960-01-01 00:00:00.123456789 as timestamp_ltz(9)) as 
timestamp_ltz(7)) AS CAST(CAST(TIMESTAMP_LTZ '1960-01-01 00:00:00.123456789' AS 
TIMESTAMP_LTZ(9)) AS TIMESTAMP_LTZ(7))#x]
++- OneRowRelation
+
+
+-- !query
+select cast(null as timestamp_ntz(9))::timestamp_ntz(7)
+-- !query analysis
+Project [cast(cast(null as timestamp_ntz(9)) as timestamp_ntz(7)) AS 
CAST(CAST(NULL AS TIMESTAMP_NTZ(9)) AS TIMESTAMP_NTZ(7))#x]
++- OneRowRelation
+
+
+-- !query
+select cast(null as timestamp_ltz(8))::timestamp_ltz(9)
+-- !query analysis
+Project [cast(cast(null as timestamp_ltz(8)) as timestamp_ltz(9)) AS 
CAST(CAST(NULL AS TIMESTAMP_LTZ(8)) AS TIMESTAMP_LTZ(9))#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 fb1662484aa9..c6ee17e8e387 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
@@ -680,6 +680,62 @@ Project [cast(cast(cast(2020-01-01 00:00:00.123456 as 
timestamp) as timestamp_lt
 +- OneRowRelation
 
 
+-- !query
+select typeof(timestamp_ntz'2020-01-01 00:00:00.123456789'::timestamp_ntz(7))
+-- !query analysis
+Project [typeof(cast(2020-01-01 00:00:00.123456789 as timestamp_ntz(7))) AS 
typeof(CAST(TIMESTAMP_NTZ '2020-01-01 00:00:00.123456789' AS 
TIMESTAMP_NTZ(7)))#x]
++- OneRowRelation
+
+
+-- !query
+select typeof(timestamp_ltz'2020-01-01 00:00:00.123456789'::timestamp_ltz(8))
+-- !query analysis
+Project [typeof(cast(2020-01-01 00:00:00.123456789 as timestamp_ltz(8))) AS 
typeof(CAST(TIMESTAMP_LTZ '2020-01-01 00:00:00.123456789' AS 
TIMESTAMP_LTZ(8)))#x]
++- OneRowRelation
+
+
+-- !query
+select timestamp_ntz'2020-01-01 00:00:00.123456789'::timestamp_ntz(7)
+-- !query analysis
+Project [cast(2020-01-01 00:00:00.123456789 as timestamp_ntz(7)) AS 
CAST(TIMESTAMP_NTZ '2020-01-01 00:00:00.123456789' AS TIMESTAMP_NTZ(7))#x]
++- OneRowRelation
+
+
+-- !query
+select timestamp_ltz'2020-01-01 00:00:00.123456789'::timestamp_ltz(8)
+-- !query analysis
+Project [cast(2020-01-01 00:00:00.123456789 as timestamp_ltz(8)) AS 
CAST(TIMESTAMP_LTZ '2020-01-01 00:00:00.123456789' AS TIMESTAMP_LTZ(8))#x]
++- OneRowRelation
+
+
+-- !query
+select timestamp_ntz'2020-01-01 
00:00:00.123456789'::timestamp_ntz(7)::timestamp_ntz(9)
+-- !query analysis
+Project [cast(cast(2020-01-01 00:00:00.123456789 as timestamp_ntz(7)) as 
timestamp_ntz(9)) AS CAST(CAST(TIMESTAMP_NTZ '2020-01-01 00:00:00.123456789' AS 
TIMESTAMP_NTZ(7)) AS TIMESTAMP_NTZ(9))#x]
++- OneRowRelation
+
+
+-- !query
+select timestamp_ltz'1960-01-01 
00:00:00.123456789'::timestamp_ltz(9)::timestamp_ltz(7)
+-- !query analysis
+Project [cast(cast(1960-01-01 00:00:00.123456789 as timestamp_ltz(9)) as 
timestamp_ltz(7)) AS CAST(CAST(TIMESTAMP_LTZ '1960-01-01 00:00:00.123456789' AS 
TIMESTAMP_LTZ(9)) AS TIMESTAMP_LTZ(7))#x]
++- OneRowRelation
+
+
+-- !query
+select cast(null as timestamp_ntz(9))::timestamp_ntz(7)
+-- !query analysis
+Project [cast(cast(null as timestamp_ntz(9)) as timestamp_ntz(7)) AS 
CAST(CAST(NULL AS TIMESTAMP_NTZ(9)) AS TIMESTAMP_NTZ(7))#x]
++- OneRowRelation
+
+
+-- !query
+select cast(null as timestamp_ltz(8))::timestamp_ltz(9)
+-- !query analysis
+Project [cast(cast(null as timestamp_ltz(8)) as timestamp_ltz(9)) AS 
CAST(CAST(NULL AS TIMESTAMP_LTZ(8)) AS TIMESTAMP_LTZ(9))#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 3d8636900938..a9e855106624 100644
--- a/sql/core/src/test/resources/sql-tests/inputs/cast.sql
+++ b/sql/core/src/test/resources/sql-tests/inputs/cast.sql
@@ -150,6 +150,20 @@ select cast(cast('2020-01-01 00:00:00.123456' as 
timestamp) as timestamp_ltz(9))
 select cast(cast('2020-01-01 00:00:00.123456789' as timestamp_ltz(9)) as 
timestamp);
 select cast(cast(cast('2020-01-01 00:00:00.123456' as timestamp) as 
timestamp_ltz(9)) as timestamp);
 
+-- SPARK-57490: cast between nanosecond timestamp types of different precision.
+-- Keep SQL-layer coverage focused on parser / typed-literal / :: resolution 
paths.
+-- Value semantics (flooring, widening, pre-epoch behavior, null propagation) 
are covered in
+-- CastSuite* unit tests.
+select typeof(timestamp_ntz'2020-01-01 00:00:00.123456789'::timestamp_ntz(7));
+select typeof(timestamp_ltz'2020-01-01 00:00:00.123456789'::timestamp_ltz(8));
+-- Exercise both typed literals and nested/chained :: casts in SQL text 
parsing.
+select timestamp_ntz'2020-01-01 00:00:00.123456789'::timestamp_ntz(7);
+select timestamp_ltz'2020-01-01 00:00:00.123456789'::timestamp_ltz(8);
+select timestamp_ntz'2020-01-01 
00:00:00.123456789'::timestamp_ntz(7)::timestamp_ntz(9);
+select timestamp_ltz'1960-01-01 
00:00:00.123456789'::timestamp_ltz(9)::timestamp_ltz(7);
+select cast(null as timestamp_ntz(9))::timestamp_ntz(7);
+select cast(null as timestamp_ltz(8))::timestamp_ltz(9);
+
 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 f152fa699184..b6da2f1c32e2 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
@@ -1522,6 +1522,70 @@ struct<CAST(CAST(CAST(2020-01-01 00:00:00.123456 AS 
TIMESTAMP) AS TIMESTAMP_LTZ(
 2020-01-01 00:00:00.123456
 
 
+-- !query
+select typeof(timestamp_ntz'2020-01-01 00:00:00.123456789'::timestamp_ntz(7))
+-- !query schema
+struct<typeof(CAST(TIMESTAMP_NTZ '2020-01-01 00:00:00.123456789' AS 
TIMESTAMP_NTZ(7))):string>
+-- !query output
+timestamp_ntz(7)
+
+
+-- !query
+select typeof(timestamp_ltz'2020-01-01 00:00:00.123456789'::timestamp_ltz(8))
+-- !query schema
+struct<typeof(CAST(TIMESTAMP_LTZ '2020-01-01 00:00:00.123456789' AS 
TIMESTAMP_LTZ(8))):string>
+-- !query output
+timestamp_ltz(8)
+
+
+-- !query
+select timestamp_ntz'2020-01-01 00:00:00.123456789'::timestamp_ntz(7)
+-- !query schema
+struct<CAST(TIMESTAMP_NTZ '2020-01-01 00:00:00.123456789' AS 
TIMESTAMP_NTZ(7)):timestamp_ntz(7)>
+-- !query output
+2020-01-01 00:00:00.1234567
+
+
+-- !query
+select timestamp_ltz'2020-01-01 00:00:00.123456789'::timestamp_ltz(8)
+-- !query schema
+struct<CAST(TIMESTAMP_LTZ '2020-01-01 00:00:00.123456789' AS 
TIMESTAMP_LTZ(8)):timestamp_ltz(8)>
+-- !query output
+2020-01-01 00:00:00.12345678
+
+
+-- !query
+select timestamp_ntz'2020-01-01 
00:00:00.123456789'::timestamp_ntz(7)::timestamp_ntz(9)
+-- !query schema
+struct<CAST(CAST(TIMESTAMP_NTZ '2020-01-01 00:00:00.123456789' AS 
TIMESTAMP_NTZ(7)) AS TIMESTAMP_NTZ(9)):timestamp_ntz(9)>
+-- !query output
+2020-01-01 00:00:00.1234567
+
+
+-- !query
+select timestamp_ltz'1960-01-01 
00:00:00.123456789'::timestamp_ltz(9)::timestamp_ltz(7)
+-- !query schema
+struct<CAST(CAST(TIMESTAMP_LTZ '1960-01-01 00:00:00.123456789' AS 
TIMESTAMP_LTZ(9)) AS TIMESTAMP_LTZ(7)):timestamp_ltz(7)>
+-- !query output
+1960-01-01 00:00:00.1234567
+
+
+-- !query
+select cast(null as timestamp_ntz(9))::timestamp_ntz(7)
+-- !query schema
+struct<CAST(CAST(NULL AS TIMESTAMP_NTZ(9)) AS 
TIMESTAMP_NTZ(7)):timestamp_ntz(7)>
+-- !query output
+NULL
+
+
+-- !query
+select cast(null as timestamp_ltz(8))::timestamp_ltz(9)
+-- !query schema
+struct<CAST(CAST(NULL AS TIMESTAMP_LTZ(8)) AS 
TIMESTAMP_LTZ(9)):timestamp_ltz(9)>
+-- !query output
+NULL
+
+
 -- !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 466eb68cb5ff..408cb06ef36b 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
@@ -784,6 +784,70 @@ struct<CAST(CAST(CAST(2020-01-01 00:00:00.123456 AS 
TIMESTAMP) AS TIMESTAMP_LTZ(
 2020-01-01 00:00:00.123456
 
 
+-- !query
+select typeof(timestamp_ntz'2020-01-01 00:00:00.123456789'::timestamp_ntz(7))
+-- !query schema
+struct<typeof(CAST(TIMESTAMP_NTZ '2020-01-01 00:00:00.123456789' AS 
TIMESTAMP_NTZ(7))):string>
+-- !query output
+timestamp_ntz(7)
+
+
+-- !query
+select typeof(timestamp_ltz'2020-01-01 00:00:00.123456789'::timestamp_ltz(8))
+-- !query schema
+struct<typeof(CAST(TIMESTAMP_LTZ '2020-01-01 00:00:00.123456789' AS 
TIMESTAMP_LTZ(8))):string>
+-- !query output
+timestamp_ltz(8)
+
+
+-- !query
+select timestamp_ntz'2020-01-01 00:00:00.123456789'::timestamp_ntz(7)
+-- !query schema
+struct<CAST(TIMESTAMP_NTZ '2020-01-01 00:00:00.123456789' AS 
TIMESTAMP_NTZ(7)):timestamp_ntz(7)>
+-- !query output
+2020-01-01 00:00:00.1234567
+
+
+-- !query
+select timestamp_ltz'2020-01-01 00:00:00.123456789'::timestamp_ltz(8)
+-- !query schema
+struct<CAST(TIMESTAMP_LTZ '2020-01-01 00:00:00.123456789' AS 
TIMESTAMP_LTZ(8)):timestamp_ltz(8)>
+-- !query output
+2020-01-01 00:00:00.12345678
+
+
+-- !query
+select timestamp_ntz'2020-01-01 
00:00:00.123456789'::timestamp_ntz(7)::timestamp_ntz(9)
+-- !query schema
+struct<CAST(CAST(TIMESTAMP_NTZ '2020-01-01 00:00:00.123456789' AS 
TIMESTAMP_NTZ(7)) AS TIMESTAMP_NTZ(9)):timestamp_ntz(9)>
+-- !query output
+2020-01-01 00:00:00.1234567
+
+
+-- !query
+select timestamp_ltz'1960-01-01 
00:00:00.123456789'::timestamp_ltz(9)::timestamp_ltz(7)
+-- !query schema
+struct<CAST(CAST(TIMESTAMP_LTZ '1960-01-01 00:00:00.123456789' AS 
TIMESTAMP_LTZ(9)) AS TIMESTAMP_LTZ(7)):timestamp_ltz(7)>
+-- !query output
+1960-01-01 00:00:00.1234567
+
+
+-- !query
+select cast(null as timestamp_ntz(9))::timestamp_ntz(7)
+-- !query schema
+struct<CAST(CAST(NULL AS TIMESTAMP_NTZ(9)) AS 
TIMESTAMP_NTZ(7)):timestamp_ntz(7)>
+-- !query output
+NULL
+
+
+-- !query
+select cast(null as timestamp_ltz(8))::timestamp_ltz(9)
+-- !query schema
+struct<CAST(CAST(NULL AS TIMESTAMP_LTZ(8)) AS 
TIMESTAMP_LTZ(9)):timestamp_ltz(9)>
+-- !query output
+NULL
+
+
 -- !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