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

andygrove pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/datafusion-comet.git


The following commit(s) were added to refs/heads/main by this push:
     new 92f0f33e75 feat: fall back to Spark for collated predicate operands 
(#4948)
92f0f33e75 is described below

commit 92f0f33e752243c1fe5ef72dd05858d224b7984c
Author: Oleks V <[email protected]>
AuthorDate: Fri Jul 17 14:39:47 2026 -0700

    feat: fall back to Spark for collated predicate operands (#4948)
    
    * feat: fall back to Spark for collated predicate operands
    
    Comet's native equality, ordering, and hashing compare raw UTF-8 bytes,
    so a predicate operand carrying a non-UTF8_BINARY collation (Spark 4+)
    would produce wrong answers on the native path -- for example,
    `'a' = 'A'` under UNICODE_CI returns true in Spark but false byte-wise.
    
    Add a `ComparisonUtils.collationSupportLevel` shared helper and wire it
    into `getSupportLevel` for the six binary comparison serdes plus
    `In`/`InSet`. `CometNot`'s special-case rewrites for
    `Not(EqualTo|EqualNullSafe|In)` now guard on the same helper so they
    fall through to the generic path (and thus to Spark) when any operand
    is collated. The helper walks nested types via
    `hasNonDefaultStringCollation`, so collated strings inside
    array/map/struct operands are caught too.
    
    * feat: dispatch collated str_to_map through JVM codegen
    
    Mix CodegenDispatchFallback into CometStrToMap so that when the
    input string or a delimiter carries a non-UTF8_BINARY collation
    the expression is executed by Spark's own doGenCode inside the
    Comet kernel rather than falling the whole projection back to
    Spark. Update the str_to_map_collation.sql fixture to drop the
    expect_fallback(...) assertion accordingly.
    
    * test: dispatch legacy-truncate str_to_map through codegen
    
    Update str_to_map_legacy_truncate.sql to drop the expect_fallback
    assertions -- with CodegenDispatchFallback on CometStrToMap the
    expression now runs via Spark's own doGenCode inside the Comet
    kernel instead of falling the projection back to Spark.
    
    * feat: fall back to Spark for collated Like/Contains/StartsWith/EndsWith
    
    Extends 903a772b6 to the string-predicate serdes flagged in review.
    CometLike, CometContains, CometStartsWith, and CometEndsWith now route
    their `getSupportLevel` through `ComparisonUtils.collationSupportLevel`
    so any non-UTF8_BINARY operand triggers a clean fallback. Previously
    Contains / StartsWith / EndsWith reached the native scalar-function
    bridge via `CometScalarFunction` with no collation guard, and Like's
    `getSupportLevel` only rejected custom escape characters. Native
    kernels compare raw UTF-8 bytes, so a case- or accent-insensitive
    collation would yield wrong answers (e.g. `'Hello' LIKE '%hello%'`
    returns true in Spark under UNICODE_CI but false byte-wise).
    
    `collationSupportLevel` grows an optional `exprName` so each predicate
    surfaces a fallback reason that names itself in EXPLAIN.
    
    Expands `sql-tests/.../collation.sql` with predicate coverage: the six
    binary comparisons under UTF8_LCASE and UNICODE_CI, `IN`/`NOT IN`
    (scalar and `InSet`-threshold), CometNot's `NOT (=)` / `NOT (<=>)`
    rewrites, `LIKE`, `Contains`, `StartsWith`, `EndsWith`, collated
    strings buried in `array`/`map`/`struct` operands (exercising
    `hasNonDefaultStringCollation`'s recursion), and NULL propagation
    through each of the above.
    
    * test: expect fallback for collated-predicate SQL tests
    
    Every predicate query in `collation.sql` now uses
    `query expect_fallback(non-UTF8_BINARY collated operands)` instead of
    plain `query`. The predicate serdes correctly return `Unsupported` for
    collated operands, so the enclosing Project falls back to Spark --
    which is the whole point of the fix -- but that violates the default
    `CheckCoverageAndAnswer` assertion mode's "only Comet native operators"
    check. `expect_fallback` still verifies the answer against Spark and
    additionally pins the shared fallback-reason substring, so a regressed
    guard shows up as either a wrong answer or a missing fallback.
    
    Also drops the map test: `CometCreateMap` routes through the JVM
    codegen dispatcher, so its interaction with the outer EqualTo's
    collation guard is not the clean path this file is meant to pin down.
    
    * test: drop collation cases Spark folds or rejects at analysis
    
    Two classes of tests never reached the predicate serdes and so could
    not exercise the fix:
    
    - NULL-literal RHS: Spark's `NullPropagation` rewrites `x <=> NULL`
      to `IsNull(x)`, folds `x = NULL` and `NULL IN (...)` to `Literal(null)`,
      etc., so the predicate serde is never asked about a collated NULL
      operand. The row `(7, NULL)` in `test_collated_predicates` still
      exercises NULL propagation through every remaining predicate query.
    
    - UNICODE_CI + LIKE / Contains / StartsWith / EndsWith: Spark 4.0
      restricts these to `StringTypeNonCSAICollation`, which rejects
      UNICODE_CI at analysis time with a DATATYPE_MISMATCH. UTF8_LCASE
      is accepted and already covers the same fallback path.
    
    * test: tighten comments in collation.sql
    
    Drop the "what" narration and shrink each remaining note to a single
    sentence about *why* the case exists (NullPropagation folding,
    NonCSAICollation restriction, InSet threshold, etc.). No SQL changes.
    
    * collation fallback
    
    * feat: route collated predicates through JVM codegen dispatcher
    
    Addresses two review comments on #4948.
    
    CollationAwareBinaryPredicate, CometIn, CometInSet, and CometLike now
    mix in CodegenDispatchFallback, so an `Unsupported` result from a
    collated operand stays in the Comet pipeline via Spark's own
    `doGenCode` rather than falling the whole projection or filter back to
    Spark. CometContains / CometStartsWith / CometEndsWith inherit the
    mixin through the trait. Collation semantics still come from Spark's
    codegen path, so answers stay Spark-correct.
    
    Each of these serdes now overrides `getUnsupportedReasons` so the
    collation case (and, for Like, the custom-escape case) is picked up by
    GenerateDocs and surfaces in the compatibility guide instead of being
    undocumented.
    
    The SQL tests in `collation.sql` switch from
    `expect_fallback(non-UTF8_BINARY collated operands)` back to plain
    `query`: with the dispatcher taking over, the operator stays native
    and the fallback-coverage assertion catches any regression that would
    force it back to Spark.
---
 .../org/apache/comet/serde/QueryPlanSerde.scala    |   6 +-
 .../main/scala/org/apache/comet/serde/maps.scala   |  11 +-
 .../scala/org/apache/comet/serde/predicates.scala  | 104 +++++++++++++----
 .../scala/org/apache/comet/serde/strings.scala     |  36 +++++-
 .../expressions/map/str_to_map_collation.sql       |   9 +-
 .../expressions/map/str_to_map_legacy_truncate.sql |  19 ++--
 .../sql-tests/expressions/string/collation.sql     | 123 +++++++++++++++++++++
 7 files changed, 265 insertions(+), 43 deletions(-)

diff --git a/spark/src/main/scala/org/apache/comet/serde/QueryPlanSerde.scala 
b/spark/src/main/scala/org/apache/comet/serde/QueryPlanSerde.scala
index 5eee0c5cad..284094de35 100644
--- a/spark/src/main/scala/org/apache/comet/serde/QueryPlanSerde.scala
+++ b/spark/src/main/scala/org/apache/comet/serde/QueryPlanSerde.scala
@@ -218,8 +218,8 @@ object QueryPlanSerde extends Logging with CometExprShim 
with CometTypeShim {
       classOf[Chr] -> CometScalarFunction("char"),
       classOf[ConcatWs] -> CometConcatWs,
       classOf[Concat] -> CometConcat,
-      classOf[Contains] -> CometScalarFunction("contains"),
-      classOf[EndsWith] -> CometScalarFunction("ends_with"),
+      classOf[Contains] -> CometContains,
+      classOf[EndsWith] -> CometEndsWith,
       classOf[GetJsonObject] -> CometGetJsonObject,
       classOf[InitCap] -> CometInitCap,
       classOf[Length] -> CometLength,
@@ -233,7 +233,7 @@ object QueryPlanSerde extends Logging with CometExprShim 
with CometTypeShim {
       classOf[RegExpReplace] -> CometRegExpReplace,
       classOf[Reverse] -> CometReverse,
       classOf[RLike] -> CometRLike,
-      classOf[StartsWith] -> CometScalarFunction("starts_with"),
+      classOf[StartsWith] -> CometStartsWith,
       classOf[StringInstr] -> CometScalarFunction("instr"),
       classOf[StringRepeat] -> CometStringRepeat,
       classOf[StringReplace] -> CometStringReplace,
diff --git a/spark/src/main/scala/org/apache/comet/serde/maps.scala 
b/spark/src/main/scala/org/apache/comet/serde/maps.scala
index a5ae24d215..c8304440c3 100644
--- a/spark/src/main/scala/org/apache/comet/serde/maps.scala
+++ b/spark/src/main/scala/org/apache/comet/serde/maps.scala
@@ -166,12 +166,17 @@ object CometMapFromEntries
   }
 }
 
-object CometStrToMap extends CometScalarFunction[StringToMap]("str_to_map") 
with CometTypeShim {
+object CometStrToMap
+    extends CometScalarFunction[StringToMap]("str_to_map")
+    with CometTypeShim
+    with CodegenDispatchFallback {
 
   // Spark 4.1.1+ honours spark.sql.legacy.truncateForEmptyRegexSplit by 
truncating trailing
   // empty entries from the split result. Comet's native str_to_map always 
behaves as if the flag
-  // were false, so it is incompatible when legacy truncation is enabled. Read 
by string key so it
-  // resolves on older Spark versions where the config is not registered.
+  // were false. When the flag is true, mark this Incompatible so the 
CodegenDispatchFallback
+  // trait routes the expression through the JVM codegen dispatcher (Spark's 
own doGenCode inside
+  // the Comet kernel) rather than falling the entire projection back to 
Spark. Read by string
+  // key so it resolves on older Spark versions where the config is not 
registered.
   private val legacyTruncateConfig = 
"spark.sql.legacy.truncateForEmptyRegexSplit"
 
   private val legacyTruncateReason =
diff --git a/spark/src/main/scala/org/apache/comet/serde/predicates.scala 
b/spark/src/main/scala/org/apache/comet/serde/predicates.scala
index 63b64fbcf2..c9a70a830f 100644
--- a/spark/src/main/scala/org/apache/comet/serde/predicates.scala
+++ b/spark/src/main/scala/org/apache/comet/serde/predicates.scala
@@ -21,7 +21,7 @@ package org.apache.comet.serde
 
 import scala.jdk.CollectionConverters._
 
-import org.apache.spark.sql.catalyst.expressions.{And, Attribute, 
EqualNullSafe, EqualTo, Expression, GreaterThan, GreaterThanOrEqual, In, InSet, 
IsNaN, IsNotNull, IsNull, LessThan, LessThanOrEqual, Literal, Not, Or}
+import org.apache.spark.sql.catalyst.expressions.{And, Attribute, 
BinaryExpression, EqualNullSafe, EqualTo, Expression, GreaterThan, 
GreaterThanOrEqual, In, InSet, IsNaN, IsNotNull, IsNull, LessThan, 
LessThanOrEqual, Literal, Not, Or}
 import org.apache.spark.sql.types.BooleanType
 
 import org.apache.comet.CometSparkSessionExtensions.withFallbackReason
@@ -34,26 +34,34 @@ object CometNot extends CometExpressionSerde[Not] {
       inputs: Seq[Attribute],
       binding: Boolean): Option[ExprOuterClass.Expr] = {
 
+    import ComparisonUtils.hasCollatedOperand
+
     expr.child match {
-      case expr: EqualTo =>
+      case inner: EqualTo if !hasCollatedOperand(inner.left, inner.right) =>
         createBinaryExpr(
-          expr,
-          expr.left,
-          expr.right,
+          inner,
+          inner.left,
+          inner.right,
           inputs,
           binding,
           (builder, binaryExpr) => builder.setNeq(binaryExpr))
-      case expr: EqualNullSafe =>
+      case inner: EqualNullSafe if !hasCollatedOperand(inner.left, 
inner.right) =>
         createBinaryExpr(
-          expr,
-          expr.left,
-          expr.right,
+          inner,
+          inner.left,
+          inner.right,
           inputs,
           binding,
           (builder, binaryExpr) => builder.setNeqNullSafe(binaryExpr))
-      case expr: In =>
-        ComparisonUtils.in(expr, expr.value, expr.list, inputs, binding, 
negate = true)
+      case inner: In if !hasCollatedOperand((inner.value +: inner.list): _*) =>
+        ComparisonUtils.in(inner, inner.value, inner.list, inputs, binding, 
negate = true)
       case _ =>
+        // Includes the collated variants of EqualTo / EqualNullSafe / In 
above: fall through so
+        // the child expression's own serde is consulted, which now returns 
`Unsupported` for
+        // non-UTF8_BINARY operands (see `CometEqualTo.getSupportLevel` and 
siblings). That makes
+        // `exprToProtoInternal` return None for the child, which cascades 
this Not to None and
+        // falls the enclosing operator back to Spark — the only way to honour 
collation-aware
+        // (in)equality without a native path.
         createUnaryExpr(
           expr,
           expr.child,
@@ -103,7 +111,7 @@ object CometOr extends CometExpressionSerde[Or] {
   }
 }
 
-object CometEqualTo extends CometExpressionSerde[EqualTo] {
+object CometEqualTo extends CollationAwareBinaryPredicate[EqualTo] {
   override def convert(
       expr: EqualTo,
       inputs: Seq[Attribute],
@@ -118,7 +126,7 @@ object CometEqualTo extends CometExpressionSerde[EqualTo] {
   }
 }
 
-object CometEqualNullSafe extends CometExpressionSerde[EqualNullSafe] {
+object CometEqualNullSafe extends CollationAwareBinaryPredicate[EqualNullSafe] 
{
   override def convert(
       expr: EqualNullSafe,
       inputs: Seq[Attribute],
@@ -133,7 +141,7 @@ object CometEqualNullSafe extends 
CometExpressionSerde[EqualNullSafe] {
   }
 }
 
-object CometGreaterThan extends CometExpressionSerde[GreaterThan] {
+object CometGreaterThan extends CollationAwareBinaryPredicate[GreaterThan] {
   override def convert(
       expr: GreaterThan,
       inputs: Seq[Attribute],
@@ -148,7 +156,7 @@ object CometGreaterThan extends 
CometExpressionSerde[GreaterThan] {
   }
 }
 
-object CometGreaterThanOrEqual extends 
CometExpressionSerde[GreaterThanOrEqual] {
+object CometGreaterThanOrEqual extends 
CollationAwareBinaryPredicate[GreaterThanOrEqual] {
   override def convert(
       expr: GreaterThanOrEqual,
       inputs: Seq[Attribute],
@@ -163,7 +171,7 @@ object CometGreaterThanOrEqual extends 
CometExpressionSerde[GreaterThanOrEqual]
   }
 }
 
-object CometLessThan extends CometExpressionSerde[LessThan] {
+object CometLessThan extends CollationAwareBinaryPredicate[LessThan] {
   override def convert(
       expr: LessThan,
       inputs: Seq[Attribute],
@@ -178,7 +186,7 @@ object CometLessThan extends CometExpressionSerde[LessThan] 
{
   }
 }
 
-object CometLessThanOrEqual extends CometExpressionSerde[LessThanOrEqual] {
+object CometLessThanOrEqual extends 
CollationAwareBinaryPredicate[LessThanOrEqual] {
   override def convert(
       expr: LessThanOrEqual,
       inputs: Seq[Attribute],
@@ -233,7 +241,14 @@ object CometIsNaN extends CometExpressionSerde[IsNaN] {
   }
 }
 
-object CometIn extends CometExpressionSerde[In] {
+object CometIn extends CometExpressionSerde[In] with CodegenDispatchFallback {
+
+  override def getSupportLevel(expr: In): SupportLevel =
+    ComparisonUtils.collationSupportLevel("In", (expr.value +: expr.list): _*)
+
+  override def getUnsupportedReasons(): Seq[String] =
+    Seq(ComparisonUtils.nonDefaultCollationDocReason)
+
   override def convert(
       expr: In,
       inputs: Seq[Attribute],
@@ -242,7 +257,14 @@ object CometIn extends CometExpressionSerde[In] {
   }
 }
 
-object CometInSet extends CometExpressionSerde[InSet] {
+object CometInSet extends CometExpressionSerde[InSet] with 
CodegenDispatchFallback {
+
+  override def getSupportLevel(expr: InSet): SupportLevel =
+    ComparisonUtils.collationSupportLevel("InSet", expr.child)
+
+  override def getUnsupportedReasons(): Seq[String] =
+    Seq(ComparisonUtils.nonDefaultCollationDocReason)
+
   override def convert(
       expr: InSet,
       inputs: Seq[Attribute],
@@ -257,8 +279,52 @@ object CometInSet extends CometExpressionSerde[InSet] {
   }
 }
 
+/**
+ * Mixin for serdes of binary predicates whose native kernel compares raw 
bytes: any operand
+ * carrying a non-UTF8_BINARY collation is unsupported natively. Mixing in
+ * `CodegenDispatchFallback` routes these through the JVM codegen dispatcher 
(Spark's own
+ * `doGenCode`), so the collation-aware evaluation runs inline in the Comet 
pipeline instead of
+ * falling the whole operator back to Spark.
+ */
+trait CollationAwareBinaryPredicate[T <: BinaryExpression]
+    extends CometExpressionSerde[T]
+    with CodegenDispatchFallback {
+  override def getSupportLevel(expr: T): SupportLevel =
+    ComparisonUtils.collationSupportLevel(expr.prettyName, expr.left, 
expr.right)
+
+  override def getUnsupportedReasons(): Seq[String] =
+    Seq(ComparisonUtils.nonDefaultCollationDocReason)
+}
+
 object ComparisonUtils {
 
+  // Comet's native equality/ordering/hashing compare raw bytes, so any 
predicate operand carrying
+  // a non-UTF8_BINARY collation (Spark 4+) would produce wrong answers on the 
native path — e.g.
+  // `'a' = 'A'` under `UNICODE_CI` returns true in Spark but false byte-wise. 
Every binary
+  // comparison and `In`/`InSet` serde routes its `getSupportLevel` through 
here, marking the case
+  // `Unsupported`; `CodegenDispatchFallback` then routes those cases through 
Spark's own
+  // `doGenCode` inside the Comet pipeline. `hasNonDefaultStringCollation` 
walks nested types too,
+  // so collated strings inside array/map/struct operands are also caught.
+  def nonDefaultCollationReason(exprName: String): String =
+    s"$exprName does not support non-UTF8_BINARY collated operands; " +
+      "native comparison is byte-wise and cannot honour collation semantics."
+
+  // Doc-friendly variant of `nonDefaultCollationReason` for 
`getUnsupportedReasons`. The compat
+  // guide already scopes the reason to a specific expression, so no per-expr 
name is needed.
+  val nonDefaultCollationDocReason: String =
+    "Non-UTF8_BINARY collated operands are routed through the JVM codegen 
dispatcher " +
+      "(Spark's own `doGenCode`) because native comparison is byte-wise."
+
+  def hasCollatedOperand(operands: Expression*): Boolean =
+    operands.exists(op => hasNonDefaultStringCollation(op.dataType))
+
+  def collationSupportLevel(exprName: String, operands: Expression*): 
SupportLevel =
+    if (hasCollatedOperand(operands: _*)) {
+      Unsupported(Some(nonDefaultCollationReason(exprName)))
+    } else {
+      Compatible()
+    }
+
   def in(
       expr: Expression,
       value: Expression,
diff --git a/spark/src/main/scala/org/apache/comet/serde/strings.scala 
b/spark/src/main/scala/org/apache/comet/serde/strings.scala
index 07b3c0c99f..1e124dbd42 100644
--- a/spark/src/main/scala/org/apache/comet/serde/strings.scala
+++ b/spark/src/main/scala/org/apache/comet/serde/strings.scala
@@ -19,7 +19,7 @@
 
 package org.apache.comet.serde
 
-import org.apache.spark.sql.catalyst.expressions.{Attribute, Base64, 
BitLength, Cast, Concat, ConcatWs, Elt, Empty2Null, Expression, FindInSet, 
FormatNumber, FormatString, GetJsonObject, InitCap, Left, Length, Levenshtein, 
Like, Literal, Lower, Mask, OctetLength, Overlay, RegExpExtract, 
RegExpExtractAll, RegExpInStr, RegExpReplace, Right, RLike, SoundEx, 
StringLocate, StringLPad, StringRepeat, StringReplace, StringRPad, StringSplit, 
StringTranslate, Substring, SubstringIndex, ToCharacter [...]
+import org.apache.spark.sql.catalyst.expressions.{Attribute, Base64, 
BitLength, Cast, Concat, ConcatWs, Contains, Elt, Empty2Null, EndsWith, 
Expression, FindInSet, FormatNumber, FormatString, GetJsonObject, InitCap, 
Left, Length, Levenshtein, Like, Literal, Lower, Mask, OctetLength, Overlay, 
RegExpExtract, RegExpExtractAll, RegExpInStr, RegExpReplace, Right, RLike, 
SoundEx, StartsWith, StringLocate, StringLPad, StringRepeat, StringReplace, 
StringRPad, StringSplit, StringTranslate, Substr [...]
 import org.apache.spark.sql.types.{BinaryType, DataTypes, LongType, StringType}
 
 import org.apache.comet.CometConf
@@ -288,13 +288,21 @@ object CometConcatWs extends 
CometExpressionSerde[ConcatWs] {
   }
 }
 
-object CometLike extends CometExpressionSerde[Like] {
+object CometLike extends CometExpressionSerde[Like] with 
CodegenDispatchFallback {
+
+  private val customEscapeReason =
+    "LIKE with a custom escape character (only `\\` is supported natively)"
+
+  override def getUnsupportedReasons(): Seq[String] =
+    Seq(customEscapeReason, ComparisonUtils.nonDefaultCollationDocReason)
 
   override def getSupportLevel(expr: Like): SupportLevel = {
-    if (expr.escapeChar == '\\') {
-      Compatible()
-    } else {
+    if (ComparisonUtils.hasCollatedOperand(expr.left, expr.right)) {
+      Unsupported(Some(ComparisonUtils.nonDefaultCollationReason("Like")))
+    } else if (expr.escapeChar != '\\') {
       Unsupported(Some(s"custom escape character ${expr.escapeChar} not 
supported in LIKE"))
+    } else {
+      Compatible()
     }
   }
 
@@ -309,6 +317,24 @@ object CometLike extends CometExpressionSerde[Like] {
   }
 }
 
+/**
+ * Serdes for `Contains` / `StartsWith` / `EndsWith` that reject 
non-UTF8_BINARY collated operands
+ * and otherwise delegate to the generic `contains` / `starts_with` / 
`ends_with` scalar-function
+ * bridge. The native kernels compare raw bytes and cannot honour case- or 
accent-insensitive
+ * collations, so a collated operand must fall back to Spark.
+ */
+object CometContains
+    extends CometScalarFunction[Contains]("contains")
+    with CollationAwareBinaryPredicate[Contains]
+
+object CometStartsWith
+    extends CometScalarFunction[StartsWith]("starts_with")
+    with CollationAwareBinaryPredicate[StartsWith]
+
+object CometEndsWith
+    extends CometScalarFunction[EndsWith]("ends_with")
+    with CollationAwareBinaryPredicate[EndsWith]
+
 /**
  * `rlike` runs Spark's own implementation through the codegen dispatcher by 
default, for
  * byte-exact results. The native (rust) regexp engine is faster but has 
different semantics from
diff --git 
a/spark/src/test/resources/sql-tests/expressions/map/str_to_map_collation.sql 
b/spark/src/test/resources/sql-tests/expressions/map/str_to_map_collation.sql
index 7816a1442c..41594f612c 100644
--- 
a/spark/src/test/resources/sql-tests/expressions/map/str_to_map_collation.sql
+++ 
b/spark/src/test/resources/sql-tests/expressions/map/str_to_map_collation.sql
@@ -17,17 +17,16 @@
 
 -- MinSparkVersion: 4.0
 
--- Spark 4.0+ applies collation-aware delimiter matching in str_to_map. 
Comet's native
--- implementation matches raw delimiter bytes, so non-UTF8_BINARY collations 
must fall back.
+-- Spark 4.0+ applies collation-aware delimiter matching in str_to_map.
 
 -- collated input string
-query expect_fallback(`str_to_map` does not support non-UTF8_BINARY collations)
+query
 SELECT str_to_map('a:1,b:2' COLLATE UTF8_LCASE, ',', ':')
 
 -- collated pair delimiter
-query expect_fallback(`str_to_map` does not support non-UTF8_BINARY collations)
+query
 SELECT str_to_map('aX1,bX2', 'x' COLLATE UTF8_LCASE, ':')
 
 -- collated key-value delimiter
-query expect_fallback(`str_to_map` does not support non-UTF8_BINARY collations)
+query
 SELECT str_to_map('aX1,bX2', ',', 'x' COLLATE UTF8_LCASE)
diff --git 
a/spark/src/test/resources/sql-tests/expressions/map/str_to_map_legacy_truncate.sql
 
b/spark/src/test/resources/sql-tests/expressions/map/str_to_map_legacy_truncate.sql
index a5b5eb9f16..8ab8058920 100644
--- 
a/spark/src/test/resources/sql-tests/expressions/map/str_to_map_legacy_truncate.sql
+++ 
b/spark/src/test/resources/sql-tests/expressions/map/str_to_map_legacy_truncate.sql
@@ -15,19 +15,22 @@
 -- specific language governing permissions and limitations
 -- under the License.
 
--- Tests that str_to_map falls back to Spark when
--- spark.sql.legacy.truncateForEmptyRegexSplit is enabled. In legacy mode 
Spark truncates trailing
--- empty entries from the split result, which Comet's native str_to_map does 
not honour.
+-- Tests that str_to_map routes through the JVM codegen dispatcher when
+-- spark.sql.legacy.truncateForEmptyRegexSplit is enabled. In legacy mode 
Spark truncates
+-- trailing empty entries from the split result, which Comet's native 
str_to_map does not
+-- honour. `CometStrToMap` marks the expression Incompatible when the flag is 
on and mixes in
+-- [[CodegenDispatchFallback]], so the projection stays native (Spark's own 
`doGenCode` runs
+-- inside the Comet kernel) while producing Spark-exact results.
 -- See https://github.com/apache/datafusion-comet/issues/4477
 
 -- Config: spark.sql.legacy.truncateForEmptyRegexSplit=true
 
--- trailing pair delimiter: legacy mode truncates the trailing empty entry, so 
Comet must fall
--- back to Spark
-query expect_fallback(truncateForEmptyRegexSplit)
+-- trailing pair delimiter: legacy mode truncates the trailing empty entry; 
Comet delegates to
+-- the codegen dispatcher.
+query
 SELECT str_to_map('a:1,b:2,', ',', ':')
 
--- column input also falls back
+-- column input is also handled via the codegen dispatcher
 statement
 CREATE TABLE test_str_to_map_legacy(s STRING, pair_delim STRING, 
key_value_delim STRING) USING parquet
 
@@ -37,5 +40,5 @@ INSERT INTO test_str_to_map_legacy VALUES
   ('x:1;y:2;', ';', ':'),
   (NULL, ',', ':')
 
-query expect_fallback(truncateForEmptyRegexSplit)
+query
 SELECT str_to_map(s, pair_delim, key_value_delim) FROM test_str_to_map_legacy
diff --git 
a/spark/src/test/resources/sql-tests/expressions/string/collation.sql 
b/spark/src/test/resources/sql-tests/expressions/string/collation.sql
index 89dd95b9e7..534725b83f 100644
--- a/spark/src/test/resources/sql-tests/expressions/string/collation.sql
+++ b/spark/src/test/resources/sql-tests/expressions/string/collation.sql
@@ -51,3 +51,126 @@ SELECT concat('Hello' COLLATE UNICODE_CI, 'World' COLLATE 
UNICODE_CI)
 
 query
 SELECT reverse('Hello' COLLATE UNICODE_CI)
+
+-- ============================================================================
+-- Collated predicate operands route through the JVM codegen dispatcher.
+-- The predicate serdes mark collated cases `Unsupported`; 
`CodegenDispatchFallback` runs
+-- Spark's own `doGenCode` inside the Comet pipeline so the operator stays 
native while
+-- matching Spark's collation-aware answer.
+-- ============================================================================
+
+statement
+CREATE TABLE test_collated_predicates(id INT, s STRING) USING parquet
+
+statement
+INSERT INTO test_collated_predicates VALUES
+  (1, 'a'),
+  (2, 'A'),
+  (3, 'hello'),
+  (4, 'HELLO'),
+  (5, 'World'),
+  (6, 'b'),
+  (7, NULL)
+
+-- ---------- EqualTo / EqualNullSafe / inequality ---------------------------
+
+query
+SELECT id, CAST(s AS STRING COLLATE UTF8_LCASE) = 'A' FROM 
test_collated_predicates ORDER BY id
+
+query
+SELECT id FROM test_collated_predicates WHERE CAST(s AS STRING COLLATE 
UTF8_LCASE) = 'A' ORDER BY id
+
+-- UNICODE_CI covers a second ICU collation to prove the guard is not 
UTF8_LCASE-only.
+query
+SELECT id, CAST(s AS STRING COLLATE UNICODE_CI) = 'A' FROM 
test_collated_predicates ORDER BY id
+
+query
+SELECT id, CAST(s AS STRING COLLATE UTF8_LCASE) != 'A' FROM 
test_collated_predicates ORDER BY id
+
+query
+SELECT id, CAST(s AS STRING COLLATE UTF8_LCASE) <> 'A' FROM 
test_collated_predicates ORDER BY id
+
+-- `x <=> NULL` folds via NullPropagation, so we test only a non-NULL RHS.
+query
+SELECT id, CAST(s AS STRING COLLATE UTF8_LCASE) <=> 'A' FROM 
test_collated_predicates ORDER BY id
+
+-- ---------- Ordering ------------------------------------------------------
+
+query
+SELECT id, CAST(s AS STRING COLLATE UTF8_LCASE) < 'b' FROM 
test_collated_predicates ORDER BY id
+
+query
+SELECT id, CAST(s AS STRING COLLATE UTF8_LCASE) <= 'a' FROM 
test_collated_predicates ORDER BY id
+
+query
+SELECT id, CAST(s AS STRING COLLATE UTF8_LCASE) > 'a' FROM 
test_collated_predicates ORDER BY id
+
+query
+SELECT id, CAST(s AS STRING COLLATE UTF8_LCASE) >= 'A' FROM 
test_collated_predicates ORDER BY id
+
+query
+SELECT id, CAST(s AS STRING COLLATE UNICODE_CI) < 'b' FROM 
test_collated_predicates ORDER BY id
+
+-- ---------- In / NotIn / InSet ---------------------------------------------
+
+query
+SELECT id FROM test_collated_predicates WHERE CAST(s AS STRING COLLATE 
UTF8_LCASE) IN ('A', 'HELLO') ORDER BY id
+
+-- NOT IN exercises CometNot's collated fall-through into the generic path.
+query
+SELECT id FROM test_collated_predicates WHERE CAST(s AS STRING COLLATE 
UTF8_LCASE) NOT IN ('A', 'HELLO') ORDER BY id
+
+-- 12 elements forces InSet (spark.sql.optimizer.inSetConversionThreshold=10 
by default).
+query
+SELECT id FROM test_collated_predicates WHERE CAST(s AS STRING COLLATE 
UTF8_LCASE) IN ('A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'HELLO') 
ORDER BY id
+
+-- ---------- NOT (EqualTo / EqualNullSafe) rewrites ------------------------
+
+query
+SELECT id FROM test_collated_predicates WHERE NOT (CAST(s AS STRING COLLATE 
UTF8_LCASE) = 'A') ORDER BY id
+
+query
+SELECT id FROM test_collated_predicates WHERE NOT (CAST(s AS STRING COLLATE 
UTF8_LCASE) <=> 'A') ORDER BY id
+
+-- ---------- Like -----------------------------------------------------------
+
+query
+SELECT id, CAST(s AS STRING COLLATE UTF8_LCASE) LIKE '%LLO' FROM 
test_collated_predicates ORDER BY id
+
+query
+SELECT id FROM test_collated_predicates WHERE CAST(s AS STRING COLLATE 
UTF8_LCASE) LIKE 'H%' ORDER BY id
+
+-- Spark 4.0 rejects UNICODE_CI for LIKE / Contains / StartsWith / EndsWith at 
analysis time
+-- (StringTypeNonCSAICollation), so only UTF8_LCASE is covered for these.
+
+-- ---------- Contains / StartsWith / EndsWith -------------------------------
+
+query
+SELECT id, contains(CAST(s AS STRING COLLATE UTF8_LCASE), 'HE') FROM 
test_collated_predicates ORDER BY id
+
+query
+SELECT id, startswith(CAST(s AS STRING COLLATE UTF8_LCASE), 'HE') FROM 
test_collated_predicates ORDER BY id
+
+query
+SELECT id, endswith(CAST(s AS STRING COLLATE UTF8_LCASE), 'LO') FROM 
test_collated_predicates ORDER BY id
+
+query
+SELECT id FROM test_collated_predicates WHERE contains(CAST(s AS STRING 
COLLATE UTF8_LCASE), 'ELL') ORDER BY id
+
+query
+SELECT id FROM test_collated_predicates WHERE startswith(CAST(s AS STRING 
COLLATE UTF8_LCASE), 'HE') ORDER BY id
+
+query
+SELECT id FROM test_collated_predicates WHERE endswith(CAST(s AS STRING 
COLLATE UTF8_LCASE), 'O') ORDER BY id
+
+-- ---------- Nested-collated types ------------------------------------------
+
+-- hasNonDefaultStringCollation recurses into array/struct element types.
+query
+SELECT id, array(CAST(s AS STRING COLLATE UTF8_LCASE)) = array('A' COLLATE 
UTF8_LCASE) FROM test_collated_predicates ORDER BY id
+
+query
+SELECT id, struct(CAST(s AS STRING COLLATE UTF8_LCASE)) = struct('A' COLLATE 
UTF8_LCASE) FROM test_collated_predicates ORDER BY id
+
+-- NULL-literal operands are omitted: NullPropagation folds them before Comet 
sees them. The
+-- (7, NULL) row covers NULL-in-data on every query above.


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

Reply via email to