comphead commented on code in PR #5420:
URL: https://github.com/apache/datafusion-comet/pull/5420#discussion_r4277251013


##########
spark/src/test/scala/org/apache/comet/exec/CometAggregateSuite.scala:
##########
@@ -2372,6 +2409,228 @@ class CometAggregateSuite extends CometTestBase with 
AdaptiveSparkPlanHelper {
     }
   }
 
+  test("high-precision global decimal AVG and TRY_AVG fall back to Spark") {
+    withSQLConf(
+      SQLConf.ADAPTIVE_EXECUTION_ENABLED.key -> "false",
+      SQLConf.WHOLESTAGE_CODEGEN_ENABLED.key -> "true",
+      CometConf.COMET_SHUFFLE_ENABLED.key -> "true",
+      CometConf.COMET_SHUFFLE_MODE.key -> "native") {
+      withTempDir { dir =>
+        Seq((1, "0.6"), (2, "0.6"))
+          .toDF("ord", "raw_v")
+          .selectExpr(
+            "ord",
+            "CAST(raw_v AS DECIMAL(27,27)) AS v27",
+            "CAST(raw_v AS DECIMAL(28,28)) AS v28",
+            "CAST(raw_v AS DECIMAL(38,38)) AS v38")
+          .write
+          .mode("overwrite")
+          .parquet(dir.toString)
+        withParquetTable(spark.read.parquet(dir.toString).coalesce(1), 
"global_avg") {
+          val expectedAverage = new java.math.BigDecimal("0.6").setScale(38)
+          // Precision 27 stays native; 28 starts fallback. At 38, the 
intermediate 1.2
+          // overflows in Comet although Spark can divide it and return 0.6.
+          for ((ansi, aggregates, nativeCount, expected) <- Seq(
+              (false, "AVG(v27)", 2, Row(expectedAverage.setScale(31))),
+              (false, "AVG(v28)", 0, Row(expectedAverage.setScale(32))),
+              (true, "AVG(v38)", 0, Row(expectedAverage)),
+              (true, "TRY_AVG(v38)", 0, Row(expectedAverage)),
+              (false, "AVG(v38), COUNT(DISTINCT ord)", 0, Row(expectedAverage, 
2L)),
+              // Object aggregation overflows when materializing its partial 
buffer.
+              (false, "AVG(v38), sort_array(collect_list(ord))", 0, Row(null, 
Seq(1, 2))))) {
+            withSQLConf(SQLConf.ANSI_ENABLED.key -> ansi.toString) {
+              val df = sql(s"SELECT $aggregates FROM global_avg")
+              checkAnswer(df, Seq(expected))
+              assert(getNumCometHashAggregate(df) == nativeCount)
+            }
+          }
+          // Merging individually valid partials can overflow too; keep the 
shuffled final safe.
+          spark.read
+            .parquet(dir.toString)
+            .repartition(2, col("ord"))
+            .createOrReplaceTempView("global_avg")
+          val shuffled = sql("SELECT AVG(v38), TRY_AVG(v38) FROM global_avg")
+          checkAnswer(shuffled, Seq(Row(expectedAverage, expectedAverage)))
+          assert(getNumCometHashAggregate(shuffled) == 0)
+        }
+      }
+    }
+  }
+
+  test("high-precision DISTINCT decimal AVG preserves Spark shuffle 
semantics") {
+    withSQLConf(
+      SQLConf.ADAPTIVE_EXECUTION_ENABLED.key -> "true",
+      SQLConf.SHUFFLE_PARTITIONS.key -> "2",
+      SQLConf.COALESCE_PARTITIONS_ENABLED.key -> "false",
+      CometConf.COMET_SHUFFLE_ENABLED.key -> "true",
+      // Native wide-decimal hash routing is tracked separately in #6005.
+      CometConf.COMET_SHUFFLE_MODE.key -> "jvm") {
+      withTempDir { dir =>
+        Seq((1, "0.6"), (2, "0.6"), (3, "0.2"), (4, "0.3"))
+          .toDF("ord", "raw_v")
+          .selectExpr("ord", "CAST(raw_v AS DECIMAL(38,38)) AS v")
+          .write
+          .mode("overwrite")
+          .parquet(dir.toString)
+        withParquetTable(
+          spark.read.parquet(dir.toString).repartition(2, col("ord")),
+          "distinct_avg") {
+          // Spark hashes all three distinct values to one partition. 
Materializing its
+          // partial sum overflows, unlike the adjacent scalar stages 
exercised above.
+          val df = sql("SELECT AVG(DISTINCT v) FROM distinct_avg")
+          checkSparkAnswer(df)
+          checkAnswer(df, Seq(Row(null)))
+        }
+      }
+    }
+  }
+
+  test("grouped decimal AVG widens division and reports final result 
overflow") {

Review Comment:
   Most of this test looks expressible as SQL fixtures. The `k = 1` and 
`TRY_AVG` results could be plain `query` records in 
`expressions/aggregate/avg.sql`, which already asserts native execution in 
place of the `CometHashAggregateExec` size checks. The `k = 2` ANSI case could 
be a `-- Config: spark.sql.ansi.enabled=true` file with `query 
expect_error(NUMERIC_VALUE_OUT_OF_RANGE)`. `decimal AVG with object hash 
aggregate falls back at maximum precision` (line 2540) could be `query 
expect_fallback(Grouped decimal AVG at maximum precision)`, which other 
fixtures in `expressions/aggregate` already use. I have not checked that 
`expect_fallback` sees operator-level reasons. The global fallback and 
shuffle-writer tests depend on partition layout and should stay in Scala.



##########
native/spark-expr/src/agg_funcs/avg_decimal.rs:
##########
@@ -671,31 +609,281 @@ impl GroupsAccumulator for AvgDecimalGroupsAccumulator {
 }
 
 /// Returns the `sum`/`count` as a i128 Decimal128 with
-/// target_scale and target_precision and return None if overflows.
+/// target_scale and target_precision, returning the rounded wide value on 
overflow.
 ///
 /// * sum: The total sum value stored as Decimal128 with sum_scale
 /// * count: total count, stored as a i128 (*NOT* a Decimal128 value)
 /// * target_min: The minimum output value possible to represent with the 
target precision
 /// * target_max: The maximum output value possible to represent with the 
target precision
 /// * scaler: scale factor for avg
 #[inline(always)]
-fn avg(sum: i128, count: i128, target_min: i128, target_max: i128, scaler: 
i128) -> Option<i128> {
-    if let Some(value) = sum.checked_mul(scaler) {
-        // `sum / count` with ROUND_HALF_UP
+fn avg(
+    sum: i128,
+    count: i128,
+    target_min: i128,
+    target_max: i128,
+    scaler: i128,
+) -> std::result::Result<i128, i256> {
+    // The scaled numerator can exceed i128 while both the sum and final 
average fit.
+    // Use the inexpensive path when it fits, and widen only that intermediate 
otherwise.
+    let rounded = if let Some(value) = sum.checked_mul(scaler) {
         let (div, rem) = value.div_rem(&count);
         let half = div_ceil(count, 2);
-        let half_neg = half.neg_wrapping();
-        let new_value = match value >= 0 {
+        let rounded = match value >= 0 {
             true if rem >= half => div.add_wrapping(1),
-            false if rem <= half_neg => div.sub_wrapping(1),
+            false if rem <= half.neg_wrapping() => div.sub_wrapping(1),
             _ => div,
         };
-        if new_value >= target_min && new_value <= target_max {
-            Some(new_value)
+        i256::from_i128(rounded)
+    } else {
+        // A Decimal128 numerator times the Decimal128 scale factor fits 
within i256.
+        let value = i256::from_i128(sum) * i256::from_i128(scaler);
+        let divisor = i256::from_i128(count);
+        let (div, rem) = (value / divisor, value % divisor);
+        let half = i256::from_i128(div_ceil(count, 2));
+        if rem >= half {
+            div + i256::ONE
+        } else if rem <= -half {
+            div - i256::ONE
         } else {
-            None
+            div
         }
+    };
+    if rounded >= i256::from_i128(target_min) && rounded <= 
i256::from_i128(target_max) {
+        Ok(rounded.to_i128().expect("bounded Decimal128 average"))
     } else {
-        None
+        Err(rounded)
+    }
+}
+
+fn avg_result_overflow_error(value: i256, precision: u8, scale: i8) -> 
crate::SparkError {

Review Comment:
   The value in this message differs from Spark's. 
`DecimalDivideWithOverflowCheck` calls `toPrecision` on the raw quotient from 
`Decimal./`, which divides at scale `MAX_SCALE + 1` (39) with 
`RoundingMode.DOWN`. On failure `toPrecision` passes that unrounded quotient to 
`cannotChangeDecimalPrecisionError`, which renders `value.toPlainString`. For 
the `DECIMAL(38,18)` row `100000000000000000` in `grouped decimal AVG widens 
division and reports final result overflow`, I expect Spark to print the value 
with 39 fractional digits, where this formats the rounded result at the result 
scale. The test only checks for `NUMERIC_VALUE_OUT_OF_RANGE`, so it would not 
catch the difference. I derived this from the Spark 4.0 source and did not run 
it. Could the error format the quotient truncated at scale 39, or could a short 
note go next to the known-divergence line for grouped `AVG`?



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