You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@spark.apache.org by we...@apache.org on 2025/03/24 13:30:14 UTC

(spark) branch master updated: [SPARK-51576][SQL] Prohibit ANSI cast from `array` to `array<_, containsNull = false>`

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

wenchen 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 084143b977a9 [SPARK-51576][SQL] Prohibit ANSI cast from `array<variant, containsNull = false>` to `array<_, containsNull = false>`
084143b977a9 is described below

commit 084143b977a9c1a8eee50aea77125f29fa5267d0
Author: Harsh Motwani <ha...@databricks.com>
AuthorDate: Mon Mar 24 21:29:58 2025 +0800

    [SPARK-51576][SQL] Prohibit ANSI cast from `array<variant, containsNull = false>` to `array<_, containsNull = false>`
    
    ### What changes were proposed in this pull request?
    
    This PR prohibits ANSI cast from `ArrayType(VariantType, containsNull = false)` to `ArrayType(_, containsNull = false)`, or `StructField(VariantType, nullable = false)` to `StructField(_, nullable = true)` or `MapType(_, VariantType, valueContainsNull = false)` to `MapType(_, _, valueContainsNull = false)`.
    
    ### Why are the changes needed?
    
    Variant is the only data type which can be ANSI cast from a non-null value to a null value. More specifically, variant nulls (generated using `parse_json('null')`) when cast to string result in `null`.
    However, `array<variant, containsNull = false>` can be cast to `array<string, containsNull = true>`. This should not be allowed since `array(parse_json('null'))` would give `array(null)` after this cast.
    Here is a demonstration of this problem where we create an `array<string, containsNull = true>` which in fact does not contain nulls:
    ```
    >>> from pyspark.sql.functions import col
    >>> from pyspark.sql.types import StringType, VariantType, ArrayType
    >>> 
    >>> df = spark.sql("select array(parse_json('null')) arr")
    >>> df.printSchema()
    root
     |-- arr: array (nullable = false)
     |    |-- element: variant (containsNull = false)
    
    >>> df2 = df.select(col('arr').cast(ArrayType(StringType(), False)))
    >>> df2.selectExpr("arr[0] is null").show()
    +----------------+
    |(arr[0] IS NULL)|
    +----------------+
    |            true|
    +----------------+
    
    >>> df2.printSchema()
    root
     |-- arr: array (nullable = false)
     |    |-- element: string (containsNull = false)
    ```
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes, users can no longer ANSI cast from `array<variant, containsNull = false>` to `array<_, containsNull = false>`. However, it is unlikely that users were hardcoding `containsNull = false`. The same is the case for struct field and maps.
    
    ### How was this patch tested?
    
    Unit tests making sure that only `containsNull = false` cases fail.
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    No.
    
    Closes #50343 from harshmotw-db/harsh-motwani_data/variant_cast_fix.
    
    Authored-by: Harsh Motwani <ha...@databricks.com>
    Signed-off-by: Wenchen Fan <we...@databricks.com>
---
 .../spark/sql/catalyst/expressions/Cast.scala      | 10 ++--
 .../catalyst/expressions/CastWithAnsiOnSuite.scala | 54 +++++++++++++++++++++-
 2 files changed, 60 insertions(+), 4 deletions(-)

diff --git a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/Cast.scala b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/Cast.scala
index f35e9e42927e..7a4145933fc7 100644
--- a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/Cast.scala
+++ b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/Cast.scala
@@ -134,19 +134,23 @@ object Cast extends QueryErrorsBase {
     // to convert data of these types to Variant Objects.
     case (_, VariantType) => variant.VariantGet.checkDataType(from, allowStructsAndMaps = false)
 
+    // non-null variants can generate nulls even in ANSI mode
     case (ArrayType(fromType, fn), ArrayType(toType, tn)) =>
-      canAnsiCast(fromType, toType) && resolvableNullability(fn, tn)
+      canAnsiCast(fromType, toType) && resolvableNullability(fn || (fromType == VariantType), tn)
 
+    // non-null variants can generate nulls even in ANSI mode
     case (MapType(fromKey, fromValue, fn), MapType(toKey, toValue, tn)) =>
       canAnsiCast(fromKey, toKey) && canAnsiCast(fromValue, toValue) &&
-        resolvableNullability(fn, tn)
+        resolvableNullability(fn || (fromValue == VariantType), tn)
 
+    // non-null variants can generate nulls even in ANSI mode
     case (StructType(fromFields), StructType(toFields)) =>
       fromFields.length == toFields.length &&
         fromFields.zip(toFields).forall {
           case (fromField, toField) =>
             canAnsiCast(fromField.dataType, toField.dataType) &&
-              resolvableNullability(fromField.nullable, toField.nullable)
+              resolvableNullability(fromField.nullable || (fromField.dataType == VariantType),
+                toField.nullable)
         }
 
     case (udt1: UserDefinedType[_], udt2: UserDefinedType[_]) if udt2.acceptsType(udt1) => true
diff --git a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/CastWithAnsiOnSuite.scala b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/CastWithAnsiOnSuite.scala
index 340a5f30112e..b62b8c1302cc 100644
--- a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/CastWithAnsiOnSuite.scala
+++ b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/CastWithAnsiOnSuite.scala
@@ -23,13 +23,14 @@ import java.time.DateTimeException
 import org.apache.spark.{SparkArithmeticException, SparkRuntimeException}
 import org.apache.spark.sql.Row
 import org.apache.spark.sql.catalyst.InternalRow
+import org.apache.spark.sql.catalyst.analysis.TypeCheckResult
 import org.apache.spark.sql.catalyst.analysis.TypeCheckResult.DataTypeMismatch
 import org.apache.spark.sql.catalyst.util.DateTimeConstants.MILLIS_PER_SECOND
 import org.apache.spark.sql.catalyst.util.DateTimeTestUtils
 import org.apache.spark.sql.catalyst.util.DateTimeTestUtils.{withDefaultTimeZone, UTC}
 import org.apache.spark.sql.errors.QueryErrorsBase
 import org.apache.spark.sql.types._
-import org.apache.spark.unsafe.types.UTF8String
+import org.apache.spark.unsafe.types.{UTF8String, VariantVal}
 
 /**
  * Test suite for data type casting expression [[Cast]] with ANSI mode enabled.
@@ -182,6 +183,57 @@ class CastWithAnsiOnSuite extends CastSuiteBase with QueryErrorsBase {
     }
   }
 
+  test("ANSI mode: disallow variant cast to non-nullable types") {
+    // Array
+    val variantVal = new VariantVal(Array[Byte](12, 3), Array[Byte](1, 0, 0))
+    val sourceArrayType = ArrayType(VariantType, containsNull = false)
+    val targetArrayType = ArrayType(StringType, containsNull = false)
+    val variantArray = Literal.create(Seq(variantVal), sourceArrayType)
+    assert(cast(variantArray, targetArrayType).checkInputDataTypes() == DataTypeMismatch(
+      errorSubClass = "CAST_WITHOUT_SUGGESTION",
+      messageParameters = Map(
+        "srcType" -> toSQLType(sourceArrayType),
+        "targetType" -> toSQLType(targetArrayType)
+      )
+    ))
+    // make sure containsNull = true works
+    val targetArrayType2 = ArrayType(StringType, containsNull = true)
+    assert(cast(variantArray, targetArrayType2).checkInputDataTypes() ==
+      TypeCheckResult.TypeCheckSuccess)
+
+    // Struct
+    val sourceStructType = StructType(Array(StructField("v", VariantType, nullable = false)))
+    val targetStructType = StructType(Array(StructField("v", StringType, nullable = false)))
+    val variantStruct = Literal.create(Row(variantVal), sourceStructType)
+    assert(cast(variantStruct, targetStructType).checkInputDataTypes() == DataTypeMismatch(
+      errorSubClass = "CAST_WITHOUT_SUGGESTION",
+      messageParameters = Map(
+        "srcType" -> toSQLType(sourceStructType),
+        "targetType" -> toSQLType(targetStructType)
+      )
+    ))
+    // make sure nullable = true works
+    val targetStructType2 = StructType(Array(StructField("v", StringType, nullable = true)))
+    assert(cast(variantStruct, targetStructType2).checkInputDataTypes() ==
+      TypeCheckResult.TypeCheckSuccess)
+
+    // Map
+    val sourceMapType = MapType(StringType, VariantType, valueContainsNull = false)
+    val targetMapType = MapType(StringType, StringType, valueContainsNull = false)
+    val variantMap = Literal.create(Map("k" -> variantVal), sourceMapType)
+    assert(cast(variantMap, targetMapType).checkInputDataTypes() == DataTypeMismatch(
+      errorSubClass = "CAST_WITHOUT_SUGGESTION",
+      messageParameters = Map(
+        "srcType" -> toSQLType(sourceMapType),
+        "targetType" -> toSQLType(targetMapType)
+      )
+    ))
+    // make sure valueContainsNull = true works
+    val targetMapType2 = MapType(StringType, StringType, valueContainsNull = true)
+    assert(cast(variantMap, targetMapType2).checkInputDataTypes() ==
+      TypeCheckResult.TypeCheckSuccess)
+  }
+
   test("ANSI mode: disallow type conversions between Datatime types and Boolean types") {
     val timestampLiteral = Literal(1L, TimestampType)
     val checkResult1 = cast(timestampLiteral, BooleanType).checkInputDataTypes()


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