wombatu-kun commented on code in PR #19474:
URL: https://github.com/apache/hudi/pull/19474#discussion_r4214341194


##########
hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/spark/sql/hudi/DataSkippingUtils.scala:
##########
@@ -390,6 +391,24 @@ object DataSkippingUtils extends Logging {
     }
   }
 
+  /**
+   * Wraps a translated index-lookup bound in a null-tolerant guard when the 
source expression is
+   * an actual transformation of the column (not a bare attribute reference).
+   *
+   * Whitelisted transformations may be partial functions of the source column 
(for example
+   * GetTimestamp parsing a string): re-applied on top of a min/max stat value 
that the function
+   * is undefined for, they return null, in which case f(max(col)) != 
max(f(col)) and the bound
+   * cannot be decided. A null bound must keep the file (hence Coalesce to 
true) rather than
+   * silently prune it. A false bound may still prune: each one-sided bound is 
independently
+   * valid whenever its own stat value is defined (for example "null AND 
false" evaluates to
+   * false, which is still sound).
+   */
+  private def wrapTransformedBoundNullSafe(sourceExpr: Expression, bound: 
Expression): Expression =
+    sourceExpr match {
+      case _: AttributeReference => bound
+      case _ => Coalesce(Seq(bound, TrueLiteral))

Review Comment:
   `Coalesce(bound, true)` also fires when the stat itself is null, so for a 
column that is all-null in a file `lower(col) IN ('a', 'b')` now keeps the file 
while `col IN ('a', 'b')` still prunes it. Not a blocker, but could the 
fallback be `IsNotNull(genColMinValueExpr(colName))` instead of `true`, so only 
a defined stat whose transformation yields null keeps the file?



##########
hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/functional/TestBucketIndexSupport.scala:
##########
@@ -31,13 +31,12 @@ import org.apache.hudi.index.bucket.BucketIdentifier
 import org.apache.hudi.index.bucket.partition.NumBucketsFunction
 import org.apache.hudi.keygen.{ComplexKeyGenerator, NonpartitionedKeyGenerator}
 import org.apache.hudi.keygen.constant.KeyGeneratorOptions
-import org.apache.hudi.testutils.HoodieSparkClientTestBase
+import org.apache.hudi.testutils.{HoodieDummyExpressionHolder, 
HoodieSparkClientTestBase}
 import org.apache.hudi.timeline.service.TimelineService
 
 import org.apache.avro.generic.GenericData
 import org.apache.spark.sql.{BucketPartitionUtils, 
HoodieCatalystExpressionUtils, SparkSession}
-import org.apache.spark.sql.catalyst.expressions.{Attribute, Expression, 
PredicateHelper}
-import org.apache.spark.sql.catalyst.plans.logical.LeafNode
+import org.apache.spark.sql.catalyst.expressions.PredicateHelper

Review Comment:
   This import drops `Expression`, but current master's `optimizeResolvedExpr` 
in this class returns `Expression`, so the textually clean merge with master no 
longer compiles. Could you rebase and keep `{Expression, PredicateHelper}` here?



##########
hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/spark/sql/BaseHoodieCatalystExpressionUtils.scala:
##########
@@ -70,6 +89,21 @@ abstract class BaseHoodieCatalystExpressionUtils extends 
HoodieCatalystExpressio
         case FromUTCTimestamp(OrderPreservingTransformation(attrRef), _) => 
Some(attrRef)
         case ToUnixTimestamp(OrderPreservingTransformation(attrRef), _, _, _) 
=> Some(attrRef)
         case ToUTCTimestamp(OrderPreservingTransformation(attrRef), _) => 
Some(attrRef)
+        // Post-ReplaceExpressions shape of to_date/to_timestamp with an 
explicit format: since
+        // SPARK-38240 the optimizer rewrites those RuntimeReplaceable nodes 
into
+        // GetTimestamp(source, fmt) (Cast-wrapped to date for to_date) before 
filters reach
+        // file pruning. Matched only when
+        //   (a) the format is a literal, fixed-width, year-first pattern: the 
translation
+        //       re-applies the parse on top of string min/max column stats, 
which is only
+        //       sound when lexicographic ordering of parseable strings agrees 
with
+        //       chronological ordering, and
+        //   (b) failOnError is false (non-ANSI), so probing a stat value that 
does not parse
+        //       can never throw from the index lookup; it yields null 
instead, which the
+        //       null-tolerant bounds in DataSkippingUtils turn into "keep the 
file".
+        // GetTimestamp's constructor arity differs across Spark versions but 
its left()/right()
+        // accessors are stable, hence the typed match
+        case gt: GetTimestamp if !gt.failOnError && 
isOrderPreservingDateFormat(gt.right) =>

Review Comment:
   Under `spark.sql.legacy.timeParserPolicy=LEGACY` this parse accepts 
variable-width input, so the `file_slop` fixture queried with `to_date(B, 
'yyyy-MM-dd') = '2022-11-05'` gets pruned even though its `'2022-11-5'` row 
matches, and under the Spark 3.x default `EXCEPTION` that stat throws 
`SparkUpgradeException` with `failOnError` off, uncaught on the partition-stats 
path this arm now reaches. Should the arm also require the policy to be 
`CORRECTED`, like the ANSI gate?



-- 
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]

Reply via email to