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 2025/11/22 23:36:00 UTC

(spark) branch branch-4.1 updated: [SPARK-53635][SQL] Support Scala UDFs with input args of type Seq[Row]

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

dongjoon pushed a commit to branch branch-4.1
in repository https://gitbox.apache.org/repos/asf/spark.git


The following commit(s) were added to refs/heads/branch-4.1 by this push:
     new 36e056e0f9f0 [SPARK-53635][SQL] Support Scala UDFs with input args of type Seq[Row]
36e056e0f9f0 is described below

commit 36e056e0f9f0e4c9b2ae4cb41428b8c640d9bff5
Author: jameswillis <ja...@wherobots.com>
AuthorDate: Sat Nov 22 15:35:41 2025 -0800

    [SPARK-53635][SQL] Support Scala UDFs with input args of type Seq[Row]
    
    ### What changes were proposed in this pull request?
    The refactors of encoders in Spark 4 broke support for Scala UDFs that take in an argument of type `Seq[Row]`. This change restores the Spark 3.5 behavior of passing None for the encoder for arguments of that type into `SparkUserDefinedFunction`.
    
    ### Why are the changes needed?
    This change resolves issue SPARK-53635. This will help scala UDF users migrate more smoothly to Spark 4 and maintain backwards compatability for UDFs in Scala.
    
    ### Does this PR introduce _any_ user-facing change?
    Yes. It makes Spark 4 behave like Spark 3 when faced with a Scala UDF with an input argument of type `Seq[Row]`
    
    ### How was this patch tested?
    Existing and new unit tests.
    
    ### Was this patch authored or co-authored using generative AI tooling?
    Generated-by: Copilot/GPT-5
    
    Closes #53027 from james-willis/james-willis/SPARK53635.
    
    Authored-by: jameswillis <ja...@wherobots.com>
    Signed-off-by: Dongjoon Hyun <do...@apache.org>
    (cherry picked from commit ea74e57b260d6549f4e05303f08feb01997ce93a)
    Signed-off-by: Dongjoon Hyun <do...@apache.org>
---
 .../spark/sql/expressions/UserDefinedFunction.scala       | 15 ++++++++++++---
 .../org/apache/spark/sql/DataFrameComplexTypeSuite.scala  |  8 ++++++++
 2 files changed, 20 insertions(+), 3 deletions(-)

diff --git a/sql/api/src/main/scala/org/apache/spark/sql/expressions/UserDefinedFunction.scala b/sql/api/src/main/scala/org/apache/spark/sql/expressions/UserDefinedFunction.scala
index 6a22cbfaf351..d6da8ce17e83 100644
--- a/sql/api/src/main/scala/org/apache/spark/sql/expressions/UserDefinedFunction.scala
+++ b/sql/api/src/main/scala/org/apache/spark/sql/expressions/UserDefinedFunction.scala
@@ -17,11 +17,11 @@
 
 package org.apache.spark.sql.expressions
 
-import scala.reflect.runtime.universe.TypeTag
+import scala.reflect.runtime.universe.{typeOf, TypeTag}
 import scala.util.Try
 
 import org.apache.spark.annotation.Stable
-import org.apache.spark.sql.{Column, Encoder}
+import org.apache.spark.sql.{Column, Encoder, Row}
 import org.apache.spark.sql.catalyst.ScalaReflection
 import org.apache.spark.sql.catalyst.encoders.AgnosticEncoder
 import org.apache.spark.sql.internal.{InvokeInlineUserDefinedFunction, UserDefinedFunctionLike}
@@ -130,8 +130,17 @@ object SparkUserDefinedFunction {
       returnTypeTag: TypeTag[_],
       inputTypeTags: TypeTag[_]*): SparkUserDefinedFunction = {
     val outputEncoder = ScalaReflection.encoderFor(returnTypeTag)
+    // Return None for any input that is a Seq[Row].
+    // This replicates the Spark 3 behavior of passing None as the encoder for such inputs.
+    val seqRowType = typeOf[Seq[_]]
+    val rowType = typeOf[Row]
     val inputEncoders = inputTypeTags.map { tag =>
-      Try(ScalaReflection.encoderFor(tag)).toOption
+      val tpe = tag.tpe
+      if (tpe <:< seqRowType && tpe.typeArgs.nonEmpty && tpe.typeArgs.head =:= rowType) {
+        None
+      } else {
+        Try(ScalaReflection.encoderFor(tag)).toOption
+      }
     }
     SparkUserDefinedFunction(
       f = function,
diff --git a/sql/core/src/test/scala/org/apache/spark/sql/DataFrameComplexTypeSuite.scala b/sql/core/src/test/scala/org/apache/spark/sql/DataFrameComplexTypeSuite.scala
index 8024b579e5d0..984ff8fc51dd 100644
--- a/sql/core/src/test/scala/org/apache/spark/sql/DataFrameComplexTypeSuite.scala
+++ b/sql/core/src/test/scala/org/apache/spark/sql/DataFrameComplexTypeSuite.scala
@@ -71,6 +71,14 @@ class DataFrameComplexTypeSuite extends QueryTest with SharedSparkSession {
     df.select(map($"a", $"b").as("s")).select(f($"s".getItem("a"))).collect()
   }
 
+  test("SPARK-53635 array of structure to Seq[Row]") {
+    val f = udf((items: Seq[Row], num: Int) => items.length + num)
+    val df = spark.sql("SELECT array(struct(1 as a), struct(2 as a), struct(3 as a)) as items")
+    val result = df.select(f($"items", lit(3)).alias("countNum")).collect()
+    assert(result.length == 1)
+    assert(result(0).getAs[Int]("countNum") == 6)
+  }
+
   test("SPARK-12477 accessing null element in array field") {
     val df = sparkContext.parallelize(Seq((Seq("val1", null, "val2"),
       Seq(Some(1), None, Some(2))))).toDF("s", "i")


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