This is an automated email from the ASF dual-hosted git repository.
zhengruifeng 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 9299b130825d [SPARK-58875][SQL][ML][PYTHON][CONNECT] Add WrapUDT
expression for zero-copy linalg conversion
9299b130825d is described below
commit 9299b130825deb4d80b4835001a0cb96f6a09094
Author: Ruifeng Zheng <[email protected]>
AuthorDate: Mon Aug 24 15:06:46 2026 +0800
[SPARK-58875][SQL][ML][PYTHON][CONNECT] Add WrapUDT expression for
zero-copy linalg conversion
### What changes were proposed in this pull request?
This PR adds a Catalyst `WrapUDT` expression that wraps a column with a
target UDT when the
column data type matches the UDT's underlying SQL type. `WrapUDT` is paired
with the existing
`UnwrapUDT`, so callers can convert between a UDT column and its underlying
Catalyst value without
copying the data.
It exposes `wrap_udt` through the Scala SQL functions API and PySpark SQL
functions, including
Spark Connect. The Scala API includes both `wrap_udt(Column,
UserDefinedType[_])` and a
`wrap_udt(Column, Column)` overload so Python can pass the target UDT JSON
as a literal column,
similar to schema handling in `from_json`.
This PR also adds ML-side tests for zero-copy conversions across `.ml` and
`.mllib` vector and
matrix UDTs by composing `unwrap_udt` and `wrap_udt`.
### Why are the changes needed?
Spark ML has parallel `.ml` and `.mllib` linalg UDTs whose serialized SQL
layouts can match even
when the logical UDT classes differ. Existing conversion paths often
deserialize the UDT value and
create a new vector or matrix object even when only the logical UDT wrapper
needs to change.
This UDT deserialization and object-copying cost is a long-standing issue
from the first day of the
DataFrame-based `.ml` APIs: ML pipelines exchange linalg values through
DataFrame UDT columns, and
moving values across `.ml` and `.mllib` UDT boundaries has historically
gone through object
conversion even when the Catalyst payload already has the right layout.
`WrapUDT` provides a typed Catalyst expression for changing the logical UDT
over an already matching
underlying SQL value. This enables zero-copy linalg conversions and avoids
per-row object conversion
for these cases.
### Does this PR introduce _any_ user-facing change?
Yes. It adds a public `wrap_udt` SQL function API in Scala and PySpark,
with Spark Connect support.
The function wraps a column as a target UDT when the column's data type
matches the UDT's underlying
SQL type. Existing `unwrap_udt` behavior is unchanged.
For example, in PySpark:
```python
from pyspark.sql import Row, functions as sf
from pyspark.sql.types import StructField, StructType
from pyspark.ml.linalg import VectorUDT
schema = StructType([StructField("vec", VectorUDT.sqlType(), True)])
df = spark.createDataFrame(
[(Row(type=1, size=None, indices=None, values=[1.0, 2.0, 3.0]),)],
schema)
df.select("*", sf.wrap_udt("vec", VectorUDT())).show()
```
Output:
```text
+--------------------+-------------+
| vec|wrap_udt(vec)|
+--------------------+-------------+
|{1, NULL, NULL, [...|[1.0,2.0,3.0]|
+--------------------+-------------+
```
### How was this patch tested?
Added and updated tests for:
- Catalyst `WrapUDT` positive and negative type checking.
- SQL `wrap_udt` and `unwrap_udt` round trips.
- ML vector and matrix UDT conversions between `.ml` and `.mllib` linalg
types.
- PySpark `wrap_udt` examples for `VectorUDT` and `MatrixUDT`.
- Spark Connect JVM plan generation and golden files for `wrap_udt`.
Ran:
```
git diff --check upstream/master...HEAD
conda run -n spark-dev-313 python -m py_compile
python/pyspark/sql/functions/builtin.py
build/sbt -java-home /usr/lib/jvm/java-17-openjdk-amd64 sql-api/compile
build/sbt -java-home /usr/lib/jvm/java-17-openjdk-amd64
"connect-client-jvm/testOnly org.apache.spark.sql.PlanGenerationTestSuite -- -z
wrap_udt"
```
### Was this patch authored or co-authored using generative AI tooling?
Generated-by: OpenAI Codex (GPT-5)
Closes #58124 from zhengruifeng/spark-58875-wrap-udt.
Authored-by: Ruifeng Zheng <[email protected]>
Signed-off-by: Ruifeng Zheng <[email protected]>
---
.../scala/org/apache/spark/ml/FunctionsSuite.scala | 128 ++++++++++++++++++++-
.../source/reference/pyspark.sql/functions.rst | 1 +
python/pyspark/ml/tests/test_linalg.py | 106 ++++++++++++++++-
python/pyspark/sql/connect/functions/builtin.py | 24 ++++
python/pyspark/sql/functions/__init__.py | 1 +
python/pyspark/sql/functions/builtin.py | 109 ++++++++++++++++++
.../scala/org/apache/spark/sql/functions.scala | 26 +++++
.../sql/catalyst/analysis/FunctionRegistry.scala | 1 +
.../spark/sql/catalyst/expressions/UnwrapUDT.scala | 2 +
.../expressions/{UnwrapUDT.scala => WrapUDT.scala} | 46 ++++++--
.../expressions/WrapUDTExpressionSuite.scala | 115 ++++++++++++++++++
.../apache/spark/sql/PlanGenerationTestSuite.scala | 10 ++
.../test/scala/org/apache/spark/sql/TestUDT.scala | 42 +++++++
.../explain-results/function_unwrap_udt.explain | 2 +
.../explain-results/function_wrap_udt.explain | 2 +
.../query-tests/queries/function_unwrap_udt.json | 127 ++++++++++++++++++++
.../queries/function_unwrap_udt.proto.bin | Bin 0 -> 1281 bytes
.../query-tests/queries/function_wrap_udt.json | 104 +++++++++++++++++
.../queries/function_wrap_udt.proto.bin | Bin 0 -> 1077 bytes
.../test/scala/org/apache/spark/sql/TestUDT.scala | 42 +++++++
.../apache/spark/sql/DataFrameFunctionsSuite.scala | 1 +
.../apache/spark/sql/UserDefinedTypeSuite.scala | 29 ++++-
22 files changed, 903 insertions(+), 15 deletions(-)
diff --git a/mllib/src/test/scala/org/apache/spark/ml/FunctionsSuite.scala
b/mllib/src/test/scala/org/apache/spark/ml/FunctionsSuite.scala
index 7fcb1d2fbfbb..4608d9bd683d 100644
--- a/mllib/src/test/scala/org/apache/spark/ml/FunctionsSuite.scala
+++ b/mllib/src/test/scala/org/apache/spark/ml/FunctionsSuite.scala
@@ -19,16 +19,50 @@ package org.apache.spark.ml
import org.apache.spark.SparkException
import org.apache.spark.ml.functions._
-import org.apache.spark.ml.linalg.{Vector, Vectors}
+import org.apache.spark.ml.linalg.{Matrices, MatrixUDT, Vector, Vectors,
VectorUDT}
import org.apache.spark.ml.util.MLTest
-import org.apache.spark.mllib.linalg.{Vectors => OldVectors}
-import org.apache.spark.sql.AnalysisException
-import org.apache.spark.sql.functions.col
+import org.apache.spark.mllib.linalg.{Matrices => OldMatrices, MatrixUDT =>
OldMatrixUDT,
+ Vectors => OldVectors, VectorUDT => OldVectorUDT}
+import org.apache.spark.sql.{AnalysisException, DataFrame, Row}
+import org.apache.spark.sql.functions.{col, unwrap_udt, wrap_udt}
+import org.apache.spark.sql.types.{StructField, StructType, UserDefinedType}
class FunctionsSuite extends MLTest {
import testImplicits._
+ private def checkWrapUDTConversion(
+ df: DataFrame,
+ targetUDT: UserDefinedType[_],
+ expected: Any): Unit = {
+ val converted = df.select(wrap_udt(unwrap_udt(col("value")),
targetUDT).as("value"))
+ assert(converted.schema("value").dataType === targetUDT)
+ assert(converted.first().get(0) === expected)
+ }
+
+ private def checkWrapUDTTypeMismatch(df: DataFrame, targetUDT:
UserDefinedType[_]): Unit = {
+ val e = intercept[AnalysisException] {
+ df.select(wrap_udt(unwrap_udt(col("value")),
targetUDT).as("value")).collect()
+ }
+ assert(e.getCondition === "DATATYPE_MISMATCH.UNEXPECTED_INPUT_TYPE")
+ }
+
+ private def checkNullableWrapUDTConversion(
+ value: Any,
+ sourceUDT: UserDefinedType[_],
+ targetUDT: UserDefinedType[_],
+ expected: Any): Unit = {
+ val schema = StructType(Seq(StructField("value", sourceUDT, nullable =
true)))
+ val df = spark.createDataFrame(
+ spark.sparkContext.parallelize(Seq(Row(value), Row(null))),
+ schema)
+ val converted = df.select(wrap_udt(unwrap_udt(col("value")),
targetUDT).as("value"))
+
+ assert(converted.schema("value").dataType === targetUDT)
+ assert(converted.schema("value").nullable)
+ assert(converted.collect().map(_.get(0)).toSeq === Seq(expected, null))
+ }
+
test("test vector_to_array") {
val df = Seq(
(Vectors.dense(1.0, 2.0, 3.0), OldVectors.dense(10.0, 20.0, 30.0)),
@@ -130,4 +164,90 @@ class FunctionsSuite extends MLTest {
val result = df.select(array_argmax(col("arr"))).as[Int].collect()
assert(result === Array(2, 1, 0, 1, 0, -1))
}
+
+ test("wrap and unwrap vector and matrix UDT columns") {
+ val oldVector = OldVectors.sparse(3, Array(1), Array(2.0))
+ val oldVectorDF = Seq(Tuple1(oldVector)).toDF("value")
+ checkWrapUDTConversion(
+ oldVectorDF,
+ new OldVectorUDT,
+ oldVector)
+ checkWrapUDTConversion(
+ oldVectorDF,
+ new VectorUDT,
+ oldVector.asML)
+ checkWrapUDTTypeMismatch(
+ oldVectorDF,
+ new OldMatrixUDT)
+ checkWrapUDTTypeMismatch(
+ oldVectorDF,
+ new MatrixUDT)
+
+ val mlVector = Vectors.dense(1.0, 2.0)
+ val mlVectorDF = Seq(Tuple1(mlVector)).toDF("value")
+ checkWrapUDTConversion(
+ mlVectorDF,
+ new VectorUDT,
+ mlVector)
+ checkWrapUDTConversion(
+ mlVectorDF,
+ new OldVectorUDT,
+ OldVectors.fromML(mlVector))
+ checkWrapUDTTypeMismatch(
+ mlVectorDF,
+ new OldMatrixUDT)
+ checkWrapUDTTypeMismatch(
+ mlVectorDF,
+ new MatrixUDT)
+
+ val oldMatrix = OldMatrices.dense(2, 2, Array(1.0, 2.0, 3.0, 4.0))
+ val oldMatrixDF = Seq(Tuple1(oldMatrix)).toDF("value")
+ checkWrapUDTConversion(
+ oldMatrixDF,
+ new OldMatrixUDT,
+ oldMatrix)
+ checkWrapUDTConversion(
+ oldMatrixDF,
+ new MatrixUDT,
+ oldMatrix.asML)
+ checkWrapUDTTypeMismatch(
+ oldMatrixDF,
+ new OldVectorUDT)
+ checkWrapUDTTypeMismatch(
+ oldMatrixDF,
+ new VectorUDT)
+
+ val mlMatrix = Matrices.dense(2, 2, Array(1.0, 2.0, 3.0, 4.0))
+ val mlMatrixDF = Seq(Tuple1(mlMatrix)).toDF("value")
+ checkWrapUDTConversion(
+ mlMatrixDF,
+ new MatrixUDT,
+ mlMatrix)
+ checkWrapUDTConversion(
+ mlMatrixDF,
+ new OldMatrixUDT,
+ OldMatrices.fromML(mlMatrix))
+ checkWrapUDTTypeMismatch(
+ mlMatrixDF,
+ new OldVectorUDT)
+ checkWrapUDTTypeMismatch(
+ mlMatrixDF,
+ new VectorUDT)
+ }
+
+ test("wrap and unwrap nullable vector UDT columns") {
+ val oldVector = OldVectors.sparse(3, Array(1), Array(2.0))
+ checkNullableWrapUDTConversion(
+ oldVector,
+ new OldVectorUDT,
+ new VectorUDT,
+ oldVector.asML)
+
+ val mlVector = Vectors.dense(1.0, 2.0)
+ checkNullableWrapUDTConversion(
+ mlVector,
+ new VectorUDT,
+ new OldVectorUDT,
+ OldVectors.fromML(mlVector))
+ }
}
diff --git a/python/docs/source/reference/pyspark.sql/functions.rst
b/python/docs/source/reference/pyspark.sql/functions.rst
index 031c48734c49..94c5a9a87e13 100644
--- a/python/docs/source/reference/pyspark.sql/functions.rst
+++ b/python/docs/source/reference/pyspark.sql/functions.rst
@@ -779,6 +779,7 @@ UDF, UDTF and UDT
udf
udtf
unwrap_udt
+ wrap_udt
Table-Valued Functions
diff --git a/python/pyspark/ml/tests/test_linalg.py
b/python/pyspark/ml/tests/test_linalg.py
index 17185ad7b5fd..eea293188ef2 100644
--- a/python/pyspark/ml/tests/test_linalg.py
+++ b/python/pyspark/ml/tests/test_linalg.py
@@ -19,6 +19,7 @@ import array as pyarray
from numpy import arange, array, array_equal, inf, ones, tile, zeros
+from pyspark.errors import AnalysisException
from pyspark.serializers import CPickleSerializer
from pyspark.ml.linalg import (
DenseMatrix,
@@ -32,7 +33,8 @@ from pyspark.ml.linalg import (
)
from pyspark.testing.mllibutils import MLlibTestCase
from pyspark.sql import Row
-from pyspark.sql.functions import unwrap_udt
+from pyspark.sql.functions import lit, unwrap_udt, wrap_udt
+from pyspark.sql.types import StructField, StructType
class VectorTests(MLlibTestCase):
@@ -367,6 +369,49 @@ class VectorUDTTests(MLlibTestCase):
]
self.assertEqual(results, expected)
+ def test_wrap_udt(self):
+ schema = StructType([StructField("vec", VectorUDT.sqlType(), True)])
+ vector_struct = Row("type", "size", "indices", "values")
+ df = self.spark.createDataFrame(
+ [
+ (vector_struct(1, None, None, [1.0, 2.0]),),
+ (vector_struct(0, 2, [1], [2.0]),),
+ ],
+ schema,
+ )
+ wrapped = df.select(wrap_udt("vec", VectorUDT()).alias("vec"))
+
+ self.assertEqual(wrapped.schema["vec"].dataType, VectorUDT())
+ self.assertEqual(wrapped.collect(), [Row(vec=self.dv1),
Row(vec=self.sv1)])
+
+ def test_wrap_udt_type_mismatch(self):
+ schema = StructType([StructField("vec", VectorUDT.sqlType(), True)])
+ vector_struct = Row("type", "size", "indices", "values")
+ df = self.spark.createDataFrame(
+ [(vector_struct(1, None, None, [1.0, 2.0]),)],
+ schema,
+ )
+
+ with self.assertRaises(AnalysisException) as context:
+ df.select(wrap_udt("vec", MatrixUDT())).collect()
+ self.assertEqual(
+ context.exception.getCondition(),
"DATATYPE_MISMATCH.UNEXPECTED_INPUT_TYPE"
+ )
+
+ def test_wrap_unwrap_udt_round_trip(self):
+ df = self.spark.createDataFrame([(self.dv1,), (self.sv1,)], ["vec"])
+ round_trip = df.select(wrap_udt(unwrap_udt("vec"),
VectorUDT()).alias("vec"))
+
+ self.assertEqual(round_trip.schema["vec"].dataType, VectorUDT())
+ self.assertEqual(round_trip.collect(), [Row(vec=self.dv1),
Row(vec=self.sv1)])
+
+ def test_wrap_unwrap_udt_round_trip_with_udt_column(self):
+ df = self.spark.createDataFrame([(self.dv1,), (self.sv1,)], ["vec"])
+ round_trip = df.select(wrap_udt(unwrap_udt("vec"),
lit(VectorUDT().json())).alias("vec"))
+
+ self.assertEqual(round_trip.schema["vec"].dataType, VectorUDT())
+ self.assertEqual(round_trip.collect(), [Row(vec=self.dv1),
Row(vec=self.sv1)])
+
def test_hashable(self):
_ = hash(VectorUDT())
@@ -404,6 +449,65 @@ class MatrixUDTTests(MLlibTestCase):
else:
raise ValueError("Expected a matrix but got type %r" % type(m))
+ def test_wrap_udt(self):
+ schema = StructType([StructField("mat", MatrixUDT.sqlType(), True)])
+ matrix_struct = Row(
+ "type",
+ "numRows",
+ "numCols",
+ "colPtrs",
+ "rowIndices",
+ "values",
+ "isTransposed",
+ )
+ df = self.spark.createDataFrame(
+ [
+ (matrix_struct(1, 3, 2, None, None, [0.0, 1.0, 4.0, 5.0, 9.0,
10.0], False),),
+ (matrix_struct(0, 1, 1, [0, 1], [0], [2.0], False),),
+ ],
+ schema,
+ )
+ wrapped = df.select(wrap_udt("mat", MatrixUDT()).alias("mat"))
+
+ self.assertEqual(wrapped.schema["mat"].dataType, MatrixUDT())
+ self.assertEqual(wrapped.collect(), [Row(mat=self.dm1),
Row(mat=self.sm1)])
+
+ def test_wrap_udt_type_mismatch(self):
+ schema = StructType([StructField("mat", MatrixUDT.sqlType(), True)])
+ matrix_struct = Row(
+ "type",
+ "numRows",
+ "numCols",
+ "colPtrs",
+ "rowIndices",
+ "values",
+ "isTransposed",
+ )
+ df = self.spark.createDataFrame(
+ [(matrix_struct(1, 3, 2, None, None, [0.0, 1.0, 4.0, 5.0, 9.0,
10.0], False),)],
+ schema,
+ )
+
+ with self.assertRaises(AnalysisException) as context:
+ df.select(wrap_udt("mat", VectorUDT())).collect()
+ self.assertEqual(
+ context.exception.getCondition(),
"DATATYPE_MISMATCH.UNEXPECTED_INPUT_TYPE"
+ )
+
+ def test_wrap_unwrap_udt_round_trip(self):
+ df = self.spark.createDataFrame([(self.dm1,), (self.sm1,)], ["mat"])
+ round_trip = df.select(wrap_udt(unwrap_udt("mat"),
MatrixUDT()).alias("mat"))
+
+ self.assertEqual(round_trip.schema["mat"].dataType, MatrixUDT())
+ self.assertEqual(round_trip.collect(), [Row(mat=self.dm1),
Row(mat=self.sm1)])
+
+ def test_wrap_unwrap_udt_round_trip_with_udt_column(self):
+ df = self.spark.createDataFrame([(self.dm1,), (self.sm1,)], ["mat"])
+ round_trip = df.select(wrap_udt(unwrap_udt("mat"),
lit(MatrixUDT().json())).alias("mat"))
+
+ self.assertEqual(round_trip.schema["mat"].dataType, MatrixUDT())
+ self.assertEqual(round_trip.collect(), [Row(mat=self.dm1),
Row(mat=self.sm1)])
+
def test_hashable(self):
_ = hash(MatrixUDT())
diff --git a/python/pyspark/sql/connect/functions/builtin.py
b/python/pyspark/sql/connect/functions/builtin.py
index 9d622925bf19..453980fa83e1 100644
--- a/python/pyspark/sql/connect/functions/builtin.py
+++ b/python/pyspark/sql/connect/functions/builtin.py
@@ -65,7 +65,11 @@ from pyspark.sql.types import (
ArrayType,
MapType,
StringType,
+ UserDefinedType as _UserDefinedType,
)
+
+if TYPE_CHECKING:
+ from pyspark.sql.types import UserDefinedType
from pyspark.sql.utils import enum_to_value as _enum_to_value
# The implementations of pandas_udf, arrow_udf and udaf are embedded in
pyspark.sql.functions
@@ -5780,6 +5784,26 @@ def unwrap_udt(col: "ColumnOrName") -> Column:
unwrap_udt.__doc__ = pysparkfuncs.unwrap_udt.__doc__
+def wrap_udt(col: "ColumnOrName", udt: "Union[UserDefinedType, Column]") ->
Column:
+ if isinstance(udt, _UserDefinedType):
+ udt_col = lit(udt.json())
+ elif isinstance(udt, Column):
+ udt_col = udt
+ else:
+ raise PySparkTypeError(
+ errorClass="NOT_EXPECTED_TYPE",
+ messageParameters={
+ "expected_type": "UserDefinedType or Column",
+ "arg_name": "udt",
+ "arg_type": type(udt).__name__,
+ },
+ )
+ return _invoke_function("wrap_udt", _to_col(col), _to_col(udt_col))
+
+
+wrap_udt.__doc__ = pysparkfuncs.wrap_udt.__doc__
+
+
def udf(
f: Optional[Union[Callable[..., Any], "DataTypeOrString"]] = None,
returnType: "DataTypeOrString" = StringType(),
diff --git a/python/pyspark/sql/functions/__init__.py
b/python/pyspark/sql/functions/__init__.py
index 462509a5d7a2..4ef3b1d5067f 100644
--- a/python/pyspark/sql/functions/__init__.py
+++ b/python/pyspark/sql/functions/__init__.py
@@ -628,4 +628,5 @@ __all__ = [ # noqa: F405
"udtf",
"arrow_udtf",
"unwrap_udt",
+ "wrap_udt",
]
diff --git a/python/pyspark/sql/functions/builtin.py
b/python/pyspark/sql/functions/builtin.py
index 5acb3301be4d..1f5d240be04d 100644
--- a/python/pyspark/sql/functions/builtin.py
+++ b/python/pyspark/sql/functions/builtin.py
@@ -52,8 +52,12 @@ from pyspark.sql.types import (
MapType,
NumericType,
_from_numpy_type,
+ UserDefinedType as _UserDefinedType,
)
+if TYPE_CHECKING:
+ from pyspark.sql.types import UserDefinedType
+
# Keep UserDefinedFunction import for backwards compatible import; moved in
SPARK-22409
from pyspark.sql.udf import UserDefinedFunction, _create_py_udf # noqa: F401
from pyspark.sql.udtf import AnalyzeArgument, AnalyzeResult # noqa: F401
@@ -29199,6 +29203,10 @@ def unwrap_udt(col: "ColumnOrName") -> Column:
:class:`~pyspark.sql.Column`
The underlying representation.
+ See Also
+ --------
+ :meth:`pyspark.sql.functions.wrap_udt`
+
Examples
--------
Example 1: Unwrap ML-specific UDT - VectorUDT
@@ -29244,6 +29252,107 @@ def unwrap_udt(col: "ColumnOrName") -> Column:
return _invoke_function("unwrap_udt", _to_java_column(col))
+@_try_remote_functions
+def wrap_udt(col: "ColumnOrName", udt: "Union[UserDefinedType, Column]") ->
Column:
+ """
+ Wrap a column as a user-defined type.
+
+ .. versionadded:: 4.4.0
+
+ Parameters
+ ----------
+ col : :class:`~pyspark.sql.Column` or column name
+ The column to wrap. The column data type must match the UDT's
underlying SQL type.
+ udt : :class:`~pyspark.sql.types.UserDefinedType` or
:class:`~pyspark.sql.Column`
+ The target user-defined type, or a constant string column containing
its JSON
+ representation.
+
+ Returns
+ -------
+ :class:`~pyspark.sql.Column`
+ A column of the target user-defined type.
+
+ See Also
+ --------
+ :meth:`pyspark.sql.functions.unwrap_udt`
+
+ Examples
+ --------
+ Example 1: Wrapping a vector struct as VectorUDT
+
+ >>> from pyspark.sql import functions as sf
+ >>> from pyspark.sql import Row
+ >>> from pyspark.sql.types import StructField, StructType
+ >>> from pyspark.ml.linalg import VectorUDT
+ >>> vector_schema = StructType([StructField("vec", VectorUDT.sqlType(),
True)])
+ >>> df = spark.createDataFrame(
+ ... [(Row(type=1, size=None, indices=None, values=[1.0, 2.0, 3.0]),)],
+ ... vector_schema)
+ >>> df.select("*", sf.wrap_udt("vec", VectorUDT())).show()
+ +--------------------+...+
+ | vec|wrap_udt(vec...|
+ +--------------------+...+
+ |{1, NULL, NULL, [...|...[1.0,2.0,3.0]|
+ +--------------------+...+
+ >>> row = df.select(sf.wrap_udt("vec", VectorUDT())).first()
+ >>> type(row[0])
+ <class 'pyspark.ml.linalg.DenseVector'>
+
+ Example 2: Wrapping a matrix struct as MatrixUDT
+
+ >>> from pyspark.sql import functions as sf
+ >>> from pyspark.sql import Row
+ >>> from pyspark.sql.types import StructField, StructType
+ >>> from pyspark.mllib.linalg import MatrixUDT
+ >>> matrix_schema = StructType([StructField("mat", MatrixUDT.sqlType(),
True)])
+ >>> df = spark.createDataFrame(
+ ... [(
+ ... Row(
+ ... type=1,
+ ... numRows=2,
+ ... numCols=2,
+ ... colPtrs=None,
+ ... rowIndices=None,
+ ... values=[1.0, 2.0, 3.0, 4.0],
+ ... isTransposed=False),
+ ... )],
+ ... matrix_schema)
+ >>> df.select("*", sf.wrap_udt("mat", MatrixUDT())).printSchema()
+ root
+ |-- mat: struct (nullable = true)
+ | |-- type: byte (nullable = false)
+ | |-- numRows: integer (nullable = false)
+ | |-- numCols: integer (nullable = false)
+ | |-- colPtrs: array (nullable = true)
+ | | |-- element: integer (containsNull = false)
+ | |-- rowIndices: array (nullable = true)
+ | | |-- element: integer (containsNull = false)
+ | |-- values: array (nullable = true)
+ | | |-- element: double (containsNull = false)
+ | |-- isTransposed: boolean (nullable = false)
+ |-- wrap_udt(mat...: matrix... (nullable = true)
+ >>> row = df.select(sf.wrap_udt("mat", MatrixUDT())).first()
+ >>> type(row[0])
+ <class 'pyspark.mllib.linalg.DenseMatrix'>
+ """
+ from pyspark.sql.classic.column import _to_java_column
+
+ if isinstance(udt, _UserDefinedType):
+ udt_col = lit(udt.json())
+ elif isinstance(udt, Column):
+ udt_col = udt
+ else:
+ raise PySparkTypeError(
+ errorClass="NOT_EXPECTED_TYPE",
+ messageParameters={
+ "expected_type": "UserDefinedType or Column",
+ "arg_name": "udt",
+ "arg_type": type(udt).__name__,
+ },
+ )
+ return _invoke_function("wrap_udt", _to_java_column(col),
_to_java_column(udt_col))
+
+
# ---------------------- Datasketch functions ------------------------------
diff --git a/sql/api/src/main/scala/org/apache/spark/sql/functions.scala
b/sql/api/src/main/scala/org/apache/spark/sql/functions.scala
index 9c79e1117332..307a82e5a44a 100644
--- a/sql/api/src/main/scala/org/apache/spark/sql/functions.scala
+++ b/sql/api/src/main/scala/org/apache/spark/sql/functions.scala
@@ -17856,6 +17856,32 @@ object functions {
*/
def unwrap_udt(column: Column): Column = Column.internalFn("unwrap_udt",
column)
+ /**
+ * Wrap a column as a user-defined type.
+ * @param column
+ * the column to wrap. The column data type must match the UDT's
underlying SQL type.
+ * @param udt
+ * the target user-defined type.
+ * @group udf_funcs
+ * @since 4.4.0
+ */
+ def wrap_udt(column: Column, udt: UserDefinedType[_]): Column = {
+ wrap_udt(column, lit(udt.json))
+ }
+
+ /**
+ * Wrap a column as a user-defined type.
+ * @param column
+ * the column to wrap. The column data type must match the UDT's
underlying SQL type.
+ * @param udt
+ * the target user-defined type as a constant JSON string column.
+ * @group udf_funcs
+ * @since 4.4.0
+ */
+ def wrap_udt(column: Column, udt: Column): Column = {
+ Column.internalFn("wrap_udt", column, udt)
+ }
+
// ---------------------- Vector Functions ----------------------
/**
diff --git
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/FunctionRegistry.scala
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/FunctionRegistry.scala
index dd70ea9d177a..4facf16cefbe 100644
---
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/FunctionRegistry.scala
+++
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/FunctionRegistry.scala
@@ -1123,6 +1123,7 @@ object FunctionRegistry {
registerInternalExpression[Days]("days")
registerInternalExpression[Hours]("hours")
registerInternalExpression[UnwrapUDT]("unwrap_udt")
+ registerInternalExpression[WrapUDT]("wrap_udt")
registerInternalExpression[MonotonicallyIncreasingID]("distributed_id",
setAlias = true)
registerInternalExpression[DistributedSequenceID]("distributed_sequence_id")
registerInternalExpression[PandasProduct]("pandas_product")
diff --git
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/UnwrapUDT.scala
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/UnwrapUDT.scala
index 249e3955a81f..8183e6a447bd 100644
---
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/UnwrapUDT.scala
+++
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/UnwrapUDT.scala
@@ -25,6 +25,8 @@ import org.apache.spark.sql.types.{DataType, UserDefinedType}
/**
* Unwrap UDT data type column into its underlying type.
+ *
+ * @see [[WrapUDT]] for converting an underlying SQL type column to a UDT.
*/
case class UnwrapUDT(child: Expression) extends UnaryExpression with
NonSQLExpression {
diff --git
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/UnwrapUDT.scala
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/WrapUDT.scala
similarity index 55%
copy from
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/UnwrapUDT.scala
copy to
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/WrapUDT.scala
index 249e3955a81f..770dd1f8fa47 100644
---
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/UnwrapUDT.scala
+++
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/WrapUDT.scala
@@ -17,39 +17,69 @@
package org.apache.spark.sql.catalyst.expressions
+import org.apache.spark.sql.AnalysisException
import org.apache.spark.sql.catalyst.analysis.TypeCheckResult
import org.apache.spark.sql.catalyst.analysis.TypeCheckResult.DataTypeMismatch
import org.apache.spark.sql.catalyst.expressions.Cast._
import org.apache.spark.sql.catalyst.expressions.codegen.{CodegenContext,
ExprCode}
+import org.apache.spark.sql.catalyst.types.DataTypeUtils
+import org.apache.spark.sql.catalyst.util.TypeUtils.ordinalNumber
import org.apache.spark.sql.types.{DataType, UserDefinedType}
/**
- * Unwrap UDT data type column into its underlying type.
+ * Wrap a column with a UDT whose underlying SQL type matches the column data
type.
+ *
+ * @see [[UnwrapUDT]] for converting a UDT column to its underlying SQL type.
*/
-case class UnwrapUDT(child: Expression) extends UnaryExpression with
NonSQLExpression {
+case class WrapUDT(child: Expression, udt: UserDefinedType[_])
+ extends UnaryExpression with NonSQLExpression {
- override protected def doGenCode(ctx: CodegenContext, ev: ExprCode):
ExprCode = child.genCode(ctx)
+ def this(child: Expression, udt: Expression) = {
+ this(child, WrapUDT.parseUDT(udt))
+ }
+
+ override protected def doGenCode(ctx: CodegenContext, ev: ExprCode):
ExprCode = {
+ child.genCode(ctx)
+ }
override def checkInputDataTypes(): TypeCheckResult = {
- if (child.dataType.isInstanceOf[UserDefinedType[_]]) {
+ if (DataTypeUtils.sameType(child.dataType, udt.sqlType)) {
TypeCheckResult.TypeCheckSuccess
} else {
DataTypeMismatch(
errorSubClass = "UNEXPECTED_INPUT_TYPE",
messageParameters = Map(
"paramIndex" -> ordinalNumber(0),
- "requiredType" -> toSQLType("UserDefinedType"),
+ "requiredType" -> toSQLType(udt.sqlType),
"inputSql" -> toSQLExpr(child),
"inputType" -> toSQLType(child.dataType)))
}
}
- override def dataType: DataType =
child.dataType.asInstanceOf[UserDefinedType[_]].sqlType
+
+ override def dataType: DataType = udt
override def nullSafeEval(input: Any): Any = input
- override def prettyName: String = "unwrap_udt"
+ override def prettyName: String = "wrap_udt"
- override protected def withNewChildInternal(newChild: Expression): UnwrapUDT
= {
+ override protected def withNewChildInternal(newChild: Expression): WrapUDT =
{
copy(child = newChild)
}
}
+
+object WrapUDT {
+ private def parseUDT(expression: Expression): UserDefinedType[_] = {
+ ExprUtils.evalTypeExpr(expression) match {
+ case udt: UserDefinedType[_] => udt
+ case dataType =>
+ throw new AnalysisException(
+ errorClass = "DATATYPE_MISMATCH.UNEXPECTED_INPUT_TYPE",
+ messageParameters = Map(
+ "sqlExpr" -> toSQLExpr(expression),
+ "paramIndex" -> ordinalNumber(1),
+ "requiredType" -> toSQLType("UserDefinedType"),
+ "inputSql" -> toSQLExpr(expression),
+ "inputType" -> toSQLType(dataType)))
+ }
+ }
+}
diff --git
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/WrapUDTExpressionSuite.scala
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/WrapUDTExpressionSuite.scala
new file mode 100644
index 000000000000..afd14131e5ec
--- /dev/null
+++
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/WrapUDTExpressionSuite.scala
@@ -0,0 +1,115 @@
+/*
+ * 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.expressions
+
+import org.apache.spark.{SPARK_DOC_ROOT, SparkFunSuite}
+import org.apache.spark.sql.AnalysisException
+import org.apache.spark.sql.catalyst.FunctionIdentifier
+import org.apache.spark.sql.catalyst.analysis.FunctionRegistry
+import org.apache.spark.sql.catalyst.analysis.TypeCheckResult.DataTypeMismatch
+import org.apache.spark.sql.catalyst.expressions.Cast.{toSQLExpr, toSQLType}
+import org.apache.spark.sql.catalyst.util.GenericArrayData
+import org.apache.spark.sql.catalyst.util.TypeUtils.ordinalNumber
+import org.apache.spark.sql.types.{BooleanType, IntegerType, StringType,
TestUDT}
+
+class WrapUDTExpressionSuite extends SparkFunSuite with ExpressionEvalHelper {
+
+ test("WrapUDT should use target UDT with matching SQL type") {
+ val udt = new TestUDT.MyDenseVectorUDT()
+ val data = new GenericArrayData(Array[Any](1.0, 2.0))
+ val wrapUDTExpression = WrapUDT(Literal.create(data, udt.sqlType), udt)
+
+ assert(wrapUDTExpression.checkInputDataTypes().isSuccess)
+ assert(wrapUDTExpression.dataType == udt)
+ checkEvaluation(wrapUDTExpression, data)
+ }
+
+ test("WrapUDT should parse target UDT from foldable expression") {
+ val udt = new TestUDT.MyDenseVectorUDT()
+ val data = new GenericArrayData(Array[Any](1.0, 2.0))
+ val json = udt.json
+ val target = Concat(Seq(
+ Literal.create(json.substring(0, 8), StringType),
+ Literal.create(json.substring(8), StringType)))
+ val wrapUDTExpression = new WrapUDT(Literal.create(data, udt.sqlType),
target)
+
+ assert(wrapUDTExpression.checkInputDataTypes().isSuccess)
+ assert(wrapUDTExpression.dataType == udt)
+ checkEvaluation(wrapUDTExpression, data)
+ }
+
+ test("WrapUDT target expression should be a UDT") {
+ val target = Literal.create("int", StringType)
+ checkError(
+ exception = intercept[AnalysisException] {
+ new WrapUDT(Literal.create(1, IntegerType), target)
+ },
+ condition = "DATATYPE_MISMATCH.UNEXPECTED_INPUT_TYPE",
+ parameters = Map(
+ "sqlExpr" -> toSQLExpr(target),
+ "paramIndex" -> ordinalNumber(1),
+ "requiredType" -> toSQLType("UserDefinedType"),
+ "inputSql" -> toSQLExpr(target),
+ "inputType" -> toSQLType(IntegerType)))
+ }
+
+ test("WrapUDT target expression should be foldable") {
+ val target = AttributeReference("udt", StringType)()
+ checkError(
+ exception = intercept[AnalysisException] {
+ new WrapUDT(Literal.create(1, IntegerType), target)
+ },
+ condition = "INVALID_SCHEMA.NON_STRING_LITERAL",
+ parameters = Map("inputSchema" -> toSQLExpr(target)))
+ }
+
+ test("WrapUDT should reject wrong number of arguments through
FunctionRegistry") {
+ val expression = Literal.create(1, IntegerType)
+ val target = Literal.create(new TestUDT.MyDenseVectorUDT().json,
StringType)
+
+ Seq(
+ Seq(expression) -> "1",
+ Seq(expression, target, Literal.create("extra", StringType)) ->
"3").foreach {
+ case (arguments, actualNum) =>
+ checkError(
+ exception = intercept[AnalysisException] {
+
FunctionRegistry.internal.lookupFunction(FunctionIdentifier("wrap_udt"),
arguments)
+ },
+ condition = "WRONG_NUM_ARGS.WITHOUT_SUGGESTION",
+ parameters = Map(
+ "functionName" -> "`wrap_udt`",
+ "expectedNum" -> "2",
+ "actualNum" -> actualNum,
+ "docroot" -> SPARK_DOC_ROOT))
+ }
+ }
+
+ test("WrapUDT input type should match target UDT SQL type") {
+ val b1 = Literal.create(false, BooleanType)
+ val udt = new TestUDT.MyDenseVectorUDT()
+ val wrapUDTExpression = WrapUDT(b1, udt)
+ assert(wrapUDTExpression.checkInputDataTypes() ==
+ DataTypeMismatch(
+ errorSubClass = "UNEXPECTED_INPUT_TYPE",
+ messageParameters = Map(
+ "paramIndex" -> ordinalNumber(0),
+ "requiredType" -> toSQLType(udt.sqlType),
+ "inputSql" -> "\"false\"",
+ "inputType" -> "\"BOOLEAN\"")))
+ }
+}
diff --git
a/sql/connect/client/jvm/src/test/scala/org/apache/spark/sql/PlanGenerationTestSuite.scala
b/sql/connect/client/jvm/src/test/scala/org/apache/spark/sql/PlanGenerationTestSuite.scala
index 6346cf4b5cd8..b117f1a0b5cd 100644
---
a/sql/connect/client/jvm/src/test/scala/org/apache/spark/sql/PlanGenerationTestSuite.scala
+++
b/sql/connect/client/jvm/src/test/scala/org/apache/spark/sql/PlanGenerationTestSuite.scala
@@ -106,6 +106,8 @@ class PlanGenerationTestSuite extends ConnectFunSuite with
Logging {
private val printer = JsonFormat.printer().usingTypeRegistry(registry)
+ private val testUDT = new TestUDT.NewArrayUDT()
+
private var session: SparkSession = _
override protected def beforeAll(): Unit = {
@@ -3300,6 +3302,14 @@ class PlanGenerationTestSuite extends ConnectFunSuite
with Logging {
fn.typeof(fn.col("g"))
}
+ functionTest("wrap_udt") {
+ fn.wrap_udt(fn.array(fn.col("b")), testUDT)
+ }
+
+ functionTest("unwrap_udt") {
+ fn.unwrap_udt(fn.wrap_udt(fn.array(fn.col("b")), testUDT))
+ }
+
functionTest("stack") {
fn.stack(lit(2), fn.col("g"), fn.col("g"), fn.col("g"))
}
diff --git
a/sql/connect/client/jvm/src/test/scala/org/apache/spark/sql/TestUDT.scala
b/sql/connect/client/jvm/src/test/scala/org/apache/spark/sql/TestUDT.scala
new file mode 100644
index 000000000000..77704b8a57c3
--- /dev/null
+++ b/sql/connect/client/jvm/src/test/scala/org/apache/spark/sql/TestUDT.scala
@@ -0,0 +1,42 @@
+/*
+ * 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
+
+import org.apache.spark.sql.types.{ArrayType, DataType, DoubleType,
SQLUserDefinedType, UserDefinedType}
+
+object TestUDT {
+
+ @SQLUserDefinedType(udt = classOf[NewArrayUDT])
+ private[sql] class NewArray(val values: Array[Double]) extends Serializable
+
+ private[sql] class NewArrayUDT extends UserDefinedType[NewArray] {
+
+ override def sqlType: DataType = ArrayType(DoubleType, containsNull =
false)
+
+ override def serialize(obj: NewArray): Any = obj.values
+
+ override def deserialize(datum: Any): NewArray = {
+ datum match {
+ case values: Array[_] =>
+ new NewArray(values.map(_.asInstanceOf[Double]))
+ }
+ }
+
+ override def userClass: Class[NewArray] = classOf[NewArray]
+ }
+}
diff --git
a/sql/connect/common/src/test/resources/query-tests/explain-results/function_unwrap_udt.explain
b/sql/connect/common/src/test/resources/query-tests/explain-results/function_unwrap_udt.explain
new file mode 100644
index 000000000000..08b1a3eb011d
--- /dev/null
+++
b/sql/connect/common/src/test/resources/query-tests/explain-results/function_unwrap_udt.explain
@@ -0,0 +1,2 @@
+Project [unwrap_udt(wrap_udt(array(b#0),
org.apache.spark.sql.TestUDT$NewArrayUDT)) AS unwrap_udt(wrap_udt(array(b),
org.apache.spark.sql.TestUDT$NewArrayUDT))#0]
++- LocalRelation <empty>, [id#0L, a#0, b#0, d#0, e#0, f#0, g#0]
diff --git
a/sql/connect/common/src/test/resources/query-tests/explain-results/function_wrap_udt.explain
b/sql/connect/common/src/test/resources/query-tests/explain-results/function_wrap_udt.explain
new file mode 100644
index 000000000000..a1fb72ba2b77
--- /dev/null
+++
b/sql/connect/common/src/test/resources/query-tests/explain-results/function_wrap_udt.explain
@@ -0,0 +1,2 @@
+Project [wrap_udt(array(b#0), org.apache.spark.sql.TestUDT$NewArrayUDT) AS
wrap_udt(array(b), org.apache.spark.sql.TestUDT$NewArrayUDT)#0]
++- LocalRelation <empty>, [id#0L, a#0, b#0, d#0, e#0, f#0, g#0]
diff --git
a/sql/connect/common/src/test/resources/query-tests/queries/function_unwrap_udt.json
b/sql/connect/common/src/test/resources/query-tests/queries/function_unwrap_udt.json
new file mode 100644
index 000000000000..9a8af401c78d
--- /dev/null
+++
b/sql/connect/common/src/test/resources/query-tests/queries/function_unwrap_udt.json
@@ -0,0 +1,127 @@
+{
+ "common": {
+ "planId": "1"
+ },
+ "project": {
+ "input": {
+ "common": {
+ "planId": "0"
+ },
+ "localRelation": {
+ "schema":
"struct\u003cid:bigint,a:int,b:double,d:struct\u003cid:bigint,a:int,b:double\u003e,e:array\u003cint\u003e,f:map\u003cstring,struct\u003cid:bigint,a:int,b:double\u003e\u003e,g:string\u003e"
+ }
+ },
+ "expressions": [{
+ "unresolvedFunction": {
+ "functionName": "unwrap_udt",
+ "arguments": [{
+ "unresolvedFunction": {
+ "functionName": "wrap_udt",
+ "arguments": [{
+ "unresolvedFunction": {
+ "functionName": "array",
+ "arguments": [{
+ "unresolvedAttribute": {
+ "unparsedIdentifier": "b"
+ },
+ "common": {
+ "origin": {
+ "jvmOrigin": {
+ "stackTrace": [{
+ "classLoaderName": "app",
+ "declaringClass": "org.apache.spark.sql.functions$",
+ "methodName": "col",
+ "fileName": "functions.scala"
+ }, {
+ "classLoaderName": "app",
+ "declaringClass":
"org.apache.spark.sql.PlanGenerationTestSuite",
+ "methodName": "~~trimmed~anonfun~~",
+ "fileName": "PlanGenerationTestSuite.scala"
+ }]
+ }
+ }
+ }
+ }],
+ "isInternal": false
+ },
+ "common": {
+ "origin": {
+ "jvmOrigin": {
+ "stackTrace": [{
+ "classLoaderName": "app",
+ "declaringClass": "org.apache.spark.sql.functions$",
+ "methodName": "array",
+ "fileName": "functions.scala"
+ }, {
+ "classLoaderName": "app",
+ "declaringClass":
"org.apache.spark.sql.PlanGenerationTestSuite",
+ "methodName": "~~trimmed~anonfun~~",
+ "fileName": "PlanGenerationTestSuite.scala"
+ }]
+ }
+ }
+ }
+ }, {
+ "literal": {
+ "string":
"{\"type\":\"udt\",\"class\":\"org.apache.spark.sql.TestUDT$NewArrayUDT\",\"pyClass\":null,\"sqlType\":{\"type\":\"array\",\"elementType\":\"double\",\"containsNull\":false}}"
+ },
+ "common": {
+ "origin": {
+ "jvmOrigin": {
+ "stackTrace": [{
+ "classLoaderName": "app",
+ "declaringClass": "org.apache.spark.sql.functions$",
+ "methodName": "wrap_udt",
+ "fileName": "functions.scala"
+ }, {
+ "classLoaderName": "app",
+ "declaringClass":
"org.apache.spark.sql.PlanGenerationTestSuite",
+ "methodName": "~~trimmed~anonfun~~",
+ "fileName": "PlanGenerationTestSuite.scala"
+ }]
+ }
+ }
+ }
+ }],
+ "isInternal": true
+ },
+ "common": {
+ "origin": {
+ "jvmOrigin": {
+ "stackTrace": [{
+ "classLoaderName": "app",
+ "declaringClass": "org.apache.spark.sql.functions$",
+ "methodName": "wrap_udt",
+ "fileName": "functions.scala"
+ }, {
+ "classLoaderName": "app",
+ "declaringClass":
"org.apache.spark.sql.PlanGenerationTestSuite",
+ "methodName": "~~trimmed~anonfun~~",
+ "fileName": "PlanGenerationTestSuite.scala"
+ }]
+ }
+ }
+ }
+ }],
+ "isInternal": true
+ },
+ "common": {
+ "origin": {
+ "jvmOrigin": {
+ "stackTrace": [{
+ "classLoaderName": "app",
+ "declaringClass": "org.apache.spark.sql.functions$",
+ "methodName": "unwrap_udt",
+ "fileName": "functions.scala"
+ }, {
+ "classLoaderName": "app",
+ "declaringClass": "org.apache.spark.sql.PlanGenerationTestSuite",
+ "methodName": "~~trimmed~anonfun~~",
+ "fileName": "PlanGenerationTestSuite.scala"
+ }]
+ }
+ }
+ }
+ }]
+ }
+}
\ No newline at end of file
diff --git
a/sql/connect/common/src/test/resources/query-tests/queries/function_unwrap_udt.proto.bin
b/sql/connect/common/src/test/resources/query-tests/queries/function_unwrap_udt.proto.bin
new file mode 100644
index 000000000000..1ec310e22f77
Binary files /dev/null and
b/sql/connect/common/src/test/resources/query-tests/queries/function_unwrap_udt.proto.bin
differ
diff --git
a/sql/connect/common/src/test/resources/query-tests/queries/function_wrap_udt.json
b/sql/connect/common/src/test/resources/query-tests/queries/function_wrap_udt.json
new file mode 100644
index 000000000000..7376698e8516
--- /dev/null
+++
b/sql/connect/common/src/test/resources/query-tests/queries/function_wrap_udt.json
@@ -0,0 +1,104 @@
+{
+ "common": {
+ "planId": "1"
+ },
+ "project": {
+ "input": {
+ "common": {
+ "planId": "0"
+ },
+ "localRelation": {
+ "schema":
"struct\u003cid:bigint,a:int,b:double,d:struct\u003cid:bigint,a:int,b:double\u003e,e:array\u003cint\u003e,f:map\u003cstring,struct\u003cid:bigint,a:int,b:double\u003e\u003e,g:string\u003e"
+ }
+ },
+ "expressions": [{
+ "unresolvedFunction": {
+ "functionName": "wrap_udt",
+ "arguments": [{
+ "unresolvedFunction": {
+ "functionName": "array",
+ "arguments": [{
+ "unresolvedAttribute": {
+ "unparsedIdentifier": "b"
+ },
+ "common": {
+ "origin": {
+ "jvmOrigin": {
+ "stackTrace": [{
+ "classLoaderName": "app",
+ "declaringClass": "org.apache.spark.sql.functions$",
+ "methodName": "col",
+ "fileName": "functions.scala"
+ }, {
+ "classLoaderName": "app",
+ "declaringClass":
"org.apache.spark.sql.PlanGenerationTestSuite",
+ "methodName": "~~trimmed~anonfun~~",
+ "fileName": "PlanGenerationTestSuite.scala"
+ }]
+ }
+ }
+ }
+ }],
+ "isInternal": false
+ },
+ "common": {
+ "origin": {
+ "jvmOrigin": {
+ "stackTrace": [{
+ "classLoaderName": "app",
+ "declaringClass": "org.apache.spark.sql.functions$",
+ "methodName": "array",
+ "fileName": "functions.scala"
+ }, {
+ "classLoaderName": "app",
+ "declaringClass":
"org.apache.spark.sql.PlanGenerationTestSuite",
+ "methodName": "~~trimmed~anonfun~~",
+ "fileName": "PlanGenerationTestSuite.scala"
+ }]
+ }
+ }
+ }
+ }, {
+ "literal": {
+ "string":
"{\"type\":\"udt\",\"class\":\"org.apache.spark.sql.TestUDT$NewArrayUDT\",\"pyClass\":null,\"sqlType\":{\"type\":\"array\",\"elementType\":\"double\",\"containsNull\":false}}"
+ },
+ "common": {
+ "origin": {
+ "jvmOrigin": {
+ "stackTrace": [{
+ "classLoaderName": "app",
+ "declaringClass": "org.apache.spark.sql.functions$",
+ "methodName": "wrap_udt",
+ "fileName": "functions.scala"
+ }, {
+ "classLoaderName": "app",
+ "declaringClass":
"org.apache.spark.sql.PlanGenerationTestSuite",
+ "methodName": "~~trimmed~anonfun~~",
+ "fileName": "PlanGenerationTestSuite.scala"
+ }]
+ }
+ }
+ }
+ }],
+ "isInternal": true
+ },
+ "common": {
+ "origin": {
+ "jvmOrigin": {
+ "stackTrace": [{
+ "classLoaderName": "app",
+ "declaringClass": "org.apache.spark.sql.functions$",
+ "methodName": "wrap_udt",
+ "fileName": "functions.scala"
+ }, {
+ "classLoaderName": "app",
+ "declaringClass": "org.apache.spark.sql.PlanGenerationTestSuite",
+ "methodName": "~~trimmed~anonfun~~",
+ "fileName": "PlanGenerationTestSuite.scala"
+ }]
+ }
+ }
+ }
+ }]
+ }
+}
\ No newline at end of file
diff --git
a/sql/connect/common/src/test/resources/query-tests/queries/function_wrap_udt.proto.bin
b/sql/connect/common/src/test/resources/query-tests/queries/function_wrap_udt.proto.bin
new file mode 100644
index 000000000000..dd9898055ff5
Binary files /dev/null and
b/sql/connect/common/src/test/resources/query-tests/queries/function_wrap_udt.proto.bin
differ
diff --git
a/sql/connect/common/src/test/scala/org/apache/spark/sql/TestUDT.scala
b/sql/connect/common/src/test/scala/org/apache/spark/sql/TestUDT.scala
new file mode 100644
index 000000000000..77704b8a57c3
--- /dev/null
+++ b/sql/connect/common/src/test/scala/org/apache/spark/sql/TestUDT.scala
@@ -0,0 +1,42 @@
+/*
+ * 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
+
+import org.apache.spark.sql.types.{ArrayType, DataType, DoubleType,
SQLUserDefinedType, UserDefinedType}
+
+object TestUDT {
+
+ @SQLUserDefinedType(udt = classOf[NewArrayUDT])
+ private[sql] class NewArray(val values: Array[Double]) extends Serializable
+
+ private[sql] class NewArrayUDT extends UserDefinedType[NewArray] {
+
+ override def sqlType: DataType = ArrayType(DoubleType, containsNull =
false)
+
+ override def serialize(obj: NewArray): Any = obj.values
+
+ override def deserialize(datum: Any): NewArray = {
+ datum match {
+ case values: Array[_] =>
+ new NewArray(values.map(_.asInstanceOf[Double]))
+ }
+ }
+
+ override def userClass: Class[NewArray] = classOf[NewArray]
+ }
+}
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 f634774faec6..142b4e6dd26e 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
@@ -84,6 +84,7 @@ class DataFrameFunctionsSuite extends SharedSparkSession {
"bucket", "days", "hours", "months", "years", // Datasource v2 partition
transformations
"product", // Discussed in https://github.com/apache/spark/pull/30745
"unwrap_udt",
+ "wrap_udt",
"timestamp_add",
"timestamp_diff"
)
diff --git
a/sql/core/src/test/scala/org/apache/spark/sql/UserDefinedTypeSuite.scala
b/sql/core/src/test/scala/org/apache/spark/sql/UserDefinedTypeSuite.scala
index 18adb45c1234..04be47523c58 100644
--- a/sql/core/src/test/scala/org/apache/spark/sql/UserDefinedTypeSuite.scala
+++ b/sql/core/src/test/scala/org/apache/spark/sql/UserDefinedTypeSuite.scala
@@ -293,8 +293,33 @@ class UserDefinedTypeSuite extends SharedSparkSession with
ParquetTest
val unwrappedFeaturesArrays: Array[Array[Double]] =
unwrappedFeatures.collect()
assert(unwrappedFeaturesArrays.length === 2)
- java.util.Arrays.equals(unwrappedFeaturesArrays(0), Array(0.1, 1.0))
- java.util.Arrays.equals(unwrappedFeaturesArrays(1), Array(0.2, 2.0))
+ assert(Arrays.equals(unwrappedFeaturesArrays(0), Array(0.1, 1.0)))
+ assert(Arrays.equals(unwrappedFeaturesArrays(1), Array(0.2, 2.0)))
+ }
+
+ test("Test wrap_udt function") {
+ val udt = new TestUDT.MyDenseVectorUDT()
+ val df = Seq(Array(0.1, 1.0), Array(0.2, 2.0)).toDF("features")
+ val wrappedFeatures = df.select(wrap_udt(col("features"),
udt).as("features"))
+
+ assert(wrappedFeatures.schema("features").dataType === udt)
+ checkAnswer(
+ wrappedFeatures,
+ Seq(
+ Row(new TestUDT.MyDenseVector(Array(0.1, 1.0))),
+ Row(new TestUDT.MyDenseVector(Array(0.2, 2.0)))))
+ }
+
+ test("Test unwrap_udt and wrap_udt round trip") {
+ val udt = new TestUDT.MyDenseVectorUDT()
+ val roundTrip = pointsRDD.select(wrap_udt(unwrap_udt(col("features")),
udt).as("features"))
+
+ assert(roundTrip.schema("features").dataType === udt)
+ checkAnswer(
+ roundTrip,
+ Seq(
+ Row(new TestUDT.MyDenseVector(Array(0.1, 1.0))),
+ Row(new TestUDT.MyDenseVector(Array(0.2, 2.0)))))
}
test("SPARK-46289: UDT ordering") {
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]