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 0346c594c44f [SPARK-57660][SQL] Support casting between TIME(p) and
TIMESTAMP_LTZ(q)
0346c594c44f is described below
commit 0346c594c44fea651835dd61fc9caa29f5072e27
Author: Maxim Gekk <[email protected]>
AuthorDate: Wed Jun 24 21:36:04 2026 +0200
[SPARK-57660][SQL] Support casting between TIME(p) and TIMESTAMP_LTZ(q)
### What changes were proposed in this pull request?
This PR adds bidirectional casts between the `TIME(p)` data type (`p` in
`[0, 9]`) and `TIMESTAMP_LTZ(q)` (`q` in `[6, 9]`, where `q=6` is the
microsecond `TimestampType` and `q` in `[7, 9]` is the nanosecond
`TimestampLTZNanosType`).
It is the `TIMESTAMP_LTZ` counterpart of #56677 / SPARK-57618 (`TIME` <->
`TIMESTAMP_NTZ`) and a sub-task of SPARK-56822.
Semantics follow the SQL standard (section 6.13 `<cast specification>`):
- `CAST(TIMESTAMP_LTZ(q) AS TIME(p))` (rule 15.d): the LTZ value is an
absolute instant, so its time-of-day is the local wall-clock time observed in
the session time zone, truncated to precision `p`. Unlike `TIMESTAMP_NTZ ->
TIME`, this direction depends on the session time zone.
- `CAST(TIME(p) AS TIMESTAMP_LTZ(q))` (rule 17.c): the date fields come
from `CURRENT_DATE` and the time fields from the value; the resulting local
date-time is interpreted in the session time zone to produce the instant. Since
`CURRENT_DATE` is stable within a query, the cast is stabilized via the
existing `ComputeCurrentTime` optimizer rule, so it shares the same date
literal as `current_date()`.
Both directions of `TIME` <-> `TIMESTAMP_LTZ` therefore depend on the
session time zone (whereas for `TIMESTAMP_NTZ` only `TIME -> TIMESTAMP_NTZ`
does). Fractional precision handling is pure truncation (floor toward the
precision step; no rounding). Both directions always succeed, so no new
nullability or error condition is introduced.
Implementation notes:
- New rule-table entries in `Cast.canCast` / `Cast.canAnsiCast` for the
four pairs. `canTryCast` inherits these for atomic types.
- All four pairs are marked `needsTimeZone` (both directions read the
session zone).
- Interpreted and codegen paths for both directions.
- `ComputeCurrentTime` scans `CAST` nodes and, applying the new
`Cast.isTimeToTimestampLTZ` predicate on the resolved plan, rewrites `TIME ->
TIMESTAMP_LTZ` into a zone-aware date+time builder (new internal
`MakeTimestampLTZ` / `MakeTimestampLTZNanos`) anchored on the query current
date. As with the NTZ feature, these casts are intentionally *not* tagged with
`CURRENT_LIKE` (inline-table validation treats `CURRENT_LIKE` as safe to
defer). The `Cast` eval/codegen fallback (using `curre [...]
- New helpers: `SparkDateTimeUtils.timestampToNanosOfDay` /
`timestampLTZNanosToNanosOfDay` and `DateTimeUtils.makeTimestampLTZNanos`.
Out of scope: Structured Streaming batch-timestamp parity for `TIME ->
TIMESTAMP_LTZ` (the cast uses the optimizer-instant current date rather than
the micro-batch timestamp).
### Why are the changes needed?
Spark supports `TIME` <-> `TIMESTAMP_NTZ` casts (SPARK-57618) but not
`TIME` <-> `TIMESTAMP_LTZ`. These conversions are required by the SQL standard
and are a common user need (attaching a time-of-day to a timestamp, or
extracting the time-of-day from a timestamp). This is a sub-task of SPARK-56822
(timestamps with nanosecond precision).
### Does this PR introduce _any_ user-facing change?
Yes. Casting between `TIME(p)` and `TIMESTAMP_LTZ(q)` is now allowed
(previously it failed analysis with a cast type-mismatch). Examples:
```sql
-- extract the time-of-day in the session time zone
SELECT CAST(TIMESTAMP'2020-05-17 12:34:56.789012' AS TIME(6));
-- 12:34:56.789012
-- attach the current date, interpreted in the session time zone
SELECT CAST(TIME'12:34:56.789012345' AS TIMESTAMP_LTZ(9));
-- <current_date> 12:34:56.789012345
```
This is a new feature on an unreleased branch; there is no behavior change
relative to a released version.
### How was this patch tested?
- New unit tests in `CastSuiteBase` (run under ANSI on and off):
allowed-pair / `needsTimeZone` matrix, `isTimeToTimestampLTZ` truth table,
`TIMESTAMP_LTZ(q) -> TIME(p)` values across all precisions (including pre-epoch
and sub-microsecond truncation), interpreted-vs-codegen consistency, and
zone-fixed round trips.
- New tests in `DateExpressionsSuite` for `MakeTimestampLTZ` /
`MakeTimestampLTZNanos` (including canonicalization on precision).
- New test in `ComputeCurrentTimeSuite` asserting the forward cast is
rewritten with a query-stable current-date literal consistent with
`current_date()`.
- New unit tests in `DateTimeUtilsSuite` for `makeTimestampLTZNanos` and
the time-of-day extraction helpers.
- New deterministic cases in `cast.sql` (and the imported
`nonansi/cast.sql`) with regenerated golden files.
### Was this patch authored or co-authored using generative AI tooling?
Generated-by: Cursor (Claude Opus 4.8)
Closes #56733 from MaxGekk/time-cast-timestamp_ltz.
Authored-by: Maxim Gekk <[email protected]>
Signed-off-by: Max Gekk <[email protected]>
---
.../sql/catalyst/util/SparkDateTimeUtils.scala | 20 ++
.../spark/sql/catalyst/expressions/Cast.scala | 89 ++++++-
.../catalyst/expressions/datetimeExpressions.scala | 86 +++++++
.../sql/catalyst/optimizer/finishAnalysis.scala | 30 ++-
.../spark/sql/catalyst/util/DateTimeUtils.scala | 36 +++
.../sql/catalyst/expressions/CastSuiteBase.scala | 152 ++++++++---
.../expressions/DateExpressionsSuite.scala | 85 +++++++
.../optimizer/ComputeCurrentTimeSuite.scala | 26 +-
.../sql/catalyst/util/DateTimeUtilsSuite.scala | 41 +++
.../sql-tests/analyzer-results/cast.sql.out | 247 ++++++++++++++++++
.../analyzer-results/nonansi/cast.sql.out | 247 ++++++++++++++++++
.../src/test/resources/sql-tests/inputs/cast.sql | 49 ++++
.../test/resources/sql-tests/results/cast.sql.out | 283 +++++++++++++++++++++
.../sql-tests/results/nonansi/cast.sql.out | 283 +++++++++++++++++++++
14 files changed, 1627 insertions(+), 47 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 ac93e3c1415a..be6c507f70ac 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
@@ -408,6 +408,26 @@ trait SparkDateTimeUtils {
Math.floorMod(v.epochMicros, MICROS_PER_DAY) * NANOS_PER_MICROS +
v.nanosWithinMicro
}
+ /**
+ * Extracts the time-of-day component (nanoseconds since midnight) from a
`TIMESTAMP_LTZ`
+ * microsecond value. `TIMESTAMP_LTZ` denotes an absolute instant, so its
time-of-day is the
+ * local wall-clock time observed in the session time zone `zoneId`. The
result stays in
+ * `[0, NANOS_PER_DAY)`.
+ */
+ def timestampToNanosOfDay(micros: Long, zoneId: ZoneId): Long = {
+ getLocalDateTime(micros, zoneId).toLocalTime.toNanoOfDay
+ }
+
+ /**
+ * Extracts the time-of-day component (nanoseconds since midnight) from a
nanosecond-precision
+ * `TIMESTAMP_LTZ` value. `TIMESTAMP_LTZ` denotes an absolute instant, so
its time-of-day is the
+ * local wall-clock time observed in the session time zone `zoneId`. The
sub-microsecond digits
+ * carried in `nanosWithinMicro` are preserved, and the result stays in `[0,
NANOS_PER_DAY)`.
+ */
+ def timestampLTZNanosToNanosOfDay(v: TimestampNanosVal, zoneId: ZoneId):
Long = {
+ timestampNanosToInstant(v).atZone(zoneId).toLocalTime.toNanoOfDay
+ }
+
/**
* Converts a local date at the default JVM time zone to the number of days
since 1970-01-01 in
* the hybrid calendar (Julian + Gregorian) by discarding the time part. The
resulted days are
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 406e18ea1f7b..9ae34f94424c 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
@@ -158,13 +158,16 @@ object Cast extends QueryErrorsBase {
case (_: TimeType, _: TimeType) => true
case (_: TimeType, _: IntegralType) => true
- // TIME(p) <-> TIMESTAMP_NTZ(q), q in [6, 9] (precision 6 is the micro
TimestampNTZType,
- // [7, 9] is TimestampNTZNanosType). Restricted to the NTZ family on
purpose: TIMESTAMP_LTZ
- // is not a valid counterpart for these casts.
+ // TIME(p) <-> TIMESTAMP_NTZ(q) / TIMESTAMP_LTZ(q), q in [6, 9] (precision
6 is the micro
+ // TimestampNTZType / TimestampType, [7, 9] is TimestampNTZNanosType /
TimestampLTZNanosType).
case (_: TimeType, TimestampNTZType) => true
case (TimestampNTZType, _: TimeType) => true
case (_: TimeType, _: TimestampNTZNanosType) => true
case (_: TimestampNTZNanosType, _: TimeType) => true
+ case (_: TimeType, TimestampType) => true
+ case (TimestampType, _: TimeType) => true
+ case (_: TimeType, _: TimestampLTZNanosType) => true
+ case (_: TimestampLTZNanosType, _: TimeType) => true
// non-null variants can generate nulls even in ANSI mode
case (ArrayType(fromType, fn), ArrayType(toType, tn)) =>
@@ -319,13 +322,16 @@ object Cast extends QueryErrorsBase {
case (_: TimeType, _: TimeType) => true
case (_: TimeType, _: IntegralType) => true
- // TIME(p) <-> TIMESTAMP_NTZ(q), q in [6, 9] (precision 6 is the micro
TimestampNTZType,
- // [7, 9] is TimestampNTZNanosType). Restricted to the NTZ family on
purpose: TIMESTAMP_LTZ
- // is not a valid counterpart for these casts.
+ // TIME(p) <-> TIMESTAMP_NTZ(q) / TIMESTAMP_LTZ(q), q in [6, 9] (precision
6 is the micro
+ // TimestampNTZType / TimestampType, [7, 9] is TimestampNTZNanosType /
TimestampLTZNanosType).
case (_: TimeType, TimestampNTZType) => true
case (TimestampNTZType, _: TimeType) => true
case (_: TimeType, _: TimestampNTZNanosType) => true
case (_: TimestampNTZNanosType, _: TimeType) => true
+ case (_: TimeType, TimestampType) => true
+ case (TimestampType, _: TimeType) => true
+ case (_: TimeType, _: TimestampLTZNanosType) => true
+ case (_: TimestampLTZNanosType, _: TimeType) => true
case (ArrayType(fromType, fn), ArrayType(toType, tn)) =>
canCast(fromType, toType) &&
@@ -371,7 +377,9 @@ object Cast extends QueryErrorsBase {
* with it: a conversion needs the session time zone exactly when its
`castTo*` / `castTo*Code`
* path reads `zoneId`. This is principally the string/date casts of the LTZ
timestamp families
* (TIMESTAMP and TIMESTAMP_LTZ(p)), the cross-family TIMESTAMP_LTZ <->
TIMESTAMP_NTZ conversions
- * (micro and nanosecond), and TIME -> TIMESTAMP_NTZ (whose date fields come
from CURRENT_DATE).
+ * (micro and nanosecond), TIME -> TIMESTAMP_NTZ (whose date fields come
from CURRENT_DATE), and
+ * both directions of TIME <-> TIMESTAMP_LTZ (the LTZ value is an absolute
instant, so extracting
+ * its time-of-day and attaching CURRENT_DATE to a TIME both depend on the
session time zone).
*/
def needsTimeZone(from: DataType, to: DataType): Boolean = (from, to) match {
case (VariantType, _) => true
@@ -404,6 +412,14 @@ object Cast extends QueryErrorsBase {
// time-of-day and is intentionally zone-independent, so it is absent here.
case (_: TimeType, TimestampNTZType) => true
case (_: TimeType, _: TimestampNTZNanosType) => true
+ // TIME <-> TIMESTAMP_LTZ depends on the session time zone in both
directions: the LTZ value is
+ // an absolute instant, so its time-of-day is the local wall clock
observed in the session zone,
+ // and TIME -> TIMESTAMP_LTZ attaches CURRENT_DATE (resolved in the
session zone) and converts
+ // the resulting local date-time to an instant in that zone.
+ case (_: TimeType, TimestampType) => true
+ case (TimestampType, _: TimeType) => true
+ case (_: TimeType, _: TimestampLTZNanosType) => true
+ case (_: TimestampLTZNanosType, _: TimeType) => true
case (ArrayType(fromType, _), ArrayType(toType, _)) =>
needsTimeZone(fromType, toType)
case (MapType(fromKey, fromValue, _), MapType(toKey, toValue, _)) =>
needsTimeZone(fromKey, toKey) || needsTimeZone(fromValue, toValue)
@@ -428,6 +444,19 @@ object Cast extends QueryErrorsBase {
case _ => false
}
+ /**
+ * Returns true for a cast from `TIME(p)` to `TIMESTAMP_LTZ(q)` (q in [6,
9]; q=6 is the micro
+ * `TimestampType`, [7, 9] is `TimestampLTZNanosType`). Like the NTZ
counterpart, such casts
+ * derive their date fields from `CURRENT_DATE`, so they are stabilized
within a query by
+ * [[org.apache.spark.sql.catalyst.optimizer.ComputeCurrentTime]], which
scans `CAST` nodes and
+ * uses this predicate on the resolved plan.
+ */
+ def isTimeToTimestampLTZ(from: DataType, to: DataType): Boolean = (from, to)
match {
+ case (_: TimeType, TimestampType) => true
+ case (_: TimeType, _: TimestampLTZNanosType) => true
+ case _ => false
+ }
+
/**
* Returns true iff we can safely up-cast the `from` type to `to` type
without any truncating or
* precision lose or possible runtime failures. For example, long -> int,
string -> int are not
@@ -882,6 +911,14 @@ case class Cast(
} else {
buildCast[Float](_, f => doubleToTimestamp(f.toDouble))
}
+ case _: TimeType =>
+ // Per ANSI, the date fields come from CURRENT_DATE (resolved in the
session time zone), and
+ // the resulting local date-time is converted to an instant in that
zone. In a real query
+ // plan ComputeCurrentTime rewrites this cast with a query-stable date
literal; this eval
+ // path is the fallback for direct expression evaluation and reads the
current date in the
+ // session time zone.
+ buildCast[Long](_, nanos =>
+ DateTimeUtils.makeTimestamp(currentDate(zoneId), nanos, zoneId))
}
private[this] def castToTimestampNTZ(from: DataType): Any => Any = from
match {
@@ -932,6 +969,12 @@ case class Cast(
DateTimeUtils.timestampNTZNanosToLTZNanos(v, zoneId, precision))
case DateType =>
buildCast[Int](_, d => TimestampNanosVal.fromParts(daysToMicros(d,
zoneId), 0.toShort))
+ case _: TimeType =>
+ // See castToTimestamp: the date fields come from CURRENT_DATE in the
session time zone, with
+ // the query-stable rewrite handled by ComputeCurrentTime. The
sub-microsecond digits of the
+ // TIME value are preserved up to the target precision.
+ buildCast[Long](_, nanos =>
+ DateTimeUtils.makeTimestampLTZNanos(currentDate(zoneId), nanos,
precision, zoneId))
}
private[this] def castToTimestampNTZNanos(
@@ -1024,6 +1067,14 @@ case class Cast(
case _: TimestampNTZNanosType =>
buildCast[TimestampNanosVal](_, v =>
DateTimeUtils.truncateTimeToPrecision(
DateTimeUtils.timestampNTZNanosToNanosOfDay(v), to.precision))
+ case TimestampType =>
+ // TIMESTAMP_LTZ is an absolute instant; its time-of-day is the local
wall clock observed in
+ // the session time zone.
+ buildCast[Long](_, micros => DateTimeUtils.truncateTimeToPrecision(
+ DateTimeUtils.timestampToNanosOfDay(micros, zoneId), to.precision))
+ case _: TimestampLTZNanosType =>
+ buildCast[TimestampNanosVal](_, v =>
DateTimeUtils.truncateTimeToPrecision(
+ DateTimeUtils.timestampLTZNanosToNanosOfDay(v, zoneId), to.precision))
// Unreachable for valid casts: `canCast(_, TimeType)` only allows the
source types handled
// above (and NullType is short-circuited in castInternal). Fail fast to
keep the interpreted
// and codegen (castToTimeCode) paths consistent if a future canCast arm
is added without a
@@ -1761,6 +1812,20 @@ case class Cast(
$evPrim = $dateTimeUtilsCls.truncateTimeToPrecision(
$dateTimeUtilsCls.timestampNTZNanosToNanosOfDay($c),
${to.precision});
"""
+ case TimestampType =>
+ val zid = zoneIdValue(ctx)
+ (c, evPrim, _) =>
+ code"""
+ $evPrim = $dateTimeUtilsCls.truncateTimeToPrecision(
+ $dateTimeUtilsCls.timestampToNanosOfDay($c, $zid),
${to.precision});
+ """
+ case _: TimestampLTZNanosType =>
+ val zid = zoneIdValue(ctx)
+ (c, evPrim, _) =>
+ code"""
+ $evPrim = $dateTimeUtilsCls.truncateTimeToPrecision(
+ $dateTimeUtilsCls.timestampLTZNanosToNanosOfDay($c, $zid),
${to.precision});
+ """
// Unreachable for valid casts (see castToTime). Fail fast at codegen
time instead of
// silently emitting a null, matching the interpreted path.
case _ =>
@@ -1967,6 +2032,11 @@ case class Cast(
}
"""
}
+ case _: TimeType =>
+ val zid = zoneIdValue(ctx)
+ (c, evPrim, evNull) =>
+ code"$evPrim = $dateTimeUtilsCls.makeTimestamp(" +
+ code"$dateTimeUtilsCls.currentDate($zid), $c, $zid);"
}
private[this] def castToTimestampNTZCode(
@@ -2054,6 +2124,11 @@ case class Cast(
(c, evPrim, evNull) =>
code"$evPrim = TimestampNanosVal.fromParts(" +
code"$dateTimeUtilsCls.daysToMicros($c, $zid), (short) 0);"
+ case _: TimeType =>
+ val zid = zoneIdValue(ctx)
+ (c, evPrim, evNull) =>
+ code"$evPrim = $dateTimeUtilsCls.makeTimestampLTZNanos(" +
+ code"$dateTimeUtilsCls.currentDate($zid), $c, $precision, $zid);"
}
private[this] def castToTimestampNTZNanosCode(
diff --git
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/datetimeExpressions.scala
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/datetimeExpressions.scala
index 3dfe12cc6108..667f79c37288 100644
---
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/datetimeExpressions.scala
+++
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/datetimeExpressions.scala
@@ -3066,6 +3066,92 @@ case class MakeTimestampNTZNanos(left: Expression,
right: Expression, precision:
}
}
+/**
+ * Creates a `TIMESTAMP_LTZ` (micro `TimestampType`) from a date and a local
time interpreted in the
+ * session time zone. This is the LTZ counterpart of [[MakeTimestampNTZ]];
because the result is an
+ * absolute instant it is time-zone aware. It is an internal expression used by
+ * [[org.apache.spark.sql.catalyst.optimizer.ComputeCurrentTime]] to rewrite
+ * `CAST(time AS TIMESTAMP_LTZ)` with a query-stable current date; it is not
registered as a SQL
+ * function.
+ */
+case class MakeTimestampLTZ(
+ left: Expression,
+ right: Expression,
+ timeZoneId: Option[String] = None)
+ extends BinaryExpression
+ with RuntimeReplaceable
+ with ExpectsInputTypes
+ with TimeZoneAwareExpression {
+
+ override lazy val replacement: Expression = StaticInvoke(
+ classOf[DateTimeUtils.type],
+ TimestampType,
+ "makeTimestamp",
+ Seq(left, right, Literal.create(zoneId.getId, StringType)),
+ Seq(left.dataType, right.dataType, StringType)
+ )
+
+ override def inputTypes: Seq[AbstractDataType] = Seq(DateType, AnyTimeType)
+
+ override def prettyName: String = "make_timestamp_ltz"
+
+ // TimeZoneAwareExpression's `final override val nodePatterns` wins in
linearization and would
+ // otherwise drop RUNTIME_REPLACEABLE; re-add it via this hook, mirroring
the other
+ // RuntimeReplaceable + TimeZoneAwareExpression siblings (e.g. ParseToDate,
ParseToTimestamp).
+ override def nodePatternsInternal(): Seq[TreePattern] =
Seq(RUNTIME_REPLACEABLE)
+
+ override def withTimeZone(timeZoneId: String): TimeZoneAwareExpression =
+ copy(timeZoneId = Option(timeZoneId))
+
+ override protected def withNewChildrenInternal(
+ newLeft: Expression, newRight: Expression): Expression = {
+ copy(left = newLeft, right = newRight)
+ }
+}
+
+/**
+ * Creates a nanosecond-precision `TIMESTAMP_LTZ(precision)` (precision in [7,
9]) from a date and a
+ * local time interpreted in the session time zone, preserving the time's
sub-microsecond digits up
+ * to `precision`. This is the nanosecond, time-zone aware counterpart of
[[MakeTimestampLTZ]]. It
+ * is an internal expression used by
[[org.apache.spark.sql.catalyst.optimizer.ComputeCurrentTime]]
+ * to rewrite `CAST(time AS TIMESTAMP_LTZ(precision))` with a query-stable
current date; it is not
+ * registered as a SQL function.
+ */
+case class MakeTimestampLTZNanos(
+ left: Expression,
+ right: Expression,
+ precision: Int,
+ timeZoneId: Option[String] = None)
+ extends BinaryExpression
+ with RuntimeReplaceable
+ with ExpectsInputTypes
+ with TimeZoneAwareExpression {
+
+ override lazy val replacement: Expression = StaticInvoke(
+ classOf[DateTimeUtils.type],
+ TimestampLTZNanosType(precision),
+ "makeTimestampLTZNanos",
+ Seq(left, right, Literal(precision), Literal.create(zoneId.getId,
StringType)),
+ Seq(left.dataType, right.dataType, IntegerType, StringType)
+ )
+
+ override def inputTypes: Seq[AbstractDataType] = Seq(DateType, AnyTimeType)
+
+ override def prettyName: String = "make_timestamp_ltz_nanos"
+
+ // See MakeTimestampLTZ: re-add RUNTIME_REPLACEABLE that
TimeZoneAwareExpression's final
+ // nodePatterns would otherwise drop in linearization.
+ override def nodePatternsInternal(): Seq[TreePattern] =
Seq(RUNTIME_REPLACEABLE)
+
+ override def withTimeZone(timeZoneId: String): TimeZoneAwareExpression =
+ copy(timeZoneId = Option(timeZoneId))
+
+ override protected def withNewChildrenInternal(
+ newLeft: Expression, newRight: Expression): Expression = {
+ copy(left = newLeft, right = newRight)
+ }
+}
+
// scalastyle:off line.size.limit
@ExpressionDescription(
usage = """
diff --git
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/finishAnalysis.scala
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/finishAnalysis.scala
index 74182f781890..437b67d0855d 100644
---
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/finishAnalysis.scala
+++
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/finishAnalysis.scala
@@ -120,13 +120,14 @@ object ComputeCurrentTime extends Rule[LogicalPlan] {
val currentDates = collection.mutable.HashMap.empty[ZoneId, Literal]
val localTimestamps = collection.mutable.HashMap.empty[ZoneId, Literal]
- // The CAST bit is included so this rule can find TIME -> TIMESTAMP_NTZ
casts (which depend on
- // CURRENT_DATE) and stabilize them below. CAST is a broad pattern, so
this widens the rule's
- // traversal to most plans; the precise `Cast.isTimeToTimestampNTZ` guard
keeps the rewrite
+ // The CAST bit is included so this rule can find TIME -> TIMESTAMP_NTZ
and TIME ->
+ // TIMESTAMP_LTZ casts (which derive their date fields from CURRENT_DATE)
and stabilize them
+ // below. CAST is a broad pattern, so this widens the rule's traversal to
most plans; the
+ // precise `Cast.isTimeToTimestampNTZ` / `Cast.isTimeToTimestampLTZ`
guards keep the rewrite
// scoped. We intentionally do not tag these casts with CURRENT_LIKE
instead: inline-table
// validation treats CURRENT_LIKE as safe to defer, so tagging would let
unrelated non-foldable
- // NTZ-target casts (e.g. CAST(rand() AS TIMESTAMP_NTZ)) bypass that
validation (see SPARK-57618
- // and ResolveInlineTablesSuite).
+ // NTZ/LTZ-target casts (e.g. CAST(rand() AS TIMESTAMP_NTZ)) bypass that
validation (see
+ // SPARK-57618 and ResolveInlineTablesSuite).
def transformCondition(treePatternbits: TreePatternBits): Boolean = {
treePatternbits.containsPattern(CURRENT_LIKE) ||
treePatternbits.containsPattern(CAST)
}
@@ -160,6 +161,25 @@ object ComputeCurrentTime extends Rule[LogicalPlan] {
throw SparkException.internalError(
s"Unexpected target type in TIME -> TIMESTAMP_NTZ rewrite:
$other")
}
+ // CAST(time AS TIMESTAMP_LTZ(q)) likewise fills the date fields
from CURRENT_DATE.
+ // Rewrite it to a zone-aware date+time builder anchored on the same
query-stable current
+ // date literal, so all references agree within the query.
+ case c: Cast if Cast.isTimeToTimestampLTZ(c.child.dataType,
c.dataType) =>
+ val dateLit = currentDates.getOrElseUpdate(c.zoneId, {
+ Literal.create(
+ DateTimeUtils.microsToDays(currentTimestampMicros, c.zoneId),
DateType)
+ })
+ c.dataType match {
+ case l: TimestampLTZNanosType =>
+ MakeTimestampLTZNanos(dateLit, c.child, l.precision,
c.timeZoneId).replacement
+ case _: TimestampType =>
+ MakeTimestampLTZ(dateLit, c.child, c.timeZoneId).replacement
+ case other =>
+ // Unreachable: the outer guard `Cast.isTimeToTimestampLTZ`
only matches the micro
+ // TimestampType and the nanosecond TimestampLTZNanosType
targets.
+ throw SparkException.internalError(
+ s"Unexpected target type in TIME -> TIMESTAMP_LTZ rewrite:
$other")
+ }
case currentTimeType : CurrentTime =>
val truncatedTime = truncateTimeToPrecision(currentTimeOfDayNanos,
currentTimeType.precision)
diff --git
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/util/DateTimeUtils.scala
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/util/DateTimeUtils.scala
index 48ca74c29f25..481f99709858 100644
---
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/util/DateTimeUtils.scala
+++
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/util/DateTimeUtils.scala
@@ -1143,6 +1143,42 @@ object DateTimeUtils extends SparkDateTimeUtils {
makeTimestamp(days, nanos, zoneId)
}
+ /**
+ * Makes a nanosecond-precision `TIMESTAMP_LTZ(precision)` (precision in [7,
9]) from a date and
+ * a local time interpreted in the time zone `zoneId`, preserving the time's
sub-microsecond
+ * digits up to `precision`. This is the nanosecond counterpart of
+ * [[makeTimestamp(days:Int,nanos:Long,zoneId:java\.time\.ZoneId)*]].
+ *
+ * @param days The number of days since the epoch 1970-01-01.
+ * Negative numbers represent earlier days.
+ * @param nanos The number of nanoseconds within the day since midnight.
+ * @param precision The fractional-second precision of the target
`TIMESTAMP_LTZ(precision)`.
+ * @param zoneId The time zone ID at which the operation is performed.
+ * @return The composite `(epochMicros, nanosWithinMicro)` pair since the
epoch
+ * 1970-01-01 00:00:00Z.
+ */
+ def makeTimestampLTZNanos(
+ days: Int,
+ nanos: Long,
+ precision: Int,
+ zoneId: ZoneId): TimestampNanosVal = {
+ val ldt = LocalDateTime.of(daysToLocalDate(days), nanosToLocalTime(nanos))
+ instantToTimestampNanos(ldt.atZone(zoneId).toInstant, precision)
+ }
+
+ /**
+ * Makes a nanosecond-precision `TIMESTAMP_LTZ(precision)` from a date and a
local time with a
+ * time zone string. Used by the `CAST(time AS TIMESTAMP_LTZ(precision))`
rewrite, which embeds
+ * the resolved session time zone as a string literal.
+ */
+ def makeTimestampLTZNanos(
+ days: Int,
+ nanos: Long,
+ precision: Int,
+ timezone: UTF8String): TimestampNanosVal = {
+ makeTimestampLTZNanos(days, nanos, precision, getZoneId(timezone.toString))
+ }
+
/**
* Adds a day-time interval to a time.
*
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 73b7c872fc13..8cc38996d3f8 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
@@ -2504,50 +2504,40 @@ abstract class CastSuiteBase extends SparkFunSuite with
ExpressionEvalHelper {
}
}
- test("cast between TIME and TIMESTAMP_NTZ is allowed only for the NTZ
family") {
+ test("cast between TIME and TIMESTAMP_NTZ / TIMESTAMP_LTZ is allowed") {
for (p <- TimeType.MIN_PRECISION to TimeType.MAX_PRECISION) {
- assert(Cast.canCast(TimeType(p), TimestampNTZType))
- assert(Cast.canCast(TimestampNTZType, TimeType(p)))
- assert(Cast.canAnsiCast(TimeType(p), TimestampNTZType))
- assert(Cast.canAnsiCast(TimestampNTZType, TimeType(p)))
// try_cast inherits the allowed pairs (canTryCast delegates to
canAnsiCast for atomic types).
// These casts never fail, so try_cast behaves exactly like cast.
- assert(Cast.canTryCast(TimeType(p), TimestampNTZType))
- assert(Cast.canTryCast(TimestampNTZType, TimeType(p)))
- // TIMESTAMP_LTZ (the micro TimestampType) is not a valid counterpart.
- assert(!Cast.canCast(TimeType(p), TimestampType))
- assert(!Cast.canCast(TimestampType, TimeType(p)))
- assert(!Cast.canAnsiCast(TimeType(p), TimestampType))
- assert(!Cast.canAnsiCast(TimestampType, TimeType(p)))
- assert(!Cast.canTryCast(TimeType(p), TimestampType))
- assert(!Cast.canTryCast(TimestampType, TimeType(p)))
- // The disallowed pairs are rejected at analysis, not merely by the
canCast predicate. The
- // suite `cast` helper applies the active evalMode, so canAnsiCast /
canCast is exercised
- // under ANSI on / off respectively.
- assert(cast(Literal.create(0L, TimeType(p)),
TimestampType).checkInputDataTypes().isFailure)
- assert(cast(Literal.create(0L, TimestampType),
TimeType(p)).checkInputDataTypes().isFailure)
+ Seq(TimestampNTZType, TimestampType).foreach { micro =>
+ assert(Cast.canCast(TimeType(p), micro))
+ assert(Cast.canCast(micro, TimeType(p)))
+ assert(Cast.canAnsiCast(TimeType(p), micro))
+ assert(Cast.canAnsiCast(micro, TimeType(p)))
+ assert(Cast.canTryCast(TimeType(p), micro))
+ assert(Cast.canTryCast(micro, TimeType(p)))
+ }
foreachNanosPrecision { q =>
- assert(Cast.canCast(TimeType(p), TimestampNTZNanosType(q)))
- assert(Cast.canCast(TimestampNTZNanosType(q), TimeType(p)))
- assert(Cast.canAnsiCast(TimeType(p), TimestampNTZNanosType(q)))
- assert(Cast.canAnsiCast(TimestampNTZNanosType(q), TimeType(p)))
- assert(Cast.canTryCast(TimeType(p), TimestampNTZNanosType(q)))
- assert(Cast.canTryCast(TimestampNTZNanosType(q), TimeType(p)))
- // TIMESTAMP_LTZ nanos is not a valid counterpart either.
- assert(!Cast.canCast(TimeType(p), TimestampLTZNanosType(q)))
- assert(!Cast.canCast(TimestampLTZNanosType(q), TimeType(p)))
- assert(!Cast.canAnsiCast(TimeType(p), TimestampLTZNanosType(q)))
- assert(!Cast.canAnsiCast(TimestampLTZNanosType(q), TimeType(p)))
- assert(!Cast.canTryCast(TimeType(p), TimestampLTZNanosType(q)))
- assert(!Cast.canTryCast(TimestampLTZNanosType(q), TimeType(p)))
+ Seq(TimestampNTZNanosType(q), TimestampLTZNanosType(q)).foreach {
nanos =>
+ assert(Cast.canCast(TimeType(p), nanos))
+ assert(Cast.canCast(nanos, TimeType(p)))
+ assert(Cast.canAnsiCast(TimeType(p), nanos))
+ assert(Cast.canAnsiCast(nanos, TimeType(p)))
+ assert(Cast.canTryCast(TimeType(p), nanos))
+ assert(Cast.canTryCast(nanos, TimeType(p)))
+ }
}
}
- // Only TIME -> TIMESTAMP_NTZ depends on the session time zone
(CURRENT_DATE); the reverse
- // direction extracts the UTC wall-clock time-of-day and is
zone-independent.
+ // TIME -> TIMESTAMP_NTZ depends on the session time zone (CURRENT_DATE);
the reverse direction
+ // extracts the UTC wall-clock time-of-day and is zone-independent. Both
directions of
+ // TIME <-> TIMESTAMP_LTZ depend on the session zone (the LTZ value is an
absolute instant).
assert(Cast.needsTimeZone(TimeType(0), TimestampNTZType))
assert(Cast.needsTimeZone(TimeType(9), TimestampNTZNanosType(9)))
assert(!Cast.needsTimeZone(TimestampNTZType, TimeType(0)))
assert(!Cast.needsTimeZone(TimestampNTZNanosType(9), TimeType(9)))
+ assert(Cast.needsTimeZone(TimeType(0), TimestampType))
+ assert(Cast.needsTimeZone(TimestampType, TimeType(0)))
+ assert(Cast.needsTimeZone(TimeType(9), TimestampLTZNanosType(9)))
+ assert(Cast.needsTimeZone(TimestampLTZNanosType(9), TimeType(9)))
}
test("isTimeToTimestampNTZ identifies only TIME -> TIMESTAMP_NTZ") {
@@ -2570,6 +2560,28 @@ abstract class CastSuiteBase extends SparkFunSuite with
ExpressionEvalHelper {
assert(!Cast.isTimeToTimestampNTZ(TimestampNTZType, TimestampNTZType))
}
+ test("isTimeToTimestampLTZ identifies only TIME -> TIMESTAMP_LTZ") {
+ for (p <- TimeType.MIN_PRECISION to TimeType.MAX_PRECISION) {
+ // True only for TIME -> TIMESTAMP_LTZ (the current-date-dependent
direction). q = 6 is the
+ // micro TimestampType.
+ assert(Cast.isTimeToTimestampLTZ(TimeType(p), TimestampType))
+ // The reverse direction is handled by the eval/codegen path, and
TIMESTAMP_NTZ is not a
+ // counterpart of this predicate.
+ assert(!Cast.isTimeToTimestampLTZ(TimestampType, TimeType(p)))
+ assert(!Cast.isTimeToTimestampLTZ(TimeType(p), TimestampNTZType))
+ assert(!Cast.isTimeToTimestampLTZ(TimestampNTZType, TimeType(p)))
+ foreachNanosPrecision { q =>
+ assert(Cast.isTimeToTimestampLTZ(TimeType(p),
TimestampLTZNanosType(q)))
+ assert(!Cast.isTimeToTimestampLTZ(TimestampLTZNanosType(q),
TimeType(p)))
+ assert(!Cast.isTimeToTimestampLTZ(TimeType(p),
TimestampNTZNanosType(q)))
+ assert(!Cast.isTimeToTimestampLTZ(TimestampNTZNanosType(q),
TimeType(p)))
+ }
+ }
+ // Pairs that involve neither a TIME source nor a TIMESTAMP_LTZ target are
false.
+ assert(!Cast.isTimeToTimestampLTZ(DateType, TimestampType))
+ assert(!Cast.isTimeToTimestampLTZ(TimestampType, TimestampType))
+ }
+
test("cast timestamp_ntz to time") {
// Per ANSI rule 15.d the time-of-day fields are extracted and truncated
to the target
// precision; the operation is deterministic and time-zone independent.
@@ -2629,6 +2641,78 @@ abstract class CastSuiteBase extends SparkFunSuite with
ExpressionEvalHelper {
}
}
+ test("cast timestamp_ltz to time") {
+ // The LTZ value is an absolute instant; its time-of-day is the local wall
clock observed in the
+ // session time zone, truncated to the target precision. Use a fixed
non-UTC zone to exercise
+ // the zone-dependent path; the instants below avoid DST transitions in
that zone.
+ val zone = LA
+ val zid = Option(zone.getId)
+ val instants = Seq(
+ timestampLTZ(2020, 5, 17, 12, 34, 56, 789012345, zone),
+ timestampLTZ(1969, 12, 31, 23, 59, 59, 123456789, zone),
+ timestampLTZ(1, 1, 1, 0, 0, 0, 1, zone))
+ instants.foreach { inst =>
+ for (p <- TimeType.MIN_PRECISION to TimeType.MAX_PRECISION) {
+ // q = 6: the micro TimestampType. Its time-of-day has microsecond
resolution.
+ val micros = DateTimeUtils.instantToMicros(inst)
+ val expectedFromMicros = truncateTimeToPrecision(
+
DateTimeUtils.microsToInstant(micros).atZone(zone).toLocalTime.toNanoOfDay, p)
+ checkEvaluation(
+ cast(Literal.create(micros, TimestampType), TimeType(p), zid),
expectedFromMicros)
+
+ // q in [7, 9]: the nanosecond TimestampLTZNanosType.
+ foreachNanosPrecision { q =>
+ val truncatedInst =
+ inst.minusNanos((inst.getNano -
nanoOfSecTruncator(q)(inst.getNano)).toLong)
+ val v = instantToNanosVal(truncatedInst)
+ val expected =
+
truncateTimeToPrecision(truncatedInst.atZone(zone).toLocalTime.toNanoOfDay, p)
+ checkEvaluation(
+ cast(Literal.create(v, TimestampLTZNanosType(q)), TimeType(p),
zid), expected)
+ }
+ }
+ }
+
+ // Interpreted vs codegen consistency for the zone-dependent reverse
direction.
+ for (p <- TimeType.MIN_PRECISION to TimeType.MAX_PRECISION) {
+ checkConsistencyBetweenInterpretedAndCodegen(
+ (child: Expression) => Cast(child, TimeType(p), zid), TimestampType)
+ foreachNanosPrecision { q =>
+ checkConsistencyBetweenInterpretedAndCodegen(
+ (child: Expression) => Cast(child, TimeType(p), zid),
TimestampLTZNanosType(q))
+ }
+ }
+ }
+
+ test("cast time to timestamp_ltz and back") {
+ // TIME -> TIMESTAMP_LTZ takes the date from CURRENT_DATE, which cancels
out on the round trip
+ // back to TIME. Pin the zone to UTC so no DST transition can shift the
wall clock, making these
+ // assertions deterministic regardless of the current date.
+ val times = Seq(
+ localTime(0, 0, 0),
+ localTime(12, 34, 56, 789012),
+ localTime(12, 34, 56, 789012, 345),
+ localTime(23, 59, 59, 999999, 999))
+ times.foreach { nanos =>
+ // Both casts are pinned to UTC: unlike the NTZ reverse direction,
TIMESTAMP_LTZ -> TIME is
+ // zone-dependent, so the outer cast must read the time-of-day in the
same zone the inner cast
+ // used to build the instant.
+ // q = 6 keeps microsecond resolution.
+ checkEvaluation(
+ cast(cast(Literal(nanos, TimeType(9)), TimestampType, UTC_OPT),
TimeType(9), UTC_OPT),
+ truncateTimeToPrecision(nanos, 6))
+ // q in [7, 9] keeps the corresponding sub-microsecond digits.
+ foreachNanosPrecision { q =>
+ checkEvaluation(
+ cast(
+ cast(Literal(nanos, TimeType(9)), TimestampLTZNanosType(q),
UTC_OPT),
+ TimeType(9),
+ UTC_OPT),
+ truncateTimeToPrecision(nanos, q))
+ }
+ }
+ }
+
test("SPARK-52620: cast time to decimal with sufficient precision and
scale") {
// Test various TIME values converted to DecimalType(14, 9), which always
has sufficient
// precision and scale to represent the number of (nano)seconds since
midnight. Note that
diff --git
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/DateExpressionsSuite.scala
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/DateExpressionsSuite.scala
index 474a8a04aa71..0d7a01fb2953 100644
---
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/DateExpressionsSuite.scala
+++
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/DateExpressionsSuite.scala
@@ -2625,6 +2625,91 @@ class DateExpressionsSuite extends SparkFunSuite with
ExpressionEvalHelper {
MakeTimestampNTZNanos(dateLit(date), timeNanos, 7).canonicalized)
}
+ test("SPARK-57660: make timestamp_ltz from date and time") {
+ Seq(
+ ("2025-06-20", "15:20:30.123456", "America/Los_Angeles", LA),
+ ("2025-06-20", "15:20:30.123456", "+01:00", CET)
+ ).foreach { case (date, time, tz, zoneId) =>
+ // The local date-time is interpreted in the given zone to produce the
instant.
+ checkEvaluation(
+ MakeTimestampLTZ(dateLit(date), timeLit(time), Option(tz)),
+ timestampToMicros(s"${date}T${time}", zoneId))
+ }
+ // Null inputs propagate to a null result.
+ checkEvaluation(
+ MakeTimestampLTZ(Literal(null, DateType), timeLit("15:20:30"), UTC_OPT),
null)
+ checkEvaluation(
+ MakeTimestampLTZ(dateLit("2025-06-20"), Literal(null, TimeType()),
UTC_OPT), null)
+ // The result type is the micro TIMESTAMP_LTZ (TimestampType).
+ assert(MakeTimestampLTZ(dateLit("2025-06-20"), timeLit("15:20:30"),
UTC_OPT).dataType ===
+ TimestampType)
+ }
+
+ test("SPARK-57660: make nanosecond timestamp_ltz from date and time") {
+ val date = "2025-06-20"
+ val timeNanos = Literal.create(localTime(15, 20, 30, 123456, 789),
TimeType(9))
+ Seq("America/Los_Angeles" -> LA, "+01:00" -> CET).foreach { case (tz,
zoneId) =>
+ val micros = timestampToMicros(s"${date}T15:20:30.123456", zoneId)
+ // Precision 9 preserves all sub-microsecond digits; lower precisions
floor them.
+ checkEvaluation(MakeTimestampLTZNanos(dateLit(date), timeNanos, 9,
Option(tz)),
+ TimestampNanosVal.fromParts(micros, 789.toShort))
+ checkEvaluation(MakeTimestampLTZNanos(dateLit(date), timeNanos, 8,
Option(tz)),
+ TimestampNanosVal.fromParts(micros, 780.toShort))
+ checkEvaluation(MakeTimestampLTZNanos(dateLit(date), timeNanos, 7,
Option(tz)),
+ TimestampNanosVal.fromParts(micros, 700.toShort))
+ }
+ // Pre-epoch date.
+ val preEpochMicros = timestampToMicros("1969-12-31T23:59:59.123456", UTC)
+ checkEvaluation(
+ MakeTimestampLTZNanos(dateLit("1969-12-31"),
+ Literal.create(localTime(23, 59, 59, 123456, 789), TimeType(9)), 9,
UTC_OPT),
+ TimestampNanosVal.fromParts(preEpochMicros, 789.toShort))
+ // Null inputs propagate to a null result.
+ checkEvaluation(MakeTimestampLTZNanos(Literal(null, DateType), timeNanos,
9, UTC_OPT), null)
+ checkEvaluation(
+ MakeTimestampLTZNanos(dateLit(date), Literal(null, TimeType(9)), 9,
UTC_OPT), null)
+ // The result type carries the requested nanosecond precision.
+ assert(MakeTimestampLTZNanos(dateLit(date), timeNanos, 7,
UTC_OPT).dataType ===
+ TimestampLTZNanosType(7))
+ // The precision participates in equality and canonicalization: builders
that differ only in
+ // precision are distinct, so two casts to different TIMESTAMP_LTZ(q) are
never conflated.
+ assert(MakeTimestampLTZNanos(dateLit(date), timeNanos, 9, UTC_OPT) !=
+ MakeTimestampLTZNanos(dateLit(date), timeNanos, 7, UTC_OPT))
+ assert(MakeTimestampLTZNanos(dateLit(date), timeNanos, 9,
UTC_OPT).canonicalized ==
+ MakeTimestampLTZNanos(dateLit(date), timeNanos, 9,
UTC_OPT).canonicalized)
+ assert(MakeTimestampLTZNanos(dateLit(date), timeNanos, 9,
UTC_OPT).canonicalized !=
+ MakeTimestampLTZNanos(dateLit(date), timeNanos, 7,
UTC_OPT).canonicalized)
+ }
+
+ test("SPARK-57660: make timestamp_ltz across DST transitions") {
+ // This is the zone-conversion branch of the TIME -> TIMESTAMP_LTZ cast
(ComputeCurrentTime
+ // rewrites the cast into these builders). The Cast itself can't be
DST-tested deterministically
+ // because it takes the date from CURRENT_DATE, so the gap/overlap
behavior is asserted here on
+ // the builders with explicit transition dates.
+ val tz = "America/Los_Angeles"
+ // Spring-forward gap: 2020-03-08 02:30 does not exist in LA; java.time
shifts it forward to
+ // 03:30 PDT. The builder must resolve to that instant.
+ val gapInstant = LocalDateTime.of(2020, 3, 8, 3, 30,
0).atZone(LA).toInstant
+ checkEvaluation(
+ MakeTimestampLTZ(dateLit("2020-03-08"), timeLit("02:30:00"), Option(tz)),
+ DateTimeUtils.instantToMicros(gapInstant))
+ // Sub-microsecond digits survive the gap shift.
+ val gapNanoInstant = LocalDateTime.of(2020, 3, 8, 3, 30, 0,
789012000).atZone(LA).toInstant
+ checkEvaluation(
+ MakeTimestampLTZNanos(
+ dateLit("2020-03-08"),
+ Literal.create(localTime(2, 30, 0, 789012, 345), TimeType(9)), 9,
Option(tz)),
+
TimestampNanosVal.fromParts(DateTimeUtils.instantToMicros(gapNanoInstant),
345.toShort))
+ // Fall-back overlap: 2020-11-01 01:30 occurs twice in LA; java.time picks
the earlier offset
+ // (PDT, UTC-7). Pin the expected instant to that explicit offset so the
assertion does not just
+ // mirror the builder's own atZone resolution.
+ val overlapInstant =
+ LocalDateTime.of(2020, 11, 1, 1, 30,
0).atOffset(java.time.ZoneOffset.ofHours(-7)).toInstant
+ checkEvaluation(
+ MakeTimestampLTZ(dateLit("2020-11-01"), timeLit("01:30:00"), Option(tz)),
+ DateTimeUtils.instantToMicros(overlapInstant))
+ }
+
test("SPARK-53113: try to make timestamp from date, time, and timezone") {
Seq(
("2023-10-01", "12:34:56.123456", "America/Los_Angeles", LA),
diff --git
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/optimizer/ComputeCurrentTimeSuite.scala
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/optimizer/ComputeCurrentTimeSuite.scala
index 34b38cbaed8f..a94bb6f6c5c1 100644
---
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/optimizer/ComputeCurrentTimeSuite.scala
+++
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/optimizer/ComputeCurrentTimeSuite.scala
@@ -30,7 +30,7 @@ import org.apache.spark.sql.catalyst.plans.logical.{Filter,
LocalRelation, Logic
import org.apache.spark.sql.catalyst.rules.RuleExecutor
import org.apache.spark.sql.catalyst.util.DateTimeUtils
import org.apache.spark.sql.internal.SQLConf
-import org.apache.spark.sql.types.{DateType, IntegerType,
TimestampNTZNanosType, TimestampNTZType, TimeType}
+import org.apache.spark.sql.types.{DateType, IntegerType,
TimestampLTZNanosType, TimestampNTZNanosType, TimestampNTZType, TimestampType,
TimeType}
import org.apache.spark.unsafe.types.UTF8String
class ComputeCurrentTimeSuite extends PlanTest {
@@ -297,6 +297,30 @@ class ComputeCurrentTimeSuite extends PlanTest {
assert(remainingCasts.isEmpty)
}
+ test("CAST(time AS TIMESTAMP_LTZ) is stabilized with the query current
date") {
+ val timeLit = Literal(0L, TimeType(6))
+ val in = Project(Seq(
+ Alias(Cast(timeLit, TimestampType), "a")(),
+ Alias(Cast(timeLit, TimestampLTZNanosType(9)), "b")(),
+ Alias(CurrentDate(), "c")()), LocalRelation())
+
+ val min = DateTimeUtils.currentDate(ZoneId.systemDefault())
+ val plan = Optimize.execute(in.analyze).asInstanceOf[Project]
+ val max = DateTimeUtils.currentDate(ZoneId.systemDefault())
+
+ // The two casts and current_date() must all be anchored to the same
current-date literal.
+ val dateLits = dateLiterals(plan)
+ assert(dateLits.size == 3)
+ assert(dateLits.toSet.size == 1)
+ assert(dateLits.forall(d => d >= min && d <= max))
+
+ // The TIME -> TIMESTAMP_LTZ casts must be rewritten away (replaced by a
date+time builder).
+ val remainingCasts = plan.flatMap(_.expressions.flatMap(_.collect {
+ case c: Cast if Cast.isTimeToTimestampLTZ(c.child.dataType, c.dataType)
=> c
+ }))
+ assert(remainingCasts.isEmpty)
+ }
+
private def dateLiterals(plan: LogicalPlan):
scala.collection.mutable.ArrayBuffer[Int] = {
val buf = new scala.collection.mutable.ArrayBuffer[Int]
plan.transformWithSubqueries { case subQuery =>
diff --git
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/util/DateTimeUtilsSuite.scala
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/util/DateTimeUtilsSuite.scala
index e60ebf118207..6db96d8d366a 100644
---
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/util/DateTimeUtilsSuite.scala
+++
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/util/DateTimeUtilsSuite.scala
@@ -1673,6 +1673,47 @@ class DateTimeUtilsSuite extends SparkFunSuite with
Matchers with SQLHelper {
localTime(23, 59, 59, 123456, 789))
}
+ test("makeTimestampLTZNanos") {
+ // At UTC the local date-time coincides with the instant, so the result
matches the NTZ builder.
+ val nanos = localTime(23, 59, 59, 999999, 999)
+ val microsOfDay = localTime(23, 59, 59, 999999) / NANOS_PER_MICROS
+ assert(makeTimestampLTZNanos(0, nanos, 9, ZoneOffset.UTC) ===
+ TimestampNanosVal.fromParts(microsOfDay, 999.toShort))
+ assert(makeTimestampLTZNanos(0, nanos, 8, ZoneOffset.UTC) ===
+ TimestampNanosVal.fromParts(microsOfDay, 990.toShort))
+ assert(makeTimestampLTZNanos(0, nanos, 7, ZoneOffset.UTC) ===
+ TimestampNanosVal.fromParts(microsOfDay, 900.toShort))
+ // Pre-epoch date at UTC.
+ assert(makeTimestampLTZNanos(
+ days(1969, 12, 31), localTime(23, 59, 59, 123456, 789), 9,
ZoneOffset.UTC) ===
+ TimestampNanosVal.fromParts(date(1969, 12, 31, 23, 59, 59, 123456),
789.toShort))
+ // A non-UTC zone shifts the epoch-micros part by the zone offset;
sub-micro digits survive.
+ val zone = getZoneId("America/Los_Angeles")
+ val inst = LocalDateTime.of(2020, 5, 17, 12, 34, 56,
789012345).atZone(zone).toInstant
+ assert(makeTimestampLTZNanos(days(2020, 5, 17), localTime(12, 34, 56,
789012, 345), 9, zone) ===
+ TimestampNanosVal.fromParts(instantToMicros(inst), 345.toShort))
+ }
+
+ test("timestampLTZ time-of-day extraction") {
+ val zone = getZoneId("America/Los_Angeles")
+ // Micro TimestampLTZ: time-of-day is the local wall clock observed in the
zone.
+ val microInst = LocalDateTime.of(2020, 5, 17, 12, 34, 56,
789012000).atZone(zone).toInstant
+ assert(timestampToNanosOfDay(instantToMicros(microInst), zone) ===
+ localTime(12, 34, 56, 789012))
+ // At UTC the time-of-day equals the value modulo one day, including
pre-epoch values.
+ assert(timestampToNanosOfDay(0, ZoneOffset.UTC) === 0)
+ assert(timestampToNanosOfDay(date(1969, 12, 31, 23, 59, 59, 123456),
ZoneOffset.UTC) ===
+ localTime(23, 59, 59, 123456))
+
+ // Nanosecond TimestampLTZ preserves the sub-microsecond digits.
+ val nanoInst = LocalDateTime.of(2020, 5, 17, 12, 34, 56,
789012345).atZone(zone).toInstant
+ val v = TimestampNanosVal.fromParts(instantToMicros(nanoInst), 345.toShort)
+ assert(timestampLTZNanosToNanosOfDay(v, zone) === localTime(12, 34, 56,
789012, 345))
+ val vPre = TimestampNanosVal.fromParts(date(1969, 12, 31, 23, 59, 59,
123456), 789.toShort)
+ assert(timestampLTZNanosToNanosOfDay(vPre, ZoneOffset.UTC) ===
+ localTime(23, 59, 59, 123456, 789))
+ }
+
test("instant to nanos of day") {
assert(instantToNanosOfDay(Instant.parse("1970-01-01T00:00:01.001002003Z"),
"UTC") ==
1001002003)
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 91788c515310..fd23613e3ad4 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
@@ -1992,3 +1992,250 @@ org.apache.spark.sql.catalyst.ExtendedAnalysisException
"fragment" : "CASE WHEN true THEN time'12:34:56' ELSE
timestamp_ntz'2020-05-17 12:34:56' END"
} ]
}
+
+
+-- !query
+SELECT CAST(timestamp'2020-05-17 12:34:56.789012' AS TIME(6))
+-- !query analysis
+[Analyzer test output redacted due to nondeterminism]
+
+
+-- !query
+SELECT CAST(timestamp'2020-05-17 12:34:56.789012' AS TIME(3))
+-- !query analysis
+[Analyzer test output redacted due to nondeterminism]
+
+
+-- !query
+SELECT CAST(timestamp'2020-05-17 12:34:56.789012' AS TIME(0))
+-- !query analysis
+[Analyzer test output redacted due to nondeterminism]
+
+
+-- !query
+SELECT CAST(timestamp_ltz'2020-05-17 12:34:56.789012345'::timestamp_ltz(9) AS
TIME(9))
+-- !query analysis
+Project [cast(cast(2020-05-17 12:34:56.789012345 as timestamp_ltz(9)) as
time(9)) AS CAST(CAST(TIMESTAMP_LTZ '2020-05-17 12:34:56.789012345' AS
TIMESTAMP_LTZ(9)) AS TIME(9))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT CAST(timestamp_ltz'2020-05-17 12:34:56.789012345'::timestamp_ltz(9) AS
TIME(7))
+-- !query analysis
+Project [cast(cast(2020-05-17 12:34:56.789012345 as timestamp_ltz(9)) as
time(7)) AS CAST(CAST(TIMESTAMP_LTZ '2020-05-17 12:34:56.789012345' AS
TIMESTAMP_LTZ(9)) AS TIME(7))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT CAST(timestamp_ltz'2020-05-17 12:34:56.789012345'::timestamp_ltz(9) AS
TIME(6))
+-- !query analysis
+Project [cast(cast(2020-05-17 12:34:56.789012345 as timestamp_ltz(9)) as
time(6)) AS CAST(CAST(TIMESTAMP_LTZ '2020-05-17 12:34:56.789012345' AS
TIMESTAMP_LTZ(9)) AS TIME(6))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT CAST(timestamp_ltz'2020-05-17 12:34:56.789012345'::timestamp_ltz(7) AS
TIME(9))
+-- !query analysis
+Project [cast(cast(2020-05-17 12:34:56.789012345 as timestamp_ltz(7)) as
time(9)) AS CAST(CAST(TIMESTAMP_LTZ '2020-05-17 12:34:56.789012345' AS
TIMESTAMP_LTZ(7)) AS TIME(9))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT timestamp'2020-05-17 12:34:56.789012' :: TIME(6)
+-- !query analysis
+[Analyzer test output redacted due to nondeterminism]
+
+
+-- !query
+SELECT typeof(CAST(TIME'12:34:56' AS TIMESTAMP_LTZ))
+-- !query analysis
+Project [typeof(cast(12:34:56 as timestamp)) AS typeof(CAST(TIME '12:34:56' AS
TIMESTAMP))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT typeof(CAST(TIME'12:34:56' AS TIMESTAMP_LTZ(9)))
+-- !query analysis
+Project [typeof(cast(12:34:56 as timestamp_ltz(9))) AS typeof(CAST(TIME
'12:34:56' AS TIMESTAMP_LTZ(9)))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT CAST(CAST(TIME'12:34:56' AS TIMESTAMP_LTZ) AS DATE) = current_date()
+-- !query analysis
+[Analyzer test output redacted due to nondeterminism]
+
+
+-- !query
+SELECT CAST(CAST(TIME'12:34:56.789012345' AS TIMESTAMP_LTZ(9)) AS DATE) =
current_date()
+-- !query analysis
+[Analyzer test output redacted due to nondeterminism]
+
+
+-- !query
+SELECT CAST(CAST(TIME'12:34:56.789012' AS TIMESTAMP_LTZ) AS TIME(6))
+-- !query analysis
+Project [cast(cast(12:34:56.789012 as timestamp) as time(6)) AS CAST(CAST(TIME
'12:34:56.789012' AS TIMESTAMP) AS TIME(6))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT CAST(CAST(TIME'12:34:56.789012345' AS TIMESTAMP_LTZ(9)) AS TIME(9))
+-- !query analysis
+Project [cast(cast(12:34:56.789012345 as timestamp_ltz(9)) as time(9)) AS
CAST(CAST(TIME '12:34:56.789012345' AS TIMESTAMP_LTZ(9)) AS TIME(9))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT CAST(CAST(TIME'12:34:56.789012345' AS TIMESTAMP_LTZ(7)) AS TIME(9))
+-- !query analysis
+Project [cast(cast(12:34:56.789012345 as timestamp_ltz(7)) as time(9)) AS
CAST(CAST(TIME '12:34:56.789012345' AS TIMESTAMP_LTZ(7)) AS TIME(9))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT CAST(CAST(TIME'12:34:56.789012345'::time(9) AS TIMESTAMP_LTZ(6)) AS
TIME(9))
+-- !query analysis
+Project [cast(cast(cast(12:34:56.789012345 as time(9)) as timestamp) as
time(9)) AS CAST(CAST(CAST(TIME '12:34:56.789012345' AS TIME(9)) AS TIMESTAMP)
AS TIME(9))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT CAST(CAST(NULL AS TIME) AS TIMESTAMP_LTZ)
+-- !query analysis
+Project [cast(cast(null as time(6)) as timestamp) AS CAST(CAST(NULL AS
TIME(6)) AS TIMESTAMP)#x]
++- OneRowRelation
+
+
+-- !query
+SELECT CAST(CAST(NULL AS TIME) AS TIMESTAMP_LTZ(9))
+-- !query analysis
+Project [cast(cast(null as time(6)) as timestamp_ltz(9)) AS CAST(CAST(NULL AS
TIME(6)) AS TIMESTAMP_LTZ(9))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT CAST(CAST(NULL AS TIMESTAMP_LTZ) AS TIME(6))
+-- !query analysis
+Project [cast(cast(null as timestamp) as time(6)) AS CAST(CAST(NULL AS
TIMESTAMP) AS TIME(6))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT CAST(CAST(NULL AS TIMESTAMP_LTZ(9)) AS TIME(6))
+-- !query analysis
+Project [cast(cast(null as timestamp_ltz(9)) as time(6)) AS CAST(CAST(NULL AS
TIMESTAMP_LTZ(9)) AS TIME(6))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT CAST(x AS DATE) = current_date() FROM VALUES (CAST(TIME'12:34:56' AS
TIMESTAMP_LTZ)) t(x)
+-- !query analysis
+[Analyzer test output redacted due to nondeterminism]
+
+
+-- !query
+SELECT CAST(x AS TIME(6)) FROM VALUES (CAST(TIME'12:34:56.789012' AS
TIMESTAMP_LTZ)) t(x)
+-- !query analysis
+Project [cast(x#x as time(6)) AS x#x]
++- SubqueryAlias t
+ +- LocalRelation [x#x]
+
+
+-- !query
+SELECT CAST(x AS TIME(9)) FROM VALUES (CAST(TIME'12:34:56.789012345' AS
TIMESTAMP_LTZ(9))) t(x)
+-- !query analysis
+Project [cast(x#x as time(9)) AS x#x]
++- SubqueryAlias t
+ +- LocalRelation [x#x]
+
+
+-- !query
+SELECT time'12:34:56' UNION ALL SELECT timestamp'2020-05-17 12:34:56'
+-- !query analysis
+org.apache.spark.sql.catalyst.ExtendedAnalysisException
+{
+ "errorClass" : "INCOMPATIBLE_COLUMN_TYPE",
+ "sqlState" : "42825",
+ "messageParameters" : {
+ "columnOrdinalNumber" : "first",
+ "dataType1" : "\"TIMESTAMP\"",
+ "dataType2" : "\"TIME(6)\"",
+ "hint" : "",
+ "operator" : "UNION",
+ "tableOrdinalNumber" : "second"
+ },
+ "queryContext" : [ {
+ "objectType" : "",
+ "objectName" : "",
+ "startIndex" : 1,
+ "stopIndex" : 69,
+ "fragment" : "SELECT time'12:34:56' UNION ALL SELECT timestamp'2020-05-17
12:34:56'"
+ } ]
+}
+
+
+-- !query
+SELECT coalesce(time'12:34:56', timestamp'2020-05-17 12:34:56')
+-- !query analysis
+org.apache.spark.sql.catalyst.ExtendedAnalysisException
+{
+ "errorClass" : "DATATYPE_MISMATCH.DATA_DIFF_TYPES",
+ "sqlState" : "42K09",
+ "messageParameters" : {
+ "dataType" : "(\"TIME(6)\" or \"TIMESTAMP\")",
+ "functionName" : "`coalesce`",
+ "sqlExpr" : "\"coalesce(TIME '12:34:56', TIMESTAMP '2020-05-17
12:34:56')\""
+ },
+ "queryContext" : [ {
+ "objectType" : "",
+ "objectName" : "",
+ "startIndex" : 8,
+ "stopIndex" : 63,
+ "fragment" : "coalesce(time'12:34:56', timestamp'2020-05-17 12:34:56')"
+ } ]
+}
+
+
+-- !query
+SELECT coalesce(time'12:34:56', timestamp_ltz'2020-05-17
12:34:56.789012345'::timestamp_ltz(9))
+-- !query analysis
+org.apache.spark.sql.catalyst.ExtendedAnalysisException
+{
+ "errorClass" : "DATATYPE_MISMATCH.DATA_DIFF_TYPES",
+ "sqlState" : "42K09",
+ "messageParameters" : {
+ "dataType" : "(\"TIME(6)\" or \"TIMESTAMP_LTZ(9)\")",
+ "functionName" : "`coalesce`",
+ "sqlExpr" : "\"coalesce(TIME '12:34:56', CAST(TIMESTAMP_LTZ '2020-05-17
12:34:56.789012345' AS TIMESTAMP_LTZ(9)))\""
+ },
+ "queryContext" : [ {
+ "objectType" : "",
+ "objectName" : "",
+ "startIndex" : 8,
+ "stopIndex" : 95,
+ "fragment" : "coalesce(time'12:34:56', timestamp_ltz'2020-05-17
12:34:56.789012345'::timestamp_ltz(9))"
+ } ]
+}
+
+
+-- !query
+SELECT CASE WHEN true THEN time'12:34:56' ELSE timestamp'2020-05-17 12:34:56'
END
+-- !query analysis
+org.apache.spark.sql.catalyst.ExtendedAnalysisException
+{
+ "errorClass" : "DATATYPE_MISMATCH.DATA_DIFF_TYPES",
+ "sqlState" : "42K09",
+ "messageParameters" : {
+ "dataType" : "[\"TIME(6)\", \"TIMESTAMP\"]",
+ "functionName" : "`casewhen`",
+ "sqlExpr" : "\"CASE WHEN true THEN TIME '12:34:56' ELSE TIMESTAMP
'2020-05-17 12:34:56' END\""
+ },
+ "queryContext" : [ {
+ "objectType" : "",
+ "objectName" : "",
+ "startIndex" : 8,
+ "stopIndex" : 81,
+ "fragment" : "CASE WHEN true THEN time'12:34:56' ELSE timestamp'2020-05-17
12:34:56' END"
+ } ]
+}
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 eba639340d26..b68104d9f03a 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
@@ -1839,3 +1839,250 @@ org.apache.spark.sql.catalyst.ExtendedAnalysisException
"fragment" : "CASE WHEN true THEN time'12:34:56' ELSE
timestamp_ntz'2020-05-17 12:34:56' END"
} ]
}
+
+
+-- !query
+SELECT CAST(timestamp'2020-05-17 12:34:56.789012' AS TIME(6))
+-- !query analysis
+[Analyzer test output redacted due to nondeterminism]
+
+
+-- !query
+SELECT CAST(timestamp'2020-05-17 12:34:56.789012' AS TIME(3))
+-- !query analysis
+[Analyzer test output redacted due to nondeterminism]
+
+
+-- !query
+SELECT CAST(timestamp'2020-05-17 12:34:56.789012' AS TIME(0))
+-- !query analysis
+[Analyzer test output redacted due to nondeterminism]
+
+
+-- !query
+SELECT CAST(timestamp_ltz'2020-05-17 12:34:56.789012345'::timestamp_ltz(9) AS
TIME(9))
+-- !query analysis
+Project [cast(cast(2020-05-17 12:34:56.789012345 as timestamp_ltz(9)) as
time(9)) AS CAST(CAST(TIMESTAMP_LTZ '2020-05-17 12:34:56.789012345' AS
TIMESTAMP_LTZ(9)) AS TIME(9))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT CAST(timestamp_ltz'2020-05-17 12:34:56.789012345'::timestamp_ltz(9) AS
TIME(7))
+-- !query analysis
+Project [cast(cast(2020-05-17 12:34:56.789012345 as timestamp_ltz(9)) as
time(7)) AS CAST(CAST(TIMESTAMP_LTZ '2020-05-17 12:34:56.789012345' AS
TIMESTAMP_LTZ(9)) AS TIME(7))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT CAST(timestamp_ltz'2020-05-17 12:34:56.789012345'::timestamp_ltz(9) AS
TIME(6))
+-- !query analysis
+Project [cast(cast(2020-05-17 12:34:56.789012345 as timestamp_ltz(9)) as
time(6)) AS CAST(CAST(TIMESTAMP_LTZ '2020-05-17 12:34:56.789012345' AS
TIMESTAMP_LTZ(9)) AS TIME(6))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT CAST(timestamp_ltz'2020-05-17 12:34:56.789012345'::timestamp_ltz(7) AS
TIME(9))
+-- !query analysis
+Project [cast(cast(2020-05-17 12:34:56.789012345 as timestamp_ltz(7)) as
time(9)) AS CAST(CAST(TIMESTAMP_LTZ '2020-05-17 12:34:56.789012345' AS
TIMESTAMP_LTZ(7)) AS TIME(9))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT timestamp'2020-05-17 12:34:56.789012' :: TIME(6)
+-- !query analysis
+[Analyzer test output redacted due to nondeterminism]
+
+
+-- !query
+SELECT typeof(CAST(TIME'12:34:56' AS TIMESTAMP_LTZ))
+-- !query analysis
+Project [typeof(cast(12:34:56 as timestamp)) AS typeof(CAST(TIME '12:34:56' AS
TIMESTAMP))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT typeof(CAST(TIME'12:34:56' AS TIMESTAMP_LTZ(9)))
+-- !query analysis
+Project [typeof(cast(12:34:56 as timestamp_ltz(9))) AS typeof(CAST(TIME
'12:34:56' AS TIMESTAMP_LTZ(9)))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT CAST(CAST(TIME'12:34:56' AS TIMESTAMP_LTZ) AS DATE) = current_date()
+-- !query analysis
+[Analyzer test output redacted due to nondeterminism]
+
+
+-- !query
+SELECT CAST(CAST(TIME'12:34:56.789012345' AS TIMESTAMP_LTZ(9)) AS DATE) =
current_date()
+-- !query analysis
+[Analyzer test output redacted due to nondeterminism]
+
+
+-- !query
+SELECT CAST(CAST(TIME'12:34:56.789012' AS TIMESTAMP_LTZ) AS TIME(6))
+-- !query analysis
+Project [cast(cast(12:34:56.789012 as timestamp) as time(6)) AS CAST(CAST(TIME
'12:34:56.789012' AS TIMESTAMP) AS TIME(6))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT CAST(CAST(TIME'12:34:56.789012345' AS TIMESTAMP_LTZ(9)) AS TIME(9))
+-- !query analysis
+Project [cast(cast(12:34:56.789012345 as timestamp_ltz(9)) as time(9)) AS
CAST(CAST(TIME '12:34:56.789012345' AS TIMESTAMP_LTZ(9)) AS TIME(9))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT CAST(CAST(TIME'12:34:56.789012345' AS TIMESTAMP_LTZ(7)) AS TIME(9))
+-- !query analysis
+Project [cast(cast(12:34:56.789012345 as timestamp_ltz(7)) as time(9)) AS
CAST(CAST(TIME '12:34:56.789012345' AS TIMESTAMP_LTZ(7)) AS TIME(9))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT CAST(CAST(TIME'12:34:56.789012345'::time(9) AS TIMESTAMP_LTZ(6)) AS
TIME(9))
+-- !query analysis
+Project [cast(cast(cast(12:34:56.789012345 as time(9)) as timestamp) as
time(9)) AS CAST(CAST(CAST(TIME '12:34:56.789012345' AS TIME(9)) AS TIMESTAMP)
AS TIME(9))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT CAST(CAST(NULL AS TIME) AS TIMESTAMP_LTZ)
+-- !query analysis
+Project [cast(cast(null as time(6)) as timestamp) AS CAST(CAST(NULL AS
TIME(6)) AS TIMESTAMP)#x]
++- OneRowRelation
+
+
+-- !query
+SELECT CAST(CAST(NULL AS TIME) AS TIMESTAMP_LTZ(9))
+-- !query analysis
+Project [cast(cast(null as time(6)) as timestamp_ltz(9)) AS CAST(CAST(NULL AS
TIME(6)) AS TIMESTAMP_LTZ(9))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT CAST(CAST(NULL AS TIMESTAMP_LTZ) AS TIME(6))
+-- !query analysis
+Project [cast(cast(null as timestamp) as time(6)) AS CAST(CAST(NULL AS
TIMESTAMP) AS TIME(6))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT CAST(CAST(NULL AS TIMESTAMP_LTZ(9)) AS TIME(6))
+-- !query analysis
+Project [cast(cast(null as timestamp_ltz(9)) as time(6)) AS CAST(CAST(NULL AS
TIMESTAMP_LTZ(9)) AS TIME(6))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT CAST(x AS DATE) = current_date() FROM VALUES (CAST(TIME'12:34:56' AS
TIMESTAMP_LTZ)) t(x)
+-- !query analysis
+[Analyzer test output redacted due to nondeterminism]
+
+
+-- !query
+SELECT CAST(x AS TIME(6)) FROM VALUES (CAST(TIME'12:34:56.789012' AS
TIMESTAMP_LTZ)) t(x)
+-- !query analysis
+Project [cast(x#x as time(6)) AS x#x]
++- SubqueryAlias t
+ +- LocalRelation [x#x]
+
+
+-- !query
+SELECT CAST(x AS TIME(9)) FROM VALUES (CAST(TIME'12:34:56.789012345' AS
TIMESTAMP_LTZ(9))) t(x)
+-- !query analysis
+Project [cast(x#x as time(9)) AS x#x]
++- SubqueryAlias t
+ +- LocalRelation [x#x]
+
+
+-- !query
+SELECT time'12:34:56' UNION ALL SELECT timestamp'2020-05-17 12:34:56'
+-- !query analysis
+org.apache.spark.sql.catalyst.ExtendedAnalysisException
+{
+ "errorClass" : "INCOMPATIBLE_COLUMN_TYPE",
+ "sqlState" : "42825",
+ "messageParameters" : {
+ "columnOrdinalNumber" : "first",
+ "dataType1" : "\"TIMESTAMP\"",
+ "dataType2" : "\"TIME(6)\"",
+ "hint" : "",
+ "operator" : "UNION",
+ "tableOrdinalNumber" : "second"
+ },
+ "queryContext" : [ {
+ "objectType" : "",
+ "objectName" : "",
+ "startIndex" : 1,
+ "stopIndex" : 69,
+ "fragment" : "SELECT time'12:34:56' UNION ALL SELECT timestamp'2020-05-17
12:34:56'"
+ } ]
+}
+
+
+-- !query
+SELECT coalesce(time'12:34:56', timestamp'2020-05-17 12:34:56')
+-- !query analysis
+org.apache.spark.sql.catalyst.ExtendedAnalysisException
+{
+ "errorClass" : "DATATYPE_MISMATCH.DATA_DIFF_TYPES",
+ "sqlState" : "42K09",
+ "messageParameters" : {
+ "dataType" : "(\"TIME(6)\" or \"TIMESTAMP\")",
+ "functionName" : "`coalesce`",
+ "sqlExpr" : "\"coalesce(TIME '12:34:56', TIMESTAMP '2020-05-17
12:34:56')\""
+ },
+ "queryContext" : [ {
+ "objectType" : "",
+ "objectName" : "",
+ "startIndex" : 8,
+ "stopIndex" : 63,
+ "fragment" : "coalesce(time'12:34:56', timestamp'2020-05-17 12:34:56')"
+ } ]
+}
+
+
+-- !query
+SELECT coalesce(time'12:34:56', timestamp_ltz'2020-05-17
12:34:56.789012345'::timestamp_ltz(9))
+-- !query analysis
+org.apache.spark.sql.catalyst.ExtendedAnalysisException
+{
+ "errorClass" : "DATATYPE_MISMATCH.DATA_DIFF_TYPES",
+ "sqlState" : "42K09",
+ "messageParameters" : {
+ "dataType" : "(\"TIME(6)\" or \"TIMESTAMP_LTZ(9)\")",
+ "functionName" : "`coalesce`",
+ "sqlExpr" : "\"coalesce(TIME '12:34:56', CAST(TIMESTAMP_LTZ '2020-05-17
12:34:56.789012345' AS TIMESTAMP_LTZ(9)))\""
+ },
+ "queryContext" : [ {
+ "objectType" : "",
+ "objectName" : "",
+ "startIndex" : 8,
+ "stopIndex" : 95,
+ "fragment" : "coalesce(time'12:34:56', timestamp_ltz'2020-05-17
12:34:56.789012345'::timestamp_ltz(9))"
+ } ]
+}
+
+
+-- !query
+SELECT CASE WHEN true THEN time'12:34:56' ELSE timestamp'2020-05-17 12:34:56'
END
+-- !query analysis
+org.apache.spark.sql.catalyst.ExtendedAnalysisException
+{
+ "errorClass" : "DATATYPE_MISMATCH.DATA_DIFF_TYPES",
+ "sqlState" : "42K09",
+ "messageParameters" : {
+ "dataType" : "[\"TIME(6)\", \"TIMESTAMP\"]",
+ "functionName" : "`casewhen`",
+ "sqlExpr" : "\"CASE WHEN true THEN TIME '12:34:56' ELSE TIMESTAMP
'2020-05-17 12:34:56' END\""
+ },
+ "queryContext" : [ {
+ "objectType" : "",
+ "objectName" : "",
+ "startIndex" : 8,
+ "stopIndex" : 81,
+ "fragment" : "CASE WHEN true THEN time'12:34:56' ELSE timestamp'2020-05-17
12:34:56' END"
+ } ]
+}
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 e32dc528d1fd..2932658aa561 100644
--- a/sql/core/src/test/resources/sql-tests/inputs/cast.sql
+++ b/sql/core/src/test/resources/sql-tests/inputs/cast.sql
@@ -374,3 +374,52 @@ SELECT time'12:34:56' UNION ALL SELECT
timestamp_ntz'2020-05-17 12:34:56';
SELECT coalesce(time'12:34:56', timestamp_ntz'2020-05-17 12:34:56');
SELECT coalesce(time'12:34:56', timestamp_ntz'2020-05-17
12:34:56.789012345'::timestamp_ntz(9));
SELECT CASE WHEN true THEN time'12:34:56' ELSE timestamp_ntz'2020-05-17
12:34:56' END;
+
+-- SPARK-57660: cast TIMESTAMP_LTZ(q) to TIME(p) extracts the local wall-clock
time-of-day in the
+-- session time zone and truncates to p. The LTZ literal is parsed in the
session zone and the
+-- time-of-day is read back in the same zone, so these values stay
zone-independent.
+SELECT CAST(timestamp'2020-05-17 12:34:56.789012' AS TIME(6));
+SELECT CAST(timestamp'2020-05-17 12:34:56.789012' AS TIME(3));
+SELECT CAST(timestamp'2020-05-17 12:34:56.789012' AS TIME(0));
+-- Nanosecond TIMESTAMP_LTZ(q) preserves the sub-microsecond digits up to p.
+SELECT CAST(timestamp_ltz'2020-05-17 12:34:56.789012345'::timestamp_ltz(9) AS
TIME(9));
+SELECT CAST(timestamp_ltz'2020-05-17 12:34:56.789012345'::timestamp_ltz(9) AS
TIME(7));
+SELECT CAST(timestamp_ltz'2020-05-17 12:34:56.789012345'::timestamp_ltz(9) AS
TIME(6));
+SELECT CAST(timestamp_ltz'2020-05-17 12:34:56.789012345'::timestamp_ltz(7) AS
TIME(9));
+-- Double colon syntax.
+SELECT timestamp'2020-05-17 12:34:56.789012' :: TIME(6);
+
+-- SPARK-57660: cast TIME(p) to TIMESTAMP_LTZ(q) takes the date from
CURRENT_DATE and interprets the
+-- result in the session time zone. SQL-layer coverage stays deterministic:
type resolution, the date
+-- anchor equals current_date(), and the value round-trips back to TIME. The
current-date
+-- stabilization is covered by ComputeCurrentTimeSuite and the value semantics
by CastSuite* tests.
+SELECT typeof(CAST(TIME'12:34:56' AS TIMESTAMP_LTZ));
+SELECT typeof(CAST(TIME'12:34:56' AS TIMESTAMP_LTZ(9)));
+-- The date fields come from the query current date.
+SELECT CAST(CAST(TIME'12:34:56' AS TIMESTAMP_LTZ) AS DATE) = current_date();
+SELECT CAST(CAST(TIME'12:34:56.789012345' AS TIMESTAMP_LTZ(9)) AS DATE) =
current_date();
+-- Round-trips re-extract the original time-of-day (truncated to the
intermediate precision).
+SELECT CAST(CAST(TIME'12:34:56.789012' AS TIMESTAMP_LTZ) AS TIME(6));
+SELECT CAST(CAST(TIME'12:34:56.789012345' AS TIMESTAMP_LTZ(9)) AS TIME(9));
+SELECT CAST(CAST(TIME'12:34:56.789012345' AS TIMESTAMP_LTZ(7)) AS TIME(9));
+SELECT CAST(CAST(TIME'12:34:56.789012345'::time(9) AS TIMESTAMP_LTZ(6)) AS
TIME(9));
+-- Null propagation in both directions.
+SELECT CAST(CAST(NULL AS TIME) AS TIMESTAMP_LTZ);
+SELECT CAST(CAST(NULL AS TIME) AS TIMESTAMP_LTZ(9));
+SELECT CAST(CAST(NULL AS TIMESTAMP_LTZ) AS TIME(6));
+SELECT CAST(CAST(NULL AS TIMESTAMP_LTZ(9)) AS TIME(6));
+
+-- SPARK-57660: inside an inline table the TIME -> TIMESTAMP_LTZ cast is
foldable and carries no
+-- CURRENT_LIKE pattern, so it is early-evaluated before ComputeCurrentTime.
Coverage stays
+-- deterministic: the date anchor still equals current_date() and the value
round-trips back to TIME.
+SELECT CAST(x AS DATE) = current_date() FROM VALUES (CAST(TIME'12:34:56' AS
TIMESTAMP_LTZ)) t(x);
+SELECT CAST(x AS TIME(6)) FROM VALUES (CAST(TIME'12:34:56.789012' AS
TIMESTAMP_LTZ)) t(x);
+SELECT CAST(x AS TIME(9)) FROM VALUES (CAST(TIME'12:34:56.789012345' AS
TIMESTAMP_LTZ(9))) t(x);
+
+-- SPARK-57660: TIME and TIMESTAMP_LTZ have no common type, so implicit
coercion is rejected; an
+-- explicit CAST (as above) is required. This guards `findWiderDateTimeType`
against ever widening
+-- TIME, which would silently inject CURRENT_DATE into UNION / coalesce / CASE.
+SELECT time'12:34:56' UNION ALL SELECT timestamp'2020-05-17 12:34:56';
+SELECT coalesce(time'12:34:56', timestamp'2020-05-17 12:34:56');
+SELECT coalesce(time'12:34:56', timestamp_ltz'2020-05-17
12:34:56.789012345'::timestamp_ltz(9));
+SELECT CASE WHEN true THEN time'12:34:56' ELSE timestamp'2020-05-17 12:34:56'
END;
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 223504de7397..31d2b5fdcd02 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
@@ -3162,3 +3162,286 @@ org.apache.spark.sql.catalyst.ExtendedAnalysisException
"fragment" : "CASE WHEN true THEN time'12:34:56' ELSE
timestamp_ntz'2020-05-17 12:34:56' END"
} ]
}
+
+
+-- !query
+SELECT CAST(timestamp'2020-05-17 12:34:56.789012' AS TIME(6))
+-- !query schema
+struct<CAST(TIMESTAMP '2020-05-17 12:34:56.789012' AS TIME(6)):time(6)>
+-- !query output
+12:34:56.789012
+
+
+-- !query
+SELECT CAST(timestamp'2020-05-17 12:34:56.789012' AS TIME(3))
+-- !query schema
+struct<CAST(TIMESTAMP '2020-05-17 12:34:56.789012' AS TIME(3)):time(3)>
+-- !query output
+12:34:56.789
+
+
+-- !query
+SELECT CAST(timestamp'2020-05-17 12:34:56.789012' AS TIME(0))
+-- !query schema
+struct<CAST(TIMESTAMP '2020-05-17 12:34:56.789012' AS TIME(0)):time(0)>
+-- !query output
+12:34:56
+
+
+-- !query
+SELECT CAST(timestamp_ltz'2020-05-17 12:34:56.789012345'::timestamp_ltz(9) AS
TIME(9))
+-- !query schema
+struct<CAST(CAST(TIMESTAMP_LTZ '2020-05-17 12:34:56.789012345' AS
TIMESTAMP_LTZ(9)) AS TIME(9)):time(9)>
+-- !query output
+12:34:56.789012345
+
+
+-- !query
+SELECT CAST(timestamp_ltz'2020-05-17 12:34:56.789012345'::timestamp_ltz(9) AS
TIME(7))
+-- !query schema
+struct<CAST(CAST(TIMESTAMP_LTZ '2020-05-17 12:34:56.789012345' AS
TIMESTAMP_LTZ(9)) AS TIME(7)):time(7)>
+-- !query output
+12:34:56.7890123
+
+
+-- !query
+SELECT CAST(timestamp_ltz'2020-05-17 12:34:56.789012345'::timestamp_ltz(9) AS
TIME(6))
+-- !query schema
+struct<CAST(CAST(TIMESTAMP_LTZ '2020-05-17 12:34:56.789012345' AS
TIMESTAMP_LTZ(9)) AS TIME(6)):time(6)>
+-- !query output
+12:34:56.789012
+
+
+-- !query
+SELECT CAST(timestamp_ltz'2020-05-17 12:34:56.789012345'::timestamp_ltz(7) AS
TIME(9))
+-- !query schema
+struct<CAST(CAST(TIMESTAMP_LTZ '2020-05-17 12:34:56.789012345' AS
TIMESTAMP_LTZ(7)) AS TIME(9)):time(9)>
+-- !query output
+12:34:56.7890123
+
+
+-- !query
+SELECT timestamp'2020-05-17 12:34:56.789012' :: TIME(6)
+-- !query schema
+struct<CAST(TIMESTAMP '2020-05-17 12:34:56.789012' AS TIME(6)):time(6)>
+-- !query output
+12:34:56.789012
+
+
+-- !query
+SELECT typeof(CAST(TIME'12:34:56' AS TIMESTAMP_LTZ))
+-- !query schema
+struct<typeof(CAST(TIME '12:34:56' AS TIMESTAMP)):string>
+-- !query output
+timestamp
+
+
+-- !query
+SELECT typeof(CAST(TIME'12:34:56' AS TIMESTAMP_LTZ(9)))
+-- !query schema
+struct<typeof(CAST(TIME '12:34:56' AS TIMESTAMP_LTZ(9))):string>
+-- !query output
+timestamp_ltz(9)
+
+
+-- !query
+SELECT CAST(CAST(TIME'12:34:56' AS TIMESTAMP_LTZ) AS DATE) = current_date()
+-- !query schema
+struct<(CAST(CAST(TIME '12:34:56' AS TIMESTAMP) AS DATE) =
current_date()):boolean>
+-- !query output
+true
+
+
+-- !query
+SELECT CAST(CAST(TIME'12:34:56.789012345' AS TIMESTAMP_LTZ(9)) AS DATE) =
current_date()
+-- !query schema
+struct<(CAST(CAST(TIME '12:34:56.789012345' AS TIMESTAMP_LTZ(9)) AS DATE) =
current_date()):boolean>
+-- !query output
+true
+
+
+-- !query
+SELECT CAST(CAST(TIME'12:34:56.789012' AS TIMESTAMP_LTZ) AS TIME(6))
+-- !query schema
+struct<CAST(CAST(TIME '12:34:56.789012' AS TIMESTAMP) AS TIME(6)):time(6)>
+-- !query output
+12:34:56.789012
+
+
+-- !query
+SELECT CAST(CAST(TIME'12:34:56.789012345' AS TIMESTAMP_LTZ(9)) AS TIME(9))
+-- !query schema
+struct<CAST(CAST(TIME '12:34:56.789012345' AS TIMESTAMP_LTZ(9)) AS
TIME(9)):time(9)>
+-- !query output
+12:34:56.789012345
+
+
+-- !query
+SELECT CAST(CAST(TIME'12:34:56.789012345' AS TIMESTAMP_LTZ(7)) AS TIME(9))
+-- !query schema
+struct<CAST(CAST(TIME '12:34:56.789012345' AS TIMESTAMP_LTZ(7)) AS
TIME(9)):time(9)>
+-- !query output
+12:34:56.7890123
+
+
+-- !query
+SELECT CAST(CAST(TIME'12:34:56.789012345'::time(9) AS TIMESTAMP_LTZ(6)) AS
TIME(9))
+-- !query schema
+struct<CAST(CAST(CAST(TIME '12:34:56.789012345' AS TIME(9)) AS TIMESTAMP) AS
TIME(9)):time(9)>
+-- !query output
+12:34:56.789012
+
+
+-- !query
+SELECT CAST(CAST(NULL AS TIME) AS TIMESTAMP_LTZ)
+-- !query schema
+struct<CAST(CAST(NULL AS TIME(6)) AS TIMESTAMP):timestamp>
+-- !query output
+NULL
+
+
+-- !query
+SELECT CAST(CAST(NULL AS TIME) AS TIMESTAMP_LTZ(9))
+-- !query schema
+struct<CAST(CAST(NULL AS TIME(6)) AS TIMESTAMP_LTZ(9)):timestamp_ltz(9)>
+-- !query output
+NULL
+
+
+-- !query
+SELECT CAST(CAST(NULL AS TIMESTAMP_LTZ) AS TIME(6))
+-- !query schema
+struct<CAST(CAST(NULL AS TIMESTAMP) AS TIME(6)):time(6)>
+-- !query output
+NULL
+
+
+-- !query
+SELECT CAST(CAST(NULL AS TIMESTAMP_LTZ(9)) AS TIME(6))
+-- !query schema
+struct<CAST(CAST(NULL AS TIMESTAMP_LTZ(9)) AS TIME(6)):time(6)>
+-- !query output
+NULL
+
+
+-- !query
+SELECT CAST(x AS DATE) = current_date() FROM VALUES (CAST(TIME'12:34:56' AS
TIMESTAMP_LTZ)) t(x)
+-- !query schema
+struct<(CAST(x AS DATE) = current_date()):boolean>
+-- !query output
+true
+
+
+-- !query
+SELECT CAST(x AS TIME(6)) FROM VALUES (CAST(TIME'12:34:56.789012' AS
TIMESTAMP_LTZ)) t(x)
+-- !query schema
+struct<x:time(6)>
+-- !query output
+12:34:56.789012
+
+
+-- !query
+SELECT CAST(x AS TIME(9)) FROM VALUES (CAST(TIME'12:34:56.789012345' AS
TIMESTAMP_LTZ(9))) t(x)
+-- !query schema
+struct<x:time(9)>
+-- !query output
+12:34:56.789012345
+
+
+-- !query
+SELECT time'12:34:56' UNION ALL SELECT timestamp'2020-05-17 12:34:56'
+-- !query schema
+struct<>
+-- !query output
+org.apache.spark.sql.catalyst.ExtendedAnalysisException
+{
+ "errorClass" : "INCOMPATIBLE_COLUMN_TYPE",
+ "sqlState" : "42825",
+ "messageParameters" : {
+ "columnOrdinalNumber" : "first",
+ "dataType1" : "\"TIMESTAMP\"",
+ "dataType2" : "\"TIME(6)\"",
+ "hint" : "",
+ "operator" : "UNION",
+ "tableOrdinalNumber" : "second"
+ },
+ "queryContext" : [ {
+ "objectType" : "",
+ "objectName" : "",
+ "startIndex" : 1,
+ "stopIndex" : 69,
+ "fragment" : "SELECT time'12:34:56' UNION ALL SELECT timestamp'2020-05-17
12:34:56'"
+ } ]
+}
+
+
+-- !query
+SELECT coalesce(time'12:34:56', timestamp'2020-05-17 12:34:56')
+-- !query schema
+struct<>
+-- !query output
+org.apache.spark.sql.catalyst.ExtendedAnalysisException
+{
+ "errorClass" : "DATATYPE_MISMATCH.DATA_DIFF_TYPES",
+ "sqlState" : "42K09",
+ "messageParameters" : {
+ "dataType" : "(\"TIME(6)\" or \"TIMESTAMP\")",
+ "functionName" : "`coalesce`",
+ "sqlExpr" : "\"coalesce(TIME '12:34:56', TIMESTAMP '2020-05-17
12:34:56')\""
+ },
+ "queryContext" : [ {
+ "objectType" : "",
+ "objectName" : "",
+ "startIndex" : 8,
+ "stopIndex" : 63,
+ "fragment" : "coalesce(time'12:34:56', timestamp'2020-05-17 12:34:56')"
+ } ]
+}
+
+
+-- !query
+SELECT coalesce(time'12:34:56', timestamp_ltz'2020-05-17
12:34:56.789012345'::timestamp_ltz(9))
+-- !query schema
+struct<>
+-- !query output
+org.apache.spark.sql.catalyst.ExtendedAnalysisException
+{
+ "errorClass" : "DATATYPE_MISMATCH.DATA_DIFF_TYPES",
+ "sqlState" : "42K09",
+ "messageParameters" : {
+ "dataType" : "(\"TIME(6)\" or \"TIMESTAMP_LTZ(9)\")",
+ "functionName" : "`coalesce`",
+ "sqlExpr" : "\"coalesce(TIME '12:34:56', CAST(TIMESTAMP_LTZ '2020-05-17
12:34:56.789012345' AS TIMESTAMP_LTZ(9)))\""
+ },
+ "queryContext" : [ {
+ "objectType" : "",
+ "objectName" : "",
+ "startIndex" : 8,
+ "stopIndex" : 95,
+ "fragment" : "coalesce(time'12:34:56', timestamp_ltz'2020-05-17
12:34:56.789012345'::timestamp_ltz(9))"
+ } ]
+}
+
+
+-- !query
+SELECT CASE WHEN true THEN time'12:34:56' ELSE timestamp'2020-05-17 12:34:56'
END
+-- !query schema
+struct<>
+-- !query output
+org.apache.spark.sql.catalyst.ExtendedAnalysisException
+{
+ "errorClass" : "DATATYPE_MISMATCH.DATA_DIFF_TYPES",
+ "sqlState" : "42K09",
+ "messageParameters" : {
+ "dataType" : "[\"TIME(6)\", \"TIMESTAMP\"]",
+ "functionName" : "`casewhen`",
+ "sqlExpr" : "\"CASE WHEN true THEN TIME '12:34:56' ELSE TIMESTAMP
'2020-05-17 12:34:56' END\""
+ },
+ "queryContext" : [ {
+ "objectType" : "",
+ "objectName" : "",
+ "startIndex" : 8,
+ "stopIndex" : 81,
+ "fragment" : "CASE WHEN true THEN time'12:34:56' ELSE timestamp'2020-05-17
12:34:56' END"
+ } ]
+}
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 18ce49c59bbf..3b7477beab01 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
@@ -2162,3 +2162,286 @@ org.apache.spark.sql.catalyst.ExtendedAnalysisException
"fragment" : "CASE WHEN true THEN time'12:34:56' ELSE
timestamp_ntz'2020-05-17 12:34:56' END"
} ]
}
+
+
+-- !query
+SELECT CAST(timestamp'2020-05-17 12:34:56.789012' AS TIME(6))
+-- !query schema
+struct<CAST(TIMESTAMP '2020-05-17 12:34:56.789012' AS TIME(6)):time(6)>
+-- !query output
+12:34:56.789012
+
+
+-- !query
+SELECT CAST(timestamp'2020-05-17 12:34:56.789012' AS TIME(3))
+-- !query schema
+struct<CAST(TIMESTAMP '2020-05-17 12:34:56.789012' AS TIME(3)):time(3)>
+-- !query output
+12:34:56.789
+
+
+-- !query
+SELECT CAST(timestamp'2020-05-17 12:34:56.789012' AS TIME(0))
+-- !query schema
+struct<CAST(TIMESTAMP '2020-05-17 12:34:56.789012' AS TIME(0)):time(0)>
+-- !query output
+12:34:56
+
+
+-- !query
+SELECT CAST(timestamp_ltz'2020-05-17 12:34:56.789012345'::timestamp_ltz(9) AS
TIME(9))
+-- !query schema
+struct<CAST(CAST(TIMESTAMP_LTZ '2020-05-17 12:34:56.789012345' AS
TIMESTAMP_LTZ(9)) AS TIME(9)):time(9)>
+-- !query output
+12:34:56.789012345
+
+
+-- !query
+SELECT CAST(timestamp_ltz'2020-05-17 12:34:56.789012345'::timestamp_ltz(9) AS
TIME(7))
+-- !query schema
+struct<CAST(CAST(TIMESTAMP_LTZ '2020-05-17 12:34:56.789012345' AS
TIMESTAMP_LTZ(9)) AS TIME(7)):time(7)>
+-- !query output
+12:34:56.7890123
+
+
+-- !query
+SELECT CAST(timestamp_ltz'2020-05-17 12:34:56.789012345'::timestamp_ltz(9) AS
TIME(6))
+-- !query schema
+struct<CAST(CAST(TIMESTAMP_LTZ '2020-05-17 12:34:56.789012345' AS
TIMESTAMP_LTZ(9)) AS TIME(6)):time(6)>
+-- !query output
+12:34:56.789012
+
+
+-- !query
+SELECT CAST(timestamp_ltz'2020-05-17 12:34:56.789012345'::timestamp_ltz(7) AS
TIME(9))
+-- !query schema
+struct<CAST(CAST(TIMESTAMP_LTZ '2020-05-17 12:34:56.789012345' AS
TIMESTAMP_LTZ(7)) AS TIME(9)):time(9)>
+-- !query output
+12:34:56.7890123
+
+
+-- !query
+SELECT timestamp'2020-05-17 12:34:56.789012' :: TIME(6)
+-- !query schema
+struct<CAST(TIMESTAMP '2020-05-17 12:34:56.789012' AS TIME(6)):time(6)>
+-- !query output
+12:34:56.789012
+
+
+-- !query
+SELECT typeof(CAST(TIME'12:34:56' AS TIMESTAMP_LTZ))
+-- !query schema
+struct<typeof(CAST(TIME '12:34:56' AS TIMESTAMP)):string>
+-- !query output
+timestamp
+
+
+-- !query
+SELECT typeof(CAST(TIME'12:34:56' AS TIMESTAMP_LTZ(9)))
+-- !query schema
+struct<typeof(CAST(TIME '12:34:56' AS TIMESTAMP_LTZ(9))):string>
+-- !query output
+timestamp_ltz(9)
+
+
+-- !query
+SELECT CAST(CAST(TIME'12:34:56' AS TIMESTAMP_LTZ) AS DATE) = current_date()
+-- !query schema
+struct<(CAST(CAST(TIME '12:34:56' AS TIMESTAMP) AS DATE) =
current_date()):boolean>
+-- !query output
+true
+
+
+-- !query
+SELECT CAST(CAST(TIME'12:34:56.789012345' AS TIMESTAMP_LTZ(9)) AS DATE) =
current_date()
+-- !query schema
+struct<(CAST(CAST(TIME '12:34:56.789012345' AS TIMESTAMP_LTZ(9)) AS DATE) =
current_date()):boolean>
+-- !query output
+true
+
+
+-- !query
+SELECT CAST(CAST(TIME'12:34:56.789012' AS TIMESTAMP_LTZ) AS TIME(6))
+-- !query schema
+struct<CAST(CAST(TIME '12:34:56.789012' AS TIMESTAMP) AS TIME(6)):time(6)>
+-- !query output
+12:34:56.789012
+
+
+-- !query
+SELECT CAST(CAST(TIME'12:34:56.789012345' AS TIMESTAMP_LTZ(9)) AS TIME(9))
+-- !query schema
+struct<CAST(CAST(TIME '12:34:56.789012345' AS TIMESTAMP_LTZ(9)) AS
TIME(9)):time(9)>
+-- !query output
+12:34:56.789012345
+
+
+-- !query
+SELECT CAST(CAST(TIME'12:34:56.789012345' AS TIMESTAMP_LTZ(7)) AS TIME(9))
+-- !query schema
+struct<CAST(CAST(TIME '12:34:56.789012345' AS TIMESTAMP_LTZ(7)) AS
TIME(9)):time(9)>
+-- !query output
+12:34:56.7890123
+
+
+-- !query
+SELECT CAST(CAST(TIME'12:34:56.789012345'::time(9) AS TIMESTAMP_LTZ(6)) AS
TIME(9))
+-- !query schema
+struct<CAST(CAST(CAST(TIME '12:34:56.789012345' AS TIME(9)) AS TIMESTAMP) AS
TIME(9)):time(9)>
+-- !query output
+12:34:56.789012
+
+
+-- !query
+SELECT CAST(CAST(NULL AS TIME) AS TIMESTAMP_LTZ)
+-- !query schema
+struct<CAST(CAST(NULL AS TIME(6)) AS TIMESTAMP):timestamp>
+-- !query output
+NULL
+
+
+-- !query
+SELECT CAST(CAST(NULL AS TIME) AS TIMESTAMP_LTZ(9))
+-- !query schema
+struct<CAST(CAST(NULL AS TIME(6)) AS TIMESTAMP_LTZ(9)):timestamp_ltz(9)>
+-- !query output
+NULL
+
+
+-- !query
+SELECT CAST(CAST(NULL AS TIMESTAMP_LTZ) AS TIME(6))
+-- !query schema
+struct<CAST(CAST(NULL AS TIMESTAMP) AS TIME(6)):time(6)>
+-- !query output
+NULL
+
+
+-- !query
+SELECT CAST(CAST(NULL AS TIMESTAMP_LTZ(9)) AS TIME(6))
+-- !query schema
+struct<CAST(CAST(NULL AS TIMESTAMP_LTZ(9)) AS TIME(6)):time(6)>
+-- !query output
+NULL
+
+
+-- !query
+SELECT CAST(x AS DATE) = current_date() FROM VALUES (CAST(TIME'12:34:56' AS
TIMESTAMP_LTZ)) t(x)
+-- !query schema
+struct<(CAST(x AS DATE) = current_date()):boolean>
+-- !query output
+true
+
+
+-- !query
+SELECT CAST(x AS TIME(6)) FROM VALUES (CAST(TIME'12:34:56.789012' AS
TIMESTAMP_LTZ)) t(x)
+-- !query schema
+struct<x:time(6)>
+-- !query output
+12:34:56.789012
+
+
+-- !query
+SELECT CAST(x AS TIME(9)) FROM VALUES (CAST(TIME'12:34:56.789012345' AS
TIMESTAMP_LTZ(9))) t(x)
+-- !query schema
+struct<x:time(9)>
+-- !query output
+12:34:56.789012345
+
+
+-- !query
+SELECT time'12:34:56' UNION ALL SELECT timestamp'2020-05-17 12:34:56'
+-- !query schema
+struct<>
+-- !query output
+org.apache.spark.sql.catalyst.ExtendedAnalysisException
+{
+ "errorClass" : "INCOMPATIBLE_COLUMN_TYPE",
+ "sqlState" : "42825",
+ "messageParameters" : {
+ "columnOrdinalNumber" : "first",
+ "dataType1" : "\"TIMESTAMP\"",
+ "dataType2" : "\"TIME(6)\"",
+ "hint" : "",
+ "operator" : "UNION",
+ "tableOrdinalNumber" : "second"
+ },
+ "queryContext" : [ {
+ "objectType" : "",
+ "objectName" : "",
+ "startIndex" : 1,
+ "stopIndex" : 69,
+ "fragment" : "SELECT time'12:34:56' UNION ALL SELECT timestamp'2020-05-17
12:34:56'"
+ } ]
+}
+
+
+-- !query
+SELECT coalesce(time'12:34:56', timestamp'2020-05-17 12:34:56')
+-- !query schema
+struct<>
+-- !query output
+org.apache.spark.sql.catalyst.ExtendedAnalysisException
+{
+ "errorClass" : "DATATYPE_MISMATCH.DATA_DIFF_TYPES",
+ "sqlState" : "42K09",
+ "messageParameters" : {
+ "dataType" : "(\"TIME(6)\" or \"TIMESTAMP\")",
+ "functionName" : "`coalesce`",
+ "sqlExpr" : "\"coalesce(TIME '12:34:56', TIMESTAMP '2020-05-17
12:34:56')\""
+ },
+ "queryContext" : [ {
+ "objectType" : "",
+ "objectName" : "",
+ "startIndex" : 8,
+ "stopIndex" : 63,
+ "fragment" : "coalesce(time'12:34:56', timestamp'2020-05-17 12:34:56')"
+ } ]
+}
+
+
+-- !query
+SELECT coalesce(time'12:34:56', timestamp_ltz'2020-05-17
12:34:56.789012345'::timestamp_ltz(9))
+-- !query schema
+struct<>
+-- !query output
+org.apache.spark.sql.catalyst.ExtendedAnalysisException
+{
+ "errorClass" : "DATATYPE_MISMATCH.DATA_DIFF_TYPES",
+ "sqlState" : "42K09",
+ "messageParameters" : {
+ "dataType" : "(\"TIME(6)\" or \"TIMESTAMP_LTZ(9)\")",
+ "functionName" : "`coalesce`",
+ "sqlExpr" : "\"coalesce(TIME '12:34:56', CAST(TIMESTAMP_LTZ '2020-05-17
12:34:56.789012345' AS TIMESTAMP_LTZ(9)))\""
+ },
+ "queryContext" : [ {
+ "objectType" : "",
+ "objectName" : "",
+ "startIndex" : 8,
+ "stopIndex" : 95,
+ "fragment" : "coalesce(time'12:34:56', timestamp_ltz'2020-05-17
12:34:56.789012345'::timestamp_ltz(9))"
+ } ]
+}
+
+
+-- !query
+SELECT CASE WHEN true THEN time'12:34:56' ELSE timestamp'2020-05-17 12:34:56'
END
+-- !query schema
+struct<>
+-- !query output
+org.apache.spark.sql.catalyst.ExtendedAnalysisException
+{
+ "errorClass" : "DATATYPE_MISMATCH.DATA_DIFF_TYPES",
+ "sqlState" : "42K09",
+ "messageParameters" : {
+ "dataType" : "[\"TIME(6)\", \"TIMESTAMP\"]",
+ "functionName" : "`casewhen`",
+ "sqlExpr" : "\"CASE WHEN true THEN TIME '12:34:56' ELSE TIMESTAMP
'2020-05-17 12:34:56' END\""
+ },
+ "queryContext" : [ {
+ "objectType" : "",
+ "objectName" : "",
+ "startIndex" : 8,
+ "stopIndex" : 81,
+ "fragment" : "CASE WHEN true THEN time'12:34:56' ELSE timestamp'2020-05-17
12:34:56' END"
+ } ]
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]