You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@spark.apache.org by we...@apache.org on 2026/08/19 17:59:47 UTC

(spark) branch master updated: [SPARK-57818][SQL] Support nanosecond-precision NTZ timestamps in convert_timezone

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

cloud-fan 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 31f5f5abb8d7 [SPARK-57818][SQL] Support nanosecond-precision NTZ timestamps in convert_timezone
31f5f5abb8d7 is described below

commit 31f5f5abb8d7ba5b75338df6e9810ee8677d3ff1
Author: Rakesh Raushan <ra...@gmail.com>
AuthorDate: Thu Aug 20 01:59:31 2026 +0800

    [SPARK-57818][SQL] Support nanosecond-precision NTZ timestamps in convert_timezone
    
    ### What changes were proposed in this pull request?
    Extends ConvertTimezone (convert_timezone()) to accept nanosecond-precision timestamps (TIMESTAMP_NTZ(p), p in [7, 9]) as input, alongside the existing microsecond TimestampType inputs. ConvertTimezone would returns a nanosecond NTZ value at the same precision with the remainder preserved in case of TIMESTAMP_NTZ(p). Existing microsecond computation won't be affected by this change.
    
    ### Why are the changes needed?
    ConvertTimezone types its source as TimestampNTZType only and returns TimestampNTZType, using convertTimestampNtzToAnotherTz(Long, ...). It rejects all nanosecond timestamp types. We should allow TIMESTAMP_NTZ(p).
    
    ### Does this PR introduce _any_ user-facing change?
    Yes, convert_timezone function would accept TIMESTAMP_NTZ(p) and return NTZ value at same precision with remainder preserved.
    
    ### How was this patch tested?
    Added unit tests in DateExpressionsSuite.scala and also generated golden files.
    
    ### Was this patch authored or co-authored using generative AI tooling?
    Used claude code for writing UTs and generating golden files.
    
    Closes #57008 from iRakson/SPARK-57818.
    
    Authored-by: Rakesh Raushan <ra...@gmail.com>
    Signed-off-by: Wenchen Fan <we...@databricks.com>
---
 .../catalyst/expressions/datetimeExpressions.scala | 58 +++++++++++--
 .../expressions/DateExpressionsSuite.scala         | 98 ++++++++++++++++++++++
 .../analyzer-results/timestamp-ltz-nanos.sql.out   | 25 ++++++
 .../analyzer-results/timestamp-ntz-nanos.sql.out   | 23 +++++
 .../sql-tests/inputs/timestamp-ltz-nanos.sql       |  6 ++
 .../sql-tests/inputs/timestamp-ntz-nanos.sql       | 10 +++
 .../sql-tests/results/timestamp-ltz-nanos.sql.out  | 27 ++++++
 .../sql-tests/results/timestamp-ntz-nanos.sql.out  | 26 ++++++
 8 files changed, 266 insertions(+), 7 deletions(-)

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 ed895f909d3b..fea6a813c292 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
@@ -4894,20 +4894,64 @@ case class ConvertTimezone(
     Seq(
       StringTypeWithCollation(supportsTrimCollation = true),
       StringTypeWithCollation(supportsTrimCollation = true),
-      TimestampNTZType)
-  override def dataType: DataType = TimestampNTZType
-
-  override def nullSafeEval(srcTz: Any, tgtTz: Any, micros: Any): Any = {
-    DateTimeUtils.convertTimestampNtzToAnotherTz(
+      TypeCollection(TimestampNTZType, AnyTimestampNanoType))
+
+  // sourceTs's requiredType, as actually enforced by this method: TypeCollection includes
+  // AnyTimestampNanoType (rather than an NTZ-only nanos type) only so that an LTZ(p) input is
+  // accepted here and rejected below with the friendly message, instead of being silently
+  // widened to TimestampNTZType by the generic datetime-to-datetime implicit cast rule. That
+  // makes AnyTimestampNanoType.simpleString (which lists timestamp_ltz(p)) leak into the
+  // generic super.checkInputDataTypes() mismatch message for this param when sourceTs is some
+  // unrelated type (e.g. an int), even though LTZ(p) is never actually accepted. Route both the
+  // generic mismatch and the explicit LTZ rejection through the same message so they agree.
+  private def wrongSourceTsType: DataTypeMismatch = DataTypeMismatch(
+    errorSubClass = "UNEXPECTED_INPUT_TYPE",
+    messageParameters = Map(
+      "paramIndex" -> ordinalNumber(2),
+      "requiredType" -> toSQLType("(timestamp_ntz or timestamp_ntz(p) with p in [7, 9])"),
+      "inputSql" -> toSQLExpr(sourceTs),
+      "inputType" -> toSQLType(sourceTs.dataType)))
+
+  override def checkInputDataTypes(): TypeCheckResult = super.checkInputDataTypes() match {
+    case TypeCheckSuccess if sourceTs.dataType.isInstanceOf[TimestampLTZNanosType] =>
+      wrongSourceTsType
+    case DataTypeMismatch("UNEXPECTED_INPUT_TYPE", params)
+        if params.get("paramIndex").contains(ordinalNumber(2)) =>
+      wrongSourceTsType
+    case result => result
+  }
+
+  private def isTsNanos: Boolean = sourceTs.dataType.isInstanceOf[AnyTimestampNanoType]
+
+  // Preserves the exact source precision (7/8/9); AnyTimestampNanoType.defaultConcreteType would
+  // always widen the result to precision 9.
+  override def dataType: DataType = if (isTsNanos) sourceTs.dataType else TimestampNTZType
+
+  override def nullSafeEval(srcTz: Any, tgtTz: Any, ts: Any): Any = {
+    val micros = if (isTsNanos) ts.asInstanceOf[TimestampNanosVal].epochMicros else ts
+    val convertedTs = DateTimeUtils.convertTimestampNtzToAnotherTz(
       srcTz.asInstanceOf[UTF8String].toString,
       tgtTz.asInstanceOf[UTF8String].toString,
       micros.asInstanceOf[Long])
+    if (isTsNanos) {
+      TimestampNanosVal.fromParts(
+        convertedTs, ts.asInstanceOf[TimestampNanosVal].nanosWithinMicro)
+    } else convertedTs
   }
 
   override def doGenCode(ctx: CodegenContext, ev: ExprCode): ExprCode = {
     val dtu = DateTimeUtils.getClass.getName.stripSuffix("$")
-    defineCodeGen(ctx, ev, (srcTz, tgtTz, micros) =>
-      s"""$dtu.convertTimestampNtzToAnotherTz($srcTz.toString(), $tgtTz.toString(), $micros)""")
+    if (isTsNanos) {
+      defineCodeGen(ctx, ev, (srcTz, tgtTz, ts) => {
+        val convertedMicros = s"$dtu.convertTimestampNtzToAnotherTz(" +
+          s"$srcTz.toString(), $tgtTz.toString(), $ts.epochMicros)"
+        s"org.apache.spark.unsafe.types.TimestampNanosVal.fromParts(" +
+          s"$convertedMicros, $ts.nanosWithinMicro)"
+      })
+    } else {
+      defineCodeGen(ctx, ev, (srcTz, tgtTz, micros) =>
+        s"""$dtu.convertTimestampNtzToAnotherTz($srcTz.toString(), $tgtTz.toString(), $micros)""")
+    }
   }
 
   override def prettyName: String = "convert_timezone"
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 691263e5b77d..ef90fa5d68a5 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
@@ -2533,6 +2533,104 @@ class DateExpressionsSuite extends SparkFunSuite with ExpressionEvalHelper {
     }
   }
 
+  test("SPARK-57818: convert_timezone over nanosecond-precision timestamps") {
+    val ntzType9 = TimestampNTZNanosType(9)
+
+    // The nanosWithinMicro remainder is carried through unchanged by a zone conversion; only the
+    // whole-microsecond part shifts with the zone offset.
+    val srcNanos = DateTimeUtils.localDateTimeToTimestampNanos(
+      LocalDateTime.parse("2022-03-27T03:00:00.123456789"), 9)
+    val expectedNanos = DateTimeUtils.localDateTimeToTimestampNanos(
+      LocalDateTime.parse("2022-03-27T04:00:00.123456789"), 9)
+    assert(srcNanos.nanosWithinMicro == expectedNanos.nanosWithinMicro)
+    checkEvaluation(
+      ConvertTimezone(
+        Literal("Europe/Brussels"),
+        Literal("Europe/Moscow"),
+        Literal.create(srcNanos, ntzType9)),
+      expectedNanos)
+
+    // Pre-epoch values exercise the negative-epoch path. The expected epochMicros is derived from
+    // the already-verified micros-only conversion (SPARK-37552 tests that path); this only checks
+    // that the nanos wiring delegates to it correctly and carries the remainder through unchanged.
+    val preEpochSrc = DateTimeUtils.localDateTimeToTimestampNanos(
+      LocalDateTime.parse("1960-01-01T00:00:00.000000001"), 9)
+    val preEpochExpectedMicros = DateTimeUtils.convertTimestampNtzToAnotherTz(
+      "Europe/Moscow", "Europe/Brussels", preEpochSrc.epochMicros)
+    checkEvaluation(
+      ConvertTimezone(
+        Literal("Europe/Moscow"),
+        Literal("Europe/Brussels"),
+        Literal.create(preEpochSrc, ntzType9)),
+      TimestampNanosVal.fromParts(preEpochExpectedMicros, preEpochSrc.nanosWithinMicro))
+
+    // Precision (7/8/9) is preserved on the result; AnyTimestampNanoType.defaultConcreteType
+    // would incorrectly always widen it to 9.
+    Seq(7, 8, 9).foreach { precision =>
+      val ntzType = TimestampNTZNanosType(precision)
+      val src = DateTimeUtils.localDateTimeToTimestampNanos(
+        LocalDateTime.parse("2022-03-27T03:00:00.123456789"), precision)
+      val convertExpr = ConvertTimezone(
+        Literal("Europe/Brussels"), Literal("Europe/Moscow"), Literal.create(src, ntzType))
+      assert(convertExpr.dataType === ntzType)
+    }
+
+    // LTZ(p) nanos values are rejected: this function is NTZ-only, matching the existing
+    // TimestampNTZType-only micro path. Unlike that micro path -- which implicitly casts a
+    // plain LTZ TimestampType argument down to TimestampNTZType -- a nanos source must not be
+    // silently reinterpreted from LTZ to NTZ, since that would drop the source time zone
+    // information without the user asking for it.
+    val ltzNanos = DateTimeUtils.instantToTimestampNanos(Instant.parse("2022-03-27T03:00:00Z"), 9)
+    val ltzMismatch = ConvertTimezone(
+      Literal("Europe/Brussels"), Literal("Europe/Moscow"),
+      Literal.create(ltzNanos, TimestampLTZNanosType(9)))
+      .checkInputDataTypes().asInstanceOf[DataTypeMismatch]
+    assert(ltzMismatch.errorSubClass == "UNEXPECTED_INPUT_TYPE")
+
+    // A wholly invalid source type (not any kind of timestamp) hits the generic type check
+    // instead of the explicit LTZ guard above; both paths must report the same requiredType,
+    // since neither actually accepts an LTZ(p) source.
+    val wrongTypeMismatch = ConvertTimezone(
+      Literal("Europe/Brussels"), Literal("Europe/Moscow"), Literal(1))
+      .checkInputDataTypes().asInstanceOf[DataTypeMismatch]
+    assert(wrongTypeMismatch.errorSubClass == "UNEXPECTED_INPUT_TYPE")
+    assert(wrongTypeMismatch.messageParameters("requiredType") ===
+      ltzMismatch.messageParameters("requiredType"))
+
+    // NULL handling: a NULL nanosecond timestamp, and NULL zone arguments with a non-NULL
+    // nanosecond timestamp.
+    checkEvaluation(
+      ConvertTimezone(
+        Literal("America/Los_Angeles"),
+        Literal("UTC"),
+        Literal.create(null, ntzType9)),
+      null)
+    checkEvaluation(
+      ConvertTimezone(
+        Literal.create(null, StringType),
+        Literal("UTC"),
+        Literal.create(srcNanos, ntzType9)),
+      null)
+    checkEvaluation(
+      ConvertTimezone(
+        Literal("America/Los_Angeles"),
+        Literal.create(null, StringType),
+        Literal.create(srcNanos, ntzType9)),
+      null)
+
+    outstandingTimezonesIds.foreach { sourceTz =>
+      outstandingTimezonesIds.foreach { targetTz =>
+        checkConsistencyBetweenInterpretedAndCodegen(
+          (_: Expression, _: Expression, sourceTs: Expression) =>
+            ConvertTimezone(
+              Literal(sourceTz),
+              Literal(targetTz),
+              sourceTs),
+          StringType, StringType, ntzType9)
+      }
+    }
+  }
+
   test("SPARK-38195: add a quantity of interval units to a timestamp") {
     // Check case-insensitivity
     checkEvaluation(
diff --git a/sql/core/src/test/resources/sql-tests/analyzer-results/timestamp-ltz-nanos.sql.out b/sql/core/src/test/resources/sql-tests/analyzer-results/timestamp-ltz-nanos.sql.out
index 6c028a1c3127..368ddf703b92 100644
--- a/sql/core/src/test/resources/sql-tests/analyzer-results/timestamp-ltz-nanos.sql.out
+++ b/sql/core/src/test/resources/sql-tests/analyzer-results/timestamp-ltz-nanos.sql.out
@@ -716,6 +716,31 @@ Project [(2020-01-01 19:04:05.123456789 - cast(null as timestamp_ltz(9))) AS (TI
 +- OneRowRelation
 
 
+-- !query
+SELECT convert_timezone('Europe/Brussels', 'Europe/Moscow',
+    '2022-03-27 03:00:00.123456789 UTC' :: timestamp_ltz(9))
+-- !query analysis
+org.apache.spark.sql.catalyst.ExtendedAnalysisException
+{
+  "errorClass" : "DATATYPE_MISMATCH.UNEXPECTED_INPUT_TYPE",
+  "sqlState" : "42K09",
+  "messageParameters" : {
+    "inputSql" : "\"CAST(2022-03-27 03:00:00.123456789 UTC AS TIMESTAMP_LTZ(9))\"",
+    "inputType" : "\"TIMESTAMP_LTZ(9)\"",
+    "paramIndex" : "third",
+    "requiredType" : "\"(TIMESTAMP_NTZ OR TIMESTAMP_NTZ(P) WITH P IN [7, 9])\"",
+    "sqlExpr" : "\"convert_timezone(Europe/Brussels, Europe/Moscow, CAST(2022-03-27 03:00:00.123456789 UTC AS TIMESTAMP_LTZ(9)))\""
+  },
+  "queryContext" : [ {
+    "objectType" : "",
+    "objectName" : "",
+    "startIndex" : 8,
+    "stopIndex" : 120,
+    "fragment" : "convert_timezone('Europe/Brussels', 'Europe/Moscow',\n    '2022-03-27 03:00:00.123456789 UTC' :: timestamp_ltz(9))"
+  } ]
+}
+
+
 -- !query
 SELECT max(c), min(c) FROM VALUES
   (TIMESTAMP_LTZ '2020-01-01 00:00:00.000000001 UTC'),
diff --git a/sql/core/src/test/resources/sql-tests/analyzer-results/timestamp-ntz-nanos.sql.out b/sql/core/src/test/resources/sql-tests/analyzer-results/timestamp-ntz-nanos.sql.out
index c64be7281b47..8fa51bf5ff5c 100644
--- a/sql/core/src/test/resources/sql-tests/analyzer-results/timestamp-ntz-nanos.sql.out
+++ b/sql/core/src/test/resources/sql-tests/analyzer-results/timestamp-ntz-nanos.sql.out
@@ -644,6 +644,29 @@ Project [(2020-01-02 03:04:05.123456789 - cast(null as timestamp_ntz(9))) AS (TI
 +- OneRowRelation
 
 
+-- !query
+SELECT convert_timezone('Europe/Brussels', 'Europe/Moscow',
+    TIMESTAMP_NTZ '2022-03-27 03:00:00.123456789')
+-- !query analysis
+Project [convert_timezone(Europe/Brussels, Europe/Moscow, 2022-03-27 03:00:00.123456789) AS convert_timezone(Europe/Brussels, Europe/Moscow, TIMESTAMP_NTZ '2022-03-27 03:00:00.123456789')#x]
++- OneRowRelation
+
+
+-- !query
+SELECT typeof(convert_timezone('Europe/Brussels', 'Europe/Moscow',
+    '2022-03-27 03:00:00.1234567' :: timestamp_ntz(7)))
+-- !query analysis
+Project [typeof(convert_timezone(Europe/Brussels, Europe/Moscow, cast(2022-03-27 03:00:00.1234567 as timestamp_ntz(7)))) AS typeof(convert_timezone(Europe/Brussels, Europe/Moscow, CAST(2022-03-27 03:00:00.1234567 AS TIMESTAMP_NTZ(7))))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT convert_timezone('America/Los_Angeles', 'UTC', CAST(NULL AS timestamp_ntz(9)))
+-- !query analysis
+Project [convert_timezone(America/Los_Angeles, UTC, cast(null as timestamp_ntz(9))) AS convert_timezone(America/Los_Angeles, UTC, CAST(NULL AS TIMESTAMP_NTZ(9)))#x]
++- OneRowRelation
+
+
 -- !query
 SELECT max(c), min(c) FROM VALUES
   (TIMESTAMP_NTZ '2020-01-01 00:00:00.000000001'),
diff --git a/sql/core/src/test/resources/sql-tests/inputs/timestamp-ltz-nanos.sql b/sql/core/src/test/resources/sql-tests/inputs/timestamp-ltz-nanos.sql
index 0f18303867ba..1eba70d4f4b2 100644
--- a/sql/core/src/test/resources/sql-tests/inputs/timestamp-ltz-nanos.sql
+++ b/sql/core/src/test/resources/sql-tests/inputs/timestamp-ltz-nanos.sql
@@ -205,6 +205,12 @@ SELECT TIMESTAMP_LTZ '2020-01-01 00:00:00.123456789 UTC' - TIMESTAMP_LTZ '1960-0
 -- NULL operand propagates.
 SELECT TIMESTAMP_LTZ '2020-01-02 03:04:05.123456789 UTC' - CAST(NULL AS timestamp_ltz(9));
 
+-- SPARK-57818: convert_timezone is NTZ-only, so a nanosecond LTZ(p) source is rejected rather
+-- than silently reinterpreted as NTZ (the positive TIMESTAMP_NTZ(p) path is covered in
+-- timestamp-ntz-nanos.sql).
+SELECT convert_timezone('Europe/Brussels', 'Europe/Moscow',
+    '2022-03-27 03:00:00.123456789 UTC' :: timestamp_ltz(9));
+
 -- SPARK-57103: MAX / MIN over nanosecond-precision TIMESTAMP_LTZ. The aggregate preserves the
 -- nanosecond type and orders by the sub-microsecond remainder; NULLs are ignored. Values are
 -- rendered in the session time zone (America/Los_Angeles).
diff --git a/sql/core/src/test/resources/sql-tests/inputs/timestamp-ntz-nanos.sql b/sql/core/src/test/resources/sql-tests/inputs/timestamp-ntz-nanos.sql
index 478356098d8c..266829fe7ab2 100644
--- a/sql/core/src/test/resources/sql-tests/inputs/timestamp-ntz-nanos.sql
+++ b/sql/core/src/test/resources/sql-tests/inputs/timestamp-ntz-nanos.sql
@@ -178,6 +178,16 @@ SELECT TIMESTAMP_NTZ '2020-01-01 00:00:00.123456789' - TIMESTAMP_NTZ '1960-01-01
 -- NULL operand propagates.
 SELECT TIMESTAMP_NTZ '2020-01-02 03:04:05.123456789' - CAST(NULL AS timestamp_ntz(9));
 
+-- SPARK-57818: convert_timezone over nanosecond-precision TIMESTAMP_NTZ. The sub-microsecond
+-- remainder is carried through unchanged; only the whole-microsecond part shifts with the zone
+-- offset, and the result keeps the source's exact precision.
+SELECT convert_timezone('Europe/Brussels', 'Europe/Moscow',
+    TIMESTAMP_NTZ '2022-03-27 03:00:00.123456789');
+SELECT typeof(convert_timezone('Europe/Brussels', 'Europe/Moscow',
+    '2022-03-27 03:00:00.1234567' :: timestamp_ntz(7)));
+-- NULL nanosecond timestamp.
+SELECT convert_timezone('America/Los_Angeles', 'UTC', CAST(NULL AS timestamp_ntz(9)));
+
 -- SPARK-57103: MAX / MIN over nanosecond-precision TIMESTAMP_NTZ. The aggregate preserves the
 -- nanosecond type and orders by the sub-microsecond remainder (two values share the same
 -- microsecond and differ only within it); NULLs are ignored.
diff --git a/sql/core/src/test/resources/sql-tests/results/timestamp-ltz-nanos.sql.out b/sql/core/src/test/resources/sql-tests/results/timestamp-ltz-nanos.sql.out
index 2d1ae31273c1..5fae7cc5041e 100644
--- a/sql/core/src/test/resources/sql-tests/results/timestamp-ltz-nanos.sql.out
+++ b/sql/core/src/test/resources/sql-tests/results/timestamp-ltz-nanos.sql.out
@@ -808,6 +808,33 @@ struct<(TIMESTAMP_LTZ '2020-01-01 19:04:05.123456789' - CAST(NULL AS TIMESTAMP_L
 NULL
 
 
+-- !query
+SELECT convert_timezone('Europe/Brussels', 'Europe/Moscow',
+    '2022-03-27 03:00:00.123456789 UTC' :: timestamp_ltz(9))
+-- !query schema
+struct<>
+-- !query output
+org.apache.spark.sql.catalyst.ExtendedAnalysisException
+{
+  "errorClass" : "DATATYPE_MISMATCH.UNEXPECTED_INPUT_TYPE",
+  "sqlState" : "42K09",
+  "messageParameters" : {
+    "inputSql" : "\"CAST(2022-03-27 03:00:00.123456789 UTC AS TIMESTAMP_LTZ(9))\"",
+    "inputType" : "\"TIMESTAMP_LTZ(9)\"",
+    "paramIndex" : "third",
+    "requiredType" : "\"(TIMESTAMP_NTZ OR TIMESTAMP_NTZ(P) WITH P IN [7, 9])\"",
+    "sqlExpr" : "\"convert_timezone(Europe/Brussels, Europe/Moscow, CAST(2022-03-27 03:00:00.123456789 UTC AS TIMESTAMP_LTZ(9)))\""
+  },
+  "queryContext" : [ {
+    "objectType" : "",
+    "objectName" : "",
+    "startIndex" : 8,
+    "stopIndex" : 120,
+    "fragment" : "convert_timezone('Europe/Brussels', 'Europe/Moscow',\n    '2022-03-27 03:00:00.123456789 UTC' :: timestamp_ltz(9))"
+  } ]
+}
+
+
 -- !query
 SELECT max(c), min(c) FROM VALUES
   (TIMESTAMP_LTZ '2020-01-01 00:00:00.000000001 UTC'),
diff --git a/sql/core/src/test/resources/sql-tests/results/timestamp-ntz-nanos.sql.out b/sql/core/src/test/resources/sql-tests/results/timestamp-ntz-nanos.sql.out
index 709a64321fec..46fb523e378a 100644
--- a/sql/core/src/test/resources/sql-tests/results/timestamp-ntz-nanos.sql.out
+++ b/sql/core/src/test/resources/sql-tests/results/timestamp-ntz-nanos.sql.out
@@ -726,6 +726,32 @@ struct<(TIMESTAMP_NTZ '2020-01-02 03:04:05.123456789' - CAST(NULL AS TIMESTAMP_N
 NULL
 
 
+-- !query
+SELECT convert_timezone('Europe/Brussels', 'Europe/Moscow',
+    TIMESTAMP_NTZ '2022-03-27 03:00:00.123456789')
+-- !query schema
+struct<convert_timezone(Europe/Brussels, Europe/Moscow, TIMESTAMP_NTZ '2022-03-27 03:00:00.123456789'):timestamp_ntz(9)>
+-- !query output
+2022-03-27 04:00:00.123456789
+
+
+-- !query
+SELECT typeof(convert_timezone('Europe/Brussels', 'Europe/Moscow',
+    '2022-03-27 03:00:00.1234567' :: timestamp_ntz(7)))
+-- !query schema
+struct<typeof(convert_timezone(Europe/Brussels, Europe/Moscow, CAST(2022-03-27 03:00:00.1234567 AS TIMESTAMP_NTZ(7)))):string>
+-- !query output
+timestamp_ntz(7)
+
+
+-- !query
+SELECT convert_timezone('America/Los_Angeles', 'UTC', CAST(NULL AS timestamp_ntz(9)))
+-- !query schema
+struct<convert_timezone(America/Los_Angeles, UTC, CAST(NULL AS TIMESTAMP_NTZ(9))):timestamp_ntz(9)>
+-- !query output
+NULL
+
+
 -- !query
 SELECT max(c), min(c) FROM VALUES
   (TIMESTAMP_NTZ '2020-01-01 00:00:00.000000001'),


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@spark.apache.org
For additional commands, e-mail: commits-help@spark.apache.org