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

LuciferYang 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 c8a3f3d6f99b [SPARK-60028][SQL] Do not split LIKE 'prefix%suffix' over 
a nondeterministic input
c8a3f3d6f99b is described below

commit c8a3f3d6f99bc43b955abdcd27d6256f24860d8c
Author: YangJie <[email protected]>
AuthorDate: Mon Oct 12 10:58:51 2026 +0800

    [SPARK-60028][SQL] Do not split LIKE 'prefix%suffix' over a 
nondeterministic input
    
    ### What changes were proposed in this pull request?
    
    `LikeSimplification` rewrites `input LIKE 'prefix%suffix'` into a length 
check, a `StartsWith` and an `EndsWith`, each over its own copy of `input`. 
This PR makes that rewrite only when the input is deterministic. A 
nondeterministic input keeps the `LIKE`. The other single-pattern rewrites read 
the input once and are unchanged.
    
    ### Why are the changes needed?
    
    A nondeterministic input is evaluated once per copy, so the three checks 
can each see a different value and pass together for a row where no single 
value matches the pattern:
    
    ```scala
    val pick = udf(() => if (scala.util.Random.nextBoolean()) "ay" else 
"bz").asNondeterministic()
    spark.udf.register("pick", pick)
    spark.sql(
      "SELECT count_if(r) FROM (SELECT pick() LIKE 'a%z' AS r FROM range(0, 
1000, 1, 1))").show()
    // a few hundred on master (239 in one run); 0 with this change
    ```
    
    The optimized plan on master computes `r` as `((octet_length(pick()) >= 2) 
AND (StartsWith(pick(), a) AND EndsWith(pick(), z)))`. `pick() ILIKE 'A%Z'` 
becomes `lower(pick()) LIKE 'a%z'` and fails the same way. The `LIKE ALL` and 
`LIKE ANY` rewrites already skip an input that is not cheap.
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes. `LIKE` or `ILIKE` with a `'prefix%suffix'` pattern over a 
nondeterministic input now evaluates the input once per row, so the result is 
that of matching that one value. Released versions have the same rewrite, with 
`length` instead of `octet_length` in the length check.
    
    ### How was this patch tested?
    
    `LikeSimplificationSuite`: `uuid() LIKE 'abc%def'` is left unchanged, 
`uuid() LIKE 'abc%'` is still rewritten to `StartsWith`, and `substring(a, 1, 
5) LIKE 'abc%def'`, deterministic but not cheap, is still split into the length 
check, `StartsWith` and `EndsWith`. The first assertion fails without the 
change.
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Opus 5
    
    Closes #59300 from LuciferYang/SPARK-60028.
    
    Lead-authored-by: YangJie <[email protected]>
    Co-authored-by: reviewer <reviewer@local>
    Signed-off-by: yangjie01 <[email protected]>
---
 .../spark/sql/catalyst/optimizer/expressions.scala    |  4 +++-
 .../catalyst/optimizer/LikeSimplificationSuite.scala  | 19 +++++++++++++++++++
 2 files changed, 22 insertions(+), 1 deletion(-)

diff --git 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/expressions.scala
 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/expressions.scala
index 23eb8009e247..671ce87d4fa2 100644
--- 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/expressions.scala
+++ 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/expressions.scala
@@ -903,7 +903,9 @@ object LikeSimplification extends Rule[LogicalPlan] with 
PredicateHelper {
           Some(EndsWith(input, Literal.create(postfix, input.dataType)))
         // 'a%a' pattern is basically same with 'a%' && '%a'.
         // However, the additional length condition is required to prevent 'a' 
match 'a%a'.
-        case startsAndEndsWith(prefix, postfix) =>
+        // The replacement reads the input three times, so it is made only for 
a deterministic
+        // input; a nondeterministic one could give each read a different 
value.
+        case startsAndEndsWith(prefix, postfix) if input.deterministic =>
           // The length guard only rejects inputs too short to hold both the 
prefix and the
           // suffix. When the collation matches raw bytes 
(supportsBinaryEquality),
           // StartsWith/EndsWith pin the literal bytes of the prefix and 
suffix, so a
diff --git 
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/optimizer/LikeSimplificationSuite.scala
 
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/optimizer/LikeSimplificationSuite.scala
index 8b142f0c53d7..911531ff19d6 100644
--- 
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/optimizer/LikeSimplificationSuite.scala
+++ 
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/optimizer/LikeSimplificationSuite.scala
@@ -77,6 +77,25 @@ class LikeSimplificationSuite extends PlanTest {
     comparePlans(optimized, correctAnswer)
   }
 
+  test("SPARK-60028: do not simplify 'prefix%suffix' over a nondeterministic 
input") {
+    // The replacement reads the input three times, and each read could see a 
different value.
+    val input = Uuid(Some(0L))
+    val originalQuery = testRelation.where(input like "abc%def").analyze
+    comparePlans(Optimize.execute(originalQuery), originalQuery)
+
+    // A replacement that reads the input once is still made.
+    comparePlans(
+      Optimize.execute(testRelation.where(input like "abc%").analyze),
+      testRelation.where(StartsWith(input, "abc")).analyze)
+
+    // The three-read replacement is still made for a deterministic input, 
even one not cheap.
+    val substr = $"a".substring(1, 5)
+    comparePlans(
+      Optimize.execute(testRelation.where(substr like "abc%def").analyze),
+      testRelation.where(OctetLength(substr) >= 6 &&
+        (StartsWith(substr, "abc") && EndsWith(substr, "def"))).analyze)
+  }
+
   test("simplify Like into Contains") {
     val originalQuery =
       testRelation


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

Reply via email to