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 8885867c9be2 [SPARK-57340][SQL] Support EXTRACT/date_part HOUR, MINUTE 
and SECOND over nanosecond-precision timestamps
8885867c9be2 is described below

commit 8885867c9be23d66ec0087186d7eb7faafd2d561
Author: Stevo Mitric <[email protected]>
AuthorDate: Sat Jun 13 14:19:25 2026 +0200

    [SPARK-57340][SQL] Support EXTRACT/date_part HOUR, MINUTE and SECOND over 
nanosecond-precision timestamps
    
    ### What changes were proposed in this pull request?
    
    Support `EXTRACT(field FROM source)` and `date_part(field, source)` over 
the nanosecond-precision timestamp types `TIMESTAMP_NTZ(p)` / 
`TIMESTAMP_LTZ(p)`. Follow-up to SPARK-57315, which covered `hour()` / 
`minute()` / `second()` but not `EXTRACT` / `date_part` (they bypass the 
function builders via `DatePart.parseExtractField`).
    
    - **HOUR / MINUTE**: cast the nanosecond input down to the matching 
microsecond type via the existing `NanosTimestampCast.castToMicros` (lossless 
for integer fields).
    - **SECOND**: new `SecondWithFractionNanos` expression returning 
`DECIMAL(11, 9)` with the full nanosecond fraction, instead of truncating to 
the `DECIMAL(8, 6)` of `SecondWithFraction`. Backed by a new 
`DateTimeUtils.getSecondsWithFractionNanos`.
    
    Date-based fields (`YEAR`, `MONTH`, ...) are out of scope
    
    ### Why are the changes needed?
    
    `EXTRACT` / `date_part` are documented as equivalent to `hour()` / 
`minute()` / `second()`, but after SPARK-57315 they still fail analysis on 
nanosecond timestamps. Part of the nanosecond timestamp preview (SPARK-56822).
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes, but only with the preview flag `spark.sql.timestampNanosTypes.enabled` 
set to `true`
    
    ```sql
    SELECT extract(HOUR FROM TIMESTAMP_NTZ '2018-02-14 12:58:59.123456789');
    -- 12 (previously: analysis error)
    SELECT extract(SECOND FROM TIMESTAMP_NTZ '2018-02-14 12:58:59.123456789');
    -- 59.123456789 (DECIMAL(11, 9))
    ```
    
    ### How was this patch tested?
    Tests in this PR.
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Code (Claude Fable 5)
    
    Closes #56466 from stevomitric/stevomitric/extract.
    
    Authored-by: Stevo Mitric <[email protected]>
    Signed-off-by: Max Gekk <[email protected]>
---
 .../catalyst/expressions/datetimeExpressions.scala | 82 ++++++++++++++++--
 .../spark/sql/catalyst/util/DateTimeUtils.scala    | 15 +++-
 .../expressions/DateExpressionsSuite.scala         | 29 +++++++
 .../analyzer-results/timestamp-ltz-nanos.sql.out   | 84 +++++++++++++++++++
 .../analyzer-results/timestamp-ntz-nanos.sql.out   | 70 ++++++++++++++++
 .../sql-tests/inputs/timestamp-ltz-nanos.sql       | 24 ++++++
 .../sql-tests/inputs/timestamp-ntz-nanos.sql       | 19 +++++
 .../sql-tests/results/timestamp-ltz-nanos.sql.out  | 96 ++++++++++++++++++++++
 .../sql-tests/results/timestamp-ntz-nanos.sql.out  | 80 ++++++++++++++++++
 .../sql/TimestampNanosFunctionsSuiteBase.scala     | 48 +++++++++++
 10 files changed, 540 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 e7867b0ac6b4..922d45c4654c 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
@@ -43,7 +43,7 @@ import org.apache.spark.sql.internal.SQLConf
 import org.apache.spark.sql.internal.types.StringTypeWithCollation
 import org.apache.spark.sql.types._
 import org.apache.spark.sql.types.DayTimeIntervalType.DAY
-import org.apache.spark.unsafe.types.{CalendarInterval, UTF8String}
+import org.apache.spark.unsafe.types.{CalendarInterval, TimestampNanosVal, 
UTF8String}
 
 /**
  * Common base class for time zone aware expressions.
@@ -68,7 +68,8 @@ trait TimeZoneAwareExpression extends Expression {
   @transient lazy val zoneId: ZoneId = DateTimeUtils.getZoneId(timeZoneId.get)
 
   def zoneIdForType(dataType: DataType): ZoneId = dataType match {
-    case _: TimestampNTZType => java.time.ZoneOffset.UTC
+    // The TIMESTAMP_NTZ family carries wall-clock fields, so it is evaluated 
in UTC.
+    case _: TimestampNTZType | _: TimestampNTZNanosType => 
java.time.ZoneOffset.UTC
     case _ => zoneId
   }
 }
@@ -468,6 +469,62 @@ case class SecondWithFraction(child: Expression, 
timeZoneId: Option[String] = No
     copy(child = newChild)
 }
 
+/**
+ * Returns the second component with the fractional part of a 
nanosecond-precision timestamp
+ * (`TIMESTAMP_NTZ(p)` / `TIMESTAMP_LTZ(p)`, `p` in `[7, 9]`) as `DECIMAL(11, 
9)`.
+ *
+ * This is the nanosecond counterpart of [[SecondWithFraction]] on the
+ * `EXTRACT(SECOND FROM ...)` / `date_part('SECOND', ...)` path. Unlike the 
integer time-of-day
+ * fields, the result depends on the sub-microsecond digits, so the input is 
not cast down to
+ * microseconds. The scale is fixed at the maximum nanosecond precision, like
+ * [[SecondsOfTimeWithFraction]] does for TIME values: digits below the type's 
precision `p` are
+ * always zero because values are floored to `p` at the type boundary.
+ */
+case class SecondWithFractionNanos(child: Expression, timeZoneId: 
Option[String] = None)
+  extends UnaryExpression with TimeZoneAwareExpression {
+
+  def this(child: Expression) = this(child, None)
+
+  override def nullIntolerant: Boolean = true
+
+  // 2 digits for the seconds, and 9 digits for the fractional part with 
nanosecond precision.
+  override def dataType: DataType = DecimalType(11, 9)
+
+  override def checkInputDataTypes(): TypeCheckResult = child.dataType match {
+    case _: TimestampNTZNanosType | _: TimestampLTZNanosType => 
TypeCheckSuccess
+    case _ =>
+      DataTypeMismatch(
+        errorSubClass = "UNEXPECTED_INPUT_TYPE",
+        messageParameters = Map(
+          "paramIndex" -> ordinalNumber(0),
+          "requiredType" ->
+            Seq(TimestampNTZNanosType(), 
TimestampLTZNanosType()).map(toSQLType).mkString(" or "),
+          "inputSql" -> toSQLExpr(child),
+          "inputType" -> toSQLType(child.dataType)))
+  }
+
+  override def withTimeZone(timeZoneId: String): SecondWithFractionNanos =
+    copy(timeZoneId = Option(timeZoneId))
+
+  // TIMESTAMP_NTZ(p) extracts the wall-clock fields, so it is evaluated in 
UTC;
+  // TIMESTAMP_LTZ(p) uses the session time zone.
+  @transient private lazy val zoneIdInEval: ZoneId = 
zoneIdForType(child.dataType)
+
+  override protected def nullSafeEval(timestamp: Any): Any = {
+    DateTimeUtils.getSecondsWithFractionNanos(
+      timestamp.asInstanceOf[TimestampNanosVal], zoneIdInEval)
+  }
+
+  override def doGenCode(ctx: CodegenContext, ev: ExprCode): ExprCode = {
+    val zid = ctx.addReferenceObj("zoneId", zoneIdInEval, 
classOf[ZoneId].getName)
+    val dtu = DateTimeUtils.getClass.getName.stripSuffix("$")
+    defineCodeGen(ctx, ev, c => s"$dtu.getSecondsWithFractionNanos($c, $zid)")
+  }
+
+  override protected def withNewChildInternal(newChild: Expression): 
SecondWithFractionNanos =
+    copy(child = newChild)
+}
+
 trait GetDateField extends UnaryExpression with ImplicitCastInputTypes {
   override def nullIntolerant: Boolean = true
   val func: Int => Any
@@ -3341,9 +3398,22 @@ object DatePart {
     case "DAYOFWEEK" | "DOW" => DayOfWeek(source)
     case "DAYOFWEEK_ISO" | "DOW_ISO" => Add(WeekDay(source), Literal(1))
     case "DOY" => DayOfYear(source)
-    case "HOUR" | "H" | "HOURS" | "HR" | "HRS" => Hour(source)
-    case "MINUTE" | "M" | "MIN" | "MINS" | "MINUTES" => Minute(source)
-    case "SECOND" | "S" | "SEC" | "SECONDS" | "SECS" => 
SecondWithFraction(source)
+    case "HOUR" | "H" | "HOURS" | "HR" | "HRS" =>
+      // Nanosecond-precision timestamps are cast down to the matching 
microsecond timestamp
+      // type, which is lossless for the integer time-of-day fields 
(SPARK-57340); other types
+      // are passed through unchanged.
+      Hour(NanosTimestampCast.castToMicros(source))
+    case "MINUTE" | "M" | "MIN" | "MINS" | "MINUTES" =>
+      Minute(NanosTimestampCast.castToMicros(source))
+    case "SECOND" | "S" | "SEC" | "SECONDS" | "SECS" =>
+      source.dataType match {
+        // EXTRACT(SECOND) exposes the fractional digits, so 
nanosecond-precision timestamps
+        // keep their sub-microsecond digits and widen the result to 
DECIMAL(11, 9) instead of
+        // casting down to microseconds (SPARK-57340).
+        case _: TimestampNTZNanosType | _: TimestampLTZNanosType =>
+          SecondWithFractionNanos(source)
+        case _ => SecondWithFraction(source)
+      }
     case _ =>
       throw 
QueryCompilationErrors.literalTypeUnsupportedForSourceTypeError(extractField, 
source)
   }
@@ -3413,7 +3483,7 @@ object DatePartExpressionBuilder extends 
ExpressionBuilder {
               - "DOY" - the day of the year (1 - 365/366)
               - "HOUR", ("H", "HOURS", "HR", "HRS") - The hour field (0 - 23)
               - "MINUTE", ("M", "MIN", "MINS", "MINUTES") - the minutes field 
(0 - 59)
-              - "SECOND", ("S", "SEC", "SECONDS", "SECS") - the seconds field, 
including fractional parts
+              - "SECOND", ("S", "SEC", "SECONDS", "SECS") - the seconds field, 
including fractional parts. Returns a DECIMAL(8, 6) value, or a DECIMAL(11, 9) 
value for the nanosecond-precision timestamps TIMESTAMP_NTZ(p) / 
TIMESTAMP_LTZ(p) with p in [7, 9]
           - Supported string values of `field` for interval(which consists of 
`months`, `days`, `microseconds`) are(case insensitive):
               - "YEAR", ("Y", "YEARS", "YR", "YRS") - the total `months` / 12
               - "MONTH", ("MON", "MONS", "MONTHS") - the total `months` % 12
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 c5e6b5cbce93..38263a7ebc1c 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
@@ -29,7 +29,7 @@ import org.apache.spark.{QueryContext, SparkException, 
SparkIllegalArgumentExcep
 import org.apache.spark.sql.catalyst.util.DateTimeConstants._
 import org.apache.spark.sql.errors.QueryExecutionErrors
 import org.apache.spark.sql.types.{Decimal, DoubleExactNumeric, 
TimestampNTZType, TimestampType}
-import org.apache.spark.unsafe.types.{CalendarInterval, UTF8String}
+import org.apache.spark.unsafe.types.{CalendarInterval, TimestampNanosVal, 
UTF8String}
 
 /**
  * Helper functions for converting between internal and external date and time 
representations.
@@ -148,6 +148,19 @@ object DateTimeUtils extends SparkDateTimeUtils {
     Decimal(getMicroseconds(micros, zoneId), 8, 6)
   }
 
+  /**
+   * Returns the seconds part and its fractional part with nanoseconds, of a 
nanosecond-precision
+   * timestamp. The integral seconds and the six microsecond fraction digits 
come from the
+   * wall-clock fields of `epochMicros` in the given zone; `nanosWithinMicro` 
contributes the
+   * three sub-microsecond digits. The scale is fixed at the maximum 
nanosecond precision (9):
+   * digits below the value's type precision are always zero because values 
are floored to the
+   * type precision at the type boundary.
+   */
+  def getSecondsWithFractionNanos(ts: TimestampNanosVal, zoneId: ZoneId): 
Decimal = {
+    val nanosOfMinute =
+      getMicroseconds(ts.epochMicros, zoneId) * NANOS_PER_MICROS + 
ts.nanosWithinMicro
+    Decimal(nanosOfMinute, 11, 9)
+  }
 
   /**
    * Returns the second value with fraction from a given TIME (TimeType) value.
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 4a2b23fe059b..2ffc30b3d7c4 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
@@ -1272,6 +1272,35 @@ class DateExpressionsSuite extends SparkFunSuite with 
ExpressionEvalHelper {
     }
   }
 
+  test("SPARK-57340: extract the seconds part with fraction from nanosecond 
timestamps") {
+    import org.apache.spark.sql.catalyst.util.TimestampNanosTestUtils._
+    // NTZ extracts the wall-clock fields and is zone-independent: the 
expression evaluates in
+    // UTC regardless of the session time zone.
+    val ntzValue = localDateTimeToNanosVal(timestampNTZ(2020, 1, 1, 13, 24, 
35, 123456789))
+    outstandingTimezonesIds.foreach { timezone =>
+      checkEvaluation(
+        SecondWithFractionNanos(
+          Literal.create(ntzValue, TimestampNTZNanosType(9)), Some(timezone)),
+        Decimal(35123456789L, 11, 9))
+    }
+    // LTZ extracts in the session time zone. The second field (including the 
nanosecond
+    // fraction) is invariant under whole-minute zone offsets, such as 
Asia/Kolkata (+05:30).
+    val ltzValue = 
instantToNanosVal(Instant.parse("2020-01-01T21:24:35.987654321Z"))
+    def ltzExpr(timezone: String): SecondWithFractionNanos =
+      SecondWithFractionNanos(Literal.create(ltzValue, 
TimestampLTZNanosType(9)), Some(timezone))
+    checkEvaluation(ltzExpr("America/Los_Angeles"), Decimal(35987654321L, 11, 
9))
+    checkEvaluation(ltzExpr("Asia/Kolkata"), Decimal(35987654321L, 11, 9))
+    // Pre-epoch values exercise the negative-epoch path; the wall-clock 
fields stay positive.
+    val preEpoch = localDateTimeToNanosVal(timestampNTZ(1960, 1, 1, 5, 6, 7, 
123456789))
+    checkEvaluation(
+      SecondWithFractionNanos(Literal.create(preEpoch, 
TimestampNTZNanosType(9)), Some("UTC")),
+      Decimal(7123456789L, 11, 9))
+    // NULL input.
+    checkEvaluation(
+      SecondWithFractionNanos(Literal.create(null, TimestampNTZNanosType(9)), 
Some("UTC")),
+      null)
+  }
+
   test("SPARK-34903: timestamps difference") {
     val end = Instant.parse("2019-10-04T11:04:01.123456Z")
     outstandingTimezonesIds.foreach { tz =>
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 fa8c562c9416..bbc249f3deb2 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
@@ -221,3 +221,87 @@ SELECT second(TIMESTAMP_LTZ '2020-01-01 13:24:35.123456789 
UTC')
 -- !query analysis
 Project [second(cast(TimestampNanosVal(1577885075123456, 789) as timestamp), 
Some(America/Los_Angeles)) AS second(TimestampNanosVal(1577885075123456, 
789))#x]
 +- OneRowRelation
+
+
+-- !query
+SELECT extract(HOUR FROM TIMESTAMP_LTZ '2020-01-01 13:24:35.123456789')
+-- !query analysis
+Project [extract(HOUR, TimestampNanosVal(1577913875123456, 789)) AS 
extract(HOUR FROM TimestampNanosVal(1577913875123456, 789))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT extract(MINUTE FROM TIMESTAMP_LTZ '2020-01-01 13:24:35.123456789')
+-- !query analysis
+Project [extract(MINUTE, TimestampNanosVal(1577913875123456, 789)) AS 
extract(MINUTE FROM TimestampNanosVal(1577913875123456, 789))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT extract(SECOND FROM TIMESTAMP_LTZ '2020-01-01 13:24:35.123456789')
+-- !query analysis
+Project [extract(SECOND, TimestampNanosVal(1577913875123456, 789)) AS 
extract(SECOND FROM TimestampNanosVal(1577913875123456, 789))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT date_part('HOUR', TIMESTAMP_LTZ '2020-01-01 13:24:35.123456789')
+-- !query analysis
+Project [date_part(HOUR, TimestampNanosVal(1577913875123456, 789)) AS 
date_part(HOUR, TimestampNanosVal(1577913875123456, 789))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT date_part('MINUTE', TIMESTAMP_LTZ '2020-01-01 13:24:35.123456789')
+-- !query analysis
+Project [date_part(MINUTE, TimestampNanosVal(1577913875123456, 789)) AS 
date_part(MINUTE, TimestampNanosVal(1577913875123456, 789))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT date_part('SECOND', TIMESTAMP_LTZ '2020-01-01 13:24:35.123456789')
+-- !query analysis
+Project [date_part(SECOND, TimestampNanosVal(1577913875123456, 789)) AS 
date_part(SECOND, TimestampNanosVal(1577913875123456, 789))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT extract(SECOND FROM '2020-01-01 13:24:35.999999999' :: timestamp_ltz(7))
+-- !query analysis
+Project [extract(SECOND, cast(2020-01-01 13:24:35.999999999 as 
timestamp_ltz(7))) AS extract(SECOND FROM CAST(2020-01-01 13:24:35.999999999 AS 
TIMESTAMP_LTZ(7)))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT extract(SECOND FROM '2020-01-01 13:24:35.999999999' :: timestamp_ltz(8))
+-- !query analysis
+Project [extract(SECOND, cast(2020-01-01 13:24:35.999999999 as 
timestamp_ltz(8))) AS extract(SECOND FROM CAST(2020-01-01 13:24:35.999999999 AS 
TIMESTAMP_LTZ(8)))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT extract(SECOND FROM NULL :: timestamp_ltz(9))
+-- !query analysis
+Project [extract(SECOND, cast(null as timestamp_ltz(9))) AS extract(SECOND 
FROM CAST(NULL AS TIMESTAMP_LTZ(9)))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT extract(SECOND FROM TIMESTAMP_LTZ '1960-01-01 13:24:35.123456789')
+-- !query analysis
+Project [extract(SECOND, TimestampNanosVal(-315542124876544, 789)) AS 
extract(SECOND FROM TimestampNanosVal(-315542124876544, 789))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT extract(MINUTE FROM TIMESTAMP_LTZ '2020-01-01 13:24:35.123456789 
Asia/Kolkata')
+-- !query analysis
+Project [extract(MINUTE, TimestampNanosVal(1577865275123456, 789)) AS 
extract(MINUTE FROM TimestampNanosVal(1577865275123456, 789))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT extract(SECOND FROM TIMESTAMP_LTZ '2020-01-01 13:24:35.123456789 
Asia/Kolkata')
+-- !query analysis
+Project [extract(SECOND, TimestampNanosVal(1577865275123456, 789)) AS 
extract(SECOND FROM TimestampNanosVal(1577865275123456, 789))#x]
++- OneRowRelation
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 97bcd3b290a0..85192002d55e 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
@@ -179,3 +179,73 @@ SELECT second(TIMESTAMP_NTZ '1960-01-01 
13:24:35.123456789')
 -- !query analysis
 Project [second(cast(TimestampNanosVal(-315570924876544, 789) as 
timestamp_ntz), Some(America/Los_Angeles)) AS 
second(TimestampNanosVal(-315570924876544, 789))#x]
 +- OneRowRelation
+
+
+-- !query
+SELECT extract(HOUR FROM TIMESTAMP_NTZ '2020-01-01 13:24:35.123456789')
+-- !query analysis
+Project [extract(HOUR, TimestampNanosVal(1577885075123456, 789)) AS 
extract(HOUR FROM TimestampNanosVal(1577885075123456, 789))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT extract(MINUTE FROM TIMESTAMP_NTZ '2020-01-01 13:24:35.123456789')
+-- !query analysis
+Project [extract(MINUTE, TimestampNanosVal(1577885075123456, 789)) AS 
extract(MINUTE FROM TimestampNanosVal(1577885075123456, 789))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT extract(SECOND FROM TIMESTAMP_NTZ '2020-01-01 13:24:35.123456789')
+-- !query analysis
+Project [extract(SECOND, TimestampNanosVal(1577885075123456, 789)) AS 
extract(SECOND FROM TimestampNanosVal(1577885075123456, 789))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT date_part('HOUR', TIMESTAMP_NTZ '2020-01-01 13:24:35.123456789')
+-- !query analysis
+Project [date_part(HOUR, TimestampNanosVal(1577885075123456, 789)) AS 
date_part(HOUR, TimestampNanosVal(1577885075123456, 789))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT date_part('MINUTE', TIMESTAMP_NTZ '2020-01-01 13:24:35.123456789')
+-- !query analysis
+Project [date_part(MINUTE, TimestampNanosVal(1577885075123456, 789)) AS 
date_part(MINUTE, TimestampNanosVal(1577885075123456, 789))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT date_part('SECOND', TIMESTAMP_NTZ '2020-01-01 13:24:35.123456789')
+-- !query analysis
+Project [date_part(SECOND, TimestampNanosVal(1577885075123456, 789)) AS 
date_part(SECOND, TimestampNanosVal(1577885075123456, 789))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT extract(SECOND FROM '2020-01-01 13:24:35.999999999' :: timestamp_ntz(7))
+-- !query analysis
+Project [extract(SECOND, cast(2020-01-01 13:24:35.999999999 as 
timestamp_ntz(7))) AS extract(SECOND FROM CAST(2020-01-01 13:24:35.999999999 AS 
TIMESTAMP_NTZ(7)))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT extract(SECOND FROM '2020-01-01 13:24:35.999999999' :: timestamp_ntz(8))
+-- !query analysis
+Project [extract(SECOND, cast(2020-01-01 13:24:35.999999999 as 
timestamp_ntz(8))) AS extract(SECOND FROM CAST(2020-01-01 13:24:35.999999999 AS 
TIMESTAMP_NTZ(8)))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT extract(SECOND FROM NULL :: timestamp_ntz(9))
+-- !query analysis
+Project [extract(SECOND, cast(null as timestamp_ntz(9))) AS extract(SECOND 
FROM CAST(NULL AS TIMESTAMP_NTZ(9)))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT extract(SECOND FROM TIMESTAMP_NTZ '1960-01-01 13:24:35.123456789')
+-- !query analysis
+Project [extract(SECOND, TimestampNanosVal(-315570924876544, 789)) AS 
extract(SECOND FROM TimestampNanosVal(-315570924876544, 789))#x]
++- OneRowRelation
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 b318ad3021bc..37fef0416b8a 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
@@ -55,3 +55,27 @@ SELECT second(TIMESTAMP_LTZ '2020-01-01 13:24:35.123456789 
Asia/Kolkata');
 SELECT hour(TIMESTAMP_LTZ '2020-01-01 13:24:35.123456789 UTC');
 SELECT minute(TIMESTAMP_LTZ '2020-01-01 13:24:35.123456789 UTC');
 SELECT second(TIMESTAMP_LTZ '2020-01-01 13:24:35.123456789 UTC');
+
+-- EXTRACT / date_part over nanosecond-precision values (SPARK-57340). HOUR 
and MINUTE are
+-- equivalent to the hour()/minute() functions; SECOND keeps the 
sub-microsecond digits and
+-- widens the result to DECIMAL(11, 9). LTZ extracts in the session time zone.
+SELECT extract(HOUR FROM TIMESTAMP_LTZ '2020-01-01 13:24:35.123456789');
+SELECT extract(MINUTE FROM TIMESTAMP_LTZ '2020-01-01 13:24:35.123456789');
+SELECT extract(SECOND FROM TIMESTAMP_LTZ '2020-01-01 13:24:35.123456789');
+SELECT date_part('HOUR', TIMESTAMP_LTZ '2020-01-01 13:24:35.123456789');
+SELECT date_part('MINUTE', TIMESTAMP_LTZ '2020-01-01 13:24:35.123456789');
+SELECT date_part('SECOND', TIMESTAMP_LTZ '2020-01-01 13:24:35.123456789');
+
+-- Digits below the type's precision are floored at the type boundary, so they 
read back as
+-- zeros in the DECIMAL(11, 9) result.
+SELECT extract(SECOND FROM '2020-01-01 13:24:35.999999999' :: 
timestamp_ltz(7));
+SELECT extract(SECOND FROM '2020-01-01 13:24:35.999999999' :: 
timestamp_ltz(8));
+SELECT extract(SECOND FROM NULL :: timestamp_ltz(9));
+
+-- Pre-epoch nanosecond values exercise the negative-epoch path.
+SELECT extract(SECOND FROM TIMESTAMP_LTZ '1960-01-01 13:24:35.123456789');
+
+-- A source zone with a sub-hour offset (Asia/Kolkata is UTC+05:30) shifts the 
minute field,
+-- while the second field (including the nanosecond fraction) is 
offset-invariant.
+SELECT extract(MINUTE FROM TIMESTAMP_LTZ '2020-01-01 13:24:35.123456789 
Asia/Kolkata');
+SELECT extract(SECOND FROM TIMESTAMP_LTZ '2020-01-01 13:24:35.123456789 
Asia/Kolkata');
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 0ba10ff1ff06..e95530669277 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
@@ -46,3 +46,22 @@ SELECT hour(NULL :: timestamp_ntz(9));
 SELECT hour(TIMESTAMP_NTZ '1960-01-01 13:24:35.123456789');
 SELECT minute(TIMESTAMP_NTZ '1960-01-01 13:24:35.123456789');
 SELECT second(TIMESTAMP_NTZ '1960-01-01 13:24:35.123456789');
+
+-- EXTRACT / date_part over nanosecond-precision values (SPARK-57340). HOUR 
and MINUTE are
+-- equivalent to the hour()/minute() functions; SECOND keeps the 
sub-microsecond digits and
+-- widens the result to DECIMAL(11, 9).
+SELECT extract(HOUR FROM TIMESTAMP_NTZ '2020-01-01 13:24:35.123456789');
+SELECT extract(MINUTE FROM TIMESTAMP_NTZ '2020-01-01 13:24:35.123456789');
+SELECT extract(SECOND FROM TIMESTAMP_NTZ '2020-01-01 13:24:35.123456789');
+SELECT date_part('HOUR', TIMESTAMP_NTZ '2020-01-01 13:24:35.123456789');
+SELECT date_part('MINUTE', TIMESTAMP_NTZ '2020-01-01 13:24:35.123456789');
+SELECT date_part('SECOND', TIMESTAMP_NTZ '2020-01-01 13:24:35.123456789');
+
+-- Digits below the type's precision are floored at the type boundary, so they 
read back as
+-- zeros in the DECIMAL(11, 9) result.
+SELECT extract(SECOND FROM '2020-01-01 13:24:35.999999999' :: 
timestamp_ntz(7));
+SELECT extract(SECOND FROM '2020-01-01 13:24:35.999999999' :: 
timestamp_ntz(8));
+SELECT extract(SECOND FROM NULL :: timestamp_ntz(9));
+
+-- Pre-epoch nanosecond values exercise the negative-epoch path.
+SELECT extract(SECOND FROM TIMESTAMP_NTZ '1960-01-01 13:24:35.123456789');
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 aa0d6cc4c187..86e4459072fb 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
@@ -253,3 +253,99 @@ SELECT second(TIMESTAMP_LTZ '2020-01-01 13:24:35.123456789 
UTC')
 struct<second(TimestampNanosVal(1577885075123456, 789)):int>
 -- !query output
 35
+
+
+-- !query
+SELECT extract(HOUR FROM TIMESTAMP_LTZ '2020-01-01 13:24:35.123456789')
+-- !query schema
+struct<extract(HOUR FROM TimestampNanosVal(1577913875123456, 789)):int>
+-- !query output
+13
+
+
+-- !query
+SELECT extract(MINUTE FROM TIMESTAMP_LTZ '2020-01-01 13:24:35.123456789')
+-- !query schema
+struct<extract(MINUTE FROM TimestampNanosVal(1577913875123456, 789)):int>
+-- !query output
+24
+
+
+-- !query
+SELECT extract(SECOND FROM TIMESTAMP_LTZ '2020-01-01 13:24:35.123456789')
+-- !query schema
+struct<extract(SECOND FROM TimestampNanosVal(1577913875123456, 
789)):decimal(11,9)>
+-- !query output
+35.123456789
+
+
+-- !query
+SELECT date_part('HOUR', TIMESTAMP_LTZ '2020-01-01 13:24:35.123456789')
+-- !query schema
+struct<date_part(HOUR, TimestampNanosVal(1577913875123456, 789)):int>
+-- !query output
+13
+
+
+-- !query
+SELECT date_part('MINUTE', TIMESTAMP_LTZ '2020-01-01 13:24:35.123456789')
+-- !query schema
+struct<date_part(MINUTE, TimestampNanosVal(1577913875123456, 789)):int>
+-- !query output
+24
+
+
+-- !query
+SELECT date_part('SECOND', TIMESTAMP_LTZ '2020-01-01 13:24:35.123456789')
+-- !query schema
+struct<date_part(SECOND, TimestampNanosVal(1577913875123456, 
789)):decimal(11,9)>
+-- !query output
+35.123456789
+
+
+-- !query
+SELECT extract(SECOND FROM '2020-01-01 13:24:35.999999999' :: timestamp_ltz(7))
+-- !query schema
+struct<extract(SECOND FROM CAST(2020-01-01 13:24:35.999999999 AS 
TIMESTAMP_LTZ(7))):decimal(11,9)>
+-- !query output
+35.999999900
+
+
+-- !query
+SELECT extract(SECOND FROM '2020-01-01 13:24:35.999999999' :: timestamp_ltz(8))
+-- !query schema
+struct<extract(SECOND FROM CAST(2020-01-01 13:24:35.999999999 AS 
TIMESTAMP_LTZ(8))):decimal(11,9)>
+-- !query output
+35.999999990
+
+
+-- !query
+SELECT extract(SECOND FROM NULL :: timestamp_ltz(9))
+-- !query schema
+struct<extract(SECOND FROM CAST(NULL AS TIMESTAMP_LTZ(9))):decimal(11,9)>
+-- !query output
+NULL
+
+
+-- !query
+SELECT extract(SECOND FROM TIMESTAMP_LTZ '1960-01-01 13:24:35.123456789')
+-- !query schema
+struct<extract(SECOND FROM TimestampNanosVal(-315542124876544, 
789)):decimal(11,9)>
+-- !query output
+35.123456789
+
+
+-- !query
+SELECT extract(MINUTE FROM TIMESTAMP_LTZ '2020-01-01 13:24:35.123456789 
Asia/Kolkata')
+-- !query schema
+struct<extract(MINUTE FROM TimestampNanosVal(1577865275123456, 789)):int>
+-- !query output
+54
+
+
+-- !query
+SELECT extract(SECOND FROM TIMESTAMP_LTZ '2020-01-01 13:24:35.123456789 
Asia/Kolkata')
+-- !query schema
+struct<extract(SECOND FROM TimestampNanosVal(1577865275123456, 
789)):decimal(11,9)>
+-- !query output
+35.123456789
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 25bb7a9322a8..c1aa8b2c2ffb 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
@@ -205,3 +205,83 @@ SELECT second(TIMESTAMP_NTZ '1960-01-01 
13:24:35.123456789')
 struct<second(TimestampNanosVal(-315570924876544, 789)):int>
 -- !query output
 35
+
+
+-- !query
+SELECT extract(HOUR FROM TIMESTAMP_NTZ '2020-01-01 13:24:35.123456789')
+-- !query schema
+struct<extract(HOUR FROM TimestampNanosVal(1577885075123456, 789)):int>
+-- !query output
+13
+
+
+-- !query
+SELECT extract(MINUTE FROM TIMESTAMP_NTZ '2020-01-01 13:24:35.123456789')
+-- !query schema
+struct<extract(MINUTE FROM TimestampNanosVal(1577885075123456, 789)):int>
+-- !query output
+24
+
+
+-- !query
+SELECT extract(SECOND FROM TIMESTAMP_NTZ '2020-01-01 13:24:35.123456789')
+-- !query schema
+struct<extract(SECOND FROM TimestampNanosVal(1577885075123456, 
789)):decimal(11,9)>
+-- !query output
+35.123456789
+
+
+-- !query
+SELECT date_part('HOUR', TIMESTAMP_NTZ '2020-01-01 13:24:35.123456789')
+-- !query schema
+struct<date_part(HOUR, TimestampNanosVal(1577885075123456, 789)):int>
+-- !query output
+13
+
+
+-- !query
+SELECT date_part('MINUTE', TIMESTAMP_NTZ '2020-01-01 13:24:35.123456789')
+-- !query schema
+struct<date_part(MINUTE, TimestampNanosVal(1577885075123456, 789)):int>
+-- !query output
+24
+
+
+-- !query
+SELECT date_part('SECOND', TIMESTAMP_NTZ '2020-01-01 13:24:35.123456789')
+-- !query schema
+struct<date_part(SECOND, TimestampNanosVal(1577885075123456, 
789)):decimal(11,9)>
+-- !query output
+35.123456789
+
+
+-- !query
+SELECT extract(SECOND FROM '2020-01-01 13:24:35.999999999' :: timestamp_ntz(7))
+-- !query schema
+struct<extract(SECOND FROM CAST(2020-01-01 13:24:35.999999999 AS 
TIMESTAMP_NTZ(7))):decimal(11,9)>
+-- !query output
+35.999999900
+
+
+-- !query
+SELECT extract(SECOND FROM '2020-01-01 13:24:35.999999999' :: timestamp_ntz(8))
+-- !query schema
+struct<extract(SECOND FROM CAST(2020-01-01 13:24:35.999999999 AS 
TIMESTAMP_NTZ(8))):decimal(11,9)>
+-- !query output
+35.999999990
+
+
+-- !query
+SELECT extract(SECOND FROM NULL :: timestamp_ntz(9))
+-- !query schema
+struct<extract(SECOND FROM CAST(NULL AS TIMESTAMP_NTZ(9))):decimal(11,9)>
+-- !query output
+NULL
+
+
+-- !query
+SELECT extract(SECOND FROM TIMESTAMP_NTZ '1960-01-01 13:24:35.123456789')
+-- !query schema
+struct<extract(SECOND FROM TimestampNanosVal(-315570924876544, 
789)):decimal(11,9)>
+-- !query output
+35.123456789
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 0bf45b63f935..8031ed5cf7c2 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
@@ -98,6 +98,54 @@ abstract class TimestampNanosFunctionsSuiteBase extends 
SharedSparkSession {
       df.selectExpr("hour(ntz)", "minute(ntz)", "second(ntz)"),
       Row(5, 6, 7))
   }
+
+  test("SPARK-57340: extract/date_part HOUR and MINUTE over 
nanosecond-precision timestamps") {
+    Seq(7, 8, 9).foreach { p =>
+      val df = nanosDF(p)
+      val result1 = df.selectExpr(
+        "extract(HOUR FROM ntz)", "extract(MINUTE FROM ntz)",
+        "extract(HOUR FROM ltz)", "extract(MINUTE FROM ltz)")
+      val result2 = df.selectExpr(
+        "date_part('HOUR', ntz)", "date_part('MINUTE', ntz)",
+        "date_part('HOUR', ltz)", "date_part('MINUTE', ltz)")
+      val result3 = df.select(
+        extract(lit("HOUR"), col("ntz")), extract(lit("MINUTE"), col("ntz")),
+        extract(lit("HOUR"), col("ltz")), extract(lit("MINUTE"), col("ltz")))
+      checkAnswer(result1, result2)
+      checkAnswer(result1, result3)
+      checkAnswer(result1, Seq(Row(13, 24, 13, 24), Row(null, null, null, 
null)))
+    }
+  }
+
+  test("SPARK-57340: extract/date_part SECOND keeps the nanosecond fraction") {
+    // EXTRACT(SECOND) widens the result to DECIMAL(11, 9); digits below the 
type's precision
+    // `p` were floored when the values were created, so they read back as 
zeros.
+    Seq(
+      7 -> ("35.123456700", "35.987654300"),
+      8 -> ("35.123456780", "35.987654320"),
+      9 -> ("35.123456789", "35.987654321")
+    ).foreach { case (p, (ntzSec, ltzSec)) =>
+      val df = nanosDF(p)
+      val result1 = df.selectExpr("extract(SECOND FROM ntz)", "extract(SECOND 
FROM ltz)")
+      val result2 = df.selectExpr("date_part('SECOND', ntz)", 
"date_part('SECOND', ltz)")
+      val result3 = df.select(
+        extract(lit("SECOND"), col("ntz")), extract(lit("SECOND"), col("ltz")))
+      checkAnswer(result1, result2)
+      checkAnswer(result1, result3)
+      checkAnswer(result1, Seq(
+        Row(new java.math.BigDecimal(ntzSec), new 
java.math.BigDecimal(ltzSec)),
+        Row(null, null)))
+    }
+  }
+
+  test("SPARK-57340: extract SECOND over pre-epoch nanosecond TIMESTAMP_NTZ") {
+    val schema = new StructType().add("ntz", TimestampNTZNanosType(9))
+    val data = Seq(Row(LocalDateTime.parse("1960-01-01T05:06:07.123456789")))
+    val df = spark.createDataFrame(spark.sparkContext.parallelize(data), 
schema)
+    checkAnswer(
+      df.selectExpr("extract(SECOND FROM ntz)"),
+      Row(new java.math.BigDecimal("7.123456789")))
+  }
 }
 
 // Runs the nanosecond timestamp function tests with ANSI mode enabled 
explicitly.


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to