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

MaxGekk pushed a commit to branch branch-4.x
in repository https://gitbox.apache.org/repos/asf/spark.git


The following commit(s) were added to refs/heads/branch-4.x by this push:
     new e09a4fb15a45 [SPARK-57315][SQL] Support HOUR, MINUTE and SECOND 
functions over nanosecond-precision timestamps
e09a4fb15a45 is described below

commit e09a4fb15a451cb00c266af30df540126b6fe3ab
Author: Maxim Gekk <[email protected]>
AuthorDate: Wed Jun 10 13:36:28 2026 +0200

    [SPARK-57315][SQL] Support HOUR, MINUTE and SECOND functions over 
nanosecond-precision timestamps
    
    ### What changes were proposed in this pull request?
    
    This PR lets `hour()`, `minute()` and `second()` operate on the 
nanosecond-precision timestamp types `TIMESTAMP_NTZ(p)` / `TIMESTAMP_LTZ(p)` 
(`p` in `[7, 9]`), the preview types under the nanosecond timestamp umbrella 
(SPARK-56822).
    
    It extends the function expression builders `HourExpressionBuilder`, 
`MinuteExpressionBuilder` and `SecondExpressionBuilder`. When the argument is a 
nanosecond-precision timestamp, the builder casts it down to the matching 
microsecond timestamp type and reuses the existing `Hour` / `Minute` / `Second` 
expressions:
    
    - `TimestampNTZNanosType(p)` -> `TimestampNTZType`
    - `TimestampLTZNanosType(p)` -> `TimestampType`
    
    The cast keeps `epochMicros` and drops the sub-microsecond digits, which is 
lossless for these integer time-of-day fields. The conversion is centralized in 
a small `NanosTimestampCast.castToMicros` helper. `SecondWithFraction` (the 
`extract(SECOND)` path returning `DECIMAL(8, 6)`) is intentionally not covered 
because its result depends on the sub-microsecond digits.
    
    This is an alternative implementation to #56366, which addresses the same 
JIRA with a dedicated analyzer rule (`ResolveTimestampNanosExpressions`) 
instead of changing the builders.
    
    Closes #56366
    
    ### Why are the changes needed?
    
    To support `HOUR` / `MINUTE` / `SECOND` over the nanosecond timestamp types 
as part of the nanosecond timestamp preview (SPARK-56822), reusing the existing 
microsecond expressions rather than introducing new ones.
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes, but only when the preview flag `spark.sql.timestampNanosTypes.enabled` 
is set to `true` (default `false`). With the flag on, `hour`/`minute`/`second` 
now accept `TIMESTAMP_NTZ(p)` / `TIMESTAMP_LTZ(p)` arguments; previously they 
failed analysis with a type-check error. Behavior for microsecond `TIMESTAMP` / 
`TIMESTAMP_NTZ`, `DATE`, strings, and `TIME` is unchanged.
    
    Example (flag enabled):
    ```sql
    SELECT hour(TIMESTAMP_NTZ '2018-02-14 12:58:59.123456789'); -- 12
    SELECT minute(TIMESTAMP_LTZ '2009-07-30 12:58:59.123456789'); -- 58
    ```
    
    ### How was this patch tested?
    
    - New end-to-end golden tests in `timestamp-ntz-nanos.sql` / 
`timestamp-ltz-nanos.sql` covering: full nanosecond precision (`p = 9`, via 
typed literals), explicit precision via `::` casts (`p = 7/8`), NULLs, 
pre-epoch values (negative-epoch path), and `TIMESTAMP_LTZ` extraction across 
source time zones (`Asia/Kolkata`, `UTC`) while the session zone is 
`America/Los_Angeles`.
    - Added runnable nanosecond-literal examples to the 
`hour`/`minute`/`second` function docs, validated by `ExpressionInfoSuite` 
("check outputs of expression examples") and `ExpressionsSchemaSuite`.
    - Updated the `hour`/`minute`/`second` scaladoc in `functions.scala`.
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    Generated-by: Cursor (Claude Opus 4.8)
    
    Closes #56368 from MaxGekk/nanos-hour-builders.
    
    Authored-by: Maxim Gekk <[email protected]>
    Signed-off-by: Max Gekk <[email protected]>
    (cherry picked from commit 19aec7afac4d32d36b46da0186426b0a4e4490bf)
    Signed-off-by: Max Gekk <[email protected]>
---
 .../scala/org/apache/spark/sql/functions.scala     |  12 ++-
 .../sql/catalyst/expressions/timeExpressions.scala |  53 ++++++++-
 .../analyzer-results/timestamp-ltz-nanos.sql.out   | 105 ++++++++++++++++++
 .../analyzer-results/timestamp-ntz-nanos.sql.out   |  63 +++++++++++
 .../sql-tests/inputs/timestamp-ltz-nanos.sql       |  25 +++++
 .../sql-tests/inputs/timestamp-ntz-nanos.sql       |  16 +++
 .../sql-tests/results/timestamp-ltz-nanos.sql.out  | 120 +++++++++++++++++++++
 .../sql-tests/results/timestamp-ntz-nanos.sql.out  |  72 +++++++++++++
 .../sql/TimestampNanosFunctionsSuiteBase.scala     | 111 +++++++++++++++++++
 9 files changed, 570 insertions(+), 7 deletions(-)

diff --git a/sql/api/src/main/scala/org/apache/spark/sql/functions.scala 
b/sql/api/src/main/scala/org/apache/spark/sql/functions.scala
index ab21a784796d..fe91d0bd7b69 100644
--- a/sql/api/src/main/scala/org/apache/spark/sql/functions.scala
+++ b/sql/api/src/main/scala/org/apache/spark/sql/functions.scala
@@ -7726,7 +7726,9 @@ object functions {
   def dayofyear(e: Column): Column = Column.fn("dayofyear", e)
 
   /**
-   * Extracts the hours as an integer from a given date/time/timestamp/string.
+   * Extracts the hours as an integer from a given date/time/timestamp/string. 
The input may also
+   * be a nanosecond-precision timestamp `TIMESTAMP_NTZ(p)` or 
`TIMESTAMP_LTZ(p)` (`p` in
+   * `[7, 9]`, since 4.3.0), in which case the sub-microsecond digits are 
ignored.
    * @return
    *   An integer, or null if the input was a string that could not be cast to 
a date
    * @group datetime_funcs
@@ -7799,7 +7801,9 @@ object functions {
   def last_day(e: Column): Column = Column.fn("last_day", e)
 
   /**
-   * Extracts the minutes as an integer from a given 
date/time/timestamp/string.
+   * Extracts the minutes as an integer from a given 
date/time/timestamp/string. The input may
+   * also be a nanosecond-precision timestamp `TIMESTAMP_NTZ(p)` or 
`TIMESTAMP_LTZ(p)` (`p` in
+   * `[7, 9]`, since 4.3.0), in which case the sub-microsecond digits are 
ignored.
    * @return
    *   An integer, or null if the input was a string that could not be cast to 
a date
    * @group datetime_funcs
@@ -7905,7 +7909,9 @@ object functions {
     Column.fn("next_day", date, dayOfWeek)
 
   /**
-   * Extracts the seconds as an integer from a given 
date/time/timestamp/string.
+   * Extracts the seconds as an integer from a given 
date/time/timestamp/string. The input may
+   * also be a nanosecond-precision timestamp `TIMESTAMP_NTZ(p)` or 
`TIMESTAMP_LTZ(p)` (`p` in
+   * `[7, 9]`, since 4.3.0), in which case the sub-microsecond digits are 
ignored.
    * @return
    *   An integer, or null if the input was a string that could not be cast to 
a timestamp
    * @group datetime_funcs
diff --git 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/timeExpressions.scala
 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/timeExpressions.scala
index 6469a70349a7..85afdf72eecc 100644
--- 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/timeExpressions.scala
+++ 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/timeExpressions.scala
@@ -34,7 +34,7 @@ import 
org.apache.spark.sql.catalyst.util.TypeUtils.ordinalNumber
 import org.apache.spark.sql.errors.{QueryCompilationErrors, 
QueryExecutionErrors}
 import org.apache.spark.sql.internal.SQLConf
 import org.apache.spark.sql.internal.types.StringTypeWithCollation
-import org.apache.spark.sql.types.{AbstractDataType, AnyTimeType, ByteType, 
DataType, DayTimeIntervalType, DecimalType, IntegerType, IntegralType, 
LongType, NumericType, ObjectType, TimeType}
+import org.apache.spark.sql.types.{AbstractDataType, AnyTimeType, ByteType, 
DataType, DayTimeIntervalType, DecimalType, IntegerType, IntegralType, 
LongType, NumericType, ObjectType, TimestampLTZNanosType, 
TimestampNTZNanosType, TimestampNTZType, TimestampType, TimeType}
 import org.apache.spark.sql.types.DayTimeIntervalType.{HOUR, SECOND}
 import org.apache.spark.unsafe.types.UTF8String
 
@@ -157,6 +157,24 @@ object TimePart {
     }
 }
 
+private[expressions] object NanosTimestampCast {
+  /**
+   * Casts a nanosecond-precision timestamp (`TIMESTAMP_NTZ(p)` / 
`TIMESTAMP_LTZ(p)`, `p` in
+   * `[7, 9]`) down to the matching microsecond timestamp type so that 
expressions accepting only
+   * the microsecond timestamp types can reuse it:
+   *   - TimestampNTZNanosType(p) -> TimestampNTZType
+   *   - TimestampLTZNanosType(p) -> TimestampType
+   *
+   * The cast keeps `epochMicros` and drops the sub-microsecond digits, which 
is lossless for the
+   * integer time-of-day fields (hour/minute/second). Inputs of any other type 
are returned as is.
+   */
+  def castToMicros(child: Expression): Expression = child.dataType match {
+    case _: TimestampNTZNanosType => Cast(child, TimestampNTZType)
+    case _: TimestampLTZNanosType => Cast(child, TimestampType)
+    case _ => child
+  }
+}
+
 /**
  * * Parses a column to a time based on the supplied format.
  */
@@ -240,12 +258,19 @@ case class MinutesOfTime(child: Expression)
 
     If `expr` is a TIMESTAMP or a string that can be cast to timestamp,
     it returns the minute of that timestamp.
+    If `expr` is a nanosecond-precision timestamp TIMESTAMP_NTZ(p) or 
TIMESTAMP_LTZ(p)
+    with p in [7, 9] (since 4.3.0), it returns the minute of that timestamp, 
ignoring the
+    sub-microsecond digits.
     If `expr` is a TIME type (since 4.1.0), it returns the minute of the 
time-of-day.
   """,
   examples = """
     Examples:
       > SELECT _FUNC_('2009-07-30 12:58:59');
        58
+      > SELECT _FUNC_(TIMESTAMP_NTZ '2009-07-30 12:58:59.123456789');
+       58
+      > SELECT _FUNC_(TIMESTAMP_LTZ '2009-07-30 12:58:59.123456789');
+       58
       > SELECT _FUNC_(TIME'23:59:59.999999');
        59
   """,
@@ -262,7 +287,9 @@ object MinuteExpressionBuilder extends ExpressionBuilder {
         case _: TimeType =>
           MinutesOfTime(child)
         case _ =>
-          Minute(child)
+          // Casts nanosecond-precision timestamps down to the microsecond 
timestamp type;
+          // other types are passed through unchanged.
+          Minute(NanosTimestampCast.castToMicros(child))
       }
     }
   }
@@ -298,12 +325,19 @@ case class HoursOfTime(child: Expression)
 
     If `expr` is a TIMESTAMP or a string that can be cast to timestamp,
     it returns the hour of that timestamp.
+    If `expr` is a nanosecond-precision timestamp TIMESTAMP_NTZ(p) or 
TIMESTAMP_LTZ(p)
+    with p in [7, 9] (since 4.3.0), it returns the hour of that timestamp, 
ignoring the
+    sub-microsecond digits.
     If `expr` is a TIME type (since 4.1.0), it returns the hour of the 
time-of-day.
   """,
   examples = """
     Examples:
       > SELECT _FUNC_('2018-02-14 12:58:59');
        12
+      > SELECT _FUNC_(TIMESTAMP_NTZ '2018-02-14 12:58:59.123456789');
+       12
+      > SELECT _FUNC_(TIMESTAMP_LTZ '2018-02-14 12:58:59.123456789');
+       12
       > SELECT _FUNC_(TIME'13:59:59.999999');
        13
   """,
@@ -319,7 +353,9 @@ object HourExpressionBuilder extends ExpressionBuilder {
         case _: TimeType =>
           HoursOfTime(child)
         case _ =>
-          Hour(child)
+          // Casts nanosecond-precision timestamps down to the microsecond 
timestamp type;
+          // other types are passed through unchanged.
+          Hour(NanosTimestampCast.castToMicros(child))
       }
     }
   }
@@ -381,12 +417,19 @@ case class SecondsOfTime(child: Expression)
 
     If `expr` is a TIMESTAMP or a string that can be cast to timestamp,
     it returns the second of that timestamp.
+    If `expr` is a nanosecond-precision timestamp TIMESTAMP_NTZ(p) or 
TIMESTAMP_LTZ(p)
+    with p in [7, 9] (since 4.3.0), it returns the second of that timestamp, 
ignoring the
+    sub-microsecond digits.
     If `expr` is a TIME type (since 4.1.0), it returns the second of the 
time-of-day.
   """,
   examples = """
     Examples:
       > SELECT _FUNC_('2018-02-14 12:58:59');
        59
+      > SELECT _FUNC_(TIMESTAMP_NTZ '2018-02-14 12:58:59.123456789');
+       59
+      > SELECT _FUNC_(TIMESTAMP_LTZ '2018-02-14 12:58:59.123456789');
+       59
       > SELECT _FUNC_(TIME'13:25:59.999999');
        59
   """,
@@ -402,7 +445,9 @@ object SecondExpressionBuilder extends ExpressionBuilder {
         case _: TimeType =>
           SecondsOfTime(child)
         case _ =>
-          Second(child)
+          // Casts nanosecond-precision timestamps down to the microsecond 
timestamp type;
+          // other types are passed through unchanged.
+          Second(NanosTimestampCast.castToMicros(child))
       }
     }
   }
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 6f896ab8014c..fa8c562c9416 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
@@ -116,3 +116,108 @@ SELECT named_struct('f', CAST(NULL AS timestamp_ltz(9)))
 -- !query analysis
 Project [named_struct(f, cast(null as timestamp_ltz(9))) AS named_struct(f, 
CAST(NULL AS TIMESTAMP_LTZ(9)))#x]
 +- OneRowRelation
+
+
+-- !query
+SELECT hour(TIMESTAMP_LTZ '2020-01-01 13:24:35.123456789')
+-- !query analysis
+Project [hour(cast(TimestampNanosVal(1577913875123456, 789) as timestamp), 
Some(America/Los_Angeles)) AS hour(TimestampNanosVal(1577913875123456, 789))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT minute(TIMESTAMP_LTZ '2020-01-01 13:24:35.123456789')
+-- !query analysis
+Project [minute(cast(TimestampNanosVal(1577913875123456, 789) as timestamp), 
Some(America/Los_Angeles)) AS minute(TimestampNanosVal(1577913875123456, 
789))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT second(TIMESTAMP_LTZ '2020-01-01 13:24:35.123456789')
+-- !query analysis
+Project [second(cast(TimestampNanosVal(1577913875123456, 789) as timestamp), 
Some(America/Los_Angeles)) AS second(TimestampNanosVal(1577913875123456, 
789))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT hour('2020-01-01 13:24:35.999999999' :: timestamp_ltz(7))
+-- !query analysis
+Project [hour(cast(cast(2020-01-01 13:24:35.999999999 as timestamp_ltz(7)) as 
timestamp), Some(America/Los_Angeles)) AS hour(CAST(2020-01-01 
13:24:35.999999999 AS TIMESTAMP_LTZ(7)))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT second('2020-01-01 13:24:35.999999999' :: timestamp_ltz(8))
+-- !query analysis
+Project [second(cast(cast(2020-01-01 13:24:35.999999999 as timestamp_ltz(8)) 
as timestamp), Some(America/Los_Angeles)) AS second(CAST(2020-01-01 
13:24:35.999999999 AS TIMESTAMP_LTZ(8)))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT hour(NULL :: timestamp_ltz(9))
+-- !query analysis
+Project [hour(cast(cast(null as timestamp_ltz(9)) as timestamp), 
Some(America/Los_Angeles)) AS hour(CAST(NULL AS TIMESTAMP_LTZ(9)))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT hour(TIMESTAMP_LTZ '1960-01-01 13:24:35.123456789')
+-- !query analysis
+Project [hour(cast(TimestampNanosVal(-315542124876544, 789) as timestamp), 
Some(America/Los_Angeles)) AS hour(TimestampNanosVal(-315542124876544, 789))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT minute(TIMESTAMP_LTZ '1960-01-01 13:24:35.123456789')
+-- !query analysis
+Project [minute(cast(TimestampNanosVal(-315542124876544, 789) as timestamp), 
Some(America/Los_Angeles)) AS minute(TimestampNanosVal(-315542124876544, 
789))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT second(TIMESTAMP_LTZ '1960-01-01 13:24:35.123456789')
+-- !query analysis
+Project [second(cast(TimestampNanosVal(-315542124876544, 789) as timestamp), 
Some(America/Los_Angeles)) AS second(TimestampNanosVal(-315542124876544, 
789))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT hour(TIMESTAMP_LTZ '2020-01-01 13:24:35.123456789 Asia/Kolkata')
+-- !query analysis
+Project [hour(cast(TimestampNanosVal(1577865275123456, 789) as timestamp), 
Some(America/Los_Angeles)) AS hour(TimestampNanosVal(1577865275123456, 789))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT minute(TIMESTAMP_LTZ '2020-01-01 13:24:35.123456789 Asia/Kolkata')
+-- !query analysis
+Project [minute(cast(TimestampNanosVal(1577865275123456, 789) as timestamp), 
Some(America/Los_Angeles)) AS minute(TimestampNanosVal(1577865275123456, 
789))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT second(TIMESTAMP_LTZ '2020-01-01 13:24:35.123456789 Asia/Kolkata')
+-- !query analysis
+Project [second(cast(TimestampNanosVal(1577865275123456, 789) as timestamp), 
Some(America/Los_Angeles)) AS second(TimestampNanosVal(1577865275123456, 
789))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT hour(TIMESTAMP_LTZ '2020-01-01 13:24:35.123456789 UTC')
+-- !query analysis
+Project [hour(cast(TimestampNanosVal(1577885075123456, 789) as timestamp), 
Some(America/Los_Angeles)) AS hour(TimestampNanosVal(1577885075123456, 789))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT minute(TIMESTAMP_LTZ '2020-01-01 13:24:35.123456789 UTC')
+-- !query analysis
+Project [minute(cast(TimestampNanosVal(1577885075123456, 789) as timestamp), 
Some(America/Los_Angeles)) AS minute(TimestampNanosVal(1577885075123456, 
789))#x]
++- OneRowRelation
+
+
+-- !query
+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
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 94570f139e84..97bcd3b290a0 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
@@ -116,3 +116,66 @@ SELECT named_struct('f', CAST(NULL AS timestamp_ntz(9)))
 -- !query analysis
 Project [named_struct(f, cast(null as timestamp_ntz(9))) AS named_struct(f, 
CAST(NULL AS TIMESTAMP_NTZ(9)))#x]
 +- OneRowRelation
+
+
+-- !query
+SELECT hour(TIMESTAMP_NTZ '2020-01-01 13:24:35.123456789')
+-- !query analysis
+Project [hour(cast(TimestampNanosVal(1577885075123456, 789) as timestamp_ntz), 
Some(America/Los_Angeles)) AS hour(TimestampNanosVal(1577885075123456, 789))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT minute(TIMESTAMP_NTZ '2020-01-01 13:24:35.123456789')
+-- !query analysis
+Project [minute(cast(TimestampNanosVal(1577885075123456, 789) as 
timestamp_ntz), Some(America/Los_Angeles)) AS 
minute(TimestampNanosVal(1577885075123456, 789))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT second(TIMESTAMP_NTZ '2020-01-01 13:24:35.123456789')
+-- !query analysis
+Project [second(cast(TimestampNanosVal(1577885075123456, 789) as 
timestamp_ntz), Some(America/Los_Angeles)) AS 
second(TimestampNanosVal(1577885075123456, 789))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT hour('2020-01-01 13:24:35.999999999' :: timestamp_ntz(7))
+-- !query analysis
+Project [hour(cast(cast(2020-01-01 13:24:35.999999999 as timestamp_ntz(7)) as 
timestamp_ntz), Some(America/Los_Angeles)) AS hour(CAST(2020-01-01 
13:24:35.999999999 AS TIMESTAMP_NTZ(7)))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT second('2020-01-01 13:24:35.999999999' :: timestamp_ntz(8))
+-- !query analysis
+Project [second(cast(cast(2020-01-01 13:24:35.999999999 as timestamp_ntz(8)) 
as timestamp_ntz), Some(America/Los_Angeles)) AS second(CAST(2020-01-01 
13:24:35.999999999 AS TIMESTAMP_NTZ(8)))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT hour(NULL :: timestamp_ntz(9))
+-- !query analysis
+Project [hour(cast(cast(null as timestamp_ntz(9)) as timestamp_ntz), 
Some(America/Los_Angeles)) AS hour(CAST(NULL AS TIMESTAMP_NTZ(9)))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT hour(TIMESTAMP_NTZ '1960-01-01 13:24:35.123456789')
+-- !query analysis
+Project [hour(cast(TimestampNanosVal(-315570924876544, 789) as timestamp_ntz), 
Some(America/Los_Angeles)) AS hour(TimestampNanosVal(-315570924876544, 789))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT minute(TIMESTAMP_NTZ '1960-01-01 13:24:35.123456789')
+-- !query analysis
+Project [minute(cast(TimestampNanosVal(-315570924876544, 789) as 
timestamp_ntz), Some(America/Los_Angeles)) AS 
minute(TimestampNanosVal(-315570924876544, 789))#x]
++- OneRowRelation
+
+
+-- !query
+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
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 f7c36256a6be..b318ad3021bc 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
@@ -30,3 +30,28 @@ SELECT CAST(NULL AS timestamp_ltz(9));
 SELECT array(CAST(NULL AS timestamp_ltz(9)));
 SELECT map('k', CAST(NULL AS timestamp_ltz(9)));
 SELECT named_struct('f', CAST(NULL AS timestamp_ltz(9)));
+
+-- HOUR/MINUTE/SECOND over nanosecond-precision values (SPARK-57315). LTZ 
extracts in the
+-- session time zone; the sub-microsecond digits never affect the integer 
field.
+SELECT hour(TIMESTAMP_LTZ '2020-01-01 13:24:35.123456789');
+SELECT minute(TIMESTAMP_LTZ '2020-01-01 13:24:35.123456789');
+SELECT second(TIMESTAMP_LTZ '2020-01-01 13:24:35.123456789');
+SELECT hour('2020-01-01 13:24:35.999999999' :: timestamp_ltz(7));
+SELECT second('2020-01-01 13:24:35.999999999' :: timestamp_ltz(8));
+SELECT hour(NULL :: timestamp_ltz(9));
+
+-- Pre-epoch nanosecond values exercise the negative-epoch path; 
HOUR/MINUTE/SECOND
+-- read the wall-clock fields in the session time zone.
+SELECT hour(TIMESTAMP_LTZ '1960-01-01 13:24:35.123456789');
+SELECT minute(TIMESTAMP_LTZ '1960-01-01 13:24:35.123456789');
+SELECT second(TIMESTAMP_LTZ '1960-01-01 13:24:35.123456789');
+
+-- LTZ nanos: the literal's time zone defines the instant, which is then 
extracted in the
+-- session time zone (America/Los_Angeles, UTC-08:00). A source zone with a 
sub-hour offset
+-- (Asia/Kolkata is UTC+05:30) shifts both the hour and the minute fields.
+SELECT hour(TIMESTAMP_LTZ '2020-01-01 13:24:35.123456789 Asia/Kolkata');
+SELECT minute(TIMESTAMP_LTZ '2020-01-01 13:24:35.123456789 Asia/Kolkata');
+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');
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 c1db88eb409b..0ba10ff1ff06 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
@@ -30,3 +30,19 @@ SELECT CAST(NULL AS timestamp_ntz(9));
 SELECT array(CAST(NULL AS timestamp_ntz(9)));
 SELECT map('k', CAST(NULL AS timestamp_ntz(9)));
 SELECT named_struct('f', CAST(NULL AS timestamp_ntz(9)));
+
+-- HOUR/MINUTE/SECOND over nanosecond-precision values (SPARK-57315). NTZ 
extracts the
+-- wall-clock components, so the result is zone-independent and the 
sub-microsecond digits
+-- never affect the integer field.
+SELECT hour(TIMESTAMP_NTZ '2020-01-01 13:24:35.123456789');
+SELECT minute(TIMESTAMP_NTZ '2020-01-01 13:24:35.123456789');
+SELECT second(TIMESTAMP_NTZ '2020-01-01 13:24:35.123456789');
+SELECT hour('2020-01-01 13:24:35.999999999' :: timestamp_ntz(7));
+SELECT second('2020-01-01 13:24:35.999999999' :: timestamp_ntz(8));
+SELECT hour(NULL :: timestamp_ntz(9));
+
+-- Pre-epoch nanosecond values exercise the negative-epoch path; 
HOUR/MINUTE/SECOND
+-- still read the wall-clock fields and remain zone-independent.
+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');
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 75171edef611..aa0d6cc4c187 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
@@ -133,3 +133,123 @@ SELECT named_struct('f', CAST(NULL AS timestamp_ltz(9)))
 struct<named_struct(f, CAST(NULL AS 
TIMESTAMP_LTZ(9))):struct<f:timestamp_ltz(9)>>
 -- !query output
 {"f":null}
+
+
+-- !query
+SELECT hour(TIMESTAMP_LTZ '2020-01-01 13:24:35.123456789')
+-- !query schema
+struct<hour(TimestampNanosVal(1577913875123456, 789)):int>
+-- !query output
+13
+
+
+-- !query
+SELECT minute(TIMESTAMP_LTZ '2020-01-01 13:24:35.123456789')
+-- !query schema
+struct<minute(TimestampNanosVal(1577913875123456, 789)):int>
+-- !query output
+24
+
+
+-- !query
+SELECT second(TIMESTAMP_LTZ '2020-01-01 13:24:35.123456789')
+-- !query schema
+struct<second(TimestampNanosVal(1577913875123456, 789)):int>
+-- !query output
+35
+
+
+-- !query
+SELECT hour('2020-01-01 13:24:35.999999999' :: timestamp_ltz(7))
+-- !query schema
+struct<hour(CAST(2020-01-01 13:24:35.999999999 AS TIMESTAMP_LTZ(7))):int>
+-- !query output
+13
+
+
+-- !query
+SELECT second('2020-01-01 13:24:35.999999999' :: timestamp_ltz(8))
+-- !query schema
+struct<second(CAST(2020-01-01 13:24:35.999999999 AS TIMESTAMP_LTZ(8))):int>
+-- !query output
+35
+
+
+-- !query
+SELECT hour(NULL :: timestamp_ltz(9))
+-- !query schema
+struct<hour(CAST(NULL AS TIMESTAMP_LTZ(9))):int>
+-- !query output
+NULL
+
+
+-- !query
+SELECT hour(TIMESTAMP_LTZ '1960-01-01 13:24:35.123456789')
+-- !query schema
+struct<hour(TimestampNanosVal(-315542124876544, 789)):int>
+-- !query output
+13
+
+
+-- !query
+SELECT minute(TIMESTAMP_LTZ '1960-01-01 13:24:35.123456789')
+-- !query schema
+struct<minute(TimestampNanosVal(-315542124876544, 789)):int>
+-- !query output
+24
+
+
+-- !query
+SELECT second(TIMESTAMP_LTZ '1960-01-01 13:24:35.123456789')
+-- !query schema
+struct<second(TimestampNanosVal(-315542124876544, 789)):int>
+-- !query output
+35
+
+
+-- !query
+SELECT hour(TIMESTAMP_LTZ '2020-01-01 13:24:35.123456789 Asia/Kolkata')
+-- !query schema
+struct<hour(TimestampNanosVal(1577865275123456, 789)):int>
+-- !query output
+23
+
+
+-- !query
+SELECT minute(TIMESTAMP_LTZ '2020-01-01 13:24:35.123456789 Asia/Kolkata')
+-- !query schema
+struct<minute(TimestampNanosVal(1577865275123456, 789)):int>
+-- !query output
+54
+
+
+-- !query
+SELECT second(TIMESTAMP_LTZ '2020-01-01 13:24:35.123456789 Asia/Kolkata')
+-- !query schema
+struct<second(TimestampNanosVal(1577865275123456, 789)):int>
+-- !query output
+35
+
+
+-- !query
+SELECT hour(TIMESTAMP_LTZ '2020-01-01 13:24:35.123456789 UTC')
+-- !query schema
+struct<hour(TimestampNanosVal(1577885075123456, 789)):int>
+-- !query output
+5
+
+
+-- !query
+SELECT minute(TIMESTAMP_LTZ '2020-01-01 13:24:35.123456789 UTC')
+-- !query schema
+struct<minute(TimestampNanosVal(1577885075123456, 789)):int>
+-- !query output
+24
+
+
+-- !query
+SELECT second(TIMESTAMP_LTZ '2020-01-01 13:24:35.123456789 UTC')
+-- !query schema
+struct<second(TimestampNanosVal(1577885075123456, 789)):int>
+-- !query output
+35
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 39542fdd121e..25bb7a9322a8 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
@@ -133,3 +133,75 @@ SELECT named_struct('f', CAST(NULL AS timestamp_ntz(9)))
 struct<named_struct(f, CAST(NULL AS 
TIMESTAMP_NTZ(9))):struct<f:timestamp_ntz(9)>>
 -- !query output
 {"f":null}
+
+
+-- !query
+SELECT hour(TIMESTAMP_NTZ '2020-01-01 13:24:35.123456789')
+-- !query schema
+struct<hour(TimestampNanosVal(1577885075123456, 789)):int>
+-- !query output
+13
+
+
+-- !query
+SELECT minute(TIMESTAMP_NTZ '2020-01-01 13:24:35.123456789')
+-- !query schema
+struct<minute(TimestampNanosVal(1577885075123456, 789)):int>
+-- !query output
+24
+
+
+-- !query
+SELECT second(TIMESTAMP_NTZ '2020-01-01 13:24:35.123456789')
+-- !query schema
+struct<second(TimestampNanosVal(1577885075123456, 789)):int>
+-- !query output
+35
+
+
+-- !query
+SELECT hour('2020-01-01 13:24:35.999999999' :: timestamp_ntz(7))
+-- !query schema
+struct<hour(CAST(2020-01-01 13:24:35.999999999 AS TIMESTAMP_NTZ(7))):int>
+-- !query output
+13
+
+
+-- !query
+SELECT second('2020-01-01 13:24:35.999999999' :: timestamp_ntz(8))
+-- !query schema
+struct<second(CAST(2020-01-01 13:24:35.999999999 AS TIMESTAMP_NTZ(8))):int>
+-- !query output
+35
+
+
+-- !query
+SELECT hour(NULL :: timestamp_ntz(9))
+-- !query schema
+struct<hour(CAST(NULL AS TIMESTAMP_NTZ(9))):int>
+-- !query output
+NULL
+
+
+-- !query
+SELECT hour(TIMESTAMP_NTZ '1960-01-01 13:24:35.123456789')
+-- !query schema
+struct<hour(TimestampNanosVal(-315570924876544, 789)):int>
+-- !query output
+13
+
+
+-- !query
+SELECT minute(TIMESTAMP_NTZ '1960-01-01 13:24:35.123456789')
+-- !query schema
+struct<minute(TimestampNanosVal(-315570924876544, 789)):int>
+-- !query output
+24
+
+
+-- !query
+SELECT second(TIMESTAMP_NTZ '1960-01-01 13:24:35.123456789')
+-- !query schema
+struct<second(TimestampNanosVal(-315570924876544, 789)):int>
+-- !query output
+35
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
new file mode 100644
index 000000000000..0bf45b63f935
--- /dev/null
+++ 
b/sql/core/src/test/scala/org/apache/spark/sql/TimestampNanosFunctionsSuiteBase.scala
@@ -0,0 +1,111 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.spark.sql
+
+import java.time.{Instant, LocalDateTime}
+
+import org.apache.spark.SparkConf
+import org.apache.spark.sql.functions._
+import org.apache.spark.sql.internal.SQLConf
+import org.apache.spark.sql.test.SharedSparkSession
+import org.apache.spark.sql.types._
+
+/**
+ * End-to-end tests for the `hour`, `minute` and `second` functions over the 
nanosecond-precision
+ * timestamp types `TIMESTAMP_NTZ(p)` / `TIMESTAMP_LTZ(p)` (`p` in `[7, 9]`), 
part of the
+ * nanosecond timestamp preview (SPARK-56822). Each test exercises both the 
SQL path
+ * (`selectExpr`) and the Scala `Column` API (`functions.hour` / `minute` / 
`second`).
+ */
+abstract class TimestampNanosFunctionsSuiteBase extends SharedSparkSession {
+
+  // The nanosecond timestamp types are gated behind a preview flag that is 
enabled by default
+  // under tests (Utils.isTesting), so it is not set here. The session time 
zone is fixed so that
+  // the TIMESTAMP_LTZ extraction below is deterministic.
+  override def sparkConf: SparkConf = super.sparkConf
+    .set(SQLConf.SESSION_LOCAL_TIMEZONE.key, "America/Los_Angeles")
+
+  // A DataFrame with one TIMESTAMP_NTZ(p) and one TIMESTAMP_LTZ(p) nanosecond 
column plus a NULL
+  // row. The wall-clock time of both non-null values is 13:24:35 in the 
session time zone, so the
+  // hour/minute/second fields match regardless of `p`; the sub-microsecond 
digits never affect the
+  // integer result.
+  private def nanosDF(precision: Int): DataFrame = {
+    val schema = new StructType()
+      .add("ntz", TimestampNTZNanosType(precision))
+      .add("ltz", TimestampLTZNanosType(precision))
+    val data = Seq(
+      Row(
+        // TIMESTAMP_NTZ is zone-independent: 13:24:35 wall-clock.
+        LocalDateTime.parse("2020-01-01T13:24:35.123456789"),
+        // TIMESTAMP_LTZ: 21:24:35 UTC -> 13:24:35 in America/Los_Angeles 
(UTC-8 in January).
+        Instant.parse("2020-01-01T21:24:35.987654321Z")),
+      Row(null, null))
+    spark.createDataFrame(spark.sparkContext.parallelize(data), schema)
+  }
+
+  test("SPARK-57315: hour over nanosecond-precision timestamps") {
+    Seq(7, 8, 9).foreach { p =>
+      val df = nanosDF(p)
+      val result1 = df.selectExpr("hour(ntz)", "hour(ltz)")
+      val result2 = df.select(hour(col("ntz")), hour(col("ltz")))
+      checkAnswer(result1, result2)
+      checkAnswer(result1, Seq(Row(13, 13), Row(null, null)))
+    }
+  }
+
+  test("SPARK-57315: minute over nanosecond-precision timestamps") {
+    Seq(7, 8, 9).foreach { p =>
+      val df = nanosDF(p)
+      val result1 = df.selectExpr("minute(ntz)", "minute(ltz)")
+      val result2 = df.select(minute(col("ntz")), minute(col("ltz")))
+      checkAnswer(result1, result2)
+      checkAnswer(result1, Seq(Row(24, 24), Row(null, null)))
+    }
+  }
+
+  test("SPARK-57315: second over nanosecond-precision timestamps") {
+    Seq(7, 8, 9).foreach { p =>
+      val df = nanosDF(p)
+      val result1 = df.selectExpr("second(ntz)", "second(ltz)")
+      val result2 = df.select(second(col("ntz")), second(col("ltz")))
+      checkAnswer(result1, result2)
+      checkAnswer(result1, Seq(Row(35, 35), Row(null, null)))
+    }
+  }
+
+  test("SPARK-57315: hour/minute/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.select(hour(col("ntz")), minute(col("ntz")), second(col("ntz"))),
+      Row(5, 6, 7))
+    checkAnswer(
+      df.selectExpr("hour(ntz)", "minute(ntz)", "second(ntz)"),
+      Row(5, 6, 7))
+  }
+}
+
+// Runs the nanosecond timestamp function tests with ANSI mode enabled 
explicitly.
+class TimestampNanosFunctionsAnsiOnSuite extends 
TimestampNanosFunctionsSuiteBase {
+  override def sparkConf: SparkConf = 
super.sparkConf.set(SQLConf.ANSI_ENABLED.key, "true")
+}
+
+// Runs the nanosecond timestamp function tests with ANSI mode disabled 
explicitly.
+class TimestampNanosFunctionsAnsiOffSuite extends 
TimestampNanosFunctionsSuiteBase {
+  override def sparkConf: SparkConf = 
super.sparkConf.set(SQLConf.ANSI_ENABLED.key, "false")
+}


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


Reply via email to