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 c07d9be47060 [SPARK-57816][SQL] Support nanosecond-precision
timestamps in date_format, to_char and to_varchar
c07d9be47060 is described below
commit c07d9be4706083d46658bbfe2a3dcfb26123af38
Author: Stevo Mitric <[email protected]>
AuthorDate: Fri Jul 17 17:26:31 2026 +0800
[SPARK-57816][SQL] Support nanosecond-precision timestamps in date_format,
to_char and to_varchar
### What changes were proposed in this pull request?
Extend `DateFormatClass` (which backs `date_format`, `to_char` and
`to_varchar`) to accept nanosecond-precision timestamps
(`TIMESTAMP_NTZ(p)`/`TIMESTAMP_LTZ(p)`, p in [7, 9]). Both eval and codegen
render via the nanosecond-aware `TimestampFormatter`: NTZ renders its wall
clock zone-independently, LTZ renders in the session zone, and sub-precision
digits floor to zeros. Also guards `SimplifyDateTimeConversions` from rewriting
a nanosecond child, whose round-trip through microsecond ` [...]
### Why are the changes needed?
Nanosecond input was narrowed to microseconds, so the sub-microsecond
digits never reached the formatter.
### Does this PR introduce _any_ user-facing change?
Yes. `date_format`/`to_char`/`to_varchar` now render nanosecond timestamps
at full precision (preview `spark.sql.timestampNanosTypes.enabled`).
### How was this patch tested?
`DateExpressionsSuite`, `SimplifyDateTimeConversionsSuite`,
`TimestampNanosFunctionsSuiteBase`, and `timestamp-{ltz,ntz}-nanos.sql` golden
files.
### Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Code
Closes #57225 from stevomitric/stevomitric/spark-57816-nanos.
Authored-by: Stevo Mitric <[email protected]>
Signed-off-by: Wenchen Fan <[email protected]>
---
.../catalyst/expressions/datetimeExpressions.scala | 94 ++++++++++++++++-
.../spark/sql/catalyst/optimizer/expressions.scala | 3 +-
.../expressions/DateExpressionsSuite.scala | 64 +++++++++++
.../SimplifyDateTimeConversionsSuite.scala | 27 +++++
.../analyzer-results/timestamp-ltz-nanos.sql.out | 63 +++++++++++
.../analyzer-results/timestamp-ntz-nanos.sql.out | 56 ++++++++++
.../sql-tests/inputs/timestamp-ltz-nanos.sql | 19 ++++
.../sql-tests/inputs/timestamp-ntz-nanos.sql | 15 +++
.../sql-tests/results/timestamp-ltz-nanos.sql.out | 72 +++++++++++++
.../sql-tests/results/timestamp-ntz-nanos.sql.out | 64 +++++++++++
.../sql/TimestampNanosFunctionsSuiteBase.scala | 117 ++++++++++++++++++++-
11 files changed, 591 insertions(+), 3 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 5e7057ae5165..b8d6933541ef 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
@@ -1322,7 +1322,7 @@ case class DateFormatClass(left: Expression, right:
Expression, timeZoneId: Opti
def this(left: Expression, right: Expression) = this(left, right, None)
override def inputTypes: Seq[AbstractDataType] =
- Seq(TypeCollection(TimestampType, AnyTimeType),
+ Seq(TypeCollection(TimestampType, AnyTimeType, AnyTimestampNanoType),
StringTypeWithCollation(supportsTrimCollation = true))
override def withTimeZone(timeZoneId: String): TimeZoneAwareExpression =
@@ -1335,6 +1335,22 @@ case class DateFormatClass(left: Expression, right:
Expression, timeZoneId: Opti
TimeFormatter(format.toString, isParsing = false))
DateFormatClass.formatTimeWithError(
tf, timestamp.asInstanceOf[Long], prettyName, format.toString)
+ case t: TimestampNTZNanosType =>
+ // NTZ is zone-independent: render the UTC-grid wall clock, ignoring
the session zone.
+ val formatter =
formatterOption.getOrElse(getFormatter(format.toString))
+ DateFormatClass.formatNanosWithError(
+ formatter, timestamp.asInstanceOf[TimestampNanosVal], t.precision,
+ isNTZ = true, prettyName, format.toString)
+ case t: TimestampLTZNanosType =>
+ // LTZ renders in the formatter's session zone.
+ val formatter =
formatterOption.getOrElse(getFormatter(format.toString))
+ DateFormatClass.formatNanosWithError(
+ formatter, timestamp.asInstanceOf[TimestampNanosVal], t.precision,
+ isNTZ = false, prettyName, format.toString)
+ case other: AnyTimestampNanoType =>
+ // AnyTimestampNanoType is not sealed; a future subtype would
otherwise fall through to
+ // the micros branch below and fail with a cryptic ClassCastException.
Mirror doGenCode.
+ throw SparkException.internalError(s"Unexpected nanosecond timestamp
type: $other")
case _ =>
val formatter =
formatterOption.getOrElse(getFormatter(format.toString))
UTF8String.fromString(formatter.format(timestamp.asInstanceOf[Long]))
@@ -1373,6 +1389,50 @@ case class DateFormatClass(left: Expression, right:
Expression, timeZoneId: Opti
|$format.toString()))""".stripMargin.replaceAll("\n", "")
})
}
+ case t @ (_: TimestampNTZNanosType | _: TimestampLTZNanosType) =>
+ // Object carrier: `$timestamp` is the boxed `TimestampNanosVal`
reference (no unbox).
+ // NTZ renders zone-independently; LTZ renders in the formatter's
session zone. Both route
+ // through `DateFormatClass.formatNanosWithError`, which maps an
invalid pattern (e.g. a
+ // zone token over the zone-less NTZ value) to
INVALID_PARAMETER_VALUE.PATTERN. Mirrors the
+ // TIME branch's `formatTimeWithError`.
+ val (isNTZ, precision) = t match {
+ case ntz: TimestampNTZNanosType => (true, ntz.precision)
+ case ltz: TimestampLTZNanosType => (false, ltz.precision)
+ // Compiler exhaustiveness only: `t` is statically DataType, so
scalac cannot see that
+ // the outer case restricts it to the two concrete types. A genuine
future subtype is
+ // caught by the outer `case other: AnyTimestampNanoType` guard
before reaching here.
+ case other => throw SparkException.internalError(
+ s"Unexpected nanosecond timestamp type: $other")
+ }
+ val dfc = DateFormatClass.getClass.getName.stripSuffix("$")
+ val funcName = ctx.addReferenceObj("funcName", prettyName)
+ formatterOption.map { tf =>
+ val timestampFormatter = ctx.addReferenceObj("timestampFormatter",
tf)
+ val fmtStr = ctx.addReferenceObj("fmtStr", right.eval().toString)
+ defineCodeGen(ctx, ev, (timestamp, _) => {
+ s"""$dfc.formatNanosWithError(
+ | $timestampFormatter, $timestamp, $precision, $isNTZ,
$funcName, $fmtStr)"""
+ .stripMargin
+ })
+ }.getOrElse {
+ val tf = TimestampFormatter.getClass.getName.stripSuffix("$")
+ val ldf = LegacyDateFormats.getClass.getName.stripSuffix("$")
+ val zid = ctx.addReferenceObj("zoneId", zoneId,
classOf[ZoneId].getName)
+ defineCodeGen(ctx, ev, (timestamp, format) => {
+ s"""|$dfc.formatNanosWithError(
+ | $tf$$.MODULE$$.apply(
+ | $format.toString(),
+ | $zid,
+ | $ldf$$.MODULE$$.SIMPLE_DATE_FORMAT(),
+ | false),
+ | $timestamp, $precision, $isNTZ, $funcName,
$format.toString())""".stripMargin
+ })
+ }
+ case other: AnyTimestampNanoType =>
+ // AnyTimestampNanoType is not sealed; a future subtype would
otherwise fall through to
+ // the micros branch below and emit `format($timestamp)`, treating the
boxed
+ // TimestampNanosVal as a Long. Mirror nullSafeEval's outer guard.
+ throw SparkException.internalError(s"Unexpected nanosecond timestamp
type: $other")
case _ =>
formatterOption.map { tf =>
val timestampFormatter = ctx.addReferenceObj("timestampFormatter",
tf)
@@ -1428,6 +1488,38 @@ object DateFormatClass {
cause = e)
}
}
+
+ /**
+ * Formats a nanosecond-precision timestamp value, mapping an invalid
pattern to a Spark error.
+ * Used by both eval and codegen. NTZ renders zone-independently
(`formatWithoutTimeZoneNanos`);
+ * LTZ renders in the formatter's session zone (`formatNanos`). A pattern
the value cannot satisfy
+ * -- most commonly a zone token (`z`/`Z`/`X`/`O`/`VV`) over the zone-less
NTZ value, which raises
+ * `java.time.DateTimeException` from the underlying `LocalDateTime.format`
-- is surfaced as
+ * INVALID_PARAMETER_VALUE.PATTERN, mirroring [[formatTimeWithError]]
instead of leaking the raw
+ * java.time exception.
+ */
+ def formatNanosWithError(
+ tf: TimestampFormatter,
+ v: TimestampNanosVal,
+ precision: Int,
+ isNTZ: Boolean,
+ funcName: String,
+ pattern: String): UTF8String = {
+ try {
+ val formatted =
+ if (isNTZ) tf.formatWithoutTimeZoneNanos(v, precision) else
tf.formatNanos(v, precision)
+ UTF8String.fromString(formatted)
+ } catch {
+ case e: java.time.DateTimeException =>
+ throw new SparkRuntimeException(
+ errorClass = "INVALID_PARAMETER_VALUE.PATTERN",
+ messageParameters = Map(
+ "parameter" -> toSQLId("format"),
+ "functionName" -> toSQLId(funcName),
+ "value" -> toSQLValue(pattern, StringType)),
+ cause = e)
+ }
+ }
}
/**
diff --git
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/expressions.scala
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/expressions.scala
index de847dddc935..13fa3b09c23c 100644
---
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/expressions.scala
+++
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/expressions.scala
@@ -1217,7 +1217,8 @@ object SimplifyDateTimeConversions extends
Rule[LogicalPlan] {
timeZoneId3)
if pattern.semanticEquals(pattern2) &&
pattern.semanticEquals(pattern3)
&& timeZoneId == timeZoneId2 && timeZoneId == timeZoneId3
- && !child.dataType.isInstanceOf[TimeType] =>
+ && !child.dataType.isInstanceOf[TimeType]
+ && !child.dataType.isInstanceOf[AnyTimestampNanoType] =>
e
// Remove a timestamp to string conversion followed by a string to
timestamp conversions if
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 690256156069..69908974367d 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
@@ -360,6 +360,70 @@ class DateExpressionsSuite extends SparkFunSuite with
ExpressionEvalHelper {
)
}
+ test("SPARK-57816: DateFormat over nanosecond-precision timestamps") {
+ import org.apache.spark.sql.catalyst.util.TimestampNanosTestUtils._
+ val ntz = localDateTimeToNanosVal(timestampNTZ(2020, 1, 1, 13, 24, 35,
123456789))
+ // The 9-`S` pattern is a fixed-width fraction field (date_format uses
isParsing = false), so
+ // it always emits 9 digits; truncating to `p` zeros the low digits rather
than dropping them.
+ Seq(
+ 9 -> "2020-01-01 13:24:35.123456789",
+ 8 -> "2020-01-01 13:24:35.123456780",
+ 7 -> "2020-01-01 13:24:35.123456700").foreach { case (p, expected) =>
+ checkEvaluation(
+ DateFormatClass(Literal.create(ntz, TimestampNTZNanosType(p)),
+ Literal("yyyy-MM-dd HH:mm:ss.SSSSSSSSS"), UTC_OPT),
+ expected)
+ }
+ // LTZ renders in the session zone.
+ val ltz =
instantToNanosVal(Instant.parse("2020-01-01T21:24:35.987654321Z"))
+ checkEvaluation(
+ DateFormatClass(Literal.create(ltz, TimestampLTZNanosType(9)),
+ Literal("yyyy-MM-dd HH:mm:ss.SSSSSSSSS"),
Option("America/Los_Angeles")),
+ "2020-01-01 13:24:35.987654321")
+ // A non-foldable format takes the per-row-formatter path (formatterOption
is None) in both
+ // eval and codegen; checkEvaluation exercises both. The result matches
the foldable case.
+ checkEvaluation(
+ DateFormatClass(Literal.create(ntz, TimestampNTZNanosType(9)),
+ NonFoldableLiteral.create("yyyy-MM-dd HH:mm:ss.SSSSSSSSS",
StringType), UTC_OPT),
+ "2020-01-01 13:24:35.123456789")
+ checkEvaluation(
+ DateFormatClass(Literal.create(ltz, TimestampLTZNanosType(9)),
+ NonFoldableLiteral.create("yyyy-MM-dd HH:mm:ss.SSSSSSSSS", StringType),
+ Option("America/Los_Angeles")),
+ "2020-01-01 13:24:35.987654321")
+ // NULL handling.
+ checkEvaluation(
+ DateFormatClass(Literal.create(null, TimestampNTZNanosType(9)),
+ Literal("yyyy-MM-dd HH:mm:ss.SSSSSSSSS"), UTC_OPT), null)
+
+ // Pre-epoch value combined with sub-precision truncation: epochMicros is
negative but the
+ // sub-micro digits floor toward the precision step independently, so p=7
zeros the low 2
+ // digits of a 1960 timestamp exactly like a post-epoch one.
+ val preEpochNtz = localDateTimeToNanosVal(timestampNTZ(1960, 1, 1, 13, 24,
35, 123456789))
+ Seq(
+ 9 -> "1960-01-01 13:24:35.123456789",
+ 7 -> "1960-01-01 13:24:35.123456700").foreach { case (p, expected) =>
+ checkEvaluation(
+ DateFormatClass(Literal.create(preEpochNtz, TimestampNTZNanosType(p)),
+ Literal("yyyy-MM-dd HH:mm:ss.SSSSSSSSS"), UTC_OPT),
+ expected)
+ }
+
+ // A zone-token pattern (here `z`) cannot render the zone-less NTZ wall
clock: the underlying
+ // LocalDateTime.format raises java.time.DateTimeException, which surfaces
as a clean
+ // INVALID_PARAMETER_VALUE.PATTERN Spark error (mirroring the TIME path)
rather than leaking the
+ // raw java.time exception. checkErrorInExpression exercises both eval and
codegen.
+ checkErrorInExpression[SparkRuntimeException](
+ DateFormatClass(Literal.create(ntz, TimestampNTZNanosType(9)),
+ Literal("yyyy-MM-dd HH:mm:ss z"), UTC_OPT),
+ condition = "INVALID_PARAMETER_VALUE.PATTERN",
+ parameters = Map(
+ "parameter" -> "`format`",
+ "functionName" -> "`date_format`",
+ "value" -> "'yyyy-MM-dd HH:mm:ss z'")
+ )
+ }
+
test("Hour") {
assert(Hour(Literal.create(null, DateType), UTC_OPT).resolved === false)
assert(Hour(Literal(ts), UTC_OPT).resolved)
diff --git
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/optimizer/SimplifyDateTimeConversionsSuite.scala
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/optimizer/SimplifyDateTimeConversionsSuite.scala
index e67d8f2dc7f8..32fa999b65c2 100644
---
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/optimizer/SimplifyDateTimeConversionsSuite.scala
+++
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/optimizer/SimplifyDateTimeConversionsSuite.scala
@@ -91,4 +91,31 @@ class SimplifyDateTimeConversionsSuite extends PlanTest {
// Should NOT be simplified - optimized plan should equal original (no
rewrite)
comparePlans(optimized, originalQuery)
}
+
+ test("SPARK-57816: SimplifyDateTimeConversions skips nanosecond timestamp
child") {
+ val pattern = "yyyy-MM-dd HH:mm:ss.SSSSSSSSS"
+ Seq(TimestampNTZNanosType(9), TimestampLTZNanosType(9)).foreach {
nanosType =>
+ val nanosAttr = AttributeReference("t", nanosType)()
+ val nanosRelation = LocalRelation(nanosAttr)
+
+ val df = DateFormatClass(nanosAttr, pattern)
+
+ // date_format(to_timestamp(date_format(nanos_col, p), p), p) should NOT
simplify because
+ // the round trip through micro TimestampType truncates sub-microsecond
precision.
+ val originalQuery = nanosRelation
+ .select(
+ DateFormatClass(
+ GetTimestamp(
+ df,
+ pattern,
+ TimestampType),
+ pattern) as "c1")
+ .analyze
+
+ val optimized = Optimize.execute(originalQuery)
+
+ // Should NOT be simplified - optimized plan should equal original (no
rewrite)
+ comparePlans(optimized, originalQuery)
+ }
+ }
}
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 454eeddf2f23..3d38acc777e6 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
@@ -1036,6 +1036,69 @@ Sort [v#x ASC NULLS FIRST], true
+- OneRowRelation
+-- !query
+SELECT date_format(TIMESTAMP_LTZ '2020-01-01 13:24:35.123456789', 'yyyy-MM-dd
HH:mm:ss.SSSSSSSSS')
+-- !query analysis
+Project [date_format(2020-01-01 13:24:35.123456789, yyyy-MM-dd
HH:mm:ss.SSSSSSSSS, Some(America/Los_Angeles)) AS date_format(TIMESTAMP_LTZ
'2020-01-01 13:24:35.123456789', yyyy-MM-dd HH:mm:ss.SSSSSSSSS)#x]
++- OneRowRelation
+
+
+-- !query
+SELECT date_format('2020-01-01 13:24:35.123456789' :: timestamp_ltz(8),
'yyyy-MM-dd HH:mm:ss.SSSSSSSSS')
+-- !query analysis
+Project [date_format(cast(2020-01-01 13:24:35.123456789 as timestamp_ltz(8)),
yyyy-MM-dd HH:mm:ss.SSSSSSSSS, Some(America/Los_Angeles)) AS
date_format(CAST(2020-01-01 13:24:35.123456789 AS TIMESTAMP_LTZ(8)), yyyy-MM-dd
HH:mm:ss.SSSSSSSSS)#x]
++- OneRowRelation
+
+
+-- !query
+SELECT date_format('2020-01-01 13:24:35.123456789' :: timestamp_ltz(7),
'yyyy-MM-dd HH:mm:ss.SSSSSSSSS')
+-- !query analysis
+Project [date_format(cast(2020-01-01 13:24:35.123456789 as timestamp_ltz(7)),
yyyy-MM-dd HH:mm:ss.SSSSSSSSS, Some(America/Los_Angeles)) AS
date_format(CAST(2020-01-01 13:24:35.123456789 AS TIMESTAMP_LTZ(7)), yyyy-MM-dd
HH:mm:ss.SSSSSSSSS)#x]
++- OneRowRelation
+
+
+-- !query
+SELECT date_format(TIMESTAMP_LTZ '2020-01-01 21:24:35.123456789 UTC',
'yyyy-MM-dd HH:mm:ss.SSSSSSSSS')
+-- !query analysis
+Project [date_format(2020-01-01 13:24:35.123456789, yyyy-MM-dd
HH:mm:ss.SSSSSSSSS, Some(America/Los_Angeles)) AS date_format(TIMESTAMP_LTZ
'2020-01-01 13:24:35.123456789', yyyy-MM-dd HH:mm:ss.SSSSSSSSS)#x]
++- OneRowRelation
+
+
+-- !query
+SELECT to_char(TIMESTAMP_LTZ '2020-01-01 13:24:35.123456789', 'yyyy-MM-dd
HH:mm:ss.SSSSSSSSS')
+-- !query analysis
+Project [date_format(2020-01-01 13:24:35.123456789, yyyy-MM-dd
HH:mm:ss.SSSSSSSSS, Some(America/Los_Angeles)) AS date_format(TIMESTAMP_LTZ
'2020-01-01 13:24:35.123456789', yyyy-MM-dd HH:mm:ss.SSSSSSSSS)#x]
++- OneRowRelation
+
+
+-- !query
+SELECT to_char('2020-01-01 13:24:35.123456789' :: timestamp_ltz(7),
'yyyy-MM-dd HH:mm:ss.SSSSSSSSS')
+-- !query analysis
+Project [date_format(cast(2020-01-01 13:24:35.123456789 as timestamp_ltz(7)),
yyyy-MM-dd HH:mm:ss.SSSSSSSSS, Some(America/Los_Angeles)) AS
date_format(CAST(2020-01-01 13:24:35.123456789 AS TIMESTAMP_LTZ(7)), yyyy-MM-dd
HH:mm:ss.SSSSSSSSS)#x]
++- OneRowRelation
+
+
+-- !query
+SELECT to_varchar('2020-01-01 13:24:35.123456789' :: timestamp_ltz(8),
'HH:mm:ss.SSSSSSSSS')
+-- !query analysis
+Project [date_format(cast(2020-01-01 13:24:35.123456789 as timestamp_ltz(8)),
HH:mm:ss.SSSSSSSSS, Some(America/Los_Angeles)) AS date_format(CAST(2020-01-01
13:24:35.123456789 AS TIMESTAMP_LTZ(8)), HH:mm:ss.SSSSSSSSS)#x]
++- OneRowRelation
+
+
+-- !query
+SELECT date_format(TIMESTAMP_LTZ '1960-01-01 13:24:35.123456789 UTC',
'yyyy-MM-dd HH:mm:ss.SSSSSSSSS')
+-- !query analysis
+Project [date_format(1960-01-01 05:24:35.123456789, yyyy-MM-dd
HH:mm:ss.SSSSSSSSS, Some(America/Los_Angeles)) AS date_format(TIMESTAMP_LTZ
'1960-01-01 05:24:35.123456789', yyyy-MM-dd HH:mm:ss.SSSSSSSSS)#x]
++- OneRowRelation
+
+
+-- !query
+SELECT date_format(NULL :: timestamp_ltz(9), 'yyyy-MM-dd HH:mm:ss.SSSSSSSSS')
+-- !query analysis
+Project [date_format(cast(null as timestamp_ltz(9)), yyyy-MM-dd
HH:mm:ss.SSSSSSSSS, Some(America/Los_Angeles)) AS date_format(CAST(NULL AS
TIMESTAMP_LTZ(9)), yyyy-MM-dd HH:mm:ss.SSSSSSSSS)#x]
++- OneRowRelation
+
+
-- !query
SELECT date_trunc('SECOND', TIMESTAMP_LTZ '2020-01-01 12:34:56.123456789 UTC')
-- !query analysis
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 879c084a7496..ff49a5870e29 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
@@ -870,6 +870,62 @@ Sort [v#x ASC NULLS FIRST], true
+- OneRowRelation
+-- !query
+SELECT date_format(TIMESTAMP_NTZ '2020-01-01 13:24:35.123456789', 'yyyy-MM-dd
HH:mm:ss.SSSSSSSSS')
+-- !query analysis
+Project [date_format(2020-01-01 13:24:35.123456789, yyyy-MM-dd
HH:mm:ss.SSSSSSSSS, Some(America/Los_Angeles)) AS date_format(TIMESTAMP_NTZ
'2020-01-01 13:24:35.123456789', yyyy-MM-dd HH:mm:ss.SSSSSSSSS)#x]
++- OneRowRelation
+
+
+-- !query
+SELECT date_format('2020-01-01 13:24:35.123456789' :: timestamp_ntz(8),
'yyyy-MM-dd HH:mm:ss.SSSSSSSSS')
+-- !query analysis
+Project [date_format(cast(2020-01-01 13:24:35.123456789 as timestamp_ntz(8)),
yyyy-MM-dd HH:mm:ss.SSSSSSSSS, Some(America/Los_Angeles)) AS
date_format(CAST(2020-01-01 13:24:35.123456789 AS TIMESTAMP_NTZ(8)), yyyy-MM-dd
HH:mm:ss.SSSSSSSSS)#x]
++- OneRowRelation
+
+
+-- !query
+SELECT date_format('2020-01-01 13:24:35.123456789' :: timestamp_ntz(7),
'yyyy-MM-dd HH:mm:ss.SSSSSSSSS')
+-- !query analysis
+Project [date_format(cast(2020-01-01 13:24:35.123456789 as timestamp_ntz(7)),
yyyy-MM-dd HH:mm:ss.SSSSSSSSS, Some(America/Los_Angeles)) AS
date_format(CAST(2020-01-01 13:24:35.123456789 AS TIMESTAMP_NTZ(7)), yyyy-MM-dd
HH:mm:ss.SSSSSSSSS)#x]
++- OneRowRelation
+
+
+-- !query
+SELECT to_char(TIMESTAMP_NTZ '2020-01-01 13:24:35.123456789', 'yyyy-MM-dd
HH:mm:ss.SSSSSSSSS')
+-- !query analysis
+Project [date_format(2020-01-01 13:24:35.123456789, yyyy-MM-dd
HH:mm:ss.SSSSSSSSS, Some(America/Los_Angeles)) AS date_format(TIMESTAMP_NTZ
'2020-01-01 13:24:35.123456789', yyyy-MM-dd HH:mm:ss.SSSSSSSSS)#x]
++- OneRowRelation
+
+
+-- !query
+SELECT to_char('2020-01-01 13:24:35.123456789' :: timestamp_ntz(7),
'yyyy-MM-dd HH:mm:ss.SSSSSSSSS')
+-- !query analysis
+Project [date_format(cast(2020-01-01 13:24:35.123456789 as timestamp_ntz(7)),
yyyy-MM-dd HH:mm:ss.SSSSSSSSS, Some(America/Los_Angeles)) AS
date_format(CAST(2020-01-01 13:24:35.123456789 AS TIMESTAMP_NTZ(7)), yyyy-MM-dd
HH:mm:ss.SSSSSSSSS)#x]
++- OneRowRelation
+
+
+-- !query
+SELECT to_varchar('2020-01-01 13:24:35.123456789' :: timestamp_ntz(8),
'HH:mm:ss.SSSSSSSSS')
+-- !query analysis
+Project [date_format(cast(2020-01-01 13:24:35.123456789 as timestamp_ntz(8)),
HH:mm:ss.SSSSSSSSS, Some(America/Los_Angeles)) AS date_format(CAST(2020-01-01
13:24:35.123456789 AS TIMESTAMP_NTZ(8)), HH:mm:ss.SSSSSSSSS)#x]
++- OneRowRelation
+
+
+-- !query
+SELECT date_format(TIMESTAMP_NTZ '1960-01-01 13:24:35.123456789', 'yyyy-MM-dd
HH:mm:ss.SSSSSSSSS')
+-- !query analysis
+Project [date_format(1960-01-01 13:24:35.123456789, yyyy-MM-dd
HH:mm:ss.SSSSSSSSS, Some(America/Los_Angeles)) AS date_format(TIMESTAMP_NTZ
'1960-01-01 13:24:35.123456789', yyyy-MM-dd HH:mm:ss.SSSSSSSSS)#x]
++- OneRowRelation
+
+
+-- !query
+SELECT date_format(NULL :: timestamp_ntz(9), 'yyyy-MM-dd HH:mm:ss.SSSSSSSSS')
+-- !query analysis
+Project [date_format(cast(null as timestamp_ntz(9)), yyyy-MM-dd
HH:mm:ss.SSSSSSSSS, Some(America/Los_Angeles)) AS date_format(CAST(NULL AS
TIMESTAMP_NTZ(9)), yyyy-MM-dd HH:mm:ss.SSSSSSSSS)#x]
++- OneRowRelation
+
+
-- !query
SELECT date_trunc('SECOND', TIMESTAMP_NTZ '2020-01-01 12:34:56.123456789')
-- !query analysis
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 a6bed71cc1ac..427f89c96666 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
@@ -329,6 +329,25 @@ SELECT v, lead(v) OVER (ORDER BY v) AS next_v FROM (
UNION ALL SELECT TIMESTAMP_LTZ '2020-01-01 00:00:00.000000100'
UNION ALL SELECT TIMESTAMP_LTZ '2020-01-01 00:00:00.000000500') ORDER BY v;
+-- SPARK-57816: date_format / to_char / to_varchar over nanosecond-precision
values. The pattern's
+-- fractional-second placeholders render up to nanosecond digits; a 9-`S`
field is fixed width, so
+-- digits below the type's precision floor to zeros rather than being dropped.
LTZ renders in the
+-- session time zone (America/Los_Angeles, UTC-08:00), so a bare literal
round-trips while an
+-- explicit-zone literal shifts the rendered wall clock. to_char / to_varchar
route through the
+-- same code path.
+SELECT date_format(TIMESTAMP_LTZ '2020-01-01 13:24:35.123456789', 'yyyy-MM-dd
HH:mm:ss.SSSSSSSSS');
+SELECT date_format('2020-01-01 13:24:35.123456789' :: timestamp_ltz(8),
'yyyy-MM-dd HH:mm:ss.SSSSSSSSS');
+SELECT date_format('2020-01-01 13:24:35.123456789' :: timestamp_ltz(7),
'yyyy-MM-dd HH:mm:ss.SSSSSSSSS');
+-- An explicit-zone literal (UTC) is shifted into the session zone before
rendering.
+SELECT date_format(TIMESTAMP_LTZ '2020-01-01 21:24:35.123456789 UTC',
'yyyy-MM-dd HH:mm:ss.SSSSSSSSS');
+SELECT to_char(TIMESTAMP_LTZ '2020-01-01 13:24:35.123456789', 'yyyy-MM-dd
HH:mm:ss.SSSSSSSSS');
+SELECT to_char('2020-01-01 13:24:35.123456789' :: timestamp_ltz(7),
'yyyy-MM-dd HH:mm:ss.SSSSSSSSS');
+SELECT to_varchar('2020-01-01 13:24:35.123456789' :: timestamp_ltz(8),
'HH:mm:ss.SSSSSSSSS');
+-- Pre-epoch value exercises the negative-epoch path.
+SELECT date_format(TIMESTAMP_LTZ '1960-01-01 13:24:35.123456789 UTC',
'yyyy-MM-dd HH:mm:ss.SSSSSSSSS');
+-- NULL nanosecond timestamp.
+SELECT date_format(NULL :: timestamp_ltz(9), 'yyyy-MM-dd HH:mm:ss.SSSSSSSSS');
+
-- SPARK-57821: date_trunc keeps the nanosecond type/family and zeroes the
whole fraction (including
-- the sub-microsecond digits); MICROSECOND keeps epochMicros and only drops
nanosWithinMicro. LTZ
-- truncates in the session time zone (America/Los_Angeles, UTC-08:00), so DAY
over an early-hours
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 aa173fb72620..384acca073be 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
@@ -269,6 +269,21 @@ SELECT v, lead(v) OVER (ORDER BY v) AS next_v FROM (
UNION ALL SELECT TIMESTAMP_NTZ '2020-01-01 00:00:00.000000100'
UNION ALL SELECT TIMESTAMP_NTZ '2020-01-01 00:00:00.000000500') ORDER BY v;
+-- SPARK-57816: date_format / to_char / to_varchar over nanosecond-precision
values. The pattern's
+-- fractional-second placeholders render up to nanosecond digits; a 9-`S`
field is fixed width, so
+-- digits below the type's precision floor to zeros rather than being dropped.
NTZ renders its
+-- wall clock zone-independently. to_char / to_varchar route through the same
code path.
+SELECT date_format(TIMESTAMP_NTZ '2020-01-01 13:24:35.123456789', 'yyyy-MM-dd
HH:mm:ss.SSSSSSSSS');
+SELECT date_format('2020-01-01 13:24:35.123456789' :: timestamp_ntz(8),
'yyyy-MM-dd HH:mm:ss.SSSSSSSSS');
+SELECT date_format('2020-01-01 13:24:35.123456789' :: timestamp_ntz(7),
'yyyy-MM-dd HH:mm:ss.SSSSSSSSS');
+SELECT to_char(TIMESTAMP_NTZ '2020-01-01 13:24:35.123456789', 'yyyy-MM-dd
HH:mm:ss.SSSSSSSSS');
+SELECT to_char('2020-01-01 13:24:35.123456789' :: timestamp_ntz(7),
'yyyy-MM-dd HH:mm:ss.SSSSSSSSS');
+SELECT to_varchar('2020-01-01 13:24:35.123456789' :: timestamp_ntz(8),
'HH:mm:ss.SSSSSSSSS');
+-- Pre-epoch value exercises the negative-epoch path.
+SELECT date_format(TIMESTAMP_NTZ '1960-01-01 13:24:35.123456789', 'yyyy-MM-dd
HH:mm:ss.SSSSSSSSS');
+-- NULL nanosecond timestamp.
+SELECT date_format(NULL :: timestamp_ntz(9), 'yyyy-MM-dd HH:mm:ss.SSSSSSSSS');
+
-- SPARK-57821: date_trunc keeps the nanosecond type/family and zeroes the
whole fraction (including
-- the sub-microsecond digits); MICROSECOND keeps epochMicros and only drops
nanosWithinMicro. NTZ
-- is zone-independent, so DAY/HOUR read the wall clock and never shift.
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 8aa3586481e7..f1f3b7f61022 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
@@ -1127,6 +1127,78 @@ struct<v:timestamp_ltz(9),next_v:timestamp_ltz(9)>
2020-01-01 00:00:00.0000009 NULL
+-- !query
+SELECT date_format(TIMESTAMP_LTZ '2020-01-01 13:24:35.123456789', 'yyyy-MM-dd
HH:mm:ss.SSSSSSSSS')
+-- !query schema
+struct<date_format(TIMESTAMP_LTZ '2020-01-01 13:24:35.123456789', yyyy-MM-dd
HH:mm:ss.SSSSSSSSS):string>
+-- !query output
+2020-01-01 13:24:35.123456789
+
+
+-- !query
+SELECT date_format('2020-01-01 13:24:35.123456789' :: timestamp_ltz(8),
'yyyy-MM-dd HH:mm:ss.SSSSSSSSS')
+-- !query schema
+struct<date_format(CAST(2020-01-01 13:24:35.123456789 AS TIMESTAMP_LTZ(8)),
yyyy-MM-dd HH:mm:ss.SSSSSSSSS):string>
+-- !query output
+2020-01-01 13:24:35.123456780
+
+
+-- !query
+SELECT date_format('2020-01-01 13:24:35.123456789' :: timestamp_ltz(7),
'yyyy-MM-dd HH:mm:ss.SSSSSSSSS')
+-- !query schema
+struct<date_format(CAST(2020-01-01 13:24:35.123456789 AS TIMESTAMP_LTZ(7)),
yyyy-MM-dd HH:mm:ss.SSSSSSSSS):string>
+-- !query output
+2020-01-01 13:24:35.123456700
+
+
+-- !query
+SELECT date_format(TIMESTAMP_LTZ '2020-01-01 21:24:35.123456789 UTC',
'yyyy-MM-dd HH:mm:ss.SSSSSSSSS')
+-- !query schema
+struct<date_format(TIMESTAMP_LTZ '2020-01-01 13:24:35.123456789', yyyy-MM-dd
HH:mm:ss.SSSSSSSSS):string>
+-- !query output
+2020-01-01 13:24:35.123456789
+
+
+-- !query
+SELECT to_char(TIMESTAMP_LTZ '2020-01-01 13:24:35.123456789', 'yyyy-MM-dd
HH:mm:ss.SSSSSSSSS')
+-- !query schema
+struct<date_format(TIMESTAMP_LTZ '2020-01-01 13:24:35.123456789', yyyy-MM-dd
HH:mm:ss.SSSSSSSSS):string>
+-- !query output
+2020-01-01 13:24:35.123456789
+
+
+-- !query
+SELECT to_char('2020-01-01 13:24:35.123456789' :: timestamp_ltz(7),
'yyyy-MM-dd HH:mm:ss.SSSSSSSSS')
+-- !query schema
+struct<date_format(CAST(2020-01-01 13:24:35.123456789 AS TIMESTAMP_LTZ(7)),
yyyy-MM-dd HH:mm:ss.SSSSSSSSS):string>
+-- !query output
+2020-01-01 13:24:35.123456700
+
+
+-- !query
+SELECT to_varchar('2020-01-01 13:24:35.123456789' :: timestamp_ltz(8),
'HH:mm:ss.SSSSSSSSS')
+-- !query schema
+struct<date_format(CAST(2020-01-01 13:24:35.123456789 AS TIMESTAMP_LTZ(8)),
HH:mm:ss.SSSSSSSSS):string>
+-- !query output
+13:24:35.123456780
+
+
+-- !query
+SELECT date_format(TIMESTAMP_LTZ '1960-01-01 13:24:35.123456789 UTC',
'yyyy-MM-dd HH:mm:ss.SSSSSSSSS')
+-- !query schema
+struct<date_format(TIMESTAMP_LTZ '1960-01-01 05:24:35.123456789', yyyy-MM-dd
HH:mm:ss.SSSSSSSSS):string>
+-- !query output
+1960-01-01 05:24:35.123456789
+
+
+-- !query
+SELECT date_format(NULL :: timestamp_ltz(9), 'yyyy-MM-dd HH:mm:ss.SSSSSSSSS')
+-- !query schema
+struct<date_format(CAST(NULL AS TIMESTAMP_LTZ(9)), yyyy-MM-dd
HH:mm:ss.SSSSSSSSS):string>
+-- !query output
+NULL
+
+
-- !query
SELECT date_trunc('SECOND', TIMESTAMP_LTZ '2020-01-01 12:34:56.123456789 UTC')
-- !query schema
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 acb2fe195115..dbb3a77295ee 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
@@ -924,6 +924,70 @@ struct<v:timestamp_ntz(9),next_v:timestamp_ntz(9)>
2020-01-01 00:00:00.0000009 NULL
+-- !query
+SELECT date_format(TIMESTAMP_NTZ '2020-01-01 13:24:35.123456789', 'yyyy-MM-dd
HH:mm:ss.SSSSSSSSS')
+-- !query schema
+struct<date_format(TIMESTAMP_NTZ '2020-01-01 13:24:35.123456789', yyyy-MM-dd
HH:mm:ss.SSSSSSSSS):string>
+-- !query output
+2020-01-01 13:24:35.123456789
+
+
+-- !query
+SELECT date_format('2020-01-01 13:24:35.123456789' :: timestamp_ntz(8),
'yyyy-MM-dd HH:mm:ss.SSSSSSSSS')
+-- !query schema
+struct<date_format(CAST(2020-01-01 13:24:35.123456789 AS TIMESTAMP_NTZ(8)),
yyyy-MM-dd HH:mm:ss.SSSSSSSSS):string>
+-- !query output
+2020-01-01 13:24:35.123456780
+
+
+-- !query
+SELECT date_format('2020-01-01 13:24:35.123456789' :: timestamp_ntz(7),
'yyyy-MM-dd HH:mm:ss.SSSSSSSSS')
+-- !query schema
+struct<date_format(CAST(2020-01-01 13:24:35.123456789 AS TIMESTAMP_NTZ(7)),
yyyy-MM-dd HH:mm:ss.SSSSSSSSS):string>
+-- !query output
+2020-01-01 13:24:35.123456700
+
+
+-- !query
+SELECT to_char(TIMESTAMP_NTZ '2020-01-01 13:24:35.123456789', 'yyyy-MM-dd
HH:mm:ss.SSSSSSSSS')
+-- !query schema
+struct<date_format(TIMESTAMP_NTZ '2020-01-01 13:24:35.123456789', yyyy-MM-dd
HH:mm:ss.SSSSSSSSS):string>
+-- !query output
+2020-01-01 13:24:35.123456789
+
+
+-- !query
+SELECT to_char('2020-01-01 13:24:35.123456789' :: timestamp_ntz(7),
'yyyy-MM-dd HH:mm:ss.SSSSSSSSS')
+-- !query schema
+struct<date_format(CAST(2020-01-01 13:24:35.123456789 AS TIMESTAMP_NTZ(7)),
yyyy-MM-dd HH:mm:ss.SSSSSSSSS):string>
+-- !query output
+2020-01-01 13:24:35.123456700
+
+
+-- !query
+SELECT to_varchar('2020-01-01 13:24:35.123456789' :: timestamp_ntz(8),
'HH:mm:ss.SSSSSSSSS')
+-- !query schema
+struct<date_format(CAST(2020-01-01 13:24:35.123456789 AS TIMESTAMP_NTZ(8)),
HH:mm:ss.SSSSSSSSS):string>
+-- !query output
+13:24:35.123456780
+
+
+-- !query
+SELECT date_format(TIMESTAMP_NTZ '1960-01-01 13:24:35.123456789', 'yyyy-MM-dd
HH:mm:ss.SSSSSSSSS')
+-- !query schema
+struct<date_format(TIMESTAMP_NTZ '1960-01-01 13:24:35.123456789', yyyy-MM-dd
HH:mm:ss.SSSSSSSSS):string>
+-- !query output
+1960-01-01 13:24:35.123456789
+
+
+-- !query
+SELECT date_format(NULL :: timestamp_ntz(9), 'yyyy-MM-dd HH:mm:ss.SSSSSSSSS')
+-- !query schema
+struct<date_format(CAST(NULL AS TIMESTAMP_NTZ(9)), yyyy-MM-dd
HH:mm:ss.SSSSSSSSS):string>
+-- !query output
+NULL
+
+
-- !query
SELECT date_trunc('SECOND', TIMESTAMP_NTZ '2020-01-01 12:34:56.123456789')
-- !query schema
diff --git
a/sql/core/src/test/scala/org/apache/spark/sql/TimestampNanosFunctionsSuiteBase.scala
b/sql/core/src/test/scala/org/apache/spark/sql/TimestampNanosFunctionsSuiteBase.scala
index ee350f5c5e2d..14e346753d8e 100644
---
a/sql/core/src/test/scala/org/apache/spark/sql/TimestampNanosFunctionsSuiteBase.scala
+++
b/sql/core/src/test/scala/org/apache/spark/sql/TimestampNanosFunctionsSuiteBase.scala
@@ -19,7 +19,7 @@ package org.apache.spark.sql
import java.time.{Instant, LocalDateTime}
-import org.apache.spark.SparkConf
+import org.apache.spark.{SparkConf, SparkRuntimeException,
SparkUnsupportedOperationException}
import org.apache.spark.sql.functions._
import org.apache.spark.sql.internal.SQLConf
import org.apache.spark.sql.test.SharedSparkSession
@@ -666,6 +666,121 @@ abstract class TimestampNanosFunctionsSuiteBase extends
SharedSparkSession {
}
}
}
+
+ test("SPARK-57816: date_format / to_char / to_varchar over
nanosecond-precision timestamps") {
+ // The 9-`S` pattern is a fixed-width fraction field, so it always emits 9
digits; truncating to
+ // precision `p` zeros the low digits (floor); it does not drop them. The
session zone is
+ // America/Los_Angeles: TIMESTAMP_NTZ renders its wall clock
zone-independently, while the
+ // TIMESTAMP_LTZ instant (21:24:35 UTC) shifts to 13:24:35 in the session
zone (UTC-8 in Jan).
+ val fmt = "yyyy-MM-dd HH:mm:ss.SSSSSSSSS"
+ val ntzStr = "2020-01-01T13:24:35.123456789"
+ val ltzStr = "2020-01-01T21:24:35.987654321Z"
+ Seq(
+ 9 -> ("2020-01-01 13:24:35.123456789", "2020-01-01 13:24:35.987654321"),
+ 8 -> ("2020-01-01 13:24:35.123456780", "2020-01-01 13:24:35.987654320"),
+ 7 -> ("2020-01-01 13:24:35.123456700", "2020-01-01 13:24:35.987654300")
+ ).foreach { case (p, (ntzExpected, ltzExpected)) =>
+ val ntz = ntzNanos(ntzStr, p)
+ // SQL path: date_format, to_char, to_varchar all render sub-microsecond
digits.
+ checkAnswer(
+ ntz.selectExpr(
+ s"date_format(c, '$fmt')", s"to_char(c, '$fmt')", s"to_varchar(c,
'$fmt')"),
+ Row(ntzExpected, ntzExpected, ntzExpected))
+ // Column API agrees with the SQL path.
+ checkAnswer(
+ ntz.select(date_format(col("c"), fmt), to_char(col("c"), lit(fmt)),
+ to_varchar(col("c"), lit(fmt))),
+ Row(ntzExpected, ntzExpected, ntzExpected))
+
+ val ltz = ltzNanos(ltzStr, p)
+ checkAnswer(
+ ltz.selectExpr(
+ s"date_format(c, '$fmt')", s"to_char(c, '$fmt')", s"to_varchar(c,
'$fmt')"),
+ Row(ltzExpected, ltzExpected, ltzExpected))
+ checkAnswer(
+ ltz.select(date_format(col("c"), fmt), to_char(col("c"), lit(fmt)),
+ to_varchar(col("c"), lit(fmt))),
+ Row(ltzExpected, ltzExpected, ltzExpected))
+ }
+
+ // NULL nanosecond input renders NULL for all three functions.
+ Seq(7, 8, 9).foreach { p =>
+ val ntzNull = spark.createDataFrame(
+ spark.sparkContext.parallelize(Seq(Row(null))),
+ new StructType().add("c", TimestampNTZNanosType(p)))
+ val ltzNull = spark.createDataFrame(
+ spark.sparkContext.parallelize(Seq(Row(null))),
+ new StructType().add("c", TimestampLTZNanosType(p)))
+ checkAnswer(
+ ntzNull.selectExpr(
+ s"date_format(c, '$fmt')", s"to_char(c, '$fmt')", s"to_varchar(c,
'$fmt')"),
+ Row(null, null, null))
+ checkAnswer(
+ ltzNull.selectExpr(
+ s"date_format(c, '$fmt')", s"to_char(c, '$fmt')", s"to_varchar(c,
'$fmt')"),
+ Row(null, null, null))
+ }
+ }
+
+ test("SPARK-57816: date_format / to_char / to_varchar over nanos reject the
LEGACY " +
+ "time parser policy") {
+ // date_format never sets forTimestampNTZ, so under LEGACY it builds a
legacy formatter for
+ // both NTZ and LTZ nanos, whose nanos format methods raise
+ // TIMESTAMP_NANOS_WITH_LEGACY_TIME_PARSER (a micros value would format
fine). Mirrors the
+ // JSON-path LEGACY coverage (SPARK-57456) at the expression level.
+ def rootNanosError(e: Throwable): SparkUnsupportedOperationException = {
+ var cause = e
+ while (cause != null &&
!cause.isInstanceOf[SparkUnsupportedOperationException]) {
+ cause = cause.getCause
+ }
+ assert(cause != null, s"Expected
TIMESTAMP_NANOS_WITH_LEGACY_TIME_PARSER, but got: $e")
+ cause.asInstanceOf[SparkUnsupportedOperationException]
+ }
+ withSQLConf(SQLConf.LEGACY_TIME_PARSER_POLICY.key -> "LEGACY") {
+ val fmt = "yyyy-MM-dd HH:mm:ss.SSSSSSSSS"
+ val expectedParameters =
+ Map("config" -> ("\"" + SQLConf.LEGACY_TIME_PARSER_POLICY.key + "\""))
+ val dfs = Seq(
+ ntzNanos("2020-01-01T13:24:35.123456789", 9),
+ ltzNanos("2020-01-01T21:24:35.987654321Z", 9))
+ dfs.foreach { df =>
+ Seq(s"date_format(c, '$fmt')", s"to_char(c, '$fmt')", s"to_varchar(c,
'$fmt')")
+ .foreach { expr =>
+ checkError(
+ exception = rootNanosError(intercept[Exception] {
+ df.selectExpr(expr).collect()
+ }),
+ condition =
"UNSUPPORTED_FEATURE.TIMESTAMP_NANOS_WITH_LEGACY_TIME_PARSER",
+ parameters = expectedParameters)
+ }
+ }
+ }
+ }
+
+ test("SPARK-57816: date_format / to_char / to_varchar over NTZ nanos reject
a zone-token " +
+ "pattern with a clean error") {
+ // A zone token (`z`, `Z`, `X`, `O`, `VV`) has no meaning for the
zone-less TIMESTAMP_NTZ wall
+ // clock, so rendering raises java.time.DateTimeException underneath.
date_format maps it to a
+ // clean INVALID_PARAMETER_VALUE.PATTERN Spark error rather than leaking
the raw java.time
+ // exception. LTZ is zone-aware, so the same pattern renders the session
zone instead of
+ // erroring - hence this is NTZ-only.
+ val ntz = ntzNanos("2020-01-01T13:24:35.123456789", 9)
+ Seq("z", "Z", "X", "O", "VV").foreach { zoneToken =>
+ val fmt = s"yyyy-MM-dd HH:mm:ss $zoneToken"
+ Seq(s"date_format(c, '$fmt')", s"to_char(c, '$fmt')", s"to_varchar(c,
'$fmt')")
+ .foreach { expr =>
+ checkError(
+ exception = intercept[SparkRuntimeException] {
+ ntz.selectExpr(expr).collect()
+ },
+ condition = "INVALID_PARAMETER_VALUE.PATTERN",
+ parameters = Map(
+ "parameter" -> "`format`",
+ "functionName" -> "`date_format`",
+ "value" -> s"'$fmt'"))
+ }
+ }
+ }
}
// Runs the nanosecond timestamp function tests with ANSI mode enabled
explicitly.
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]