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

dtenedor 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 7fd360913344 [SPARK-60003][SQL] Do not promote collation-only CAST 
when applying LCT
7fd360913344 is described below

commit 7fd3609133445be25b934485bde6c4704d578d42
Author: Serge Rielau <[email protected]>
AuthorDate: Tue Oct 6 22:38:22 2026 -0700

    [SPARK-60003][SQL] Do not promote collation-only CAST when applying LCT
    
    ### What changes were proposed in this pull request?
    
    SPARK-58798 nested every `Cast` when applying string-family LCT so 
CHAR/VARCHAR truncation and padding stay on the inner node. That is correct 
when the length constraint changes. For a collation-only change, wrapping 
`CAST(non-string AS STRING COLLATE x)` makes the outer Cast see a string child, 
so collation strength is promoted from Default to Implicit. A `CASE` of that 
CAST next to a UTF8_BINARY literal then becomes Implicit UNICODE, which beats 
an Implicit column in `concat` and fa [...]
    
    This PR:
    - Nests Cast only when the CHAR/VARCHAR constraint changes; collation-only 
LCT retargets with `copy(dataType)` so a non-string CAST stays Default.
    - Keeps SPARK-58798 same-collation LCT (`max(n, m)` including mixed 
Implicit vs Default CHAR).
    - Restores the pre-SPARK-58798 Default rule that UTF8_BINARY yields to the 
other Default collation. Two Implicit columns, or two non-UTF8 Default CASTs, 
still mismatch.
    
    Does not change CAST collation strength to Implicit.
    
    ### Why are the changes needed?
    
    `CREATE TABLE AS SELECT` of `a || ... || CASE ... TRY_CAST(b AS STRING 
COLLATE UNICODE_CI_AI) ...` fails with `INDETERMINATE_COLLATION_IN_SCHEMA` when 
`a` is already that collation. The analyzed CASE was Implicit UNICODE vs 
Default UTF8_BINARY, so CASE became Implicit and then conflicted with concat.
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes. After SPARK-58798, collation-only LCT wrapping could raise 
`INDETERMINATE_COLLATION_IN_SCHEMA` on concat/CTAS that previously succeeded. 
Those queries succeed again. Two Default non-UTF8 collated CASTs still conflict.
    
    ### How was this patch tested?
    
    - `IndeterminateCollationTestSuite`: CASE/concat/CTAS repro (`TRY_CAST(date 
AS STRING COLLATE UNICODE_CI_AI)`) and negative two Default non-UTF8 CASTs.
    - `BasicCharVarcharTestSuite`: SPARK-58798 nested truncation `CAST('abcdef' 
AS VARCHAR(2)` inside COALESCE and SPARK-58794 mixed-strength CHAR pad.
    
    ```
    JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64 build/sbt 
-Dsbt.override.build.repos=true \
      'sql/testOnly 
org.apache.spark.sql.collation.IndeterminateCollationTestSuite 
org.apache.spark.sql.BasicCharVarcharTestSuite -- -z "collated mixed-length 
LCT" -z "collated cast of non-string" -z "two Default non-UTF8" -z "least 
common type for COALESCE"'
    ```
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    Generated-by: Cursor Grok 4.6
    
    Closes #59260 from srielau/SPARK-60003.
    
    Authored-by: Serge Rielau <[email protected]>
    Signed-off-by: Daniel Tenedorio <[email protected]>
---
 .../catalyst/analysis/CollationTypeCoercion.scala  | 32 ++++++--------
 .../apache/spark/sql/CharVarcharTestSuite.scala    | 10 +++++
 .../IndeterminateCollationTestSuite.scala          | 51 ++++++++++++++++++++++
 3 files changed, 74 insertions(+), 19 deletions(-)

diff --git 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/CollationTypeCoercion.scala
 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/CollationTypeCoercion.scala
index ea1a1917d8f5..82160dec2a18 100644
--- 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/CollationTypeCoercion.scala
+++ 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/CollationTypeCoercion.scala
@@ -111,12 +111,10 @@ object CollationTypeCoercion extends SQLConfHelper {
   /**
    * Changes the data type of the expression to the given `newType`.
    *
-   * Never retarget an existing Cast (`cast.copy(dataType = ...)`). Explicit 
CAST
-   * truncation / overflow (ISO 6.13) and CHAR padding must stay on the inner 
node;
-   * LCT is an outer Cast. Uncollated TypeCoercion already nests.
-   *
-   * Literals: `copy(dataType)` is enough when only collation changes. When a 
string
-   * constraint changes (CHAR(2) to CHAR(4)), wrap in Cast so padding is 
re-applied.
+   * Literal and Cast: wrap in a new Cast when the CHAR/VARCHAR constraint
+   * changes so truncation, overflow, and padding stay on the original node.
+   * Collation-only changes retarget with `copy(dataType)`, which keeps a
+   * non-string Cast child non-string and therefore Default strength.
    */
   private def changeType(expr: Expression, newType: DataType): Expression = {
     mergeTypes(expr.dataType, newType) match {
@@ -127,8 +125,10 @@ object CollationTypeCoercion extends SQLConfHelper {
           case lit: Literal if stringConstraintChanged(lit.dataType, 
newDataType) =>
             Cast(lit, newDataType, timeZoneId = 
Some(conf.sessionLocalTimeZone))
           case lit: Literal => lit.copy(dataType = newDataType)
-          case cast: Cast =>
+          case cast: Cast if stringConstraintChanged(cast.dataType, 
newDataType) =>
             Cast(cast, newDataType, timeZoneId = 
Some(conf.sessionLocalTimeZone))
+          case cast: Cast =>
+            cast.copy(dataType = newDataType)
           case subquery: SubqueryExpression =>
             changeTypeInSubquery(subquery, newType)
 
@@ -448,19 +448,13 @@ object CollationTypeCoercion extends SQLConfHelper {
   /**
    * Resolves collation strength independently of CHAR/VARCHAR length.
    *
-   * This rule always runs. First-class CHAR/VARCHAR appear whenever
-   * `charVarcharFirstClassTypes` is true (`standardSemantics` or
-   * `preserveCharVarcharTypeInfo`), not only under `standardSemantics`.
-   *
-   * Same collation, including mixed strength: take the string-family LCT 
`max(n, m)`
-   * (pads, never truncates) and attach the stronger strength. Example:
-   * `coalesce(CAST('a' AS CHAR(2) COLLATE UTF8_LCASE),
-   * CAST(1 AS CHAR(4) COLLATE UTF8_LCASE))` is CHAR(4) COLLATE UTF8_LCASE
-   * (Implicit CHAR(2) from a string CAST vs Default CHAR(4) from a non-string 
CAST).
+   * Runs whenever first-class CHAR/VARCHAR are enabled (`standardSemantics` or
+   * `preserveCharVarcharTypeInfo`).
    *
-   * Different collations at equal strength: mismatch (error if Explicit, else
-   * indeterminate). Different collations at unequal strength: the stronger 
operand
-   * wins in full, including its length (SQL collation precedence).
+   * Same collation: string-family LCT `max(n, m)` with the stronger strength.
+   * Different collations, equal strength: mismatch (error if Explicit, else
+   * indeterminate). Different collations, unequal strength: the stronger
+   * operand wins in full, including its length.
    */
   private def getWinningStringType(
       left: StringTypeWithContext,
diff --git 
a/sql/core/src/test/scala/org/apache/spark/sql/CharVarcharTestSuite.scala 
b/sql/core/src/test/scala/org/apache/spark/sql/CharVarcharTestSuite.scala
index 9f5274d18701..ab553de65de6 100644
--- a/sql/core/src/test/scala/org/apache/spark/sql/CharVarcharTestSuite.scala
+++ b/sql/core/src/test/scala/org/apache/spark/sql/CharVarcharTestSuite.scala
@@ -1189,6 +1189,14 @@ class BasicCharVarcharTestSuite extends 
SharedSparkSession {
       // LCT(NULL, T) = T
       assert(sql("SELECT coalesce(null, cast('a' AS CHAR(5))) AS c")
         .schema.head.dataType === CharType(5))
+      // SPARK-58798 intent: LCT widen nests Cast. Inner CAST('abcdef' AS 
VARCHAR(2)/CHAR(2))
+      // truncates to "ab"; retargeting the inner Cast to length 4 would yield 
"abcd".
+      checkAnswer(
+        sql("SELECT coalesce(cast('abcdef' AS VARCHAR(2)), cast('x' AS 
VARCHAR(4))) AS c"),
+        Row("ab"))
+      checkAnswer(
+        sql("SELECT coalesce(cast('abcdef' AS CHAR(2)), cast('x' AS CHAR(4))) 
AS c"),
+        Row("ab  "))
     }
   }
 
@@ -1549,6 +1557,8 @@ class BasicCharVarcharTestSuite extends 
SharedSparkSession {
           s"$key=$value same-strength mixed CHAR lengths")
         assert(sql(mixedStrength).schema.head.dataType === CharType(4, 
"UTF8_LCASE"),
           s"$key=$value Implicit CHAR(2) vs Default CHAR(4) must widen, not 
narrow")
+        // Mixed strength must still pad the Implicit CHAR(2) to CHAR(4), not 
keep length 2.
+        checkAnswer(sql(mixedStrength), Row("a   "))
       }
     }
   }
diff --git 
a/sql/core/src/test/scala/org/apache/spark/sql/collation/IndeterminateCollationTestSuite.scala
 
b/sql/core/src/test/scala/org/apache/spark/sql/collation/IndeterminateCollationTestSuite.scala
index 8c5d2eb9d40f..f60153034f8b 100644
--- 
a/sql/core/src/test/scala/org/apache/spark/sql/collation/IndeterminateCollationTestSuite.scala
+++ 
b/sql/core/src/test/scala/org/apache/spark/sql/collation/IndeterminateCollationTestSuite.scala
@@ -259,4 +259,55 @@ class IndeterminateCollationTestSuite extends 
SharedSparkSession {
       sql(s"SELECT c1 || c2 as col FROM $testTableName").show()
     }
   }
+
+  // SPARK-60003: a Default-strength collated CAST over a non-string child
+  // (TRY_CAST(date AS STRING COLLATE UNICODE_CI_AI)) in a CASE next to a 
default literal
+  // must not poison an outer concat whose other operand is already 
UNICODE_CI_AI.
+  test("collated cast of non-string in CASE concat stays determinate") {
+    withTable(testTableName, "tst_cast_collation_out") {
+      sql(
+        s"""
+           |CREATE TABLE $testTableName (
+           |  a STRING COLLATE UNICODE_CI_AI,
+           |  b DATE
+           |) USING $dataSource
+           |""".stripMargin)
+      sql(s"INSERT INTO $testTableName VALUES ('x', DATE'2020-01-01')")
+
+      checkAnswer(
+        sql(
+          s"""SELECT collation(
+             |  a || 'X' ||
+             |  (CASE WHEN b IS NULL THEN 'XXXXXX'
+             |        ELSE TRY_CAST(b AS STRING COLLATE UNICODE_CI_AI) END) || 
'X')
+             |FROM $testTableName""".stripMargin),
+        Row("SYSTEM.BUILTIN.UNICODE_CI_AI"))
+
+      sql(
+        s"""CREATE TABLE tst_cast_collation_out USING $dataSource AS SELECT
+           |  a || 'X' ||
+           |  (CASE WHEN b IS NULL THEN 'XXXXXX'
+           |        ELSE TRY_CAST(b AS STRING COLLATE UNICODE_CI_AI) END) || 
'X' AS o
+           |FROM $testTableName""".stripMargin)
+    }
+  }
+
+  // SPARK-58798 did not change this: two Default non-UTF8 collated CASTs 
still conflict.
+  test("two Default non-UTF8 collated casts stay indeterminate") {
+    withTable(testTableName, "tst_two_default_casts") {
+      sql(
+        s"""
+           |CREATE TABLE $testTableName (
+           |  a STRING COLLATE UNICODE_CI_AI,
+           |  b DATE
+           |) USING $dataSource
+           |""".stripMargin)
+      assertIndeterminateCollationInSchemaError("o")(
+        sql(
+          s"""CREATE TABLE tst_two_default_casts USING $dataSource AS SELECT
+             |  TRY_CAST(b AS STRING COLLATE UNICODE_CI_AI) ||
+             |  TRY_CAST(b AS STRING COLLATE UTF8_LCASE) AS o
+             |FROM $testTableName""".stripMargin))
+    }
+  }
 }


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

Reply via email to