spark-test commented on code in PR #60060:
URL: https://github.com/apache/spark/pull/60060#discussion_r4488772195


##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/CollationTypeCoercion.scala:
##########
@@ -399,6 +401,11 @@ object CollationTypeCoercion extends SQLConfHelper {
 
       Some(addContextToStringType(cast.dataType, castStrength))
 
+    // An explicitly collated target type (even UTF8_BINARY), not the path, 
decides the
+    // collation, like a column of that type.
+    case v: VariantGet if 
DataTypeUtils.hasNonDefaultStringCharOrVarcharType(v.dataType) =>

Review Comment:
   Agreed that other computed collated join keys already have this gap on the 
base branch. These keys did not: there, both variant_get keys were cast to 
STRING before the join, so the join and the bloom filter compared the same 
bytes. With this change the keys stay collated, so an injected runtime bloom 
filter can drop exactly the case-variant matches this PR is meant to return, 
and the result then depends on table statistics. That leaves the fix incomplete 
for the joins it targets. Skipping keys that are not binary-stable on the 
selective-predicate path, as the materialized-cache path already does, would 
close it with a small change.
   
   <!-- SPARK_DEV_REVIEW_REPLY 
{"feedback_id":"inline:4471925682","thread_id":"inline:4471925682","verdict_sha256":"59061fb453faa1efe1d363432f1e0c8210bcaa166a1a94e30ae4fd2465106af8"}
 -->



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/CollationTypeCoercion.scala:
##########
@@ -390,15 +392,23 @@ object CollationTypeCoercion extends SQLConfHelper {
     case collate: Collate =>
       Some(addContextToStringType(collate.dataType, Explicit))
 
+    // A cast from a string, or from VARIANT to an explicitly collated type 
(even UTF8_BINARY),
+    // keeps that collation like a column of that type; `v:a::STRING COLLATE 
X` is such a cast.
     case cast: Cast =>
-      val castStrength = if (hasStringType(cast.child.dataType)) {
+      val castStrength = if (hasStringType(cast.child.dataType) ||

Review Comment:
   **Blocking (P1):** Giving a cast from VARIANT with an explicit collation 
Implicit strength makes joins valid that the base branch rejected, and those 
joins can now lose rows silently. Take `a JOIN b ON a.v:k::STRING COLLATE 
UTF8_LCASE = b.v:k::STRING`. On the base branch both casts had Default strength 
in different collations, so analysis failed with 
`INDETERMINATE_COLLATION_IN_EXPRESSION`. Now the explicit cast wins, the other 
key is retargeted to UTF8_LCASE, and the join matches case-insensitively on 
expression keys that `RewriteCollationJoin` does not wrap in `CollationKey`.
   
   When the default-on runtime bloom filter applies (an application side that 
scans at least 10GB, a selectively filtered creation side under 10MB, and a 
shuffle join), the selective-predicate path of `InjectRuntimeFilter` builds 
`BloomFilterAggregate(XxHash64(key))`, which hashes the raw bytes of these 
non-binary keys. A row of `a` with `'Open'` whose only match in `b` is `'open'` 
fails `BloomFilterMightContain` and is removed before the join, with no error; 
an outer join pruned on its null-supplying side null-extends the row instead. 
The same holds when the other key is any `variant_get` call or `CAST(b.id AS 
STRING)`. For collated `variant_get` keys the base branch already returned 
case-sensitive matches, but these queries never ran before, so the change turns 
an explicit error into a result whose completeness depends on table statistics.
   
   **Recommended change:** Make InjectRuntimeFilter skip a join key whose type 
is not binary-stable on the selective-predicate bloom filter path, as it 
already does for materialized caches, and add runtime-filter join tests whose 
keys differ only by case: collated variant_get keys, and casts from VARIANT to 
an explicit collation joined to plain casts or variant_get calls.
   
   **Why this works:** A key that is not binary-stable can compare equal under 
its collation while hashing differently. If such keys are refused before a 
creation side is chosen, no bloom filter can reject a row that the join would 
match, and the join's collation-aware comparison alone decides the result.
   
   **Scope:** Tighten runtime bloom filter key eligibility in the optimizer.
   
   **Compatibility:** Runtime filter injection for binary-stable join keys is 
unchanged, including the materialized-cache handling.
   
   **Risks:** Runtime bloom filters are no longer injected for joins on 
non-binary collated keys, including computed collated keys that receive unsound 
filters on the base branch, so those joins can read and shuffle more rows. This 
is a plan-only change and can be rolled back.
   
   **Constraints:** Joins on binary-stable keys keep their current runtime 
filter behavior, including collated attribute pairs that RewriteCollationJoin 
wraps in CollationKey.
   
   **Exclusions:**
   - Collation-aware hashing for runtime bloom filters, which would keep 
pruning for collated keys, is left for a follow-up.
   
   **Success:** No runtime bloom filter is injected for an equi-join key whose 
type is not binary-stable. Joins on collated variant_get keys return every 
case-variant match whether or not runtime bloom filters are enabled. Joins that 
this change newly accepts in a non-binary collation, such as a cast from 
VARIANT with an explicit collation joined to a plain cast from VARIANT, return 
every match whether or not runtime bloom filters are enabled.
   
   **Verification:**
   - **Regression:** Force runtime bloom filters on, with a small 
application-side threshold, a selective creation side and broadcast disabled. 
Then inner, left semi (including one built from an IN subquery), left outer and 
right outer joins on two collated variant_get keys whose values differ only by 
case each return the case-insensitive matches. No outer join null-extends a row 
that has such a match, and the optimized plan has no bloom filter on those keys.
   - **Regression:** With the same settings, an inner join and a left semi join 
on a.v:k::STRING COLLATE UTF8_LCASE = b.v:k::STRING, and an inner join on 
a.v:k::STRING COLLATE UTF8_LCASE = variant_get(b.v, '$.k', 'STRING'), which 
fail at analysis on the base branch, return every case-insensitive match, and 
the optimized plan has no bloom filter on those keys.
   - **Compatibility:** Joins on binary-stable keys still receive runtime bloom 
filters as on the base branch, including collated attribute pairs that are 
rewritten to CollationKey.



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/CollationTypeCoercion.scala:
##########
@@ -390,15 +392,23 @@ object CollationTypeCoercion extends SQLConfHelper {
     case collate: Collate =>
       Some(addContextToStringType(collate.dataType, Explicit))
 
+    // A cast from a string, or from VARIANT to an explicitly collated type 
(even UTF8_BINARY),
+    // keeps that collation like a column of that type; `v:a::STRING COLLATE 
X` is such a cast.
     case cast: Cast =>
-      val castStrength = if (hasStringType(cast.child.dataType)) {
+      val castStrength = if (hasStringType(cast.child.dataType) ||
+          (cast.child.dataType.isInstanceOf[VariantType] &&
+            
DataTypeUtils.hasNonDefaultStringCharOrVarcharType(cast.dataType))) {

Review Comment:
   **Blocking (P1):** `hasNonDefaultStringCharOrVarcharType` treats a target as 
explicitly collated whenever it is not the `StringType` companion object, but 
by the time this arm runs the analyzer may already have replaced a plain 
`STRING` target with a new instance. Under an object default collation, 
`ApplyDefaultCollation` (and `DefaultCollationTypeCoercion` in the single-pass 
analyzer) replaces the companion target of every user cast with 
`StringType(<default collation>)`, so a plain cast from VARIANT now reads as 
explicit and gets Implicit strength.
   
   For example, with `t (id INT, p STRING, v VARIANT)`:
   
   ```sql
   CREATE VIEW vw DEFAULT COLLATION UTF8_LCASE AS
   SELECT id FROM t WHERE v:s::STRING = p;
   ```
   
   On the base branch the cast keeps Default strength, `p` wins, and the view 
works. Now the cast is Implicit UTF8_LCASE, it conflicts with the Implicit 
UTF8_BINARY of `p`, and the statement fails with 
`INDETERMINATE_COLLATION_IN_EXPRESSION`. The same happens in CTAS and RTAS, 
temporary views and `ALTER VIEW AS`, and whenever a stored view or SQL function 
with a default collation is analyzed again, so existing objects of this shape 
start failing after an upgrade. `variant_get(v, '$.s', 'STRING') = p` in the 
same view still works, because its target is never rewritten. Under `DEFAULT 
COLLATION UTF8_BINARY` the fixed-point analyzer discards the value-equal 
rewrite while the single-pass analyzer keeps the new instance, so the two 
analyzers disagree on the same query. Coercion's own retargeting 
(`cast.copy(dataType = ...)`) also produces non-companion types, so in a nested 
merge a plain cast can flip to Implicit in a later pass. The new view test 
compares `plain_cast` only with a recolor
 ed literal, where Default and Implicit strength give the same answer, so it 
does not catch this.
   
   This extends existing discussions 
[1](https://github.com/apache/spark/pull/60060#issuecomment-6664340600), 
[2](https://github.com/apache/spark/pull/60060#pullrequestreview-5779474253).
   
   See **Shared repair plan 1** in the review body.



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/CollationTypeCoercion.scala:
##########
@@ -390,15 +392,23 @@ object CollationTypeCoercion extends SQLConfHelper {
     case collate: Collate =>
       Some(addContextToStringType(collate.dataType, Explicit))
 
+    // A cast from a string, or from VARIANT to an explicitly collated type 
(even UTF8_BINARY),
+    // keeps that collation like a column of that type; `v:a::STRING COLLATE 
X` is such a cast.
     case cast: Cast =>
-      val castStrength = if (hasStringType(cast.child.dataType)) {
+      val castStrength = if (hasStringType(cast.child.dataType) ||
+          (cast.child.dataType.isInstanceOf[VariantType] &&

Review Comment:
   **Non-blocking (P2):** This checks only for a top-level `VariantType` 
source, while `hasStringType` just above checks the source recursively. A cast 
whose source nests VARIANT inside an ARRAY or STRUCT, with no string in it, 
therefore keeps Default strength even when its target is explicitly collated. 
For example, `CAST(variant_get(v, '$.a', 'ARRAY<VARIANT>') AS ARRAY<STRING 
COLLATE UTF8_LCASE>)[0] = 'open'` still fails with 
`INDETERMINATE_COLLATION_IN_EXPRESSION`, and against a plain `STRING` column 
the column's collation silently wins. That is the failure this PR fixes for 
`v:a::ARRAY<STRING COLLATE UTF8_LCASE>`. The same applies to an 
`ARRAY<VARIANT>` or `STRUCT<x: VARIANT>` column cast to a collated type. A 
`MAP<STRING, VARIANT>` source is already Implicit through its string key, so 
whether the declared collation applies now depends on how the extraction is 
spelled.
   
   **Verification:**
   - **Regression:** Under both analyzers, CAST(variant_get(v, '$.a', 
'ARRAY<VARIANT>') AS ARRAY<STRING COLLATE UTF8_LCASE>) matches 
case-insensitively when compared with a literal through element access or 
array_contains, and fails with INDETERMINATE_COLLATION_IN_EXPRESSION when 
compared with a plain STRING column, as the direct VARIANT cast does. A 
STRUCT<x: VARIANT> source cast to STRUCT<x: STRING COLLATE UTF8_LCASE> behaves 
the same.
   - **Compatibility:** Container casts of VARIANT values to plain STRING 
targets keep Default strength, and casts whose source contains a string keep 
Implicit strength.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


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

Reply via email to