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 1ed3eae30c6a [SPARK-57526][SQL] Add the `timestamp_nanos` function to 
create nanosecond-precision timestamps from numeric nanoseconds
1ed3eae30c6a is described below

commit 1ed3eae30c6ad69e64aef38fae4f01d5ceb84cf0
Author: Maxim Gekk <[email protected]>
AuthorDate: Mon Jun 22 09:00:23 2026 +0200

    [SPARK-57526][SQL] Add the `timestamp_nanos` function to create 
nanosecond-precision timestamps from numeric nanoseconds
    
    ### What changes were proposed in this pull request?
    Adds a built-in `timestamp_nanos(expr)` function. It reads `expr` as a 
count of nanoseconds since `1970-01-01 00:00:00 UTC` and returns a 
nanosecond-precision `TIMESTAMP_LTZ(9)` — the natural inverse of `unix_nanos`.
    
    The argument is an integral or `DECIMAL` count. `DECIMAL` is what lets it 
reach the whole `[0001, 9999]` calendar range, since year-9999 nanoseconds 
(~2.5e20) overflow a 64-bit `BIGINT` — the same reason `unix_nanos` returns 
`DECIMAL(21, 0)`. `FLOAT`/`DOUBLE`/`STRING` are rejected at analysis (a 
fractional or string nanosecond count isn't meaningful), and a count outside 
the representable range fails with the `DATETIME_OVERFLOW` error condition.
    
    Implementation: a new `NanosToTimestamp` expression in 
`datetimeExpressions.scala` (interpreted + codegen), registered in 
`FunctionRegistry`, and exposed as `functions.timestamp_nanos` in the shared 
`sql/api` module so the Scala Spark Connect client picks it up automatically. 
PySpark and R are out of scope and tracked as follow-ups; `timestamp_nanos` is 
on the PySpark function-parity allowlist meanwhile.
    
    Follow-up: the peer 
`timestamp_seconds`/`timestamp_millis`/`timestamp_micros` still throw a raw 
`ArithmeticException` on overflow; migrating them to `DATETIME_OVERFLOW` is 
tracked in [SPARK-57577](https://issues.apache.org/jira/browse/SPARK-57577).
    
    ### Why are the changes needed?
    Part of the 
[SPARK-56822](https://issues.apache.org/jira/browse/SPARK-56822) umbrella 
(nanosecond-precision timestamps). Spark has `timestamp_seconds` / 
`timestamp_millis` / `timestamp_micros` but no nanosecond counterpart.
    
    ### Does this PR introduce _any_ user-facing change?
    Yes — a new `timestamp_nanos(expr)` function in SQL and the Scala API 
(including the Scala Spark Connect client), returning `TIMESTAMP_LTZ(9)`. This 
is a change only within the unreleased nanosecond-timestamp preview.
    
    ```sql
    SELECT timestamp_nanos(1230219000123456789);
    -- 2008-12-25 07:30:00.123456789
    ```
    
    ### 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 #56616 from MaxGekk/timestamp_nanos.
    
    Authored-by: Maxim Gekk <[email protected]>
    Signed-off-by: Max Gekk <[email protected]>
---
 python/pyspark/sql/tests/test_functions.py         |  1 +
 .../scala/org/apache/spark/sql/functions.scala     |  9 +++
 .../sql/catalyst/analysis/FunctionRegistry.scala   |  1 +
 .../catalyst/expressions/datetimeExpressions.scala | 92 ++++++++++++++++++++++
 .../spark/sql/errors/QueryExecutionErrors.scala    | 10 +++
 .../expressions/DateExpressionsSuite.scala         | 59 ++++++++++++++
 .../sql-functions/sql-expression-schema.md         |  1 +
 .../analyzer-results/timestamp-ltz-nanos.sql.out   | 59 ++++++++++++++
 .../sql-tests/inputs/timestamp-ltz-nanos.sql       | 14 ++++
 .../sql-tests/results/timestamp-ltz-nanos.sql.out  | 73 +++++++++++++++++
 .../sql/TimestampNanosFunctionsSuiteBase.scala     | 36 +++++++++
 11 files changed, 355 insertions(+)

diff --git a/python/pyspark/sql/tests/test_functions.py 
b/python/pyspark/sql/tests/test_functions.py
index 10aa01e5a600..c9ca0fca96a7 100644
--- a/python/pyspark/sql/tests/test_functions.py
+++ b/python/pyspark/sql/tests/test_functions.py
@@ -84,6 +84,7 @@ class FunctionsTestsMixin:
         # Functions that we expect to be missing in python until they are 
added to pyspark
         expected_missing_in_py = {
             "unix_nanos",  # SPARK-57527: PySpark support tracked as a 
follow-up
+            "timestamp_nanos",  # SPARK-57526: PySpark support tracked as a 
follow-up
         }
 
         self.assertEqual(
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 76748f0ae942..8aea50291cdc 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
@@ -8569,6 +8569,15 @@ object functions {
    */
   def timestamp_micros(e: Column): Column = Column.fn("timestamp_micros", e)
 
+  /**
+   * Creates a timestamp with the local time zone and nanosecond precision 
(TIMESTAMP_LTZ(9)) from
+   * the number of nanoseconds since UTC epoch.
+   *
+   * @group datetime_funcs
+   * @since 4.3.0
+   */
+  def timestamp_nanos(e: Column): Column = Column.fn("timestamp_nanos", e)
+
   /**
    * Gets the difference between the timestamps in the specified units by 
truncating the fraction
    * part.
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 2c47fca543a9..415a842c9bf4 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
@@ -774,6 +774,7 @@ object FunctionRegistry {
     expression[SecondsToTimestamp]("timestamp_seconds"),
     expression[MillisToTimestamp]("timestamp_millis"),
     expression[MicrosToTimestamp]("timestamp_micros"),
+    expression[NanosToTimestamp]("timestamp_nanos"),
     expression[UnixSeconds]("unix_seconds"),
     expression[UnixMillis]("unix_millis"),
     expression[UnixMicros]("unix_micros"),
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 3fbef82ef246..3f773e5bb6dc 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
@@ -759,6 +759,98 @@ case class MicrosToTimestamp(child: Expression)
     copy(child = newChild)
 }
 
+// scalastyle:off line.size.limit line.contains.tab
+@ExpressionDescription(
+  usage = "_FUNC_(nanoseconds) - Creates timestamp with the local time zone 
and nanosecond precision (TIMESTAMP_LTZ(9)) from the number of nanoseconds 
since UTC epoch.",
+  examples = """
+    Examples:
+      > SET spark.sql.timestampNanosTypes.enabled=true;
+      spark.sql.timestampNanosTypes.enabled    true
+      > SELECT _FUNC_(1230219000123456789);
+       2008-12-25 07:30:00.123456789
+  """,
+  group = "datetime_funcs",
+  since = "4.3.0")
+// scalastyle:on line.size.limit line.contains.tab
+case class NanosToTimestamp(child: Expression)
+  extends UnaryExpression with ExpectsInputTypes {
+  override def nullIntolerant: Boolean = true
+
+  // Accepts an integral or DECIMAL nanosecond count only. DECIMAL is required 
to span the full
+  // [0001, 9999] calendar range: nanos for year 9999 (~2.5e20) overflow a 
64-bit BIGINT, the same
+  // reason the inverse `unix_nanos` returns DECIMAL(21, 0); an integral 
argument is widened to
+  // BigInteger directly. FLOAT/DOUBLE/STRING are intentionally rejected at 
analysis rather than
+  // implicitly coerced: a fractional or string nanosecond count is not 
meaningful, and the implicit
+  // DECIMAL coercion (FLOAT -> DECIMAL(14, 7), DOUBLE -> DECIMAL(30, 15)) 
would silently overflow
+  // for realistic magnitudes.
+  override def inputTypes: Seq[AbstractDataType] = 
Seq(TypeCollection(IntegralType, DecimalType))
+
+  override def dataType: DataType = TimestampLTZNanosType(9)
+
+  // Maps the integer nanosecond count to the (epochMicros, nanosWithinMicro) 
pair with floor
+  // semantics, so the sub-microsecond remainder is always in [0, 999] 
(matching the negative-input
+  // behavior of `floorDiv`/`floorMod`). When `epochMicros` overflows 64 bits 
-- i.e. the input is
+  // outside the representable timestamp range -- `longValueExact` throws, 
which is surfaced as a
+  // DATETIME_OVERFLOW error.
+  //
+  // Like the sibling 
`timestamp_micros`/`timestamp_millis`/`timestamp_seconds` constructors, the
+  // result is not validated against the [0001, 9999] calendar range: only the 
64-bit `epochMicros`
+  // boundary is guarded, so a count whose `epochMicros` still fits in a long 
but lands past year
+  // 9999 (up to the long-micros maximum, ~year 294247) yields an out-of-range 
value rather than an
+  // error. This is intentional, keeping the nanosecond constructor consistent 
with its micro peers.
+  override def nullSafeEval(input: Any): Any = {
+    val n = child.dataType match {
+      case _: DecimalType =>
+        input.asInstanceOf[Decimal].toJavaBigDecimal
+          .setScale(0, java.math.RoundingMode.FLOOR).toBigInteger
+      case _: IntegralType =>
+        BigInteger.valueOf(input.asInstanceOf[Number].longValue())
+    }
+    val thousand = BigInteger.valueOf(NANOS_PER_MICROS)
+    val rem = n.mod(thousand)
+    val micros = try {
+      n.subtract(rem).divide(thousand).longValueExact()
+    } catch {
+      case _: ArithmeticException => throw 
QueryExecutionErrors.timestampNanosOverflowError(n)
+    }
+    TimestampNanosVal.fromParts(micros, rem.shortValueExact())
+  }
+
+  override protected def doGenCode(ctx: CodegenContext, ev: ExprCode): 
ExprCode = {
+    nullSafeCodeGen(ctx, ev, c => {
+      val n = ctx.freshName("nanos")
+      val thousand = ctx.freshName("thousand")
+      val rem = ctx.freshName("rem")
+      val micros = ctx.freshName("micros")
+      val toBigInteger = child.dataType match {
+        case _: DecimalType =>
+          s"$c.toJavaBigDecimal().setScale(0, 
java.math.RoundingMode.FLOOR).toBigInteger()"
+        case _: IntegralType =>
+          s"java.math.BigInteger.valueOf((long) $c)"
+      }
+      val errors = QueryExecutionErrors.getClass.getName.stripSuffix("$")
+      s"""
+         |java.math.BigInteger $n = $toBigInteger;
+         |java.math.BigInteger $thousand = 
java.math.BigInteger.valueOf(${NANOS_PER_MICROS}L);
+         |java.math.BigInteger $rem = $n.mod($thousand);
+         |long $micros;
+         |try {
+         |  $micros = $n.subtract($rem).divide($thousand).longValueExact();
+         |} catch (java.lang.ArithmeticException e) {
+         |  throw $errors.timestampNanosOverflowError($n);
+         |}
+         |${ev.value} = 
org.apache.spark.unsafe.types.TimestampNanosVal.fromParts(
+         |  $micros, $rem.shortValueExact());
+         |""".stripMargin
+    })
+  }
+
+  override def prettyName: String = "timestamp_nanos"
+
+  override protected def withNewChildInternal(newChild: Expression): 
NanosToTimestamp =
+    copy(child = newChild)
+}
+
 abstract class TimestampToLongBase extends UnaryExpression
   with ExpectsInputTypes {
   override def nullIntolerant: Boolean = true
diff --git 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/errors/QueryExecutionErrors.scala
 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/errors/QueryExecutionErrors.scala
index 48c3ef0c6a93..f4db9c9041f2 100644
--- 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/errors/QueryExecutionErrors.scala
+++ 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/errors/QueryExecutionErrors.scala
@@ -2646,6 +2646,16 @@ private[sql] object QueryExecutionErrors extends 
QueryErrorsBase with ExecutionE
       summary = "")
   }
 
+  def timestampNanosOverflowError(nanos: java.math.BigInteger): 
SparkArithmeticException = {
+    new SparkArithmeticException(
+      errorClass = "DATETIME_OVERFLOW",
+      messageParameters = Map(
+        "operation" ->
+          s"create a TIMESTAMP_LTZ(9) from $nanos nanoseconds since the 
epoch"),
+      context = Array.empty,
+      summary = "")
+  }
+
   def timeAddIntervalOverflowError(
       time: Long,
       timePrecision: Int,
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 8771123ad120..d6b18a9370e0 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
@@ -1743,6 +1743,65 @@ class DateExpressionsSuite extends SparkFunSuite with 
ExpressionEvalHelper {
     }
   }
 
+  test("SPARK-57526: timestamp_nanos builds a TIMESTAMP_LTZ(9) from 
nanoseconds") {
+    import org.apache.spark.sql.catalyst.util.TimestampNanosTestUtils._
+
+    // DECIMAL input is accepted as-is; a wide DECIMAL(38, 0) holds every 
input below.
+    def tsNanos(n: BigInt): NanosToTimestamp =
+      NanosToTimestamp(Literal.create(Decimal(BigDecimal(n), 38, 0), 
DecimalType(38, 0)))
+
+    assert(tsNanos(0).dataType === TimestampLTZNanosType(9))
+
+    // The JIRA example: 1230219000123456789 ns -> 1230219000123456 micros + 
789 ns.
+    checkEvaluation(tsNanos(BigInt("1230219000123456789")), 
nanosVal(1230219000123456L, 789))
+
+    // An integral argument is accepted directly (widened to BigInteger), 
exercising the
+    // IntegralType eval/codegen path rather than the DECIMAL one. Cover every 
integral width
+    // (TINYINT/SMALLINT/INT/BIGINT) so the `(long)` codegen cast is checked 
for each.
+    checkEvaluation(NanosToTimestamp(Literal(2.toByte)), nanosVal(0L, 2))
+    checkEvaluation(NanosToTimestamp(Literal(1000.toShort)), nanosVal(1L, 0))
+    checkEvaluation(NanosToTimestamp(Literal(1000)), nanosVal(1L, 0))
+    checkEvaluation(
+      NanosToTimestamp(Literal(1230219000123456789L)), 
nanosVal(1230219000123456L, 789))
+    checkEvaluation(NanosToTimestamp(Literal(-1L)), nanosVal(-1L, 999))
+
+    // FLOAT/DOUBLE/STRING are rejected at analysis: a fractional or string 
nanosecond count is not
+    // meaningful, and the implicit DECIMAL coercion would silently overflow 
for realistic values.
+    Seq(Literal(1.0f), Literal(1.0d), Literal("1")).foreach { lit =>
+      val mismatch = 
NanosToTimestamp(lit).checkInputDataTypes().asInstanceOf[DataTypeMismatch]
+      assert(mismatch.errorSubClass == "UNEXPECTED_INPUT_TYPE")
+    }
+
+    // Pre-epoch / negative inputs use floor semantics, so nanosWithinMicro 
stays in [0, 999]:
+    // -1 ns floors to epochMicros = -1 with a 999 ns remainder.
+    checkEvaluation(tsNanos(BigInt(-1)), nanosVal(-1L, 999))
+    checkEvaluation(tsNanos(BigInt(-1000)), nanosVal(-1L, 0))
+    checkEvaluation(tsNanos(BigInt(-1500)), nanosVal(-2L, 500))
+
+    // NULL input.
+    checkEvaluation(
+      NanosToTimestamp(Literal.create(null, DecimalType(38, 0))), null)
+
+    // Full [0001, 9999] range: a DECIMAL nanosecond count far beyond a 64-bit 
BIGINT decodes
+    // losslessly back to the original value (proving the function spans the 
whole calendar range).
+    Seq(
+      localDateTimeToNanosVal(timestampNTZ(9999, 12, 31, 23, 59, 59, 
999999999)),
+      localDateTimeToNanosVal(timestampNTZ(1, 1, 1, 0, 0, 0, 1))
+    ).foreach { v =>
+      val n = BigInt(v.epochMicros) * NANOS_PER_MICROS + 
v.nanosWithinMicro.toInt
+      checkEvaluation(tsNanos(n), v)
+      // Round-trips with the inverse unix_nanos for the same full-range 
values.
+      checkEvaluation(UnixNanos(tsNanos(n)), Decimal(BigDecimal(n), 21, 0))
+    }
+
+    // Out-of-range input: epochMicros overflows a 64-bit long, surfaced as 
DATETIME_OVERFLOW.
+    checkErrorInExpression[SparkArithmeticException](
+      tsNanos(BigInt("10000000000000000000000000")),
+      condition = "DATETIME_OVERFLOW",
+      parameters = Map("operation" ->
+        "create a TIMESTAMP_LTZ(9) from 10000000000000000000000000 nanoseconds 
since the epoch"))
+  }
+
   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 3ff81b7f57f0..6297aece4cbb 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
@@ -255,6 +255,7 @@
 | org.apache.spark.sql.catalyst.expressions.Murmur3Hash | hash | SELECT 
hash('Spark', array(123), 2) | struct<hash(Spark, array(123), 2):int> |
 | org.apache.spark.sql.catalyst.expressions.NTile | ntile | SELECT a, b, 
ntile(2) OVER (PARTITION BY a ORDER BY b) FROM VALUES ('A1', 2), ('A1', 1), 
('A2', 3), ('A1', 1) tab(a, b) | struct<a:string,b:int,ntile(2) OVER (PARTITION 
BY a ORDER BY b ASC NULLS FIRST ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT 
ROW):int> |
 | org.apache.spark.sql.catalyst.expressions.NaNvl | nanvl | SELECT 
nanvl(cast('NaN' as double), 123) | struct<nanvl(CAST(NaN AS DOUBLE), 
123):double> |
+| org.apache.spark.sql.catalyst.expressions.NanosToTimestamp | timestamp_nanos 
| SELECT timestamp_nanos(1230219000123456789) | 
struct<timestamp_nanos(1230219000123456789):timestamp_ltz(9)> |
 | org.apache.spark.sql.catalyst.expressions.NextDay | next_day | SELECT 
next_day('2015-01-14', 'TU') | struct<next_day(2015-01-14, TU):date> |
 | org.apache.spark.sql.catalyst.expressions.Not | ! | SELECT ! true | 
struct<(NOT true):boolean> |
 | org.apache.spark.sql.catalyst.expressions.Not | not | SELECT not true | 
struct<(NOT true):boolean> |
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 55fe3d6065ec..4ae37b45aa73 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
@@ -774,3 +774,62 @@ 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
+
+
+-- !query
+SELECT timestamp_nanos(1230219000123456789)
+-- !query analysis
+Project [timestamp_nanos(1230219000123456789) AS 
timestamp_nanos(1230219000123456789)#x]
++- OneRowRelation
+
+
+-- !query
+SELECT timestamp_nanos(-1)
+-- !query analysis
+Project [timestamp_nanos(-1) AS timestamp_nanos(-1)#x]
++- OneRowRelation
+
+
+-- !query
+SELECT timestamp_nanos(253402300799999999999BD)
+-- !query analysis
+Project [timestamp_nanos(253402300799999999999) AS 
timestamp_nanos(253402300799999999999)#x]
++- OneRowRelation
+
+
+-- !query
+SELECT timestamp_nanos(10000000000000000000000000BD)
+-- !query analysis
+Project [timestamp_nanos(10000000000000000000000000) AS 
timestamp_nanos(10000000000000000000000000)#x]
++- OneRowRelation
+
+
+-- !query
+SELECT timestamp_nanos(1.0D)
+-- !query analysis
+org.apache.spark.sql.catalyst.ExtendedAnalysisException
+{
+  "errorClass" : "DATATYPE_MISMATCH.UNEXPECTED_INPUT_TYPE",
+  "sqlState" : "42K09",
+  "messageParameters" : {
+    "inputSql" : "\"1.0\"",
+    "inputType" : "\"DOUBLE\"",
+    "paramIndex" : "first",
+    "requiredType" : "(\"INTEGRAL\" or \"DECIMAL\")",
+    "sqlExpr" : "\"timestamp_nanos(1.0)\""
+  },
+  "queryContext" : [ {
+    "objectType" : "",
+    "objectName" : "",
+    "startIndex" : 8,
+    "stopIndex" : 28,
+    "fragment" : "timestamp_nanos(1.0D)"
+  } ]
+}
+
+
+-- !query
+SELECT timestamp_nanos(CAST(NULL AS BIGINT))
+-- !query analysis
+Project [timestamp_nanos(cast(null as bigint)) AS timestamp_nanos(CAST(NULL AS 
BIGINT))#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 3b43b0f756de..146486879fff 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
@@ -224,3 +224,17 @@ SELECT unix_nanos(TIMESTAMP_LTZ '9999-12-31 
23:59:59.999999999 UTC');
 SELECT unix_nanos(TIMESTAMP_LTZ '1960-01-01 00:00:00.000000001 UTC');
 -- NULL nanosecond timestamp.
 SELECT unix_nanos(NULL :: timestamp_ltz(9));
+
+-- SPARK-57526: timestamp_nanos builds a TIMESTAMP_LTZ(9) from a nanosecond 
count since the epoch.
+-- An integral argument is accepted directly; the LTZ result renders in the 
session zone.
+SELECT timestamp_nanos(1230219000123456789);
+-- Negative input floors toward the past, so the sub-microsecond remainder 
stays in [0, 999].
+SELECT timestamp_nanos(-1);
+-- DECIMAL input reaches beyond a 64-bit BIGINT, up to year 9999 (nanos ~ 
2.5e20).
+SELECT timestamp_nanos(253402300799999999999BD);
+-- Out-of-range input: epochMicros overflows a 64-bit long, so the conversion 
fails at runtime.
+SELECT timestamp_nanos(10000000000000000000000000BD);
+-- DOUBLE is rejected at analysis: only integral and DECIMAL nanosecond counts 
are accepted.
+SELECT timestamp_nanos(1.0D);
+-- NULL input.
+SELECT timestamp_nanos(CAST(NULL AS BIGINT));
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 50b22a2d4f44..6a3585c414c2 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
@@ -866,3 +866,76 @@ SELECT unix_nanos(NULL :: timestamp_ltz(9))
 struct<unix_nanos(CAST(NULL AS TIMESTAMP_LTZ(9))):decimal(21,0)>
 -- !query output
 NULL
+
+
+-- !query
+SELECT timestamp_nanos(1230219000123456789)
+-- !query schema
+struct<timestamp_nanos(1230219000123456789):timestamp_ltz(9)>
+-- !query output
+2008-12-25 07:30:00.123456789
+
+
+-- !query
+SELECT timestamp_nanos(-1)
+-- !query schema
+struct<timestamp_nanos(-1):timestamp_ltz(9)>
+-- !query output
+1969-12-31 15:59:59.999999999
+
+
+-- !query
+SELECT timestamp_nanos(253402300799999999999BD)
+-- !query schema
+struct<timestamp_nanos(253402300799999999999):timestamp_ltz(9)>
+-- !query output
+9999-12-31 15:59:59.999999999
+
+
+-- !query
+SELECT timestamp_nanos(10000000000000000000000000BD)
+-- !query schema
+struct<>
+-- !query output
+org.apache.spark.SparkArithmeticException
+{
+  "errorClass" : "DATETIME_OVERFLOW",
+  "sqlState" : "22008",
+  "messageParameters" : {
+    "operation" : "create a TIMESTAMP_LTZ(9) from 10000000000000000000000000 
nanoseconds since the epoch"
+  }
+}
+
+
+-- !query
+SELECT timestamp_nanos(1.0D)
+-- !query schema
+struct<>
+-- !query output
+org.apache.spark.sql.catalyst.ExtendedAnalysisException
+{
+  "errorClass" : "DATATYPE_MISMATCH.UNEXPECTED_INPUT_TYPE",
+  "sqlState" : "42K09",
+  "messageParameters" : {
+    "inputSql" : "\"1.0\"",
+    "inputType" : "\"DOUBLE\"",
+    "paramIndex" : "first",
+    "requiredType" : "(\"INTEGRAL\" or \"DECIMAL\")",
+    "sqlExpr" : "\"timestamp_nanos(1.0)\""
+  },
+  "queryContext" : [ {
+    "objectType" : "",
+    "objectName" : "",
+    "startIndex" : 8,
+    "stopIndex" : 28,
+    "fragment" : "timestamp_nanos(1.0D)"
+  } ]
+}
+
+
+-- !query
+SELECT timestamp_nanos(CAST(NULL AS BIGINT))
+-- !query schema
+struct<timestamp_nanos(CAST(NULL AS BIGINT)):timestamp_ltz(9)>
+-- !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 f42920a8a0ee..f19f1741479c 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
@@ -572,6 +572,42 @@ abstract class TimestampNanosFunctionsSuiteBase extends 
SharedSparkSession {
       checkAnswer(ltz.select(unix_nanos(col("c"))), Row(null))
     }
   }
+
+  test("SPARK-57526: timestamp_nanos builds nanosecond-precision TIMESTAMP_LTZ 
values") {
+    // 1230219000123456789 ns since the epoch -> 2008-12-25 15:30:00.123456789 
UTC. The result is a
+    // TIMESTAMP_LTZ(9); collecting it yields the absolute Instant regardless 
of the session zone.
+    val nanos = 1230219000123456789L
+    val instant = Instant.parse("2008-12-25T15:30:00.123456789Z")
+    val sqlRes = spark.sql(s"SELECT timestamp_nanos($nanos)")
+    val colRes = spark.range(1).select(timestamp_nanos(lit(nanos)))
+    // The SQL and Scala Column API agree, return the expected instant, and 
keep the LTZ(9) type.
+    checkAnswer(sqlRes, colRes)
+    checkAnswer(sqlRes, Row(instant))
+    assert(sqlRes.schema.head.dataType === TimestampLTZNanosType(9))
+
+    // A BIGINT argument is accepted directly through the dedicated 
IntegralType path (widened to
+    // BigInteger, no DECIMAL coercion), so the integral literal works without 
a cast.
+    checkAnswer(spark.sql(s"SELECT timestamp_nanos(${nanos}L)"), Row(instant))
+
+    // DECIMAL input reaches the full [0001, 9999] calendar range, beyond a 
64-bit BIGINT of nanos.
+    Seq(
+      Instant.parse("9999-12-31T23:59:59.999999999Z"),
+      Instant.parse("0001-01-01T00:00:00.000000001Z")
+    ).foreach { i =>
+      val n = BigInt(i.getEpochSecond) * 1000000000L + i.getNano
+      checkAnswer(
+        spark.range(1).select(timestamp_nanos(lit(BigDecimal(n).bigDecimal))),
+        Row(i))
+    }
+  }
+
+  test("SPARK-57526: timestamp_nanos over NULL input") {
+    val df = spark.createDataFrame(
+      spark.sparkContext.parallelize(Seq(Row(null))),
+      new StructType().add("n", LongType))
+    checkAnswer(df.select(timestamp_nanos(col("n"))), Row(null))
+    checkAnswer(df.selectExpr("timestamp_nanos(n)"), 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