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

HyukjinKwon 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 a584b11749e8 [SPARK-60574][SQL] Quote collated string literals in 
connector expressions
a584b11749e8 is described below

commit a584b11749e817b60af14ce5cc91099d5af21b0a
Author: Linhong Liu <[email protected]>
AuthorDate: Wed Oct 28 10:24:58 2026 -0700

    [SPARK-60574][SQL] Quote collated string literals in connector expressions
    
    ### What changes were proposed in this pull request?
    
    Quote every StringType literal in the connector SQL renderer, including 
non-default collations and CHAR/VARCHAR string types. Add a comment explaining 
why the type pattern is required.
    
    ### Why are the changes needed?
    
    The `StringType` singleton pattern matches only UTF8_BINARY strings without 
a length constraint: `StringType.equals` compares both the collation ID and the 
constraint. It therefore misses non-default collations and CHAR/VARCHAR 
literals, even when their collation is UTF8_BINARY, and renders them without 
SQL quoting or escaping. A UTF8_LCASE literal containing `it's` currently 
renders as `it's` without surrounding SQL quotes, instead of `'it''s'`. Pattern 
predicates strip the assumed s [...]
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes, a bug fix. Collated string literals and CHAR/VARCHAR literals receive 
the same quoting and escaping as ordinary string literals. NULL continues to 
render as NULL.
    
    ### How was this patch tested?
    
    Added regression coverage for collated and constrained string types, NULL, 
empty and quoted strings, and pattern-predicate descriptions. Four new literal 
cases fail on unchanged Apache code. On JDK 17, all 10 LiteralValueSuite tests, 
21 V2PredicateSuite tests, and the JDBC filter-compilation test passed. 
Catalyst main/test and SQL test Scalastyle checks and git diff --check passed.
    
    ```bash
    build/sbt 'catalyst/testOnly 
org.apache.spark.sql.connector.expressions.LiteralValueSuite'
    build/sbt 'sql/testOnly 
org.apache.spark.sql.execution.datasources.v2.V2PredicateSuite'
    build/sbt 'sql/testOnly org.apache.spark.sql.jdbc.JDBCSuite -- -z "compile 
filters"'
    ```
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    Generated-by: OpenAI Codex (GPT-6)
    
    Closes #59812 from linhongliu-db/fix-collated-literal-quoting.
    
    Authored-by: Linhong Liu <[email protected]>
    Signed-off-by: Hyukjin Kwon <[email protected]>
---
 .../spark/sql/connector/expressions/expressions.scala    |  6 +++++-
 .../sql/connector/expressions/LiteralValueSuite.scala    | 16 +++++++++++++++-
 .../sql/execution/datasources/v2/V2PredicateSuite.scala  | 16 ++++++++++++++++
 3 files changed, 36 insertions(+), 2 deletions(-)

diff --git 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/connector/expressions/expressions.scala
 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/connector/expressions/expressions.scala
index 0028f78d22f9..409681cb1156 100644
--- 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/connector/expressions/expressions.scala
+++ 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/connector/expressions/expressions.scala
@@ -393,7 +393,11 @@ private[sql] final case class LiteralValue[T](value: T, 
dataType: DataType) exte
       "NULL"
     } else {
       dataType match {
-        case StringType => s"'${s"$value".replace("'", "''")}'"
+        // The StringType value pattern matches only UTF8_BINARY strings 
without a length
+        // constraint. Other collations and CHAR/VARCHAR are StringType 
instances that need the
+        // same quoting and escaping.
+        // Pattern predicates also rely on these quotes when extracting the 
literal contents.
+        case _: StringType => s"'${s"$value".replace("'", "''")}'"
         case BinaryType =>
           assert(value.isInstanceOf[Array[Byte]])
           val bytes = value.asInstanceOf[Array[Byte]]
diff --git 
a/sql/catalyst/src/test/scala/org/apache/spark/sql/connector/expressions/LiteralValueSuite.scala
 
b/sql/catalyst/src/test/scala/org/apache/spark/sql/connector/expressions/LiteralValueSuite.scala
index 9a21f14e022a..bc408db48317 100644
--- 
a/sql/catalyst/src/test/scala/org/apache/spark/sql/connector/expressions/LiteralValueSuite.scala
+++ 
b/sql/catalyst/src/test/scala/org/apache/spark/sql/connector/expressions/LiteralValueSuite.scala
@@ -18,7 +18,8 @@
 package org.apache.spark.sql.connector.expressions
 
 import org.apache.spark.SparkFunSuite
-import org.apache.spark.sql.types.{BinaryType, BooleanType, IntegerType, 
StringType}
+import org.apache.spark.sql.types.{BinaryType, BooleanType, CharType, 
IntegerType, StringType, VarcharType}
+import org.apache.spark.unsafe.types.UTF8String
 
 class LiteralValueSuite extends SparkFunSuite {
 
@@ -35,6 +36,19 @@ class LiteralValueSuite extends SparkFunSuite {
     assert(LiteralValue("it's", StringType).toString === "'it''s'")
   }
 
+  for (dataType <- Seq(
+      StringType, StringType("UTF8_LCASE"), StringType("UNICODE_CI"),
+      CharType(8), VarcharType(8))) {
+    test(s"collated and constrained string literals should be quoted: 
$dataType") {
+      for ((value, expected) <- Seq(
+          "" -> "''", "a" -> "'a'", "abc" -> "'abc'", "null" -> "'null'",
+          "it's" -> "'it''s'", "a%b_c" -> "'a%b_c'", "a\\b" -> "'a\\b'")) {
+        assert(LiteralValue(UTF8String.fromString(value), dataType).toString 
=== expected)
+      }
+      assert(LiteralValue(null, dataType).toString === "NULL")
+    }
+  }
+
   test("non-null numeric literals should not be quoted") {
     assert(LiteralValue(42, IntegerType).toString === "42")
     assert(LiteralValue(0, IntegerType).toString === "0")
diff --git 
a/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/v2/V2PredicateSuite.scala
 
b/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/v2/V2PredicateSuite.scala
index b3a097180725..932a105dd392 100644
--- 
a/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/v2/V2PredicateSuite.scala
+++ 
b/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/v2/V2PredicateSuite.scala
@@ -308,6 +308,22 @@ class V2PredicateSuite extends SparkFunSuite {
     assert(PredicateUtils.toV1(predicate4) == None)
   }
 
+  Seq("STARTS_WITH", "ENDS_WITH", "CONTAINS").foreach { predicateName =>
+    test(s"$predicateName descriptions preserve collated string literals") {
+      for {
+        collation <- Seq("UTF8_LCASE", "UNICODE_CI")
+        value <- Seq("", "a", "abc", "it's", "a%b_c", "a\\b")
+      } {
+        val string = UTF8String.fromString(value)
+        val predicate = new Predicate(predicateName,
+          Array[Expression](ref("a"), LiteralValue(string, 
StringType(collation))))
+        val binaryPredicate = new Predicate(predicateName,
+          Array[Expression](ref("a"), LiteralValue(string, StringType)))
+        assert(predicate.describe() == binaryPredicate.describe())
+      }
+    }
+  }
+
   test("StringStartsWith") {
     val literal = LiteralValue(UTF8String.fromString("str"), StringType)
     val predicate1 = new Predicate("STARTS_WITH",


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

Reply via email to