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


##########
hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/spark/sql/hudi/dataskipping/rules/ConstantOperandRule.scala:
##########
@@ -17,23 +17,33 @@
 
 package org.apache.spark.sql.hudi.dataskipping.rules
 
-import org.apache.spark.sql.catalyst.expressions.{Add, BitwiseOr, DateAdd, 
DateDiff, DateSub, Divide, Expression, Literal, Multiply, ShiftLeft, ShiftRight}
-import org.apache.spark.sql.hudi.dataskipping.{Increasing, RangeRule, Step}
-import org.apache.spark.sql.types.Decimal
+import org.apache.spark.sql.catalyst.expressions.{Add, And, BitwiseOr, 
DateAdd, DateDiff, DateSub, Divide, Expression, GreaterThanOrEqual, 
LessThanOrEqual, Literal, Multiply, ShiftLeft, ShiftRight}
+import org.apache.spark.sql.hudi.dataskipping.{Increasing, RangeRule, Step, 
TermRange}
+import org.apache.spark.sql.types.{ByteType, DataType, Decimal, DoubleType, 
FloatType, IntegerType, LongType, ShortType}
+
+import scala.util.Try
 
 /**
  * Binary operators with the column on one side and a constant on the other:
  * `Add`, `Multiply`, `Divide`, `DateAdd`, `DateSub`, `DateDiff`, `ShiftLeft`, 
`ShiftRight`, `BitwiseOr`.
  *
  * Shared guard: the other operand is constant. Today that is enforced per 
function:
- *  - `Add`: the other side is foldable (either side)
- *  - `Multiply`: the other side is a positive numeric literal (either side)
+ *  - `Add`: float / double result and a finite constant, or integral result 
and a constant (either side)
+ *  - `Multiply`: float / double or integral result, and the other side is a 
positive numeric literal
+ *    (either side)
  *  - `Divide`: the divisor is a positive numeric literal (column as dividend 
only)
- *  - `DateAdd`, `DateSub`, `DateDiff` (end only), `ShiftLeft`, `ShiftRight`, 
`BitwiseOr`: ANY second
- *    operand is accepted today, constant or not
+ *  - `ShiftLeft`: the shift amount is constant
+ *  - `DateAdd`, `DateSub`, `DateDiff` (end only), `ShiftRight`, `BitwiseOr`: 
ANY second operand is
+ *    accepted today, constant or not
  *
  * NOTE: A column-referencing other operand could reverse the ordering (for 
ex, A + A * -2 = -A), and a
  *       non-deterministic one is re-evaluated against the min/max bounds (for 
ex, A + rand())
+ *
+ * NOTE: Integral `Add`, `Multiply` and `ShiftLeft` wrap around on overflow in 
non-ANSI mode, so the
+ *       max can map below the min (for ex, Long.MaxValue * 2 = -2), and throw 
in ANSI mode. Their

Review Comment:
   `ShiftLeft` has no eval mode, so it wraps around on overflow with ANSI on as 
well, and only `Add` and `Multiply` throw. Could this NOTE, the `IntegralType` 
scaladoc, the ANSI comment in `testLookupThroughFullOptimizer` and the 
description say so?



##########
hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/spark/sql/hudi/dataskipping/rules/ConstantOperandRule.scala:
##########
@@ -63,14 +89,112 @@ object ConstantOperandRule extends RangeRule {
         Step(term, left, Increasing, fileGuard = None, partial = false),
         Step(term, right, Increasing, fileGuard = None, partial = false))
 
-    case ShiftLeft(value, _) => Seq(Step(term, value, Increasing, fileGuard = 
None, partial = false))
+    // NOTE: Spark only shifts int and bigint values, by an int amount
+    case ShiftLeft(value, amount) =>
+      IntegralType.of(term).filter(t => t.dataType == IntegerType || 
t.dataType == LongType).toSeq.flatMap { t =>
+        integralConstant(amount).map(n => guardedStep(term, value, 
t.shiftLeftFits(n)))
+      }
     case ShiftRight(value, _) => Seq(Step(term, value, Increasing, fileGuard = 
None, partial = false))
 
     case _ => Nil
   }
 
   private def when(guard: Boolean)(step: => Step): Option[Step] = if (guard) 
Some(step) else None
 
+  private def guardedStep(term: Expression, child: Expression, fileGuard: 
TermRange => Expression): Step =
+    Step(term, child, Increasing, fileGuard = Some(fileGuard), partial = false)
+
+  // NOTE: Expression indexes skip the resolution check, and some unresolved 
terms throw on `dataType`
+  private def hasFloatingPointResult(term: Expression): Boolean = 
Try(term.dataType).toOption.exists {
+    case FloatType | DoubleType => true
+    case _ => false
+  }
+
+  // An infinite addend maps the opposite infinity to NaN, which sorts above 
every number (for ex,
+  // -Infinity + Infinity = NaN > 1 + Infinity). Foldable operands evaluate 
without input, which is
+  // how the optimizer folds them into literals
+  private def isFiniteConstant(expr: Expression): Boolean =
+    expr.foldable && Try(expr.eval()).toOption.exists {
+      case f: Float => !f.isInfinite && !f.isNaN
+      case d: Double => !d.isInfinite && !d.isNaN
+      case _ => false
+    }
+
+  // NOTE: Foldable operands evaluate without input, which is how the 
optimizer folds them into literals
+  private def integralConstant(expr: Expression): Option[Long] =
+    if (expr.foldable) Try(expr.eval()).toOption.collect(integralValue) else 
None
+
+  // NOTE: Literal only, as `isPositiveNumericLiteral`
+  private def positiveIntegralLiteral(expr: Expression): Option[Long] = expr 
match {
+    case Literal(value, _) => integralValue.lift(value).filter(_ > 0)
+    case _ => None
+  }
+
+  private val integralValue: PartialFunction[Any, Long] = {
+    case b: Byte => b.toLong
+    case s: Short => s.toLong
+    case i: Int => i.toLong
+    case l: Long => l
+  }
+
+  /**
+   * An integral result type and its limits. Past them, integral arithmetic 
wraps around in non-ANSI
+   * mode and throws in ANSI mode. Each guard holds when no value of the 
file's range goes past them:
+   * the step is increasing, so checking the min and the max covers every 
value in between. A file
+   * that may overflow gets a NULL bound and is kept, and a file that holds 
the guard never evaluates
+   * an overflowing bound, so ANSI does not throw.
+   */
+  private case class IntegralType(dataType: DataType, min: Long, max: Long, 
bits: Int) {

Review Comment:
   `IntegralType` is also the name of Spark's 
`org.apache.spark.sql.types.IntegralType`, the parent of exactly these four 
types, while this class holds a type's limits rather than being one. Could it 
be named after what it holds, for ex `IntegralLimits`?



##########
hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/spark/sql/hudi/dataskipping/rules/ConstantOperandRule.scala:
##########
@@ -17,23 +17,33 @@
 
 package org.apache.spark.sql.hudi.dataskipping.rules
 
-import org.apache.spark.sql.catalyst.expressions.{Add, BitwiseOr, DateAdd, 
DateDiff, DateSub, Divide, Expression, Literal, Multiply, ShiftLeft, ShiftRight}
-import org.apache.spark.sql.hudi.dataskipping.{Increasing, RangeRule, Step}
-import org.apache.spark.sql.types.Decimal
+import org.apache.spark.sql.catalyst.expressions.{Add, And, BitwiseOr, 
DateAdd, DateDiff, DateSub, Divide, Expression, GreaterThanOrEqual, 
LessThanOrEqual, Literal, Multiply, ShiftLeft, ShiftRight}
+import org.apache.spark.sql.hudi.dataskipping.{Increasing, RangeRule, Step, 
TermRange}
+import org.apache.spark.sql.types.{ByteType, DataType, Decimal, DoubleType, 
FloatType, IntegerType, LongType, ShortType}
+
+import scala.util.Try
 
 /**
  * Binary operators with the column on one side and a constant on the other:
  * `Add`, `Multiply`, `Divide`, `DateAdd`, `DateSub`, `DateDiff`, `ShiftLeft`, 
`ShiftRight`, `BitwiseOr`.
  *
  * Shared guard: the other operand is constant. Today that is enforced per 
function:
- *  - `Add`: the other side is foldable (either side)
- *  - `Multiply`: the other side is a positive numeric literal (either side)
+ *  - `Add`: float / double result and a finite constant, or integral result 
and a constant (either side)
+ *  - `Multiply`: float / double or integral result, and the other side is a 
positive numeric literal
+ *    (either side)
  *  - `Divide`: the divisor is a positive numeric literal (column as dividend 
only)
- *  - `DateAdd`, `DateSub`, `DateDiff` (end only), `ShiftLeft`, `ShiftRight`, 
`BitwiseOr`: ANY second
- *    operand is accepted today, constant or not
+ *  - `ShiftLeft`: the shift amount is constant
+ *  - `DateAdd`, `DateSub`, `DateDiff` (end only), `ShiftRight`, `BitwiseOr`: 
ANY second operand is
+ *    accepted today, constant or not
  *
  * NOTE: A column-referencing other operand could reverse the ordering (for 
ex, A + A * -2 = -A), and a
  *       non-deterministic one is re-evaluated against the min/max bounds (for 
ex, A + rand())
+ *
+ * NOTE: Integral `Add`, `Multiply` and `ShiftLeft` wrap around on overflow in 
non-ANSI mode, so the
+ *       max can map below the min (for ex, Long.MaxValue * 2 = -2), and throw 
in ANSI mode. Their
+ *       steps carry a file guard that the file's range does not overflow (see 
[[IntegralType]]).
+ *       Float / double results round monotonically and saturate to infinity 
instead. Decimal results
+ *       overflow into NULL and are not matched (#20460)

Review Comment:
   Decimal overflow only ever yields NULL, or an error under ANSI, which a 
`partial = true` step already turns into a kept file, so could decimal `Add` / 
`Multiply` keep pruning that way rather than being rejected outright? `Divide` 
needs the same here or in a follow-up, since a decimal quotient overflows into 
NULL too (for ex, a `decimal(38, 0)` column divided by 2 has the result type 
`decimal(38, 6)`).



##########
hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/TestDataSkippingUtils.scala:
##########
@@ -1168,4 +1169,79 @@ object TestDataSkippingUtils {
         Seq("file_1"))
     )
   }
+
+  def testIntegralOverflowSource(): java.util.stream.Stream[Arguments] = {
+    java.util.stream.Stream.of(
+      // Issue #20460 repros: integral arithmetic wraps around on overflow in 
non-ANSI mode, so
+      // re-applying it over min/max stats maps the max below the min and 
would wrongly prune
+      // file_1, whose first row matches (51 * 2 = 102, 0 + 1 = 1, 1 << 1 = 
2). The file may
+      // overflow, so its bounds are unknown and it is kept
+      arguments(
+        "A * 2L > 100",
+        Seq(
+          IndexRow("file_1", valueCount = 2, A_minValue = 51, A_maxValue = 
Long.MaxValue, A_nullCount = 0)
+        ),
+        Seq("file_1")),
+      arguments(
+        "A + 1 > 0",
+        Seq(
+          IndexRow("file_1", valueCount = 2, A_minValue = 0, A_maxValue = 
Long.MaxValue, A_nullCount = 0)
+        ),
+        Seq("file_1")),
+      arguments(
+        "shiftleft(A, 1) > 0",
+        Seq(
+          IndexRow("file_1", valueCount = 2, A_minValue = 1, A_maxValue = 
Long.MaxValue, A_nullCount = 0)
+        ),
+        Seq("file_1")),
+      // Integral arithmetic still prunes files that cannot overflow. At the 
limit (file_1) the
+      // bounds are exact and prune; one past it (file_2) a row wraps around 
in non-ANSI mode and
+      // matches (for ex, Long.MaxValue + 1 = Long.MinValue < 0), so the file 
is kept
+      arguments(
+        "A + 1 < 0",
+        Seq(
+          IndexRow("file_1", valueCount = 2, A_minValue = Long.MaxValue - 10, 
A_maxValue = Long.MaxValue - 1, A_nullCount = 0),
+          IndexRow("file_2", valueCount = 2, A_minValue = Long.MaxValue - 10, 
A_maxValue = Long.MaxValue, A_nullCount = 0)
+        ),
+        Seq("file_2")),
+      arguments(
+        "A + -1 > 0",
+        Seq(
+          IndexRow("file_1", valueCount = 2, A_minValue = Long.MinValue + 1, 
A_maxValue = Long.MinValue + 10, A_nullCount = 0),
+          IndexRow("file_2", valueCount = 2, A_minValue = Long.MinValue, 
A_maxValue = Long.MinValue + 10, A_nullCount = 0)
+        ),
+        Seq("file_2")),
+      // Long.MinValue / 3 does not divide evenly, so the min limit rounds 
towards zero
+      arguments(
+        "A * 3L < 0",
+        Seq(
+          IndexRow("file_1", valueCount = 2, A_minValue = 1, A_maxValue = 
Long.MaxValue / 3, A_nullCount = 0),
+          IndexRow("file_2", valueCount = 2, A_minValue = 1, A_maxValue = 
Long.MaxValue / 3 + 1, A_nullCount = 0)
+        ),
+        Seq("file_2")),
+      arguments(
+        "A * 3L > 0",
+        Seq(
+          IndexRow("file_1", valueCount = 2, A_minValue = Long.MinValue / 3, 
A_maxValue = -1, A_nullCount = 0),
+          IndexRow("file_2", valueCount = 2, A_minValue = Long.MinValue / 3 - 
1, A_maxValue = -1, A_nullCount = 0)
+        ),
+        Seq("file_2")),
+      // A bigint shifts by the low 6 bits of the amount, so 65 shifts by 1

Review Comment:
   65 shifts by 1 under any mask of 1 to 6 bits, so this pair still passes with 
a wrong `bits` width, and no test reaches the tinyint / smallint limits either. 
Could the pair use an amount such as 33, plus an int amount between 32 and 63 
and a smallint term in the golden inputs?



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