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

cloud-fan 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 85ad6f3d797a [SPARK-57727][SQL] Fix incorrect query result due to 
inferAdditionalConstraints incorrectly substituting attributes with 
non-binary-stable collations
85ad6f3d797a is described below

commit 85ad6f3d797af0c7c3a58105f8d66a11e46c4315
Author: Eric Yang <[email protected]>
AuthorDate: Fri Jul 3 21:01:52 2026 +0800

    [SPARK-57727][SQL] Fix incorrect query result due to 
inferAdditionalConstraints incorrectly substituting attributes with 
non-binary-stable collations
    
    ### What changes were proposed in this pull request?
    Make attribute substitution in 
`QueryPlanConstraints.inferAdditionalConstraints` collation-aware. For a 
binary-stable `source`, substitution is unchanged (any position). For a 
non-binary-stable (e.g. collated) `source`, substitute only where it is a 
direct operand of a same-collation comparison, so it is never moved into a 
context that changes the effective collation.
    There is another draft PR https://github.com/apache/spark/pull/56836 fixes 
the issue in a different way in which the change is more scoped/safer but can 
be too conservative.
    
    This mirrors the existing guard in another rule `ConstantPropagation` 
(SPARK-55647).
    
    ### Why are the changes needed?
    `inferAdditionalConstraints` infers a predicate on `b` from `a = b` and a 
predicate on `a` by substituting `a` with `b`. This is only valid when `a` and 
`b` are byte-for-byte interchangeable. Under a non-binary-stable collation, `a 
= b` is a collation equality (e.g. `'hello' = 'HELLO'` under `UTF8_LCASE`), not 
byte equality, so substituting into a comparison evaluated in a different 
collation produces a wrong constraint and silently drops rows:
    ```sql
    CREATE TABLE t (a STRING COLLATE UTF8_LCASE, b STRING COLLATE UTF8_LCASE);
    INSERT INTO t VALUES ('hello', 'HELLO');
    SELECT a, b FROM t WHERE a = b AND a = 'hello' COLLATE UTF8_BINARY;
    ```
    
    ### Does this PR introduce _any_ user-facing change?
    Yes, fixed incorrect query result.
    
    ### How was this patch tested?
    Added UT.
    
    ### Was this patch authored or co-authored using generative AI tooling?
    Yes. Claude Code
    
    Closes #56860 from jiwen624/SPARK-57727-perf.
    
    Authored-by: Eric Yang <[email protected]>
    Signed-off-by: Wenchen Fan <[email protected]>
    (cherry picked from commit bb9c363978a21f4294aa70d7e70cdaee50c66fa9)
    Signed-off-by: Wenchen Fan <[email protected]>
---
 .../plans/logical/QueryPlanConstraints.scala       | 21 ++++++++++++---
 .../spark/sql/collation/CollationSuite.scala       | 30 ++++++++++++++++++++++
 2 files changed, 48 insertions(+), 3 deletions(-)

diff --git 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/logical/QueryPlanConstraints.scala
 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/logical/QueryPlanConstraints.scala
index 6a1aa54a54d0..b023d7320808 100644
--- 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/logical/QueryPlanConstraints.scala
+++ 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/logical/QueryPlanConstraints.scala
@@ -129,9 +129,24 @@ trait ConstraintHelper {
   private def replaceConstraints(
       constraints: ExpressionSet,
       source: Expression,
-      destination: Expression): ExpressionSet = constraints.map(_ transform {
-    case e: Expression if e.semanticEquals(source) => destination
-  })
+      destination: Expression): ExpressionSet = {
+    if (isBinaryStable(source.dataType)) {
+      constraints.map(_ transform {
+        case e: Expression if e.semanticEquals(source) => destination
+      })
+    } else {
+      constraints.map(_ transform {
+        case b: BinaryComparison if sameCollationOperand(b, source) =>
+          b.withNewChildren(b.children.map { c =>
+            if (c.semanticEquals(source)) destination else c
+          })
+      })
+    }
+  }
+
+  private def sameCollationOperand(b: BinaryComparison, source: Expression): 
Boolean =
+    (b.left.semanticEquals(source) || b.right.semanticEquals(source)) &&
+      b.left.dataType == source.dataType && b.right.dataType == source.dataType
 
   /**
    * Infers a set of `isNotNull` constraints from null intolerant expressions 
as well as
diff --git 
a/sql/core/src/test/scala/org/apache/spark/sql/collation/CollationSuite.scala 
b/sql/core/src/test/scala/org/apache/spark/sql/collation/CollationSuite.scala
index 711e1e091a98..045748ad7cb1 100644
--- 
a/sql/core/src/test/scala/org/apache/spark/sql/collation/CollationSuite.scala
+++ 
b/sql/core/src/test/scala/org/apache/spark/sql/collation/CollationSuite.scala
@@ -26,6 +26,7 @@ import org.apache.spark.sql.{AnalysisException, QueryTest, 
Row}
 import org.apache.spark.sql.catalyst.ExtendedAnalysisException
 import org.apache.spark.sql.catalyst.analysis.ResolvedIdentifier
 import org.apache.spark.sql.catalyst.expressions._
+import org.apache.spark.sql.catalyst.plans.logical.Filter
 import org.apache.spark.sql.catalyst.util.CollationFactory
 import org.apache.spark.sql.connector.{DatasourceV2SQLBase, 
FakeV2ProviderWithCustomSchema}
 import org.apache.spark.sql.connector.catalog.{CatalogV2Util, Identifier, 
InMemoryTable}
@@ -2440,6 +2441,35 @@ class CollationSuite extends DatasourceV2SQLBase with 
AdaptiveSparkPlanHelper {
     }
   }
 
+  test("SPARK-57727: constraint inference does not substitute 
non-binary-stable attributes") {
+    withTable("t1") {
+      sql("CREATE TABLE t1 (a STRING COLLATE UTF8_LCASE, b STRING COLLATE 
UTF8_LCASE)")
+      sql("INSERT INTO t1 VALUES ('hello', 'HELLO')")
+
+      checkAnswer(
+        sql("SELECT a, b FROM t1 WHERE a = b AND a = 'hello' COLLATE 
UTF8_BINARY"),
+        Row("hello", "HELLO")
+      )
+    }
+  }
+
+  test("SPARK-57727: same-collation constraint inference is preserved") {
+    withTable("t1") {
+      sql("CREATE TABLE t1 (a STRING COLLATE UTF8_LCASE, b STRING COLLATE 
UTF8_LCASE)")
+
+      val optimized =
+        sql("SELECT a, b FROM t1 WHERE a = b AND a = 
'hello'").queryExecution.optimizedPlan
+      val inferredOnB = optimized.exists {
+        case Filter(cond, _) => cond.exists {
+          case e: EqualTo => e.references.exists(_.name == "b") && 
e.toString.contains("hello")
+          case _ => false
+        }
+        case _ => false
+      }
+      assert(inferredOnB, s"expected inferred 'b = hello' filter to be 
preserved:\n$optimized")
+    }
+  }
+
   test("ConstantPropagation: replaces binary-stable attributes with 
contradicting predicates") {
     withTable("t1") {
       sql("CREATE TABLE t1 (c STRING)")


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

Reply via email to