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 77b603b72518 [SPARK-57809][SQL] Support nanosecond-precision 
timestamps in collect_list/listagg ordering safety
77b603b72518 is described below

commit 77b603b72518b9b4242e22254bc0bcd32d9077ae
Author: Rakesh Raushan <[email protected]>
AuthorDate: Thu Jul 2 17:17:00 2026 +0200

    [SPARK-57809][SQL] Support nanosecond-precision timestamps in 
collect_list/listagg ordering safety
    
    ### What changes were proposed in this pull request?
    Add nanosecond arms so the ORDER BY-cast optimization applies to NTZ nanos 
and correctly excludes LTZ nanos, matching the microsecond behavior.
    
    ### Why are the changes needed?
    collect.scala isCastEqualityPreserving (~L750-762) marks TimestampNTZType 
as cast-equality-preserving and TimestampType (LTZ, DST) as unsafe, but has no 
arm for TimestampNTZNanosType / TimestampLTZNanosType, so both fall to case _ 
=> false. NTZ nanos should be treated like NTZ micro (safe); LTZ nanos like LTZ 
micro.
    
    ### Does this PR introduce _any_ user-facing change?
    Yes, nanosecond precision timestamps behaviour would be same as microsecond 
timestamp behaviour now.
    
    ### How was this patch tested?
    Added UT.
    
    ### Was this patch authored or co-authored using generative AI tooling?
    Claude for writing UT.
    
    Closes #56946 from iRakson/SPARK-57809.
    
    Authored-by: Rakesh Raushan <[email protected]>
    Signed-off-by: Max Gekk <[email protected]>
---
 .../catalyst/expressions/aggregate/collect.scala   |  6 +--
 .../sql/TimestampNanosFunctionsSuiteBase.scala     | 58 ++++++++++++++++++++++
 2 files changed, 61 insertions(+), 3 deletions(-)

diff --git 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/aggregate/collect.scala
 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/aggregate/collect.scala
index 8f15acb536e5..8a370e9a9dc7 100644
--- 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/aggregate/collect.scala
+++ 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/aggregate/collect.scala
@@ -747,7 +747,7 @@ case class ListAgg(
   private def isCastEqualityPreserving(dt: DataType): Boolean = dt match {
     case _: IntegerType | LongType | ShortType | ByteType => true
     case _: DecimalType => true
-    case _: DateType | TimestampNTZType => true
+    case _: DateType | TimestampNTZType | _: TimestampNTZNanosType => true
     case _: TimeType => true
     case _: CalendarIntervalType => true
     case _: YearMonthIntervalType => true
@@ -757,8 +757,8 @@ case class ListAgg(
     case st: StringType => st.isUTF8BinaryCollation
     case _: DoubleType | FloatType => false
     // During DST fall-back, two distinct UTC epochs can format to the same 
local time string
-    // because the default format omits the timezone offset. TimestampNTZType 
is safe (uses UTC).
-    case _: TimestampType => false
+    // because the default format omits the timezone offset. NTZ types are 
safe (use UTC).
+    case _: TimestampType | _: TimestampLTZNanosType => false
     case _ => false
   }
 
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 f19f1741479c..ee350f5c5e2d 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
@@ -608,6 +608,64 @@ abstract class TimestampNanosFunctionsSuiteBase extends 
SharedSparkSession {
     checkAnswer(df.select(timestamp_nanos(col("n"))), Row(null))
     checkAnswer(df.selectExpr("timestamp_nanos(n)"), Row(null))
   }
+
+  test("SPARK-57809: listagg(distinct cast(ts as string)) within group (order 
by ts) " +
+    "over nanosecond-precision timestamps") {
+    // isCastEqualityPreserving: NTZ nanos is safe (UTC, no DST ambiguity), 
LTZ nanos is unsafe
+    // (same DST fall-back risk as micro TIMESTAMP_LTZ). This mirrors the 
micro-precision behavior:
+    // TimestampNTZType -> true, TimestampType -> false.
+    Seq(7, 8, 9).foreach { p =>
+      val ntzDF = spark.createDataFrame(
+        spark.sparkContext.parallelize(Seq(
+          Row(LocalDateTime.parse("2020-01-01T12:00:00.100000000")),
+          Row(LocalDateTime.parse("2020-01-02T12:00:00.200000000")))),
+        new StructType().add("ts", TimestampNTZNanosType(p)))
+
+      // NTZ nanos: cast to string is equality-preserving, so LISTAGG(DISTINCT 
...) is allowed.
+      withSQLConf(SQLConf.LISTAGG_ALLOW_DISTINCT_CAST_WITH_ORDER.key -> 
"true") {
+        val result = ntzDF.selectExpr(
+          "listagg(distinct cast(ts as string), ', ') within group (order by 
ts)").collect()
+        assert(result.length == 1 && result.head.getString(0) != null,
+          s"NTZ nanos p=$p: listagg should succeed with a non-null result")
+      }
+
+      val ltzDF = spark.createDataFrame(
+        spark.sparkContext.parallelize(Seq(
+          Row(Instant.parse("2020-01-01T20:00:00.100000000Z")),
+          Row(Instant.parse("2020-01-02T20:00:00.200000000Z")))),
+        new StructType().add("ts", TimestampLTZNanosType(p)))
+
+      withSQLConf(SQLConf.LISTAGG_ALLOW_DISTINCT_CAST_WITH_ORDER.key -> 
"true") {
+        checkError(
+          exception = intercept[AnalysisException] {
+            ltzDF.selectExpr(
+              "listagg(distinct cast(ts as string)) within group (order by 
ts)")
+          },
+          condition =
+            
"INVALID_WITHIN_GROUP_EXPRESSION.MISMATCH_WITH_DISTINCT_INPUT_UNSAFE_CAST",
+          parameters = Map(
+            "funcName" -> "`listagg`",
+            "inputType" -> s""""TIMESTAMP_LTZ($p)"""",
+            "castType" -> "\"STRING\""
+          )
+        )
+      }
+      withSQLConf(SQLConf.LISTAGG_ALLOW_DISTINCT_CAST_WITH_ORDER.key -> 
"false") {
+        checkError(
+          exception = intercept[AnalysisException] {
+            ltzDF.selectExpr(
+              "listagg(distinct cast(ts as string)) within group (order by 
ts)")
+          },
+          condition = 
"INVALID_WITHIN_GROUP_EXPRESSION.MISMATCH_WITH_DISTINCT_INPUT",
+          parameters = Map(
+            "funcName" -> "`listagg`",
+            "funcArg" -> "\"CAST(ts AS STRING)\"",
+            "orderingExpr" -> "\"ts\""
+          )
+        )
+      }
+    }
+  }
 }
 
 // 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