viirya commented on code in PR #7017:
URL: https://github.com/apache/datafusion-comet/pull/7017#discussion_r4288426323


##########
docs/source/user-guide/latest/compatibility/operators.md:
##########
@@ -74,6 +74,24 @@ key whose type cannot hold a null element or field, such as 
`array(coalesce(x, 0
 `spark.comet.expression.SortOrder.allowIncompatible=true` to run the other 
null orders natively
 anyway.
 
+## Hash Aggregation
+
+Decimal `avg` with input precision 25 or more falls back to Spark, except over 
`DECIMAL(38, 38)`.

Review Comment:
   The rescale overflow comes from the `avg` expression, so it applies to sort 
aggregates and windows as well as hash aggregates. The window section below 
says so, but this paragraph sits under "Hash Aggregation", and the `avg`, 
`mean` and `try_avg` rows in `expressions.md` link here. Could the paragraph 
say explicitly that it applies to every operator that runs `avg`, or move to a 
spot that isn't tied to one operator? If the i256 fix from #5420 comes in here 
instead, this paragraph and the three `expressions.md` notes can go away.



##########
spark/src/main/scala/org/apache/spark/sql/comet/CometWindowExec.scala:
##########
@@ -193,6 +193,19 @@ object CometWindowExec extends 
CometOperatorSerde[WindowExec] {
     }
   }
 
+  /**
+   * Whether a decimal AVG keeps its running sum at DecimalType.MAX_PRECISION, 
which Spark does
+   * for input precision 28 and up. Spark's window buffer keeps that sum 
unbounded, while the
+   * native AVG skips the row that takes it out of the precision, so 
DECIMAL(38,38) inputs 0.6,
+   * 0.6, -0.6 average to 0 over the whole partition rather than Spark's 0.2, 
and to 0.6 rather
+   * than 0.2 in a running frame, where ANSI mode raises instead.
+   * https://github.com/apache/datafusion-comet/issues/5418
+   */
+  private def hasMaxPrecisionDecimalSum(avg: Average): Boolean = 
avg.sumDataType match {

Review Comment:
   This is the same check as `CometBaseAggregate.hasMaxPrecisionDecimalAvg` in 
`operators.scala`. The name also reads as if it were about SUM, even though it 
takes an `Average`. Could both call sites share one helper, for example 
`AggSerde.avgHasMaxPrecisionDecimalSum(avg: Average)`? Then the aggregate and 
window guards can't drift apart when #5420 changes them.



##########
spark/src/main/scala/org/apache/comet/serde/aggregates.scala:
##########
@@ -235,14 +235,42 @@ object CometAverage extends 
CometAggregateExpressionSerde[Average] {
     // deferred (overflow nulls count differently) and stays unsafe for mixed 
execution.
     !fn.child.dataType.isInstanceOf[DecimalType]
 
+  private val rescaledSumOverflowReason =
+    "a large sum of a DECIMAL with precision 25 or more overflows when the 
native average " +
+      "rescales it, which returns NULL " +
+      "(https://github.com/apache/datafusion-comet/issues/7007)"
+
   override def getUnsupportedReasons(): Seq[String] = Seq(
     "YearMonthIntervalType and DayTimeIntervalType inputs are not supported")
 
+  override def getIncompatibleReasons(): Seq[String] = Seq(
+    "For a `DECIMAL` input with precision 25 or more, other than `DECIMAL(38, 
38)`, the native " +
+      "average rescales the sum within 38 digits before dividing it by the 
count, so a large " +
+      "sum returns `NULL` where Spark returns the average. Under ANSI, an 
average that Spark " +
+      "rejects with `NUMERIC_VALUE_OUT_OF_RANGE` also returns `NULL` " +
+      "([#7007](https://github.com/apache/datafusion-comet/issues/7007)).")
+
+  /**
+   * Whether the native average can overflow when it rescales the sum to the 
result scale. It
+   * multiplies the sum, which holds up to `sumDataType.precision` digits, by 
10 to the power of
+   * the scale difference in an `i128`, which holds 38 digits, before dividing 
by the count, while
+   * Spark divides with `BigDecimal`. That covers input precision 25 and up, 
except scale 38,
+   * which needs no rescale.
+   */
+  private def rescaledSumCanOverflow(expr: Average): Boolean =
+    (expr.sumDataType, expr.dataType) match {
+      case (sum: DecimalType, result: DecimalType) =>
+        sum.precision + result.scale - sum.scale > DecimalType.MAX_PRECISION
+      case _ => false
+    }
+
   override def getSupportLevel(expr: Average): SupportLevel =
-    if (AggSerde.avgDataTypeSupported(expr.dataType)) {
-      Compatible()
-    } else {
+    if (!AggSerde.avgDataTypeSupported(expr.dataType)) {
       Unsupported(Some(s"Unsupported data type: ${expr.dataType}"))
+    } else if (rescaledSumCanOverflow(expr)) {
+      Incompatible(Some(rescaledSumOverflowReason))

Review Comment:
   Precision 25 and up is common for decimals that come out of arithmetic, not 
only for `DECIMAL(38,18)` columns. For example, `DECIMAL(15,2) * DECIMAL(10,2)` 
is `DECIMAL(26,4)`. So with this gate, `avg(price * qty)` falls back by 
default, and so does every other aggregate in the same operator. For grouped 
queries that's a sizable regression for a bug that only shows up once the sum 
passes about 1.7e34 unscaled.
   
   #5420 already has a small, self-contained fix for `avg()`. It keeps the i128 
fast path, widens to i256 only when the scaled numerator overflows, and raises 
`NUMERIC_VALUE_OUT_OF_RANGE` under ANSI when the average doesn't fit the result 
type. Could we bring just that hunk in here instead of the `Incompatible` gate? 
That would remove this support level, `getIncompatibleReasons`, the three 
`expressions.md` notes and the `allowIncompatible` opt-ins in the five existing 
tests. Grouped AVG over `DECIMAL(25..37, s)` would also stay native and 
correct. The precision 38 guards would still be needed until the rest of #5420 
lands.
   
   If you'd rather keep the gate for now, could the PR description and the docs 
mention the arithmetic-result case, so users can tell why `avg(a * b)` stopped 
running natively?



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