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 8a0b0f4d5f57 [SPARK-57585][SQL] Resolve a common TIME(p) type for 
mixed-precision operands in set and conditional operations
8a0b0f4d5f57 is described below

commit 8a0b0f4d5f57551412f5dc44a72ea1699090e2d0
Author: Maxim Gekk <[email protected]>
AuthorDate: Fri Jun 26 15:09:37 2026 +0200

    [SPARK-57585][SQL] Resolve a common TIME(p) type for mixed-precision 
operands in set and conditional operations
    
    ### What changes were proposed in this pull request?
    Extend common-type resolution so two `TIME(p)` operands of differing 
fractional-seconds precision resolve to `TIME(max(p1, p2))`:
    - `TypeCoercionHelper.findWiderDateTimeType` now widens two `TIME` operands 
to the larger precision. Because both the default (`TypeCoercion`) and ANSI 
(`AnsiTypeCoercion`) `findTightestCommonType` implementations delegate to this 
single method, the change covers UNION/INTERSECT/EXCEPT, 
COALESCE/CASE/IF/NULLIF, GREATEST/LEAST, IN lists/subqueries, and array/map 
literals.
    - `Cast.canANSIStoreAssign` allows lossless TIME widening (smaller -> 
larger precision) as a silent store assignment, while narrowing stays 
explicit-CAST-only. `UpCastRule.canUpCast` already keeps cross-precision TIME 
explicit-only, mirroring the nanosecond-timestamp precedent.
    
    Cross-family pairs (TIME vs DATE / TIMESTAMP) remain incomparable.
    
    ### Why are the changes needed?
    ANSI SQL (ISO/IEC 9075-2, result-type rule for a set of comparable datetime 
types) requires that across datetime operands of differing fractional-seconds 
precision the result type is the datetime type with the largest 
fractional-seconds precision. Previously `findWiderDateTimeType` returned 
`None` for any pair involving `TIME`, so set and conditional operations over 
mixed `TIME(p)` raised an analysis error and required an explicit `CAST`. The 
widening cast (`TIME(p1) -> TIME(p2)` via  [...]
    
    ### Does this PR introduce _any_ user-facing change?
    Yes, within the unreleased TIME type (`Unstable`). Set and conditional 
operations over mixed `TIME(p)` now resolve `TIME(max(p))` without an explicit 
cast; values from the smaller-precision side are widened losslessly. Inserting 
a smaller-precision TIME value into a larger-precision TIME column now 
succeeds, while the reverse (narrowing) still requires an explicit cast.
    
    For example:
    ```sql
    SELECT typeof(c), c FROM (
      SELECT '12:34:56.789' :: TIME(3) AS c
        UNION ALL SELECT '01:02:03.456789' :: TIME(6)) ORDER BY c;
    -- time(6)  01:02:03.456789
    -- time(6)  12:34:56.789
    ```
    
    ### How was this patch tested?
    - Added type-coercion unit tests in `TypeCoercionSuite` and 
`AnsiTypeCoercionSuite` (widening to max precision, equal precision, and 
TIME-vs-DATE/TIMESTAMP returning `None`).
    - Added a store-assignment / up-cast contract test in `CastSuiteBase`.
    - Added golden-file coverage in `time.sql` for UNION, COALESCE, CASE, 
NULLIF, GREATEST/LEAST, array/map, IN, and store-assignment INSERTs (widening 
succeeds, narrowing fails without an explicit cast).
    - Ran the affected catalyst suites and the `time.sql` `SQLQueryTestSuite`.
    
    ### Was this patch authored or co-authored using generative AI tooling?
    Generated-by: Cursor (Claude Opus 4.8)
    
    Closes #56805 from MaxGekk/time-common-type.
    
    Authored-by: Maxim Gekk <[email protected]>
    Signed-off-by: Max Gekk <[email protected]>
    (cherry picked from commit 0e1563a7bc43a0680c64315fb1a1fe7481f4092e)
    Signed-off-by: Max Gekk <[email protected]>
---
 .../sql/catalyst/analysis/TypeCoercionHelper.scala |   6 +
 .../spark/sql/catalyst/expressions/Cast.scala      |   4 +
 .../catalyst/analysis/AnsiTypeCoercionSuite.scala  |  14 ++
 .../sql/catalyst/analysis/TypeCoercionSuite.scala  |  11 ++
 .../sql/catalyst/expressions/CastSuiteBase.scala   |  15 ++
 .../sql-tests/analyzer-results/time.sql.out        | 219 +++++++++++++++++++++
 .../src/test/resources/sql-tests/inputs/time.sql   |  60 ++++++
 .../test/resources/sql-tests/results/time.sql.out  | 208 +++++++++++++++++++
 8 files changed, 537 insertions(+)

diff --git 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/TypeCoercionHelper.scala
 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/TypeCoercionHelper.scala
index c70a3fdd62f6..e6329e465e00 100644
--- 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/TypeCoercionHelper.scala
+++ 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/TypeCoercionHelper.scala
@@ -245,6 +245,12 @@ abstract class TypeCoercionHelper {
 
   protected def findWiderDateTimeType(d1: DatetimeType, d2: DatetimeType): 
Option[DatetimeType] =
     (d1, d2) match {
+      // Two TIME operands of differing fractional-seconds precision widen to 
the larger precision
+      // (ANSI SQL result type for a set of comparable datetime types). 
Widening from a smaller to a
+      // larger precision is a lossless up-cast via truncateTimeToPrecision. 
Cross-family pairs
+      // (TIME vs DATE / TIMESTAMP) remain incomparable and fall through to 
the None arms below.
+      case (t1: TimeType, t2: TimeType) =>
+        Some(TimeType(math.max(t1.precision, t2.precision)))
       case (_, _: TimeType) => None
       case (_: TimeType, _) => None
 
diff --git 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/Cast.scala
 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/Cast.scala
index 9ae34f94424c..346047a8ba82 100644
--- 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/Cast.scala
+++ 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/Cast.scala
@@ -524,6 +524,10 @@ object Cast extends QueryErrorsBase {
     case (TimestampType, _: TimestampNTZNanosType) => false
     case (TimestampNTZType, _: TimestampLTZNanosType) => false
     case (_: AnyTimestampNanoType, _: AnyTimestampNanoType) => false
+    // SPARK-57585: widening a TIME(p) to a larger precision is lossless and 
allowed as a silent
+    // store assignment, while narrowing (e.g. TIME(6) -> TIME(3)) drops 
fractional-seconds digits
+    // and stays explicit-CAST-only. Equal precision is handled by the `from 
== to` short-circuit.
+    case (f: TimeType, t: TimeType) => f.precision <= t.precision
     case (_: DatetimeType, _: DatetimeType) => true
 
     case (ArrayType(fromType, fn), ArrayType(toType, tn)) =>
diff --git 
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/analysis/AnsiTypeCoercionSuite.scala
 
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/analysis/AnsiTypeCoercionSuite.scala
index dbbce0888640..80d94cc3760d 100644
--- 
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/analysis/AnsiTypeCoercionSuite.scala
+++ 
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/analysis/AnsiTypeCoercionSuite.scala
@@ -153,6 +153,9 @@ class AnsiTypeCoercionSuite extends TypeCoercionSuiteBase {
     widenTest(TimestampType, TimestampLTZNanosType(9), 
Some(TimestampLTZNanosType(9)))
     widenTest(TimestampLTZNanosType(7), TimestampNTZNanosType(9), 
Some(TimestampLTZNanosType(9)))
     widenTest(DateType, TimestampNTZNanosType(7), 
Some(TimestampNTZNanosType(7)))
+    // Two TIME operands widen to the larger fractional-seconds precision 
(SPARK-57585).
+    widenTest(TimeType(3), TimeType(6), Some(TimeType(6)))
+    widenTest(TimeType(0), TimeType(9), Some(TimeType(9)))
   }
 
   test("tightest common bound for types") {
@@ -247,6 +250,17 @@ class AnsiTypeCoercionSuite extends TypeCoercionSuiteBase {
     // nanos <-> TIME has no common datetime type.
     widenTest(TimestampLTZNanosType(9), TimeType(6), None)
     widenTest(TimestampNTZNanosType(9), TimeType(6), None)
+
+    // TIME(p) types (SPARK-57585).
+    // Two TIME operands widen to the larger fractional-seconds precision.
+    widenTest(TimeType(3), TimeType(6), Some(TimeType(6)))
+    widenTest(TimeType(6), TimeType(3), Some(TimeType(6)))
+    widenTest(TimeType(0), TimeType(9), Some(TimeType(9)))
+    widenTest(TimeType(6), TimeType(6), Some(TimeType(6)))
+    // TIME has no common datetime type with DATE or the TIMESTAMP families.
+    widenTest(TimeType(6), DateType, None)
+    widenTest(TimeType(6), TimestampType, None)
+    widenTest(TimeType(6), TimestampNTZType, None)
     // No common type with non-datetime types.
     widenTest(IntegerType, TimestampLTZNanosType(9), None)
     widenTest(StringType, TimestampNTZNanosType(9), None)
diff --git 
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/analysis/TypeCoercionSuite.scala
 
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/analysis/TypeCoercionSuite.scala
index de0f3207bebc..d990a393ff65 100644
--- 
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/analysis/TypeCoercionSuite.scala
+++ 
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/analysis/TypeCoercionSuite.scala
@@ -670,6 +670,17 @@ class TypeCoercionSuite extends TypeCoercionSuiteBase {
     // nanos <-> TIME has no common datetime type.
     widenTest(TimestampLTZNanosType(9), TimeType(6), None)
     widenTest(TimestampNTZNanosType(9), TimeType(6), None)
+
+    // TIME(p) types (SPARK-57585).
+    // Two TIME operands widen to the larger fractional-seconds precision.
+    widenTest(TimeType(3), TimeType(6), Some(TimeType(6)))
+    widenTest(TimeType(6), TimeType(3), Some(TimeType(6)))
+    widenTest(TimeType(0), TimeType(9), Some(TimeType(9)))
+    widenTest(TimeType(6), TimeType(6), Some(TimeType(6)))
+    // TIME has no common datetime type with DATE or the TIMESTAMP families.
+    widenTest(TimeType(6), DateType, None)
+    widenTest(TimeType(6), TimestampType, None)
+    widenTest(TimeType(6), TimestampNTZType, None)
     // No common type with non-datetime types.
     widenTest(IntegerType, TimestampLTZNanosType(9), None)
     widenTest(StringType, TimestampNTZNanosType(9), None)
diff --git 
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/CastSuiteBase.scala
 
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/CastSuiteBase.scala
index 8cc38996d3f8..23d5783155fd 100644
--- 
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/CastSuiteBase.scala
+++ 
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/CastSuiteBase.scala
@@ -802,6 +802,21 @@ abstract class CastSuiteBase extends SparkFunSuite with 
ExpressionEvalHelper {
     }
   }
 
+  test("SPARK-57585: cross-precision TIME store-assignment and up-cast 
contract") {
+    for {
+      p1 <- TimeType.MIN_PRECISION to TimeType.MAX_PRECISION
+      p2 <- TimeType.MIN_PRECISION to TimeType.MAX_PRECISION
+    } {
+      // Cross-precision TIME casts are never up-casts (only equal precision 
is, via from == to),
+      // matching the nanos precedent; STRICT store assignment rejects them. 
The both-direction
+      // assertions also guard against a future blanket datetime arm in 
UpCastRule.
+      assert(Cast.canUpCast(TimeType(p1), TimeType(p2)) == (p1 == p2))
+      // ANSI store assignment allows lossless widening (p1 <= p2) and equal 
precision, but blocks
+      // lossy narrowing (p1 > p2) to avoid silently dropping 
fractional-seconds digits.
+      assert(Cast.canANSIStoreAssign(TimeType(p1), TimeType(p2)) == (p1 <= p2))
+    }
+  }
+
   test("cross-family nanos cast: admissibility, store-assignment and up-cast 
contract") {
     for {
       p <- TimestampLTZNanosType.MIN_PRECISION to 
TimestampLTZNanosType.MAX_PRECISION
diff --git 
a/sql/core/src/test/resources/sql-tests/analyzer-results/time.sql.out 
b/sql/core/src/test/resources/sql-tests/analyzer-results/time.sql.out
index d69a6df9eb5e..d5d5700316f8 100644
--- a/sql/core/src/test/resources/sql-tests/analyzer-results/time.sql.out
+++ b/sql/core/src/test/resources/sql-tests/analyzer-results/time.sql.out
@@ -2294,3 +2294,222 @@ SELECT 
time_from_micros(time_to_micros(TIME'14:30:00.5'))
 -- !query analysis
 Project [time_from_micros(time_to_micros(14:30:00.5)) AS 
time_from_micros(time_to_micros(TIME '14:30:00.5'))#x]
 +- OneRowRelation
+
+
+-- !query
+SELECT typeof(c), c FROM (
+  SELECT '12:34:56.789' :: TIME(3) AS c
+    UNION ALL SELECT '01:02:03.456789' :: TIME(6)) ORDER BY c
+-- !query analysis
+Sort [c#x ASC NULLS FIRST], true
++- Project [typeof(c#x) AS typeof(c)#x, c#x]
+   +- SubqueryAlias __auto_generated_subquery_name
+      +- Union false, false
+         :- Project [cast(c#x as time(6)) AS c#x]
+         :  +- Project [cast(12:34:56.789 as time(3)) AS c#x]
+         :     +- OneRowRelation
+         +- Project [cast(01:02:03.456789 as time(6)) AS CAST(01:02:03.456789 
AS TIME(6))#x]
+            +- OneRowRelation
+
+
+-- !query
+SELECT typeof(c), c FROM (
+  SELECT '00:00:00' :: TIME(0) AS c
+    UNION ALL SELECT '23:59:59.123456789' :: TIME(9)) ORDER BY c
+-- !query analysis
+Sort [c#x ASC NULLS FIRST], true
++- Project [typeof(c#x) AS typeof(c)#x, c#x]
+   +- SubqueryAlias __auto_generated_subquery_name
+      +- Union false, false
+         :- Project [cast(c#x as time(9)) AS c#x]
+         :  +- Project [cast(00:00:00 as time(0)) AS c#x]
+         :     +- OneRowRelation
+         +- Project [cast(23:59:59.123456789 as time(9)) AS 
CAST(23:59:59.123456789 AS TIME(9))#x]
+            +- OneRowRelation
+
+
+-- !query
+SELECT typeof(c), c FROM (
+  SELECT '01:02:03.456' :: TIME(3) AS c
+    INTERSECT SELECT '01:02:03.456000' :: TIME(6))
+-- !query analysis
+Project [typeof(c#x) AS typeof(c)#x, c#x]
++- SubqueryAlias __auto_generated_subquery_name
+   +- Intersect false
+      :- Project [cast(c#x as time(6)) AS c#x]
+      :  +- Project [cast(01:02:03.456 as time(3)) AS c#x]
+      :     +- OneRowRelation
+      +- Project [cast(01:02:03.456000 as time(6)) AS CAST(01:02:03.456000 AS 
TIME(6))#x]
+         +- OneRowRelation
+
+
+-- !query
+SELECT typeof(c), c FROM (
+  SELECT '12:34:56.789' :: TIME(3) AS c
+    EXCEPT SELECT '01:02:03.456789' :: TIME(6)) ORDER BY c
+-- !query analysis
+Sort [c#x ASC NULLS FIRST], true
++- Project [typeof(c#x) AS typeof(c)#x, c#x]
+   +- SubqueryAlias __auto_generated_subquery_name
+      +- Except false
+         :- Project [cast(c#x as time(6)) AS c#x]
+         :  +- Project [cast(12:34:56.789 as time(3)) AS c#x]
+         :     +- OneRowRelation
+         +- Project [cast(01:02:03.456789 as time(6)) AS CAST(01:02:03.456789 
AS TIME(6))#x]
+            +- OneRowRelation
+
+
+-- !query
+SELECT typeof(v), v FROM (SELECT coalesce(
+  CAST(NULL AS TIME(3)), '01:02:03.456789' :: TIME(6)) AS v)
+-- !query analysis
+Project [typeof(v#x) AS typeof(v)#x, v#x]
++- SubqueryAlias __auto_generated_subquery_name
+   +- Project [coalesce(cast(cast(null as time(3)) as time(6)), 
cast(01:02:03.456789 as time(6))) AS v#x]
+      +- OneRowRelation
+
+
+-- !query
+SELECT typeof(v), v FROM (SELECT CASE WHEN true
+  THEN '12:34:56.789' :: TIME(3) ELSE '01:02:03.456789' :: TIME(6) END AS v)
+-- !query analysis
+Project [typeof(v#x) AS typeof(v)#x, v#x]
++- SubqueryAlias __auto_generated_subquery_name
+   +- Project [CASE WHEN true THEN cast(cast(12:34:56.789 as time(3)) as 
time(6)) ELSE cast(01:02:03.456789 as time(6)) END AS v#x]
+      +- OneRowRelation
+
+
+-- !query
+SELECT typeof(v), v FROM (SELECT nullif(
+  '12:34:56.789' :: TIME(3), '01:02:03.456789' :: TIME(6)) AS v)
+-- !query analysis
+Project [typeof(v#x) AS typeof(v)#x, v#x]
++- SubqueryAlias __auto_generated_subquery_name
+   +- Project [nullif(cast(12:34:56.789 as time(3)), cast(01:02:03.456789 as 
time(6))) AS v#x]
+      +- OneRowRelation
+
+
+-- !query
+SELECT typeof(greatest('12:34:56.789' :: TIME(3), '01:02:03.456789' :: 
TIME(6)))
+-- !query analysis
+Project [typeof(greatest(cast(cast(12:34:56.789 as time(3)) as time(6)), 
cast(01:02:03.456789 as time(6)))) AS typeof(greatest(CAST(12:34:56.789 AS 
TIME(3)), CAST(01:02:03.456789 AS TIME(6))))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT greatest('12:34:56.789' :: TIME(3), '01:02:03.456789' :: TIME(6))
+-- !query analysis
+Project [greatest(cast(cast(12:34:56.789 as time(3)) as time(6)), 
cast(01:02:03.456789 as time(6))) AS greatest(CAST(12:34:56.789 AS TIME(3)), 
CAST(01:02:03.456789 AS TIME(6)))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT least('12:34:56.789' :: TIME(3), '01:02:03.456789' :: TIME(6))
+-- !query analysis
+Project [least(cast(cast(12:34:56.789 as time(3)) as time(6)), 
cast(01:02:03.456789 as time(6))) AS least(CAST(12:34:56.789 AS TIME(3)), 
CAST(01:02:03.456789 AS TIME(6)))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT typeof(array('12:34:56.789' :: TIME(3), '01:02:03.456789' :: TIME(6)))
+-- !query analysis
+Project [typeof(array(cast(cast(12:34:56.789 as time(3)) as time(6)), 
cast(01:02:03.456789 as time(6)))) AS typeof(array(CAST(12:34:56.789 AS 
TIME(3)), CAST(01:02:03.456789 AS TIME(6))))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT typeof(map('a', '12:34:56.789' :: TIME(3), 'b', '01:02:03.456789' :: 
TIME(6)))
+-- !query analysis
+Project [typeof(map(a, cast(cast(12:34:56.789 as time(3)) as time(6)), b, 
cast(01:02:03.456789 as time(6)))) AS typeof(map(a, CAST(12:34:56.789 AS 
TIME(3)), b, CAST(01:02:03.456789 AS TIME(6))))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT '01:02:03.456789' :: TIME(6) IN ('12:34:56.789' :: TIME(3), 
'01:02:03.456789' :: TIME(6))
+-- !query analysis
+Project [cast(cast(01:02:03.456789 as time(6)) as time(6)) IN 
(cast(cast(12:34:56.789 as time(3)) as time(6)),cast(cast(01:02:03.456789 as 
time(6)) as time(6))) AS (CAST(01:02:03.456789 AS TIME(6)) IN 
(CAST(12:34:56.789 AS TIME(3)), CAST(01:02:03.456789 AS TIME(6))))#x]
++- OneRowRelation
+
+
+-- !query
+SELECT '01:02:03.456789123' :: TIME(9) IN (
+  '12:34:56.789' :: TIME(3), '01:02:03.456789' :: TIME(6), 
'01:02:03.456789123' :: TIME(9))
+-- !query analysis
+Project [cast(cast(01:02:03.456789123 as time(9)) as time(9)) IN 
(cast(cast(12:34:56.789 as time(3)) as time(9)),cast(cast(01:02:03.456789 as 
time(6)) as time(9)),cast(cast(01:02:03.456789123 as time(9)) as time(9))) AS 
(CAST(01:02:03.456789123 AS TIME(9)) IN (CAST(12:34:56.789 AS TIME(3)), 
CAST(01:02:03.456789 AS TIME(6)), CAST(01:02:03.456789123 AS TIME(9))))#x]
++- OneRowRelation
+
+
+-- !query
+CREATE TABLE time_widen_tbl (t6 TIME(6)) USING parquet
+-- !query analysis
+CreateDataSourceTableCommand `spark_catalog`.`default`.`time_widen_tbl`, false
+
+
+-- !query
+INSERT INTO time_widen_tbl SELECT '01:02:03.456' :: TIME(3)
+-- !query analysis
+InsertIntoHadoopFsRelationCommand file:[not included in 
comparison]/{warehouse_dir}/time_widen_tbl, false, Parquet, [path=file:[not 
included in comparison]/{warehouse_dir}/time_widen_tbl], Append, 
`spark_catalog`.`default`.`time_widen_tbl`, 
org.apache.spark.sql.execution.datasources.InMemoryFileIndex(file:[not included 
in comparison]/{warehouse_dir}/time_widen_tbl), [t6]
++- Project [cast(CAST(01:02:03.456 AS TIME(3))#x as time(6)) AS t6#x]
+   +- Project [cast(01:02:03.456 as time(3)) AS CAST(01:02:03.456 AS 
TIME(3))#x]
+      +- OneRowRelation
+
+
+-- !query
+SELECT typeof(t6), t6 FROM time_widen_tbl
+-- !query analysis
+Project [typeof(t6#x) AS typeof(t6)#x, t6#x]
++- SubqueryAlias spark_catalog.default.time_widen_tbl
+   +- Relation spark_catalog.default.time_widen_tbl[t6#x] parquet
+
+
+-- !query
+DROP TABLE time_widen_tbl
+-- !query analysis
+DropTable false, false
++- ResolvedIdentifier V2SessionCatalog(spark_catalog), default.time_widen_tbl
+
+
+-- !query
+CREATE TABLE time_narrow_tbl (t3 TIME(3)) USING parquet
+-- !query analysis
+CreateDataSourceTableCommand `spark_catalog`.`default`.`time_narrow_tbl`, false
+
+
+-- !query
+INSERT INTO time_narrow_tbl SELECT '01:02:03.456789' :: TIME(6)
+-- !query analysis
+org.apache.spark.sql.AnalysisException
+{
+  "errorClass" : "INCOMPATIBLE_DATA_FOR_TABLE.CANNOT_SAFELY_CAST",
+  "sqlState" : "KD000",
+  "messageParameters" : {
+    "colName" : "`t3`",
+    "srcType" : "\"TIME(6)\"",
+    "tableName" : "`spark_catalog`.`default`.`time_narrow_tbl`",
+    "targetType" : "\"TIME(3)\""
+  }
+}
+
+
+-- !query
+INSERT INTO time_narrow_tbl SELECT CAST('01:02:03.456789' :: TIME(6) AS 
TIME(3))
+-- !query analysis
+InsertIntoHadoopFsRelationCommand file:[not included in 
comparison]/{warehouse_dir}/time_narrow_tbl, false, Parquet, [path=file:[not 
included in comparison]/{warehouse_dir}/time_narrow_tbl], Append, 
`spark_catalog`.`default`.`time_narrow_tbl`, 
org.apache.spark.sql.execution.datasources.InMemoryFileIndex(file:[not included 
in comparison]/{warehouse_dir}/time_narrow_tbl), [t3]
++- Project [CAST(CAST(01:02:03.456789 AS TIME(6)) AS TIME(3))#x AS t3#x]
+   +- Project [cast(cast(01:02:03.456789 as time(6)) as time(3)) AS 
CAST(CAST(01:02:03.456789 AS TIME(6)) AS TIME(3))#x]
+      +- OneRowRelation
+
+
+-- !query
+SELECT typeof(t3), t3 FROM time_narrow_tbl
+-- !query analysis
+Project [typeof(t3#x) AS typeof(t3)#x, t3#x]
++- SubqueryAlias spark_catalog.default.time_narrow_tbl
+   +- Relation spark_catalog.default.time_narrow_tbl[t3#x] parquet
+
+
+-- !query
+DROP TABLE time_narrow_tbl
+-- !query analysis
+DropTable false, false
++- ResolvedIdentifier V2SessionCatalog(spark_catalog), default.time_narrow_tbl
diff --git a/sql/core/src/test/resources/sql-tests/inputs/time.sql 
b/sql/core/src/test/resources/sql-tests/inputs/time.sql
index f81944716881..1a836b270edc 100644
--- a/sql/core/src/test/resources/sql-tests/inputs/time.sql
+++ b/sql/core/src/test/resources/sql-tests/inputs/time.sql
@@ -366,3 +366,63 @@ SELECT time_to_millis(time_from_millis(52200500));
 SELECT time_from_millis(time_to_millis(TIME'14:30:00.5'));
 SELECT time_to_micros(time_from_micros(52200500000));
 SELECT time_from_micros(time_to_micros(TIME'14:30:00.5'));
+
+-- SPARK-57585: common TIME(p) type for mixed-precision operands in set and 
conditional operations.
+-- The result type is the TIME with the largest fractional-seconds precision; 
values from the
+-- smaller-precision side are widened losslessly.
+
+-- UNION widens TIME(3) and TIME(6) to TIME(6).
+SELECT typeof(c), c FROM (
+  SELECT '12:34:56.789' :: TIME(3) AS c
+    UNION ALL SELECT '01:02:03.456789' :: TIME(6)) ORDER BY c;
+-- UNION over the precision extremes widens TIME(0) and TIME(9) to TIME(9).
+SELECT typeof(c), c FROM (
+  SELECT '00:00:00' :: TIME(0) AS c
+    UNION ALL SELECT '23:59:59.123456789' :: TIME(9)) ORDER BY c;
+-- INTERSECT widens TIME(3) and TIME(6) to TIME(6); the matching value is 
compared at TIME(6).
+SELECT typeof(c), c FROM (
+  SELECT '01:02:03.456' :: TIME(3) AS c
+    INTERSECT SELECT '01:02:03.456000' :: TIME(6));
+-- EXCEPT widens TIME(3) and TIME(6) to TIME(6).
+SELECT typeof(c), c FROM (
+  SELECT '12:34:56.789' :: TIME(3) AS c
+    EXCEPT SELECT '01:02:03.456789' :: TIME(6)) ORDER BY c;
+
+-- coalesce keeps the first non-null, widened to the wider precision.
+SELECT typeof(v), v FROM (SELECT coalesce(
+  CAST(NULL AS TIME(3)), '01:02:03.456789' :: TIME(6)) AS v);
+-- CASE WHEN unifies its branches to TIME(6).
+SELECT typeof(v), v FROM (SELECT CASE WHEN true
+  THEN '12:34:56.789' :: TIME(3) ELSE '01:02:03.456789' :: TIME(6) END AS v);
+-- NULLIF resolves the common type of its arguments.
+SELECT typeof(v), v FROM (SELECT nullif(
+  '12:34:56.789' :: TIME(3), '01:02:03.456789' :: TIME(6)) AS v);
+
+-- greatest / least widen their arguments to the common TIME type and pick the 
extreme value.
+SELECT typeof(greatest('12:34:56.789' :: TIME(3), '01:02:03.456789' :: 
TIME(6)));
+SELECT greatest('12:34:56.789' :: TIME(3), '01:02:03.456789' :: TIME(6));
+SELECT least('12:34:56.789' :: TIME(3), '01:02:03.456789' :: TIME(6));
+
+-- array() unifies element types and map() value types to the common TIME type.
+SELECT typeof(array('12:34:56.789' :: TIME(3), '01:02:03.456789' :: TIME(6)));
+SELECT typeof(map('a', '12:34:56.789' :: TIME(3), 'b', '01:02:03.456789' :: 
TIME(6)));
+
+-- IN list resolves the common type across mixed TIME(p) elements.
+SELECT '01:02:03.456789' :: TIME(6) IN ('12:34:56.789' :: TIME(3), 
'01:02:03.456789' :: TIME(6));
+-- A 3-way mix folds across all precisions in findWiderCommonType, resolving 
the comparison at
+-- TIME(9).
+SELECT '01:02:03.456789123' :: TIME(9) IN (
+  '12:34:56.789' :: TIME(3), '01:02:03.456789' :: TIME(6), 
'01:02:03.456789123' :: TIME(9));
+
+-- Store assignment: widening TIME(3) -> TIME(6) succeeds, narrowing TIME(6) 
-> TIME(3) requires
+-- an explicit cast.
+CREATE TABLE time_widen_tbl (t6 TIME(6)) USING parquet;
+INSERT INTO time_widen_tbl SELECT '01:02:03.456' :: TIME(3);
+SELECT typeof(t6), t6 FROM time_widen_tbl;
+DROP TABLE time_widen_tbl;
+
+CREATE TABLE time_narrow_tbl (t3 TIME(3)) USING parquet;
+INSERT INTO time_narrow_tbl SELECT '01:02:03.456789' :: TIME(6);
+INSERT INTO time_narrow_tbl SELECT CAST('01:02:03.456789' :: TIME(6) AS 
TIME(3));
+SELECT typeof(t3), t3 FROM time_narrow_tbl;
+DROP TABLE time_narrow_tbl;
diff --git a/sql/core/src/test/resources/sql-tests/results/time.sql.out 
b/sql/core/src/test/resources/sql-tests/results/time.sql.out
index c61c37f27cd3..dace78ab938d 100644
--- a/sql/core/src/test/resources/sql-tests/results/time.sql.out
+++ b/sql/core/src/test/resources/sql-tests/results/time.sql.out
@@ -2810,3 +2810,211 @@ SELECT 
time_from_micros(time_to_micros(TIME'14:30:00.5'))
 struct<time_from_micros(time_to_micros(TIME '14:30:00.5')):time(6)>
 -- !query output
 14:30:00.5
+
+
+-- !query
+SELECT typeof(c), c FROM (
+  SELECT '12:34:56.789' :: TIME(3) AS c
+    UNION ALL SELECT '01:02:03.456789' :: TIME(6)) ORDER BY c
+-- !query schema
+struct<typeof(c):string,c:time(6)>
+-- !query output
+time(6)        01:02:03.456789
+time(6)        12:34:56.789
+
+
+-- !query
+SELECT typeof(c), c FROM (
+  SELECT '00:00:00' :: TIME(0) AS c
+    UNION ALL SELECT '23:59:59.123456789' :: TIME(9)) ORDER BY c
+-- !query schema
+struct<typeof(c):string,c:time(9)>
+-- !query output
+time(9)        00:00:00
+time(9)        23:59:59.123456789
+
+
+-- !query
+SELECT typeof(c), c FROM (
+  SELECT '01:02:03.456' :: TIME(3) AS c
+    INTERSECT SELECT '01:02:03.456000' :: TIME(6))
+-- !query schema
+struct<typeof(c):string,c:time(6)>
+-- !query output
+time(6)        01:02:03.456
+
+
+-- !query
+SELECT typeof(c), c FROM (
+  SELECT '12:34:56.789' :: TIME(3) AS c
+    EXCEPT SELECT '01:02:03.456789' :: TIME(6)) ORDER BY c
+-- !query schema
+struct<typeof(c):string,c:time(6)>
+-- !query output
+time(6)        12:34:56.789
+
+
+-- !query
+SELECT typeof(v), v FROM (SELECT coalesce(
+  CAST(NULL AS TIME(3)), '01:02:03.456789' :: TIME(6)) AS v)
+-- !query schema
+struct<typeof(v):string,v:time(6)>
+-- !query output
+time(6)        01:02:03.456789
+
+
+-- !query
+SELECT typeof(v), v FROM (SELECT CASE WHEN true
+  THEN '12:34:56.789' :: TIME(3) ELSE '01:02:03.456789' :: TIME(6) END AS v)
+-- !query schema
+struct<typeof(v):string,v:time(6)>
+-- !query output
+time(6)        12:34:56.789
+
+
+-- !query
+SELECT typeof(v), v FROM (SELECT nullif(
+  '12:34:56.789' :: TIME(3), '01:02:03.456789' :: TIME(6)) AS v)
+-- !query schema
+struct<typeof(v):string,v:time(3)>
+-- !query output
+time(3)        12:34:56.789
+
+
+-- !query
+SELECT typeof(greatest('12:34:56.789' :: TIME(3), '01:02:03.456789' :: 
TIME(6)))
+-- !query schema
+struct<typeof(greatest(CAST(12:34:56.789 AS TIME(3)), CAST(01:02:03.456789 AS 
TIME(6)))):string>
+-- !query output
+time(6)
+
+
+-- !query
+SELECT greatest('12:34:56.789' :: TIME(3), '01:02:03.456789' :: TIME(6))
+-- !query schema
+struct<greatest(CAST(12:34:56.789 AS TIME(3)), CAST(01:02:03.456789 AS 
TIME(6))):time(6)>
+-- !query output
+12:34:56.789
+
+
+-- !query
+SELECT least('12:34:56.789' :: TIME(3), '01:02:03.456789' :: TIME(6))
+-- !query schema
+struct<least(CAST(12:34:56.789 AS TIME(3)), CAST(01:02:03.456789 AS 
TIME(6))):time(6)>
+-- !query output
+01:02:03.456789
+
+
+-- !query
+SELECT typeof(array('12:34:56.789' :: TIME(3), '01:02:03.456789' :: TIME(6)))
+-- !query schema
+struct<typeof(array(CAST(12:34:56.789 AS TIME(3)), CAST(01:02:03.456789 AS 
TIME(6)))):string>
+-- !query output
+array<time(6)>
+
+
+-- !query
+SELECT typeof(map('a', '12:34:56.789' :: TIME(3), 'b', '01:02:03.456789' :: 
TIME(6)))
+-- !query schema
+struct<typeof(map(a, CAST(12:34:56.789 AS TIME(3)), b, CAST(01:02:03.456789 AS 
TIME(6)))):string>
+-- !query output
+map<string,time(6)>
+
+
+-- !query
+SELECT '01:02:03.456789' :: TIME(6) IN ('12:34:56.789' :: TIME(3), 
'01:02:03.456789' :: TIME(6))
+-- !query schema
+struct<(CAST(01:02:03.456789 AS TIME(6)) IN (CAST(12:34:56.789 AS TIME(3)), 
CAST(01:02:03.456789 AS TIME(6)))):boolean>
+-- !query output
+true
+
+
+-- !query
+SELECT '01:02:03.456789123' :: TIME(9) IN (
+  '12:34:56.789' :: TIME(3), '01:02:03.456789' :: TIME(6), 
'01:02:03.456789123' :: TIME(9))
+-- !query schema
+struct<(CAST(01:02:03.456789123 AS TIME(9)) IN (CAST(12:34:56.789 AS TIME(3)), 
CAST(01:02:03.456789 AS TIME(6)), CAST(01:02:03.456789123 AS TIME(9)))):boolean>
+-- !query output
+true
+
+
+-- !query
+CREATE TABLE time_widen_tbl (t6 TIME(6)) USING parquet
+-- !query schema
+struct<>
+-- !query output
+
+
+
+-- !query
+INSERT INTO time_widen_tbl SELECT '01:02:03.456' :: TIME(3)
+-- !query schema
+struct<>
+-- !query output
+
+
+
+-- !query
+SELECT typeof(t6), t6 FROM time_widen_tbl
+-- !query schema
+struct<typeof(t6):string,t6:time(6)>
+-- !query output
+time(6)        01:02:03.456
+
+
+-- !query
+DROP TABLE time_widen_tbl
+-- !query schema
+struct<>
+-- !query output
+
+
+
+-- !query
+CREATE TABLE time_narrow_tbl (t3 TIME(3)) USING parquet
+-- !query schema
+struct<>
+-- !query output
+
+
+
+-- !query
+INSERT INTO time_narrow_tbl SELECT '01:02:03.456789' :: TIME(6)
+-- !query schema
+struct<>
+-- !query output
+org.apache.spark.sql.AnalysisException
+{
+  "errorClass" : "INCOMPATIBLE_DATA_FOR_TABLE.CANNOT_SAFELY_CAST",
+  "sqlState" : "KD000",
+  "messageParameters" : {
+    "colName" : "`t3`",
+    "srcType" : "\"TIME(6)\"",
+    "tableName" : "`spark_catalog`.`default`.`time_narrow_tbl`",
+    "targetType" : "\"TIME(3)\""
+  }
+}
+
+
+-- !query
+INSERT INTO time_narrow_tbl SELECT CAST('01:02:03.456789' :: TIME(6) AS 
TIME(3))
+-- !query schema
+struct<>
+-- !query output
+
+
+
+-- !query
+SELECT typeof(t3), t3 FROM time_narrow_tbl
+-- !query schema
+struct<typeof(t3):string,t3:time(3)>
+-- !query output
+time(3)        01:02:03.456
+
+
+-- !query
+DROP TABLE time_narrow_tbl
+-- !query schema
+struct<>
+-- !query output
+


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

Reply via email to