This is an automated email from the ASF dual-hosted git repository.

HyukjinKwon 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 576c2dbeb5d0 [SPARK-60572][SQL] Preserve NULL string day counts in 
date arithmetic
576c2dbeb5d0 is described below

commit 576c2dbeb5d0498f8c1afd6c6b6b9eff2711b6bb
Author: Linhong Liu <[email protected]>
AuthorDate: Wed Oct 28 10:22:57 2026 -0700

    [SPARK-60572][SQL] Preserve NULL string day counts in date arithmetic
    
    ### What changes were proposed in this pull request?
    
    Fix `date_add` and `date_sub` to preserve a foldable NULL string day count 
during
    type coercion. Keep the cast result boxed and construct an INT literal with
    `Literal.create`, so NULL does not become zero.
    
    ### Why are the changes needed?
    
    With ANSI mode disabled, string day counts are evaluated during analysis. 
Unboxing
    a NULL cast result with `asInstanceOf[Int]` produces zero, causing these 
functions
    to return the input date instead of NULL.
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes, a bug fix. For example:
    
    ```sql
    SET spark.sql.ansi.enabled=false;
    SELECT date_add(DATE '2020-01-01', CAST(NULL AS STRING)),
           date_sub(DATE '2020-01-01', CAST(NULL AS STRING));
    ```
    
    Previously this returned `2020-01-01, 2020-01-01`; it now returns `NULL, 
NULL`.
    Valid non-null string day counts and invalid-string errors are unchanged.
    
    ### How was this patch tested?
    
    Added coercion and SQL regression coverage for direct NULL strings, foldable
    conditional/cast expressions, ANSI mode on and off, valid string day 
counts, and
    invalid-string errors. The new coercion test fails without the fix because 
the
    day-count literal is `0` instead of NULL.
    
    Passed on JDK 17:
    
    ```bash
    build/sbt 'catalyst/testOnly 
org.apache.spark.sql.catalyst.analysis.TypeCoercionSuite'
    build/sbt 'sql/testOnly org.apache.spark.sql.DateFunctionsSuite -- -z "date 
arithmetic" -z "function date_add" -z "function date_sub"'
    ```
    
    The commands passed 63 Catalyst tests and five SQL tests. Catalyst 
main/test and
    SQL test Scalastyle checks also passed, along with `git diff --check`.
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    Generated-by: OpenAI Codex (GPT-6)
    
    Closes #59806 from linhongliu-db/fix-null-date-day-counts.
    
    Authored-by: Linhong Liu <[email protected]>
    Signed-off-by: Hyukjin Kwon <[email protected]>
---
 .../analysis/StringLiteralTypeCoercion.scala       | 10 ++++---
 .../sql/catalyst/analysis/TypeCoercionSuite.scala  | 17 ++++++++++++
 .../org/apache/spark/sql/DateFunctionsSuite.scala  | 31 ++++++++++++++++++++++
 3 files changed, 54 insertions(+), 4 deletions(-)

diff --git 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/StringLiteralTypeCoercion.scala
 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/StringLiteralTypeCoercion.scala
index d1b63dc90b06..c6a3de0617d2 100644
--- 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/StringLiteralTypeCoercion.scala
+++ 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/StringLiteralTypeCoercion.scala
@@ -26,23 +26,25 @@ import org.apache.spark.sql.types.{IntegerType, StringType}
  * type coerce the second argument to [[IntegerType]], if necessary.
  */
 object StringLiteralTypeCoercion {
+  // Keep cast results boxed: unboxing a NULL as Int yields zero, which would 
make
+  // date_add/date_sub return the input date instead of NULL.
   def apply(expression: Expression): Expression = expression match {
     case DateAdd(l, r) if r.dataType.isInstanceOf[StringType] && r.foldable =>
       val days = try {
-        Cast(r, IntegerType, ansiEnabled = true).eval().asInstanceOf[Int]
+        Cast(r, IntegerType, ansiEnabled = true).eval()
       } catch {
         case e: NumberFormatException =>
           throw 
QueryCompilationErrors.secondArgumentOfFunctionIsNotIntegerError("date_add", e)
       }
-      DateAdd(l, Literal(days))
+      DateAdd(l, Literal.create(days, IntegerType))
     case DateSub(l, r) if r.dataType.isInstanceOf[StringType] && r.foldable =>
       val days = try {
-        Cast(r, IntegerType, ansiEnabled = true).eval().asInstanceOf[Int]
+        Cast(r, IntegerType, ansiEnabled = true).eval()
       } catch {
         case e: NumberFormatException =>
           throw 
QueryCompilationErrors.secondArgumentOfFunctionIsNotIntegerError("date_sub", e)
       }
-      DateSub(l, Literal(days))
+      DateSub(l, Literal.create(days, IntegerType))
     case other => other
   }
 }
diff --git 
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/analysis/TypeCoercionSuite.scala
 
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/analysis/TypeCoercionSuite.scala
index d2d0823ccea8..df3e58da673f 100644
--- 
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/analysis/TypeCoercionSuite.scala
+++ 
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/analysis/TypeCoercionSuite.scala
@@ -1938,6 +1938,23 @@ class TypeCoercionSuite extends TypeCoercionSuiteBase {
     ruleTest(rules, Divide(nullLit, 1L), Divide(Cast(nullLit, DoubleType), 
Cast(1L, DoubleType)))
   }
 
+  test("date arithmetic preserves foldable NULL string day counts") {
+    val date = Literal(0, DateType)
+    val nullString = Literal.create(null, StringType)
+    for (days <- Seq(
+        nullString,
+        If(Literal(true), nullString, Literal("2")),
+        Cast(Literal.create(null, IntegerType), StringType))) {
+      for (expression <- Seq(DateAdd(date, days), DateSub(date, days))) {
+        val coerced = StringLiteralTypeCoercion(expression)
+        assert(coerced.children(1) == Literal.create(null, IntegerType))
+        assert(coerced.eval() == null)
+      }
+    }
+    assert(StringLiteralTypeCoercion(DateAdd(date, Literal("2"))).eval() == 2)
+    assert(StringLiteralTypeCoercion(DateSub(date, Literal("2"))).eval() == -2)
+  }
+
   test("Do not promote strings in binary arithmetic with intervals") {
     val rule = TypeCoercion.PromoteStrings
     // Verify String literal is not promoted in binary arithmetic operations 
with
diff --git 
a/sql/core/src/test/scala/org/apache/spark/sql/DateFunctionsSuite.scala 
b/sql/core/src/test/scala/org/apache/spark/sql/DateFunctionsSuite.scala
index 693eb3b86b0c..c46ca8ffcb8d 100644
--- a/sql/core/src/test/scala/org/apache/spark/sql/DateFunctionsSuite.scala
+++ b/sql/core/src/test/scala/org/apache/spark/sql/DateFunctionsSuite.scala
@@ -445,6 +445,37 @@ class DateFunctionsSuite extends SharedSparkSession {
       Seq(Row(Date.valueOf("2015-05-31")), Row(Date.valueOf("2015-06-01"))))
   }
 
+  gridTest("date arithmetic preserves NULL string day counts")(Seq(false, 
true)) { ansiEnabled =>
+    withSQLConf(SQLConf.ANSI_ENABLED.key -> ansiEnabled.toString) {
+      for (days <- Seq(
+          "CAST(NULL AS STRING)",
+          "IF(true, CAST(NULL AS STRING), '2')",
+          "CAST(CAST(NULL AS INT) AS STRING)")) {
+        checkAnswer(
+          sql(s"SELECT date_add(DATE '2020-01-01', $days), " +
+            s"date_sub(DATE '2020-01-01', $days)"),
+          Row(null, null))
+      }
+      checkAnswer(
+        sql("SELECT date_add(DATE '2020-01-01', '2'), date_sub(DATE 
'2020-01-01', '2')"),
+        Row(Date.valueOf("2020-01-03"), Date.valueOf("2019-12-30")))
+    }
+  }
+
+  test("date arithmetic rejects invalid string day counts in non-ANSI mode") {
+    withSQLConf(SQLConf.ANSI_ENABLED.key -> "false") {
+      for (function <- Seq("date_add", "date_sub"); days <- Seq("x", "1.5")) {
+        checkError(
+          exception = intercept[AnalysisException] {
+            sql(s"SELECT $function(DATE '2020-01-01', '$days')").collect()
+          },
+          condition = "SECOND_FUNCTION_ARGUMENT_NOT_INTEGER",
+          parameters = Map("functionName" -> function),
+          sqlState = "22023")
+      }
+    }
+  }
+
   test("time_add") {
     val t1 = Timestamp.valueOf("2015-07-31 23:59:59")
     val t2 = Timestamp.valueOf("2015-12-31 00:00:00")


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to