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 a9472969d8f5 [SPARK-57777][SQL][CONNECT] Distinguish explicit 
collation when rendering string literals to SQL
a9472969d8f5 is described below

commit a9472969d8f574b86e0deb33f9676f1e1e66508c
Author: Wenchen Fan <[email protected]>
AuthorDate: Wed Jul 1 22:11:39 2026 +0200

    [SPARK-57777][SQL][CONNECT] Distinguish explicit collation when rendering 
string literals to SQL
    
    ### What changes were proposed in this pull request?
    
    This PR makes `Literal.sql` render an explicit `collate` clause for any 
string literal whose
    type carries an explicit collation, **including an explicit 
`UTF8_BINARY`**, while still rendering
    the default (un-collated) `StringType` without a clause.
    
    `Literal.sql` previously had two arms for string literals:
    
    ```scala
    case (v: UTF8String, StringType) =>            // matched any UTF8_BINARY 
StringType by value equality
      "'" + escaped + "'"
    case (v: UTF8String, st: StringType) =>        // only reached for 
non-UTF8_BINARY collations
      "'" + escaped + "'" + st.typeName.substring(6)
    ```
    
    The first arm matches via the `StringType` case object's `equals` (which 
compares `collationId`
    and `constraint`), so it collapsed both the *default* `StringType` and an 
*explicitly collated*
    `UTF8_BINARY` `StringType` into the same clause-less output. The two arms 
are now merged into one
    that decides the clause via 
`DataTypeUtils.isDefaultStringCharOrVarcharType` (the same
    singleton-identity check used elsewhere, e.g. by `ApplyDefaultCollation` 
and `SHOW CREATE TABLE`):
    
    ```scala
    case (v: UTF8String, st: StringType) =>
      val collateClause =
        if (DataTypeUtils.isDefaultStringCharOrVarcharType(st)) "" else s" 
collate ${st.collationName}"
      "'" + escaped + "'" + collateClause
    ```
    
    For non-default collations the produced string is byte-for-byte identical 
to the previous
    `st.typeName.substring(6)` output (`typeName` is `s"string collate 
$collationName"`).
    
    This PR also removes a test-only normalization in `PlanGenerationTestSuite` 
that stamped
    `UTF8_BINARY` onto every string-type proto whose `collation` field was 
empty before writing the
    golden files. That shim made the generated `query-tests` golden artifacts 
(`.proto.bin` / `.json`,
    and the downstream `.explain` files) misrepresent the real wire format: a 
default `StringType` is
    serialized with an **empty** `collation` field (the "undetermined / default 
collation" sentinel),
    not `UTF8_BINARY`. The affected golden files were regenerated so they now 
reflect what a real
    client actually sends.
    
    ### Why are the changes needed?
    
    A default `StringType` and an explicitly-`UTF8_BINARY` `StringType` are 
semantically different:
    the former is "undetermined" and is eligible to inherit a default collation 
during analysis (e.g.
    `CREATE TABLE ... DEFAULT COLLATION UTF8_LCASE AS SELECT 'x'`), while the 
latter is explicitly
    pinned and must not inherit. `Literal.sql` is used in view text, `SHOW 
CREATE TABLE`, error
    messages, etc.; rendering an explicitly-collated `UTF8_BINARY` literal 
without a `collate` clause
    loses that distinction and is not faithful on re-parse. This aligns 
string-literal SQL rendering
    with how the rest of the engine already distinguishes explicit collation.
    
    The test-normalization removal is a correctness fix for the Spark Connect 
golden files: they are
    meant to be a faithful record of the protocol, and they were showing 
`collate UTF8_BINARY` on
    default strings where the actual proto omits the collation.
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes. `Literal.sql` now appends ` collate UTF8_BINARY` when a string 
literal's type is an explicit
    (non-default) `UTF8_BINARY` `StringType`. A plain default string literal is 
unchanged (no clause),
    and literals with other explicit collations are unchanged. This affects SQL 
text generated from
    literals (e.g. view definitions, `SHOW CREATE TABLE`, error messages).
    
    ### How was this patch tested?
    
    Existing suites, all passing:
    
    - `catalyst/testOnly 
org.apache.spark.sql.catalyst.expressions.LiteralExpressionSuite` (49)
    - `connect-client-jvm/testOnly 
org.apache.spark.sql.PlanGenerationTestSuite` (727; golden files regenerated 
and reviewed)
    - `connect/testOnly 
org.apache.spark.sql.connect.ProtoToParsedPlanTestSuite` (732; explain golden 
regenerated)
    - `sql/testOnly org.apache.spark.sql.SQLQueryTestSuite` for the collation 
inputs (`collations-basic`, `view-with-default-collation`, 
`collations-padding-trim`, `collations-string-functions`, `collations-aliases`, 
`listagg-collations`) (12; no golden changes)
    - `sql/testOnly 
org.apache.spark.sql.execution.command.{v1,v2}.ShowCreateTableSuite` (47)
    
    The regenerated golden diff is limited to dropping the test-stamped 
`collation: "UTF8_BINARY"`
    from default-string proto types and the corresponding `CAST(NULL AS STRING 
COLLATE UTF8_BINARY)`
    -> `CAST(NULL AS STRING)` in one explain file.
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Code (Opus 4.8)
    
    Closes #56892 from cloud-fan/cloud-fan/string-literal-default-collation.
    
    Authored-by: Wenchen Fan <[email protected]>
    Signed-off-by: Max Gekk <[email protected]>
---
 .../spark/sql/catalyst/expressions/literals.scala   |  17 ++++++++++++-----
 .../expressions/LiteralExpressionSuite.scala        |  13 +++++++++++++
 .../apache/spark/sql/PlanGenerationTestSuite.scala  |  12 ------------
 .../explain-results/function_typedLit.explain       |   2 +-
 .../query-tests/queries/csv_from_dataset.json       |   1 -
 .../query-tests/queries/csv_from_dataset.proto.bin  | Bin 169 -> 156 bytes
 .../query-tests/queries/function_lit_array.json     |   2 --
 .../queries/function_lit_array.proto.bin            | Bin 5346 -> 5320 bytes
 .../query-tests/queries/function_typedLit.json      |  16 ----------------
 .../query-tests/queries/function_typedLit.proto.bin | Bin 11681 -> 11471 bytes
 .../query-tests/queries/json_from_dataset.json      |   1 -
 .../query-tests/queries/json_from_dataset.proto.bin | Bin 180 -> 167 bytes
 12 files changed, 26 insertions(+), 38 deletions(-)

diff --git 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/literals.scala
 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/literals.scala
index b1ed1a494be8..cea16999c437 100644
--- 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/literals.scala
+++ 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/literals.scala
@@ -592,13 +592,20 @@ case class Literal (value: Any, dataType: DataType) 
extends LeafExpression {
   override def sql: String = (value, dataType) match {
     case (_, NullType | _: ArrayType | _: MapType | _: StructType) if value == 
null => "NULL"
     case _ if value == null => s"CAST(NULL AS ${dataType.sql})"
-    case (v: UTF8String, StringType) =>
-      // Escapes all backslashes and single quotes.
-      "'" + v.toString.replace("\\", "\\\\").replace("'", "\\'") + "'"
     case (v: UTF8String, st: StringType) =>
+      // Only render a `collate` clause for an explicit collation (including 
an explicit
+      // `UTF8_BINARY`). The default `StringType` (the case object) has no 
explicit collation, so
+      // it must render without a clause and stay distinguishable from an 
explicitly-collated
+      // string on re-parse (e.g. so that default-collation resolution does 
not treat an
+      // explicitly-collated literal as eligible for inheriting a default 
collation).
+      val collateClause =
+        if (DataTypeUtils.isDefaultStringCharOrVarcharType(st)) {
+          ""
+        } else {
+          s" collate ${st.collationName}"
+        }
       // Escapes all backslashes and single quotes.
-      "'" + v.toString.replace("\\", "\\\\").replace("'", "\\'") +
-        "'" + st.typeName.substring(6)
+      "'" + v.toString.replace("\\", "\\\\").replace("'", "\\'") + "'" + 
collateClause
     case (v: Byte, ByteType) => s"${v}Y"
     case (v: Short, ShortType) => s"${v}S"
     case (v: Long, LongType) => s"${v}L"
diff --git 
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/LiteralExpressionSuite.scala
 
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/LiteralExpressionSuite.scala
index b69b29f78914..58fff9bad849 100644
--- 
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/LiteralExpressionSuite.scala
+++ 
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/LiteralExpressionSuite.scala
@@ -842,4 +842,17 @@ class LiteralExpressionSuite extends SparkFunSuite with 
ExpressionEvalHelper {
     assert(lit.dataType === GeometryType(0))
     assert(lit.value.isInstanceOf[BinaryView])
   }
+
+  test("SPARK-57777: render explicit collation in string literal SQL") {
+    // The default `StringType` (case object) has no explicit collation, so it 
renders
+    // without a `collate` clause.
+    assert(Literal(UTF8String.fromString("x"), StringType).sql === "'x'")
+    // A non-default (non-singleton) `UTF8_BINARY` `StringType` is an explicit 
collation, so it
+    // renders the clause and stays distinguishable from the default on 
re-parse.
+    assert(Literal(UTF8String.fromString("x"), StringType("UTF8_BINARY")).sql 
===
+      "'x' collate UTF8_BINARY")
+    // Other explicit collations are rendered as before.
+    assert(Literal(UTF8String.fromString("x"), StringType("UTF8_LCASE")).sql 
===
+      "'x' collate UTF8_LCASE")
+  }
 }
diff --git 
a/sql/connect/client/jvm/src/test/scala/org/apache/spark/sql/PlanGenerationTestSuite.scala
 
b/sql/connect/client/jvm/src/test/scala/org/apache/spark/sql/PlanGenerationTestSuite.scala
index a74b25459bad..36b2cab4d586 100644
--- 
a/sql/connect/client/jvm/src/test/scala/org/apache/spark/sql/PlanGenerationTestSuite.scala
+++ 
b/sql/connect/client/jvm/src/test/scala/org/apache/spark/sql/PlanGenerationTestSuite.scala
@@ -198,7 +198,6 @@ class PlanGenerationTestSuite extends ConnectFunSuite with 
Logging {
   /**
    * Normalize proto messages for stable comparison:
    *   - Trim JVM origin fields (lines, stack traces, anonymous function names)
-   *   - Populate default StringType collation when missing (UTF8_BINARY)
    */
   private def normalizeProtoForComparison[T <: protobuf.Message](message: T): 
T = {
     def trim(builder: proto.JvmOrigin.Builder): Unit = {
@@ -221,17 +220,6 @@ class PlanGenerationTestSuite extends ConnectFunSuite with 
Logging {
     val builder = message.toBuilder
 
     builder match {
-      // For comparison only, we add UTF8_BINARY when StringType collation is 
missing
-      // to ensure deterministic plan equality across environments.
-      case dt: proto.DataType.Builder if dt.getKindCase == 
proto.DataType.KindCase.STRING =>
-        val sb = dt.getStringBuilder
-        if (sb.getCollation.isEmpty) {
-          val defaultCollationName =
-            CollationFactory
-              .fetchCollation(CollationFactory.UTF8_BINARY_COLLATION_ID)
-              .collationName
-          sb.setCollation(defaultCollationName)
-        }
       case exp: proto.Relation.Builder
           if exp.hasCommon && exp.getCommon.hasOrigin && 
exp.getCommon.getOrigin.hasJvmOrigin =>
         trim(exp.getCommonBuilder.getOriginBuilder.getJvmOriginBuilder)
diff --git 
a/sql/connect/common/src/test/resources/query-tests/explain-results/function_typedLit.explain
 
b/sql/connect/common/src/test/resources/query-tests/explain-results/function_typedLit.explain
index 5a827ca88ee7..3c878be34143 100644
--- 
a/sql/connect/common/src/test/resources/query-tests/explain-results/function_typedLit.explain
+++ 
b/sql/connect/common/src/test/resources/query-tests/explain-results/function_typedLit.explain
@@ -1,2 +1,2 @@
-Project [id#0L, id#0L, 1 AS 1#0, null AS NULL#0, true AS true#0, 68 AS 68#0, 
9872 AS 9872#0, -8726532 AS -8726532#0, 7834609328726532 AS 
7834609328726532#0L, 2.718281828459045 AS 2.718281828459045#0, -0.8 AS -0.8#0, 
89.97620 AS 89.97620#0, 89889.7667231 AS 89889.7667231#0, connect! AS 
connect!#0, T AS T#0, ABCDEFGHIJ AS ABCDEFGHIJ#0, 
0x78797A7B7C7D7E7F808182838485868788898A8B8C8D8E AS 
X'78797A7B7C7D7E7F808182838485868788898A8B8C8D8E'#0, 0x0806 AS X'0806'#0, [8,6] 
AS ARRAY(8, 6)#0, null A [...]
+Project [id#0L, id#0L, 1 AS 1#0, null AS NULL#0, true AS true#0, 68 AS 68#0, 
9872 AS 9872#0, -8726532 AS -8726532#0, 7834609328726532 AS 
7834609328726532#0L, 2.718281828459045 AS 2.718281828459045#0, -0.8 AS -0.8#0, 
89.97620 AS 89.97620#0, 89889.7667231 AS 89889.7667231#0, connect! AS 
connect!#0, T AS T#0, ABCDEFGHIJ AS ABCDEFGHIJ#0, 
0x78797A7B7C7D7E7F808182838485868788898A8B8C8D8E AS 
X'78797A7B7C7D7E7F808182838485868788898A8B8C8D8E'#0, 0x0806 AS X'0806'#0, [8,6] 
AS ARRAY(8, 6)#0, null A [...]
 +- LocalRelation <empty>, [id#0L, a#0, b#0]
diff --git 
a/sql/connect/common/src/test/resources/query-tests/queries/csv_from_dataset.json
 
b/sql/connect/common/src/test/resources/query-tests/queries/csv_from_dataset.json
index e4b31258f984..d34fcb6f758e 100644
--- 
a/sql/connect/common/src/test/resources/query-tests/queries/csv_from_dataset.json
+++ 
b/sql/connect/common/src/test/resources/query-tests/queries/csv_from_dataset.json
@@ -18,7 +18,6 @@
           "name": "c1",
           "dataType": {
             "string": {
-              "collation": "UTF8_BINARY"
             }
           },
           "nullable": true
diff --git 
a/sql/connect/common/src/test/resources/query-tests/queries/csv_from_dataset.proto.bin
 
b/sql/connect/common/src/test/resources/query-tests/queries/csv_from_dataset.proto.bin
index c39243a10a8e..5f8bd50685ca 100644
Binary files 
a/sql/connect/common/src/test/resources/query-tests/queries/csv_from_dataset.proto.bin
 and 
b/sql/connect/common/src/test/resources/query-tests/queries/csv_from_dataset.proto.bin
 differ
diff --git 
a/sql/connect/common/src/test/resources/query-tests/queries/function_lit_array.json
 
b/sql/connect/common/src/test/resources/query-tests/queries/function_lit_array.json
index 9b77a8bd9b42..9e5227af8685 100644
--- 
a/sql/connect/common/src/test/resources/query-tests/queries/function_lit_array.json
+++ 
b/sql/connect/common/src/test/resources/query-tests/queries/function_lit_array.json
@@ -521,7 +521,6 @@
           "array": {
             "elementType": {
               "string": {
-                "collation": "UTF8_BINARY"
               }
             },
             "containsNull": true
@@ -579,7 +578,6 @@
           "array": {
             "elementType": {
               "string": {
-                "collation": "UTF8_BINARY"
               }
             },
             "containsNull": true
diff --git 
a/sql/connect/common/src/test/resources/query-tests/queries/function_lit_array.proto.bin
 
b/sql/connect/common/src/test/resources/query-tests/queries/function_lit_array.proto.bin
index 20b6d81c3cee..db81bd4e7eba 100644
Binary files 
a/sql/connect/common/src/test/resources/query-tests/queries/function_lit_array.proto.bin
 and 
b/sql/connect/common/src/test/resources/query-tests/queries/function_lit_array.proto.bin
 differ
diff --git 
a/sql/connect/common/src/test/resources/query-tests/queries/function_typedLit.json
 
b/sql/connect/common/src/test/resources/query-tests/queries/function_typedLit.json
index 41ca771596ef..eba5b99fe495 100644
--- 
a/sql/connect/common/src/test/resources/query-tests/queries/function_typedLit.json
+++ 
b/sql/connect/common/src/test/resources/query-tests/queries/function_typedLit.json
@@ -82,7 +82,6 @@
         },
         "dataType": {
           "string": {
-            "collation": "UTF8_BINARY"
           }
         }
       },
@@ -958,7 +957,6 @@
                   "name": "_2",
                   "dataType": {
                     "string": {
-                      "collation": "UTF8_BINARY"
                     }
                   },
                   "nullable": true
@@ -1023,7 +1021,6 @@
                   "name": "_2",
                   "dataType": {
                     "string": {
-                      "collation": "UTF8_BINARY"
                     }
                   },
                   "nullable": true
@@ -1106,7 +1103,6 @@
           "map": {
             "keyType": {
               "string": {
-                "collation": "UTF8_BINARY"
               }
             },
             "valueType": {
@@ -1157,7 +1153,6 @@
           "map": {
             "keyType": {
               "string": {
-                "collation": "UTF8_BINARY"
               }
             },
             "valueType": {
@@ -1202,7 +1197,6 @@
               "name": "_1",
               "dataType": {
                 "string": {
-                  "collation": "UTF8_BINARY"
                 }
               },
               "nullable": true
@@ -1545,7 +1539,6 @@
                   "name": "_2",
                   "dataType": {
                     "string": {
-                      "collation": "UTF8_BINARY"
                     }
                   },
                   "nullable": true
@@ -1555,7 +1548,6 @@
                     "array": {
                       "elementType": {
                         "string": {
-                          "collation": "UTF8_BINARY"
                         }
                       },
                       "containsNull": true
@@ -1629,7 +1621,6 @@
                   "name": "b",
                   "dataType": {
                     "string": {
-                      "collation": "UTF8_BINARY"
                     }
                   },
                   "nullable": true
@@ -1707,7 +1698,6 @@
               "map": {
                 "keyType": {
                   "string": {
-                    "collation": "UTF8_BINARY"
                   }
                 },
                 "valueType": {
@@ -1813,14 +1803,12 @@
               "map": {
                 "keyType": {
                   "string": {
-                    "collation": "UTF8_BINARY"
                   }
                 },
                 "valueType": {
                   "array": {
                     "elementType": {
                       "string": {
-                        "collation": "UTF8_BINARY"
                       }
                     },
                     "containsNull": true
@@ -1896,7 +1884,6 @@
               "map": {
                 "keyType": {
                   "string": {
-                    "collation": "UTF8_BINARY"
                   }
                 },
                 "valueType": {
@@ -1992,7 +1979,6 @@
                 "map": {
                   "keyType": {
                     "string": {
-                      "collation": "UTF8_BINARY"
                     }
                   },
                   "valueType": {
@@ -2010,7 +1996,6 @@
                     "name": "_1",
                     "dataType": {
                       "string": {
-                        "collation": "UTF8_BINARY"
                       }
                     },
                     "nullable": true
@@ -2024,7 +2009,6 @@
                         },
                         "valueType": {
                           "string": {
-                            "collation": "UTF8_BINARY"
                           }
                         },
                         "valueContainsNull": true
diff --git 
a/sql/connect/common/src/test/resources/query-tests/queries/function_typedLit.proto.bin
 
b/sql/connect/common/src/test/resources/query-tests/queries/function_typedLit.proto.bin
index 5068b513a927..4388912bdb1f 100644
Binary files 
a/sql/connect/common/src/test/resources/query-tests/queries/function_typedLit.proto.bin
 and 
b/sql/connect/common/src/test/resources/query-tests/queries/function_typedLit.proto.bin
 differ
diff --git 
a/sql/connect/common/src/test/resources/query-tests/queries/json_from_dataset.json
 
b/sql/connect/common/src/test/resources/query-tests/queries/json_from_dataset.json
index f29245374e6e..d6f992d09a5c 100644
--- 
a/sql/connect/common/src/test/resources/query-tests/queries/json_from_dataset.json
+++ 
b/sql/connect/common/src/test/resources/query-tests/queries/json_from_dataset.json
@@ -18,7 +18,6 @@
           "name": "c1",
           "dataType": {
             "string": {
-              "collation": "UTF8_BINARY"
             }
           },
           "nullable": true
diff --git 
a/sql/connect/common/src/test/resources/query-tests/queries/json_from_dataset.proto.bin
 
b/sql/connect/common/src/test/resources/query-tests/queries/json_from_dataset.proto.bin
index 1ce2e676ce30..0fce9d9ff8c7 100644
Binary files 
a/sql/connect/common/src/test/resources/query-tests/queries/json_from_dataset.proto.bin
 and 
b/sql/connect/common/src/test/resources/query-tests/queries/json_from_dataset.proto.bin
 differ


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

Reply via email to