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

Yicong-Huang pushed a commit to branch branch-4.x
in repository https://gitbox.apache.org/repos/asf/spark.git


The following commit(s) were added to refs/heads/branch-4.x by this push:
     new 9cadc90a3dc9 [SPARK-59911][PYTHON] Migrate SQL_SCALAR_PANDAS_UDF to a 
pandas eval type handler
9cadc90a3dc9 is described below

commit 9cadc90a3dc90c70eacece16160866a764fcbbe3
Author: Yicong Huang <[email protected]>
AuthorDate: Fri Oct 2 10:44:09 2026 +0000

    [SPARK-59911][PYTHON] Migrate SQL_SCALAR_PANDAS_UDF to a pandas eval type 
handler
    
    ### What changes were proposed in this pull request?
    
    This is the first step of migrating the pandas UDF eval types onto the 
eval-handler framework (umbrella SPARK-59415), following the Arrow handler work 
already shipped in `pyspark/eval_handlers/_arrow.py`.
    
    New file `python/pyspark/eval_handlers/_pandas.py` adds 
`PandasScalarUDFHandler(BatchEvalTypeHandler)` for `SQL_SCALAR_PANDAS_UDF`, the 
pandas counterpart of `ArrowScalarUDFHandler`: convert each input RecordBatch 
to pandas Series (struct columns become DataFrames via `df_for_struct=True`), 
invoke each UDF once per batch, check the row count, and convert the pandas 
results back to one RecordBatch. It calls the Arrow<->pandas conversions in 
`pyspark.sql.conversion` (`ArrowToPandasConve [...]
    
    The class hierarchy after this PR (pandas mirrors Arrow -- both extend the 
category base directly):
    
        EvalTypeHandler
          BatchEvalTypeHandler
            ArrowScalarUDFHandler          (SQL_SCALAR_ARROW_UDF, shipped)
            PandasScalarUDFHandler         (SQL_SCALAR_PANDAS_UDF, new)
    
    The `SQL_SCALAR_PANDAS_UDF` branch is removed from `worker.py::read_udfs` 
(both the serializer-selection entry and the mapper block); `read_udfs` now 
dispatches it through `get_eval_type_handler`, exactly as the migrated Arrow 
types already do. All other pandas branches are left untouched. 
`read_single_udf` still handles `SQL_SCALAR_PANDAS_UDF` (the handler path calls 
it).
    
    One supporting fix in `pyspark/sql/conversion.py`: 
`ArrowToPandasConversion.to_pandas` declared `timezone: str`, but every sibling 
conversion method (and the `_convert_array` it delegates to) types it 
`Optional[str]`, and the worker passes `runner_conf.timezone` which is 
`Optional[str]`. The handler surfaces this (it is type-checked, unlike 
`worker.py`), so the annotation is corrected to `Optional[str]`. 
Annotation-only; no runtime change.
    
    This is a pure refactor with no behavior change: the handler issues the 
same `ArrowToPandasConversion.to_pandas` / 
`PandasToArrowConversion.from_pandas` calls with the same parameters, in the 
same validation order, raising the same error types -- it is the previous 
`read_udfs` mapper body moved into the handler. The one structural change -- 
inlining the args/kwargs offsets (`f(*pos, **kw)`) instead of pre-combining 
them through `wrap_kwargs_support` -- is equivalent (`wrap_kwargs_supp [...]
    
    The handler passes `prefers_large_types=runner_conf.use_large_var_types` 
inline, matching the current mapper. Note the pre-existing inconsistencies in 
the other (untouched) pandas branches -- `GROUPED_AGG_PANDAS_ITER` pins 
`prefers_large_types=False`, and the stateful branches omit it (use the 
default). If these turn out to be bugs they should be fixed in a separate 
ticket, not here.
    
    #### Sketch: how SQL_GROUPED_MAP_PANDAS_UDF(201) would follow this pattern
    
    Confirming the pattern generalizes to the Grouped category. 
`SQL_GROUPED_MAP_PANDAS_UDF` is a grouped type (input 
`Iterator[Iterator[pa.RecordBatch]]`, `ArrowStreamGroupSerializer`), so it 
extends the grouped category base directly and calls the same conversions 
directly -- no pandas-specific base:
    
        class 
PandasGroupedMapUDFHandler(GroupedEvalTypeHandler["pa.RecordBatch"]):
            eval_type = PythonEvalType.SQL_GROUPED_MAP_PANDAS_UDF
            # __init__: parse key/value offsets (extract_key_value_indexes), 
build
            #   output_schema = StructType([StructField("_0", return_type)]).
            # run(split_index, data): for each group, materialize its batches 
into one
            #   pa.Table, then
            #       all_series = ArrowToPandasConversion.to_pandas(table, 
timezone=rc.timezone,
            #                        
prefer_int_ext_dtype=rc.prefer_int_ext_dtype)
            #   build the value DataFrame from value_offsets, call the UDF 
(with the key
            #   tuple when num_udf_args == 2), verify the pandas result, then
            #       yield PandasToArrowConversion.from_pandas([result], 
output_schema, ...)
    
    The current `GROUPED_MAP_PANDAS_UDF` mapper already makes exactly those two 
calls, so moving it into a handler is the same mechanical move as this PR -- 
only the category base (`Grouped` vs `Batch`), the per-group loop, key 
handling, the pandas result verification, and the output-batch resizing wrapper 
differ. If a second pandas handler ends up repeating the same 
runner_conf-to-parameter argument list, that is the point to factor a shared 
helper (as the Arrow side keeps shared verifie [...]
    
    ### Why are the changes needed?
    
    Consolidating each Arrow/Pandas UDF eval type behind an `EvalTypeHandler` 
keeps the per-type logic together and shrinks the long `read_udfs` dispatch. 
This PR extends the pattern from the Arrow types to the first pandas type.
    
    ### Does this PR introduce _any_ user-facing change?
    
    No.
    
    ### How was this patch tested?
    
    - New unit tests 
`python/pyspark/eval_handlers/tests/test_pandas_eval_type_handlers.py` (built 
on a real `RunnerConf`): registration, per-batch invocation, output type 
coercion, multiple UDFs, keyword-offset binding, struct return (DataFrame), the 
three error paths (non-sized result, row-count mismatch, struct return that is 
not a DataFrame), and that the handler forwards `preferIntExtensionDtype` to 
the input conversion and `useLargeVarTypes` to the output conversion.
    - The existing per-eval-type suite 
`pyspark.sql.tests.pandas.test_pandas_udf_scalar` (the behavior gate for 
`SQL_SCALAR_PANDAS_UDF`) and the ASV per-eval-type benchmark (regression gate).
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    Yes, authored with Claude (Opus 4.8) via Claude Code.
    
    This pull request and its description were written by Isaac.
    
    Closes #59171 from Yicong-Huang/pandas-handler-base.
    
    Authored-by: Yicong Huang <[email protected]>
    Signed-off-by: Yicong-Huang <[email protected]>
    (cherry picked from commit 49a83896423a2bc6a1ff28f0ede8b94d6f7f6164)
    Signed-off-by: Yicong-Huang <[email protected]>
---
 dev/sparktestsupport/modules.py                    |   1 +
 python/pyspark/eval_handlers/__init__.py           |   6 +-
 python/pyspark/eval_handlers/_pandas.py            | 123 ++++++++++++++++
 .../tests/test_pandas_eval_type_handlers.py        | 162 +++++++++++++++++++++
 python/pyspark/sql/conversion.py                   |   7 +-
 python/pyspark/worker.py                           |  72 ---------
 6 files changed, 293 insertions(+), 78 deletions(-)

diff --git a/dev/sparktestsupport/modules.py b/dev/sparktestsupport/modules.py
index ff921796e65d..cb964c62d437 100644
--- a/dev/sparktestsupport/modules.py
+++ b/dev/sparktestsupport/modules.py
@@ -665,6 +665,7 @@ pyspark_sql = Module(
         # unittests
         "pyspark.eval_handlers.tests.test_arrow_eval_type_handlers",
         "pyspark.eval_handlers.tests.test_base_eval_type_handlers",
+        "pyspark.eval_handlers.tests.test_pandas_eval_type_handlers",
         "pyspark.sql.tests.test_artifact",
         "pyspark.sql.tests.test_catalog",
         "pyspark.sql.tests.test_column",
diff --git a/python/pyspark/eval_handlers/__init__.py 
b/python/pyspark/eval_handlers/__init__.py
index 2f90d4b68b6a..63ab04aab9fd 100644
--- a/python/pyspark/eval_handlers/__init__.py
+++ b/python/pyspark/eval_handlers/__init__.py
@@ -24,8 +24,8 @@ declares its ``eval_type`` and self-registers at class 
definition, which
 ``read_udfs`` looks up via ``get_eval_type_handler``. Importing this package
 imports the concrete handler submodules so they register.
 
-``_arrow`` imports pyarrow lazily, so it registers its handlers without pyarrow
-installed and defers the pyarrow check to when a handler runs.
+``_arrow`` and ``_pandas`` import pyarrow (and pandas) lazily, so they register
+their handlers without those installed and defer the checks to when a handler 
runs.
 """
 
-from pyspark.eval_handlers import _arrow  # noqa: F401  # registers handlers 
on import
+from pyspark.eval_handlers import _arrow, _pandas  # noqa: F401  # registers 
handlers on import
diff --git a/python/pyspark/eval_handlers/_pandas.py 
b/python/pyspark/eval_handlers/_pandas.py
new file mode 100644
index 000000000000..b5c0c7f83b3e
--- /dev/null
+++ b/python/pyspark/eval_handlers/_pandas.py
@@ -0,0 +1,123 @@
+#
+# 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.
+#
+
+"""Handlers for the pandas UDF eval types (the UDF exchanges ``pd.Series`` /
+``pd.DataFrame`` values, converted to and from Arrow via the conversion 
utilities
+in ``pyspark.sql.conversion``, the pandas counterpart of 
``ArrowBatchTransformer``).
+
+pandas and pyarrow are imported lazily (inside ``run`` and the type-checking 
block)
+so the module stays importable and its handlers register without either 
installed.
+Each handler calls ``require_minimum_pandas_version`` and
+``require_minimum_pyarrow_version`` in ``__init__`` so a missing or too-old
+dependency surfaces a clear error when the handler runs.
+"""
+
+from __future__ import annotations
+
+from collections.abc import Iterator
+from typing import TYPE_CHECKING, Any
+
+from pyspark.errors import PySparkTypeError, PySparkValueError
+from pyspark.eval_handlers._base import BatchEvalTypeHandler
+from pyspark.eval_handlers.verification import verify_result_row_count
+from pyspark.sql.conversion import ArrowToPandasConversion, 
PandasToArrowConversion
+from pyspark.sql.pandas.utils import (
+    require_minimum_pandas_version,
+    require_minimum_pyarrow_version,
+)
+from pyspark.sql.types import StructField, StructType
+from pyspark.util import PythonEvalType
+
+if TYPE_CHECKING:
+    import pyarrow as pa
+
+    from pyspark.worker_util import EvalConf, RunnerConf
+
+
+class PandasScalarUDFHandler(BatchEvalTypeHandler["pa.RecordBatch"]):
+    """SQL_SCALAR_PANDAS_UDF: convert each input RecordBatch to pandas Series
+    (struct columns become DataFrames), invoke each UDF once per batch, check 
the
+    row count, and convert the pandas results back to one RecordBatch. The 
pandas
+    counterpart of ArrowScalarUDFHandler, calling the Arrow<->pandas 
conversions in
+    ``pyspark.sql.conversion`` directly with the runner_conf-derived 
parameters."""
+
+    eval_type = PythonEvalType.SQL_SCALAR_PANDAS_UDF
+
+    def __init__(
+        self, udfs: list[tuple[Any, ...]], runner_conf: "RunnerConf", 
eval_conf: "EvalConf"
+    ) -> None:
+        require_minimum_pandas_version()
+        require_minimum_pyarrow_version()
+        super().__init__(udfs, runner_conf, eval_conf)
+        self._return_schema = StructType(
+            [StructField("_%d" % i, rt) for i, (_, _, _, rt) in 
enumerate(udfs)]
+        )
+
+    def run(self, split_index: int, data: "Iterator[pa.RecordBatch]") -> 
"Iterator[pa.RecordBatch]":
+        import pandas as pd
+
+        runner_conf = self._runner_conf
+        for input_batch in data:
+            num_rows = input_batch.num_rows
+
+            # Input: Arrow -> pandas Series (struct columns become DataFrames).
+            pandas_columns = ArrowToPandasConversion.to_pandas(
+                input_batch,
+                timezone=runner_conf.timezone,
+                struct_in_pandas="dict",
+                ndarray_as_list=False,
+                prefer_int_ext_dtype=runner_conf.prefer_int_ext_dtype,
+                df_for_struct=True,
+            )
+
+            # Process: evaluate each UDF column-wise on pandas Series.
+            results = []
+            for udf_func, args_offsets, kwargs_offsets, return_type in 
self._udfs:
+                result = udf_func(
+                    *[pandas_columns[o] for o in args_offsets],
+                    **{k: pandas_columns[v] for k, v in 
kwargs_offsets.items()},
+                )
+                if not hasattr(result, "__len__"):
+                    pd_type = (
+                        "pandas.DataFrame"
+                        if isinstance(return_type, StructType)
+                        else "pandas.Series"
+                    )
+                    raise PySparkTypeError(
+                        errorClass="UDF_RETURN_TYPE",
+                        messageParameters={"expected": pd_type, "actual": 
type(result).__name__},
+                    )
+                verify_result_row_count(len(result), num_rows)
+                # struct_in_pandas="dict": UDF must return a DataFrame for 
struct types.
+                if isinstance(return_type, StructType) and not 
isinstance(result, pd.DataFrame):
+                    raise PySparkValueError(
+                        "Invalid return type. Please make sure that the UDF 
returns a "
+                        "pandas.DataFrame when the specified return type is 
StructType."
+                    )
+                results.append(result)
+
+            # Output: pandas -> Arrow.
+            yield PandasToArrowConversion.from_pandas(
+                results,
+                self._return_schema,
+                timezone=runner_conf.timezone,
+                safecheck=runner_conf.safecheck,
+                arrow_cast=True,
+                prefers_large_types=runner_conf.use_large_var_types,
+                assign_cols_by_name=runner_conf.assign_cols_by_name,
+                
int_to_decimal_coercion_enabled=runner_conf.int_to_decimal_coercion_enabled,
+            )
diff --git 
a/python/pyspark/eval_handlers/tests/test_pandas_eval_type_handlers.py 
b/python/pyspark/eval_handlers/tests/test_pandas_eval_type_handlers.py
new file mode 100644
index 000000000000..3f2e4d516be6
--- /dev/null
+++ b/python/pyspark/eval_handlers/tests/test_pandas_eval_type_handlers.py
@@ -0,0 +1,162 @@
+#
+# 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.
+#
+
+"""Tests for the pandas eval type handlers (``_pandas``).
+
+The handler tests build handler input in the same wire format the serializers
+produce -- a flat RecordBatch, one column per UDF argument -- and assert on the
+output batches via ``run(0, <input>)``. Handlers are built on a real 
``RunnerConf``
+(from a plain conf dict), so the tests exercise the actual conf keys the 
handler
+reads; the forwarding tests pass a single conf key to confirm it reaches the
+Arrow<->pandas conversion.
+"""
+
+import os
+import unittest
+from unittest.mock import patch
+
+from pyspark.errors import PySparkRuntimeError, PySparkTypeError, 
PySparkValueError
+from pyspark.eval_handlers._base import get_eval_type_handler
+from pyspark.sql.types import LongType, StringType, StructField, StructType
+from pyspark.testing.utils import (
+    have_pandas,
+    have_pyarrow,
+    pandas_requirement_message,
+    pyarrow_requirement_message,
+)
+from pyspark.util import PythonEvalType
+
+with patch.dict(os.environ, {"SPARK_PYTHON_RUNTIME": "PYTHON_WORKER"}):
+    from pyspark.worker_util import RunnerConf
+
+if have_pandas and have_pyarrow:
+    import pandas as pd
+    import pyarrow as pa
+
+    from pyspark.eval_handlers._pandas import PandasScalarUDFHandler
+
+_missing_message = pandas_requirement_message or pyarrow_requirement_message
+
+
+def _batch(**columns):
+    """A RecordBatch of int64 columns, one per ``name=values`` kwarg."""
+    return pa.RecordBatch.from_arrays(
+        [pa.array(values, type=pa.int64()) for values in columns.values()],
+        list(columns),
+    )
+
+
+def _udf(func, return_type=None, args=(0,), kwargs=None):
+    """A scalar pandas UDF tuple ``(func, args_offsets, kwargs_offsets, 
return_type)``."""
+    return (func, list(args), dict(kwargs or {}), return_type or LongType())
+
+
+def _handler(*udfs, runner_conf=None):
+    """Build a PandasScalarUDFHandler from one or more ``_udf`` tuples."""
+    return PandasScalarUDFHandler(
+        udfs=list(udfs), runner_conf=runner_conf or RunnerConf({}), 
eval_conf=None
+    )
+
+
[email protected](not (have_pandas and have_pyarrow), _missing_message)
+class PandasEvalTypeHandlerRegistrationTests(unittest.TestCase):
+    def test_pandas_eval_types_are_registered(self):
+        # The migrated pandas eval type dispatches to its handler by lookup.
+        self.assertIs(
+            get_eval_type_handler(PythonEvalType.SQL_SCALAR_PANDAS_UDF),
+            PandasScalarUDFHandler,
+        )
+
+
[email protected](not (have_pandas and have_pyarrow), _missing_message)
+class PandasScalarUDFHandlerTests(unittest.TestCase):
+    def test_invokes_udf_per_batch(self):
+        handler = _handler(_udf(lambda s: s + 1))
+        out = list(handler.run(0, iter([_batch(a=[1, 2, 3])])))
+        self.assertEqual([b.column(0).to_pylist() for b in out], [[2, 3, 4]])
+
+    def test_coerces_output_to_return_type(self):
+        # The UDF returns int32, but the declared return type is LongType 
(int64).
+        handler = _handler(_udf(lambda s: (s + 1).astype("int32")))
+        out = list(handler.run(0, iter([_batch(a=[10, 20])])))
+        self.assertEqual(out[0].schema.field(0).type, pa.int64())
+        self.assertEqual(out[0].column(0).to_pylist(), [11, 21])
+
+    def test_multiple_udfs_produce_one_column_each(self):
+        handler = _handler(
+            _udf(lambda s: s + 1, args=(0,)),
+            _udf(lambda s: s * 2, args=(1,)),
+        )
+        out = list(handler.run(0, iter([_batch(a=[1, 2], b=[10, 20])])))
+        self.assertEqual(out[0].column(0).to_pylist(), [2, 3])
+        self.assertEqual(out[0].column(1).to_pylist(), [20, 40])
+
+    def test_passes_arg_by_keyword_offset(self):
+        # Validates the inline args/kwargs offset handling: y is bound by 
keyword.
+        handler = _handler(_udf(lambda x, y: x - y, args=(0,), kwargs={"y": 
1}))
+        out = list(handler.run(0, iter([_batch(a=[10, 20], b=[3, 5])])))
+        self.assertEqual(out[0].column(0).to_pylist(), [7, 15])
+
+    def test_struct_return_type_takes_dataframe(self):
+        # struct_in_pandas="dict" + df_for_struct=True: a struct return is a 
DataFrame.
+        struct_type = StructType([StructField("x", LongType()), 
StructField("y", LongType())])
+        handler = _handler(
+            _udf(lambda s: pd.DataFrame({"x": s, "y": s * 10}), 
return_type=struct_type)
+        )
+        out = list(handler.run(0, iter([_batch(a=[1, 2])])))
+        self.assertEqual(
+            out[0].column(0).to_pylist(),
+            [{"x": 1, "y": 10}, {"x": 2, "y": 20}],
+        )
+
+    def test_rejects_non_sized_result(self):
+        handler = _handler(_udf(lambda s: 42))
+        with self.assertRaises(PySparkTypeError):
+            list(handler.run(0, iter([_batch(a=[1, 2])])))
+
+    def test_rejects_row_count_mismatch(self):
+        handler = _handler(_udf(lambda s: s.head(1)))
+        with self.assertRaises(PySparkRuntimeError):
+            list(handler.run(0, iter([_batch(a=[1, 2, 3])])))
+
+    def test_requires_dataframe_for_struct_return(self):
+        struct_type = StructType([StructField("x", LongType())])
+        handler = _handler(_udf(lambda s: s, return_type=struct_type))
+        with self.assertRaises(PySparkValueError):
+            list(handler.run(0, iter([_batch(a=[1, 2])])))
+
+    def test_forwards_prefer_int_ext_dtype_to_input_conversion(self):
+        # preferIntExtensionDtype flows to the Arrow->pandas conversion: the 
UDF sees
+        # a nullable "Int64" input instead of the default "int64".
+        dtype_name = _udf(lambda s: pd.Series([str(s.dtype)] * len(s)), 
return_type=StringType())
+        conf = 
RunnerConf({"spark.sql.execution.pythonUDF.pandas.preferIntExtensionDtype": 
"true"})
+        out = list(_handler(dtype_name, runner_conf=conf).run(0, 
iter([_batch(a=[1, 2])])))
+        self.assertEqual(out[0].column(0).to_pylist(), ["Int64", "Int64"])
+
+    def test_forwards_use_large_var_types_to_output_conversion(self):
+        # useLargeVarTypes flows to the pandas->Arrow conversion: a string 
result
+        # becomes large_string rather than string.
+        to_str = _udf(lambda s: s.astype(str), return_type=StringType())
+        conf = RunnerConf({"spark.sql.execution.arrow.useLargeVarTypes": 
"true"})
+        out = list(_handler(to_str, runner_conf=conf).run(0, 
iter([_batch(a=[1, 2])])))
+        self.assertTrue(pa.types.is_large_string(out[0].schema.field(0).type))
+
+
+if __name__ == "__main__":
+    from pyspark.testing import main
+
+    main()
diff --git a/python/pyspark/sql/conversion.py b/python/pyspark/sql/conversion.py
index b0c819a69d7f..0711a110d147 100644
--- a/python/pyspark/sql/conversion.py
+++ b/python/pyspark/sql/conversion.py
@@ -1824,7 +1824,7 @@ class ArrowToPandasConversion:
     def to_pandas(
         cls,
         batch: Union["pa.RecordBatch", "pa.Table"],
-        timezone: str,
+        timezone: Optional[str],
         schema: Optional["StructType"] = None,
         struct_in_pandas: str = "dict",
         ndarray_as_list: bool = False,
@@ -1838,8 +1838,9 @@ class ArrowToPandasConversion:
         ----------
         batch : pa.RecordBatch or pa.Table
             The Arrow RecordBatch or Table to convert.
-        timezone : str
-            Timezone for timestamp conversion.
+        timezone : str or None
+            Timezone for timestamp conversion. Required if the data contains
+            timestamp types.
         schema : StructType, optional
             Spark schema for type conversion. If None, types are inferred from 
Arrow.
         struct_in_pandas : str
diff --git a/python/pyspark/worker.py b/python/pyspark/worker.py
index e92fa5d7afc0..ffcf12b22c29 100644
--- a/python/pyspark/worker.py
+++ b/python/pyspark/worker.py
@@ -1817,7 +1817,6 @@ def read_udfs(pickleSer, udf_info_list, eval_type, 
runner_conf, eval_conf):
         PythonEvalType.SQL_SCALAR_PANDAS_ITER_ELEMENTWISE_UDF,
         PythonEvalType.SQL_SCALAR_ARROW_ELEMENTWISE_UDF,
         PythonEvalType.SQL_SCALAR_ARROW_ITER_ELEMENTWISE_UDF,
-        PythonEvalType.SQL_SCALAR_PANDAS_UDF,
         PythonEvalType.SQL_COGROUPED_MAP_PANDAS_UDF,
         PythonEvalType.SQL_SCALAR_PANDAS_ITER_UDF,
         PythonEvalType.SQL_MAP_PANDAS_ITER_UDF,
@@ -2946,77 +2945,6 @@ def read_udfs(pickleSer, udf_info_list, eval_type, 
runner_conf, eval_conf):
 
         return func, ser
 
-    if eval_type == PythonEvalType.SQL_SCALAR_PANDAS_UDF:
-        import pandas as pd
-        import pyarrow as pa
-
-        # --- UDF preparation ---
-        udf_infos = []
-        for udf_func, udf_args_offsets, udf_kwargs_offsets, udf_return_type in 
udfs:
-            wrapped_func, args_kwargs_offsets = wrap_kwargs_support(
-                udf_func, udf_args_offsets, udf_kwargs_offsets
-            )
-            udf_infos.append((wrapped_func, args_kwargs_offsets, 
udf_return_type))
-        return_schema = StructType(
-            [StructField(f"_{i}", info[2]) for i, info in enumerate(udf_infos)]
-        )
-
-        def func(split_index: int, data: Iterator[pa.RecordBatch]) -> 
Iterator[pa.RecordBatch]:
-            for input_batch in data:
-                num_rows = input_batch.num_rows
-
-                # --- Input: Arrow -> pandas Series (struct columns become 
DataFrames) ---
-                pandas_columns = ArrowToPandasConversion.to_pandas(
-                    input_batch,
-                    timezone=runner_conf.timezone,
-                    struct_in_pandas="dict",
-                    ndarray_as_list=False,
-                    prefer_int_ext_dtype=runner_conf.prefer_int_ext_dtype,
-                    df_for_struct=True,
-                )
-
-                # --- Process: evaluate each UDF column-wise on pandas Series 
---
-                results = []
-                for udf_func, offsets, udf_return_type in udf_infos:
-                    result = udf_func(*[pandas_columns[o] for o in offsets])
-                    if not hasattr(result, "__len__"):
-                        pd_type = (
-                            "pandas.DataFrame"
-                            if isinstance(udf_return_type, StructType)
-                            else "pandas.Series"
-                        )
-                        raise PySparkTypeError(
-                            errorClass="UDF_RETURN_TYPE",
-                            messageParameters={
-                                "expected": pd_type,
-                                "actual": type(result).__name__,
-                            },
-                        )
-                    verify_result_row_count(len(result), num_rows)
-                    # struct_in_pandas="dict": UDF must return DataFrame for 
struct types
-                    if isinstance(udf_return_type, StructType) and not 
isinstance(
-                        result, pd.DataFrame
-                    ):
-                        raise PySparkValueError(
-                            "Invalid return type. Please make sure that the 
UDF returns a "
-                            "pandas.DataFrame when the specified return type 
is StructType."
-                        )
-                    results.append(result)
-
-                # --- Output: pandas -> Arrow ---
-                yield PandasToArrowConversion.from_pandas(
-                    results,
-                    return_schema,
-                    timezone=runner_conf.timezone,
-                    safecheck=runner_conf.safecheck,
-                    arrow_cast=True,
-                    prefers_large_types=runner_conf.use_large_var_types,
-                    assign_cols_by_name=runner_conf.assign_cols_by_name,
-                    
int_to_decimal_coercion_enabled=runner_conf.int_to_decimal_coercion_enabled,
-                )
-
-        return func, ser
-
     if eval_type == PythonEvalType.SQL_SCALAR_PANDAS_ITER_UDF:
         import pandas as pd
         import pyarrow as pa


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

Reply via email to