You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@spark.apache.org by we...@apache.org on 2026/07/03 13:02:23 UTC
(spark) branch branch-4.x updated: [SPARK-57727][SQL] Fix incorrect query result due to inferAdditionalConstraints incorrectly substituting attributes with non-binary-stable collations
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 <ji...@gmail.com>
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 <ji...@gmail.com>
Signed-off-by: Wenchen Fan <we...@databricks.com>
(cherry picked from commit bb9c363978a21f4294aa70d7e70cdaee50c66fa9)
Signed-off-by: Wenchen Fan <we...@databricks.com>
---
.../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: commits-unsubscribe@spark.apache.org
For additional commands, e-mail: commits-help@spark.apache.org