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 ba82f4e3f8ce [SPARK-58829][PYTHON][TEST] Add tests for pa.Table.cast
ba82f4e3f8ce is described below
commit ba82f4e3f8cefbceae5565aebac62760c1330f87
Author: Spenser Sun <[email protected]>
AuthorDate: Thu Aug 20 16:01:46 2026 +0800
[SPARK-58829][PYTHON][TEST] Add tests for pa.Table.cast
### What changes were proposed in this pull request?
Add golden-file tests for `pa.Table.cast(target_schema)` under the
SPARK-54936 umbrella (monitor upstream PyArrow/pandas behavior). New file
`python/pyspark/tests/upstream/pyarrow/test_pyarrow_table_cast.py` with
`PyArrowTableCastTests`, covering both `safe=True` (default) and `safe=False`
via separate golden files, plus a `dev/sparktestsupport/modules.py`
registration.
### Why are the changes needed?
PySpark's conversion layer relies on `pa.Table.cast(schema)` in
`DataFrame.toArrow()` and `createDataFrame()` (both classic and Spark Connect).
Unlike `pa.Array.cast` (already covered by `test_pyarrow_array_cast.py`),
`Table.cast` pins genuinely Table-level behavior that the Array-level tests
cannot:
- multi-column, whole-schema casts;
- strict positional + name-equal column matching (a name mismatch, a pure
reorder, or a wrong field count all raise `ValueError` — which is why Spark
Connect pre-aligns names via `rename_columns()` before `cast()`);
- target-field nullability enforcement;
- temporal unit/timezone coercion;
- the empty-table edges (0 columns / 0 rows).
These tests fail loudly if an upstream version changes that behavior,
instead of PySpark silently returning wrong data.
### Does this PR introduce _any_ user-facing change?
No. Test-only.
### How was this patch tested?
New golden-file tests, run without a Spark session. Verified across PyArrow
18-25 x pandas 2/3 (16/16 combinations); regenerated goldens are byte-identical
and no version-specific overrides are needed (`Table.cast` is pandas-free).
### Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Opus 4.8)
This pull request and its description were written by Isaac.
Closes #58056 from Spenserrrr/table-cast-tests.
Authored-by: Spenser Sun <[email protected]>
Signed-off-by: Ruifeng Zheng <[email protected]>
---
dev/sparktestsupport/modules.py | 1 +
.../pyarrow/golden_pyarrow_table_cast_safe.csv | 21 ++
.../pyarrow/golden_pyarrow_table_cast_safe.md | 22 ++
.../pyarrow/golden_pyarrow_table_cast_unsafe.csv | 21 ++
.../pyarrow/golden_pyarrow_table_cast_unsafe.md | 22 ++
.../upstream/pyarrow/test_pyarrow_table_cast.py | 296 +++++++++++++++++++++
6 files changed, 383 insertions(+)
diff --git a/dev/sparktestsupport/modules.py b/dev/sparktestsupport/modules.py
index b098e5b18f79..68d073e6dbde 100644
--- a/dev/sparktestsupport/modules.py
+++ b/dev/sparktestsupport/modules.py
@@ -545,6 +545,7 @@ pyspark_core = Module(
"pyspark.tests.upstream.pyarrow.test_pyarrow_ignore_timezone",
"pyspark.tests.upstream.pyarrow.test_pyarrow_scalar_type_coercion",
"pyspark.tests.upstream.pyarrow.test_pyarrow_scalar_type_inference",
+ "pyspark.tests.upstream.pyarrow.test_pyarrow_table_cast",
"pyspark.tests.upstream.pyarrow.test_pyarrow_table_to_pandas",
"pyspark.tests.upstream.pyarrow.test_pyarrow_type_coercion",
],
diff --git
a/python/pyspark/tests/upstream/pyarrow/golden_pyarrow_table_cast_safe.csv
b/python/pyspark/tests/upstream/pyarrow/golden_pyarrow_table_cast_safe.csv
new file mode 100644
index 000000000000..1cdbb39f58d0
--- /dev/null
+++ b/python/pyspark/tests/upstream/pyarrow/golden_pyarrow_table_cast_safe.csv
@@ -0,0 +1,21 @@
+test case pyarrow table cast result
+types:downcast {a: [1, 2, 3], b: [1.5, 2.5, 3.5]}@Table[a: int64, b: float64]
{a: [1, 2, 3], b: [1.5, 2.5, 3.5]}@Table[a: int32, b: float32]
+types:upcast {a: [1, 2, 3], b: [1.5, 2.5, 3.5]}@Table[a: int32, b: float32]
{a: [1, 2, 3], b: [1.5, 2.5, 3.5]}@Table[a: int64, b: float64]
+overflow:int64->int32 {a: [1099511627776, 1]}@Table[a: int64] ERR@ArrowInvalid
+truncate:float->int {a: [1.9, -2.1]}@Table[a: float64] ERR@ArrowInvalid
+names:mismatch {a: [1, 2], b: [1.5, 2.5]}@Table[a: int64, b: float64]
ERR@ValueError
+names:reordered {a: [1, 2], b: [1.5, 2.5]}@Table[a: int64, b: float64]
ERR@ValueError
+names:field-count {a: [1, 2], b: [1.5, 2.5]}@Table[a: int64, b: float64]
ERR@ValueError
+nullable:false-with-nulls {a: [1, None]}@Table[a: int64] ERR@ValueError
+nullable:false-no-nulls {a: [1, 2]}@Table[a: int64] {a: [1,
2]}@Table[a: int32]
+timestamp:us->ns {ts: [1970-01-01 00:00:00, 1970-01-01
00:00:01]}@Table[ts: timestamp[us]] {ts: [1970-01-01 00:00:00, 1970-01-01
00:00:01]}@Table[ts: timestamp[ns]]
+timestamp:attach-tz {ts: [1970-01-01 00:00:00, 1970-01-01
00:00:01]}@Table[ts: timestamp[us]] {ts: [1970-01-01 00:00:00+00:00,
1970-01-01 00:00:01+00:00]}@Table[ts: timestamp[us, tz=UTC]]
+string->large_string {s: [hello, world, None]}@Table[s: string] {s:
[hello, world, None]}@Table[s: large_string]
+binary->large_binary {b: [b'x', b'yz', None]}@Table[b: binary] {b:
[b'x', b'yz', None]}@Table[b: large_binary]
+nested:list {lst: [[1, 2], [3], None]}@Table[lst: list<item: int64>]
{lst: [[1, 2], [3], None]}@Table[lst: list<item: int32>]
+nested:list-overflow {lst: [[1099511627776, 1]]}@Table[lst: list<item:
int64>] ERR@ArrowInvalid
+nested:struct {st: [[('x', 1), ('y', 'a')], None]}@Table[st: struct<x: int64,
y: string>] {st: [[('x', 1), ('y', 'a')], None]}@Table[st: struct<x: int32,
y: large_string>]
+nested:map {m: [[('k', 1), ('j', 2)], None]}@Table[m: map<string, int64>]
{m: [[('k', 1), ('j', 2)], None]}@Table[m: map<string, int32>]
+multi-chunk-column {a: [1, 2, 3, None]}@Table[a: int64] {a: [1, 2, 3,
None]}@Table[a: int32]
+empty:0-columns {}@Table[] {}@Table[]
+empty:columns-no-rows {i: [], s: []}@Table[i: int64, s: string] {i: [],
s: []}@Table[i: int32, s: large_string]
diff --git
a/python/pyspark/tests/upstream/pyarrow/golden_pyarrow_table_cast_safe.md
b/python/pyspark/tests/upstream/pyarrow/golden_pyarrow_table_cast_safe.md
new file mode 100644
index 000000000000..397f4ebb9799
--- /dev/null
+++ b/python/pyspark/tests/upstream/pyarrow/golden_pyarrow_table_cast_safe.md
@@ -0,0 +1,22 @@
+| test case | pyarrow table
| cast result
|
+|---------------------------|-----------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------|
+| types:downcast | {a: [1, 2, 3], b: [1.5, 2.5, 3.5]}@Table[a:
int64, b: float64] | {a: [1, 2, 3], b: [1.5, 2.5, 3.5]}@Table[a:
int32, b: float32] |
+| types:upcast | {a: [1, 2, 3], b: [1.5, 2.5, 3.5]}@Table[a:
int32, b: float32] | {a: [1, 2, 3], b: [1.5, 2.5, 3.5]}@Table[a:
int64, b: float64] |
+| overflow:int64->int32 | {a: [1099511627776, 1]}@Table[a: int64]
| ERR@ArrowInvalid
|
+| truncate:float->int | {a: [1.9, -2.1]}@Table[a: float64]
| ERR@ArrowInvalid
|
+| names:mismatch | {a: [1, 2], b: [1.5, 2.5]}@Table[a: int64, b:
float64] | ERR@ValueError
|
+| names:reordered | {a: [1, 2], b: [1.5, 2.5]}@Table[a: int64, b:
float64] | ERR@ValueError
|
+| names:field-count | {a: [1, 2], b: [1.5, 2.5]}@Table[a: int64, b:
float64] | ERR@ValueError
|
+| nullable:false-with-nulls | {a: [1, None]}@Table[a: int64]
| ERR@ValueError
|
+| nullable:false-no-nulls | {a: [1, 2]}@Table[a: int64]
| {a: [1, 2]}@Table[a: int32]
|
+| timestamp:us->ns | {ts: [1970-01-01 00:00:00, 1970-01-01
00:00:01]}@Table[ts: timestamp[us]] | {ts: [1970-01-01 00:00:00, 1970-01-01
00:00:01]}@Table[ts: timestamp[ns]] |
+| timestamp:attach-tz | {ts: [1970-01-01 00:00:00, 1970-01-01
00:00:01]}@Table[ts: timestamp[us]] | {ts: [1970-01-01 00:00:00+00:00,
1970-01-01 00:00:01+00:00]}@Table[ts: timestamp[us, tz=UTC]] |
+| string->large_string | {s: [hello, world, None]}@Table[s: string]
| {s: [hello, world, None]}@Table[s: large_string]
|
+| binary->large_binary | {b: [b'x', b'yz', None]}@Table[b: binary]
| {b: [b'x', b'yz', None]}@Table[b: large_binary]
|
+| nested:list | {lst: [[1, 2], [3], None]}@Table[lst: list<item:
int64>] | {lst: [[1, 2], [3], None]}@Table[lst: list<item:
int32>] |
+| nested:list-overflow | {lst: [[1099511627776, 1]]}@Table[lst:
list<item: int64>] | ERR@ArrowInvalid
|
+| nested:struct | {st: [[('x', 1), ('y', 'a')], None]}@Table[st:
struct<x: int64, y: string>] | {st: [[('x', 1), ('y', 'a')], None]}@Table[st:
struct<x: int32, y: large_string>] |
+| nested:map | {m: [[('k', 1), ('j', 2)], None]}@Table[m:
map<string, int64>] | {m: [[('k', 1), ('j', 2)], None]}@Table[m:
map<string, int32>] |
+| multi-chunk-column | {a: [1, 2, 3, None]}@Table[a: int64]
| {a: [1, 2, 3, None]}@Table[a: int32]
|
+| empty:0-columns | {}@Table[]
| {}@Table[]
|
+| empty:columns-no-rows | {i: [], s: []}@Table[i: int64, s: string]
| {i: [], s: []}@Table[i: int32, s: large_string]
|
\ No newline at end of file
diff --git
a/python/pyspark/tests/upstream/pyarrow/golden_pyarrow_table_cast_unsafe.csv
b/python/pyspark/tests/upstream/pyarrow/golden_pyarrow_table_cast_unsafe.csv
new file mode 100644
index 000000000000..f9065e14ff57
--- /dev/null
+++ b/python/pyspark/tests/upstream/pyarrow/golden_pyarrow_table_cast_unsafe.csv
@@ -0,0 +1,21 @@
+test case pyarrow table cast result
+types:downcast {a: [1, 2, 3], b: [1.5, 2.5, 3.5]}@Table[a: int64, b: float64]
{a: [1, 2, 3], b: [1.5, 2.5, 3.5]}@Table[a: int32, b: float32]
+types:upcast {a: [1, 2, 3], b: [1.5, 2.5, 3.5]}@Table[a: int32, b: float32]
{a: [1, 2, 3], b: [1.5, 2.5, 3.5]}@Table[a: int64, b: float64]
+overflow:int64->int32 {a: [1099511627776, 1]}@Table[a: int64] {a: [0,
1]}@Table[a: int32]
+truncate:float->int {a: [1.9, -2.1]}@Table[a: float64] {a: [1,
-2]}@Table[a: int64]
+names:mismatch {a: [1, 2], b: [1.5, 2.5]}@Table[a: int64, b: float64]
ERR@ValueError
+names:reordered {a: [1, 2], b: [1.5, 2.5]}@Table[a: int64, b: float64]
ERR@ValueError
+names:field-count {a: [1, 2], b: [1.5, 2.5]}@Table[a: int64, b: float64]
ERR@ValueError
+nullable:false-with-nulls {a: [1, None]}@Table[a: int64] ERR@ValueError
+nullable:false-no-nulls {a: [1, 2]}@Table[a: int64] {a: [1,
2]}@Table[a: int32]
+timestamp:us->ns {ts: [1970-01-01 00:00:00, 1970-01-01
00:00:01]}@Table[ts: timestamp[us]] {ts: [1970-01-01 00:00:00, 1970-01-01
00:00:01]}@Table[ts: timestamp[ns]]
+timestamp:attach-tz {ts: [1970-01-01 00:00:00, 1970-01-01
00:00:01]}@Table[ts: timestamp[us]] {ts: [1970-01-01 00:00:00+00:00,
1970-01-01 00:00:01+00:00]}@Table[ts: timestamp[us, tz=UTC]]
+string->large_string {s: [hello, world, None]}@Table[s: string] {s:
[hello, world, None]}@Table[s: large_string]
+binary->large_binary {b: [b'x', b'yz', None]}@Table[b: binary] {b:
[b'x', b'yz', None]}@Table[b: large_binary]
+nested:list {lst: [[1, 2], [3], None]}@Table[lst: list<item: int64>]
{lst: [[1, 2], [3], None]}@Table[lst: list<item: int32>]
+nested:list-overflow {lst: [[1099511627776, 1]]}@Table[lst: list<item:
int64>] {lst: [[0, 1]]}@Table[lst: list<item: int32>]
+nested:struct {st: [[('x', 1), ('y', 'a')], None]}@Table[st: struct<x: int64,
y: string>] {st: [[('x', 1), ('y', 'a')], None]}@Table[st: struct<x: int32,
y: large_string>]
+nested:map {m: [[('k', 1), ('j', 2)], None]}@Table[m: map<string, int64>]
{m: [[('k', 1), ('j', 2)], None]}@Table[m: map<string, int32>]
+multi-chunk-column {a: [1, 2, 3, None]}@Table[a: int64] {a: [1, 2, 3,
None]}@Table[a: int32]
+empty:0-columns {}@Table[] {}@Table[]
+empty:columns-no-rows {i: [], s: []}@Table[i: int64, s: string] {i: [],
s: []}@Table[i: int32, s: large_string]
diff --git
a/python/pyspark/tests/upstream/pyarrow/golden_pyarrow_table_cast_unsafe.md
b/python/pyspark/tests/upstream/pyarrow/golden_pyarrow_table_cast_unsafe.md
new file mode 100644
index 000000000000..f1b1ddd381e1
--- /dev/null
+++ b/python/pyspark/tests/upstream/pyarrow/golden_pyarrow_table_cast_unsafe.md
@@ -0,0 +1,22 @@
+| test case | pyarrow table
| cast result
|
+|---------------------------|-----------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------|
+| types:downcast | {a: [1, 2, 3], b: [1.5, 2.5, 3.5]}@Table[a:
int64, b: float64] | {a: [1, 2, 3], b: [1.5, 2.5, 3.5]}@Table[a:
int32, b: float32] |
+| types:upcast | {a: [1, 2, 3], b: [1.5, 2.5, 3.5]}@Table[a:
int32, b: float32] | {a: [1, 2, 3], b: [1.5, 2.5, 3.5]}@Table[a:
int64, b: float64] |
+| overflow:int64->int32 | {a: [1099511627776, 1]}@Table[a: int64]
| {a: [0, 1]}@Table[a: int32]
|
+| truncate:float->int | {a: [1.9, -2.1]}@Table[a: float64]
| {a: [1, -2]}@Table[a: int64]
|
+| names:mismatch | {a: [1, 2], b: [1.5, 2.5]}@Table[a: int64, b:
float64] | ERR@ValueError
|
+| names:reordered | {a: [1, 2], b: [1.5, 2.5]}@Table[a: int64, b:
float64] | ERR@ValueError
|
+| names:field-count | {a: [1, 2], b: [1.5, 2.5]}@Table[a: int64, b:
float64] | ERR@ValueError
|
+| nullable:false-with-nulls | {a: [1, None]}@Table[a: int64]
| ERR@ValueError
|
+| nullable:false-no-nulls | {a: [1, 2]}@Table[a: int64]
| {a: [1, 2]}@Table[a: int32]
|
+| timestamp:us->ns | {ts: [1970-01-01 00:00:00, 1970-01-01
00:00:01]}@Table[ts: timestamp[us]] | {ts: [1970-01-01 00:00:00, 1970-01-01
00:00:01]}@Table[ts: timestamp[ns]] |
+| timestamp:attach-tz | {ts: [1970-01-01 00:00:00, 1970-01-01
00:00:01]}@Table[ts: timestamp[us]] | {ts: [1970-01-01 00:00:00+00:00,
1970-01-01 00:00:01+00:00]}@Table[ts: timestamp[us, tz=UTC]] |
+| string->large_string | {s: [hello, world, None]}@Table[s: string]
| {s: [hello, world, None]}@Table[s: large_string]
|
+| binary->large_binary | {b: [b'x', b'yz', None]}@Table[b: binary]
| {b: [b'x', b'yz', None]}@Table[b: large_binary]
|
+| nested:list | {lst: [[1, 2], [3], None]}@Table[lst: list<item:
int64>] | {lst: [[1, 2], [3], None]}@Table[lst: list<item:
int32>] |
+| nested:list-overflow | {lst: [[1099511627776, 1]]}@Table[lst:
list<item: int64>] | {lst: [[0, 1]]}@Table[lst: list<item:
int32>] |
+| nested:struct | {st: [[('x', 1), ('y', 'a')], None]}@Table[st:
struct<x: int64, y: string>] | {st: [[('x', 1), ('y', 'a')], None]}@Table[st:
struct<x: int32, y: large_string>] |
+| nested:map | {m: [[('k', 1), ('j', 2)], None]}@Table[m:
map<string, int64>] | {m: [[('k', 1), ('j', 2)], None]}@Table[m:
map<string, int32>] |
+| multi-chunk-column | {a: [1, 2, 3, None]}@Table[a: int64]
| {a: [1, 2, 3, None]}@Table[a: int32]
|
+| empty:0-columns | {}@Table[]
| {}@Table[]
|
+| empty:columns-no-rows | {i: [], s: []}@Table[i: int64, s: string]
| {i: [], s: []}@Table[i: int32, s: large_string]
|
\ No newline at end of file
diff --git a/python/pyspark/tests/upstream/pyarrow/test_pyarrow_table_cast.py
b/python/pyspark/tests/upstream/pyarrow/test_pyarrow_table_cast.py
new file mode 100644
index 000000000000..1012a97c9cdd
--- /dev/null
+++ b/python/pyspark/tests/upstream/pyarrow/test_pyarrow_table_cast.py
@@ -0,0 +1,296 @@
+#
+# 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 PyArrow's pa.Table.cast() method using golden file comparison.
+
+Unlike pa.Array.cast() (covered by test_pyarrow_array_cast.py), Table.cast()
casts a
+whole table to a target *schema*. Per-column type conversion matches the Array
tests,
+so this file pins the genuinely Table-level behavior instead: multi-column
casts, strict
+field-name/order matching, target-field nullability enforcement, temporal
coercion, and
+the empty-table edges. Spark calls Table.cast() at
+python/pyspark/sql/pandas/conversion.py:499 and :1108 (classic toArrow /
createDataFrame)
+and, in Spark Connect, at python/pyspark/sql/connect/dataframe.py:1995 and
+python/pyspark/sql/connect/session.py:647,663.
+
+This suite covers both safe=True (default) and safe=False modes:
+- safe=True: checks for overflow/truncation, raising on unsafe conversions.
+- safe=False: allows unsafe conversions (overflow wrapping, truncation).
+Each mode has its own golden file. Field-name/order and nullability errors
raise in both
+modes -- they are checked before the per-column value cast.
+
+## Golden File Cell Format
+
+Each cell uses the value@type format:
+- pyarrow Table: "{col: [val1, val2, None], ...}@Table[name: type, ...]"
+- Error: "ERR@ExceptionClassName"
+
+## Regenerating Golden Files
+
+Set SPARK_GENERATE_GOLDEN_FILES=1 before running:
+
+ SPARK_GENERATE_GOLDEN_FILES=1 python -m pytest \\
+ python/pyspark/tests/upstream/pyarrow/test_pyarrow_table_cast.py
+
+If package tabulate (https://pypi.org/project/tabulate/) is installed,
+it will also regenerate the Markdown files.
+
+## PyArrow Version Compatibility
+
+The golden files capture behavior for a specific PyArrow version. Regenerate
when
+upgrading PyArrow, as cast support may change between versions. Table.cast() is
+pandas-free (PyArrow in, PyArrow out), so no pandas-version differences apply.
+"""
+
+import unittest
+
+from pyspark.testing.utils import (
+ have_pyarrow,
+ have_pandas,
+ pyarrow_requirement_message,
+ pandas_requirement_message,
+)
+from pyspark.testing.goldenutils import GoldenFileTestMixin
+
+if have_pyarrow:
+ import pyarrow as pa
+
+
+class _PyArrowTableCastTestBase(GoldenFileTestMixin, unittest.TestCase):
+ """Base class for pa.Table.cast() golden file tests. Defines no test_* of
its own."""
+
+ def _try_cast(self, table, target_schema, safe=True) -> str:
+ """
+ Cast ``table`` to ``target_schema`` and format the result as a golden
cell,
+ returning ``ERR@<ExceptionClass>`` if the cast raises. Only the cast
is guarded:
+ a formatting error is a test bug, not a cast signal, so it propagates.
+ """
+ try:
+ result = table.cast(target_schema, safe=safe)
+ except Exception as e:
+ return f"ERR@{type(e).__name__}"
+ return self.repr_value(result, max_len=0)
+
+ def _cast_scenarios(self):
+ """
+ Ordered {name: (source_table, target_schema)} pairs, each isolating one
+ Table.cast contract. Shared by the safe and unsafe test methods.
+ """
+ scenarios = {}
+
+ # =====================================================================
+ # Multi-column type cast (whole-schema assembly)
+ # =====================================================================
+ scenarios["types:downcast"] = (
+ pa.table(
+ {
+ "a": pa.array([1, 2, 3], pa.int64()),
+ "b": pa.array([1.5, 2.5, 3.5], pa.float64()),
+ }
+ ),
+ pa.schema([("a", pa.int32()), ("b", pa.float32())]),
+ )
+ scenarios["types:upcast"] = (
+ pa.table(
+ {
+ "a": pa.array([1, 2, 3], pa.int32()),
+ "b": pa.array([1.5, 2.5, 3.5], pa.float32()),
+ }
+ ),
+ pa.schema([("a", pa.int64()), ("b", pa.float64())]),
+ )
+
+ # =====================================================================
+ # safe axis: these flip between the safe and unsafe goldens
+ # =====================================================================
+ scenarios["overflow:int64->int32"] = (
+ pa.table({"a": pa.array([2**40, 1], pa.int64())}),
+ pa.schema([("a", pa.int32())]),
+ )
+ scenarios["truncate:float->int"] = (
+ pa.table({"a": pa.array([1.9, -2.1], pa.float64())}),
+ pa.schema([("a", pa.int64())]),
+ )
+
+ # =====================================================================
+ # Columns are matched positionally + name-equal (no match/reorder by
name), so
+ # a name mismatch, a pure reorder, and a wrong field count all raise
ValueError.
+ # =====================================================================
+ base_ab = pa.table(
+ {"a": pa.array([1, 2], pa.int64()), "b": pa.array([1.5, 2.5],
pa.float64())}
+ )
+ scenarios["names:mismatch"] = (
+ base_ab,
+ pa.schema([("x", pa.int32()), ("b", pa.float32())]),
+ )
+ scenarios["names:reordered"] = (
+ base_ab,
+ pa.schema([("b", pa.float64()), ("a", pa.int64())]),
+ )
+ scenarios["names:field-count"] = (
+ base_ab,
+ pa.schema([("a", pa.int32())]),
+ )
+
+ # =====================================================================
+ # Target-field nullability enforcement
+ # =====================================================================
+ scenarios["nullable:false-with-nulls"] = (
+ pa.table({"a": pa.array([1, None], pa.int64())}),
+ pa.schema([pa.field("a", pa.int32(), nullable=False)]),
+ )
+ scenarios["nullable:false-no-nulls"] = (
+ pa.table({"a": pa.array([1, 2], pa.int64())}),
+ pa.schema([pa.field("a", pa.int32(), nullable=False)]),
+ )
+
+ # =====================================================================
+ # Temporal unit / timezone coercion
+ # =====================================================================
+ ts_us = pa.table({"ts": pa.array([0, 1_000_000], pa.timestamp("us"))})
+ scenarios["timestamp:us->ns"] = (
+ ts_us,
+ pa.schema([("ts", pa.timestamp("ns"))]),
+ )
+ scenarios["timestamp:attach-tz"] = (
+ ts_us,
+ pa.schema([("ts", pa.timestamp("us", "UTC"))]),
+ )
+
+ # =====================================================================
+ # Variable-width widening
+ # =====================================================================
+ scenarios["string->large_string"] = (
+ pa.table({"s": pa.array(["hello", "world", None], pa.string())}),
+ pa.schema([("s", pa.large_string())]),
+ )
+ scenarios["binary->large_binary"] = (
+ pa.table({"b": pa.array([b"x", b"yz", None], pa.binary())}),
+ pa.schema([("b", pa.large_binary())]),
+ )
+
+ # =====================================================================
+ # Nested column types: cast recurses into the container and casts each
inner
+ # element, carrying safe= down (see nested:list-overflow). Kept name-
and
+ # order-matched so these stay clean successes on every PyArrow version.
+ # =====================================================================
+ scenarios["nested:list"] = (
+ pa.table({"lst": pa.array([[1, 2], [3], None],
pa.list_(pa.int64()))}),
+ pa.schema([("lst", pa.list_(pa.int32()))]),
+ )
+ scenarios["nested:list-overflow"] = (
+ pa.table({"lst": pa.array([[2**40, 1]], pa.list_(pa.int64()))}),
+ pa.schema([("lst", pa.list_(pa.int32()))]),
+ )
+ scenarios["nested:struct"] = (
+ pa.table(
+ {
+ "st": pa.array(
+ [{"x": 1, "y": "a"}, None],
+ pa.struct([("x", pa.int64()), ("y", pa.string())]),
+ )
+ }
+ ),
+ pa.schema([("st", pa.struct([("x", pa.int32()), ("y",
pa.large_string())]))]),
+ )
+ scenarios["nested:map"] = (
+ pa.table(
+ {"m": pa.array([[("k", 1), ("j", 2)], None],
pa.map_(pa.string(), pa.int64()))}
+ ),
+ pa.schema([("m", pa.map_(pa.string(), pa.int32()))]),
+ )
+
+ # =====================================================================
+ # Multi-chunk column: exercises pa.ChunkedArray.cast under Table.cast
+ # =====================================================================
+ scenarios["multi-chunk-column"] = (
+ pa.table({"a": pa.chunked_array([[1, 2], [3, None]], pa.int64())}),
+ pa.schema([("a", pa.int32())]),
+ )
+
+ # =====================================================================
+ # Empty edges
+ # =====================================================================
+ scenarios["empty:0-columns"] = (pa.table({}), pa.schema([]))
+ scenarios["empty:columns-no-rows"] = (
+ pa.table({"i": pa.array([], pa.int64()), "s": pa.array([],
pa.string())}),
+ pa.schema([("i", pa.int32()), ("s", pa.large_string())]),
+ )
+
+ return scenarios
+
+
[email protected](
+ not have_pyarrow or not have_pandas,
+ pyarrow_requirement_message or pandas_requirement_message,
+)
+class PyArrowTableCastTests(_PyArrowTableCastTestBase):
+ """
+ Tests pa.Table.cast(target_schema) with safe=True and safe=False via
golden files.
+
+ Pins Table-level cast behavior distinct from pa.Array.cast: whole-schema
casts,
+ strict field-name/order matching, target-field nullability enforcement,
temporal
+ coercion, and the empty-table edges.
+ """
+
+ def _run(self, safe, golden_file_prefix, overrides):
+ scenarios = self._cast_scenarios()
+ row_names = list(scenarios.keys())
+ col_names = ["pyarrow table", "cast result"]
+
+ def compute_cell(row_name, col_name):
+ source_table, target_schema = scenarios[row_name]
+ if col_name == "pyarrow table":
+ return self.repr_value(source_table, max_len=0)
+ elif col_name == "cast result":
+ return self._try_cast(source_table, target_schema, safe=safe)
+ else:
+ raise ValueError(f"unknown column: {col_name}")
+
+ self.compare_or_generate_golden_matrix(
+ row_names=row_names,
+ col_names=col_names,
+ compute_cell=compute_cell,
+ golden_file_prefix=golden_file_prefix,
+ index_name="test case",
+ overrides=overrides,
+ )
+
+ def test_table_cast_matrix(self):
+ """Test pa.Table.cast(target_schema) with safe=True (default)."""
+ # PyArrow-version-specific expected cells; empty at the pa24/pd2
baseline.
+ overrides: dict[tuple[str, str], str] = {}
+ self._run(
+ safe=True,
+ golden_file_prefix="golden_pyarrow_table_cast_safe",
+ overrides=overrides,
+ )
+
+ def test_table_cast_matrix_unsafe(self):
+ """Test pa.Table.cast(target_schema) with safe=False."""
+ overrides: dict[tuple[str, str], str] = {}
+ self._run(
+ safe=False,
+ golden_file_prefix="golden_pyarrow_table_cast_unsafe",
+ overrides=overrides,
+ )
+
+
+if __name__ == "__main__":
+ from pyspark.testing import main
+
+ main()
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]