You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@spark.apache.org by do...@apache.org on 2026/09/23 19:35:38 UTC
(spark) branch master updated: [SPARK-59719][SQL] Mark CheckOverflow as null-intolerant to match MakeDecimal
This is an automated email from the ASF dual-hosted git repository.
dongjoon-hyun pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/spark.git
The following commit(s) were added to refs/heads/master by this push:
new 9929c6f0387a [SPARK-59719][SQL] Mark CheckOverflow as null-intolerant to match MakeDecimal
9929c6f0387a is described below
commit 9929c6f0387ab11538c94880b5ece7aee17aeec1
Author: david-mollitor-db <da...@databricks.com>
AuthorDate: Wed Sep 23 12:35:27 2026 -0700
[SPARK-59719][SQL] Mark CheckOverflow as null-intolerant to match MakeDecimal
### What changes were proposed in this pull request?
Declare `CheckOverflow` null-intolerant, matching its sibling `MakeDecimal`:
```scala
override def nullIntolerant: Boolean = true
```
`CheckOverflow` extends `UnaryExpression` and overrides `nullSafeEval`, so `UnaryExpression.eval` returns `null` whenever the child is `null` -- i.e. it already satisfies the null-intolerance contract ("any null input results in null output"). It has simply never been declared null-intolerant. Unlike its siblings `MakeDecimal` and `UnscaledValue`, `CheckOverflow` never mixed in the old `NullIntolerant` trait, so this is a pre-existing gap, not a regression: SPARK-50241 only turned tha [...]
`Cast` -- which shares the same "throws under ANSI overflow / returns null otherwise" shape -- already declares `nullIntolerant = true`, as do `MakeDecimal`, `UnscaledValue`, and the arithmetic expressions. This aligns `CheckOverflow` with them.
### Why are the changes needed?
`nullIntolerant` is consumed by the optimizer to:
- fold a null-literal input to a null literal (`NullPropagation`), and
- infer `IsNotNull(child)` constraints and push down `IsNotNull` filters (`QueryPlanConstraints.scanNullIntolerantAttribute` / `InferFiltersFromConstraints`).
`CheckOverflow` wraps decimal results throughout the plan (e.g. the decimal serializers in `SerializerBuildHelper`), so declaring the property it already honors lets these optimizations see through it, consistent with `MakeDecimal`. This is a follow-up to the nullability alignment in SPARK-59643 (#58913), addressing a related consistency gap raised in that review.
The direction the optimizer relies on holds even though `CheckOverflow` with `nullOnOverflow = true` can also return `null` from a non-null input on overflow: the contract only requires "null in => null out", not the converse -- the same situation as `Cast`.
### Does this PR introduce _any_ user-facing change?
No. This only declares an optimizer-visible property that `CheckOverflow` already honored at runtime; computed values and error behavior are unchanged. It can enable additional `IsNotNull` filter inference / null folding in query plans, but those are semantics-preserving: an inferred `IsNotNull` only removes rows whose `CheckOverflow` result would already be null, and null folding only fires for null-literal inputs, which never overflow or throw.
### How was this patch tested?
New tests for the newly-enabled optimizer behavior:
- `InferFiltersFromConstraintsSuite`: `IsNotNull('d)` is inferred from a filter `CheckOverflow('d, ...) > 0`.
- `NullPropagationSuite` (new): `CheckOverflow(Literal(null, ...), ...)` folds to a null literal under a NullPropagation-only batch, so the fold is attributable to null-intolerance.
Existing suites pass with no plan or golden-file churn: `DecimalExpressionSuite`, `PlannerSuite`, `AlignUpdateAssignmentsSuite`, `AlignMergeAssignmentsSuite`, plus the TPCDS plan-stability suites, `ExplainSuite`, and the `explain*.sql` golden files (verified the added `IsNotNull` inference does not change those plans).
### Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Opus 4.8
Closes #58972 from david-mollitor-db/checkoverflow-nullintolerant.
Authored-by: david-mollitor-db <da...@databricks.com>
Signed-off-by: Dongjoon Hyun <do...@apache.org>
---
.../catalyst/expressions/decimalExpressions.scala | 2 +
.../InferFiltersFromConstraintsSuite.scala | 14 +++++++
.../catalyst/optimizer/NullPropagationSuite.scala | 46 ++++++++++++++++++++++
3 files changed, 62 insertions(+)
diff --git a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/decimalExpressions.scala b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/decimalExpressions.scala
index f24c90768150..0cc8d8e38d4d 100644
--- a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/decimalExpressions.scala
+++ b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/decimalExpressions.scala
@@ -117,6 +117,8 @@ case class CheckOverflow(
dataType: DecimalType,
nullOnOverflow: Boolean) extends UnaryExpression with SupportQueryContext {
+ override def nullIntolerant: Boolean = true
+
override def nullable: Boolean = true
override def nullSafeEval(input: Any): Any =
diff --git a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/optimizer/InferFiltersFromConstraintsSuite.scala b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/optimizer/InferFiltersFromConstraintsSuite.scala
index bfed9427c863..0640642489a6 100644
--- a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/optimizer/InferFiltersFromConstraintsSuite.scala
+++ b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/optimizer/InferFiltersFromConstraintsSuite.scala
@@ -451,4 +451,18 @@ class InferFiltersFromConstraintsSuite extends PlanTest {
assert(joinFound, "Expected a Join node in the optimized plan")
}
+ test("SPARK-59719: infer IsNotNull through null-intolerant CheckOverflow") {
+ val dt = DecimalType(18, 0)
+ val decimalRelation = LocalRelation($"d".decimal(18, 0))
+ val query = decimalRelation
+ .where(CheckOverflow($"d", dt, nullOnOverflow = true) > Literal.create(Decimal(0), dt))
+ .analyze
+ val optimized = Optimize.execute(query)
+ val correctAnswer = decimalRelation
+ .where(IsNotNull($"d") &&
+ (CheckOverflow($"d", dt, nullOnOverflow = true) > Literal.create(Decimal(0), dt)))
+ .analyze
+ comparePlans(optimized, correctAnswer)
+ }
+
}
diff --git a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/optimizer/NullPropagationSuite.scala b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/optimizer/NullPropagationSuite.scala
new file mode 100644
index 000000000000..9f311e7becb8
--- /dev/null
+++ b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/optimizer/NullPropagationSuite.scala
@@ -0,0 +1,46 @@
+/*
+ * 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.catalyst.optimizer
+
+import org.apache.spark.sql.catalyst.dsl.expressions._
+import org.apache.spark.sql.catalyst.dsl.plans._
+import org.apache.spark.sql.catalyst.expressions.{CheckOverflow, Literal}
+import org.apache.spark.sql.catalyst.plans.PlanTest
+import org.apache.spark.sql.catalyst.plans.logical.{LocalRelation, LogicalPlan}
+import org.apache.spark.sql.catalyst.rules.RuleExecutor
+import org.apache.spark.sql.types.DecimalType
+
+class NullPropagationSuite extends PlanTest {
+
+ object Optimize extends RuleExecutor[LogicalPlan] {
+ val batches = Batch("NullPropagation", FixedPoint(50), NullPropagation) :: Nil
+ }
+
+ test("SPARK-59719: fold a null-intolerant CheckOverflow over a null literal to null") {
+ // With only NullPropagation in the batch, the fold happens because CheckOverflow is
+ // null-intolerant: a null child implies a null result.
+ val dt = DecimalType(18, 0)
+ val relation = LocalRelation($"a".int)
+ val query = relation
+ .select(CheckOverflow(Literal.create(null, dt), dt, nullOnOverflow = true).as("c"))
+ .analyze
+ val optimized = Optimize.execute(query)
+ val correctAnswer = relation.select(Literal.create(null, dt).as("c")).analyze
+ comparePlans(optimized, correctAnswer)
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@spark.apache.org
For additional commands, e-mail: commits-help@spark.apache.org