You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@spark.apache.org by ge...@apache.org on 2026/10/15 16:48:47 UTC

(spark) branch master updated: [SPARK-59992][SQL] Make element_at and array index nullable when the index is nullable under ANSI mode

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

gengliangwang 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 ba9a922fbd14 [SPARK-59992][SQL] Make element_at and array index nullable when the index is nullable under ANSI mode
ba9a922fbd14 is described below

commit ba9a922fbd14da1b5186a91184cafa5b7c50b5db
Author: richardc-db <r....@databricks.com>
AuthorDate: Thu Oct 15 09:48:34 2026 -0700

    [SPARK-59992][SQL] Make element_at and array index nullable when the index is nullable under ANSI mode
    
    ### What changes were proposed in this pull request?
    Under ANSI mode, `GetArrayItemUtil.computeNullabilityFromArray` (shared by `ElementAt` and `GetArrayItem`) now also considers `ordinal.nullable` for a non-foldable index, so `element_at(arr, idx)` and `arr[idx]` are nullable whenever the index can be null.
    
    With a non-nullable index the result keeps the existing non-nullable type, so plans that do not involve a nullable index are unchanged.
    
    ### Why are the changes needed?
    Both expressions are null-intolerant and return NULL for a NULL index, but under ANSI they reported `nullable = false` whenever the array and its elements were non-nullable (e.g. `array('a', 'b')`).
    
    The optimizer trusts that flag: `NullPropagation` truncates a `Coalesce` after its first non-nullable child, so a fallback such as `coalesce(element_at(arr, idx), 'z')` is removed. The null then lands in a column the plan treats as non-nullable, so the row carries no null marker and readers see whatever is in the slot: the query silently returns a wrong element (e.g. `'b'` instead of `'z'`) or fails with `INVALID_ARRAY_INDEX`.
    
    ### Does this PR introduce _any_ user-facing change?
    Yes, a correctness fix. With `spark.sql.ansi.enabled=true`, `SELECT coalesce(element_at(array('a', 'b'), CAST(nullif(id, 0) AS INT)), 'z') FROM range(2)` previously returned `b, a`; it now returns `z, a`. `SELECT coalesce(array('a', 'b')[CAST(nullif(id, 0) AS INT)], 'z') FROM range(2)` previously failed with `INVALID_ARRAY_INDEX` (index -1); it now returns `z, b`.
    
    `element_at` / `arr[idx]` with a nullable index now report a nullable result type under ANSI, matching non-ANSI mode. Affects released versions since Spark 3.1 (SPARK-33386).
    
    ### How was this patch tested?
    New assertions in `CollectionExpressionsSuite` (`ElementAt`) and `ComplexTypeSuite` (`GetArrayItem`): a non-null array with a nullable index is nullable, and with a non-nullable index stays non-nullable under ANSI.
    
    New end-to-end test in `DataFrameFunctionsSuite` checking that `coalesce(element_at(...), 'z')` and `coalesce(arr[...], 'z')` return `'z'` for a null index under ANSI. All three tests fail without the fix and pass with it (`build/sbt 'catalyst/testOnly *ComplexTypeSuite *CollectionExpressionsSuite'`, `build/sbt 'sql/testOnly *DataFrameFunctionsSuite -- -z SPARK-59992'`).
    
    ### Was this patch authored or co-authored using generative AI tooling?
    Generated-by: Claude Opus 5.5
    
    Closes #59244 from richardc-db/SPARK-59992-element-at-nullable-ordinal.
    
    Authored-by: richardc-db <r....@databricks.com>
    Signed-off-by: Gengliang Wang <ge...@apache.org>
---
 .../sql/catalyst/expressions/complexTypeExtractors.scala      |  2 +-
 .../sql/catalyst/expressions/CollectionExpressionsSuite.scala |  5 +++++
 .../spark/sql/catalyst/expressions/ComplexTypeSuite.scala     |  8 ++++++++
 .../scala/org/apache/spark/sql/DataFrameFunctionsSuite.scala  | 11 +++++++++++
 4 files changed, 25 insertions(+), 1 deletion(-)

diff --git a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/complexTypeExtractors.scala b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/complexTypeExtractors.scala
index 2f9c1e2dd704..7b824ea3c69d 100644
--- a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/complexTypeExtractors.scala
+++ b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/complexTypeExtractors.scala
@@ -478,7 +478,7 @@ trait GetArrayItemUtil {
           true
       }
     } else {
-      if (failOnError) arrayElementNullable || child.nullable else true
+      if (failOnError) arrayElementNullable || child.nullable || ordinal.nullable else true
     }
   }
 }
diff --git a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/CollectionExpressionsSuite.scala b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/CollectionExpressionsSuite.scala
index f3e868cdfafd..211a47282cb9 100644
--- a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/CollectionExpressionsSuite.scala
+++ b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/CollectionExpressionsSuite.scala
@@ -2477,6 +2477,11 @@ class CollectionExpressionsSuite
         assert(ElementAt(array, Literal(-1)).nullable)
         assert(!ElementAt(array, Subtract(Literal(2), Literal(1))).nullable)
         assert(ElementAt(array, AttributeReference("ordinal", IntegerType)()).nullable)
+        // A null ordinal returns null even when neither the array nor its elements can be null.
+        val nonNullArray = CreateArray(a :: Nil)
+        assert(ElementAt(nonNullArray, AttributeReference("ordinal", IntegerType)()).nullable)
+        assert(ElementAt(nonNullArray,
+          AttributeReference("ordinal", IntegerType, nullable = false)()).nullable == !ansiEnabled)
 
         // CreateArray case invalid indices
         assert(!ElementAt(array, Literal(0)).nullable)
diff --git a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/ComplexTypeSuite.scala b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/ComplexTypeSuite.scala
index abc28ff203f8..deed423b71c5 100644
--- a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/ComplexTypeSuite.scala
+++ b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/ComplexTypeSuite.scala
@@ -105,6 +105,14 @@ class ComplexTypeSuite extends SparkFunSuite with ExpressionEvalHelper {
     assert(GetArrayItem(array, Literal(1)).nullable)
     assert(!GetArrayItem(array, Subtract(Literal(2), Literal(2))).nullable)
     assert(GetArrayItem(array, AttributeReference("ordinal", IntegerType)()).nullable)
+    // A null ordinal returns null even when neither the array nor its elements can be null.
+    val nonNullArray = CreateArray(a :: Nil)
+    val nullableOrdinal = AttributeReference("ordinal", IntegerType)()
+    val nonNullOrdinal = AttributeReference("ordinal", IntegerType, nullable = false)()
+    Seq(true, false).foreach { failOnError =>
+      assert(GetArrayItem(nonNullArray, nullableOrdinal, failOnError).nullable)
+      assert(GetArrayItem(nonNullArray, nonNullOrdinal, failOnError).nullable == !failOnError)
+    }
 
     // GetArrayStructFields case
     val f1 = StructField("a", IntegerType, nullable = false)
diff --git a/sql/core/src/test/scala/org/apache/spark/sql/DataFrameFunctionsSuite.scala b/sql/core/src/test/scala/org/apache/spark/sql/DataFrameFunctionsSuite.scala
index 79ceb6782053..504a84668068 100644
--- a/sql/core/src/test/scala/org/apache/spark/sql/DataFrameFunctionsSuite.scala
+++ b/sql/core/src/test/scala/org/apache/spark/sql/DataFrameFunctionsSuite.scala
@@ -2536,6 +2536,17 @@ class DataFrameFunctionsSuite extends SharedSparkSession {
     )
   }
 
+  test("SPARK-59992: coalesce over element_at with a null index is not simplified away") {
+    // Under ANSI, element_at was marked non-nullable for a non-null array regardless of the
+    // index, so the optimizer dropped the coalesce and the null index leaked through.
+    withSQLConf(SQLConf.ANSI_ENABLED.key -> "true") {
+      checkAnswer(
+        sql("SELECT coalesce(element_at(array('a', 'b'), CAST(nullif(id, 0) AS INT)), 'z'), " +
+          "coalesce(array('a', 'b')[CAST(nullif(id, 0) AS INT) - 1], 'z') FROM range(2)"),
+        Seq(Row("z", "z"), Row("a", "a")))
+    }
+  }
+
   test("element_at function") {
     val df = Seq(
       (Seq[String]("1", "2", "3"), 1),


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@spark.apache.org
For additional commands, e-mail: commits-help@spark.apache.org