-
Notifications
You must be signed in to change notification settings - Fork 343
fix: avoid duplicate CheckOverflow evaluation for decimal division #5225
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
afbebbf
c61af2e
bd051a1
3f820f7
1a38b70
e27a9f6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -45,6 +45,15 @@ object DecimalPrecision { | |
| // happen if the Spark version is < 3.4 | ||
| case e: BinaryArithmetic if e.left.prettyName == "promote_precision" => e | ||
|
|
||
| // Recursive exprToProto calls can re-promote every decimal binary operator below. Collapse | ||
| // only equivalent wrappers so the shared promotion rule remains idempotent. | ||
| case outer @ CheckOverflow( | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a good fix, and the bottom-up collapse converges correctly at any depth. The re-promotion it works around comes from serdes calling the public
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. filed. #5248 |
||
| inner @ CheckOverflow(child: BinaryArithmetic, _, _), | ||
| dataType, | ||
| nullOnOverflow) | ||
| if inner.dataType == dataType && inner.nullOnOverflow == nullOnOverflow => | ||
| outer.copy(child = child) | ||
|
|
||
| case add @ Add(DecimalExpression(_, _), DecimalExpression(_, _), _) | ||
| if add.dataType.isInstanceOf[DecimalType] => | ||
| CheckOverflow(add, add.dataType.asInstanceOf[DecimalType], add.evalMode != EvalMode.ANSI) | ||
|
|
@@ -57,6 +66,8 @@ object DecimalPrecision { | |
| if mul.dataType.isInstanceOf[DecimalType] => | ||
| CheckOverflow(mul, mul.dataType.asInstanceOf[DecimalType], mul.evalMode != EvalMode.ANSI) | ||
|
|
||
| // Native decimal division uses i128::MAX as an overflow sentinel, so this wrapper must | ||
| // convert it to null or an ANSI error according to the expression's eval mode. | ||
| case div @ Divide(DecimalExpression(_, _), DecimalExpression(_, _), _) | ||
| if div.dataType.isInstanceOf[DecimalType] => | ||
| CheckOverflow(div, div.dataType.asInstanceOf[DecimalType], div.evalMode != EvalMode.ANSI) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| /* | ||
| * 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.comet | ||
|
|
||
| import org.apache.spark.sql.CometTestBase | ||
| import org.apache.spark.sql.catalyst.expressions.{ArrayContains, AttributeReference, Divide, EvalMode} | ||
| import org.apache.spark.sql.internal.SQLConf | ||
| import org.apache.spark.sql.types.{DecimalType, IntegerType} | ||
|
|
||
| import org.apache.comet.serde.{CometDivide, ExprOuterClass, QueryPlanSerde, Unsupported} | ||
|
|
||
| class CometDecimalPromotionSuite extends CometTestBase { | ||
|
|
||
| private case class Operation( | ||
| name: String, | ||
| symbol: String, | ||
| hasProto: ExprOuterClass.Expr => Boolean) | ||
|
|
||
| private case class TestMode(name: String, ansiEnabled: Boolean, failOnError: Boolean) | ||
|
|
||
| test("issue #5190: decimal promotion is idempotent during recursive serialization") { | ||
| val left = "CAST(id AS DECIMAL(10, 0))" | ||
| val right = "CAST(id + 1 AS DECIMAL(10, 0))" | ||
| val divide = Operation(name = "divide", symbol = "/", hasProto = _.hasDivide) | ||
| val operations = Seq( | ||
| Operation(name = "add", symbol = "+", hasProto = _.hasAdd), | ||
| Operation(name = "subtract", symbol = "-", hasProto = _.hasSubtract), | ||
| Operation(name = "multiply", symbol = "*", hasProto = _.hasMultiply), | ||
| divide, | ||
| Operation(name = "remainder", symbol = "%", hasProto = _.hasRemainder)) | ||
| val legacy = TestMode(name = "LEGACY", ansiEnabled = false, failOnError = false) | ||
| val ansi = TestMode(name = "ANSI", ansiEnabled = true, failOnError = true) | ||
|
|
||
| def check(operation: Operation, mode: TestMode, arithmetic: String): Unit = { | ||
| val name = s"${operation.name} ${mode.name}" | ||
| withSQLConf(SQLConf.ANSI_ENABLED.key -> mode.ansiEnabled.toString) { | ||
| val plan = spark | ||
| .sql(s"SELECT array_contains(array($arithmetic), $arithmetic) FROM range(1, 4)") | ||
| .queryExecution | ||
| .optimizedPlan | ||
| val expression = plan.expressions.head | ||
| val arrayContains = expression.collectFirst { case e: ArrayContains => e }.get | ||
| val promoted = DecimalPrecision.promote(expression) | ||
| assert( | ||
| DecimalPrecision.promote(promoted) == promoted, | ||
| s"$name promotion is not idempotent: $promoted") | ||
|
|
||
| val arithmeticProto = QueryPlanSerde | ||
| .exprToProto(expression, plan.children.head.output) | ||
| .get | ||
| .getScalarFunc | ||
| .getArgs(1) | ||
| assert(arithmeticProto.hasCheckOverflow, s"$name: $arithmeticProto") | ||
| val overflow = arithmeticProto.getCheckOverflow | ||
| assert( | ||
| overflow.getDatatype === QueryPlanSerde | ||
| .serializeDataType(arrayContains.right.dataType) | ||
| .get, | ||
| s"$name has the wrong CheckOverflow datatype: $arithmeticProto") | ||
| assert(overflow.getFailOnError === mode.failOnError, s"$name: $arithmeticProto") | ||
| assert( | ||
| operation.hasProto(overflow.getChild), | ||
| s"$name has duplicate CheckOverflow: $arithmeticProto") | ||
| } | ||
| } | ||
|
|
||
| operations.foreach { operation => | ||
| Seq(legacy, ansi).foreach { mode => | ||
| check(operation, mode, s"$left ${operation.symbol} $right") | ||
| } | ||
| } | ||
| check( | ||
| divide, | ||
| TestMode(name = "TRY", ansiEnabled = true, failOnError = false), | ||
| s"try_divide($left, $right)") | ||
| } | ||
|
|
||
| test("decimal Divide with a non-decimal operand is unsupported") { | ||
| // This is only a sanity check; Spark's type coercion should prevent this case. | ||
| val decimal = AttributeReference("decimal", DecimalType(10, 0))() | ||
| val integer = AttributeReference("integer", IntegerType)() | ||
| val divide = Divide(decimal, integer, EvalMode.LEGACY) | ||
|
|
||
| assert(divide.dataType === DecimalType(10, 0)) | ||
| assert(CometDivide.getSupportLevel(divide).isInstanceOf[Unsupported]) | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One thing I want to check here. The guard you removed fired on
expr.dataType.isInstanceOf[DecimalType]alone. Thepromoterule requires both operands to matchDecimalExpressionas well, andgetSupportLevelonly looks atexpr.left.dataType. So if a decimal-typedDividewith a non-decimal operand ever reaches this serde, there is noCheckOverflowanywhere and the nativei128::MAXsentinel comes back as a real value.I could not construct that shape through Spark's type coercion, so I believe it is unreachable today. Since the failure mode is silent wrong data rather than a fallback, would you be up for a defensive guard? Returning
UnsupportedfromgetSupportLevelwhenexpr.dataTypeis decimal but the operands would not matchpromote's pattern would keep the invariant checkable next to the code that depends on it.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
added a sanity check.