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]