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

voonhous pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hudi.git


The following commit(s) were added to refs/heads/master by this push:
     new 91ffb05e62cf fix(spark): stop treating lower/upper as order-preserving 
in data skipping (#20228)
91ffb05e62cf is described below

commit 91ffb05e62cf9a2f58bd10a326d07e02f69ab081
Author: Vova Kolmakov <[email protected]>
AuthorDate: Mon Oct 19 15:47:15 2026 +0700

    fix(spark): stop treating lower/upper as order-preserving in data skipping 
(#20228)
    
    * fix(spark): stop treating lower/upper as order-preserving in data skipping
    
    * addressed review comments: cover NOT startswith over date_format and trim 
the lower() case
    
    ---------
    
    Co-authored-by: Vova Kolmakov <[email protected]>
---
 .../sql/BaseHoodieCatalystExpressionUtils.scala    |  6 +-
 .../org/apache/hudi/TestDataSkippingUtils.scala    | 66 +++++++++++++++-------
 .../hudi/TestHoodieCatalystExpressionUtils.scala   |  7 ++-
 3 files changed, 53 insertions(+), 26 deletions(-)

diff --git 
a/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/spark/sql/BaseHoodieCatalystExpressionUtils.scala
 
b/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/spark/sql/BaseHoodieCatalystExpressionUtils.scala
index e2a39d7c29a5..4ea289bf496c 100644
--- 
a/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/spark/sql/BaseHoodieCatalystExpressionUtils.scala
+++ 
b/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/spark/sql/BaseHoodieCatalystExpressionUtils.scala
@@ -18,7 +18,7 @@
 package org.apache.spark.sql
 
 import org.apache.spark.sql.HoodieSparkTypeUtils.isCastPreservingOrdering
-import org.apache.spark.sql.catalyst.expressions.{Add, Attribute, 
AttributeReference, AttributeSet, BitwiseOr, Cast, DateAdd, DateDiff, 
DateFormatClass, DateSub, Divide, Exp, Expm1, Expression, FromUnixTime, 
FromUTCTimestamp, Log, Log10, Log1p, Log2, Lower, Multiply, PredicateHelper, 
ShiftLeft, ShiftRight, ToUnixTimestamp, ToUTCTimestamp, Upper}
+import org.apache.spark.sql.catalyst.expressions.{Add, Attribute, 
AttributeReference, AttributeSet, BitwiseOr, Cast, DateAdd, DateDiff, 
DateFormatClass, DateSub, Divide, Exp, Expm1, Expression, FromUnixTime, 
FromUTCTimestamp, Log, Log10, Log1p, Log2, Multiply, PredicateHelper, 
ShiftLeft, ShiftRight, ToUnixTimestamp, ToUTCTimestamp}
 import org.apache.spark.sql.execution.datasources.DataSourceStrategy
 import org.apache.spark.sql.types.DataType
 
@@ -73,8 +73,8 @@ abstract class BaseHoodieCatalystExpressionUtils extends 
HoodieCatalystExpressio
         case ToUTCTimestamp(OrderPreservingTransformation(attrRef), _) => 
Some(attrRef)
 
         // String Expressions
-        case Lower(OrderPreservingTransformation(attrRef)) => Some(attrRef)
-        case Upper(OrderPreservingTransformation(attrRef)) => Some(attrRef)
+        // NOTE: Lower/Upper are not matched: case folding does not preserve 
the binary ordering
+        //       of strings (for ex, "Zebra" < "apple", but lower("Zebra") = 
"zebra" > "apple")
         // Left API change: Improve RuntimeReplaceable
         // https://issues.apache.org/jira/browse/SPARK-38240
         case 
org.apache.spark.sql.catalyst.expressions.Left(OrderPreservingTransformation(attrRef),
 _) => Some(attrRef)
diff --git 
a/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/TestDataSkippingUtils.scala
 
b/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/TestDataSkippingUtils.scala
index 51e5aae98be6..694fb49f6df4 100644
--- 
a/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/TestDataSkippingUtils.scala
+++ 
b/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/TestDataSkippingUtils.scala
@@ -29,7 +29,7 @@ import org.apache.spark.sql.catalyst.expressions.{Expression, 
InSet, Not}
 import org.apache.spark.sql.catalyst.optimizer.OptimizeIn
 import org.apache.spark.sql.catalyst.plans.logical.LogicalPlan
 import org.apache.spark.sql.catalyst.rules.Rule
-import org.apache.spark.sql.functions.{col, lower}
+import org.apache.spark.sql.functions.{col, date_format, lower, upper}
 import org.apache.spark.sql.hudi.DataSkippingUtils
 import org.apache.spark.sql.internal.SQLConf.SESSION_LOCAL_TIMEZONE
 import org.apache.spark.sql.types._
@@ -189,15 +189,29 @@ object TestDataSkippingUtils {
         ),
         Seq("file_1", "file_2", "file_3")),
       arguments(
-        // Composite expression
+        // NOTE: file_1 may hold "B" (sorting between "ABC" and "abc"), whose 
lower-case form "b" doesn't
+        //       start w/ "abc", even though lower(B_minValue) = 
lower(B_maxValue) = "abc" does
         
Not(sparkAdapter.getExpressionFromColumn(lower(col("B")).startsWith("abc"))),
         Seq(
-          IndexRow("file_1", valueCount = 1, B_minValue = "ABA", B_maxValue = 
"ADF", B_nullCount = 1), // may contain strings starting w/ "ABC" (after upper)
-          IndexRow("file_2", valueCount = 1, B_minValue = "ADF", B_maxValue = 
"AZY", B_nullCount = 0),
-          IndexRow("file_3", valueCount = 1, B_minValue = "AAA", B_maxValue = 
"ABA", B_nullCount = 0),
-          IndexRow("file_4", valueCount = 1, B_minValue = "ABC123", B_maxValue 
= "ABC345", B_nullCount = 0) // all strings start w/ "ABC" (after upper)
+          IndexRow("file_1", valueCount = 3, B_minValue = "ABC", B_maxValue = 
"abc", B_nullCount = 0)
         ),
-        Seq("file_1", "file_2", "file_3"))
+        Seq("file_1")),
+      arguments(
+        // NOTE: file_1 holds "Zebra", whose lower-case form is the queried 
"zebra", even though
+        //       lower(B_maxValue) = "apple" sorts before it
+        sparkAdapter.getExpressionFromColumn(lower(col("B")) === "zebra"),
+        Seq(
+          IndexRow("file_1", valueCount = 2, B_minValue = "Zebra", B_maxValue 
= "apple", B_nullCount = 0)
+        ),
+        Seq("file_1")),
+      arguments(
+        // NOTE: file_1 holds "apple", whose upper-case form is the queried 
"APPLE", even though
+        //       upper(B_minValue) = "ZEBRA" sorts after it
+        sparkAdapter.getExpressionFromColumn(upper(col("B")) === "APPLE"),
+        Seq(
+          IndexRow("file_1", valueCount = 2, B_minValue = "Zebra", B_maxValue 
= "apple", B_nullCount = 0)
+        ),
+        Seq("file_1"))
     )
   }
 
@@ -263,19 +277,31 @@ object TestDataSkippingUtils {
   }
 
   def testMiscLookupFilterExpressionsSource(): 
java.util.stream.Stream[Arguments] = {
-    // NOTE: Have to use [[Arrays.stream]], as Scala can't resolve properly 2 
overloads for [[Stream.of]]
-    //       (for single element)
-    java.util.Arrays.stream(
-      Array(
-        arguments(
-          InSet(UnresolvedAttribute("A"), HashSet(0, 1)),
-          Seq(
-            IndexRow("file_1", valueCount = 1, 1, 2, 0),
-            IndexRow("file_2", valueCount = 1, -1, 1, 0),
-            IndexRow("file_3", valueCount = 1, -2, -1, 0)
-          ),
-          Seq("file_1", "file_2"))
-      )
+    java.util.stream.Stream.of(
+      arguments(
+        InSet(UnresolvedAttribute("A"), HashSet(0, 1)),
+        Seq(
+          IndexRow("file_1", valueCount = 1, 1, 2, 0),
+          IndexRow("file_2", valueCount = 1, -1, 1, 0),
+          IndexRow("file_3", valueCount = 1, -2, -1, 0)
+        ),
+        Seq("file_1", "file_2")),
+      arguments(
+        // Composite expression
+        // NOTE: date_format(C, 'yyyy-MM-dd') preserves the ordering of C, so 
this filter is translated and
+        //       file_1, whose dates all fall in 03/2022, is pruned
+        Not(sparkAdapter.getExpressionFromColumn(date_format(col("C"), 
"yyyy-MM-dd").startsWith("2022-03"))),
+        Seq(
+          IndexRow("file_1", valueCount = 2,
+            C_minValue = new Timestamp(1646625048000L), // 03/07/2022
+            C_maxValue = new Timestamp(1646797848000L), // 03/09/2022
+            C_nullCount = 0),
+          IndexRow("file_2", valueCount = 2,
+            C_minValue = new Timestamp(1646711448000L), // 03/08/2022
+            C_maxValue = new Timestamp(1648785048000L), // 04/01/2022
+            C_nullCount = 0)
+        ),
+        Seq("file_2"))
     )
   }
 
diff --git 
a/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/TestHoodieCatalystExpressionUtils.scala
 
b/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/TestHoodieCatalystExpressionUtils.scala
index a7bd52dcefd7..52f133bb6f15 100644
--- 
a/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/TestHoodieCatalystExpressionUtils.scala
+++ 
b/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/TestHoodieCatalystExpressionUtils.scala
@@ -57,11 +57,9 @@ class TestHoodieCatalystExpressionUtils extends 
SparkAdapterSupport {
   }
 
   @Test
-  def testUnaryMathAndStringTransformationsPreserveOrdering(): Unit = {
+  def testUnaryMathTransformationsPreserveOrdering(): Unit = {
     assertEquals(Some(dblAttr), matched(Exp(dblAttr)))
     assertEquals(Some(dblAttr), matched(Log(dblAttr)))
-    assertEquals(Some(strAttr), matched(Upper(strAttr)))
-    assertEquals(Some(strAttr), matched(Lower(strAttr)))
   }
 
   @Test
@@ -107,5 +105,8 @@ class TestHoodieCatalystExpressionUtils extends 
SparkAdapterSupport {
     assertEquals(None, matched(Add(Literal(1), Literal(2))))
     // Sqrt is not one of the whitelisted order-preserving transformations.
     assertEquals(None, matched(Sqrt(dblAttr)))
+    // Case folding breaks the binary ordering of strings: "Zebra" < "apple", 
but lower("Zebra") > "apple".
+    assertEquals(None, matched(Lower(strAttr)))
+    assertEquals(None, matched(Upper(strAttr)))
   }
 }

Reply via email to