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 5e2c8eff2e00 [SPARK-57557][SQL] Support the TIME data type in quantile 
aggregates
5e2c8eff2e00 is described below

commit 5e2c8eff2e009270eee5b556ec45012f7721e830
Author: Vaibhav Garg <[email protected]>
AuthorDate: Wed Jul 1 11:35:38 2026 +0200

    [SPARK-57557][SQL] Support the TIME data type in quantile aggregates
    
    ### What changes were proposed in this pull request?
    
    This PR adds support for the TIME data type (`TimeType`) as an input to the 
core quantile aggregate expressions. SPARK-57557 is a sub-task of the umbrella 
SPARK-57550 (extend TIME type support across Spark).
    
    The following expressions now accept TIME columns and return TIME:
    
    - `percentile`, `median`, `percentile_cont`, `percentile_disc` (exact 
quantiles, `percentiles.scala`)
    - `percentile_approx` / `approx_percentile` (`ApproximatePercentile`)
    - `histogram_numeric` (`HistogramNumeric`)
    
    TIME's internal representation is a `Long` (nanoseconds of day), so it 
flows through the existing value↔double conversion paths exactly like the other 
ordered, `Long`-backed types. The change is mechanical:
    
    - `TimeType` (via `AnyTimeType`) added to each expression's `inputTypes` / 
`TypeCollection`
    - A `TimeType` branch added to the update (`Long` → `Double`) and eval 
(`Double` → `Long`) conversions
    - The return type uses `child.dataType`, preserving the input precision 
(e.g. `median` over a `TIME(3)` column returns `TIME(3)`)
    
    Design notes (called out so they can be challenged in review):
    
    - **Scope:** This PR covers only the core quantile aggregates above. The 
Apache DataSketches aggregates (HLL, Theta, and the KLL quantile sketches) are 
intentionally deferred and can be added in a follow-up.
    - **Interpolation:** For the exact `percentile`/`median` (continuous 
distribution), interpolated results mirror existing numeric/interval behavior — 
e.g. the median of two TIME values returns their midpoint, which may be a value 
not present in the data; sub-nanosecond fractions are truncated.
    - **Template mirrored:** For the exact `percentile`/`median` and 
`histogram_numeric`, TIMESTAMP is not currently supported either, so the 
implementation mirrors DAY-TIME INTERVAL (also `Long`-internal). For 
`percentile_approx` and `histogram_numeric`, TIMESTAMP was already supported 
and TIME mirrors it directly.
    
    ### Why are the changes needed?
    
    The TIME data type was added by SPIP SPARK-51162 and shipped in Spark 
4.1.0, but the quantile aggregate functions did not accept it. Calling 
`percentile`, `median`, `percentile_approx`, or `histogram_numeric` on a TIME 
column failed with a type-check error, even though the operation is 
well-defined: TIME is internally a `Long` (nanoseconds of day) and quantile 
computation over an ordered numeric value applies directly. This PR closes that 
gap.
    
    ### Does this PR introduce any user-facing change?
    
    Yes. `percentile`, `median`, `percentile_cont`, `percentile_disc`, 
`percentile_approx` / `approx_percentile`, and `histogram_numeric` now accept 
TIME columns and return TIME values. Previously these expressions rejected TIME 
input with a type-check error.
    
    ### How was this patch tested?
    
    All scoped suites were run locally and passed:
    
    - `catalyst/testOnly 
org.apache.spark.sql.catalyst.expressions.aggregate.ApproximatePercentileSuite 
org.apache.spark.sql.catalyst.expressions.aggregate.PercentileSuite 
org.apache.spark.sql.catalyst.expressions.aggregate.HistogramNumericSuite` — 
all tests passed (each suite has a new `test("SPARK-57557: ...")` covering TIME 
input, return type, precision preservation, and median interpolation)
    - `sql/testOnly org.apache.spark.sql.ApproximatePercentileQuerySuite` — 20 
tests passed (new `SPARK-57557: percentile_approx supports TIME type` test, 
scalar and array-of-percentiles paths)
    - `SPARK_GENERATE_GOLDEN_FILES=1 sql/testOnly 
org.apache.spark.sql.SQLQueryTestSuite -- -z percentiles.sql` — golden files 
regenerated and manually reviewed; a new TIME section was added to 
`percentiles.sql`
    - One pre-existing `PercentileSuite` assertion (the accepted-types error 
message) was updated to include TIME
    
    The full lint/scalastyle and complete test matrix run on GitHub Actions.
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    Yes. Generated using Kiro (Claude Opus 4.8)
    
    Closes #56889 from vboo123/SPARK-57557.
    
    Lead-authored-by: Vaibhav Garg <[email protected]>
    Co-authored-by: Vaibhav Garg <[email protected]>
    Signed-off-by: Max Gekk <[email protected]>
    (cherry picked from commit 762c578cee28a9dceac98c90bee6bb78da37fc85)
    Signed-off-by: Max Gekk <[email protected]>
---
 .../aggregate/ApproximatePercentile.scala          | 15 ++++----
 .../expressions/aggregate/HistogramNumeric.scala   |  9 ++---
 .../expressions/aggregate/percentiles.scala        | 16 +++++----
 .../aggregate/ApproximatePercentileSuite.scala     | 19 +++++++++-
 .../aggregate/HistogramNumericSuite.scala          | 18 ++++++++++
 .../expressions/aggregate/PercentileSuite.scala    | 23 +++++++++---
 .../sql-tests/analyzer-results/percentiles.sql.out | 42 ++++++++++++++++++++++
 .../resources/sql-tests/inputs/percentiles.sql     | 14 ++++++++
 .../sql-tests/results/percentiles.sql.out          | 31 ++++++++++++++++
 .../sql/ApproximatePercentileQuerySuite.scala      | 20 ++++++++++-
 10 files changed, 183 insertions(+), 24 deletions(-)

diff --git 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/aggregate/ApproximatePercentile.scala
 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/aggregate/ApproximatePercentile.scala
index 8ad062ab0e2f..19e30c53b436 100644
--- 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/aggregate/ApproximatePercentile.scala
+++ 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/aggregate/ApproximatePercentile.scala
@@ -55,8 +55,8 @@ import org.apache.spark.util.ArrayImplicits._
 // scalastyle:off line.size.limit
 @ExpressionDescription(
   usage = """
-    _FUNC_(col, percentage [, accuracy]) - Returns the approximate 
`percentile` of the numeric or
-      ansi interval column `col` which is the smallest value in the ordered 
`col` values (sorted
+    _FUNC_(col, percentage [, accuracy]) - Returns the approximate 
`percentile` of the numeric,
+      ansi interval or TIME column `col` which is the smallest value in the 
ordered `col` values (sorted
       from least to greatest) such that no more than `percentage` of `col` 
values is less than
       the value or equal to that value. The value of percentage must be 
between 0.0 and 1.0.
       The `accuracy` parameter (default: 10000) is a positive numeric literal 
which controls
@@ -102,10 +102,10 @@ case class ApproximatePercentile(
   private lazy val accuracy: Long = accuracyNum.longValue
 
   override def inputTypes: Seq[AbstractDataType] = {
-    // Support NumericType, DateType, TimestampType and TimestampNTZType since 
their internal types
-    // are all numeric, and can be easily cast to double for processing.
+    // Support NumericType, DateType, TimestampType, TimestampNTZType and 
TimeType since their
+    // internal types are all numeric, and can be easily cast to double for 
processing.
     Seq(TypeCollection(NumericType, DateType, TimestampType, TimestampNTZType,
-      YearMonthIntervalType, DayTimeIntervalType),
+      YearMonthIntervalType, DayTimeIntervalType, AnyTimeType),
       TypeCollection(DoubleType, ArrayType(DoubleType, containsNull = false)), 
IntegralType)
   }
 
@@ -183,7 +183,7 @@ case class ApproximatePercentile(
       // Convert the value to a double value
       val doubleValue = child.dataType match {
         case DateType | _: YearMonthIntervalType => 
value.asInstanceOf[Int].toDouble
-        case TimestampType | TimestampNTZType | _: DayTimeIntervalType =>
+        case TimestampType | TimestampNTZType | _: DayTimeIntervalType | _: 
TimeType =>
           value.asInstanceOf[Long].toDouble
         case n: NumericType =>
           PhysicalNumericType.numeric(n)
@@ -205,7 +205,8 @@ case class ApproximatePercentile(
     val doubleResult = buffer.getPercentiles(percentages)
     val result = child.dataType match {
       case DateType | _: YearMonthIntervalType => doubleResult.map(_.toInt)
-      case TimestampType | TimestampNTZType | _: DayTimeIntervalType => 
doubleResult.map(_.toLong)
+      case TimestampType | TimestampNTZType | _: DayTimeIntervalType | _: 
TimeType =>
+        doubleResult.map(_.toLong)
       case ByteType => doubleResult.map(_.toByte)
       case ShortType => doubleResult.map(_.toShort)
       case IntegerType => doubleResult.map(_.toInt)
diff --git 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/aggregate/HistogramNumeric.scala
 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/aggregate/HistogramNumeric.scala
index 142f4a4eae4c..dadf7ac53c59 100644
--- 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/aggregate/HistogramNumeric.scala
+++ 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/aggregate/HistogramNumeric.scala
@@ -78,11 +78,11 @@ case class HistogramNumeric(
   private lazy val propagateInputType: Boolean = 
SQLConf.get.histogramNumericPropagateInputType
 
   override def inputTypes: Seq[AbstractDataType] = {
-    // Support NumericType, DateType, TimestampType and TimestampNTZType, 
YearMonthIntervalType,
-    // DayTimeIntervalType since their internal types are all numeric,
+    // Support NumericType, DateType, TimestampType, TimestampNTZType, 
TimeType,
+    // YearMonthIntervalType, DayTimeIntervalType since their internal types 
are all numeric,
     // and can be easily cast to double for processing.
     Seq(TypeCollection(NumericType, DateType, TimestampType, TimestampNTZType,
-      YearMonthIntervalType, DayTimeIntervalType), IntegerType)
+      YearMonthIntervalType, DayTimeIntervalType, AnyTimeType), IntegerType)
   }
 
   override def checkInputDataTypes(): TypeCheckResult = {
@@ -163,7 +163,8 @@ case class HistogramNumeric(
               coord.x.toInt
             case FloatType => coord.x.toFloat
             case ShortType => coord.x.toShort
-            case _: DayTimeIntervalType | LongType | TimestampType | 
TimestampNTZType =>
+            case _: DayTimeIntervalType | LongType | TimestampType | 
TimestampNTZType
+                | _: TimeType =>
               coord.x.toLong
             case d: DecimalType =>
               val bigDecimal = BigDecimal
diff --git 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/aggregate/percentiles.scala
 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/aggregate/percentiles.scala
index 6dfa1b499df2..ac351db9e471 100644
--- 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/aggregate/percentiles.scala
+++ 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/aggregate/percentiles.scala
@@ -65,6 +65,7 @@ abstract class PercentileBase
     val resultType = child.dataType match {
       case _: YearMonthIntervalType => YearMonthIntervalType()
       case _: DayTimeIntervalType => DayTimeIntervalType()
+      case t: TimeType => t
       case _ => DoubleType
     }
     if (returnPercentileArray) ArrayType(resultType, false) else resultType
@@ -75,7 +76,7 @@ abstract class PercentileBase
       case _: ArrayType => ArrayType(DoubleType, false)
       case _ => DoubleType
     }
-    Seq(NumericAndAnsiInterval, percentageExpType, IntegralType)
+    Seq(TypeCollection(NumericAndAnsiInterval, AnyTimeType), 
percentageExpType, IntegralType)
   }
 
   // Check the inputTypes are valid, and the percentageExpression satisfies:
@@ -175,6 +176,7 @@ abstract class PercentileBase
     val results = child.dataType match {
       case _: YearMonthIntervalType => percentiles.map(_.toInt)
       case _: DayTimeIntervalType => percentiles.map(_.toLong)
+      case _: TimeType => percentiles.map(_.toLong)
       case _ => percentiles
     }
     if (percentiles.isEmpty) {
@@ -255,8 +257,8 @@ abstract class PercentileBase
 @ExpressionDescription(
   usage =
     """
-      _FUNC_(col, percentage [, frequency]) - Returns the exact percentile 
value of numeric
-       or ANSI interval column `col` at the given percentage. The value of 
percentage must be
+      _FUNC_(col, percentage [, frequency]) - Returns the exact percentile 
value of numeric,
+       ANSI interval or TIME column `col` at the given percentage. The value 
of percentage must be
        between 0.0 and 1.0. The value of frequency should be positive integral
 
       _FUNC_(col, array(percentage1 [, percentage2]...) [, frequency]) - 
Returns the exact
@@ -328,7 +330,7 @@ case class Percentile(
 }
 
 @ExpressionDescription(
-  usage = "_FUNC_(col) - Returns the median of numeric or ANSI interval column 
`col`.",
+  usage = "_FUNC_(col) - Returns the median of numeric, ANSI interval or TIME 
column `col`.",
   examples = """
     Examples:
       > SELECT _FUNC_(col) FROM VALUES (0), (10) AS tab(col);
@@ -353,7 +355,7 @@ case class Median(child: Expression)
 
 /**
  * Return a percentile value based on a continuous distribution of
- * numeric or ANSI interval column at the given percentage (specified in ORDER 
BY clause).
+ * numeric, ANSI interval or TIME column at the given percentage (specified in 
ORDER BY clause).
  * The value of percentage must be between 0.0 and 1.0.
  */
 case class PercentileCont(left: Expression, right: Expression, reverse: 
Boolean = false)
@@ -480,7 +482,7 @@ case class PercentileDisc(
 // scalastyle:off line.size.limit
 @ExpressionDescription(
   usage = "_FUNC_(percentage) WITHIN GROUP (ORDER BY col) - Return a 
percentile value based on " +
-    "a continuous distribution of numeric or ANSI interval column `col` at the 
given " +
+    "a continuous distribution of numeric, ANSI interval or TIME column `col` 
at the given " +
     "`percentage` (specified in ORDER BY clause).",
   examples = """
     Examples:
@@ -506,7 +508,7 @@ object PercentileContBuilder extends ExpressionBuilder {
 // scalastyle:off line.size.limit
 @ExpressionDescription(
   usage = "_FUNC_(percentage) WITHIN GROUP (ORDER BY col) - Return a 
percentile value based on " +
-    "a discrete distribution of numeric or ANSI interval column `col` at the 
given " +
+    "a discrete distribution of numeric, ANSI interval or TIME column `col` at 
the given " +
     "`percentage` (specified in ORDER BY clause).",
   examples = """
     Examples:
diff --git 
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/aggregate/ApproximatePercentileSuite.scala
 
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/aggregate/ApproximatePercentileSuite.scala
index 48dd7764f5ad..1710a1b897a6 100644
--- 
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/aggregate/ApproximatePercentileSuite.scala
+++ 
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/aggregate/ApproximatePercentileSuite.scala
@@ -31,7 +31,7 @@ import 
org.apache.spark.sql.catalyst.expressions.aggregate.ApproximatePercentile
 import org.apache.spark.sql.catalyst.plans.logical.LocalRelation
 import org.apache.spark.sql.catalyst.util.{ArrayData, QuantileSummaries}
 import org.apache.spark.sql.catalyst.util.QuantileSummaries.Stats
-import org.apache.spark.sql.types.{ArrayType, Decimal, DecimalType, 
DoubleType, FloatType, IntegerType, IntegralType, LongType}
+import org.apache.spark.sql.types.{ArrayType, Decimal, DecimalType, 
DoubleType, FloatType, IntegerType, IntegralType, LongType, TimeType}
 import org.apache.spark.util.SizeEstimator
 
 class ApproximatePercentileSuite extends SparkFunSuite {
@@ -135,6 +135,23 @@ class ApproximatePercentileSuite extends SparkFunSuite {
     }
   }
 
+  test("SPARK-57557: ApproximatePercentile supports TIME type") {
+    // The result type mirrors the input TIME type, preserving precision.
+    assert(new ApproximatePercentile(
+      BoundReference(0, TimeType(), nullable = false), Literal(0.5)).dataType 
=== TimeType())
+    assert(new ApproximatePercentile(
+      BoundReference(0, TimeType(3), nullable = false), Literal(0.5)).dataType 
=== TimeType(3))
+
+    // TIME's internal value is a Long (nanos-of-day); update/merge/eval 
round-trips it as a Long.
+    val agg = new ApproximatePercentile(
+      BoundReference(0, TimeType(), nullable = false), Literal(0.5))
+    val buffer = agg.createAggregationBuffer()
+    (1L to 1000L).foreach(v => agg.update(buffer, InternalRow(v)))
+    val result = agg.eval(buffer)
+    assert(result.isInstanceOf[Long])
+    assert(Math.abs(result.asInstanceOf[Long] - 500L) < 5L)
+  }
+
   test("class ApproximatePercentile, low level interface, update, merge, 
eval...") {
     val childExpression = Cast(BoundReference(0, IntegerType, nullable = 
true), DoubleType)
     val inputAggregationBufferOffset = 1
diff --git 
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/aggregate/HistogramNumericSuite.scala
 
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/aggregate/HistogramNumericSuite.scala
index 82e8277b42b9..98bf07ff76a3 100644
--- 
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/aggregate/HistogramNumericSuite.scala
+++ 
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/aggregate/HistogramNumericSuite.scala
@@ -199,6 +199,24 @@ class HistogramNumericSuite extends SparkFunSuite with 
SQLHelper {
     assert(agg.eval(buffer) != null)
   }
 
+  test("SPARK-57557: HistogramNumeric supports TIME type") {
+    // With propagateInputType, the histogram's 'x' field preserves the input 
TIME type and the
+    // bin centers are returned as Long nanos-of-day.
+    withSQLConf(SQLConf.HISTOGRAM_NUMERIC_PROPAGATE_INPUT_TYPE.key -> "true") {
+      val agg = new HistogramNumeric(BoundReference(0, TimeType(), nullable = 
true), Literal(5))
+      val xType = agg.dataType match {
+        case ArrayType(StructType(Array(
+            StructField("x", t, _, _), StructField("y", _, _, _))), _) => t
+      }
+      assert(xType === TimeType())
+      val buffer = agg.createAggregationBuffer()
+      Seq(0L, 10L, 20L).foreach(v => agg.update(buffer, InternalRow(v)))
+      val result = agg.eval(buffer).asInstanceOf[GenericArrayData]
+      assert(result.numElements() > 0)
+      assert(result.getStruct(0, 2).get(0, TimeType()).isInstanceOf[Long])
+    }
+  }
+
   test("class HistogramNumeric, exercise many different numeric input types") {
     val inputs = Seq(
       (Literal(null),
diff --git 
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/aggregate/PercentileSuite.scala
 
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/aggregate/PercentileSuite.scala
index d3401613dcd8..92b036cbf94d 100644
--- 
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/aggregate/PercentileSuite.scala
+++ 
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/aggregate/PercentileSuite.scala
@@ -113,6 +113,21 @@ class PercentileSuite extends SparkFunSuite {
     }
   }
 
+  test("SPARK-57557: Percentile and median support TIME type") {
+    // The return type mirrors the input TIME type, preserving precision.
+    assert(new Percentile(BoundReference(0, TimeType(), nullable = false), 
Literal(0.5))
+      .dataType === TimeType())
+    assert(Median(BoundReference(0, TimeType(3), nullable = false)).dataType 
=== TimeType(3))
+
+    // The exact median of two TIME values interpolates (continuous 
distribution): the midpoint is
+    // returned as the internal Long value (nanos-of-day), e.g. median(0L, 
10L) -> 5L.
+    val agg = new Percentile(BoundReference(0, TimeType(), nullable = false), 
Literal(0.5))
+    val buffer = agg.createAggregationBuffer()
+    agg.update(buffer, InternalRow(0L))
+    agg.update(buffer, InternalRow(10L))
+    assert(agg.eval(buffer) === 5L)
+  }
+
   test("class Percentile, low level interface, update, merge, eval...") {
     val childExpression = Cast(BoundReference(0, IntegerType, nullable = 
true), DoubleType)
     val inputAggregationBufferOffset = 1
@@ -175,8 +190,8 @@ class PercentileSuite extends SparkFunSuite {
           errorSubClass = "UNEXPECTED_INPUT_TYPE",
           messageParameters = Map(
             "paramIndex" -> ordinalNumber(0),
-            "requiredType" -> ("(\"NUMERIC\" or \"INTERVAL DAY TO SECOND\" " +
-              "or \"INTERVAL YEAR TO MONTH\")"),
+            "requiredType" -> ("((\"NUMERIC\" or \"INTERVAL DAY TO SECOND\" " +
+              "or \"INTERVAL YEAR TO MONTH\") or \"TIME\")"),
             "inputSql" -> "\"a\"",
             "inputType" -> toSQLType(dataType)
           )
@@ -198,8 +213,8 @@ class PercentileSuite extends SparkFunSuite {
           errorSubClass = "UNEXPECTED_INPUT_TYPE",
           messageParameters = Map(
             "paramIndex" -> ordinalNumber(0),
-            "requiredType" -> ("(\"NUMERIC\" or \"INTERVAL DAY TO SECOND\" " +
-              "or \"INTERVAL YEAR TO MONTH\")"),
+            "requiredType" -> ("((\"NUMERIC\" or \"INTERVAL DAY TO SECOND\" " +
+              "or \"INTERVAL YEAR TO MONTH\") or \"TIME\")"),
             "inputSql" -> "\"a\"",
             "inputType" -> toSQLType(dataType)
           )
diff --git 
a/sql/core/src/test/resources/sql-tests/analyzer-results/percentiles.sql.out 
b/sql/core/src/test/resources/sql-tests/analyzer-results/percentiles.sql.out
index eb8102afa47e..c55b1ce4afc6 100644
--- a/sql/core/src/test/resources/sql-tests/analyzer-results/percentiles.sql.out
+++ b/sql/core/src/test/resources/sql-tests/analyzer-results/percentiles.sql.out
@@ -1188,3 +1188,45 @@ org.apache.spark.sql.catalyst.ExtendedAnalysisException
 SET spark.sql.legacy.percentileDiscCalculation = false
 -- !query analysis
 SetCommand (spark.sql.legacy.percentileDiscCalculation,Some(false))
+
+
+-- !query
+CREATE OR REPLACE TEMPORARY VIEW aggr_time AS SELECT * FROM VALUES
+  (TIME '00:00:00'), (TIME '06:00:00'), (TIME '12:00:00'), (TIME '18:00:00')
+AS aggr_time(t)
+-- !query analysis
+CreateViewCommand `aggr_time`, SELECT * FROM VALUES
+  (TIME '00:00:00'), (TIME '06:00:00'), (TIME '12:00:00'), (TIME '18:00:00')
+AS aggr_time(t), false, true, LocalTempView, UNSUPPORTED, true
+   +- Project [t#x]
+      +- SubqueryAlias aggr_time
+         +- LocalRelation [t#x]
+
+
+-- !query
+SELECT
+  median(t),
+  percentile(t, 0.5),
+  percentile_cont(0.5) WITHIN GROUP (ORDER BY t),
+  percentile_disc(0.5) WITHIN GROUP (ORDER BY t)
+FROM aggr_time
+-- !query analysis
+Aggregate [median(t#x) AS median(t)#x, percentile(t#x, cast(0.5 as double), 1, 
0, 0, false) AS percentile(t, 0.5, 1)#x, percentile_cont(t#x, cast(0.5 as 
double), false) AS percentile_cont(0.5) WITHIN GROUP (ORDER BY t)#x, 
percentile_disc(t#x, cast(0.5 as double), false, 0, 0, false) AS 
percentile_disc(0.5) WITHIN GROUP (ORDER BY t)#x]
++- SubqueryAlias aggr_time
+   +- View (`aggr_time`, [t#x])
+      +- Project [cast(t#x as time(6)) AS t#x]
+         +- Project [t#x]
+            +- SubqueryAlias aggr_time
+               +- LocalRelation [t#x]
+
+
+-- !query
+SELECT percentile_approx(t, array(0.25, 0.5, 0.75)) FROM aggr_time
+-- !query analysis
+Aggregate [percentile_approx(t#x, cast(array(0.25, cast(0.5 as decimal(2,2)), 
0.75) as array<double>), 10000, 0, 0) AS percentile_approx(t, array(0.25, 0.5, 
0.75), 10000)#x]
++- SubqueryAlias aggr_time
+   +- View (`aggr_time`, [t#x])
+      +- Project [cast(t#x as time(6)) AS t#x]
+         +- Project [t#x]
+            +- SubqueryAlias aggr_time
+               +- LocalRelation [t#x]
diff --git a/sql/core/src/test/resources/sql-tests/inputs/percentiles.sql 
b/sql/core/src/test/resources/sql-tests/inputs/percentiles.sql
index 4b3e8708222a..fe698b1145dd 100644
--- a/sql/core/src/test/resources/sql-tests/inputs/percentiles.sql
+++ b/sql/core/src/test/resources/sql-tests/inputs/percentiles.sql
@@ -427,3 +427,17 @@ SELECT
 FROM values (12, 0.25), (13, 0.25), (22, 0.25) as v(a, b);
 
 SET spark.sql.legacy.percentileDiscCalculation = false;
+
+-- SPARK-57557: percentile, median and percentile_cont/disc over the TIME type
+CREATE OR REPLACE TEMPORARY VIEW aggr_time AS SELECT * FROM VALUES
+  (TIME '00:00:00'), (TIME '06:00:00'), (TIME '12:00:00'), (TIME '18:00:00')
+AS aggr_time(t);
+
+SELECT
+  median(t),
+  percentile(t, 0.5),
+  percentile_cont(0.5) WITHIN GROUP (ORDER BY t),
+  percentile_disc(0.5) WITHIN GROUP (ORDER BY t)
+FROM aggr_time;
+
+SELECT percentile_approx(t, array(0.25, 0.5, 0.75)) FROM aggr_time;
diff --git a/sql/core/src/test/resources/sql-tests/results/percentiles.sql.out 
b/sql/core/src/test/resources/sql-tests/results/percentiles.sql.out
index 55aaa8ee7378..b6c1d721acff 100644
--- a/sql/core/src/test/resources/sql-tests/results/percentiles.sql.out
+++ b/sql/core/src/test/resources/sql-tests/results/percentiles.sql.out
@@ -1188,3 +1188,34 @@ SET spark.sql.legacy.percentileDiscCalculation = false
 struct<key:string,value:string>
 -- !query output
 spark.sql.legacy.percentileDiscCalculation     false
+
+
+-- !query
+CREATE OR REPLACE TEMPORARY VIEW aggr_time AS SELECT * FROM VALUES
+  (TIME '00:00:00'), (TIME '06:00:00'), (TIME '12:00:00'), (TIME '18:00:00')
+AS aggr_time(t)
+-- !query schema
+struct<>
+-- !query output
+
+
+
+-- !query
+SELECT
+  median(t),
+  percentile(t, 0.5),
+  percentile_cont(0.5) WITHIN GROUP (ORDER BY t),
+  percentile_disc(0.5) WITHIN GROUP (ORDER BY t)
+FROM aggr_time
+-- !query schema
+struct<median(t):time(6),percentile(t, 0.5, 1):time(6),percentile_cont(0.5) 
WITHIN GROUP (ORDER BY t):time(6),percentile_disc(0.5) WITHIN GROUP (ORDER BY 
t):time(6)>
+-- !query output
+09:00:00       09:00:00        09:00:00        06:00:00
+
+
+-- !query
+SELECT percentile_approx(t, array(0.25, 0.5, 0.75)) FROM aggr_time
+-- !query schema
+struct<percentile_approx(t, array(0.25, 0.5, 0.75), 10000):array<time(6)>>
+-- !query output
+[00:00:00,06:00:00,12:00:00]
diff --git 
a/sql/core/src/test/scala/org/apache/spark/sql/ApproximatePercentileQuerySuite.scala
 
b/sql/core/src/test/scala/org/apache/spark/sql/ApproximatePercentileQuerySuite.scala
index d9ff495fbbd8..ae24a21538f1 100644
--- 
a/sql/core/src/test/scala/org/apache/spark/sql/ApproximatePercentileQuerySuite.scala
+++ 
b/sql/core/src/test/scala/org/apache/spark/sql/ApproximatePercentileQuerySuite.scala
@@ -18,13 +18,14 @@
 package org.apache.spark.sql
 
 import java.sql.{Date, Timestamp}
-import java.time.{Duration, LocalDateTime, Period}
+import java.time.{Duration, LocalDateTime, LocalTime, Period}
 
 import 
org.apache.spark.sql.catalyst.expressions.aggregate.ApproximatePercentile
 import 
org.apache.spark.sql.catalyst.expressions.aggregate.ApproximatePercentile.DEFAULT_PERCENTILE_ACCURACY
 import 
org.apache.spark.sql.catalyst.expressions.aggregate.ApproximatePercentile.PercentileDigest
 import org.apache.spark.sql.catalyst.util.DateTimeUtils
 import org.apache.spark.sql.test.SharedSparkSession
+import org.apache.spark.sql.types.TimeType
 import org.apache.spark.tags.SlowSQLTest
 
 /**
@@ -116,6 +117,23 @@ class ApproximatePercentileQuerySuite extends 
SharedSparkSession {
     }
   }
 
+  test("SPARK-57557: percentile_approx supports TIME type") {
+    withTempView(table) {
+      spark.sql(
+        s"""SELECT * FROM VALUES
+           |  (TIME '01:00:00'), (TIME '02:00:00'), (TIME '03:00:00'),
+           |  (TIME '04:00:00'), (TIME '05:00:00') AS tab(c)
+         """.stripMargin).createOrReplaceTempView(table)
+      val scalarDf = spark.sql(s"SELECT percentile_approx(c, 0.5) FROM $table")
+      // The result type is TIME, mirroring the input column type.
+      assert(scalarDf.schema.head.dataType === TimeType())
+      checkAnswer(scalarDf, Row(LocalTime.of(3, 0)))
+      checkAnswer(
+        spark.sql(s"SELECT percentile_approx(c, array(0.2, 0.5, 0.8D)) FROM 
$table"),
+        Row(Seq(LocalTime.of(1, 0), LocalTime.of(3, 0), LocalTime.of(4, 0))))
+    }
+  }
+
   test("percentile_approx, multiple records with the minimum value in a 
partition") {
     withTempView(table) {
       spark.sparkContext.makeRDD(Seq(1, 1, 2, 1, 1, 3, 1, 1, 4, 1, 1, 5), 
4).toDF("col")


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

Reply via email to