This is an automated email from the ASF dual-hosted git repository.
MaxGekk pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/spark.git
The following commit(s) were added to refs/heads/master by this push:
new 860195ac2120 [SPARK-57511][SQL] Support explicit CAST between
TIMESTAMP_LTZ(p) and TIMESTAMP_NTZ(q) for p, q in [6, 9]
860195ac2120 is described below
commit 860195ac2120b5e8c5a79d126be5e5a1a41b2674
Author: Maxim Gekk <[email protected]>
AuthorDate: Fri Jun 19 07:48:13 2026 +0200
[SPARK-57511][SQL] Support explicit CAST between TIMESTAMP_LTZ(p) and
TIMESTAMP_NTZ(q) for p, q in [6, 9]
### What changes were proposed in this pull request?
This PR adds explicit `CAST` support between the nanosecond-capable
`TIMESTAMP_LTZ(p)` and `TIMESTAMP_NTZ(q)` types for `p, q in [6, 9]`, i.e.:
- `CAST(<timestamp_ltz(p)> AS TIMESTAMP_NTZ(q))`
- `CAST(<timestamp_ntz(p)> AS TIMESTAMP_LTZ(q))`
Recall that the parser maps precision `6` to the microsecond family members
(`TIMESTAMP_LTZ(6)` = `TIMESTAMP`, `TIMESTAMP_NTZ(6)` = `TIMESTAMP_NTZ`), so
the full matrix is covered by two groups:
- nanos <-> nanos: `TimestampLTZNanosType(p) <-> TimestampNTZNanosType(q)`
for `p, q in [7, 9]`;
- the precision-6 boundary, which mixes a micro family member with the
other family's nanos member: `TIMESTAMP <-> TimestampNTZNanosType(q)` and
`TIMESTAMP_NTZ <-> TimestampLTZNanosType(p)`.
Concretely, in `Cast.scala`:
- `canCast` / `canAnsiCast`: allow the cross-family directions above.
- `needsTimeZone`: the conversion reinterprets an absolute instant (LTZ) as
a wall-clock local date-time (NTZ) and vice versa, so it is session-time-zone
dependent (mirroring micro `TIMESTAMP <-> TIMESTAMP_NTZ`).
- `canANSIStoreAssign`: the cross-family casts stay explicit-only (not
silent store assignments) while the nanosecond types are unreleased; the
all-micro `TIMESTAMP <-> TIMESTAMP_NTZ` pair remains store-assignable.
- Interpreted and codegen conversion paths for the new source/target
combinations, reusing the existing `convertTz` micro semantics plus precision
flooring for the sub-microsecond part.
`SparkDateTimeUtils.scala` gains two small helpers,
`timestampLTZNanosToNTZNanos` and `timestampNTZNanosToLTZNanos`, that compose
the existing public conversion utilities (`timestampNanosToInstant`,
`localDateTimeToTimestampNanos`, etc.).
Out of scope (unchanged): implicit type coercion / common-type resolution
(e.g. `CASE`, `UNION` wider-type inference).
### Why are the changes needed?
Spark already supports same-family cross-precision nanos casts
(`TIMESTAMP_NTZ(p) -> TIMESTAMP_NTZ(q)`, `TIMESTAMP_LTZ(p) ->
TIMESTAMP_LTZ(q)`) and the microsecond `TIMESTAMP <-> TIMESTAMP_NTZ` casts, but
direct cross-family casts between `TIMESTAMP_LTZ(p)` and `TIMESTAMP_NTZ(q)`
were not supported for `p, q in [6, 9]`. This closes that gap so explicit
`CAST(...)` has consistent timestamp parity. This is a sub-task of SPARK-56822
(SPIP: Timestamps with nanosecond precision).
### Does this PR introduce _any_ user-facing change?
Yes, within the unreleased nanosecond-timestamp feature (gated by
`spark.sql.timestampNanosTypes.enabled`). Explicit casts that previously failed
type checking, e.g.:
```sql
SELECT CAST(TIMESTAMP_LTZ '2020-01-01 00:00:00.123456789' AS
TIMESTAMP_NTZ(7));
SELECT CAST(TIMESTAMP_NTZ '2020-01-01 00:00:00.123456789' AS
TIMESTAMP_LTZ(9));
```
now succeed, reinterpreting the value against the session time zone and
flooring the fractional second to the target precision. There is no change to
already-released types.
### How was this patch tested?
- Added catalyst unit tests in `CastSuiteBase` (run under both ANSI
on/off): admissibility / store-assignment / up-cast / `needsTimeZone` /
null-safety (`!forceNullable`) contracts (nanos<->nanos and the micro
boundary), value tests across `p, q in [7, 9]` in UTC and `America/Los_Angeles`
(widening keeps a zero sub-microsecond part, narrowing floors, pre-epoch and
null cases), a round-trip test, and the micro family member (precision 6)
to/from nanos test.
- Added a DST resolution test exercising the `America/Los_Angeles`
spring-forward gap (`2020-03-08 02:30`, non-existent local time -> shifted
forward) and fall-back overlap (`2020-11-01 01:30`, ambiguous -> earlier
offset), asserting the nanos `NTZ <-> LTZ` cast matches the production micro
`TIMESTAMP_NTZ <-> TIMESTAMP` cast on the epoch-micros part (expected micro
values obtained via `evaluateWithoutCodegen`).
- Extended `cast.sql` golden coverage (type resolution, lossless same-zone
round-trips, narrowing truncation, null propagation, and the precision-6
boundary) and regenerated the result/analyzer goldens, including the non-ANSI
variants.
- `./dev/scalastyle` passes.
### Was this patch authored or co-authored using generative AI tooling?
Generated-by: Cursor
Closes #56577 from MaxGekk/ltz-ntz-nanos-cast.
Authored-by: Maxim Gekk <[email protected]>
Signed-off-by: Max Gekk <[email protected]>
---
.../sql/catalyst/util/SparkDateTimeUtils.scala | 31 +++
.../spark/sql/catalyst/expressions/Cast.scala | 159 ++++++++-----
.../sql/catalyst/expressions/CastSuiteBase.scala | 247 ++++++++++++++++++++-
.../sql-tests/analyzer-results/cast.sql.out | 152 +++++++++++++
.../analyzer-results/nonansi/cast.sql.out | 152 +++++++++++++
.../src/test/resources/sql-tests/inputs/cast.sql | 35 +++
.../test/resources/sql-tests/results/cast.sql.out | 176 +++++++++++++++
.../sql-tests/results/nonansi/cast.sql.out | 176 +++++++++++++++
8 files changed, 1069 insertions(+), 59 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 ea20057278ce..2b992ba53fe8 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
@@ -310,6 +310,37 @@ trait SparkDateTimeUtils {
microsToInstant(v.epochMicros).plusNanos(v.nanosWithinMicro.toLong)
}
+ /**
+ * Converts a `TIMESTAMP_LTZ(p)` nanosecond value into the
`TIMESTAMP_NTZ(precision)` wall-clock
+ * value observed in the time zone `zoneId`. The LTZ value denotes an
absolute instant;
+ * rendering it as a local date-time at `zoneId` yields the NTZ
representation. Time-zone
+ * offsets shift only whole seconds, so the sub-microsecond
`nanosWithinMicro` component is
+ * preserved before being floored to the target `precision` (same flooring
as same-family
+ * narrowing casts).
+ */
+ def timestampLTZNanosToNTZNanos(
+ v: TimestampNanosVal,
+ zoneId: ZoneId,
+ precision: Int): TimestampNanosVal = {
+ val localDateTime =
timestampNanosToInstant(v).atZone(zoneId).toLocalDateTime
+ localDateTimeToTimestampNanos(localDateTime, precision)
+ }
+
+ /**
+ * Converts a `TIMESTAMP_NTZ(q)` nanosecond value into the
`TIMESTAMP_LTZ(precision)` instant
+ * obtained by interpreting its wall-clock local date-time in the time zone
`zoneId`. This is
+ * the reverse of [[timestampLTZNanosToNTZNanos]]; the sub-microsecond
`nanosWithinMicro`
+ * component is preserved across the (whole-second) offset shift before
being floored to the
+ * target `precision`.
+ */
+ def timestampNTZNanosToLTZNanos(
+ v: TimestampNanosVal,
+ zoneId: ZoneId,
+ precision: Int): TimestampNanosVal = {
+ val instant = timestampNanosToLocalDateTime(v).atZone(zoneId).toInstant
+ instantToTimestampNanos(instant, precision)
+ }
+
/**
* Converts the local date to the number of days since 1970-01-01.
*/
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 b29e6f5d953b..506babb08f34 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
@@ -115,13 +115,13 @@ object Cast extends QueryErrorsBase {
case (_: StringType, _: AnyTimestampNanoType) => true
- case (TimestampNTZType, _: TimestampNTZNanosType) => true
- case (_: TimestampNTZNanosType, TimestampNTZType) => true
- case (TimestampType, _: TimestampLTZNanosType) => true
- case (_: TimestampLTZNanosType, TimestampType) => true
-
- case (_: TimestampNTZNanosType, _: TimestampNTZNanosType) => true
- case (_: TimestampLTZNanosType, _: TimestampLTZNanosType) => true
+ // Casts between timestamp types where at least one side is a nanosecond
type are allowed for
+ // every micro/nanos and nanos/nanos combination, across both time-zone
families. Precision 6 is
+ // the micro family member (TIMESTAMP_LTZ(6) = TIMESTAMP, TIMESTAMP_NTZ(6)
= TIMESTAMP_NTZ),
+ // matched here via AnyTimestampType; the all-micro TIMESTAMP <->
TIMESTAMP_NTZ pair is above.
+ case (_: AnyTimestampNanoType, _: AnyTimestampNanoType) => true
+ case (t, _: AnyTimestampNanoType) if AnyTimestampType.acceptsType(t) =>
true
+ case (_: AnyTimestampNanoType, t) if AnyTimestampType.acceptsType(t) =>
true
case (DateType, _: TimestampLTZNanosType) => true
case (_: TimestampLTZNanosType, DateType) => true
@@ -265,13 +265,13 @@ object Cast extends QueryErrorsBase {
case (_: StringType, _: AnyTimestampNanoType) => true
- case (TimestampNTZType, _: TimestampNTZNanosType) => true
- case (_: TimestampNTZNanosType, TimestampNTZType) => true
- case (TimestampType, _: TimestampLTZNanosType) => true
- case (_: TimestampLTZNanosType, TimestampType) => true
-
- case (_: TimestampNTZNanosType, _: TimestampNTZNanosType) => true
- case (_: TimestampLTZNanosType, _: TimestampLTZNanosType) => true
+ // Casts between timestamp types where at least one side is a nanosecond
type are allowed for
+ // every micro/nanos and nanos/nanos combination, across both time-zone
families. Precision 6 is
+ // the micro family member (TIMESTAMP_LTZ(6) = TIMESTAMP, TIMESTAMP_NTZ(6)
= TIMESTAMP_NTZ),
+ // matched here via AnyTimestampType; the all-micro TIMESTAMP <->
TIMESTAMP_NTZ pair is above.
+ case (_: AnyTimestampNanoType, _: AnyTimestampNanoType) => true
+ case (t, _: AnyTimestampNanoType) if AnyTimestampType.acceptsType(t) =>
true
+ case (_: AnyTimestampNanoType, t) if AnyTimestampType.acceptsType(t) =>
true
case (DateType, _: TimestampLTZNanosType) => true
case (_: TimestampLTZNanosType, DateType) => true
@@ -365,6 +365,16 @@ object Cast extends QueryErrorsBase {
case (TimestampType, DateType) => true
case (TimestampType, TimestampNTZType) => true
case (TimestampNTZType, TimestampType) => true
+ // Cross-family nanosecond casts convert between an absolute instant (LTZ)
and a wall-clock
+ // local date-time (NTZ), so they depend on the session time zone,
mirroring the micro
+ // TIMESTAMP <-> TIMESTAMP_NTZ pair above. This includes the mixed
micro/nanos cases where one
+ // side is the precision-6 micro family member (TIMESTAMP / TIMESTAMP_NTZ).
+ case (_: TimestampLTZNanosType, _: TimestampNTZNanosType) => true
+ case (_: TimestampNTZNanosType, _: TimestampLTZNanosType) => true
+ case (TimestampType, _: TimestampNTZNanosType) => true
+ case (_: TimestampNTZNanosType, TimestampType) => true
+ case (TimestampNTZType, _: TimestampLTZNanosType) => true
+ case (_: TimestampLTZNanosType, TimestampNTZType) => true
// NTZ string is zone-independent (mirroring micro TIMESTAMP_NTZ, which is
not listed); only
// the LTZ string parse/render depends on the session time zone.
case (_: StringType, _: TimestampLTZNanosType) => true
@@ -428,27 +438,30 @@ object Cast extends QueryErrorsBase {
case (_: NumericType, _: NumericType) => true
case (_: AtomicType, _: StringType) => true
case (_: CalendarIntervalType, _: StringType) => true
- // SPARK-57293: narrowing a nanosecond-precision timestamp to its
microsecond counterpart
- // drops the sub-microsecond digits, so it is not allowed as a (silent)
store assignment.
- // This conversion stays explicit-only.
- case (_: TimestampNTZNanosType, TimestampNTZType) => false
- case (_: TimestampLTZNanosType, TimestampType) => false
- // SPARK-57323: DATE <-> nanosecond-precision timestamp requires an
explicit CAST in both
- // directions. nanos -> DATE silently drops time-of-day and
sub-microsecond digits (the same
- // rule as the narrowing above). DATE -> nanos is lossless (midnight, zero
sub-micro part),
- // but conversions involving the nanos types stay explicit-only while the
types are
- // unreleased: allowing the store assignment later is a compatible change,
revoking it is not.
- // Note this is stricter than micro DATE <-> TIMESTAMP[_NTZ], which the
catch-all below allows.
- case (DateType, _: TimestampLTZNanosType) => false
- 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.
+ // handled by the `from == to` short-circuit above; micros -> nanos
widening (e.g. TIMESTAMP_NTZ
+ // -> TIMESTAMP_NTZ(9)) is lossless and falls to the catch-all below.
case (f: TimestampNTZNanosType, t: TimestampNTZNanosType) => f.precision
<= t.precision
case (f: TimestampLTZNanosType, t: TimestampLTZNanosType) => f.precision
<= t.precision
+ // SPARK-57323: DATE <-> nanosecond-precision timestamp requires an
explicit CAST in both
+ // directions (nanos -> DATE drops fields; DATE -> nanos is lossless but
kept explicit-only
+ // while the nanos types are unreleased). Stricter than micro DATE <->
TIMESTAMP[_NTZ], which
+ // the catch-all below allows.
+ case (DateType, _: AnyTimestampNanoType) => false
+ case (_: AnyTimestampNanoType, DateType) => false
+ // SPARK-57293/57511: narrowing any nanosecond timestamp to a microsecond
timestamp drops the
+ // sub-microsecond digits, and cross-family casts additionally reinterpret
the value against the
+ // session time zone; both stay explicit-only rather than silent store
assignments while the
+ // nanos types are unreleased. This covers same-family narrowing (nanos ->
micro), cross-family
+ // nanos <-> nanos, and the mixed micro/nanos pairs at the precision-6
boundary; everything
+ // matched here is explicit-only. The all-micro TIMESTAMP <->
TIMESTAMP_NTZ pair and micros ->
+ // nanos same-family widening stay store-assignable via the catch-all
below.
+ case (_: AnyTimestampNanoType, t) if AnyTimestampType.acceptsType(t) =>
false
+ case (TimestampType, _: TimestampNTZNanosType) => false
+ case (TimestampNTZType, _: TimestampLTZNanosType) => false
+ case (_: AnyTimestampNanoType, _: AnyTimestampNanoType) => false
case (_: DatetimeType, _: DatetimeType) => true
case (ArrayType(fromType, fn), ArrayType(toType, tn)) =>
@@ -817,6 +830,8 @@ case class Cast(
buildCast[Long](_, ts => convertTz(ts, zoneId, ZoneOffset.UTC))
case _: TimestampLTZNanosType =>
buildCast[TimestampNanosVal](_, v => v.epochMicros)
+ case _: TimestampNTZNanosType =>
+ buildCast[TimestampNanosVal](_, v => convertTz(v.epochMicros, zoneId,
ZoneOffset.UTC))
// TimestampWritable.decimalToTimestamp
case DecimalType() =>
buildCast[Decimal](_, d => decimalToTimestamp(d))
@@ -851,6 +866,8 @@ case class Cast(
buildCast[Long](_, ts => convertTz(ts, ZoneOffset.UTC, zoneId))
case _: TimestampNTZNanosType =>
buildCast[TimestampNanosVal](_, v => v.epochMicros)
+ case _: TimestampLTZNanosType =>
+ buildCast[TimestampNanosVal](_, v => convertTz(v.epochMicros,
ZoneOffset.UTC, zoneId))
}
private[this] def castToTimestampLTZNanos(
@@ -865,9 +882,15 @@ case class Cast(
})
case TimestampType =>
buildCast[Long](_, m => TimestampNanosVal.fromParts(m, 0.toShort))
+ case TimestampNTZType =>
+ buildCast[Long](_, m =>
+ TimestampNanosVal.fromParts(convertTz(m, zoneId, ZoneOffset.UTC),
0.toShort))
case _: TimestampLTZNanosType =>
buildCast[TimestampNanosVal](_, v =>
DateTimeUtils.truncateTimestampNanosToPrecision(v, precision))
+ case _: TimestampNTZNanosType =>
+ buildCast[TimestampNanosVal](_, v =>
+ DateTimeUtils.timestampNTZNanosToLTZNanos(v, zoneId, precision))
case DateType =>
buildCast[Int](_, d => TimestampNanosVal.fromParts(daysToMicros(d,
zoneId), 0.toShort))
}
@@ -884,9 +907,15 @@ case class Cast(
})
case TimestampNTZType =>
buildCast[Long](_, m => TimestampNanosVal.fromParts(m, 0.toShort))
+ case TimestampType =>
+ buildCast[Long](_, m =>
+ TimestampNanosVal.fromParts(convertTz(m, ZoneOffset.UTC, zoneId),
0.toShort))
case _: TimestampNTZNanosType =>
buildCast[TimestampNanosVal](_, v =>
DateTimeUtils.truncateTimestampNanosToPrecision(v, precision))
+ case _: TimestampLTZNanosType =>
+ buildCast[TimestampNanosVal](_, v =>
+ DateTimeUtils.timestampLTZNanosToNTZNanos(v, zoneId, precision))
case DateType =>
buildCast[Int](_, d =>
TimestampNanosVal.fromParts(daysToMicros(d, ZoneOffset.UTC),
0.toShort))
@@ -1580,6 +1609,14 @@ case class Cast(
code"$evPrim =
${NumberConverter.getClass.getName.stripSuffix("$")}.toBinary($c);"
}
+ // Registers the session `zoneId` as a codegen reference object and returns
a handle to it.
+ // Kept as a method (rather than a pre-`match` val) so the reference is only
added for the arms
+ // that actually need the session time zone, not for zone-independent casts.
+ private[this] def zoneIdValue(ctx: CodegenContext): GlobalValue = {
+ val zoneIdClass = classOf[ZoneId]
+ JavaCode.global(ctx.addReferenceObj("zoneId", zoneId,
zoneIdClass.getName), zoneIdClass)
+ }
+
private[this] def castToDateCode(
from: DataType,
ctx: CodegenContext): CastFunction = {
@@ -1604,16 +1641,14 @@ case class Cast(
}
case TimestampType =>
- val zidClass = classOf[ZoneId]
- val zid = JavaCode.global(ctx.addReferenceObj("zoneId", zoneId,
zidClass.getName), zidClass)
+ val zid = zoneIdValue(ctx)
(c, evPrim, evNull) =>
code"""$evPrim = $dateTimeUtilsCls.microsToDays($c, $zid);"""
case TimestampNTZType =>
(c, evPrim, evNull) =>
code"$evPrim = $dateTimeUtilsCls.microsToDays($c,
java.time.ZoneOffset.UTC);"
case _: TimestampLTZNanosType =>
- val zidClass = classOf[ZoneId]
- val zid = JavaCode.global(ctx.addReferenceObj("zoneId", zoneId,
zidClass.getName), zidClass)
+ val zid = zoneIdValue(ctx)
(c, evPrim, evNull) =>
code"$evPrim = $dateTimeUtilsCls.microsToDays($c.epochMicros, $zid);"
case _: TimestampNTZNanosType =>
@@ -1790,10 +1825,7 @@ case class Cast(
from: DataType,
ctx: CodegenContext): CastFunction = from match {
case _: StringType =>
- val zoneIdClass = classOf[ZoneId]
- val zid = JavaCode.global(
- ctx.addReferenceObj("zoneId", zoneId, zoneIdClass.getName),
- zoneIdClass)
+ val zid = zoneIdValue(ctx)
val longOpt = ctx.freshVariable("longOpt", classOf[Option[Long]])
(c, evPrim, evNull) =>
if (ansiEnabled) {
@@ -1816,21 +1848,19 @@ case class Cast(
case _: IntegralType =>
(c, evPrim, evNull) => code"$evPrim = ${longToTimeStampCode(c)};"
case DateType =>
- val zoneIdClass = classOf[ZoneId]
- val zid = JavaCode.global(
- ctx.addReferenceObj("zoneId", zoneId, zoneIdClass.getName),
- zoneIdClass)
+ val zid = zoneIdValue(ctx)
(c, evPrim, evNull) =>
code"""$evPrim = $dateTimeUtilsCls.daysToMicros($c, $zid);"""
case TimestampNTZType =>
- val zoneIdClass = classOf[ZoneId]
- val zid = JavaCode.global(
- ctx.addReferenceObj("zoneId", zoneId, zoneIdClass.getName),
- zoneIdClass)
+ val zid = zoneIdValue(ctx)
(c, evPrim, evNull) =>
code"$evPrim = $dateTimeUtilsCls.convertTz($c, $zid,
java.time.ZoneOffset.UTC);"
case _: TimestampLTZNanosType =>
(c, evPrim, evNull) => code"$evPrim = $c.epochMicros;"
+ case _: TimestampNTZNanosType =>
+ val zid = zoneIdValue(ctx)
+ (c, evPrim, evNull) =>
+ code"$evPrim = $dateTimeUtilsCls.convertTz($c.epochMicros, $zid,
java.time.ZoneOffset.UTC);"
case DecimalType() =>
(c, evPrim, evNull) => code"$evPrim = ${decimalToTimestampCode(c)};"
case DoubleType =>
@@ -1888,14 +1918,15 @@ case class Cast(
(c, evPrim, evNull) =>
code"$evPrim = $dateTimeUtilsCls.daysToMicros($c,
java.time.ZoneOffset.UTC);"
case TimestampType =>
- val zoneIdClass = classOf[ZoneId]
- val zid = JavaCode.global(
- ctx.addReferenceObj("zoneId", zoneId, zoneIdClass.getName),
- zoneIdClass)
+ val zid = zoneIdValue(ctx)
(c, evPrim, evNull) =>
code"$evPrim = $dateTimeUtilsCls.convertTz($c,
java.time.ZoneOffset.UTC, $zid);"
case _: TimestampNTZNanosType =>
(c, evPrim, evNull) => code"$evPrim = $c.epochMicros;"
+ case _: TimestampLTZNanosType =>
+ val zid = zoneIdValue(ctx)
+ (c, evPrim, evNull) =>
+ code"$evPrim = $dateTimeUtilsCls.convertTz($c.epochMicros,
java.time.ZoneOffset.UTC, $zid);"
}
private[this] def castToTimestampLTZNanosCode(
@@ -1903,10 +1934,7 @@ case class Cast(
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 zid = zoneIdValue(ctx)
val tsOpt = ctx.freshVariable("tsOpt",
classOf[Option[TimestampNanosVal]])
(c, evPrim, evNull) =>
if (ansiEnabled) {
@@ -1929,14 +1957,20 @@ case class Cast(
case TimestampType =>
(c, evPrim, evNull) =>
code"$evPrim = TimestampNanosVal.fromParts($c, (short) 0);"
+ case TimestampNTZType =>
+ val zid = zoneIdValue(ctx)
+ (c, evPrim, evNull) =>
+ code"$evPrim = TimestampNanosVal.fromParts(" +
+ code"$dateTimeUtilsCls.convertTz($c, $zid,
java.time.ZoneOffset.UTC), (short) 0);"
case _: TimestampLTZNanosType =>
(c, evPrim, evNull) =>
code"$evPrim = $dateTimeUtilsCls.truncateTimestampNanosToPrecision($c,
$precision);"
+ case _: TimestampNTZNanosType =>
+ val zid = zoneIdValue(ctx)
+ (c, evPrim, evNull) =>
+ code"$evPrim = $dateTimeUtilsCls.timestampNTZNanosToLTZNanos($c, $zid,
$precision);"
case DateType =>
- val zoneIdClass = classOf[ZoneId]
- val zid = JavaCode.global(
- ctx.addReferenceObj("zoneId", zoneId, zoneIdClass.getName),
- zoneIdClass)
+ val zid = zoneIdValue(ctx)
(c, evPrim, evNull) =>
code"$evPrim = TimestampNanosVal.fromParts(" +
code"$dateTimeUtilsCls.daysToMicros($c, $zid), (short) 0);"
@@ -1969,9 +2003,18 @@ case class Cast(
case TimestampNTZType =>
(c, evPrim, evNull) =>
code"$evPrim = TimestampNanosVal.fromParts($c, (short) 0);"
+ case TimestampType =>
+ val zid = zoneIdValue(ctx)
+ (c, evPrim, evNull) =>
+ code"$evPrim = TimestampNanosVal.fromParts(" +
+ code"$dateTimeUtilsCls.convertTz($c, java.time.ZoneOffset.UTC,
$zid), (short) 0);"
case _: TimestampNTZNanosType =>
(c, evPrim, evNull) =>
code"$evPrim = $dateTimeUtilsCls.truncateTimestampNanosToPrecision($c,
$precision);"
+ case _: TimestampLTZNanosType =>
+ val zid = zoneIdValue(ctx)
+ (c, evPrim, evNull) =>
+ code"$evPrim = $dateTimeUtilsCls.timestampLTZNanosToNTZNanos($c, $zid,
$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 896b2eddf3f6..980c68f65f20 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,13 +101,23 @@ abstract class CastSuiteBase extends SparkFunSuite with
ExpressionEvalHelper {
checkNullCast(DateType, TimestampLTZNanosType(p))
checkNullCast(TimestampLTZNanosType(p), DateType)
}
- // Same-family cross-precision nanos casts.
+ // Same-family and cross-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(TimestampLTZNanosType(p1), TimestampNTZNanosType(p2))
+ checkNullCast(TimestampNTZNanosType(p1), TimestampLTZNanosType(p2))
+ }
+ // Cross-family casts that involve the precision-6 micro family member
(TIMESTAMP_LTZ(6) =
+ // TIMESTAMP and TIMESTAMP_NTZ(6) = TIMESTAMP_NTZ) and the other family's
nanosecond member.
+ foreachNanosPrecision { p =>
+ checkNullCast(TimestampType, TimestampNTZNanosType(p))
+ checkNullCast(TimestampNTZNanosType(p), TimestampType)
+ checkNullCast(TimestampNTZType, TimestampLTZNanosType(p))
+ checkNullCast(TimestampLTZNanosType(p), TimestampNTZType)
}
checkNullCast(StringType, BinaryType)
checkNullCast(StringType, BooleanType)
@@ -791,6 +801,61 @@ abstract class CastSuiteBase extends SparkFunSuite with
ExpressionEvalHelper {
}
}
+ test("cross-family nanos cast: admissibility, store-assignment and up-cast
contract") {
+ for {
+ p <- TimestampLTZNanosType.MIN_PRECISION to
TimestampLTZNanosType.MAX_PRECISION
+ q <- TimestampNTZNanosType.MIN_PRECISION to
TimestampNTZNanosType.MAX_PRECISION
+ } {
+ val ltz = TimestampLTZNanosType(p)
+ val ntz = TimestampNTZNanosType(q)
+ // Explicit casts are allowed in both directions, both in ANSI and
non-ANSI modes.
+ assert(Cast.canCast(ltz, ntz))
+ assert(Cast.canCast(ntz, ltz))
+ assert(Cast.canAnsiCast(ltz, ntz))
+ assert(Cast.canAnsiCast(ntz, ltz))
+ // The cross-family reinterpretation against the session zone is never a
safe up-cast.
+ assert(!Cast.canUpCast(ltz, ntz))
+ assert(!Cast.canUpCast(ntz, ltz))
+ // They stay explicit-only: never silent store assignments (mirroring
the other nanos casts).
+ assert(!Cast.canANSIStoreAssign(ltz, ntz))
+ assert(!Cast.canANSIStoreAssign(ntz, ltz))
+ // The conversion depends on the session time zone in both directions.
+ assert(Cast.needsTimeZone(ltz, ntz))
+ assert(Cast.needsTimeZone(ntz, ltz))
+ // Reinterpreting a non-null instant/local date-time never yields null,
so the cast is
+ // null-safe in both directions (mirroring the micro TIMESTAMP <->
TIMESTAMP_NTZ pair).
+ assert(!Cast.forceNullable(ltz, ntz))
+ assert(!Cast.forceNullable(ntz, ltz))
+ }
+ }
+
+ test("cross-family nanos cast: micro boundary (precision 6) admissibility
and store contract") {
+ // TIMESTAMP_LTZ(6) = TIMESTAMP and TIMESTAMP_NTZ(6) = TIMESTAMP_NTZ, so
the precision-6
+ // cross-family casts are the mixed micro/nanos pairs covered here.
+ foreachNanosPrecision { p =>
+ val pairs = Seq(
+ (TimestampType: DataType, TimestampNTZNanosType(p): DataType), //
LTZ(6) -> NTZ(p)
+ (TimestampNTZNanosType(p): DataType, TimestampType: DataType), //
NTZ(p) -> LTZ(6)
+ (TimestampNTZType: DataType, TimestampLTZNanosType(p): DataType),//
NTZ(6) -> LTZ(p)
+ (TimestampLTZNanosType(p): DataType, TimestampNTZType: DataType))//
LTZ(p) -> NTZ(6)
+ pairs.foreach { case (from, to) =>
+ // Explicit casts are allowed (ANSI and non-ANSI), but are never safe
up-casts and never
+ // silent store assignments, and they depend on the session time zone.
+ assert(Cast.canCast(from, to))
+ assert(Cast.canAnsiCast(from, to))
+ assert(!Cast.canUpCast(from, to))
+ assert(!Cast.canANSIStoreAssign(from, to))
+ assert(Cast.needsTimeZone(from, to))
+ // Null-safe like the micro TIMESTAMP <-> TIMESTAMP_NTZ pair.
+ assert(!Cast.forceNullable(from, to))
+ }
+ }
+ // Sanity: the all-micro TIMESTAMP <-> TIMESTAMP_NTZ pair (precision 6 <->
6) stays a silent
+ // store assignment, unlike the mixed micro/nanos pairs above.
+ assert(Cast.canANSIStoreAssign(TimestampType, TimestampNTZType))
+ assert(Cast.canANSIStoreAssign(TimestampNTZType, TimestampType))
+ }
+
test("SPARK-40389: canUpCast: return false if casting decimal to integral
types can cause" +
" overflow") {
Seq(ByteType, ShortType, IntegerType, LongType).foreach { integralType =>
@@ -1398,6 +1463,186 @@ abstract class CastSuiteBase extends SparkFunSuite with
ExpressionEvalHelper {
}
}
+ test("cross-family nanos cast: timestamp_ltz to timestamp_ntz") {
+ // LTZ(p) denotes an absolute instant; LTZ(p) -> NTZ(q) renders it as the
wall-clock local
+ // date-time observed in the session zone (mirroring the micro TIMESTAMP
-> TIMESTAMP_NTZ
+ // conversion on the epoch-micros part) and re-floors the sub-microsecond
digits to q.
+ Seq(UTC, LA).foreach { zone =>
+ val zid = Option(zone.getId)
+ val instants = Seq(
+ timestampLTZ(2020, 1, 1, 12, 30, 15, 123456789),
+ // Pre-epoch: epochMicros is negative, the sub-microsecond part stays
in [0, 999].
+ timestampLTZ(1969, 12, 31, 23, 59, 59, 999999789))
+ instants.foreach { instant =>
+ val srcMicros = instantToNanosVal(instant).epochMicros
+ val ntzMicros = convertTz(srcMicros, UTC, zone)
+ for {
+ p <- TimestampLTZNanosType.MIN_PRECISION to
TimestampLTZNanosType.MAX_PRECISION
+ q <- TimestampNTZNanosType.MIN_PRECISION to
TimestampNTZNanosType.MAX_PRECISION
+ } {
+ // The source already floors to p; the cross-family cast then floors
to q, i.e. to
+ // min(p, q). Only the wall-clock epoch-micros part shifts with the
zone.
+ val srcNanos = floorNanosToPrecision(789, p)
+ val expectedNanos = floorNanosToPrecision(srcNanos, q)
+ checkEvaluation(
+ cast(Literal.create(nanosVal(srcMicros, srcNanos),
TimestampLTZNanosType(p)),
+ TimestampNTZNanosType(q), zid),
+ nanosVal(ntzMicros, expectedNanos))
+ checkEvaluation(
+ cast(Literal.create(null, TimestampLTZNanosType(p)),
TimestampNTZNanosType(q), zid),
+ null)
+ }
+ }
+ // Zone sensitivity (exercises needsTimeZone): in LA the wall-clock
micros differ from UTC.
+ if (zone == LA) {
+ assert(convertTz(instantToNanosVal(instants.head).epochMicros, UTC,
zone) !=
+ instantToNanosVal(instants.head).epochMicros)
+ }
+ }
+ }
+
+ test("cross-family nanos cast: timestamp_ntz to timestamp_ltz") {
+ // NTZ(q) denotes a wall-clock local date-time; NTZ(q) -> LTZ(p)
interprets it in the session
+ // zone to obtain the absolute instant (mirroring micro TIMESTAMP_NTZ ->
TIMESTAMP on the
+ // epoch-micros part) and re-floors the sub-microsecond digits to p.
+ Seq(UTC, LA).foreach { zone =>
+ val zid = Option(zone.getId)
+ val localDateTimes = Seq(
+ LocalDateTime.of(2020, 1, 1, 12, 30, 15, 123456789),
+ LocalDateTime.of(1969, 12, 31, 23, 59, 59, 999999789))
+ localDateTimes.foreach { localDateTime =>
+ val srcMicros = localDateTimeToNanosVal(localDateTime).epochMicros
+ val ltzMicros = convertTz(srcMicros, zone, UTC)
+ for {
+ p <- TimestampLTZNanosType.MIN_PRECISION to
TimestampLTZNanosType.MAX_PRECISION
+ q <- TimestampNTZNanosType.MIN_PRECISION to
TimestampNTZNanosType.MAX_PRECISION
+ } {
+ val srcNanos = floorNanosToPrecision(789, q)
+ val expectedNanos = floorNanosToPrecision(srcNanos, p)
+ checkEvaluation(
+ cast(Literal.create(nanosVal(srcMicros, srcNanos),
TimestampNTZNanosType(q)),
+ TimestampLTZNanosType(p), zid),
+ nanosVal(ltzMicros, expectedNanos))
+ checkEvaluation(
+ cast(Literal.create(null, TimestampNTZNanosType(q)),
TimestampLTZNanosType(p), zid),
+ null)
+ }
+ }
+ if (zone == LA) {
+
assert(convertTz(localDateTimeToNanosVal(localDateTimes.head).epochMicros,
zone, UTC) !=
+ localDateTimeToNanosVal(localDateTimes.head).epochMicros)
+ }
+ }
+ }
+
+ test("cross-family nanos cast: round-trip ltz -> ntz -> ltz preserves the
instant") {
+ // Round-tripping through the wall-clock NTZ form and back to LTZ in the
same zone is the
+ // identity at equal precision (no sub-microsecond digits are dropped).
+ Seq(UTC, LA).foreach { zone =>
+ val zid = Option(zone.getId)
+ foreachNanosPrecision { p =>
+ val src = nanosVal(
+ instantToNanosVal(timestampLTZ(2020, 7, 1, 6, 15, 30,
123456789)).epochMicros,
+ floorNanosToPrecision(789, p))
+ checkEvaluation(
+ cast(
+ cast(Literal.create(src, TimestampLTZNanosType(p)),
TimestampNTZNanosType(p), zid),
+ TimestampLTZNanosType(p), zid),
+ src)
+ }
+ }
+ }
+
+ test("cross-family nanos cast: DST gap and overlap resolve like the micro
cast") {
+ // The cross-family nanos casts reuse the same java.time resolver as the
micro
+ // TIMESTAMP <-> TIMESTAMP_NTZ casts (NTZ -> LTZ interprets the wall clock
via
+ // LocalDateTime.atZone; LTZ -> NTZ renders the instant via
Instant.atZone). Exercise the LA
+ // spring-forward gap (02:30 does not exist -> java.time shifts forward)
and the fall-back
+ // overlap (01:30 occurs twice -> earlier offset), asserting the nanos
result matches the
+ // corresponding micro cast on the epoch-micros part. The existing zone
tests above use
+ // non-transition instants, so this is the only place DST resolution is
observable.
+ val la = Option(LA.getId)
+ Seq(
+ LocalDateTime.of(2020, 3, 8, 2, 30, 0, 123456789), // inside the PST
-> PDT gap
+ LocalDateTime.of(2020, 11, 1, 1, 30, 0, 123456789)) // inside the PDT
-> PST overlap
+ .foreach { ldt =>
+ // NTZ stores the wall-clock fields as epoch-micros-at-UTC, exactly like
micro TIMESTAMP_NTZ.
+ val ntzMicros = localDateTimeToNanosVal(ldt).epochMicros
+ // The expected LTZ instant is whatever the micro TIMESTAMP_NTZ ->
TIMESTAMP cast produces;
+ // for the gap this is the forward-shifted instant, for the overlap the
earlier offset.
+ val microLtz = evaluateWithoutCodegen(
+ cast(Literal.create(ntzMicros, TimestampNTZType), TimestampType,
la)).asInstanceOf[Long]
+ // The reverse micro TIMESTAMP -> TIMESTAMP_NTZ of that instant (for the
gap, the wall clock
+ // is the shifted 03:30, not the original 02:30).
+ val microNtz = evaluateWithoutCodegen(
+ cast(Literal.create(microLtz, TimestampType), TimestampNTZType,
la)).asInstanceOf[Long]
+ for {
+ p <- TimestampLTZNanosType.MIN_PRECISION to
TimestampLTZNanosType.MAX_PRECISION
+ q <- TimestampNTZNanosType.MIN_PRECISION to
TimestampNTZNanosType.MAX_PRECISION
+ } {
+ // NTZ(q) -> LTZ(p): the epoch-micros part resolves DST exactly like
the micro cast.
+ val ntzToLtzSrc = floorNanosToPrecision(789, q)
+ checkEvaluation(
+ cast(Literal.create(nanosVal(ntzMicros, ntzToLtzSrc),
TimestampNTZNanosType(q)),
+ TimestampLTZNanosType(p), la),
+ nanosVal(microLtz, floorNanosToPrecision(ntzToLtzSrc, p)))
+ // LTZ(p) -> NTZ(q): rendering the resolved instant matches the micro
cast too.
+ val ltzToNtzSrc = floorNanosToPrecision(789, p)
+ checkEvaluation(
+ cast(Literal.create(nanosVal(microLtz, ltzToNtzSrc),
TimestampLTZNanosType(p)),
+ TimestampNTZNanosType(q), la),
+ nanosVal(microNtz, floorNanosToPrecision(ltzToNtzSrc, q)))
+ }
+ }
+ }
+
+ test("cross-family nanos cast: micro family member (precision 6) to/from
nanos") {
+ // The precision-6 micro family members are TIMESTAMP_LTZ(6) = TIMESTAMP
and
+ // TIMESTAMP_NTZ(6) = TIMESTAMP_NTZ. Casting them across families
reinterprets the value against
+ // the session zone (mirroring the all-micro TIMESTAMP <-> TIMESTAMP_NTZ
conversion); micro
+ // targets carry a zero sub-microsecond part and micro sources contribute
none.
+ Seq(UTC, LA).foreach { zone =>
+ val zid = Option(zone.getId)
+ // Micro LTZ instant (sub-micro digits are not representable in the
micro type).
+ val ltzMicros = instantToMicros(timestampLTZ(2020, 1, 1, 12, 30, 15,
123456000))
+ val ntzMicrosForLtz = convertTz(ltzMicros, UTC, zone)
+ // Micro NTZ wall-clock value.
+ val ntzMicros =
+ localDateTimeToNanosVal(LocalDateTime.of(2020, 1, 1, 12, 30, 15,
123456000)).epochMicros
+ val ltzMicrosForNtz = convertTz(ntzMicros, zone, UTC)
+ foreachNanosPrecision { p =>
+ // TIMESTAMP (LTZ(6)) -> TIMESTAMP_NTZ(p): wall clock in the session
zone, sub-micro = 0.
+ checkEvaluation(
+ cast(Literal.create(ltzMicros, TimestampType),
TimestampNTZNanosType(p), zid),
+ nanosVal(ntzMicrosForLtz, 0))
+ // TIMESTAMP_NTZ (NTZ(6)) -> TIMESTAMP_LTZ(p): instant of the wall
clock, sub-micro = 0.
+ checkEvaluation(
+ cast(Literal.create(ntzMicros, TimestampNTZType),
TimestampLTZNanosType(p), zid),
+ nanosVal(ltzMicrosForNtz, 0))
+ // TIMESTAMP_NTZ(p) -> TIMESTAMP (LTZ(6)): drops the sub-microsecond
digits before the zone
+ // reinterpretation. A non-zero nanosWithinMicro on the source proves
the truncation.
+ checkEvaluation(
+ cast(Literal.create(nanosVal(ntzMicros, floorNanosToPrecision(789,
p)),
+ TimestampNTZNanosType(p)), TimestampType, zid),
+ convertTz(ntzMicros, zone, UTC))
+ // TIMESTAMP_LTZ(p) -> TIMESTAMP_NTZ (NTZ(6)): drops the
sub-microsecond digits likewise.
+ checkEvaluation(
+ cast(Literal.create(nanosVal(ltzMicros, floorNanosToPrecision(789,
p)),
+ TimestampLTZNanosType(p)), TimestampNTZType, zid),
+ convertTz(ltzMicros, UTC, zone))
+ // Null input in all four directions.
+ checkEvaluation(
+ cast(Literal.create(null, TimestampType), TimestampNTZNanosType(p),
zid), null)
+ checkEvaluation(
+ cast(Literal.create(null, TimestampNTZType),
TimestampLTZNanosType(p), zid), null)
+ checkEvaluation(
+ cast(Literal.create(null, TimestampNTZNanosType(p)), TimestampType,
zid), null)
+ checkEvaluation(
+ cast(Literal.create(null, TimestampLTZNanosType(p)),
TimestampNTZType, zid), 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 bf344eb08e05..a3c8e894f8a9 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
@@ -872,6 +872,158 @@ Project [cast(cast(null as timestamp_ltz(8)) as
timestamp_ltz(9)) AS CAST(CAST(N
+- OneRowRelation
+-- !query
+select typeof(timestamp_ltz'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_LTZ '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_ntz(9))
+-- !query analysis
+Project [typeof(cast(2020-01-01 00:00:00.123456789 as timestamp_ntz(9))) AS
typeof(CAST(TIMESTAMP_LTZ '2020-01-01 00:00:00.123456789' AS
TIMESTAMP_NTZ(9)))#x]
++- OneRowRelation
+
+
+-- !query
+select typeof(timestamp_ntz'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_NTZ '2020-01-01 00:00:00.123456789' AS
TIMESTAMP_LTZ(8)))#x]
++- OneRowRelation
+
+
+-- !query
+select typeof(timestamp_ntz'2020-01-01 00:00:00.123456789'::timestamp_ltz(9))
+-- !query analysis
+Project [typeof(cast(2020-01-01 00:00:00.123456789 as timestamp_ltz(9))) AS
typeof(CAST(TIMESTAMP_NTZ '2020-01-01 00:00:00.123456789' AS
TIMESTAMP_LTZ(9)))#x]
++- OneRowRelation
+
+
+-- !query
+select timestamp_ltz'2020-01-01
00:00:00.123456789'::timestamp_ntz(9)::timestamp_ltz(9)
+-- !query analysis
+Project [cast(cast(2020-01-01 00:00:00.123456789 as timestamp_ntz(9)) as
timestamp_ltz(9)) AS CAST(CAST(TIMESTAMP_LTZ '2020-01-01 00:00:00.123456789' AS
TIMESTAMP_NTZ(9)) AS TIMESTAMP_LTZ(9))#x]
++- OneRowRelation
+
+
+-- !query
+select timestamp_ntz'2020-01-01
00:00:00.123456789'::timestamp_ltz(9)::timestamp_ntz(9)
+-- !query analysis
+Project [cast(cast(2020-01-01 00:00:00.123456789 as timestamp_ltz(9)) as
timestamp_ntz(9)) AS CAST(CAST(TIMESTAMP_NTZ '2020-01-01 00:00:00.123456789' AS
TIMESTAMP_LTZ(9)) AS TIMESTAMP_NTZ(9))#x]
++- OneRowRelation
+
+
+-- !query
+select timestamp_ltz'2020-01-01
00:00:00.123456789'::timestamp_ntz(7)::timestamp_ltz(7)
+-- !query analysis
+Project [cast(cast(2020-01-01 00:00:00.123456789 as timestamp_ntz(7)) as
timestamp_ltz(7)) AS CAST(CAST(TIMESTAMP_LTZ '2020-01-01 00:00:00.123456789' AS
TIMESTAMP_NTZ(7)) AS TIMESTAMP_LTZ(7))#x]
++- OneRowRelation
+
+
+-- !query
+select timestamp_ntz'1960-01-01
00:00:00.123456789'::timestamp_ltz(7)::timestamp_ntz(7)
+-- !query analysis
+Project [cast(cast(1960-01-01 00:00:00.123456789 as timestamp_ltz(7)) as
timestamp_ntz(7)) AS CAST(CAST(TIMESTAMP_NTZ '1960-01-01 00:00:00.123456789' AS
TIMESTAMP_LTZ(7)) AS TIMESTAMP_NTZ(7))#x]
++- OneRowRelation
+
+
+-- !query
+select cast(null as timestamp_ltz(9))::timestamp_ntz(7)
+-- !query analysis
+Project [cast(cast(null as timestamp_ltz(9)) as timestamp_ntz(7)) AS
CAST(CAST(NULL AS TIMESTAMP_LTZ(9)) AS TIMESTAMP_NTZ(7))#x]
++- OneRowRelation
+
+
+-- !query
+select cast(null as timestamp_ntz(8))::timestamp_ltz(9)
+-- !query analysis
+Project [cast(cast(null as timestamp_ntz(8)) as timestamp_ltz(9)) AS
CAST(CAST(NULL AS TIMESTAMP_NTZ(8)) AS TIMESTAMP_LTZ(9))#x]
++- OneRowRelation
+
+
+-- !query
+select typeof(timestamp'2020-01-01 00:00:00.123456'::timestamp_ntz(9))
+-- !query analysis
+[Analyzer test output redacted due to nondeterminism]
+
+
+-- !query
+select typeof(timestamp_ntz'2020-01-01 00:00:00.123456'::timestamp_ltz(9))
+-- !query analysis
+Project [typeof(cast(2020-01-01 00:00:00.123456 as timestamp_ltz(9))) AS
typeof(CAST(TIMESTAMP_NTZ '2020-01-01 00:00:00.123456' AS TIMESTAMP_LTZ(9)))#x]
++- OneRowRelation
+
+
+-- !query
+select typeof(timestamp_ltz'2020-01-01 00:00:00.123456789'::timestamp_ntz(6))
+-- !query analysis
+Project [typeof(cast(2020-01-01 00:00:00.123456789 as timestamp_ntz)) AS
typeof(CAST(TIMESTAMP_LTZ '2020-01-01 00:00:00.123456789' AS TIMESTAMP_NTZ))#x]
++- OneRowRelation
+
+
+-- !query
+select typeof(timestamp_ntz'2020-01-01 00:00:00.123456789'::timestamp_ltz(6))
+-- !query analysis
+Project [typeof(cast(2020-01-01 00:00:00.123456789 as timestamp)) AS
typeof(CAST(TIMESTAMP_NTZ '2020-01-01 00:00:00.123456789' AS TIMESTAMP))#x]
++- OneRowRelation
+
+
+-- !query
+select timestamp'2020-01-01 00:00:00.123456'::timestamp_ntz(9)::timestamp
+-- !query analysis
+[Analyzer test output redacted due to nondeterminism]
+
+
+-- !query
+select timestamp_ntz'2020-01-01
00:00:00.123456'::timestamp_ltz(9)::timestamp_ntz
+-- !query analysis
+Project [cast(cast(2020-01-01 00:00:00.123456 as timestamp_ltz(9)) as
timestamp_ntz) AS CAST(CAST(TIMESTAMP_NTZ '2020-01-01 00:00:00.123456' AS
TIMESTAMP_LTZ(9)) AS TIMESTAMP_NTZ)#x]
++- OneRowRelation
+
+
+-- !query
+select timestamp_ltz'2020-01-01
00:00:00.123456789'::timestamp_ntz(6)::timestamp_ltz
+-- !query analysis
+Project [cast(cast(2020-01-01 00:00:00.123456789 as timestamp_ntz) as
timestamp) AS CAST(CAST(TIMESTAMP_LTZ '2020-01-01 00:00:00.123456789' AS
TIMESTAMP_NTZ) AS TIMESTAMP)#x]
++- OneRowRelation
+
+
+-- !query
+select timestamp_ntz'2020-01-01
00:00:00.123456789'::timestamp_ltz(6)::timestamp_ntz
+-- !query analysis
+Project [cast(cast(2020-01-01 00:00:00.123456789 as timestamp) as
timestamp_ntz) AS CAST(CAST(TIMESTAMP_NTZ '2020-01-01 00:00:00.123456789' AS
TIMESTAMP) AS TIMESTAMP_NTZ)#x]
++- OneRowRelation
+
+
+-- !query
+select cast(null as timestamp)::timestamp_ntz(9)
+-- !query analysis
+Project [cast(cast(null as timestamp) as timestamp_ntz(9)) AS CAST(CAST(NULL
AS TIMESTAMP) AS TIMESTAMP_NTZ(9))#x]
++- OneRowRelation
+
+
+-- !query
+select cast(null as timestamp_ntz)::timestamp_ltz(9)
+-- !query analysis
+Project [cast(cast(null as timestamp_ntz) as timestamp_ltz(9)) AS
CAST(CAST(NULL AS TIMESTAMP_NTZ) AS TIMESTAMP_LTZ(9))#x]
++- OneRowRelation
+
+
+-- !query
+select cast(null as timestamp_ntz(9))::timestamp
+-- !query analysis
+Project [cast(cast(null as timestamp_ntz(9)) as timestamp) AS CAST(CAST(NULL
AS TIMESTAMP_NTZ(9)) AS TIMESTAMP)#x]
++- OneRowRelation
+
+
+-- !query
+select cast(null as timestamp_ltz(9))::timestamp_ntz
+-- !query analysis
+Project [cast(cast(null as timestamp_ltz(9)) as timestamp_ntz) AS
CAST(CAST(NULL AS TIMESTAMP_LTZ(9)) AS TIMESTAMP_NTZ)#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 c6ee17e8e387..b18f5739fd68 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
@@ -736,6 +736,158 @@ Project [cast(cast(null as timestamp_ltz(8)) as
timestamp_ltz(9)) AS CAST(CAST(N
+- OneRowRelation
+-- !query
+select typeof(timestamp_ltz'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_LTZ '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_ntz(9))
+-- !query analysis
+Project [typeof(cast(2020-01-01 00:00:00.123456789 as timestamp_ntz(9))) AS
typeof(CAST(TIMESTAMP_LTZ '2020-01-01 00:00:00.123456789' AS
TIMESTAMP_NTZ(9)))#x]
++- OneRowRelation
+
+
+-- !query
+select typeof(timestamp_ntz'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_NTZ '2020-01-01 00:00:00.123456789' AS
TIMESTAMP_LTZ(8)))#x]
++- OneRowRelation
+
+
+-- !query
+select typeof(timestamp_ntz'2020-01-01 00:00:00.123456789'::timestamp_ltz(9))
+-- !query analysis
+Project [typeof(cast(2020-01-01 00:00:00.123456789 as timestamp_ltz(9))) AS
typeof(CAST(TIMESTAMP_NTZ '2020-01-01 00:00:00.123456789' AS
TIMESTAMP_LTZ(9)))#x]
++- OneRowRelation
+
+
+-- !query
+select timestamp_ltz'2020-01-01
00:00:00.123456789'::timestamp_ntz(9)::timestamp_ltz(9)
+-- !query analysis
+Project [cast(cast(2020-01-01 00:00:00.123456789 as timestamp_ntz(9)) as
timestamp_ltz(9)) AS CAST(CAST(TIMESTAMP_LTZ '2020-01-01 00:00:00.123456789' AS
TIMESTAMP_NTZ(9)) AS TIMESTAMP_LTZ(9))#x]
++- OneRowRelation
+
+
+-- !query
+select timestamp_ntz'2020-01-01
00:00:00.123456789'::timestamp_ltz(9)::timestamp_ntz(9)
+-- !query analysis
+Project [cast(cast(2020-01-01 00:00:00.123456789 as timestamp_ltz(9)) as
timestamp_ntz(9)) AS CAST(CAST(TIMESTAMP_NTZ '2020-01-01 00:00:00.123456789' AS
TIMESTAMP_LTZ(9)) AS TIMESTAMP_NTZ(9))#x]
++- OneRowRelation
+
+
+-- !query
+select timestamp_ltz'2020-01-01
00:00:00.123456789'::timestamp_ntz(7)::timestamp_ltz(7)
+-- !query analysis
+Project [cast(cast(2020-01-01 00:00:00.123456789 as timestamp_ntz(7)) as
timestamp_ltz(7)) AS CAST(CAST(TIMESTAMP_LTZ '2020-01-01 00:00:00.123456789' AS
TIMESTAMP_NTZ(7)) AS TIMESTAMP_LTZ(7))#x]
++- OneRowRelation
+
+
+-- !query
+select timestamp_ntz'1960-01-01
00:00:00.123456789'::timestamp_ltz(7)::timestamp_ntz(7)
+-- !query analysis
+Project [cast(cast(1960-01-01 00:00:00.123456789 as timestamp_ltz(7)) as
timestamp_ntz(7)) AS CAST(CAST(TIMESTAMP_NTZ '1960-01-01 00:00:00.123456789' AS
TIMESTAMP_LTZ(7)) AS TIMESTAMP_NTZ(7))#x]
++- OneRowRelation
+
+
+-- !query
+select cast(null as timestamp_ltz(9))::timestamp_ntz(7)
+-- !query analysis
+Project [cast(cast(null as timestamp_ltz(9)) as timestamp_ntz(7)) AS
CAST(CAST(NULL AS TIMESTAMP_LTZ(9)) AS TIMESTAMP_NTZ(7))#x]
++- OneRowRelation
+
+
+-- !query
+select cast(null as timestamp_ntz(8))::timestamp_ltz(9)
+-- !query analysis
+Project [cast(cast(null as timestamp_ntz(8)) as timestamp_ltz(9)) AS
CAST(CAST(NULL AS TIMESTAMP_NTZ(8)) AS TIMESTAMP_LTZ(9))#x]
++- OneRowRelation
+
+
+-- !query
+select typeof(timestamp'2020-01-01 00:00:00.123456'::timestamp_ntz(9))
+-- !query analysis
+[Analyzer test output redacted due to nondeterminism]
+
+
+-- !query
+select typeof(timestamp_ntz'2020-01-01 00:00:00.123456'::timestamp_ltz(9))
+-- !query analysis
+Project [typeof(cast(2020-01-01 00:00:00.123456 as timestamp_ltz(9))) AS
typeof(CAST(TIMESTAMP_NTZ '2020-01-01 00:00:00.123456' AS TIMESTAMP_LTZ(9)))#x]
++- OneRowRelation
+
+
+-- !query
+select typeof(timestamp_ltz'2020-01-01 00:00:00.123456789'::timestamp_ntz(6))
+-- !query analysis
+Project [typeof(cast(2020-01-01 00:00:00.123456789 as timestamp_ntz)) AS
typeof(CAST(TIMESTAMP_LTZ '2020-01-01 00:00:00.123456789' AS TIMESTAMP_NTZ))#x]
++- OneRowRelation
+
+
+-- !query
+select typeof(timestamp_ntz'2020-01-01 00:00:00.123456789'::timestamp_ltz(6))
+-- !query analysis
+Project [typeof(cast(2020-01-01 00:00:00.123456789 as timestamp)) AS
typeof(CAST(TIMESTAMP_NTZ '2020-01-01 00:00:00.123456789' AS TIMESTAMP))#x]
++- OneRowRelation
+
+
+-- !query
+select timestamp'2020-01-01 00:00:00.123456'::timestamp_ntz(9)::timestamp
+-- !query analysis
+[Analyzer test output redacted due to nondeterminism]
+
+
+-- !query
+select timestamp_ntz'2020-01-01
00:00:00.123456'::timestamp_ltz(9)::timestamp_ntz
+-- !query analysis
+Project [cast(cast(2020-01-01 00:00:00.123456 as timestamp_ltz(9)) as
timestamp_ntz) AS CAST(CAST(TIMESTAMP_NTZ '2020-01-01 00:00:00.123456' AS
TIMESTAMP_LTZ(9)) AS TIMESTAMP_NTZ)#x]
++- OneRowRelation
+
+
+-- !query
+select timestamp_ltz'2020-01-01
00:00:00.123456789'::timestamp_ntz(6)::timestamp_ltz
+-- !query analysis
+Project [cast(cast(2020-01-01 00:00:00.123456789 as timestamp_ntz) as
timestamp) AS CAST(CAST(TIMESTAMP_LTZ '2020-01-01 00:00:00.123456789' AS
TIMESTAMP_NTZ) AS TIMESTAMP)#x]
++- OneRowRelation
+
+
+-- !query
+select timestamp_ntz'2020-01-01
00:00:00.123456789'::timestamp_ltz(6)::timestamp_ntz
+-- !query analysis
+Project [cast(cast(2020-01-01 00:00:00.123456789 as timestamp) as
timestamp_ntz) AS CAST(CAST(TIMESTAMP_NTZ '2020-01-01 00:00:00.123456789' AS
TIMESTAMP) AS TIMESTAMP_NTZ)#x]
++- OneRowRelation
+
+
+-- !query
+select cast(null as timestamp)::timestamp_ntz(9)
+-- !query analysis
+Project [cast(cast(null as timestamp) as timestamp_ntz(9)) AS CAST(CAST(NULL
AS TIMESTAMP) AS TIMESTAMP_NTZ(9))#x]
++- OneRowRelation
+
+
+-- !query
+select cast(null as timestamp_ntz)::timestamp_ltz(9)
+-- !query analysis
+Project [cast(cast(null as timestamp_ntz) as timestamp_ltz(9)) AS
CAST(CAST(NULL AS TIMESTAMP_NTZ) AS TIMESTAMP_LTZ(9))#x]
++- OneRowRelation
+
+
+-- !query
+select cast(null as timestamp_ntz(9))::timestamp
+-- !query analysis
+Project [cast(cast(null as timestamp_ntz(9)) as timestamp) AS CAST(CAST(NULL
AS TIMESTAMP_NTZ(9)) AS TIMESTAMP)#x]
++- OneRowRelation
+
+
+-- !query
+select cast(null as timestamp_ltz(9))::timestamp_ntz
+-- !query analysis
+Project [cast(cast(null as timestamp_ltz(9)) as timestamp_ntz) AS
CAST(CAST(NULL AS TIMESTAMP_LTZ(9)) AS TIMESTAMP_NTZ)#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 a9e855106624..87621d4413d3 100644
--- a/sql/core/src/test/resources/sql-tests/inputs/cast.sql
+++ b/sql/core/src/test/resources/sql-tests/inputs/cast.sql
@@ -164,6 +164,41 @@ select timestamp_ltz'1960-01-01
00:00:00.123456789'::timestamp_ltz(9)::timestamp
select cast(null as timestamp_ntz(9))::timestamp_ntz(7);
select cast(null as timestamp_ltz(8))::timestamp_ltz(9);
+-- Cross-family cast between nanosecond TIMESTAMP_LTZ(p) and TIMESTAMP_NTZ(q).
+-- The conversion reinterprets the value against the session time zone, so
SQL-layer coverage stays
+-- zone-independent (type resolution, lossless same-zone round-trips, null
propagation); the
+-- zone-aware value semantics (flooring, pre-epoch, zone shifts) are covered
in CastSuite* unit tests.
+select typeof(timestamp_ltz'2020-01-01 00:00:00.123456789'::timestamp_ntz(7));
+select typeof(timestamp_ltz'2020-01-01 00:00:00.123456789'::timestamp_ntz(9));
+select typeof(timestamp_ntz'2020-01-01 00:00:00.123456789'::timestamp_ltz(8));
+select typeof(timestamp_ntz'2020-01-01 00:00:00.123456789'::timestamp_ltz(9));
+-- Same-zone round-trips are lossless at equal precision and re-floored when
narrowing in between.
+select timestamp_ltz'2020-01-01
00:00:00.123456789'::timestamp_ntz(9)::timestamp_ltz(9);
+select timestamp_ntz'2020-01-01
00:00:00.123456789'::timestamp_ltz(9)::timestamp_ntz(9);
+select timestamp_ltz'2020-01-01
00:00:00.123456789'::timestamp_ntz(7)::timestamp_ltz(7);
+select timestamp_ntz'1960-01-01
00:00:00.123456789'::timestamp_ltz(7)::timestamp_ntz(7);
+-- Null propagation in both directions.
+select cast(null as timestamp_ltz(9))::timestamp_ntz(7);
+select cast(null as timestamp_ntz(8))::timestamp_ltz(9);
+
+-- Boundary at precision 6: TIMESTAMP_LTZ(6) is TIMESTAMP and TIMESTAMP_NTZ(6)
is TIMESTAMP_NTZ, so
+-- these cross-family casts mix the micro family member with the other
family's nanos member.
+select typeof(timestamp'2020-01-01 00:00:00.123456'::timestamp_ntz(9));
+select typeof(timestamp_ntz'2020-01-01 00:00:00.123456'::timestamp_ltz(9));
+select typeof(timestamp_ltz'2020-01-01 00:00:00.123456789'::timestamp_ntz(6));
+select typeof(timestamp_ntz'2020-01-01 00:00:00.123456789'::timestamp_ltz(6));
+-- Same-zone round-trips: micro(6) -> nanos -> micro(6) is lossless; nanos ->
micro(6) -> nanos
+-- drops the sub-microsecond digits.
+select timestamp'2020-01-01 00:00:00.123456'::timestamp_ntz(9)::timestamp;
+select timestamp_ntz'2020-01-01
00:00:00.123456'::timestamp_ltz(9)::timestamp_ntz;
+select timestamp_ltz'2020-01-01
00:00:00.123456789'::timestamp_ntz(6)::timestamp_ltz;
+select timestamp_ntz'2020-01-01
00:00:00.123456789'::timestamp_ltz(6)::timestamp_ntz;
+-- Null propagation across the micro boundary in all four directions.
+select cast(null as timestamp)::timestamp_ntz(9);
+select cast(null as timestamp_ntz)::timestamp_ltz(9);
+select cast(null as timestamp_ntz(9))::timestamp;
+select cast(null as timestamp_ltz(9))::timestamp_ntz;
+
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 b6da2f1c32e2..24f9668b3b17 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
@@ -1586,6 +1586,182 @@ struct<CAST(CAST(NULL AS TIMESTAMP_LTZ(8)) AS
TIMESTAMP_LTZ(9)):timestamp_ltz(9)
NULL
+-- !query
+select typeof(timestamp_ltz'2020-01-01 00:00:00.123456789'::timestamp_ntz(7))
+-- !query schema
+struct<typeof(CAST(TIMESTAMP_LTZ '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_ntz(9))
+-- !query schema
+struct<typeof(CAST(TIMESTAMP_LTZ '2020-01-01 00:00:00.123456789' AS
TIMESTAMP_NTZ(9))):string>
+-- !query output
+timestamp_ntz(9)
+
+
+-- !query
+select typeof(timestamp_ntz'2020-01-01 00:00:00.123456789'::timestamp_ltz(8))
+-- !query schema
+struct<typeof(CAST(TIMESTAMP_NTZ '2020-01-01 00:00:00.123456789' AS
TIMESTAMP_LTZ(8))):string>
+-- !query output
+timestamp_ltz(8)
+
+
+-- !query
+select typeof(timestamp_ntz'2020-01-01 00:00:00.123456789'::timestamp_ltz(9))
+-- !query schema
+struct<typeof(CAST(TIMESTAMP_NTZ '2020-01-01 00:00:00.123456789' AS
TIMESTAMP_LTZ(9))):string>
+-- !query output
+timestamp_ltz(9)
+
+
+-- !query
+select timestamp_ltz'2020-01-01
00:00:00.123456789'::timestamp_ntz(9)::timestamp_ltz(9)
+-- !query schema
+struct<CAST(CAST(TIMESTAMP_LTZ '2020-01-01 00:00:00.123456789' AS
TIMESTAMP_NTZ(9)) AS TIMESTAMP_LTZ(9)):timestamp_ltz(9)>
+-- !query output
+2020-01-01 00:00:00.123456789
+
+
+-- !query
+select timestamp_ntz'2020-01-01
00:00:00.123456789'::timestamp_ltz(9)::timestamp_ntz(9)
+-- !query schema
+struct<CAST(CAST(TIMESTAMP_NTZ '2020-01-01 00:00:00.123456789' AS
TIMESTAMP_LTZ(9)) AS TIMESTAMP_NTZ(9)):timestamp_ntz(9)>
+-- !query output
+2020-01-01 00:00:00.123456789
+
+
+-- !query
+select timestamp_ltz'2020-01-01
00:00:00.123456789'::timestamp_ntz(7)::timestamp_ltz(7)
+-- !query schema
+struct<CAST(CAST(TIMESTAMP_LTZ '2020-01-01 00:00:00.123456789' AS
TIMESTAMP_NTZ(7)) AS TIMESTAMP_LTZ(7)):timestamp_ltz(7)>
+-- !query output
+2020-01-01 00:00:00.1234567
+
+
+-- !query
+select timestamp_ntz'1960-01-01
00:00:00.123456789'::timestamp_ltz(7)::timestamp_ntz(7)
+-- !query schema
+struct<CAST(CAST(TIMESTAMP_NTZ '1960-01-01 00:00:00.123456789' AS
TIMESTAMP_LTZ(7)) AS TIMESTAMP_NTZ(7)):timestamp_ntz(7)>
+-- !query output
+1960-01-01 00:00:00.1234567
+
+
+-- !query
+select cast(null as timestamp_ltz(9))::timestamp_ntz(7)
+-- !query schema
+struct<CAST(CAST(NULL AS TIMESTAMP_LTZ(9)) AS
TIMESTAMP_NTZ(7)):timestamp_ntz(7)>
+-- !query output
+NULL
+
+
+-- !query
+select cast(null as timestamp_ntz(8))::timestamp_ltz(9)
+-- !query schema
+struct<CAST(CAST(NULL AS TIMESTAMP_NTZ(8)) AS
TIMESTAMP_LTZ(9)):timestamp_ltz(9)>
+-- !query output
+NULL
+
+
+-- !query
+select typeof(timestamp'2020-01-01 00:00:00.123456'::timestamp_ntz(9))
+-- !query schema
+struct<typeof(CAST(TIMESTAMP '2020-01-01 00:00:00.123456' AS
TIMESTAMP_NTZ(9))):string>
+-- !query output
+timestamp_ntz(9)
+
+
+-- !query
+select typeof(timestamp_ntz'2020-01-01 00:00:00.123456'::timestamp_ltz(9))
+-- !query schema
+struct<typeof(CAST(TIMESTAMP_NTZ '2020-01-01 00:00:00.123456' AS
TIMESTAMP_LTZ(9))):string>
+-- !query output
+timestamp_ltz(9)
+
+
+-- !query
+select typeof(timestamp_ltz'2020-01-01 00:00:00.123456789'::timestamp_ntz(6))
+-- !query schema
+struct<typeof(CAST(TIMESTAMP_LTZ '2020-01-01 00:00:00.123456789' AS
TIMESTAMP_NTZ)):string>
+-- !query output
+timestamp_ntz
+
+
+-- !query
+select typeof(timestamp_ntz'2020-01-01 00:00:00.123456789'::timestamp_ltz(6))
+-- !query schema
+struct<typeof(CAST(TIMESTAMP_NTZ '2020-01-01 00:00:00.123456789' AS
TIMESTAMP)):string>
+-- !query output
+timestamp
+
+
+-- !query
+select timestamp'2020-01-01 00:00:00.123456'::timestamp_ntz(9)::timestamp
+-- !query schema
+struct<CAST(CAST(TIMESTAMP '2020-01-01 00:00:00.123456' AS TIMESTAMP_NTZ(9))
AS TIMESTAMP):timestamp>
+-- !query output
+2020-01-01 00:00:00.123456
+
+
+-- !query
+select timestamp_ntz'2020-01-01
00:00:00.123456'::timestamp_ltz(9)::timestamp_ntz
+-- !query schema
+struct<CAST(CAST(TIMESTAMP_NTZ '2020-01-01 00:00:00.123456' AS
TIMESTAMP_LTZ(9)) AS TIMESTAMP_NTZ):timestamp_ntz>
+-- !query output
+2020-01-01 00:00:00.123456
+
+
+-- !query
+select timestamp_ltz'2020-01-01
00:00:00.123456789'::timestamp_ntz(6)::timestamp_ltz
+-- !query schema
+struct<CAST(CAST(TIMESTAMP_LTZ '2020-01-01 00:00:00.123456789' AS
TIMESTAMP_NTZ) AS TIMESTAMP):timestamp>
+-- !query output
+2020-01-01 00:00:00.123456
+
+
+-- !query
+select timestamp_ntz'2020-01-01
00:00:00.123456789'::timestamp_ltz(6)::timestamp_ntz
+-- !query schema
+struct<CAST(CAST(TIMESTAMP_NTZ '2020-01-01 00:00:00.123456789' AS TIMESTAMP)
AS TIMESTAMP_NTZ):timestamp_ntz>
+-- !query output
+2020-01-01 00:00:00.123456
+
+
+-- !query
+select cast(null as timestamp)::timestamp_ntz(9)
+-- !query schema
+struct<CAST(CAST(NULL AS TIMESTAMP) AS TIMESTAMP_NTZ(9)):timestamp_ntz(9)>
+-- !query output
+NULL
+
+
+-- !query
+select cast(null as timestamp_ntz)::timestamp_ltz(9)
+-- !query schema
+struct<CAST(CAST(NULL AS TIMESTAMP_NTZ) AS TIMESTAMP_LTZ(9)):timestamp_ltz(9)>
+-- !query output
+NULL
+
+
+-- !query
+select cast(null as timestamp_ntz(9))::timestamp
+-- !query schema
+struct<CAST(CAST(NULL AS TIMESTAMP_NTZ(9)) AS TIMESTAMP):timestamp>
+-- !query output
+NULL
+
+
+-- !query
+select cast(null as timestamp_ltz(9))::timestamp_ntz
+-- !query schema
+struct<CAST(CAST(NULL AS TIMESTAMP_LTZ(9)) AS TIMESTAMP_NTZ):timestamp_ntz>
+-- !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 408cb06ef36b..4d66796a577b 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
@@ -848,6 +848,182 @@ struct<CAST(CAST(NULL AS TIMESTAMP_LTZ(8)) AS
TIMESTAMP_LTZ(9)):timestamp_ltz(9)
NULL
+-- !query
+select typeof(timestamp_ltz'2020-01-01 00:00:00.123456789'::timestamp_ntz(7))
+-- !query schema
+struct<typeof(CAST(TIMESTAMP_LTZ '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_ntz(9))
+-- !query schema
+struct<typeof(CAST(TIMESTAMP_LTZ '2020-01-01 00:00:00.123456789' AS
TIMESTAMP_NTZ(9))):string>
+-- !query output
+timestamp_ntz(9)
+
+
+-- !query
+select typeof(timestamp_ntz'2020-01-01 00:00:00.123456789'::timestamp_ltz(8))
+-- !query schema
+struct<typeof(CAST(TIMESTAMP_NTZ '2020-01-01 00:00:00.123456789' AS
TIMESTAMP_LTZ(8))):string>
+-- !query output
+timestamp_ltz(8)
+
+
+-- !query
+select typeof(timestamp_ntz'2020-01-01 00:00:00.123456789'::timestamp_ltz(9))
+-- !query schema
+struct<typeof(CAST(TIMESTAMP_NTZ '2020-01-01 00:00:00.123456789' AS
TIMESTAMP_LTZ(9))):string>
+-- !query output
+timestamp_ltz(9)
+
+
+-- !query
+select timestamp_ltz'2020-01-01
00:00:00.123456789'::timestamp_ntz(9)::timestamp_ltz(9)
+-- !query schema
+struct<CAST(CAST(TIMESTAMP_LTZ '2020-01-01 00:00:00.123456789' AS
TIMESTAMP_NTZ(9)) AS TIMESTAMP_LTZ(9)):timestamp_ltz(9)>
+-- !query output
+2020-01-01 00:00:00.123456789
+
+
+-- !query
+select timestamp_ntz'2020-01-01
00:00:00.123456789'::timestamp_ltz(9)::timestamp_ntz(9)
+-- !query schema
+struct<CAST(CAST(TIMESTAMP_NTZ '2020-01-01 00:00:00.123456789' AS
TIMESTAMP_LTZ(9)) AS TIMESTAMP_NTZ(9)):timestamp_ntz(9)>
+-- !query output
+2020-01-01 00:00:00.123456789
+
+
+-- !query
+select timestamp_ltz'2020-01-01
00:00:00.123456789'::timestamp_ntz(7)::timestamp_ltz(7)
+-- !query schema
+struct<CAST(CAST(TIMESTAMP_LTZ '2020-01-01 00:00:00.123456789' AS
TIMESTAMP_NTZ(7)) AS TIMESTAMP_LTZ(7)):timestamp_ltz(7)>
+-- !query output
+2020-01-01 00:00:00.1234567
+
+
+-- !query
+select timestamp_ntz'1960-01-01
00:00:00.123456789'::timestamp_ltz(7)::timestamp_ntz(7)
+-- !query schema
+struct<CAST(CAST(TIMESTAMP_NTZ '1960-01-01 00:00:00.123456789' AS
TIMESTAMP_LTZ(7)) AS TIMESTAMP_NTZ(7)):timestamp_ntz(7)>
+-- !query output
+1960-01-01 00:00:00.1234567
+
+
+-- !query
+select cast(null as timestamp_ltz(9))::timestamp_ntz(7)
+-- !query schema
+struct<CAST(CAST(NULL AS TIMESTAMP_LTZ(9)) AS
TIMESTAMP_NTZ(7)):timestamp_ntz(7)>
+-- !query output
+NULL
+
+
+-- !query
+select cast(null as timestamp_ntz(8))::timestamp_ltz(9)
+-- !query schema
+struct<CAST(CAST(NULL AS TIMESTAMP_NTZ(8)) AS
TIMESTAMP_LTZ(9)):timestamp_ltz(9)>
+-- !query output
+NULL
+
+
+-- !query
+select typeof(timestamp'2020-01-01 00:00:00.123456'::timestamp_ntz(9))
+-- !query schema
+struct<typeof(CAST(TIMESTAMP '2020-01-01 00:00:00.123456' AS
TIMESTAMP_NTZ(9))):string>
+-- !query output
+timestamp_ntz(9)
+
+
+-- !query
+select typeof(timestamp_ntz'2020-01-01 00:00:00.123456'::timestamp_ltz(9))
+-- !query schema
+struct<typeof(CAST(TIMESTAMP_NTZ '2020-01-01 00:00:00.123456' AS
TIMESTAMP_LTZ(9))):string>
+-- !query output
+timestamp_ltz(9)
+
+
+-- !query
+select typeof(timestamp_ltz'2020-01-01 00:00:00.123456789'::timestamp_ntz(6))
+-- !query schema
+struct<typeof(CAST(TIMESTAMP_LTZ '2020-01-01 00:00:00.123456789' AS
TIMESTAMP_NTZ)):string>
+-- !query output
+timestamp_ntz
+
+
+-- !query
+select typeof(timestamp_ntz'2020-01-01 00:00:00.123456789'::timestamp_ltz(6))
+-- !query schema
+struct<typeof(CAST(TIMESTAMP_NTZ '2020-01-01 00:00:00.123456789' AS
TIMESTAMP)):string>
+-- !query output
+timestamp
+
+
+-- !query
+select timestamp'2020-01-01 00:00:00.123456'::timestamp_ntz(9)::timestamp
+-- !query schema
+struct<CAST(CAST(TIMESTAMP '2020-01-01 00:00:00.123456' AS TIMESTAMP_NTZ(9))
AS TIMESTAMP):timestamp>
+-- !query output
+2020-01-01 00:00:00.123456
+
+
+-- !query
+select timestamp_ntz'2020-01-01
00:00:00.123456'::timestamp_ltz(9)::timestamp_ntz
+-- !query schema
+struct<CAST(CAST(TIMESTAMP_NTZ '2020-01-01 00:00:00.123456' AS
TIMESTAMP_LTZ(9)) AS TIMESTAMP_NTZ):timestamp_ntz>
+-- !query output
+2020-01-01 00:00:00.123456
+
+
+-- !query
+select timestamp_ltz'2020-01-01
00:00:00.123456789'::timestamp_ntz(6)::timestamp_ltz
+-- !query schema
+struct<CAST(CAST(TIMESTAMP_LTZ '2020-01-01 00:00:00.123456789' AS
TIMESTAMP_NTZ) AS TIMESTAMP):timestamp>
+-- !query output
+2020-01-01 00:00:00.123456
+
+
+-- !query
+select timestamp_ntz'2020-01-01
00:00:00.123456789'::timestamp_ltz(6)::timestamp_ntz
+-- !query schema
+struct<CAST(CAST(TIMESTAMP_NTZ '2020-01-01 00:00:00.123456789' AS TIMESTAMP)
AS TIMESTAMP_NTZ):timestamp_ntz>
+-- !query output
+2020-01-01 00:00:00.123456
+
+
+-- !query
+select cast(null as timestamp)::timestamp_ntz(9)
+-- !query schema
+struct<CAST(CAST(NULL AS TIMESTAMP) AS TIMESTAMP_NTZ(9)):timestamp_ntz(9)>
+-- !query output
+NULL
+
+
+-- !query
+select cast(null as timestamp_ntz)::timestamp_ltz(9)
+-- !query schema
+struct<CAST(CAST(NULL AS TIMESTAMP_NTZ) AS TIMESTAMP_LTZ(9)):timestamp_ltz(9)>
+-- !query output
+NULL
+
+
+-- !query
+select cast(null as timestamp_ntz(9))::timestamp
+-- !query schema
+struct<CAST(CAST(NULL AS TIMESTAMP_NTZ(9)) AS TIMESTAMP):timestamp>
+-- !query output
+NULL
+
+
+-- !query
+select cast(null as timestamp_ltz(9))::timestamp_ntz
+-- !query schema
+struct<CAST(CAST(NULL AS TIMESTAMP_LTZ(9)) AS TIMESTAMP_NTZ):timestamp_ntz>
+-- !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]