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 0995fe976525 [SPARK-57527][SQL] Add the `unix_nanos` function 
returning nanoseconds since the epoch for timestamps
0995fe976525 is described below

commit 0995fe976525fa8c46855c3e5373cd86f2acb341
Author: Maxim Gekk <[email protected]>
AuthorDate: Fri Jun 19 16:21:57 2026 +0200

    [SPARK-57527][SQL] Add the `unix_nanos` function returning nanoseconds 
since the epoch for timestamps
    
    ### What changes were proposed in this pull request?
    This PR adds a new built-in function `unix_nanos(expr)` that returns the 
number of nanoseconds since `1970-01-01 00:00:00 UTC` for a 
nanosecond-precision timestamp.
    
    Concretely:
    - Adds a `UnixNanos` expression in `datetimeExpressions.scala` that accepts 
only the nanosecond-precision timestamp types `TIMESTAMP_LTZ(p)` / 
`TIMESTAMP_NTZ(p)` (`p in [7, 9]`, i.e. `AnyTimestampNanoType`) and returns a 
lossless `DECIMAL(21, 0)`.
    - Computes `epochMicros * 1000 + nanosWithinMicro` via `BigInteger` in both 
the interpreted (`eval`) and codegen (`doGenCode`) paths. A `BIGINT` return 
type was rejected because `epochMicros * 1000` overflows 64 bits across the 
full `[0001..9999]` calendar range; `DECIMAL(21, 0)` is wide enough for every 
value (`~2.5e20` max) and stays lossless.
    - Registers `unix_nanos` in `FunctionRegistry` and adds the Scala 
`functions.unix_nanos`.
    - Adds catalyst unit tests (interpreted + codegen, plus a negative test for 
non-nanosecond input types), Scala/SQL end-to-end tests, and SQL golden-file 
coverage for `TIMESTAMP_NTZ(p)` / `TIMESTAMP_LTZ(p)`.
    
    Scope notes: the microsecond `TimestampType` input, the PySpark API 
(classic and Spark Connect Python), and R are out of scope here and tracked as 
follow-ups; `unix_nanos` is recorded in the PySpark function-parity allowlist 
in the meantime. The Scala Spark Connect client picks up `unix_nanos` 
automatically because `functions.scala` lives in the shared `sql/api` module 
(adding a Connect plan golden test is a follow-up).
    
    ### Why are the changes needed?
    Part of the 
[SPARK-56822](https://issues.apache.org/jira/browse/SPARK-56822) umbrella 
(timestamps with nanosecond precision). Spark has `unix_seconds` / 
`unix_millis` / `unix_micros` but no nanosecond counterpart, which is the 
natural inverse of nanosecond timestamp construction.
    
    ### Does this PR introduce _any_ user-facing change?
    Yes. A new `unix_nanos(timeExp)` function is available in SQL and the Scala 
API (including the Scala Spark Connect client). It accepts `TIMESTAMP_LTZ(p)` / 
`TIMESTAMP_NTZ(p)` and returns `DECIMAL(21, 0)`. This is a change only within 
the unreleased nanosecond-timestamp preview.
    
    Example:
    
    ```sql
    SELECT unix_nanos(TIMESTAMP_NTZ '2008-12-25 15:30:00.123456789');
    -- 1230219000123456789
    ```
    
    ### How was this patch tested?
    - `build/sbt 'catalyst/testOnly 
org.apache.spark.sql.catalyst.expressions.DateExpressionsSuite'`
    - `build/sbt 'sql/testOnly 
org.apache.spark.sql.TimestampNanosFunctionsAnsiOnSuite 
org.apache.spark.sql.TimestampNanosFunctionsAnsiOffSuite'`
    - `build/sbt 'sql/testOnly 
org.apache.spark.sql.expressions.ExpressionInfoSuite 
org.apache.spark.sql.ExpressionsSchemaSuite'`
    - `SPARK_GENERATE_GOLDEN_FILES=1 build/sbt 'sql/testOnly 
org.apache.spark.sql.SQLQueryTestSuite -- -z "nanos"'`
    - `./dev/scalastyle`
    
    ### Was this patch authored or co-authored using generative AI tooling?
    Generated-by: Cursor
    
    Closes #56602 from MaxGekk/unix_nanos.
    
    Authored-by: Maxim Gekk <[email protected]>
    Signed-off-by: Max Gekk <[email protected]>
    (cherry picked from commit b1ac8352c4c59d312a997b7e8e6ea94235132d09)
    Signed-off-by: Max Gekk <[email protected]>
---
 python/pyspark/sql/tests/test_functions.py         |  4 +-
 .../scala/org/apache/spark/sql/functions.scala     | 10 +++++
 .../sql/catalyst/analysis/FunctionRegistry.scala   |  1 +
 .../catalyst/expressions/datetimeExpressions.scala | 52 ++++++++++++++++++++++
 .../expressions/DateExpressionsSuite.scala         | 49 +++++++++++++++++++-
 .../sql-functions/sql-expression-schema.md         |  1 +
 .../analyzer-results/timestamp-ltz-nanos.sql.out   | 42 +++++++++++++++++
 .../analyzer-results/timestamp-ntz-nanos.sql.out   | 42 +++++++++++++++++
 .../sql-tests/inputs/timestamp-ltz-nanos.sql       | 13 ++++++
 .../sql-tests/inputs/timestamp-ntz-nanos.sql       | 13 ++++++
 .../sql-tests/results/timestamp-ltz-nanos.sql.out  | 48 ++++++++++++++++++++
 .../sql-tests/results/timestamp-ntz-nanos.sql.out  | 48 ++++++++++++++++++++
 .../sql/TimestampNanosFunctionsSuiteBase.scala     | 32 +++++++++++++
 13 files changed, 353 insertions(+), 2 deletions(-)

diff --git a/python/pyspark/sql/tests/test_functions.py 
b/python/pyspark/sql/tests/test_functions.py
index 8599d0dd46e1..10aa01e5a600 100644
--- a/python/pyspark/sql/tests/test_functions.py
+++ b/python/pyspark/sql/tests/test_functions.py
@@ -82,7 +82,9 @@ class FunctionsTestsMixin:
         missing_in_py = jvm_fn_set.difference(py_fn_set)
 
         # Functions that we expect to be missing in python until they are 
added to pyspark
-        expected_missing_in_py = set()
+        expected_missing_in_py = {
+            "unix_nanos",  # SPARK-57527: PySpark support tracked as a 
follow-up
+        }
 
         self.assertEqual(
             expected_missing_in_py, missing_in_py, "Missing functions in 
pyspark not as expected"
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 a2850f37a869..76748f0ae942 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
@@ -8199,6 +8199,16 @@ object functions {
    */
   def unix_micros(e: Column): Column = Column.fn("unix_micros", e)
 
+  /**
+   * Returns the number of nanoseconds since 1970-01-01 00:00:00 UTC for a 
nanosecond-precision
+   * timestamp (`TIMESTAMP_LTZ(p)` / `TIMESTAMP_NTZ(p)`, `p` in `[7, 9]`). The 
result is a
+   * lossless `DECIMAL(21, 0)`.
+   *
+   * @group datetime_funcs
+   * @since 4.3.0
+   */
+  def unix_nanos(e: Column): Column = Column.fn("unix_nanos", e)
+
   /**
    * Returns the number of milliseconds since 1970-01-01 00:00:00 UTC. 
Truncates higher levels of
    * precision.
diff --git 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/FunctionRegistry.scala
 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/FunctionRegistry.scala
index dea90247cc1b..2c47fca543a9 100644
--- 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/FunctionRegistry.scala
+++ 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/FunctionRegistry.scala
@@ -777,6 +777,7 @@ object FunctionRegistry {
     expression[UnixSeconds]("unix_seconds"),
     expression[UnixMillis]("unix_millis"),
     expression[UnixMicros]("unix_micros"),
+    expression[UnixNanos]("unix_nanos"),
     expression[ConvertTimezone]("convert_timezone"),
     expressionBuilder("time_bucket", TimeBucketExpressionBuilder),
 
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 bdbc071e576c..3fbef82ef246 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
@@ -17,6 +17,7 @@
 
 package org.apache.spark.sql.catalyst.expressions
 
+import java.math.BigInteger
 import java.text.ParseException
 import java.time.{DateTimeException, LocalDate, LocalDateTime, ZoneId, 
ZoneOffset}
 import java.time.format.DateTimeParseException
@@ -854,6 +855,57 @@ case class UnixMicros(child: Expression) extends 
TimestampToLongBase {
     copy(child = newChild)
 }
 
+// scalastyle:off line.contains.tab
+@ExpressionDescription(
+  usage = "_FUNC_(timestamp) - Returns the number of nanoseconds since 
1970-01-01 00:00:00 UTC.",
+  examples = """
+    Examples:
+      > SET spark.sql.timestampNanosTypes.enabled=true;
+      spark.sql.timestampNanosTypes.enabled    true
+      > SELECT _FUNC_(TIMESTAMP_NTZ '2008-12-25 15:30:00.123456789');
+       1230219000123456789
+  """,
+  group = "datetime_funcs",
+  since = "4.3.0")
+// scalastyle:on line.contains.tab
+case class UnixNanos(child: Expression)
+  extends UnaryExpression with ExpectsInputTypes {
+  override def nullIntolerant: Boolean = true
+
+  // Accepts only the nanosecond-precision timestamp types TIMESTAMP_LTZ(p) / 
TIMESTAMP_NTZ(p)
+  // (p in [7, 9]); support for the microsecond timestamp types is deferred to 
a follow-up.
+  override def inputTypes: Seq[AbstractDataType] = Seq(AnyTimestampNanoType)
+
+  // epochMicros * 1000 overflows a 64-bit BIGINT across the full [0001..9999] 
calendar range, so
+  // the result is a lossless DECIMAL with enough precision to hold every 
value (~2.5e20 max).
+  override def dataType: DataType = DecimalType(21, 0)
+
+  override def nullSafeEval(input: Any): Any = {
+    val v = input.asInstanceOf[TimestampNanosVal]
+    val nanos = BigInteger.valueOf(v.epochMicros)
+      .multiply(BigInteger.valueOf(NANOS_PER_MICROS))
+      .add(BigInteger.valueOf(v.nanosWithinMicro.toLong))
+    Decimal.apply(new java.math.BigDecimal(nanos), 21, 0)
+  }
+
+  override protected def doGenCode(ctx: CodegenContext, ev: ExprCode): 
ExprCode = {
+    nullSafeCodeGen(ctx, ev, c => {
+      val bi = ctx.freshName("nanos")
+      s"""
+         |java.math.BigInteger $bi = 
java.math.BigInteger.valueOf($c.epochMicros)
+         |  .multiply(java.math.BigInteger.valueOf(${NANOS_PER_MICROS}L))
+         |  .add(java.math.BigInteger.valueOf($c.nanosWithinMicro));
+         |${ev.value} = Decimal.apply(new java.math.BigDecimal($bi), 21, 0);
+         |""".stripMargin
+    })
+  }
+
+  override def prettyName: String = "unix_nanos"
+
+  override protected def withNewChildInternal(newChild: Expression): UnixNanos 
=
+    copy(child = newChild)
+}
+
 // scalastyle:off line.contains.tab
 @ExpressionDescription(
   usage = "_FUNC_(date) - Returns the year component of the date/timestamp.",
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 20f3dea2ec22..8771123ad120 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
@@ -41,7 +41,7 @@ import org.apache.spark.sql.internal.SQLConf
 import org.apache.spark.sql.internal.SQLConf.TimestampTypes
 import org.apache.spark.sql.types._
 import org.apache.spark.sql.types.DataTypeTestUtils.{dayTimeIntervalTypes, 
yearMonthIntervalTypes}
-import org.apache.spark.unsafe.types.{CalendarInterval, UTF8String}
+import org.apache.spark.unsafe.types.{CalendarInterval, TimestampNanosVal, 
UTF8String}
 
 class DateExpressionsSuite extends SparkFunSuite with ExpressionEvalHelper {
 
@@ -1696,6 +1696,53 @@ class DateExpressionsSuite extends SparkFunSuite with 
ExpressionEvalHelper {
     checkEvaluation(UnixMicros(Literal(timestampWithNanos)), 1000001L)
   }
 
+  test("SPARK-57527: unix_nanos over nanosecond-precision timestamps") {
+    import org.apache.spark.sql.catalyst.util.TimestampNanosTestUtils._
+
+    def expectedNanos(v: TimestampNanosVal): Decimal = {
+      val nanos = BigInt(v.epochMicros) * NANOS_PER_MICROS + 
v.nanosWithinMicro.toInt
+      Decimal(BigDecimal(nanos), 21, 0)
+    }
+
+    // 2008-12-25 15:30:00.123456789 -> 1230219000123456789 nanos since the 
epoch. unix_nanos
+    // applies no zone shift, so the NTZ wall-clock value and the LTZ instant 
at the same UTC
+    // reading produce the same result; the declared precision does not 
re-truncate the value.
+    val ntz = localDateTimeToNanosVal(timestampNTZ(2008, 12, 25, 15, 30, 0, 
123456789))
+    val ltz = 
instantToNanosVal(Instant.parse("2008-12-25T15:30:00.123456789Z"))
+    val post = Decimal(BigDecimal("1230219000123456789"), 21, 0)
+    foreachNanosPrecision { p =>
+      checkEvaluation(UnixNanos(Literal.create(ntz, 
TimestampNTZNanosType(p))), post)
+      checkEvaluation(UnixNanos(Literal.create(ltz, 
TimestampLTZNanosType(p))), post)
+    }
+
+    // Pre-epoch value exercises the negative-epoch path.
+    val preEpoch = localDateTimeToNanosVal(timestampNTZ(1960, 1, 1, 0, 0, 0, 
1))
+    checkEvaluation(
+      UnixNanos(Literal.create(preEpoch, TimestampNTZNanosType(9))), 
expectedNanos(preEpoch))
+
+    // Far-future value: epochMicros * 1000 overflows a 64-bit BIGINT, so the 
DECIMAL result must
+    // exceed Long.MaxValue and the computation must not be done in long 
arithmetic.
+    val far = localDateTimeToNanosVal(timestampNTZ(9999, 12, 31, 23, 59, 59, 
999999999))
+    checkEvaluation(UnixNanos(Literal.create(far, TimestampNTZNanosType(9))), 
expectedNanos(far))
+    val farResult =
+      UnixNanos(Literal.create(far, 
TimestampNTZNanosType(9))).eval().asInstanceOf[Decimal]
+    
assert(farResult.toJavaBigDecimal.compareTo(java.math.BigDecimal.valueOf(Long.MaxValue))
 > 0)
+
+    // NULL input.
+    checkEvaluation(UnixNanos(Literal.create(null, TimestampNTZNanosType(9))), 
null)
+    checkEvaluation(UnixNanos(Literal.create(null, TimestampLTZNanosType(9))), 
null)
+  }
+
+  test("SPARK-57527: unix_nanos rejects non-nanosecond input types") {
+    // unix_nanos accepts only the nanosecond-precision timestamp types; the 
microsecond
+    // TimestampType / TimestampNTZType (and other types) fail analysis with a 
type mismatch.
+    Seq(TimestampType, TimestampNTZType, DateType, LongType).foreach { dt =>
+      val mismatch = UnixNanos(Literal.create(null, dt))
+        .checkInputDataTypes().asInstanceOf[DataTypeMismatch]
+      assert(mismatch.errorSubClass == "UNEXPECTED_INPUT_TYPE")
+    }
+  }
+
   test("TIMESTAMP_SECONDS") {
     def testIntegralFunc(value: Number): Unit = {
       checkEvaluation(
diff --git a/sql/core/src/test/resources/sql-functions/sql-expression-schema.md 
b/sql/core/src/test/resources/sql-functions/sql-expression-schema.md
index 6003c7af52d3..3ff81b7f57f0 100644
--- a/sql/core/src/test/resources/sql-functions/sql-expression-schema.md
+++ b/sql/core/src/test/resources/sql-functions/sql-expression-schema.md
@@ -435,6 +435,7 @@
 | org.apache.spark.sql.catalyst.expressions.UnixDate | unix_date | SELECT 
unix_date(DATE("1970-01-02")) | struct<unix_date(1970-01-02):int> |
 | org.apache.spark.sql.catalyst.expressions.UnixMicros | unix_micros | SELECT 
unix_micros(TIMESTAMP('1970-01-01 00:00:01Z')) | struct<unix_micros(1970-01-01 
00:00:01Z):bigint> |
 | org.apache.spark.sql.catalyst.expressions.UnixMillis | unix_millis | SELECT 
unix_millis(TIMESTAMP('1970-01-01 00:00:01Z')) | struct<unix_millis(1970-01-01 
00:00:01Z):bigint> |
+| org.apache.spark.sql.catalyst.expressions.UnixNanos | unix_nanos | SELECT 
unix_nanos(TIMESTAMP_NTZ '2008-12-25 15:30:00.123456789') | 
struct<unix_nanos(TIMESTAMP_NTZ '2008-12-25 15:30:00.123456789'):decimal(21,0)> 
|
 | org.apache.spark.sql.catalyst.expressions.UnixSeconds | unix_seconds | 
SELECT unix_seconds(TIMESTAMP('1970-01-01 00:00:01Z')) | 
struct<unix_seconds(1970-01-01 00:00:01Z):bigint> |
 | org.apache.spark.sql.catalyst.expressions.UnixTimestamp | unix_timestamp | 
SELECT unix_timestamp() | struct<unix_timestamp(current_timestamp(), yyyy-MM-dd 
HH:mm:ss):bigint> |
 | org.apache.spark.sql.catalyst.expressions.Upper | ucase | SELECT 
ucase('SparkSql') | struct<ucase(SparkSql):string> |
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 6ce540f2fcbc..a4dadf760088 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
@@ -720,3 +720,45 @@ SELECT unix_timestamp(NULL :: timestamp_ltz(9)), 
to_unix_timestamp(NULL :: times
 -- !query analysis
 Project [unix_timestamp(cast(null as timestamp_ltz(9)), yyyy-MM-dd HH:mm:ss, 
Some(America/Los_Angeles), true) AS unix_timestamp(CAST(NULL AS 
TIMESTAMP_LTZ(9)), yyyy-MM-dd HH:mm:ss)#xL, to_unix_timestamp(cast(null as 
timestamp_ltz(9)), yyyy-MM-dd HH:mm:ss, Some(America/Los_Angeles), true) AS 
to_unix_timestamp(CAST(NULL AS TIMESTAMP_LTZ(9)), yyyy-MM-dd HH:mm:ss)#xL]
 +- OneRowRelation
+
+
+-- !query
+SELECT unix_nanos(TIMESTAMP_LTZ '2020-01-01 13:24:35.123456789 UTC')
+-- !query analysis
+Project [unix_nanos(2020-01-01 05:24:35.123456789) AS unix_nanos(TIMESTAMP_LTZ 
'2020-01-01 05:24:35.123456789')#x]
++- OneRowRelation
+
+
+-- !query
+SELECT unix_nanos('2020-01-01 13:24:35.123456789 UTC' :: timestamp_ltz(7))
+-- !query analysis
+Project [unix_nanos(cast(2020-01-01 13:24:35.123456789 UTC as 
timestamp_ltz(7))) AS unix_nanos(CAST(2020-01-01 13:24:35.123456789 UTC AS 
TIMESTAMP_LTZ(7)))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT unix_nanos('2020-01-01 13:24:35.123456789 UTC' :: timestamp_ltz(8))
+-- !query analysis
+Project [unix_nanos(cast(2020-01-01 13:24:35.123456789 UTC as 
timestamp_ltz(8))) AS unix_nanos(CAST(2020-01-01 13:24:35.123456789 UTC AS 
TIMESTAMP_LTZ(8)))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT unix_nanos(TIMESTAMP_LTZ '9999-12-31 23:59:59.999999999 UTC')
+-- !query analysis
+Project [unix_nanos(9999-12-31 15:59:59.999999999) AS unix_nanos(TIMESTAMP_LTZ 
'9999-12-31 15:59:59.999999999')#x]
++- OneRowRelation
+
+
+-- !query
+SELECT unix_nanos(TIMESTAMP_LTZ '1960-01-01 00:00:00.000000001 UTC')
+-- !query analysis
+Project [unix_nanos(1959-12-31 16:00:00.000000001) AS unix_nanos(TIMESTAMP_LTZ 
'1959-12-31 16:00:00.000000001')#x]
++- OneRowRelation
+
+
+-- !query
+SELECT unix_nanos(NULL :: timestamp_ltz(9))
+-- !query analysis
+Project [unix_nanos(cast(null as timestamp_ltz(9))) AS unix_nanos(CAST(NULL AS 
TIMESTAMP_LTZ(9)))#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 85fe657d155a..de0922d74c79 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
@@ -640,3 +640,45 @@ SELECT unix_timestamp(NULL :: timestamp_ntz(9)), 
to_unix_timestamp(NULL :: times
 -- !query analysis
 Project [unix_timestamp(cast(null as timestamp_ntz(9)), yyyy-MM-dd HH:mm:ss, 
Some(America/Los_Angeles), true) AS unix_timestamp(CAST(NULL AS 
TIMESTAMP_NTZ(9)), yyyy-MM-dd HH:mm:ss)#xL, to_unix_timestamp(cast(null as 
timestamp_ntz(9)), yyyy-MM-dd HH:mm:ss, Some(America/Los_Angeles), true) AS 
to_unix_timestamp(CAST(NULL AS TIMESTAMP_NTZ(9)), yyyy-MM-dd HH:mm:ss)#xL]
 +- OneRowRelation
+
+
+-- !query
+SELECT unix_nanos(TIMESTAMP_NTZ '2020-01-01 13:24:35.123456789')
+-- !query analysis
+Project [unix_nanos(2020-01-01 13:24:35.123456789) AS unix_nanos(TIMESTAMP_NTZ 
'2020-01-01 13:24:35.123456789')#x]
++- OneRowRelation
+
+
+-- !query
+SELECT unix_nanos('2020-01-01 13:24:35.123456789' :: timestamp_ntz(7))
+-- !query analysis
+Project [unix_nanos(cast(2020-01-01 13:24:35.123456789 as timestamp_ntz(7))) 
AS unix_nanos(CAST(2020-01-01 13:24:35.123456789 AS TIMESTAMP_NTZ(7)))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT unix_nanos('2020-01-01 13:24:35.123456789' :: timestamp_ntz(8))
+-- !query analysis
+Project [unix_nanos(cast(2020-01-01 13:24:35.123456789 as timestamp_ntz(8))) 
AS unix_nanos(CAST(2020-01-01 13:24:35.123456789 AS TIMESTAMP_NTZ(8)))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT unix_nanos(TIMESTAMP_NTZ '9999-12-31 23:59:59.999999999')
+-- !query analysis
+Project [unix_nanos(9999-12-31 23:59:59.999999999) AS unix_nanos(TIMESTAMP_NTZ 
'9999-12-31 23:59:59.999999999')#x]
++- OneRowRelation
+
+
+-- !query
+SELECT unix_nanos(TIMESTAMP_NTZ '1960-01-01 00:00:00.000000001')
+-- !query analysis
+Project [unix_nanos(1960-01-01 00:00:00.000000001) AS unix_nanos(TIMESTAMP_NTZ 
'1960-01-01 00:00:00.000000001')#x]
++- OneRowRelation
+
+
+-- !query
+SELECT unix_nanos(NULL :: timestamp_ntz(9))
+-- !query analysis
+Project [unix_nanos(cast(null as timestamp_ntz(9))) AS unix_nanos(CAST(NULL AS 
TIMESTAMP_NTZ(9)))#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 eb14e3b240d1..e208704196ba 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
@@ -202,3 +202,16 @@ SELECT unix_timestamp('2020-01-01 13:24:35.999999999' :: 
timestamp_ltz(7));
 SELECT unix_timestamp(TIMESTAMP_LTZ '1969-12-31 23:59:59.500000000 UTC');
 -- NULL nanosecond timestamp.
 SELECT unix_timestamp(NULL :: timestamp_ltz(9)), to_unix_timestamp(NULL :: 
timestamp_ltz(9));
+
+-- SPARK-57527: unix_nanos over nanosecond-precision values returns 
DECIMAL(21, 0) nanoseconds since
+-- the epoch. The explicit-zone literals below fix the instant directly, 
independent of the session
+-- time zone. The sub-microsecond digits are kept, truncated to the type's 
precision.
+SELECT unix_nanos(TIMESTAMP_LTZ '2020-01-01 13:24:35.123456789 UTC');
+SELECT unix_nanos('2020-01-01 13:24:35.123456789 UTC' :: timestamp_ltz(7));
+SELECT unix_nanos('2020-01-01 13:24:35.123456789 UTC' :: timestamp_ltz(8));
+-- Far-future value: epochMicros * 1000 overflows a 64-bit BIGINT, exercising 
the DECIMAL path.
+SELECT unix_nanos(TIMESTAMP_LTZ '9999-12-31 23:59:59.999999999 UTC');
+-- Pre-epoch value exercises the negative-epoch path.
+SELECT unix_nanos(TIMESTAMP_LTZ '1960-01-01 00:00:00.000000001 UTC');
+-- NULL nanosecond timestamp.
+SELECT unix_nanos(NULL :: timestamp_ltz(9));
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 b34cff5396b9..33b60cf39d9d 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
@@ -176,3 +176,16 @@ SELECT to_unix_timestamp('2020-01-01 13:24:35.000000001' 
:: timestamp_ntz(9));
 SELECT unix_timestamp(TIMESTAMP_NTZ '1969-12-31 23:59:59.500000000');
 -- NULL nanosecond timestamp.
 SELECT unix_timestamp(NULL :: timestamp_ntz(9)), to_unix_timestamp(NULL :: 
timestamp_ntz(9));
+
+-- SPARK-57527: unix_nanos over nanosecond-precision values returns 
DECIMAL(21, 0) nanoseconds since
+-- the epoch; NTZ applies no zone shift, so the wall-clock value is read as 
the epoch instant. The
+-- sub-microsecond digits are kept, truncated to the type's precision.
+SELECT unix_nanos(TIMESTAMP_NTZ '2020-01-01 13:24:35.123456789');
+SELECT unix_nanos('2020-01-01 13:24:35.123456789' :: timestamp_ntz(7));
+SELECT unix_nanos('2020-01-01 13:24:35.123456789' :: timestamp_ntz(8));
+-- Far-future value: epochMicros * 1000 overflows a 64-bit BIGINT, exercising 
the DECIMAL path.
+SELECT unix_nanos(TIMESTAMP_NTZ '9999-12-31 23:59:59.999999999');
+-- Pre-epoch value exercises the negative-epoch path.
+SELECT unix_nanos(TIMESTAMP_NTZ '1960-01-01 00:00:00.000000001');
+-- NULL nanosecond timestamp.
+SELECT unix_nanos(NULL :: timestamp_ntz(9));
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 8a24b41f4a3e..1f75f01da848 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
@@ -806,3 +806,51 @@ SELECT unix_timestamp(NULL :: timestamp_ltz(9)), 
to_unix_timestamp(NULL :: times
 struct<unix_timestamp(CAST(NULL AS TIMESTAMP_LTZ(9)), yyyy-MM-dd 
HH:mm:ss):bigint,to_unix_timestamp(CAST(NULL AS TIMESTAMP_LTZ(9)), yyyy-MM-dd 
HH:mm:ss):bigint>
 -- !query output
 NULL   NULL
+
+
+-- !query
+SELECT unix_nanos(TIMESTAMP_LTZ '2020-01-01 13:24:35.123456789 UTC')
+-- !query schema
+struct<unix_nanos(TIMESTAMP_LTZ '2020-01-01 05:24:35.123456789'):decimal(21,0)>
+-- !query output
+1577885075123456789
+
+
+-- !query
+SELECT unix_nanos('2020-01-01 13:24:35.123456789 UTC' :: timestamp_ltz(7))
+-- !query schema
+struct<unix_nanos(CAST(2020-01-01 13:24:35.123456789 UTC AS 
TIMESTAMP_LTZ(7))):decimal(21,0)>
+-- !query output
+1577885075123456700
+
+
+-- !query
+SELECT unix_nanos('2020-01-01 13:24:35.123456789 UTC' :: timestamp_ltz(8))
+-- !query schema
+struct<unix_nanos(CAST(2020-01-01 13:24:35.123456789 UTC AS 
TIMESTAMP_LTZ(8))):decimal(21,0)>
+-- !query output
+1577885075123456780
+
+
+-- !query
+SELECT unix_nanos(TIMESTAMP_LTZ '9999-12-31 23:59:59.999999999 UTC')
+-- !query schema
+struct<unix_nanos(TIMESTAMP_LTZ '9999-12-31 15:59:59.999999999'):decimal(21,0)>
+-- !query output
+253402300799999999999
+
+
+-- !query
+SELECT unix_nanos(TIMESTAMP_LTZ '1960-01-01 00:00:00.000000001 UTC')
+-- !query schema
+struct<unix_nanos(TIMESTAMP_LTZ '1959-12-31 16:00:00.000000001'):decimal(21,0)>
+-- !query output
+-315619199999999999
+
+
+-- !query
+SELECT unix_nanos(NULL :: timestamp_ltz(9))
+-- !query schema
+struct<unix_nanos(CAST(NULL AS TIMESTAMP_LTZ(9))):decimal(21,0)>
+-- !query output
+NULL
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 6227e2cd214c..324ea639ccc3 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
@@ -716,3 +716,51 @@ SELECT unix_timestamp(NULL :: timestamp_ntz(9)), 
to_unix_timestamp(NULL :: times
 struct<unix_timestamp(CAST(NULL AS TIMESTAMP_NTZ(9)), yyyy-MM-dd 
HH:mm:ss):bigint,to_unix_timestamp(CAST(NULL AS TIMESTAMP_NTZ(9)), yyyy-MM-dd 
HH:mm:ss):bigint>
 -- !query output
 NULL   NULL
+
+
+-- !query
+SELECT unix_nanos(TIMESTAMP_NTZ '2020-01-01 13:24:35.123456789')
+-- !query schema
+struct<unix_nanos(TIMESTAMP_NTZ '2020-01-01 13:24:35.123456789'):decimal(21,0)>
+-- !query output
+1577885075123456789
+
+
+-- !query
+SELECT unix_nanos('2020-01-01 13:24:35.123456789' :: timestamp_ntz(7))
+-- !query schema
+struct<unix_nanos(CAST(2020-01-01 13:24:35.123456789 AS 
TIMESTAMP_NTZ(7))):decimal(21,0)>
+-- !query output
+1577885075123456700
+
+
+-- !query
+SELECT unix_nanos('2020-01-01 13:24:35.123456789' :: timestamp_ntz(8))
+-- !query schema
+struct<unix_nanos(CAST(2020-01-01 13:24:35.123456789 AS 
TIMESTAMP_NTZ(8))):decimal(21,0)>
+-- !query output
+1577885075123456780
+
+
+-- !query
+SELECT unix_nanos(TIMESTAMP_NTZ '9999-12-31 23:59:59.999999999')
+-- !query schema
+struct<unix_nanos(TIMESTAMP_NTZ '9999-12-31 23:59:59.999999999'):decimal(21,0)>
+-- !query output
+253402300799999999999
+
+
+-- !query
+SELECT unix_nanos(TIMESTAMP_NTZ '1960-01-01 00:00:00.000000001')
+-- !query schema
+struct<unix_nanos(TIMESTAMP_NTZ '1960-01-01 00:00:00.000000001'):decimal(21,0)>
+-- !query output
+-315619199999999999
+
+
+-- !query
+SELECT unix_nanos(NULL :: timestamp_ntz(9))
+-- !query schema
+struct<unix_nanos(CAST(NULL AS TIMESTAMP_NTZ(9))):decimal(21,0)>
+-- !query output
+NULL
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 cbb40a8fcced..da2e9d3a8d88 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
@@ -449,6 +449,38 @@ abstract class TimestampNanosFunctionsSuiteBase extends 
SharedSparkSession {
         ltz.select(unix_timestamp(col("c")), to_unix_timestamp(col("c"))), 
Row(null, null))
     }
   }
+
+  test("SPARK-57527: unix_nanos over nanosecond-precision timestamps") {
+    // unix_nanos returns DECIMAL(21, 0) nanoseconds since the epoch and 
applies no zone shift to a
+    // timestamp argument. The chosen fractions have zeros beyond the 7th 
digit, so truncating to
+    // precision p in {7, 8, 9} leaves the sub-microsecond part unchanged and 
the result is the same
+    // for every p. Both the Scala Column API and the SQL path are exercised.
+    val ntzStr = "2020-01-01T13:24:35.123456700"
+    val ltzStr = "2020-01-01T21:24:35.987654300Z"
+    // 2020-01-01 13:24:35.123456 -> 1577885075123456 micros, + 700 ns = 
1577885075123456700.
+    val ntzExpected = Row(new java.math.BigDecimal("1577885075123456700"))
+    // 2020-01-01 21:24:35.987654 UTC -> 1577913875987654 micros, + 300 ns = 
1577913875987654300.
+    val ltzExpected = Row(new java.math.BigDecimal("1577913875987654300"))
+    Seq(7, 8, 9).foreach { p =>
+      checkAnswer(ntzNanos(ntzStr, p).select(unix_nanos(col("c"))), 
ntzExpected)
+      checkAnswer(ntzNanos(ntzStr, p).selectExpr("unix_nanos(c)"), ntzExpected)
+      checkAnswer(ltzNanos(ltzStr, p).select(unix_nanos(col("c"))), 
ltzExpected)
+      checkAnswer(ltzNanos(ltzStr, p).selectExpr("unix_nanos(c)"), ltzExpected)
+    }
+  }
+
+  test("SPARK-57527: unix_nanos over NULL nanosecond timestamps") {
+    Seq(7, 8, 9).foreach { p =>
+      val ntz = spark.createDataFrame(
+        spark.sparkContext.parallelize(Seq(Row(null))),
+        new StructType().add("c", TimestampNTZNanosType(p)))
+      val ltz = spark.createDataFrame(
+        spark.sparkContext.parallelize(Seq(Row(null))),
+        new StructType().add("c", TimestampLTZNanosType(p)))
+      checkAnswer(ntz.select(unix_nanos(col("c"))), Row(null))
+      checkAnswer(ltz.select(unix_nanos(col("c"))), Row(null))
+    }
+  }
 }
 
 // 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