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

Yicong-Huang 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 d45be0f202e3 [SPARK-58978][PYTHON] Simplify Arrow collect empty-batch 
handling
d45be0f202e3 is described below

commit d45be0f202e31d99242e2b9ee83b090c1ad8625a
Author: Yicong Huang <[email protected]>
AuthorDate: Tue Aug 25 22:26:12 2026 +0000

    [SPARK-58978][PYTHON] Simplify Arrow collect empty-batch handling
    
    ### What changes were proposed in this pull request?
    
    This PR simplifies the Arrow collect path in `PandasConversionMixin` 
(`python/pyspark/sql/pandas/conversion.py`) by consolidating how empty 
(zero-record) results are handled.
    
    - `toArrow()` now handles the empty-result case by building the table from 
`schema.empty_table()` (the `schema` it already computes), mirroring what 
`toPandas()` already does with `arrow_schema.empty_table()`. Both call sites 
now share the same shape: `pa.Table.from_batches(batches)` when there are 
records, `schema.empty_table()` otherwise.
    - `_collect_as_arrow()` drops its `empty_list_if_zero_records` parameter 
and the dead `else` branch that fabricated a single empty `RecordBatch` from a 
separately-built schema. The method now always returns the collected list 
(empty when there are no records) and ends in a single `return batches`.
    - The `prefers_large_var_types` parameter of `_collect_as_arrow()` is 
removed as well. It was introduced in SPARK-54300/SPARK-54396 solely to build 
the schema for that empty-batch branch; with the branch gone it is dead, so the 
two call sites no longer pass it (each still computes its own schema locally).
    
    ### Why are the changes needed?
    
    `toArrow()` already computes the full Arrow schema (with 
`error_on_duplicated_field_names_in_struct=True`, `timezone="UTC"`, and 
large-var-types) at the top of the method, so it can build the empty table 
directly, exactly as `toPandas()` already does. The old 
`empty_list_if_zero_records=False` path instead had `_collect_as_arrow()` 
rebuild a separate schema (without 
`error_on_duplicated_field_names_in_struct=True`) only to fabricate a single 
empty batch. That flag-less schema was neve [...]
    
    ### Does this PR introduce _any_ user-facing change?
    
    No. `toArrow()` and `toPandas()` return the same empty table with the same 
schema as before; only the internal code path changes.
    
    ### How was this patch tested?
    
    Existing Arrow tests in `python/pyspark/sql/tests/arrow/test_arrow.py` 
cover the empty-result paths (`test_toArrow_empty_rows`, 
`test_toArrow_empty_columns`, `test_toPandas_empty_*`) and the direct 
`_collect_as_arrow` self-destruct call. Extended 
`test_toArrow_duplicate_field_names` with a regression guard asserting that an 
empty result (`df.limit(0).toArrow()`) still rejects duplicated struct field 
names. This locks in existing behavior (the check already ran before the 
empty/non-emp [...]
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    No.
    
    Closes #58260 from Yicong-Huang/simplify-collect-empty.
    
    Authored-by: Yicong Huang <[email protected]>
    Signed-off-by: Yicong-Huang <[email protected]>
---
 python/pyspark/sql/pandas/conversion.py      | 38 ++++++++--------------------
 python/pyspark/sql/tests/arrow/test_arrow.py |  6 +++++
 2 files changed, 16 insertions(+), 28 deletions(-)

diff --git a/python/pyspark/sql/pandas/conversion.py 
b/python/pyspark/sql/pandas/conversion.py
index e5d89f90ea3c..3bdaa67b91e6 100644
--- a/python/pyspark/sql/pandas/conversion.py
+++ b/python/pyspark/sql/pandas/conversion.py
@@ -394,7 +394,6 @@ class PandasConversionMixin:
 
                     batches = self._collect_as_arrow(
                         split_batches=arrowPySparkSelfDestructEnabled == 
"true",
-                        prefers_large_var_types=prefers_large_var_types,
                     )
 
                     if len(batches) > 0:
@@ -493,12 +492,12 @@ class PandasConversionMixin:
         import pyarrow as pa
 
         self_destruct = arrowPySparkSelfDestructEnabled == "true"
-        batches = self._collect_as_arrow(
-            split_batches=self_destruct,
-            empty_list_if_zero_records=False,
-            prefers_large_var_types=prefers_large_var_types,
-        )
-        table = pa.Table.from_batches(batches).cast(schema)
+        batches = self._collect_as_arrow(split_batches=self_destruct)
+        if batches:
+            table = pa.Table.from_batches(batches).cast(schema)
+        else:
+            # empty dataset
+            table = schema.empty_table()
         # Ensure only the table has a reference to the batches, so that
         # self_destruct (if enabled) is effective
         del batches
@@ -507,21 +506,15 @@ class PandasConversionMixin:
     def _collect_as_arrow(
         self,
         split_batches: bool = False,
-        empty_list_if_zero_records: bool = True,
-        prefers_large_var_types: bool = False,
     ) -> List["pa.RecordBatch"]:
         """
-        Returns all records as a list of Arrow RecordBatches. PyArrow must be 
installed
-        and available on driver and worker Python environments.
-        This is an experimental feature.
+        Returns all records as a list of Arrow RecordBatches, which is empty 
if the result has
+        0 records. PyArrow must be installed and available on driver and 
worker Python
+        environments. This is an experimental feature.
 
         :param split_batches: split batches such that each column is in its 
own allocation, so
             that the selfDestruct optimization is effective; default False.
 
-        :param empty_list_if_zero_records: If True (the default), returns an 
empty list if the
-            result has 0 records. Otherwise, returns a list of length 1 
containing an empty
-            Arrow RecordBatch which includes the schema.
-
         .. note:: Experimental.
         """
         from pyspark.sql.dataframe import DataFrame
@@ -562,18 +555,7 @@ class PandasConversionMixin:
                 # Join serving thread and raise any exceptions from 
collectAsArrowToPython
                 jsocket_auth_server.getResult()
 
-        if len(batches) or empty_list_if_zero_records:
-            return batches
-        else:
-            import pyarrow as pa
-
-            from pyspark.sql.pandas.types import to_arrow_schema
-
-            schema = to_arrow_schema(
-                self.schema, timezone="UTC", 
prefers_large_types=prefers_large_var_types
-            )
-            empty_arrays = [pa.array([], type=field.type) for field in schema]
-            return [pa.RecordBatch.from_arrays(empty_arrays, schema=schema)]
+        return batches
 
 
 class SparkConversionMixin:
diff --git a/python/pyspark/sql/tests/arrow/test_arrow.py 
b/python/pyspark/sql/tests/arrow/test_arrow.py
index 7637efc2218e..df4239651d34 100644
--- a/python/pyspark/sql/tests/arrow/test_arrow.py
+++ b/python/pyspark/sql/tests/arrow/test_arrow.py
@@ -1427,6 +1427,12 @@ class ArrowTestsMixin:
         ):
             df.toArrow()
 
+        # An empty result must reject duplicated struct field names just like 
a non-empty one.
+        with self.assertRaisesRegex(
+            UnsupportedOperationException, 
"DUPLICATED_FIELD_NAME_IN_ARROW_STRUCT"
+        ):
+            df.limit(0).toArrow()
+
     def test_createDataFrame_pandas_duplicate_field_names(self):
         for arrow_enabled in [True, False]:
             with self.subTest(arrow_enabled=arrow_enabled):


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

Reply via email to