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

cloud-fan 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 cc5c0c38df8e [SPARK-57848][SQL] Support the TIME data type in 
approx_top_k
cc5c0c38df8e is described below

commit cc5c0c38df8e8038621c7b56ebd61449fa084f91
Author: Rakesh Raushan <[email protected]>
AuthorDate: Mon Jul 20 22:21:26 2026 +0800

    [SPARK-57848][SQL] Support the TIME data type in approx_top_k
    
    ### What changes were proposed in this pull request?
    Add support for TimeType to approx_top_k. After this change approx_top_k 
accepts TimeType and returns correct top-k time values and columns.
    
    ### Why are the changes needed?
    The TIME data type was added by SPIP 
[SPARK-51162](https://issues.apache.org/jira/browse/SPARK-51162) and shipped in 
Spark 4.1.0, but the approx_top_k aggregate functions did not accept it. 
Calling approx_top_k on a TIME column failed with a type-check error, even 
though the operation is well-defined. This PR closes that gap.
    
    ### Does this PR introduce _any_ user-facing change?
    Yes, Now users can use approx_top_k over TimeType columns as well.
    
    ### How was this patch tested?
    UTs added.
    
    ### Was this patch authored or co-authored using generative AI tooling?
    Used claude code for UTs.
    
    Closes #57013 from iRakson/SPARK-57848.
    
    Authored-by: Rakesh Raushan <[email protected]>
    Signed-off-by: Wenchen Fan <[email protected]>
---
 .../aggregate/ApproxTopKAggregates.scala           |  11 +-
 .../org/apache/spark/sql/ApproxTopKSuite.scala     | 159 ++++++++++++++++++++-
 2 files changed, 164 insertions(+), 6 deletions(-)

diff --git 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/aggregate/ApproxTopKAggregates.scala
 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/aggregate/ApproxTopKAggregates.scala
index b09f242ccbac..2690a52b2d31 100644
--- 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/aggregate/ApproxTopKAggregates.scala
+++ 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/aggregate/ApproxTopKAggregates.scala
@@ -234,7 +234,8 @@ object ApproxTopK {
     itemType match {
       case _: BooleanType | _: ByteType | _: ShortType | _: IntegerType |
            _: LongType | _: FloatType | _: DoubleType | _: DateType |
-           _: TimestampType | _: TimestampNTZType | _: StringType | _: 
DecimalType => true
+           _: TimestampType | _: TimestampNTZType | _: StringType |
+           _: DecimalType | _: TimeType => true
       // BinaryType is not supported now, as ItemsSketch seems cannot count 
the frequency correctly
       case _ => false
     }
@@ -255,7 +256,7 @@ object ApproxTopK {
         new ItemsSketch[Boolean](maxMapSize).asInstanceOf[ItemsSketch[Any]]
       case _: ByteType | _: ShortType | _: IntegerType | _: FloatType | _: 
DateType =>
         new ItemsSketch[Number](maxMapSize).asInstanceOf[ItemsSketch[Any]]
-      case _: LongType | _: TimestampType | _: TimestampNTZType =>
+      case _: LongType | _: TimestampType | _: TimestampNTZType | _: TimeType 
=>
         new ItemsSketch[Long](maxMapSize).asInstanceOf[ItemsSketch[Any]]
       case _: DoubleType =>
         new ItemsSketch[Double](maxMapSize).asInstanceOf[ItemsSketch[Any]]
@@ -275,7 +276,7 @@ object ApproxTopK {
       case _: BooleanType => new 
ArrayOfBooleansSerDe().asInstanceOf[ArrayOfItemsSerDe[Any]]
       case _: ByteType | _: ShortType | _: IntegerType | _: FloatType | _: 
DateType =>
         new ArrayOfNumbersSerDe().asInstanceOf[ArrayOfItemsSerDe[Any]]
-      case _: LongType | _: TimestampType | _: TimestampNTZType =>
+      case _: LongType | _: TimestampType | _: TimestampNTZType | _: TimeType 
=>
         new ArrayOfLongsSerDe().asInstanceOf[ArrayOfItemsSerDe[Any]]
       case _: DoubleType =>
         new ArrayOfDoublesSerDe().asInstanceOf[ArrayOfItemsSerDe[Any]]
@@ -378,6 +379,8 @@ class ApproxTopKAggregateBuffer[T](val sketch: 
ItemsSketch[T], private var nullC
           sketch.asInstanceOf[ItemsSketch[Long]].update(v.asInstanceOf[Long])
         case _: TimestampNTZType =>
           sketch.asInstanceOf[ItemsSketch[Long]].update(v.asInstanceOf[Long])
+        case _: TimeType =>
+          sketch.asInstanceOf[ItemsSketch[Long]].update(v.asInstanceOf[Long])
         case st: StringType =>
           val orig = v.asInstanceOf[UTF8String]
           if (UnsafeRowUtils.isBinaryStable(st)) {
@@ -453,7 +456,7 @@ class ApproxTopKAggregateBuffer[T](val sketch: 
ItemsSketch[T], private var nullC
         val item: Any = itemDataType match {
           case _: BooleanType | _: ByteType | _: ShortType | _: IntegerType |
                _: LongType | _: FloatType | _: DoubleType | _: DecimalType |
-               _: DateType | _: TimestampType | _: TimestampNTZType =>
+               _: DateType | _: TimestampType | _: TimestampNTZType | _: 
TimeType =>
             curFrequentItem.getItem
           case _: StringType =>
             curFrequentItem.getItem match {
diff --git a/sql/core/src/test/scala/org/apache/spark/sql/ApproxTopKSuite.scala 
b/sql/core/src/test/scala/org/apache/spark/sql/ApproxTopKSuite.scala
index 99efbd29c753..98aba80682e6 100644
--- a/sql/core/src/test/scala/org/apache/spark/sql/ApproxTopKSuite.scala
+++ b/sql/core/src/test/scala/org/apache/spark/sql/ApproxTopKSuite.scala
@@ -18,13 +18,13 @@
 package org.apache.spark.sql
 
 import java.sql.{Date, Timestamp}
-import java.time.LocalDateTime
+import java.time.{LocalDateTime, LocalTime}
 
 import org.apache.spark.{SparkArithmeticException, SparkRuntimeException}
 import org.apache.spark.sql.catalyst.ExtendedAnalysisException
 import org.apache.spark.sql.errors.DataTypeErrors.toSQLType
 import org.apache.spark.sql.test.SharedSparkSession
-import org.apache.spark.sql.types.{BooleanType, ByteType, DataType, DateType, 
DecimalType, DoubleType, FloatType, IntegerType, LongType, ShortType, 
StringType, TimestampNTZType, TimestampType}
+import org.apache.spark.sql.types.{BooleanType, ByteType, DataType, DateType, 
DecimalType, DoubleType, FloatType, IntegerType, LongType, ShortType, 
StringType, TimestampNTZType, TimestampType, TimeType}
 
 
 class ApproxTopKSuite extends SharedSparkSession {
@@ -63,6 +63,11 @@ class ApproxTopKSuite extends SharedSparkSession {
       "TIMESTAMP_NTZ'2025-01-01 00:00:00', TIMESTAMP_NTZ'2025-01-02 00:00:00'",
       Seq(Row(LocalDateTime.of(2025, 1, 1, 0, 0), 3),
         Row(LocalDateTime.of(2025, 1, 2, 0, 0), 1))), // Timestamp_ntz
+    ("TIME'00:00:00', TIME'00:00:00', TIME'06:00:00', TIME'06:00:00', 
TIME'06:00:00', " +
+      "TIME'08:30:00.123456', TIME'10:00:00', TIME'10:00:00', TIME'11:00:00'",
+      Seq(Row(LocalTime.of(6, 0, 0), 3), Row(LocalTime.of(0, 0, 0), 2),
+        Row(LocalTime.of(10, 0, 0), 2), Row(LocalTime.of(11, 0, 0), 1),
+        Row(LocalTime.of(8, 30, 0, 123456000), 1))), // Time
     ("CAST(0.0 AS DECIMAL(4, 1)), CAST(0.0 AS DECIMAL(4, 1)), " +
       "CAST(0.0 AS DECIMAL(4, 1)), CAST(1.0 AS DECIMAL(4, 1)), " +
       "CAST(1.0 AS DECIMAL(4, 1)), CAST(2.0 AS DECIMAL(4, 1))",
@@ -554,6 +559,22 @@ class ApproxTopKSuite extends SharedSparkSession {
     )
   )
 
+  // TIME has no common type with DATE/TIMESTAMP/TIMESTAMP_NTZ (unlike those 
three, which all
+  // mutually widen and so only fail at approx_top_k_combine runtime, not at 
UNION analysis), so
+  // it cannot join mixedDateTimeTypes: "among different ... types - fail at 
combine" below
+  // asserts every pair in that Seq unions successfully and only fails at the 
runtime combine.
+  // The two TIME entries below (different precisions) do widen against each 
other at UNION
+  // (findWiderDateTimeType(TimeType(p1), TimeType(p2)) = TimeType(max(p1, 
p2))), mirroring
+  // mixedDateTimeTypes's own internal widening -- see
+  // "SPARK-57848: among different time precisions - fail at combine" below.
+  val mixedTimeTypes: Seq[(DataType, String, Seq[String])] = Seq(
+    (TimeType(), "TIME(6)",
+      Seq("TIME'12:00:00'", "TIME'12:00:00'", "TIME'13:00:00'")),
+    (TimeType(3), "TIME(3)",
+      Seq("CAST('12:00:00.123' AS TIME(3))", "CAST('12:00:00.123' AS TIME(3))",
+        "CAST('13:00:00.123' AS TIME(3))"))
+  )
+
   // positive tests for approx_top_k_combine on every types
   gridTest("SPARK-52798: same type, same size, specified combine size - 
success")(itemsWithTopK) {
     case (input, expected) =>
@@ -664,6 +685,28 @@ class ApproxTopKSuite extends SharedSparkSession {
     checkMixedTypeError(mixedDateTimeTypes)
   }
 
+  // TIME counterpart to the test above: TIME(3) and TIME(6) widen to TIME(6) 
at UNION via
+  // findWiderDateTimeType, so the UNION succeeds, but the two sketches still 
carry their
+  // original, different itemDataType (time(3) vs time(6)), so combine fails 
at runtime.
+  test("SPARK-57848: among different time precisions - fail at combine") {
+    for (i <- 0 until mixedTimeTypes.size - 1) {
+      for (j <- i + 1 until mixedTimeTypes.size) {
+        val (type1, _, seq1) = mixedTimeTypes(i)
+        val (type2, _, seq2) = mixedTimeTypes(j)
+        withView("accumulation1", "accumulation2", "unioned") {
+          setupMixedTypeAccumulation(seq1, seq2)
+          checkError(
+            exception = intercept[SparkRuntimeException] {
+              sql("SELECT approx_top_k_combine(acc, 30) as com FROM 
unioned;").collect()
+            },
+            condition = "APPROX_TOP_K_SKETCH_TYPE_NOT_MATCH",
+            parameters = Map("type1" -> toSQLType(type1), "type2" -> 
toSQLType(type2))
+          )
+        }
+      }
+    }
+  }
+
   // enumerate all combinations of number and datetime types
   gridTest("SPARK-52798: number vs datetime - fail on UNION")(
     for {
@@ -776,6 +819,118 @@ class ApproxTopKSuite extends SharedSparkSession {
       )
   }
 
+  // TIME has no common type with DATE/TIMESTAMP/TIMESTAMP_NTZ, so mixing it 
with any of them
+  // fails at UNION analysis, unlike combining two of those three which fails 
at combine runtime.
+  gridTest("SPARK-57848: time vs datetime - fail on UNION")(
+    for {
+      (type1, typeName1, seq1) <- mixedTimeTypes
+      (type2, typeName2, seq2) <- mixedDateTimeTypes
+    } yield ((type1, typeName1, seq1), (type2, typeName2, seq2))) {
+    case ((_, type1, seq1), (_, type2, seq2)) =>
+      checkError(
+        exception = intercept[ExtendedAnalysisException] {
+          withView("accumulation1", "accumulation2", "unioned") {
+            setupMixedTypeAccumulation(seq1, seq2)
+          }
+        },
+        condition = "INCOMPATIBLE_COLUMN_TYPE",
+        parameters = Map(
+          "tableOrdinalNumber" -> "second",
+          "columnOrdinalNumber" -> "first",
+          "dataType2" -> ("\"STRUCT<sketch: BINARY NOT NULL, maxItemsTracked: 
INT NOT NULL, " +
+            "itemDataType: " + type1 + ", itemDataTypeDDL: STRING NOT 
NULL>\""),
+          "operator" -> "UNION",
+          "hint" -> "",
+          "dataType1" -> ("\"STRUCT<sketch: BINARY NOT NULL, maxItemsTracked: 
INT NOT NULL, " +
+            "itemDataType: " + type2 + ", itemDataTypeDDL: STRING NOT NULL>\"")
+        ),
+        queryContext = Array(
+          ExpectedContext(
+            "SELECT acc from accumulation1 UNION ALL SELECT acc FROM 
accumulation2", 0, 68))
+      )
+  }
+
+  // TIME has no common type with any number type either, so it fails at 
UNION, same as
+  // number vs datetime above.
+  gridTest("SPARK-57848: time vs number - fail on UNION")(
+    for {
+      (type1, typeName1, seq1) <- mixedTimeTypes
+      (type2, typeName2, seq2) <- mixedNumberTypes
+    } yield ((type1, typeName1, seq1), (type2, typeName2, seq2))) {
+    case ((_, type1, seq1), (_, type2, seq2)) =>
+      checkError(
+        exception = intercept[ExtendedAnalysisException] {
+          withView("accumulation1", "accumulation2", "unioned") {
+            setupMixedTypeAccumulation(seq1, seq2)
+          }
+        },
+        condition = "INCOMPATIBLE_COLUMN_TYPE",
+        parameters = Map(
+          "tableOrdinalNumber" -> "second",
+          "columnOrdinalNumber" -> "first",
+          "dataType2" -> ("\"STRUCT<sketch: BINARY NOT NULL, maxItemsTracked: 
INT NOT NULL, " +
+            "itemDataType: " + type1 + ", itemDataTypeDDL: STRING NOT 
NULL>\""),
+          "operator" -> "UNION",
+          "hint" -> "",
+          "dataType1" -> ("\"STRUCT<sketch: BINARY NOT NULL, maxItemsTracked: 
INT NOT NULL, " +
+            "itemDataType: " + type2 + ", itemDataTypeDDL: STRING NOT NULL>\"")
+        ),
+        queryContext = Array(
+          ExpectedContext(
+            "SELECT acc from accumulation1 UNION ALL SELECT acc FROM 
accumulation2", 0, 68))
+      )
+  }
+
+  gridTest("SPARK-57848: time vs string - fail at combine")(mixedTimeTypes) {
+    case (type1, _, seq1) =>
+      withView("accumulation1", "accumulation2", "unioned") {
+        setupMixedTypeAccumulation(
+          seq1, Seq("'a'", "'b'", "'c'", "'c'", "'c'", "'c'", "'d'", "'d'"))
+        checkError(
+          exception = intercept[SparkRuntimeException] {
+            sql("SELECT approx_top_k_combine(acc, 30) as com FROM 
unioned;").collect()
+          },
+          condition = "APPROX_TOP_K_SKETCH_TYPE_NOT_MATCH",
+          parameters = Map("type1" -> toSQLType(type1), "type2" -> 
toSQLType(StringType))
+        )
+      }
+  }
+
+  gridTest("SPARK-57848: time vs boolean - fail at UNION")(mixedTimeTypes) {
+    case (_, type1, seq1) =>
+      val seq2 = Seq("(true)", "(true)", "(false)", "(false)")
+      checkError(
+        exception = intercept[ExtendedAnalysisException] {
+          withView("accumulation1", "accumulation2", "unioned") {
+            setupMixedTypeAccumulation(seq1, seq2)
+          }
+        },
+        condition = "INCOMPATIBLE_COLUMN_TYPE",
+        parameters = Map(
+          "tableOrdinalNumber" -> "second",
+          "columnOrdinalNumber" -> "first",
+          "dataType2" -> ("\"STRUCT<sketch: BINARY NOT NULL, maxItemsTracked: 
INT NOT NULL, " +
+            "itemDataType: " + type1 + ", itemDataTypeDDL: STRING NOT 
NULL>\""),
+          "operator" -> "UNION",
+          "hint" -> "",
+          "dataType1" -> ("\"STRUCT<sketch: BINARY NOT NULL, maxItemsTracked: 
INT NOT NULL, " +
+            "itemDataType: BOOLEAN, itemDataTypeDDL: STRING NOT NULL>\"")
+        ),
+        queryContext = Array(
+          ExpectedContext(
+            "SELECT acc from accumulation1 UNION ALL SELECT acc FROM 
accumulation2", 0, 68))
+      )
+  }
+
+  test("SPARK-57848: approx_top_k over time column") {
+    val res = sql(
+      "SELECT approx_top_k(expr, 10, 100) FROM VALUES (TIME '06:00:00'), (TIME 
'06:00:00'), " +
+        "(TIME '08:00:00'), (TIME '08:00:00'), (TIME '08:00:00'), (TIME 
'10:00:00') AS tab(expr);"
+    )
+    checkAnswer(res, Row(Seq(Row(LocalTime.of(8, 0, 0), 3), 
Row(LocalTime.of(6, 0, 0), 2),
+      Row(LocalTime.of(10, 0, 0), 1))))
+  }
+
   test("SPARK-52798: string vs boolean - fail at combine") {
     withSQLConf("spark.sql.ansi.enabled" -> "true") {
       val seq1 = Seq("'a'", "'b'", "'c'", "'c'", "'c'", "'c'", "'d'", "'d'")


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

Reply via email to