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


##########
spark/src/test/resources/sql-tests/expressions/aggregate/corr.sql:
##########
@@ -37,3 +37,30 @@ INSERT INTO test_corr_nan VALUES (cast('NaN' as double), 
cast('NaN' as double),
 
 query tolerance=1e-6
 SELECT grp, corr(x, y) FROM test_corr_nan GROUP BY grp ORDER BY grp
+
+-- A constant input makes Spark divide ck by sqrt(xMk * yMk) = 0
+-- https://github.com/apache/datafusion-comet/issues/6481
+statement
+CREATE TABLE test_corr_const(y double, x double, grp string) USING parquet
+
+statement
+INSERT INTO test_corr_const VALUES (0.0, 0.1, 'a'), (1.0, 0.1, 'a'), (2.0, 
0.1, 'a'), (3.0, 0.1, 'a'), (4.0, 0.1, 'a'), (5.0, 0.1, 'a'), (1.0, 2.0, 'b'), 
(NULL, 3.0, 'c')
+
+query
+SELECT corr(y, x), corr(x, y) FROM test_corr_const WHERE grp = 'a'
+
+query
+SELECT grp, corr(y, x), corr(x, y) FROM test_corr_const GROUP BY grp ORDER BY 
grp
+
+statement
+SET spark.sql.ansi.enabled=true
+
+query expect_error(DIVIDE_BY_ZERO)
+SELECT corr(y, x) FROM test_corr_const WHERE grp = 'a'
+
+query expect_error(DIVIDE_BY_ZERO)

Review Comment:
   Could these two use `expect_error_class(DIVIDE_BY_ZERO)` instead of 
`expect_error(DIVIDE_BY_ZERO)`? `expect_error` only checks that both runs fail 
with a message containing the pattern, so it still passes if `corr` falls back 
to Spark, because Spark raises the same error. `expect_error_class` goes 
through `checkSparkError`. That also checks that the operators are native and 
that the exception class and SQLSTATE match Spark's. The non-ANSI `query` lines 
above already pass the operator check on this table, so I expect the switch to 
work (not run). With it, this file covers what the ANSI branch of the new Scala 
test asserts for one partition.



##########
native/spark-expr/src/agg_funcs/correlation.rs:
##########
@@ -594,6 +640,199 @@ mod groups_tests {
         }
     }
 
+    fn assert_divide_by_zero<T: std::fmt::Debug>(result: Result<T>) {
+        let Err(DataFusionError::External(error)) = result else {
+            panic!("expected a structured Spark divide-by-zero error, got 
{result:?}");
+        };
+        assert!(matches!(
+            error.downcast_ref::<SparkError>(),
+            Some(SparkError::DivideByZero)
+        ));
+    }
+
+    fn partial_state(input: &[ArrayRef]) -> Vec<ArrayRef> {
+        let mut partial =
+            CorrelationAccumulator::try_new(true, 
zero_divisor(EvalMode::Legacy)).unwrap();
+        partial.update_batch(input).unwrap();
+        partial
+            .state()
+            .unwrap()
+            .iter()
+            .map(|s| s.to_array_of_size(1).unwrap())
+            .collect()
+    }
+
+    #[test]
+    fn correlation_zero_denominator_respects_eval_mode() {
+        // https://github.com/apache/datafusion-comet/issues/6481
+        // x is constant at 0.1, which binary floating point cannot represent
+        // exactly. Spark divides ck by sqrt(xMk * yMk) = 0, so its Divide
+        // returns null in LEGACY mode and raises DIVIDE_BY_ZERO in ANSI mode.
+        let y: Vec<f64> = (0..6).map(f64::from).collect();
+        let x = [0.1; 6];
+        let input = |range: std::ops::Range<usize>| -> Vec<ArrayRef> {
+            vec![
+                Arc::new(Float64Array::from(y[range.clone()].to_vec())),
+                Arc::new(Float64Array::from(x[range].to_vec())),
+            ]
+        };
+        for eval_mode in [EvalMode::Legacy, EvalMode::Ansi] {
+            for null_on_divide_by_zero in [false, true] {

Review Comment:
   The `null_on_divide_by_zero` loop doubles this test without reaching a new 
branch. Every accumulator here ends with `n` = 6, so evaluation never reaches 
the `count == 1` branch, the only place the flag is read. The children run as 
`StatsType::Population` and never read it either. Fixing it to one value keeps 
the same coverage. `correlation_ansi_raises_only_on_zero_denominator` is the 
test where the loop matters.



##########
spark/src/test/scala/org/apache/comet/exec/CometAggregateSuite.scala:
##########
@@ -3357,6 +3370,90 @@ class CometAggregateSuite extends CometTestBase with 
AdaptiveSparkPlanHelper {
     }
   }
 
+  test("statistical aggregates corr preserves its captured evaluation mode") {
+    for (ansi <- Seq(false, true)) {
+      withSQLConf(SQLConf.ANSI_ENABLED.key -> ansi.toString) {
+        val expr = Corr(Literal(1.0), Literal(2.0))
+        withSQLConf(SQLConf.ANSI_ENABLED.key -> (!ansi).toString) {
+          val serialized = CometCorr.convert(
+            expr.toAggregateExpression(),
+            expr,
+            Seq.empty,
+            binding = false,
+            conf = SQLConf.get)
+          assert(serialized.isDefined)
+          val expected =
+            if (ansi) ExprOuterClass.EvalMode.ANSI else 
ExprOuterClass.EvalMode.LEGACY
+          assert(serialized.get.getCorrelation.getEvalMode == expected)
+        }
+      }
+    }
+  }
+
+  test("statistical aggregates corr zero denominator respects ANSI mode") {
+    // https://github.com/apache/datafusion-comet/issues/6481
+    withSQLConf(
+      SQLConf.ADAPTIVE_EXECUTION_ENABLED.key -> "false",
+      SQLConf.SHUFFLE_PARTITIONS.key -> "1",
+      SQLConf.FILES_MAX_PARTITION_BYTES.key -> "1048576",
+      SQLConf.FILES_OPEN_COST_IN_BYTES.key -> "1048576",
+      "spark.sql.files.minPartitionNum" -> "1",
+      CometConf.COMET_SHUFFLE_ENABLED.key -> "true",
+      CometConf.COMET_SHUFFLE_MODE.key -> "native") {
+      for (numPartitions <- Seq(1, 2)) {
+        withTempPath { path =>
+          // x is constant at 0.1, which binary floating point cannot 
represent exactly.
+          // Two files also exercise merging the partials in the final 
aggregate.
+          val files = if (numPartitions == 1) Seq(0 until 6) else Seq(0 until 
3, 3 until 6)
+          files.foreach { ys =>
+            ys.map(y => (0, y.toDouble, 0.1))
+              .toDF("g", "y", "x")
+              .coalesce(1)
+              .write
+              .mode("append")
+              .parquet(path.getCanonicalPath)
+          }
+          withParquetTable(path.getCanonicalPath, "corr_constant") {
+            assert(spark.table("corr_constant").rdd.getNumPartitions == 
numPartitions)
+            for {
+              ansi <- Seq(false, true)
+              groupBy <- Seq("", " GROUP BY g")
+            } {
+              withSQLConf(SQLConf.ANSI_ENABLED.key -> ansi.toString) {
+                for (args <- Seq("y, x", "x, y", "x, x")) {
+                  val query = s"SELECT corr($args) FROM corr_constant" + 
groupBy
+                  val df = sql(query)
+                  val aggregates = 
stripAQEPlan(df.queryExecution.executedPlan).collect {
+                    case a: CometHashAggregateExec => a
+                  }
+                  assert(aggregates.size == 2)
+                  assert(aggregates.exists(_.modes.contains(Partial)))
+                  assert(aggregates.exists(_.modes.contains(Final)))
+                  // Spark divides ck by sqrt(xMk * yMk) = 0.
+                  if (ansi) {
+                    val error = checkSparkError(df, "DIVIDE_BY_ZERO")
+                    assert(error.getSqlState == "22012")
+                    checkQueryContextMatchesSpark(error, query)
+                  } else {
+                    checkSparkAnswerAndOperator(df)
+                    checkCometAnswer(df, Seq(Row(null)))
+                  }
+                }
+
+                // The other statistical aggregates have no zero divisor here.
+                val statsQuery = "SELECT covar_pop(y, x), covar_samp(y, x), 
var_pop(x), " +
+                  "var_samp(x), stddev_pop(x), stddev_samp(x), corr(y, y) " +
+                  "FROM corr_constant" + groupBy
+                checkSparkAnswerAndOperator(statsQuery)
+                checkAnswer(sql(statsQuery), Seq(Row(0.0, 0.0, 0.0, 0.0, 0.0, 
0.0, 1.0)))

Review Comment:
   The non-ANSI branch and the `statsQuery` block repeat `statistical 
aggregates merge fractional constants across partitions`. That test already 
uses the same data (`y` from 0 to 5, `x` at 0.1, two files in both orders, with 
and without `GROUP BY`). It asserts native Partial and Final aggregates, `corr` 
= NULL and exact zeros for the other six aggregates. The single-partition cases 
are in `corr.sql`. Could this test keep only the ANSI branch (error class, 
SQLSTATE and the `checkQueryContextMatchesSpark` fragment)? Folding it as an 
`ansi = true` pass into that existing test would also work.



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