Copilot commented on code in PR #13103:
URL: https://github.com/apache/gluten/pull/13103#discussion_r4522802058


##########
cpp/velox/substrait/SubstraitToVeloxPlanValidator.cc:
##########
@@ -152,6 +152,19 @@ bool SubstraitToVeloxPlanValidator::validateRound(
     return false;
   }
 
+  const auto& valueType = params[0]->type();
+  if (valueType->isDecimal()) {

Review Comment:
   `validateRound` now indexes `params[0]` without validating `params.size()`. 
If argument conversion fails upstream but `arguments` still has 2 entries, this 
can read out-of-bounds and crash the validator. Add a guard like `if 
(params.size() < 2) return false;` before accessing `params[0]` (and ideally 
ensure `params[i]` is non-null per the header contract).



##########
cpp/velox/substrait/SubstraitToVeloxPlanValidator.cc:
##########
@@ -152,6 +152,19 @@ bool SubstraitToVeloxPlanValidator::validateRound(
     return false;
   }
 
+  const auto& valueType = params[0]->type();
+  if (valueType->isDecimal()) {
+    const auto& scale = arguments[1].value().literal();
+    if (scale.has_null() && scale.null().has_i32()) {
+      return true;
+    }
+    if (scale.has_i32() && scale.i32() >= -400 && scale.i32() <= 400) {
+      return true;
+    }
+    LOG_VALIDATION_MSG("Decimal ROUND requires an INTEGER scale in [-400, 400] 
or NULL.");
+    return false;
+  }

Review Comment:
   The function comment/header says `validateRound` validates using 
already-converted arguments, but the decimal-path still inspects the raw 
protobuf literal (`arguments[1].value().literal()`). This couples validation 
logic to Substrait encoding details and makes it harder to evolve argument 
conversion (e.g., handling casts/arithmetic consistently via `params[1]`). 
Consider validating the scale using `params[1]` (e.g., allow constant null / 
constant integer within bounds, reject non-constants) and keep protobuf 
inspection as a fallback only if required.



##########
gluten-ut/common/src/test/scala/org/apache/spark/sql/GlutenDecimalRoundSuiteBase.scala:
##########
@@ -0,0 +1,191 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.spark.sql
+
+import org.apache.gluten.config.GlutenConfig
+import org.apache.gluten.execution.ProjectExecTransformer
+
+import org.apache.spark.sql.catalyst.expressions.{Literal, Round}
+import org.apache.spark.sql.catalyst.optimizer.{ConstantFolding, 
NullPropagation}
+import org.apache.spark.sql.internal.SQLConf
+import org.apache.spark.sql.types._
+
+import scala.collection.JavaConverters._
+
+abstract class GlutenDecimalRoundSuiteBase extends GlutenSQLTestsTrait {
+  private def withInput(dataType: DecimalType, values: Seq[String])(f: => 
Unit): Unit = {
+    val schema = new StructType().add("value", dataType)
+    val rows = values.map(value => Row(new java.math.BigDecimal(value))) :+ 
Row(null)
+    withSQLConf(
+      SQLConf.ADAPTIVE_EXECUTION_ENABLED.key -> "false",
+      GlutenConfig.GLUTEN_ANSI_FALLBACK_ENABLED.key -> "false") {
+      withTempPath {
+        path =>
+          spark.createDataFrame(rows.asJava, 
schema).coalesce(1).write.parquet(path.toString)
+          withTempView("decimal_round_input") {
+            
spark.read.parquet(path.toString).createOrReplaceTempView("decimal_round_input")
+            f
+          }
+      }
+    }
+  }
+
+  private def assertExecution(df: DataFrame, native: Boolean): Unit = {
+    val plan = df.queryExecution.executedPlan
+    val locations = plan.collect {
+      case node if node.expressions.exists(_.exists(_.isInstanceOf[Round])) =>
+        node.isInstanceOf[ProjectExecTransformer]
+    }
+    assert(
+      locations.nonEmpty && locations.forall(_ == native),
+      s"Unexpected ROUND execution:\n$plan")
+  }
+
+  private def checkRound(scale: String, native: Boolean = true): Unit = {
+    val query = s"SELECT round(value, $scale) FROM decimal_round_input"
+    withSQLConf(GlutenConfig.GLUTEN_ENABLED.key -> "false") {
+      val vanilla = sql(query)
+      val expectedSchema = vanilla.schema
+      val expected = vanilla.collect().toSeq
+      withSQLConf(GlutenConfig.GLUTEN_ENABLED.key -> "true") {
+        val df = sql(query)
+        assertExecution(df, native)
+        assert(df.schema == expectedSchema)
+        checkAnswer(df, expected)
+      }
+    }
+  }
+
+  testGluten("decimal ROUND preserves schemas and HALF_UP results across 
storage boundaries") {
+    val cases = Seq(
+      (DecimalType(3, 1), Seq("2.5", "-2.5", "0")),
+      (DecimalType(18, 0), Seq("999999999999999999", "-999999999999999999")),
+      (DecimalType(19, 0), Seq("9999999999999999999", "-9999999999999999999")),
+      (DecimalType(38, 1), Seq("9" * 37 + ".9", "-" + "9" * 37 + ".9")),
+      (DecimalType(38, 38), Seq("0.6", "-0.6", "0.5", "-0.5"))
+    )
+    for ((dataType, values) <- cases) {
+      withInput(dataType, values) {
+        for (ansi <- Seq("false", "true")) {
+          withSQLConf(SQLConf.ANSI_ENABLED.key -> ansi) {
+            Seq(-400, -39, -1, 0, 1, 38, 400).foreach(scale => 
checkRound(scale.toString))
+            checkRound("-1 + 0")
+          }
+        }
+      }
+    }
+  }
+
+  testGluten("decimal ROUND handles the 38-digit HALF_UP tie and precision 
overflow") {
+    withInput(DecimalType(38, 0), Seq("4" + "9" * 37, "-4" + "9" * 37)) {
+      checkRound("-38")
+    }
+    for ((value, scale) <- Seq(("9" * 38, -1), ("5" + "0" * 37, -38), ("-5" + 
"0" * 37, -38))) {
+      withInput(DecimalType(38, 0), Seq(value)) {
+        for (ansi <- Seq("false", "true")) {
+          withSQLConf(SQLConf.ANSI_ENABLED.key -> ansi) {
+            val query = s"SELECT round(value, $scale) FROM decimal_round_input"
+            withSQLConf(GlutenConfig.GLUTEN_ENABLED.key -> "false") {
+              val error = intercept[Exception](sql(query).collect())
+              
assert(Iterator.iterate[Throwable](error)(_.getCause).takeWhile(_ != 
null).exists {
+                cause =>
+                  Option(cause.getMessage).exists {
+                    message =>
+                      message.contains("NUMERIC_VALUE_OUT_OF_RANGE") ||
+                      
message.contains("DECIMAL_PRECISION_EXCEEDS_MAX_PRECISION")
+                  }
+              })
+            }
+            val df = sql(query)
+            assertExecution(df, native = true)
+            val error = intercept[Exception](df.collect())
+            assert(Iterator.iterate[Throwable](error)(_.getCause).takeWhile(_ 
!= null).exists {
+              cause => Option(cause.getMessage).exists(_.contains("Decimal 
overflow"))
+            })
+          }
+        }
+      }
+    }
+  }
+
+  testGluten("decimal ROUND NULL scale skips an overflowing child natively") {
+    withInput(DecimalType(38, 0), Seq("9" * 38)) {
+      for (ansi <- Seq("false", "true")) {
+        withSQLConf(
+          SQLConf.ANSI_ENABLED.key -> ansi,
+          GlutenConfig.NATIVE_VALIDATION_ENABLED.key -> "true",
+          SQLConf.OPTIMIZER_EXCLUDED_RULES.key ->
+            s"${ConstantFolding.ruleName},${NullPropagation.ruleName}"
+        ) {
+          val query =
+            "SELECT round(round(value, -1), CAST(NULL AS INT)) FROM 
decimal_round_input"
+          val (expectedSchema, expected) =
+            withSQLConf(GlutenConfig.GLUTEN_ENABLED.key -> "false") {
+              val vanilla = sql(query)
+              (vanilla.schema, vanilla.collect().toSeq)
+            }
+          val df = sql(query)
+          assertExecution(df, native = true)
+          assert(df.schema == expectedSchema)
+          checkAnswer(df, expected)
+        }
+      }
+    }
+  }
+
+  testGluten("decimal ROUND debug mode bypasses compatibility fallback") {
+    withInput(DecimalType(3, 1), Seq("2.5", "-2.5", "0")) {
+      withSQLConf(
+        GlutenConfig.NATIVE_VALIDATION_ENABLED.key -> "false",
+        SQLConf.OPTIMIZER_EXCLUDED_RULES.key -> NullPropagation.ruleName) {
+        checkRound("CAST(NULL AS INT)")
+        checkRound("-401")
+        checkRound("401")

Review Comment:
   These debug-mode assertions currently require full Spark parity 
(`checkRound` compares schema + results against vanilla Spark) even for scales 
explicitly treated as unsupported by the validator (e.g., ±401). This can make 
the suite brittle and at odds with the config documentation stating unsupported 
operations may fail at runtime when native validation is disabled. Consider 
adjusting this test to (a) only assert native planning occurs, and (b) either 
accept a runtime failure for out-of-range scales (intercept/verify error) or 
avoid enforcing result parity for those out-of-range cases.



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