Files
ray-project--ray/python/ray/data/tests/unit/test_transform_pyarrow.py
T
2026-07-13 13:17:40 +08:00

3257 lines
106 KiB
Python

import re
import types
from typing import Iterable
import numpy as np
import pandas as pd
import pyarrow as pa
import pytest
from packaging.version import parse as parse_version
from ray.data._internal.arrow_ops.transform_pyarrow import (
MIN_PYARROW_VERSION_TYPE_PROMOTION,
_align_struct_fields,
_has_unhashable_pandas_types,
concat,
hash_partition,
shuffle,
try_combine_chunked_columns,
unify_schemas,
)
from ray.data._internal.tensor_extensions.arrow import (
ArrowTensorTypeV2,
_extension_array_concat_supported,
create_arrow_fixed_shape_tensor_type,
)
from ray.data._internal.utils.arrow_utils import get_pyarrow_version
from ray.data.block import BlockAccessor
from ray.data.extensions import (
ArrowConversionError,
ArrowPythonObjectArray,
ArrowPythonObjectType,
ArrowTensorArray,
ArrowTensorType,
ArrowVariableShapedTensorArray,
ArrowVariableShapedTensorType,
)
def test_try_defragment_table():
chunks = np.array_split(np.arange(1000), 10)
t = pa.Table.from_pydict(
{
"id": pa.chunked_array([pa.array(c) for c in chunks]),
}
)
assert len(t["id"].chunks) == 10
dt = try_combine_chunked_columns(t)
assert len(dt["id"].chunks) == 1
assert dt == t
def test_try_combine_chunked_columns_min_chunks_to_combine():
"""Test that the min_chunks_to_combine parameter controls the combining
threshold."""
# Create a table with 3 chunks per column.
t = pa.Table.from_pydict(
{
"a": pa.chunked_array(
[pa.array([1, 2]), pa.array([3, 4]), pa.array([5, 6])]
),
"b": pa.chunked_array(
[pa.array([7, 8]), pa.array([9, 10]), pa.array([11, 12])]
),
}
)
assert t["a"].num_chunks == 3
assert t["b"].num_chunks == 3
# Default threshold (10) should NOT combine since 3 < 10.
result = try_combine_chunked_columns(t)
assert result["a"].num_chunks == 3
assert result["b"].num_chunks == 3
# min_chunks_to_combine=1 should always combine.
result = try_combine_chunked_columns(t, min_chunks_to_combine=1)
assert result["a"].num_chunks == 1
assert result["b"].num_chunks == 1
assert result == t
# min_chunks_to_combine=3 should combine (3 >= 3).
result = try_combine_chunked_columns(t, min_chunks_to_combine=3)
assert result["a"].num_chunks == 1
assert result["b"].num_chunks == 1
# min_chunks_to_combine=4 should NOT combine (3 < 4).
result = try_combine_chunked_columns(t, min_chunks_to_combine=4)
assert result["a"].num_chunks == 3
assert result["b"].num_chunks == 3
def test_hash_partitioning():
# Test hash-partitioning of the empty table
empty_table = pa.Table.from_pydict({"idx": []})
assert {} == hash_partition(empty_table, hash_cols=["idx"], num_partitions=5)
# Test hash-partitioning of table into 1 partition (returns table itself)
t = pa.Table.from_pydict({"idx": list(range(10))})
assert {0: t} == hash_partition(t, hash_cols=["idx"], num_partitions=1)
# Test hash-partitioning of proper table
idx = list(range(100))
t = pa.Table.from_pydict(
{
"idx": pa.array(idx),
"ints": pa.array(idx),
"floats": pa.array([float(i) for i in idx]),
"strings": pa.array([str(i) for i in idx]),
"structs": pa.array(
[
{
"value": i,
}
for i in idx
]
),
}
)
single_partition_dict = hash_partition(t, hash_cols=["idx"], num_partitions=1)
# There's just 1 partition
assert len(single_partition_dict) == 1
assert t == single_partition_dict.get(0)
def _concat_and_sort_partitions(parts: Iterable[pa.Table]) -> pa.Table:
return pa.concat_tables(parts).sort_by("idx")
_5_partition_dict = hash_partition(t, hash_cols=["strings"], num_partitions=5)
assert len(_5_partition_dict) == 5
assert t == _concat_and_sort_partitions(_5_partition_dict.values())
# There could be no more partitions than elements
_structs_partition_dict = hash_partition(
t, hash_cols=["structs"], num_partitions=101
)
assert len(_structs_partition_dict) <= 101
assert t == _concat_and_sort_partitions(_structs_partition_dict.values())
@pytest.mark.parametrize(
"pa_type,expected",
[
# Nested types -> unhashable in pandas (convert to dict/list)
(pa.struct([("a", pa.int32())]), True),
(pa.list_(pa.int32()), True),
(pa.large_list(pa.int32()), True),
(pa.list_(pa.int32(), 3), True), # fixed_size_list
(pa.map_(pa.string(), pa.int32()), True),
(pa.dense_union([pa.field("x", pa.int32())]), True),
# Ray extension types -> numpy arrays / arbitrary objects in pandas
(ArrowTensorTypeV2((2, 2), pa.int64()), True),
(ArrowPythonObjectType(), True),
# Hashable primitives -> must stay False so we keep the fast path
(pa.int32(), False),
(pa.float64(), False),
(pa.bool_(), False),
(pa.string(), False),
(pa.large_string(), False),
(pa.binary(), False),
(pa.decimal128(10, 2), False),
(pa.date32(), False),
(pa.timestamp("ns"), False),
(pa.dictionary(pa.int32(), pa.string()), False),
],
)
def test_has_unhashable_pandas_types(pa_type, expected):
schema = pa.schema([("c", pa_type)])
assert _has_unhashable_pandas_types(schema) is expected
@pytest.mark.skipif(
get_pyarrow_version() < parse_version("16.0.0"),
reason="list_view / large_list_view require pyarrow 16+",
)
def test_has_unhashable_pandas_types_list_views():
# Regression: list_view/large_list_view also convert to Python lists in
# pandas, so they must be flagged as unhashable like list/large_list.
for view_type in (pa.list_view(pa.int32()), pa.large_list_view(pa.int32())):
schema = pa.schema([("c", view_type)])
assert _has_unhashable_pandas_types(schema) is True
def test_hash_partition_null_struct_consistent_across_blocks():
struct_t = pa.struct([("v", pa.int32())])
num_partitions = 8
all_null = pa.Table.from_pydict(
{"k": pa.array([None, None, None], type=struct_t), "idx": [0, 1, 2]}
)
mixed = pa.Table.from_pydict(
{
"k": pa.array([None, {"v": 1}, None], type=struct_t),
"idx": [10, 11, 12],
}
)
p1 = hash_partition(all_null, hash_cols=["k"], num_partitions=num_partitions)
p2 = hash_partition(mixed, hash_cols=["k"], num_partitions=num_partitions)
def null_partition_id(parts):
# Return the partition id holding null-key rows (there should be
# exactly one — identical null keys must co-locate).
null_pids = {
pid for pid, tbl in parts.items() if any(tbl["k"].is_null().to_pylist())
}
assert len(null_pids) == 1, null_pids
return next(iter(null_pids))
assert null_partition_id(p1) == null_partition_id(p2)
def test_shuffle():
t = pa.Table.from_pydict(
{
"index": pa.array(list(range(10))),
}
)
shuffled = shuffle(t, seed=0xDEED)
assert shuffled == pa.Table.from_pydict(
{"index": pa.array([4, 3, 6, 8, 7, 1, 5, 2, 9, 0])}
)
def test_arrow_concat_empty(simple_concat_data):
# Test empty.
assert concat(simple_concat_data["empty"]) == pa.table([])
def test_arrow_concat_single_block(simple_concat_data):
# Test single block:
out = concat([simple_concat_data["single_block"]])
assert len(out) == 2
assert out == simple_concat_data["single_block"]
def test_arrow_concat_basic(basic_concat_blocks, basic_concat_expected):
# Test two basic tables.
ts = basic_concat_blocks
out = concat(ts)
# Check length.
assert len(out) == basic_concat_expected["length"]
# Check schema.
assert out.column_names == basic_concat_expected["column_names"]
assert out.schema.types == basic_concat_expected["schema_types"]
# Confirm that concatenation is zero-copy (i.e. it didn't trigger chunk
# consolidation).
assert out["a"].num_chunks == basic_concat_expected["chunks"]
assert out["b"].num_chunks == basic_concat_expected["chunks"]
# Check content.
assert out["a"].to_pylist() == basic_concat_expected["content"]["a"]
assert out["b"].to_pylist() == basic_concat_expected["content"]["b"]
# Check equivalence.
expected = pa.concat_tables(ts)
assert out == expected
def test_arrow_concat_null_promotion(null_promotion_blocks, null_promotion_expected):
# Test null column --> well-typed column promotion.
ts = null_promotion_blocks
out = concat(ts)
# Check length.
assert len(out) == null_promotion_expected["length"]
# Check schema.
assert out.column_names == null_promotion_expected["column_names"]
assert out.schema.types == null_promotion_expected["schema_types"]
# Confirm that concatenation is zero-copy (i.e. it didn't trigger chunk
# consolidation).
assert out["a"].num_chunks == null_promotion_expected["chunks"]
assert out["b"].num_chunks == null_promotion_expected["chunks"]
# Check content.
assert out["a"].to_pylist() == null_promotion_expected["content"]["a"]
assert out["b"].to_pylist() == null_promotion_expected["content"]["b"]
# Check equivalence.
expected = pa.concat_tables(ts, promote=True)
assert out == expected
def test_arrow_concat_tensor_extension_uniform(
uniform_tensor_blocks, uniform_tensor_expected
):
# Test tensor column concatenation.
t1, t2 = uniform_tensor_blocks
ts = [t1, t2]
out = concat(ts)
# Check length.
assert len(out) == uniform_tensor_expected["length"]
# Check schema.
assert out.column_names == ["a"]
assert out.schema == uniform_tensor_expected["schema"]
# Confirm that concatenation is zero-copy (i.e. it didn't trigger chunk
# consolidation).
assert out["a"].num_chunks == uniform_tensor_expected["chunks"]
# Check content.
content = uniform_tensor_expected["content"]
np.testing.assert_array_equal(out["a"].chunk(0).to_numpy_ndarray(), content[0])
np.testing.assert_array_equal(out["a"].chunk(1).to_numpy_ndarray(), content[1])
# Check equivalence.
expected = pa.concat_tables(ts, promote=True)
assert out == expected
def test_arrow_concat_tensor_extension_variable_shaped(
variable_shaped_tensor_blocks, variable_shaped_tensor_expected
):
# Test variable_shaped tensor column concatenation.
t1, t2 = variable_shaped_tensor_blocks
ts = [t1, t2]
out = concat(ts)
# Check length.
assert len(out) == variable_shaped_tensor_expected["length"]
# Check schema.
assert out.column_names == ["a"]
assert out.schema == variable_shaped_tensor_expected["schema"]
# Confirm that concatenation is zero-copy (i.e. it didn't trigger chunk
# consolidation).
assert out["a"].num_chunks == variable_shaped_tensor_expected["chunks"]
# Check content.
content = variable_shaped_tensor_expected["content"]
for o, e in zip(out["a"].chunk(0).to_numpy(), content[0]):
np.testing.assert_array_equal(o, e)
for o, e in zip(out["a"].chunk(1).to_numpy(), content[1]):
np.testing.assert_array_equal(o, e)
# NOTE: We don't check equivalence with pyarrow.concat_tables since it currently
# fails for this case.
@pytest.mark.parametrize("preserve_order", [True, False])
def test_arrow_concat_tensor_extension_uniform_and_variable_shaped(
mixed_tensor_blocks, mixed_tensor_expected, preserve_order
):
# Test concatenating a homogeneous-shaped tensor column with a variable-shaped
# tensor column.
t1, t2 = mixed_tensor_blocks
ts = [t1, t2]
out = concat(ts, preserve_order=preserve_order)
# Check length.
assert len(out) == mixed_tensor_expected["length"]
# Check schema.
assert out.column_names == ["a"]
assert out.schema == mixed_tensor_expected["schema"]
# Confirm that concatenation is zero-copy (i.e. it didn't trigger chunk
# consolidation).
assert out["a"].num_chunks == mixed_tensor_expected["chunks"]
# Collect all arrays from output and expected.
actual = [
arr
for chunk_idx in range(out["a"].num_chunks)
for arr in out["a"].chunk(chunk_idx).to_numpy()
]
expected = [arr for chunk in mixed_tensor_expected["content"] for arr in chunk]
assert len(actual) == len(expected)
if not preserve_order:
actual = sorted(actual, key=lambda arr: arr.tobytes())
expected = sorted(expected, key=lambda arr: arr.tobytes())
for a, e in zip(actual, expected):
np.testing.assert_array_equal(a, e)
# NOTE: We don't check equivalence with pyarrow.concat_tables since it currently
# fails for this case.
def test_arrow_concat_tensor_extension_uniform_but_different(
different_shape_tensor_blocks, different_shape_tensor_expected
):
# Test concatenating two homogeneous-shaped tensor columns with differing shapes
# between them.
t1, t2 = different_shape_tensor_blocks
ts = [t1, t2]
out = concat(ts)
# Check length.
assert len(out) == different_shape_tensor_expected["length"]
# Check schema.
assert out.column_names == ["a"]
assert out.schema == different_shape_tensor_expected["schema"]
# Confirm that concatenation is zero-copy (i.e. it didn't trigger chunk
# consolidation).
assert out["a"].num_chunks == different_shape_tensor_expected["chunks"]
# Check content.
content = different_shape_tensor_expected["content"]
for o, e in zip(out["a"].chunk(0).to_numpy(), content[0]):
np.testing.assert_array_equal(o, e)
for o, e in zip(out["a"].chunk(1).to_numpy(), content[1]):
np.testing.assert_array_equal(o, e)
# NOTE: We don't check equivalence with pyarrow.concat_tables since it currently
# fails for this case.
@pytest.mark.parametrize("preserve_order", [True, False])
def test_arrow_concat_with_objects(
object_concat_blocks, object_concat_expected, preserve_order
):
t3 = concat(object_concat_blocks, preserve_order=preserve_order)
assert isinstance(t3, pa.Table)
assert len(t3) == object_concat_expected["length"]
assert isinstance(t3.schema.field("a").type, object_concat_expected["a_type"])
assert object_concat_expected["b_type"](t3.schema.field("b").type)
actual_a = t3.column("a").to_pylist()
actual_b = t3.column("b").to_pylist()
expected_a = object_concat_expected["content"]["a"]
expected_b = object_concat_expected["content"]["b"]
if preserve_order:
assert actual_a == expected_a
assert actual_b == expected_b
else:
assert sorted(actual_a, key=str) == sorted(expected_a, key=str)
assert sorted(actual_b, key=str) == sorted(expected_b, key=str)
def test_struct_with_different_field_names(
struct_different_field_names_blocks, struct_different_field_names_expected
):
# Ensures that when concatenating tables with struct columns having different
# field names, missing fields in each struct are filled with None in the
# resulting table.
# Concatenate tables with different field names in struct
t3 = concat(struct_different_field_names_blocks)
assert isinstance(t3, pa.Table)
assert len(t3) == struct_different_field_names_expected["length"]
# Check the entire schema
assert t3.schema == struct_different_field_names_expected["schema"]
# Check that missing fields are filled with None
assert (
t3.column("a").to_pylist()
== struct_different_field_names_expected["content"]["a"]
)
assert (
t3.column("d").to_pylist()
== struct_different_field_names_expected["content"]["d"]
)
def test_nested_structs(nested_structs_blocks, nested_structs_expected):
# Checks that deeply nested structs (3 levels of nesting) are handled properly
# during concatenation and the resulting table preserves the correct nesting
# structure.
# Concatenate tables with nested structs and missing fields
t3 = concat(nested_structs_blocks)
assert isinstance(t3, pa.Table)
assert len(t3) == nested_structs_expected["length"]
# Validate the schema of the resulting table
assert t3.schema == nested_structs_expected["schema"]
# Validate the data in the concatenated table
assert t3.column("a").to_pylist() == nested_structs_expected["content"]["a"]
assert t3.column("d").to_pylist() == nested_structs_expected["content"]["d"]
def test_struct_with_null_values(
struct_null_values_blocks, struct_null_values_expected
):
# Ensures that when concatenating tables with struct columns containing null
# values, the null values are properly handled, and the result reflects the
# expected structure.
# Concatenate tables with struct columns containing null values
t3 = concat(struct_null_values_blocks)
assert isinstance(t3, pa.Table)
assert len(t3) == struct_null_values_expected["length"]
# Validate the schema of the resulting table
assert (
t3.schema == struct_null_values_expected["schema"]
), f"Expected schema: {struct_null_values_expected['schema']}, but got {t3.schema}"
# Verify the PyArrow table content
assert t3.column("a").to_pylist() == struct_null_values_expected["content"]["a"]
result = t3.column("d").to_pylist()
expected = struct_null_values_expected["content"]["d"]
assert result == expected, f"Expected {expected}, but got {result}"
def test_struct_with_mismatched_lengths(
struct_mismatched_lengths_blocks, struct_mismatched_lengths_expected
):
# Verifies that when concatenating tables with struct columns of different lengths,
# the missing values are properly padded with None in the resulting table.
# Concatenate tables with struct columns of different lengths
t3 = concat(struct_mismatched_lengths_blocks)
assert isinstance(t3, pa.Table)
assert (
len(t3) == struct_mismatched_lengths_expected["length"]
) # Check that the resulting table has the correct number of rows
# Validate the schema of the resulting table
assert (
t3.schema == struct_mismatched_lengths_expected["schema"]
), f"Expected schema: {struct_mismatched_lengths_expected['schema']}, but got {t3.schema}"
# Verify the content of the resulting table
assert (
t3.column("a").to_pylist() == struct_mismatched_lengths_expected["content"]["a"]
)
result = t3.column("d").to_pylist()
expected = struct_mismatched_lengths_expected["content"]["d"]
assert result == expected, f"Expected {expected}, but got {result}"
def test_struct_with_empty_arrays(
struct_empty_arrays_blocks, struct_empty_arrays_expected
):
# Checks the behavior when concatenating tables with structs containing empty
# arrays, verifying that null structs are correctly handled.
# Concatenate tables with struct columns containing null values
t3 = concat(struct_empty_arrays_blocks)
# Verify that the concatenated result is a valid PyArrow Table
assert isinstance(t3, pa.Table)
assert (
len(t3) == struct_empty_arrays_expected["length"]
) # Check that the concatenated table has 3 rows
# Validate the schema of the resulting concatenated table
assert (
t3.schema == struct_empty_arrays_expected["schema"]
), f"Expected schema: {struct_empty_arrays_expected['schema']}, but got {t3.schema}"
# Verify the content of the concatenated table
assert t3.column("a").to_pylist() == struct_empty_arrays_expected["content"]["a"]
result = t3.column("d").to_pylist()
expected = struct_empty_arrays_expected["content"]["d"]
assert result == expected, f"Expected {expected}, but got {result}"
def test_struct_with_arrow_variable_shaped_tensor_type(
struct_variable_shaped_tensor_blocks, struct_variable_shaped_tensor_expected
):
# Test concatenating tables with struct columns containing ArrowVariableShapedTensorType
# fields, ensuring proper handling of variable-shaped tensors within structs.
# Concatenate tables with struct columns containing variable-shaped tensors
t3 = concat(struct_variable_shaped_tensor_blocks)
assert isinstance(t3, pa.Table)
assert len(t3) == struct_variable_shaped_tensor_expected["length"]
# Validate the schema of the resulting table
assert (
t3.schema == struct_variable_shaped_tensor_expected["schema"]
), f"Expected schema: {struct_variable_shaped_tensor_expected['schema']}, but got {t3.schema}"
# Verify the content of the resulting table
assert (
t3.column("id").to_pylist()
== struct_variable_shaped_tensor_expected["content"]["id"]
)
# Check that the struct column contains the expected data
result_structs = t3.column("struct_with_tensor").to_pylist()
assert len(result_structs) == 4
# Verify each struct contains the correct metadata and tensor data
expected_metadata = ["row1", "row2", "row3", "row4"]
for i, (struct, expected_meta) in enumerate(zip(result_structs, expected_metadata)):
assert struct["metadata"] == expected_meta
assert isinstance(struct["tensor"], np.ndarray)
# Verify tensor shapes match expectations
if i == 0:
assert struct["tensor"].shape == (2, 2)
np.testing.assert_array_equal(
struct["tensor"], np.ones((2, 2), dtype=np.float32)
)
elif i == 1:
assert struct["tensor"].shape == (3, 3)
np.testing.assert_array_equal(
struct["tensor"], np.zeros((3, 3), dtype=np.float32)
)
elif i == 2:
assert struct["tensor"].shape == (1, 4)
np.testing.assert_array_equal(
struct["tensor"], np.ones((1, 4), dtype=np.float32)
)
elif i == 3:
assert struct["tensor"].shape == (2, 1)
np.testing.assert_array_equal(
struct["tensor"], np.zeros((2, 1), dtype=np.float32)
)
@pytest.mark.skipif(
get_pyarrow_version() < MIN_PYARROW_VERSION_TYPE_PROMOTION,
reason="Requires PyArrow >= 14.0.0 for type promotion in nested struct fields",
)
def test_struct_with_diverging_primitive_types():
"""Test concatenating tables with struct fields that have diverging primitive types.
This tests the scenario where struct fields have the same name but different
primitive types (e.g., int64 vs float64), which requires type promotion.
"""
import pyarrow as pa
# Table 1: struct with (a: int64, b: string)
t1 = pa.table(
{
"data": pa.array(
[{"a": 1, "b": "hello"}, {"a": 2, "b": "world"}],
type=pa.struct([pa.field("a", pa.int64()), pa.field("b", pa.string())]),
)
}
)
# Table 2: struct with (a: float64, c: int32)
# Field 'a' has different type, field 'b' missing, field 'c' new
t2 = pa.table(
{
"data": pa.array(
[{"a": 1.5, "c": 100}, {"a": 2.5, "c": 200}],
type=pa.struct(
[pa.field("a", pa.float64()), pa.field("c", pa.int32())]
),
)
}
)
# Concatenate with type promotion
result = concat([t1, t2], promote_types=True)
# Verify schema: field 'a' should be promoted to float64
expected_struct_type = pa.struct(
[
pa.field("a", pa.float64()),
pa.field("b", pa.string()),
pa.field("c", pa.int32()),
]
)
assert result.schema == pa.schema([pa.field("data", expected_struct_type)])
# Verify data: int64 values should be cast to float64, missing fields filled with None
expected_data = [
{"a": 1.0, "b": "hello", "c": None},
{"a": 2.0, "b": "world", "c": None},
{"a": 1.5, "b": None, "c": 100},
{"a": 2.5, "b": None, "c": 200},
]
assert result.column("data").to_pylist() == expected_data
def test_arrow_concat_object_with_tensor_fails(object_with_tensor_fails_blocks):
with pytest.raises(ArrowConversionError) as exc_info:
concat(object_with_tensor_fails_blocks)
assert "objects and tensors" in str(exc_info.value.__cause__)
def test_unify_schemas(unify_schemas_basic_schemas, unify_schemas_multicol_schemas):
# Unifying a schema with the same schema as itself
schemas = unify_schemas_basic_schemas
assert (
unify_schemas([schemas["tensor_arr_1"], schemas["tensor_arr_1"]])
== schemas["tensor_arr_1"]
)
# Single columns with different shapes
contains_diff_shaped = [schemas["tensor_arr_1"], schemas["tensor_arr_2"]]
assert unify_schemas(contains_diff_shaped) == pa.schema(
[
("tensor_arr", ArrowVariableShapedTensorType(pa.int32(), 2)),
]
)
# Single columns with same shapes
contains_diff_types = [schemas["tensor_arr_1"], schemas["tensor_arr_3"]]
assert unify_schemas(contains_diff_types) == pa.schema(
[
("tensor_arr", ArrowTensorType((3, 5), pa.int32())),
]
)
# Single columns with a variable shaped tensor, same ndim
contains_var_shaped = [schemas["tensor_arr_1"], schemas["var_tensor_arr"]]
assert unify_schemas(contains_var_shaped) == pa.schema(
[
("tensor_arr", ArrowVariableShapedTensorType(pa.int32(), 2)),
]
)
# Single columns with a variable shaped tensor, different ndim
contains_1d2d = [schemas["tensor_arr_1"], schemas["var_tensor_arr_1d"]]
assert unify_schemas(contains_1d2d) == pa.schema(
[
("tensor_arr", ArrowVariableShapedTensorType(pa.int32(), 2)),
]
)
contains_2d3d = [schemas["tensor_arr_1"], schemas["var_tensor_arr_3d"]]
assert unify_schemas(contains_2d3d) == pa.schema(
[
("tensor_arr", ArrowVariableShapedTensorType(pa.int32(), 3)),
]
)
# Multi-column schemas
multicol = unify_schemas_multicol_schemas
assert unify_schemas(
[multicol["multicol_schema_1"], multicol["multicol_schema_2"]]
) == pa.schema(
[
("col_int", pa.int32()),
("col_fixed_tensor", ArrowTensorType((4, 2), pa.int32())),
("col_var_tensor", ArrowVariableShapedTensorType(pa.int16(), 5)),
]
)
assert unify_schemas(
[multicol["multicol_schema_1"], multicol["multicol_schema_3"]]
) == pa.schema(
[
("col_int", pa.int32()),
("col_fixed_tensor", ArrowVariableShapedTensorType(pa.int32(), 3)),
("col_var_tensor", ArrowVariableShapedTensorType(pa.int16(), 5)),
]
)
# Unifying >2 schemas together
assert unify_schemas(
[
multicol["multicol_schema_1"],
multicol["multicol_schema_2"],
multicol["multicol_schema_3"],
]
) == pa.schema(
[
("col_int", pa.int32()),
("col_fixed_tensor", ArrowVariableShapedTensorType(pa.int32(), 3)),
("col_var_tensor", ArrowVariableShapedTensorType(pa.int16(), 5)),
]
)
def test_unify_schemas_object_types(unify_schemas_object_types_schemas):
"""Test handling of object types (columns_with_objects functionality)."""
schemas = unify_schemas_object_types_schemas
# Should convert to ArrowPythonObjectType
result = unify_schemas([schemas["object_schema"], schemas["int_schema"]])
assert result == schemas["expected"]
# Test multiple object types
result = unify_schemas(
[schemas["object_schema"], schemas["int_schema"], schemas["float_schema"]]
)
assert result == schemas["expected"]
def test_unify_schemas_incompatible_tensor_dtypes(
unify_schemas_incompatible_tensor_schemas,
):
"""Test error handling for incompatible tensor dtypes."""
import pyarrow as pa
with pytest.raises(
pa.lib.ArrowTypeError,
match=re.escape(
"Can't unify tensor types with divergent scalar types: [ArrowTensorType(shape=(2, 2), dtype=int32), ArrowTensorType(shape=(2, 2), dtype=float)]"
),
):
unify_schemas(unify_schemas_incompatible_tensor_schemas)
def test_unify_schemas_objects_and_tensors(unify_schemas_objects_and_tensors_schemas):
"""Test error handling for intersection of objects and tensors."""
with pytest.raises(ValueError, match="Found columns with both objects and tensors"):
unify_schemas(unify_schemas_objects_and_tensors_schemas)
def test_unify_schemas_missing_tensor_fields(
unify_schemas_missing_tensor_fields_schemas,
):
"""Test handling of missing tensor fields in structs (has_missing_fields logic)."""
schemas = unify_schemas_missing_tensor_fields_schemas
# Should convert tensor to variable-shaped to accommodate missing field
result = unify_schemas([schemas["with_tensor"], schemas["without_tensor"]])
assert result == schemas["expected"]
def test_unify_schemas_nested_struct_tensors(
unify_schemas_nested_struct_tensors_schemas,
):
"""Test handling of nested structs with tensor fields."""
schemas = unify_schemas_nested_struct_tensors_schemas
# Should convert nested tensor to variable-shaped
result = unify_schemas([schemas["with_tensor"], schemas["without_tensor"]])
assert result == schemas["expected"]
def test_unify_schemas_edge_cases(unify_schemas_edge_cases_data):
"""Test edge cases and robustness."""
data = unify_schemas_edge_cases_data
# Empty schema list
with pytest.raises(Exception): # Should handle gracefully
unify_schemas(data["empty_schemas"])
# Single schema
assert unify_schemas([data["single_schema"]]) == data["single_schema"]
# Schemas with no common columns
result = unify_schemas(
[data["no_common_columns"]["schema1"], data["no_common_columns"]["schema2"]]
)
assert result == data["no_common_columns"]["expected"]
# All null schemas
result = unify_schemas(
[data["all_null_schemas"]["schema1"], data["all_null_schemas"]["schema2"]]
)
assert result == data["all_null_schemas"]["schema1"]
def test_unify_schemas_mixed_tensor_types(unify_schemas_mixed_tensor_data):
"""Test handling of mixed tensor types (fixed and variable shaped)."""
data = unify_schemas_mixed_tensor_data
# Should result in variable-shaped tensor
result = unify_schemas([data["fixed_shape"], data["variable_shaped"]])
assert result == data["expected_variable"]
# Test with different shapes but same dtype
result = unify_schemas([data["fixed_shape"], data["different_shape"]])
assert result == data["expected_variable"]
@pytest.mark.skipif(
get_pyarrow_version() < MIN_PYARROW_VERSION_TYPE_PROMOTION,
reason="Requires Arrow version of at least 14.0.0",
)
def test_unify_schemas_type_promotion(unify_schemas_type_promotion_data):
data = unify_schemas_type_promotion_data
# No type promotion
assert (
unify_schemas(
[data["non_null"], data["nullable"]],
promote_types=False,
)
== data["nullable"]
)
# No type promotion
with pytest.raises(pa.lib.ArrowTypeError) as exc_info:
unify_schemas(
[data["int64"], data["float64"]],
promote_types=False,
)
assert "Unable to merge: Field A has incompatible types: int64 vs double" == str(
exc_info.value
)
# Type promoted
assert (
unify_schemas(
[data["int64"], data["float64"]],
promote_types=True,
)
== data["float64"]
)
def test_arrow_block_select(block_select_data):
data = block_select_data
block_accessor = BlockAccessor.for_block(data["table"])
block = block_accessor.select(data["single_column"]["columns"])
assert block.schema == data["single_column"]["expected_schema"]
assert block.to_pandas().equals(data["df"][data["single_column"]["columns"]])
block = block_accessor.select(data["multiple_columns"]["columns"])
assert block.schema == data["multiple_columns"]["expected_schema"]
assert block.to_pandas().equals(data["df"][data["multiple_columns"]["columns"]])
with pytest.raises(ValueError):
block = block_accessor.select([lambda x: x % 3, "two"])
def test_arrow_block_slice_copy(block_slice_data):
# Test that ArrowBlock slicing properly copies the underlying Arrow
# table.
def check_for_copy(table1, table2, a, b, is_copy):
expected_slice = table1.slice(a, b - a)
assert table2.equals(expected_slice)
assert table2.schema == table1.schema
assert table1.num_columns == table2.num_columns
for col1, col2 in zip(table1.columns, table2.columns):
assert col1.num_chunks == col2.num_chunks
for chunk1, chunk2 in zip(col1.chunks, col2.chunks):
bufs1 = chunk1.buffers()
bufs2 = chunk2.buffers()
expected_offset = 0 if is_copy else a
assert chunk2.offset == expected_offset
assert len(chunk2) == b - a
if is_copy:
assert bufs2[1].address != bufs1[1].address
else:
assert bufs2[1].address == bufs1[1].address
data = block_slice_data["normal"]
table = data["table"]
a, b = data["slice_params"]["a"], data["slice_params"]["b"]
block_accessor = BlockAccessor.for_block(table)
# Test with copy.
table2 = block_accessor.slice(a, b, True)
check_for_copy(table, table2, a, b, is_copy=True)
# Test without copy.
table2 = block_accessor.slice(a, b, False)
check_for_copy(table, table2, a, b, is_copy=False)
def test_arrow_block_slice_copy_empty(block_slice_data):
# Test that ArrowBlock slicing properly copies the underlying Arrow
# table when the table is empty.
data = block_slice_data["empty"]
table = data["table"]
a, b = data["slice_params"]["a"], data["slice_params"]["b"]
expected_slice = table.slice(a, b - a)
block_accessor = BlockAccessor.for_block(table)
# Test with copy.
table2 = block_accessor.slice(a, b, True)
assert table2.equals(expected_slice)
assert table2.schema == table.schema
assert table2.num_rows == 0
# Test without copy.
table2 = block_accessor.slice(a, b, False)
assert table2.equals(expected_slice)
assert table2.schema == table.schema
assert table2.num_rows == 0
@pytest.mark.parametrize("preserve_order", [True, False])
def test_mixed_tensor_types_same_dtype(
mixed_tensor_types_same_dtype_blocks,
mixed_tensor_types_same_dtype_expected,
preserve_order,
):
"""Test mixed tensor types with same data type but different shapes."""
t1, t2 = mixed_tensor_types_same_dtype_blocks
t3 = concat([t1, t2], preserve_order=preserve_order)
assert isinstance(t3, pa.Table)
assert len(t3) == mixed_tensor_types_same_dtype_expected["length"]
# Verify schema - should have tensor field as variable-shaped
assert t3.schema == mixed_tensor_types_same_dtype_expected["schema"]
tensor_field = t3.schema.field("tensor")
assert isinstance(tensor_field.type, ArrowVariableShapedTensorType)
# Verify content
result_tensors = t3.column("tensor").to_pylist()
assert len(result_tensors) == mixed_tensor_types_same_dtype_expected["length"]
expected_tensors = mixed_tensor_types_same_dtype_expected["tensor_values"]
if not preserve_order:
result_tensors = sorted(result_tensors, key=lambda arr: arr.tobytes())
expected_tensors = sorted(expected_tensors, key=lambda arr: arr.tobytes())
for result_tensor, expected_tensor in zip(result_tensors, expected_tensors):
assert isinstance(result_tensor, np.ndarray)
assert result_tensor.shape == expected_tensor.shape
assert result_tensor.dtype == expected_tensor.dtype
np.testing.assert_array_equal(result_tensor, expected_tensor)
def test_mixed_tensor_types_fixed_shape_different(
mixed_tensor_types_fixed_shape_blocks, mixed_tensor_types_fixed_shape_expected
):
"""Test mixed tensor types with different fixed shapes."""
t1, t2 = mixed_tensor_types_fixed_shape_blocks
t3 = concat([t1, t2])
assert isinstance(t3, pa.Table)
assert len(t3) == mixed_tensor_types_fixed_shape_expected["length"]
# Verify schema - should have tensor field as variable-shaped
assert t3.schema == mixed_tensor_types_fixed_shape_expected["schema"]
tensor_field = t3.schema.field("tensor")
assert isinstance(tensor_field.type, ArrowVariableShapedTensorType)
# Verify content
result_tensors = t3.column("tensor").to_pylist()
assert len(result_tensors) == mixed_tensor_types_fixed_shape_expected["length"]
expected_tensors = mixed_tensor_types_fixed_shape_expected["tensor_values"]
# Verify each tensor
for i, (result_tensor, expected_tensor) in enumerate(
zip(result_tensors, expected_tensors)
):
assert isinstance(result_tensor, np.ndarray)
assert result_tensor.shape == expected_tensor.shape
assert result_tensor.dtype == expected_tensor.dtype
np.testing.assert_array_equal(result_tensor, expected_tensor)
def test_mixed_tensor_types_variable_shaped(
mixed_tensor_types_variable_shaped_blocks,
mixed_tensor_types_variable_shaped_expected,
):
"""Test mixed tensor types with variable-shaped tensors."""
t1, t2 = mixed_tensor_types_variable_shaped_blocks
t3 = concat([t1, t2])
assert isinstance(t3, pa.Table)
assert len(t3) == mixed_tensor_types_variable_shaped_expected["length"]
# Verify schema - should have tensor field as variable-shaped
assert t3.schema == mixed_tensor_types_variable_shaped_expected["schema"]
tensor_field = t3.schema.field("tensor")
assert isinstance(tensor_field.type, ArrowVariableShapedTensorType)
# Verify content
result_tensors = t3.column("tensor").to_pylist()
assert len(result_tensors) == mixed_tensor_types_variable_shaped_expected["length"]
expected_tensors = mixed_tensor_types_variable_shaped_expected["tensor_values"]
# Verify each tensor
for i, (result_tensor, expected_tensor) in enumerate(
zip(result_tensors, expected_tensors)
):
assert isinstance(result_tensor, np.ndarray)
assert result_tensor.shape == expected_tensor.shape
assert result_tensor.dtype == expected_tensor.dtype
np.testing.assert_array_equal(result_tensor, expected_tensor)
@pytest.mark.skipif(
not _extension_array_concat_supported(),
reason="ExtensionArrays support concatenation only in Pyarrow >= 12.0",
)
@pytest.mark.parametrize("preserve_order", [True, False])
def test_mixed_tensor_types_in_struct(
struct_with_mixed_tensor_types_blocks,
struct_with_mixed_tensor_types_expected,
preserve_order,
):
"""Test that the fix works for mixed tensor types in structs."""
t1, t2 = struct_with_mixed_tensor_types_blocks
t3 = concat([t1, t2], preserve_order=preserve_order)
assert isinstance(t3, pa.Table)
assert len(t3) == struct_with_mixed_tensor_types_expected["length"]
# Verify the result has the expected structure
assert t3.schema == struct_with_mixed_tensor_types_expected["schema"]
assert "id" in t3.column_names
assert "struct" in t3.column_names
# Verify struct field contains both types of tensors
struct_data = t3.column("struct").to_pylist()
assert len(struct_data) == struct_with_mixed_tensor_types_expected["length"]
expected_struct_values = struct_with_mixed_tensor_types_expected["struct_values"]
if not preserve_order:
# Sort both by the "id" column so we can compare element-by-element.
ids = t3.column("id").to_pylist()
order = sorted(range(len(ids)), key=lambda i: ids[i])
struct_data = [struct_data[i] for i in order]
# Verify struct values
for i, (struct_row, expected_values) in enumerate(
zip(struct_data, expected_struct_values)
):
for key, expected_value in expected_values.items():
assert struct_row[key] == expected_value
@pytest.mark.skipif(
not _extension_array_concat_supported(),
reason="ExtensionArrays support concatenation only in Pyarrow >= 12.0",
)
def test_nested_struct_with_mixed_tensor_types(
nested_struct_with_mixed_tensor_types_blocks,
nested_struct_with_mixed_tensor_types_expected,
):
"""Test nested structs with mixed tensor types at different levels."""
t1, t2 = nested_struct_with_mixed_tensor_types_blocks
t3 = concat([t1, t2])
assert isinstance(t3, pa.Table)
assert len(t3) == nested_struct_with_mixed_tensor_types_expected["length"]
# Verify the result has the expected structure
assert t3.schema == nested_struct_with_mixed_tensor_types_expected["schema"]
assert "id" in t3.column_names
assert "complex_struct" in t3.column_names
# Verify nested struct field contains both types of tensors
struct_data = t3.column("complex_struct").to_pylist()
assert len(struct_data) == nested_struct_with_mixed_tensor_types_expected["length"]
expected_fields = nested_struct_with_mixed_tensor_types_expected["expected_fields"]
# Check that nested structures are preserved
for field in expected_fields:
if field in ["nested", "outer_tensor", "outer_value"]:
assert field in struct_data[0]
elif field in ["inner_tensor", "inner_value"]:
assert field in struct_data[0]["nested"]
@pytest.mark.skipif(
not _extension_array_concat_supported(),
reason="ExtensionArrays support concatenation only in Pyarrow >= 12.0",
)
def test_multiple_tensor_fields_in_struct(
multiple_tensor_fields_struct_blocks, multiple_tensor_fields_struct_expected
):
"""Test structs with multiple tensor fields of different types."""
t1, t2 = multiple_tensor_fields_struct_blocks
t3 = concat([t1, t2])
assert isinstance(t3, pa.Table)
assert len(t3) == multiple_tensor_fields_struct_expected["length"]
# Verify the result has the expected structure
assert t3.schema == multiple_tensor_fields_struct_expected["schema"]
assert "id" in t3.column_names
assert "multi_tensor_struct" in t3.column_names
# Verify struct field contains both types of tensors
struct_data = t3.column("multi_tensor_struct").to_pylist()
assert len(struct_data) == multiple_tensor_fields_struct_expected["length"]
expected_fields = multiple_tensor_fields_struct_expected["expected_fields"]
# Check that all tensor fields are present
for row in struct_data:
for field in expected_fields:
assert field in row
def test_struct_with_incompatible_tensor_dtypes_fails():
"""Test that concatenating structs with incompatible tensor dtypes fails gracefully."""
# Block 1: Struct with float32 fixed-shape tensor
tensor_data1 = np.ones((2, 2), dtype=np.float32)
# Block 2: Struct with int64 variable-shaped tensor (different dtype)
tensor_data2 = np.array(
[
np.ones((3, 3), dtype=np.int64),
np.zeros((1, 4), dtype=np.int64),
],
dtype=object,
)
t1, t2 = _create_struct_tensor_blocks(
tensor_data1, tensor_data2, "fixed", "variable"
)
# This should fail because of incompatible tensor dtypes
with pytest.raises(
ArrowConversionError,
match=re.escape(
"Can't unify tensor types with divergent scalar types: [ArrowTensorTypeV2(shape=(2,), dtype=float), ArrowVariableShapedTensorType(ndim=2, dtype=int64)]"
),
):
concat([t1, t2])
@pytest.mark.skipif(
not _extension_array_concat_supported(),
reason="ExtensionArrays support concatenation only in Pyarrow >= 12.0",
)
@pytest.mark.parametrize("preserve_order", [True, False])
def test_struct_with_additional_fields(
struct_with_additional_fields_blocks,
struct_with_additional_fields_expected,
preserve_order,
):
"""Test structs where some blocks have additional fields."""
t1, t2 = struct_with_additional_fields_blocks
t3 = concat([t1, t2], preserve_order=preserve_order)
assert isinstance(t3, pa.Table)
assert len(t3) == struct_with_additional_fields_expected["length"]
# Verify the result has the expected structure
assert t3.schema == struct_with_additional_fields_expected["schema"]
assert "id" in t3.column_names
assert "struct" in t3.column_names
# Verify struct field contains both types of tensors
ids = t3.column("id").to_pylist()
struct_data = t3.column("struct").to_pylist()
assert len(struct_data) == struct_with_additional_fields_expected["length"]
field_presence = struct_with_additional_fields_expected["field_presence"]
extra_values = struct_with_additional_fields_expected["extra_values"]
if not preserve_order:
order = sorted(range(len(ids)), key=lambda i: ids[i])
struct_data = [struct_data[i] for i in order]
# Check field presence and values
for i, row in enumerate(struct_data):
for field, should_be_present in field_presence.items():
assert (field in row) == should_be_present
# Check extra field values
if "extra" in row:
assert row["extra"] == extra_values[i]
@pytest.mark.skipif(
not _extension_array_concat_supported(),
reason="ExtensionArrays support concatenation only in Pyarrow >= 12.0",
)
def test_struct_with_null_tensor_values(
struct_with_null_tensor_values_blocks, struct_with_null_tensor_values_expected
):
"""Test structs where some fields are missing and get filled with nulls."""
t1, t2 = struct_with_null_tensor_values_blocks
t3 = concat([t1, t2])
assert isinstance(t3, pa.Table)
assert len(t3) == struct_with_null_tensor_values_expected["length"]
# Validate schema - should have both fields
assert t3.schema == struct_with_null_tensor_values_expected["schema"]
# Validate result
assert t3.column("id").to_pylist() == struct_with_null_tensor_values_expected["ids"]
# Check the struct column directly to avoid the Arrow tensor extension null bug
struct_column = t3.column("struct")
expected_values = struct_with_null_tensor_values_expected["values"]
expected_tensor_validity = struct_with_null_tensor_values_expected[
"tensor_validity"
]
# Check each row
for i, (expected_value, expected_valid) in enumerate(
zip(expected_values, expected_tensor_validity)
):
assert struct_column[i]["value"].as_py() == expected_value
if expected_valid:
assert struct_column[i]["tensor"] is not None
else:
# Check that the tensor field is null by checking its validity
tensor_field = struct_column[i]["tensor"]
assert tensor_field.is_valid is False
# Test fixtures for _align_struct_fields tests
@pytest.fixture
def simple_struct_blocks():
"""Fixture for simple struct blocks with missing fields."""
# Block 1: Struct with fields 'a' and 'b'
struct_data1 = [{"a": 1, "b": "x"}, {"a": 2, "b": "y"}]
# Block 2: Struct with fields 'a' and 'c' (missing 'b', has 'c')
struct_data2 = [{"a": 3, "c": True}, {"a": 4, "c": False}]
return _create_basic_struct_blocks(
struct_data1, struct_data2, id_data1=None, id_data2=None
)
@pytest.fixture
def simple_struct_schema():
"""Fixture for simple struct schema with all fields."""
struct_fields = [("a", pa.int64()), ("b", pa.string()), ("c", pa.bool_())]
return _create_struct_schema(struct_fields, include_id=False)
@pytest.fixture
def nested_struct_blocks():
"""Fixture for nested struct blocks with missing fields."""
# Block 1: Nested struct with inner fields 'x' and 'y'
struct_data1 = [{"inner": {"x": 1, "y": "a"}}, {"inner": {"x": 2, "y": "b"}}]
# Block 2: Nested struct with inner fields 'x' and 'z' (missing 'y', has 'z')
struct_data2 = [{"inner": {"x": 3, "z": 1.5}}, {"inner": {"x": 4, "z": 2.5}}]
return _create_basic_struct_blocks(
struct_data1, struct_data2, column_name="outer", id_data1=None, id_data2=None
)
@pytest.fixture
def nested_struct_schema():
"""Fixture for nested struct schema with all fields."""
inner_fields = [("x", pa.int64()), ("y", pa.string()), ("z", pa.float64())]
struct_fields = [("inner", pa.struct(inner_fields))]
return _create_struct_schema(
struct_fields,
include_id=False,
other_fields=[("outer", pa.struct(struct_fields))],
)
@pytest.fixture
def missing_column_blocks():
"""Fixture for blocks where one is missing a struct column entirely."""
# Block 1: Has struct column
t1 = pa.table(
{
"struct": pa.array([{"a": 1, "b": "x"}, {"a": 2, "b": "y"}]),
"other": pa.array([10, 20]),
}
)
# Block 2: Missing struct column entirely
t2 = pa.table({"other": pa.array([30, 40])})
return t1, t2
@pytest.fixture
def missing_column_schema():
"""Fixture for schema with struct column that may be missing."""
return pa.schema(
[
("struct", pa.struct([("a", pa.int64()), ("b", pa.string())])),
("other", pa.int64()),
]
)
@pytest.fixture
def multiple_struct_blocks():
"""Fixture for blocks with multiple struct columns."""
# Block 1: Two struct columns with different field sets
struct1_data1 = [{"a": 1, "b": "x"}, {"a": 2, "b": "y"}]
struct2_data1 = [{"p": 10, "q": True}, {"p": 20, "q": False}]
# Block 2: Same struct columns but with different/missing fields
struct1_data2 = [{"a": 3, "c": 1.5}, {"a": 4, "c": 2.5}] # missing 'b', has 'c'
struct2_data2 = [
{"p": 30, "r": "alpha"},
{"p": 40, "r": "beta"},
] # missing 'q', has 'r'
t1 = pa.table(
{
"struct1": pa.array(struct1_data1),
"struct2": pa.array(struct2_data1),
}
)
t2 = pa.table(
{
"struct1": pa.array(struct1_data2),
"struct2": pa.array(struct2_data2),
}
)
return t1, t2
@pytest.fixture
def multiple_struct_schema():
"""Fixture for schema with multiple struct columns."""
struct1_fields = [("a", pa.int64()), ("b", pa.string()), ("c", pa.float64())]
struct2_fields = [("p", pa.int64()), ("q", pa.bool_()), ("r", pa.string())]
return pa.schema(
[
("struct1", pa.struct(struct1_fields)),
("struct2", pa.struct(struct2_fields)),
]
)
@pytest.fixture
def mixed_column_blocks():
"""Fixture for blocks with mix of struct and non-struct columns."""
# Block 1: Mix of struct and non-struct columns
struct_data1 = [{"a": 1, "b": "x"}, {"a": 2, "b": "y"}]
int_col1 = [10, 20]
string_col1 = ["foo", "bar"]
# Block 2: Same structure
struct_data2 = [{"a": 3, "c": True}, {"a": 4, "c": False}] # missing 'b', has 'c'
int_col2 = [30, 40]
string_col2 = ["baz", "qux"]
t1 = pa.table(
{
"struct": pa.array(struct_data1),
"int_col": pa.array(int_col1),
"string_col": pa.array(string_col1),
}
)
t2 = pa.table(
{
"struct": pa.array(struct_data2),
"int_col": pa.array(int_col2),
"string_col": pa.array(string_col2),
}
)
return t1, t2
@pytest.fixture
def mixed_column_schema():
"""Fixture for schema with mix of struct and non-struct columns."""
struct_fields = [("a", pa.int64()), ("b", pa.string()), ("c", pa.bool_())]
return pa.schema(
[
("struct", pa.struct(struct_fields)),
("int_col", pa.int64()),
("string_col", pa.string()),
]
)
@pytest.fixture
def empty_block_blocks():
"""Fixture for blocks where one is empty."""
# Empty block
empty_struct_type = pa.struct([("a", pa.int64()), ("b", pa.string())])
t1 = pa.table({"struct": pa.array([], type=empty_struct_type)})
# Non-empty block
struct_data2 = [{"a": 1, "c": True}, {"a": 2, "c": False}] # missing 'b', has 'c'
t2 = pa.table({"struct": pa.array(struct_data2)})
return t1, t2
@pytest.fixture
def empty_block_schema():
"""Fixture for schema used with empty blocks."""
struct_fields = [("a", pa.int64()), ("b", pa.string()), ("c", pa.bool_())]
return _create_struct_schema(struct_fields, include_id=False)
@pytest.fixture
def already_aligned_blocks():
"""Fixture for blocks that are already aligned."""
# Both blocks have identical schemas
struct_data1 = [{"a": 1, "b": "x"}, {"a": 2, "b": "y"}]
struct_data2 = [{"a": 3, "b": "z"}, {"a": 4, "b": "w"}]
return _create_basic_struct_blocks(
struct_data1, struct_data2, id_data1=None, id_data2=None
)
@pytest.fixture
def already_aligned_schema():
"""Fixture for schema used with already aligned blocks."""
struct_fields = [("a", pa.int64()), ("b", pa.string())]
return _create_struct_schema(struct_fields, include_id=False)
@pytest.fixture
def no_struct_blocks():
"""Fixture for blocks with no struct columns."""
# Blocks with no struct columns
int_col1 = [1, 2]
string_col1 = ["a", "b"]
int_col2 = [3, 4]
string_col2 = ["c", "d"]
t1 = pa.table({"int_col": pa.array(int_col1), "string_col": pa.array(string_col1)})
t2 = pa.table({"int_col": pa.array(int_col2), "string_col": pa.array(string_col2)})
return t1, t2
@pytest.fixture
def no_struct_schema():
"""Fixture for schema with no struct columns."""
return pa.schema([("int_col", pa.int64()), ("string_col", pa.string())])
@pytest.fixture
def deep_nesting_blocks():
"""Fixture for blocks with deeply nested structs."""
# Block 1: Deeply nested struct
struct_data1 = [
{"level2": {"level3": {"a": 1, "b": "x"}}},
{"level2": {"level3": {"a": 2, "b": "y"}}},
]
# Block 2: Same structure but missing some fields
struct_data2 = [
{"level2": {"level3": {"a": 3, "c": True}}}, # missing 'b', has 'c'
{"level2": {"level3": {"a": 4, "c": False}}},
]
return _create_basic_struct_blocks(
struct_data1, struct_data2, column_name="level1", id_data1=None, id_data2=None
)
@pytest.fixture
def deep_nesting_schema():
"""Fixture for schema with deeply nested structs."""
level3_fields = [("a", pa.int64()), ("b", pa.string()), ("c", pa.bool_())]
level2_fields = [("level3", pa.struct(level3_fields))]
level1_fields = [("level2", pa.struct(level2_fields))]
return pa.schema([("level1", pa.struct(level1_fields))])
def test_align_struct_fields_simple(simple_struct_blocks, simple_struct_schema):
"""Test basic struct field alignment with missing fields."""
t1, t2 = simple_struct_blocks
aligned_blocks = _align_struct_fields([t1, t2], simple_struct_schema)
assert len(aligned_blocks) == 2
# Check first block - should have 'c' field filled with None
result1 = aligned_blocks[0]
assert result1.schema == simple_struct_schema
assert result1["struct"].to_pylist() == [
{"a": 1, "b": "x", "c": None},
{"a": 2, "b": "y", "c": None},
]
# Check second block - should have 'b' field filled with None
result2 = aligned_blocks[1]
assert result2.schema == simple_struct_schema
assert result2["struct"].to_pylist() == [
{"a": 3, "b": None, "c": True},
{"a": 4, "b": None, "c": False},
]
def test_align_struct_fields_nested(nested_struct_blocks, nested_struct_schema):
"""Test nested struct field alignment."""
t1, t2 = nested_struct_blocks
aligned_blocks = _align_struct_fields([t1, t2], nested_struct_schema)
assert len(aligned_blocks) == 2
# Check first block - should have 'z' field filled with None
result1 = aligned_blocks[0]
assert result1.schema == nested_struct_schema
assert result1["outer"].to_pylist() == [
{"inner": {"x": 1, "y": "a", "z": None}},
{"inner": {"x": 2, "y": "b", "z": None}},
]
# Check second block - should have 'y' field filled with None
result2 = aligned_blocks[1]
assert result2.schema == nested_struct_schema
assert result2["outer"].to_pylist() == [
{"inner": {"x": 3, "y": None, "z": 1.5}},
{"inner": {"x": 4, "y": None, "z": 2.5}},
]
def test_align_struct_fields_missing_column(
missing_column_blocks, missing_column_schema
):
"""Test alignment when a struct column is missing from some blocks."""
t1, t2 = missing_column_blocks
aligned_blocks = _align_struct_fields([t1, t2], missing_column_schema)
assert len(aligned_blocks) == 2
# Check first block - should be unchanged
result1 = aligned_blocks[0]
assert result1.schema == missing_column_schema
assert result1["struct"].to_pylist() == [{"a": 1, "b": "x"}, {"a": 2, "b": "y"}]
assert result1["other"].to_pylist() == [10, 20]
# Check second block - should have null struct column
result2 = aligned_blocks[1]
assert result2.schema == missing_column_schema
assert result2["struct"].to_pylist() == [None, None]
assert result2["other"].to_pylist() == [30, 40]
def test_align_struct_fields_multiple_structs(
multiple_struct_blocks, multiple_struct_schema
):
"""Test alignment with multiple struct columns."""
t1, t2 = multiple_struct_blocks
aligned_blocks = _align_struct_fields([t1, t2], multiple_struct_schema)
assert len(aligned_blocks) == 2
# Check first block
result1 = aligned_blocks[0]
assert result1.schema == multiple_struct_schema
assert result1["struct1"].to_pylist() == [
{"a": 1, "b": "x", "c": None},
{"a": 2, "b": "y", "c": None},
]
assert result1["struct2"].to_pylist() == [
{"p": 10, "q": True, "r": None},
{"p": 20, "q": False, "r": None},
]
# Check second block
result2 = aligned_blocks[1]
assert result2.schema == multiple_struct_schema
assert result2["struct1"].to_pylist() == [
{"a": 3, "b": None, "c": 1.5},
{"a": 4, "b": None, "c": 2.5},
]
assert result2["struct2"].to_pylist() == [
{"p": 30, "q": None, "r": "alpha"},
{"p": 40, "q": None, "r": "beta"},
]
def test_align_struct_fields_non_struct_columns(
mixed_column_blocks, mixed_column_schema
):
"""Test that non-struct columns are left unchanged."""
t1, t2 = mixed_column_blocks
aligned_blocks = _align_struct_fields([t1, t2], mixed_column_schema)
assert len(aligned_blocks) == 2
# Check that non-struct columns are unchanged
for i, block in enumerate(aligned_blocks):
assert block["int_col"].to_pylist() == [10 + i * 20, 20 + i * 20]
assert (
block["string_col"].to_pylist() == ["foo", "bar"]
if i == 0
else ["baz", "qux"]
)
def test_align_struct_fields_empty_blocks(empty_block_blocks, empty_block_schema):
"""Test alignment with empty blocks."""
t1, t2 = empty_block_blocks
aligned_blocks = _align_struct_fields([t1, t2], empty_block_schema)
assert len(aligned_blocks) == 2
# Check empty block
result1 = aligned_blocks[0]
assert result1.schema == empty_block_schema
assert len(result1) == 0
# Check non-empty block
result2 = aligned_blocks[1]
assert result2.schema == empty_block_schema
assert result2["struct"].to_pylist() == [
{"a": 1, "b": None, "c": True},
{"a": 2, "b": None, "c": False},
]
def test_align_struct_fields_already_aligned(
already_aligned_blocks, already_aligned_schema
):
"""Test that already aligned blocks are returned unchanged."""
t1, t2 = already_aligned_blocks
aligned_blocks = _align_struct_fields([t1, t2], already_aligned_schema)
# Should return the original blocks unchanged
assert aligned_blocks == [t1, t2]
def test_align_struct_fields_no_struct_columns(no_struct_blocks, no_struct_schema):
"""Test alignment when there are no struct columns in the schema."""
t1, t2 = no_struct_blocks
aligned_blocks = _align_struct_fields([t1, t2], no_struct_schema)
# Should return the original blocks unchanged
assert aligned_blocks == [t1, t2]
def test_align_struct_fields_deep_nesting(deep_nesting_blocks, deep_nesting_schema):
"""Test alignment with deeply nested structs."""
t1, t2 = deep_nesting_blocks
aligned_blocks = _align_struct_fields([t1, t2], deep_nesting_schema)
assert len(aligned_blocks) == 2
# Check first block - should have 'c' field filled with None
result1 = aligned_blocks[0]
assert result1.schema == deep_nesting_schema
assert result1["level1"].to_pylist() == [
{"level2": {"level3": {"a": 1, "b": "x", "c": None}}},
{"level2": {"level3": {"a": 2, "b": "y", "c": None}}},
]
# Check second block - should have 'b' field filled with None
result2 = aligned_blocks[1]
assert result2.schema == deep_nesting_schema
assert result2["level1"].to_pylist() == [
{"level2": {"level3": {"a": 3, "b": None, "c": True}}},
{"level2": {"level3": {"a": 4, "b": None, "c": False}}},
]
# Test fixtures for tensor-related tests
@pytest.fixture
def uniform_tensor_blocks(tensor_format_context):
"""Fixture for uniform tensor blocks with same shape."""
# Block 1: Fixed shape tensors (2x2)
a1 = np.arange(12).reshape((3, 2, 2))
t1 = pa.table({"a": ArrowTensorArray.from_numpy(a1)})
# Block 2: Fixed shape tensors (2x2)
a2 = np.arange(12, 24).reshape((3, 2, 2))
t2 = pa.table({"a": ArrowTensorArray.from_numpy(a2)})
return t1, t2
@pytest.fixture
def uniform_tensor_expected(tensor_format_context):
"""Fixture for expected results from uniform tensor concatenation."""
t = create_arrow_fixed_shape_tensor_type((2, 2), pa.int64())
expected_schema = pa.schema([("a", t)])
expected_length = 6
expected_chunks = 2
# Expected content
a1 = np.arange(12).reshape((3, 2, 2))
a2 = np.arange(12, 24).reshape((3, 2, 2))
return {
"schema": expected_schema,
"length": expected_length,
"chunks": expected_chunks,
"content": [a1, a2],
}
@pytest.fixture
def variable_shaped_tensor_blocks():
"""Fixture for variable-shaped tensor blocks."""
# Block 1: Variable shape tensors
a1 = np.array(
[np.arange(4).reshape((2, 2)), np.arange(4, 13).reshape((3, 3))], dtype=object
)
t1 = pa.table({"a": ArrowTensorArray.from_numpy(a1)})
# Block 2: Variable shape tensors
a2 = np.array(
[np.arange(4).reshape((2, 2)), np.arange(4, 13).reshape((3, 3))], dtype=object
)
t2 = pa.table({"a": ArrowTensorArray.from_numpy(a2)})
return t1, t2
@pytest.fixture
def variable_shaped_tensor_expected():
"""Fixture for expected results from variable-shaped tensor concatenation."""
expected_schema = pa.schema([("a", ArrowVariableShapedTensorType(pa.int64(), 2))])
expected_length = 4
expected_chunks = 2
# Expected content
a1 = np.array(
[np.arange(4).reshape((2, 2)), np.arange(4, 13).reshape((3, 3))], dtype=object
)
a2 = np.array(
[np.arange(4).reshape((2, 2)), np.arange(4, 13).reshape((3, 3))], dtype=object
)
return {
"schema": expected_schema,
"length": expected_length,
"chunks": expected_chunks,
"content": [a1, a2],
}
@pytest.fixture
def mixed_tensor_blocks():
"""Fixture for mixed fixed-shape and variable-shaped tensor blocks."""
# Block 1: Fixed shape tensors
a1 = np.arange(12).reshape((3, 2, 2))
t1 = pa.table({"a": ArrowTensorArray.from_numpy(a1)})
# Block 2: Variable shape tensors
a2 = np.array(
[np.arange(4).reshape((2, 2)), np.arange(4, 13).reshape((3, 3))], dtype=object
)
t2 = pa.table({"a": ArrowTensorArray.from_numpy(a2)})
return t1, t2
@pytest.fixture
def mixed_tensor_expected():
"""Fixture for expected results from mixed tensor concatenation."""
expected_schema = pa.schema([("a", ArrowVariableShapedTensorType(pa.int64(), 2))])
expected_length = 5
expected_chunks = 2
# Expected content
a1 = np.arange(12).reshape((3, 2, 2))
a2 = np.array(
[np.arange(4).reshape((2, 2)), np.arange(4, 13).reshape((3, 3))], dtype=object
)
return {
"schema": expected_schema,
"length": expected_length,
"chunks": expected_chunks,
"content": [a1, a2],
}
@pytest.fixture
def different_shape_tensor_blocks():
"""Fixture for tensor blocks with different fixed shapes."""
# Block 1: Fixed shape tensors (2x2)
a1 = np.arange(12).reshape((3, 2, 2))
t1 = pa.table({"a": ArrowTensorArray.from_numpy(a1)})
# Block 2: Fixed shape tensors (3x3)
a2 = np.arange(12, 39).reshape((3, 3, 3))
t2 = pa.table({"a": ArrowTensorArray.from_numpy(a2)})
return t1, t2
@pytest.fixture
def different_shape_tensor_expected():
"""Fixture for expected results from different shape tensor concatenation."""
expected_schema = pa.schema([("a", ArrowVariableShapedTensorType(pa.int64(), 2))])
expected_length = 6
expected_chunks = 2
# Expected content
a1 = np.arange(12).reshape((3, 2, 2))
a2 = np.arange(12, 39).reshape((3, 3, 3))
return {
"schema": expected_schema,
"length": expected_length,
"chunks": expected_chunks,
"content": [a1, a2],
}
@pytest.fixture
def mixed_tensor_types_same_dtype_blocks():
"""Fixture for mixed tensor types with same dtype but different shapes."""
# Block 1: Fixed shape tensors with float32
tensor_data1 = np.ones((2, 2), dtype=np.float32)
# Block 2: Variable shape tensors with float32
tensor_data2 = np.array(
[
np.ones((3, 3), dtype=np.float32),
np.zeros((1, 4), dtype=np.float32),
],
dtype=object,
)
return _create_tensor_blocks(tensor_data1, tensor_data2, "fixed", "variable")
@pytest.fixture
def mixed_tensor_types_same_dtype_expected():
"""Fixture for expected results from mixed tensor types with same dtype."""
expected_schema = _create_tensor_schema(struct_name="tensor")
expected_tensors = [
# First 2 were converted to var-shaped with their shape expanded
# with singleton axis: from (2,) to (1, 2)
np.ones((1, 2), dtype=np.float32),
np.ones((1, 2), dtype=np.float32),
# Last 2 were left intact
np.ones((3, 3), dtype=np.float32),
np.zeros((1, 4), dtype=np.float32),
]
return _create_expected_result(expected_schema, 4, tensor_values=expected_tensors)
@pytest.fixture
def mixed_tensor_types_fixed_shape_blocks():
"""Fixture for mixed tensor types with different fixed shapes."""
# Block 1: Fixed shape tensors (2x2)
tensor_data1 = np.ones((2, 2), dtype=np.float32)
# Block 2: Fixed shape tensors (3x3)
tensor_data2 = np.zeros((3, 3), dtype=np.float32)
return _create_tensor_blocks(
tensor_data1, tensor_data2, "fixed", "fixed", id_data2=[3, 4, 5]
)
@pytest.fixture
def mixed_tensor_types_fixed_shape_expected():
"""Fixture for expected results from mixed tensor types with different fixed shapes."""
expected_schema = _create_tensor_schema(struct_name="tensor", ndim=1)
expected_tensors = [
np.ones((2,), dtype=np.float32), # First 2 converted to variable-shaped
np.ones((2,), dtype=np.float32),
np.zeros((3,), dtype=np.float32), # Last 3 variable-shaped
np.zeros((3,), dtype=np.float32),
np.zeros((3,), dtype=np.float32),
]
return _create_expected_result(expected_schema, 5, tensor_values=expected_tensors)
@pytest.fixture
def mixed_tensor_types_variable_shaped_blocks():
"""Fixture for mixed tensor types with variable-shaped tensors."""
# Block 1: Variable shape tensors
tensor_data1 = np.array(
[
np.ones((2, 2), dtype=np.float32),
np.zeros((3, 3), dtype=np.float32),
],
dtype=object,
)
# Block 2: Variable shape tensors with different shapes
tensor_data2 = np.array(
[
np.ones((1, 4), dtype=np.float32),
np.zeros((2, 1), dtype=np.float32),
],
dtype=object,
)
return _create_tensor_blocks(tensor_data1, tensor_data2, "variable", "variable")
@pytest.fixture
def mixed_tensor_types_variable_shaped_expected():
"""Fixture for expected results from mixed variable-shaped tensor types."""
expected_schema = _create_tensor_schema(struct_name="tensor")
expected_tensors = [
np.ones((2, 2), dtype=np.float32),
np.zeros((3, 3), dtype=np.float32),
np.ones((1, 4), dtype=np.float32),
np.zeros((2, 1), dtype=np.float32),
]
return _create_expected_result(expected_schema, 4, tensor_values=expected_tensors)
@pytest.fixture
def struct_with_mixed_tensor_types_blocks():
"""Fixture for struct blocks with mixed tensor types."""
# Block 1: Struct with fixed-shape tensor
tensor_data1 = np.ones((2, 2), dtype=np.float32)
# Block 2: Struct with variable-shaped tensor
tensor_data2 = np.array(
[
np.ones((3, 3), dtype=np.float32),
np.zeros((1, 4), dtype=np.float32),
],
dtype=object,
)
return _create_struct_tensor_blocks(tensor_data1, tensor_data2, "fixed", "variable")
@pytest.fixture
def struct_with_mixed_tensor_types_expected():
"""Fixture for expected results from struct with mixed tensor types."""
expected_schema = _create_tensor_schema(struct_name="struct")
expected_struct_values = [
{"value": 1}, # First two from fixed-shape tensor struct
{"value": 2},
{"value": 3}, # Last two from variable-shaped tensor struct
{"value": 4},
]
return _create_expected_result(
expected_schema, 4, struct_values=expected_struct_values
)
@pytest.fixture
def nested_struct_with_mixed_tensor_types_blocks():
"""Fixture for nested struct blocks with mixed tensor types."""
# Block 1: Nested struct with fixed-shape tensors
tensor_data1 = np.ones((2, 2), dtype=np.float32)
tensor_array1 = _create_tensor_array(tensor_data1, "fixed")
inner_struct1 = pa.StructArray.from_arrays(
[tensor_array1, pa.array([10, 20], type=pa.int64())],
names=["inner_tensor", "inner_value"],
)
outer_tensor1 = _create_tensor_array(np.zeros((2, 1), dtype=np.float32), "fixed")
outer_struct1 = pa.StructArray.from_arrays(
[inner_struct1, outer_tensor1, pa.array([1, 2], type=pa.int64())],
names=["nested", "outer_tensor", "outer_value"],
)
t1 = pa.table({"id": [1, 2], "complex_struct": outer_struct1})
# Block 2: Nested struct with variable-shaped tensors
tensor_data2 = np.array(
[
np.ones((3, 3), dtype=np.float32),
np.zeros((1, 4), dtype=np.float32),
],
dtype=object,
)
tensor_array2 = _create_tensor_array(tensor_data2, "variable")
inner_struct2 = pa.StructArray.from_arrays(
[tensor_array2, pa.array([30, 40], type=pa.int64())],
names=["inner_tensor", "inner_value"],
)
outer_tensor2 = _create_tensor_array(
np.array(
[np.ones((2, 2), dtype=np.float32), np.zeros((1, 3), dtype=np.float32)],
dtype=object,
),
"variable",
)
outer_struct2 = pa.StructArray.from_arrays(
[inner_struct2, outer_tensor2, pa.array([3, 4], type=pa.int64())],
names=["nested", "outer_tensor", "outer_value"],
)
t2 = pa.table({"id": [3, 4], "complex_struct": outer_struct2})
return t1, t2
@pytest.fixture
def nested_struct_with_mixed_tensor_types_expected():
"""Fixture for expected results from nested struct with mixed tensor types."""
expected_schema = pa.schema(
[
("id", pa.int64()),
(
"complex_struct",
pa.struct(
[
(
"nested",
pa.struct(
[
(
"inner_tensor",
ArrowVariableShapedTensorType(pa.float32(), 2),
),
("inner_value", pa.int64()),
]
),
),
(
"outer_tensor",
ArrowVariableShapedTensorType(pa.float32(), 2),
),
("outer_value", pa.int64()),
]
),
),
]
)
expected_fields = [
"nested",
"outer_tensor",
"outer_value",
"inner_tensor",
"inner_value",
]
return _create_expected_result(expected_schema, 4, expected_fields=expected_fields)
@pytest.fixture
def multiple_tensor_fields_struct_blocks():
"""Fixture for struct blocks with multiple tensor fields."""
# Block 1: Struct with multiple fixed-shape tensors
tensor1_data = np.ones((2, 2), dtype=np.float32)
tensor1_array = _create_tensor_array(tensor1_data, "fixed")
tensor2_data = np.zeros((2, 3), dtype=np.int32)
tensor2_array = _create_tensor_array(tensor2_data, "fixed")
struct_array1 = pa.StructArray.from_arrays(
[tensor1_array, tensor2_array, pa.array([1, 2], type=pa.int64())],
names=["tensor1", "tensor2", "value"],
)
t1 = pa.table({"id": [1, 2], "multi_tensor_struct": struct_array1})
# Block 2: Struct with multiple variable-shaped tensors
tensor1_data2 = np.array(
[
np.ones((3, 3), dtype=np.float32),
np.zeros((1, 4), dtype=np.float32),
],
dtype=object,
)
tensor1_array2 = _create_tensor_array(tensor1_data2, "variable")
tensor2_data2 = np.array(
[
np.ones((2, 2), dtype=np.int32),
np.zeros((3, 1), dtype=np.int32),
],
dtype=object,
)
tensor2_array2 = _create_tensor_array(tensor2_data2, "variable")
struct_array2 = pa.StructArray.from_arrays(
[tensor1_array2, tensor2_array2, pa.array([3, 4], type=pa.int64())],
names=["tensor1", "tensor2", "value"],
)
t2 = pa.table({"id": [3, 4], "multi_tensor_struct": struct_array2})
return t1, t2
@pytest.fixture
def multiple_tensor_fields_struct_expected():
"""Fixture for expected results from struct with multiple tensor fields."""
expected_schema = pa.schema(
[
("id", pa.int64()),
(
"multi_tensor_struct",
pa.struct(
[
("tensor1", ArrowVariableShapedTensorType(pa.float32(), 2)),
("tensor2", ArrowVariableShapedTensorType(pa.int32(), 2)),
("value", pa.int64()),
]
),
),
]
)
expected_fields = ["tensor1", "tensor2", "value"]
return _create_expected_result(expected_schema, 4, expected_fields=expected_fields)
@pytest.fixture
def struct_with_additional_fields_blocks():
"""Fixture for struct blocks where some have additional fields."""
# Block 1: Struct with tensor field and basic fields
tensor_data1 = np.ones((2, 2), dtype=np.float32)
# Block 2: Struct with tensor field and additional fields
tensor_data2 = np.array(
[
np.ones((3, 3), dtype=np.float32),
np.zeros((1, 4), dtype=np.float32),
],
dtype=object,
)
return _create_struct_tensor_blocks(
tensor_data1, tensor_data2, "fixed", "variable", extra_data2=["a", "b"]
)
@pytest.fixture
def struct_with_additional_fields_expected():
"""Fixture for expected results from struct with additional fields."""
expected_schema = _create_tensor_schema(struct_name="struct", include_extra=True)
expected_field_presence = {"tensor": True, "value": True, "extra": True}
expected_extra_values = [None, None, "a", "b"]
return _create_expected_result(
expected_schema,
4,
field_presence=expected_field_presence,
extra_values=expected_extra_values,
)
@pytest.fixture
def struct_with_null_tensor_values_blocks():
"""Fixture for struct blocks where some fields are missing and get filled with nulls."""
# Block 1: Struct with tensor and value fields
tensor_data1 = np.ones((2, 2), dtype=np.float32)
tensor_array1 = ArrowTensorArray.from_numpy(tensor_data1)
value_array1 = pa.array([1, 2], type=pa.int64())
struct_array1 = pa.StructArray.from_arrays(
[tensor_array1, value_array1], names=["tensor", "value"]
)
t1 = pa.table({"id": [1, 2], "struct": struct_array1})
# Block 2: Struct with only value field (missing tensor field)
value_array2 = pa.array([3], type=pa.int64())
struct_array2 = pa.StructArray.from_arrays([value_array2], names=["value"])
t2 = pa.table({"id": [3], "struct": struct_array2})
return t1, t2
@pytest.fixture
def struct_with_null_tensor_values_expected():
"""Fixture for expected results from struct with null tensor values."""
expected_schema = pa.schema(
[
("id", pa.int64()),
(
"struct",
pa.struct(
[
("tensor", ArrowTensorTypeV2((2,), pa.float32())),
("value", pa.int64()),
]
),
),
]
)
expected_length = 3
expected_ids = [1, 2, 3]
# Expected value field values
expected_values = [1, 2, 3]
# Expected tensor field validity
expected_tensor_validity = [True, True, False]
return {
"schema": expected_schema,
"length": expected_length,
"ids": expected_ids,
"values": expected_values,
"tensor_validity": expected_tensor_validity,
}
@pytest.fixture
def basic_concat_blocks():
"""Fixture for basic concat test data."""
t1 = pa.table({"a": [1, 2], "b": [5, 6]})
t2 = pa.table({"a": [3, 4], "b": [7, 8]})
return [t1, t2]
@pytest.fixture
def basic_concat_expected():
"""Fixture for basic concat expected results."""
return {
"length": 4,
"column_names": ["a", "b"],
"schema_types": [pa.int64(), pa.int64()],
"chunks": 2,
"content": {"a": [1, 2, 3, 4], "b": [5, 6, 7, 8]},
}
@pytest.fixture
def null_promotion_blocks():
"""Fixture for null promotion test data."""
t1 = pa.table({"a": [None, None], "b": [5, 6]})
t2 = pa.table({"a": [3, 4], "b": [None, None]})
return [t1, t2]
@pytest.fixture
def null_promotion_expected():
"""Fixture for null promotion expected results."""
return {
"length": 4,
"column_names": ["a", "b"],
"schema_types": [pa.int64(), pa.int64()],
"chunks": 2,
"content": {"a": [None, None, 3, 4], "b": [5, 6, None, None]},
}
@pytest.fixture
def struct_different_field_names_blocks():
"""Fixture for struct with different field names test data."""
struct_data1 = [{"x": 1, "y": "a"}, {"x": 2, "y": "b"}]
struct_data2 = [{"x": 3, "z": "c"}]
struct_type1 = pa.struct([("x", pa.int32()), ("y", pa.string())])
struct_type2 = pa.struct([("x", pa.int32()), ("z", pa.string())])
additional_columns1 = {"a": [1, 2]}
additional_columns2 = {"a": [3]}
return _create_struct_blocks_with_columns(
struct_data1,
struct_data2,
struct_type1,
struct_type2,
additional_columns1,
additional_columns2,
)
@pytest.fixture
def struct_different_field_names_expected():
"""Fixture for struct with different field names expected results."""
field_names = ["x", "y", "z"]
field_types = [pa.int32(), pa.string(), pa.string()]
additional_fields = [("a", pa.int64())]
schema = _create_simple_struct_schema(field_names, field_types, additional_fields)
content = {
"a": [1, 2, 3],
"d": [
{"x": 1, "y": "a", "z": None},
{"x": 2, "y": "b", "z": None},
{"x": 3, "y": None, "z": "c"},
],
}
return _create_struct_expected_result(schema, 3, content)
@pytest.fixture
def nested_structs_blocks():
"""Fixture for nested structs test data."""
t1 = pa.table(
{
"a": [1],
"d": pa.array(
[
{
"x": {
"y": {"p": 1}, # Missing "q"
"z": {"m": 3}, # Missing "n"
},
"w": 5,
}
],
type=pa.struct(
[
(
"x",
pa.struct(
[
(
"y",
pa.struct([("p", pa.int32())]), # Only "p"
),
(
"z",
pa.struct([("m", pa.int32())]), # Only "m"
),
]
),
),
("w", pa.int32()),
]
),
),
}
)
t2 = pa.table(
{
"a": [2],
"d": pa.array(
[
{
"x": {
"y": {"q": 7}, # Missing "p"
"z": {"n": 9}, # Missing "m"
},
"w": 10,
}
],
type=pa.struct(
[
(
"x",
pa.struct(
[
(
"y",
pa.struct([("q", pa.int32())]), # Only "q"
),
(
"z",
pa.struct([("n", pa.int32())]), # Only "n"
),
]
),
),
("w", pa.int32()),
]
),
),
}
)
return [t1, t2]
@pytest.fixture
def nested_structs_expected():
"""Fixture for nested structs expected results."""
return {
"length": 2,
"schema": pa.schema(
[
("a", pa.int64()),
(
"d",
pa.struct(
[
(
"x",
pa.struct(
[
(
"y",
pa.struct(
[("p", pa.int32()), ("q", pa.int32())]
),
),
(
"z",
pa.struct(
[("m", pa.int32()), ("n", pa.int32())]
),
),
]
),
),
("w", pa.int32()),
]
),
),
]
),
"content": {
"a": [1, 2],
"d": [
{
"x": {
"y": {"p": 1, "q": None}, # Missing "q" filled with None
"z": {"m": 3, "n": None}, # Missing "n" filled with None
},
"w": 5,
},
{
"x": {
"y": {"p": None, "q": 7}, # Missing "p" filled with None
"z": {"m": None, "n": 9}, # Missing "m" filled with None
},
"w": 10,
},
],
},
}
@pytest.fixture
def struct_null_values_blocks():
"""Fixture for struct with null values test data."""
struct_data1 = [{"x": 1, "y": "a"}, None] # Second row is null
struct_data2 = [None] # Entire struct is null
field_names = ["x", "y"]
field_types = [pa.int32(), pa.string()]
additional_columns1 = {"a": [1, 2]}
additional_columns2 = {"a": [3]}
return _create_simple_struct_blocks(
struct_data1,
struct_data2,
field_names,
field_types,
additional_columns1,
additional_columns2,
)
@pytest.fixture
def struct_null_values_expected():
"""Fixture for struct with null values expected results."""
field_names = ["x", "y"]
field_types = [pa.int32(), pa.string()]
additional_fields = [("a", pa.int64())]
schema = _create_simple_struct_schema(field_names, field_types, additional_fields)
content = {
"a": [1, 2, 3],
"d": [
{"x": 1, "y": "a"},
None, # Entire struct is None, not {"x": None, "y": None}
None, # Entire struct is None, not {"x": None, "y": None}
],
}
return _create_struct_expected_result(schema, 3, content)
@pytest.fixture
def struct_mismatched_lengths_blocks():
"""Fixture for struct with mismatched lengths test data."""
struct_data1 = [{"x": 1, "y": "a"}, {"x": 2, "y": "b"}]
struct_data2 = [{"x": 3, "y": "c"}]
field_names = ["x", "y"]
field_types = [pa.int32(), pa.string()]
additional_columns1 = {"a": [1, 2]}
additional_columns2 = {"a": [3]}
return _create_simple_struct_blocks(
struct_data1,
struct_data2,
field_names,
field_types,
additional_columns1,
additional_columns2,
)
@pytest.fixture
def struct_mismatched_lengths_expected():
"""Fixture for struct with mismatched lengths expected results."""
field_names = ["x", "y"]
field_types = [pa.int32(), pa.string()]
additional_fields = [("a", pa.int64())]
schema = _create_simple_struct_schema(field_names, field_types, additional_fields)
content = {
"a": [1, 2, 3],
"d": [
{"x": 1, "y": "a"},
{"x": 2, "y": "b"},
{"x": 3, "y": "c"},
],
}
return _create_struct_expected_result(schema, 3, content)
@pytest.fixture
def struct_empty_arrays_blocks():
"""Fixture for struct with empty arrays test data."""
struct_data1 = [{"x": 1, "y": "a"}, {"x": 2, "y": "b"}]
# Define the second table with null struct value (empty arrays for fields)
x_array = pa.array([None], type=pa.int32())
y_array = pa.array([None], type=pa.string())
# Create a struct array from null field arrays
null_struct_array = pa.StructArray.from_arrays(
[x_array, y_array],
["x", "y"],
mask=pa.array([True]),
)
t1 = pa.table(
{
"a": [1, 2],
"d": pa.array(
struct_data1, type=pa.struct([("x", pa.int32()), ("y", pa.string())])
),
}
)
t2 = pa.table({"a": [3], "d": null_struct_array})
return [t1, t2]
@pytest.fixture
def struct_empty_arrays_expected():
"""Fixture for struct with empty arrays expected results."""
field_names = ["x", "y"]
field_types = [pa.int32(), pa.string()]
additional_fields = [("a", pa.int64())]
schema = _create_simple_struct_schema(field_names, field_types, additional_fields)
content = {
"a": [1, 2, 3],
"d": [
{"x": 1, "y": "a"},
{"x": 2, "y": "b"},
None, # Entire struct is None, as PyArrow handles it
],
}
return _create_struct_expected_result(schema, 3, content)
@pytest.fixture
def unify_schemas_basic_schemas():
"""Fixture for basic unify schemas test data."""
tensor_arr_1 = pa.schema([("tensor_arr", ArrowTensorType((3, 5), pa.int32()))])
tensor_arr_2 = pa.schema([("tensor_arr", ArrowTensorType((2, 1), pa.int32()))])
tensor_arr_3 = pa.schema([("tensor_arr", ArrowTensorType((3, 5), pa.int32()))])
var_tensor_arr = pa.schema(
[
("tensor_arr", ArrowVariableShapedTensorType(pa.int32(), 2)),
]
)
var_tensor_arr_1d = pa.schema(
[
("tensor_arr", ArrowVariableShapedTensorType(pa.int32(), 1)),
]
)
var_tensor_arr_3d = pa.schema(
[
("tensor_arr", ArrowVariableShapedTensorType(pa.int32(), 3)),
]
)
return {
"tensor_arr_1": tensor_arr_1,
"tensor_arr_2": tensor_arr_2,
"tensor_arr_3": tensor_arr_3,
"var_tensor_arr": var_tensor_arr,
"var_tensor_arr_1d": var_tensor_arr_1d,
"var_tensor_arr_3d": var_tensor_arr_3d,
}
@pytest.fixture
def unify_schemas_multicol_schemas():
"""Fixture for multi-column unify schemas test data."""
multicol_schema_1 = pa.schema(
[
("col_int", pa.int32()),
("col_fixed_tensor", ArrowTensorType((4, 2), pa.int32())),
("col_var_tensor", ArrowVariableShapedTensorType(pa.int16(), 5)),
]
)
multicol_schema_2 = pa.schema(
[
("col_int", pa.int32()),
("col_fixed_tensor", ArrowTensorType((4, 2), pa.int32())),
("col_var_tensor", ArrowTensorType((9, 4, 1, 0, 5), pa.int16())),
]
)
multicol_schema_3 = pa.schema(
[
("col_int", pa.int32()),
("col_fixed_tensor", ArrowVariableShapedTensorType(pa.int32(), 3)),
("col_var_tensor", ArrowVariableShapedTensorType(pa.int16(), 5)),
]
)
return {
"multicol_schema_1": multicol_schema_1,
"multicol_schema_2": multicol_schema_2,
"multicol_schema_3": multicol_schema_3,
}
@pytest.fixture
def object_concat_blocks():
"""Fixture for object concat test data."""
obj = types.SimpleNamespace(a=1, b="test")
t1 = pa.table({"a": [3, 4], "b": [7, 8]})
t2 = pa.table({"a": ArrowPythonObjectArray.from_objects([obj, obj]), "b": [0, 1]})
return [t1, t2]
@pytest.fixture
def object_concat_expected():
"""Fixture for object concat expected results."""
obj = types.SimpleNamespace(a=1, b="test")
return {
"length": 4,
"a_type": ArrowPythonObjectType,
"b_type": pa.types.is_integer,
"content": {"a": [3, 4, obj, obj], "b": [7, 8, 0, 1]},
}
@pytest.fixture
def struct_variable_shaped_tensor_blocks():
"""Fixture for struct with variable shaped tensor test data."""
# Create variable-shaped tensor data for the first table
tensor_data1 = np.array(
[
np.ones((2, 2), dtype=np.float32),
np.zeros((3, 3), dtype=np.float32),
],
dtype=object,
)
tensor_array1 = ArrowVariableShapedTensorArray.from_numpy(tensor_data1)
# Create struct data with tensor field for the first table
metadata_array1 = pa.array(["row1", "row2"])
struct_array1 = pa.StructArray.from_arrays(
[metadata_array1, tensor_array1], names=["metadata", "tensor"]
)
t1 = pa.table({"id": [1, 2], "struct_with_tensor": struct_array1})
# Create variable-shaped tensor data for the second table
tensor_data2 = np.array(
[
np.ones((1, 4), dtype=np.float32),
np.zeros((2, 1), dtype=np.float32),
],
dtype=object,
)
tensor_array2 = ArrowVariableShapedTensorArray.from_numpy(tensor_data2)
# Create struct data with tensor field for the second table
metadata_array2 = pa.array(["row3", "row4"])
struct_array2 = pa.StructArray.from_arrays(
[metadata_array2, tensor_array2], names=["metadata", "tensor"]
)
t2 = pa.table({"id": [3, 4], "struct_with_tensor": struct_array2})
return [t1, t2]
@pytest.fixture
def struct_variable_shaped_tensor_expected():
"""Fixture for struct with variable shaped tensor expected results."""
return {
"length": 4,
"schema": pa.schema(
[
("id", pa.int64()),
(
"struct_with_tensor",
pa.struct(
[
("metadata", pa.string()),
("tensor", ArrowVariableShapedTensorType(pa.float32(), 2)),
]
),
),
]
),
"content": {"id": [1, 2, 3, 4]},
}
@pytest.fixture
def unify_schemas_object_types_schemas():
"""Fixture for object types unify schemas test data."""
from ray.data._internal.object_extensions.arrow import ArrowPythonObjectType
schema1 = pa.schema([("obj_col", ArrowPythonObjectType())])
schema2 = pa.schema([("obj_col", pa.int32())])
schema3 = pa.schema([("obj_col", pa.float64())])
expected = pa.schema([("obj_col", ArrowPythonObjectType())])
return {
"object_schema": schema1,
"int_schema": schema2,
"float_schema": schema3,
"expected": expected,
}
@pytest.fixture
def unify_schemas_incompatible_tensor_schemas():
"""Fixture for incompatible tensor dtypes unify schemas test data."""
schema1 = pa.schema([("tensor", ArrowTensorType((2, 2), pa.int32()))])
schema2 = pa.schema([("tensor", ArrowTensorType((2, 2), pa.float32()))])
return [schema1, schema2]
@pytest.fixture
def unify_schemas_objects_and_tensors_schemas():
"""Fixture for objects and tensors unify schemas test data."""
from ray.data._internal.object_extensions.arrow import ArrowPythonObjectType
schema1 = pa.schema([("col", ArrowPythonObjectType())])
schema2 = pa.schema([("col", ArrowTensorType((2, 2), pa.int32()))])
return [schema1, schema2]
@pytest.fixture
def unify_schemas_missing_tensor_fields_schemas():
"""Fixture for missing tensor fields unify schemas test data."""
schema1 = pa.schema(
[
(
"struct",
pa.struct(
[
("tensor", ArrowTensorType((2, 2), pa.int32())),
("value", pa.int64()),
]
),
)
]
)
schema2 = pa.schema(
[("struct", pa.struct([("value", pa.int64())]))] # Missing tensor field
)
expected = pa.schema(
[
(
"struct",
pa.struct(
[
("tensor", ArrowTensorType((2, 2), pa.int32())),
("value", pa.int64()),
]
),
)
]
)
return {"with_tensor": schema1, "without_tensor": schema2, "expected": expected}
@pytest.fixture
def unify_schemas_nested_struct_tensors_schemas():
"""Fixture for nested struct tensors unify schemas test data."""
schema1 = pa.schema(
[
(
"outer",
pa.struct(
[
(
"inner",
pa.struct(
[
("tensor", ArrowTensorType((3, 3), pa.float32())),
("data", pa.string()),
]
),
),
("id", pa.int64()),
]
),
)
]
)
schema2 = pa.schema(
[
(
"outer",
pa.struct(
[
(
"inner",
pa.struct([("data", pa.string())]), # Missing tensor field
),
("id", pa.int64()),
]
),
)
]
)
expected = pa.schema(
[
(
"outer",
pa.struct(
[
(
"inner",
pa.struct(
[
(
"tensor",
ArrowTensorType((3, 3), pa.float32()),
),
("data", pa.string()),
]
),
),
("id", pa.int64()),
]
),
)
]
)
return {"with_tensor": schema1, "without_tensor": schema2, "expected": expected}
@pytest.fixture
def object_with_tensor_fails_blocks():
"""Blocks that should fail when concatenating objects with tensors."""
obj = types.SimpleNamespace(a=1, b="test")
t1 = pa.table({"a": ArrowPythonObjectArray.from_objects([obj, obj])})
# Create tensor array with proper extension type
tensor_array = ArrowTensorArray.from_numpy(np.array([[1, 2], [3, 4]]))
t2 = pa.table({"a": tensor_array})
return [t1, t2]
@pytest.fixture
def simple_concat_data():
"""Test data for simple concat operations."""
return {"empty": [], "single_block": pa.table({"a": [1, 2]})}
# Helper function for creating tensor arrays
def _create_tensor_array(data, tensor_type="fixed"):
"""Helper function to create tensor arrays with consistent patterns."""
if tensor_type == "fixed":
return ArrowTensorArray.from_numpy(data)
elif tensor_type == "variable":
return ArrowVariableShapedTensorArray.from_numpy(data)
else:
raise ValueError(f"Unknown tensor type: {tensor_type}")
# Helper function for creating expected results
def _create_expected_result(schema, length, **kwargs):
"""Helper function to create expected result dictionaries."""
result = {"schema": schema, "length": length}
result.update(kwargs)
return result
# Helper function for creating tensor blocks
def _create_tensor_blocks(
tensor_data1,
tensor_data2,
tensor_type1="fixed",
tensor_type2="variable",
id_data1=None,
id_data2=None,
column_name="tensor",
):
"""Helper function to create tensor blocks with consistent patterns."""
if id_data1 is None:
id_data1 = [1, 2]
if id_data2 is None:
id_data2 = [3, 4]
tensor_array1 = _create_tensor_array(tensor_data1, tensor_type1)
tensor_array2 = _create_tensor_array(tensor_data2, tensor_type2)
t1 = pa.table({"id": id_data1, column_name: tensor_array1})
t2 = pa.table({"id": id_data2, column_name: tensor_array2})
return t1, t2
# Helper function for creating struct blocks with tensors
def _create_struct_tensor_blocks(
tensor_data1,
tensor_data2,
tensor_type1="fixed",
tensor_type2="variable",
value_data1=None,
value_data2=None,
extra_data2=None,
struct_name="struct",
id_data1=None,
id_data2=None,
):
"""Helper function to create struct blocks with tensor fields."""
if value_data1 is None:
value_data1 = [1, 2]
if value_data2 is None:
value_data2 = [3, 4]
if id_data1 is None:
id_data1 = [1, 2]
if id_data2 is None:
id_data2 = [3, 4]
tensor_array1 = _create_tensor_array(tensor_data1, tensor_type1)
tensor_array2 = _create_tensor_array(tensor_data2, tensor_type2)
value_array1 = pa.array(value_data1, type=pa.int64())
value_array2 = pa.array(value_data2, type=pa.int64())
if extra_data2 is not None:
extra_array2 = pa.array(extra_data2, type=pa.string())
struct_array1 = pa.StructArray.from_arrays(
[tensor_array1, value_array1], names=["tensor", "value"]
)
struct_array2 = pa.StructArray.from_arrays(
[tensor_array2, value_array2, extra_array2],
names=["tensor", "value", "extra"],
)
else:
struct_array1 = pa.StructArray.from_arrays(
[tensor_array1, value_array1], names=["tensor", "value"]
)
struct_array2 = pa.StructArray.from_arrays(
[tensor_array2, value_array2], names=["tensor", "value"]
)
t1 = pa.table({"id": id_data1, struct_name: struct_array1})
t2 = pa.table({"id": id_data2, struct_name: struct_array2})
return t1, t2
# Helper function for creating expected tensor schemas
def _create_tensor_schema(
tensor_type=ArrowVariableShapedTensorType,
dtype=pa.float32(),
ndim=2,
include_id=True,
struct_name="struct",
include_extra=False,
):
"""Helper function to create expected tensor schemas."""
fields = []
if include_id:
fields.append(("id", pa.int64()))
if struct_name == "struct":
struct_fields = [
("tensor", tensor_type(dtype, ndim)),
("value", pa.int64()),
]
if include_extra:
struct_fields.append(("extra", pa.string()))
fields.append((struct_name, pa.struct(struct_fields)))
else:
fields.append(("tensor", tensor_type(dtype, ndim)))
return pa.schema(fields)
# Helper function for creating basic struct blocks
def _create_basic_struct_blocks(
struct_data1,
struct_data2,
column_name="struct",
id_data1=None,
id_data2=None,
other_columns=None,
):
"""Helper function to create basic struct blocks."""
struct_array1 = pa.array(struct_data1)
struct_array2 = pa.array(struct_data2)
t1_data = {column_name: struct_array1}
t2_data = {column_name: struct_array2}
# Only add id columns if they are provided
if id_data1 is not None:
t1_data["id"] = id_data1
if id_data2 is not None:
t2_data["id"] = id_data2
if other_columns:
t1_data.update(other_columns.get("t1", {}))
t2_data.update(other_columns.get("t2", {}))
t1 = pa.table(t1_data)
t2 = pa.table(t2_data)
return t1, t2
# Helper function for creating struct schemas
def _create_struct_schema(struct_fields, include_id=True, other_fields=None):
"""Helper function to create struct schemas."""
fields = []
if include_id:
fields.append(("id", pa.int64()))
fields.append(("struct", pa.struct(struct_fields)))
if other_fields:
fields.extend(other_fields)
return pa.schema(fields)
# Helper function for creating struct blocks with additional columns
def _create_struct_blocks_with_columns(
struct_data1,
struct_data2,
struct_type1,
struct_type2,
additional_columns1=None,
additional_columns2=None,
struct_column="d",
):
"""Helper function to create struct blocks with additional columns."""
t1_data = {}
t2_data = {}
# Add additional columns first to maintain expected order
if additional_columns1:
t1_data.update(additional_columns1)
if additional_columns2:
t2_data.update(additional_columns2)
# Add struct column
t1_data[struct_column] = pa.array(struct_data1, type=struct_type1)
t2_data[struct_column] = pa.array(struct_data2, type=struct_type2)
t1 = pa.table(t1_data)
t2 = pa.table(t2_data)
return t1, t2
# Helper function for creating expected results for struct tests
def _create_struct_expected_result(schema, length, content):
"""Helper function to create expected results for struct tests."""
return {
"length": length,
"schema": schema,
"content": content,
}
# Helper function for creating struct blocks with simple field patterns
def _create_simple_struct_blocks(
struct_data1,
struct_data2,
field_names,
field_types,
additional_columns1=None,
additional_columns2=None,
struct_column="d",
):
"""Helper function to create struct blocks with simple field patterns."""
struct_type = pa.struct(list(zip(field_names, field_types)))
return _create_struct_blocks_with_columns(
struct_data1,
struct_data2,
struct_type,
struct_type,
additional_columns1,
additional_columns2,
struct_column,
)
# Helper function for creating simple struct schemas
def _create_simple_struct_schema(field_names, field_types, additional_fields=None):
"""Helper function to create simple struct schemas."""
struct_fields = list(zip(field_names, field_types))
fields = []
if additional_fields:
fields.extend(additional_fields)
fields.append(("d", pa.struct(struct_fields)))
return pa.schema(fields)
@pytest.fixture
def unify_schemas_edge_cases_data():
"""Test data for unify schemas edge cases."""
return {
"empty_schemas": [],
"single_schema": pa.schema([("col", pa.int32())]),
"no_common_columns": {
"schema1": pa.schema([("col1", pa.int32())]),
"schema2": pa.schema([("col2", pa.string())]),
"expected": pa.schema([("col1", pa.int32()), ("col2", pa.string())]),
},
"all_null_schemas": {
"schema1": pa.schema([("col", pa.null())]),
"schema2": pa.schema([("col", pa.null())]),
},
}
@pytest.fixture
def unify_schemas_mixed_tensor_data():
"""Test data for mixed tensor types in unify schemas."""
return {
"fixed_shape": pa.schema([("tensor", ArrowTensorType((2, 2), pa.int32()))]),
"variable_shaped": pa.schema(
[("tensor", ArrowVariableShapedTensorType(pa.int32(), 2))]
),
"different_shape": pa.schema([("tensor", ArrowTensorType((3, 3), pa.int32()))]),
"expected_variable": pa.schema(
[("tensor", ArrowVariableShapedTensorType(pa.int32(), 2))]
),
}
@pytest.fixture
def unify_schemas_type_promotion_data():
"""Test data for type promotion scenarios."""
return {
"non_null": pa.schema([pa.field("A", pa.int32())]),
"nullable": pa.schema([pa.field("A", pa.int32(), nullable=True)]),
"int64": pa.schema([pa.field("A", pa.int64())]),
"float64": pa.schema([pa.field("A", pa.float64())]),
}
@pytest.fixture
def block_select_data():
"""Test data for block select operations."""
df = pd.DataFrame({"one": [10, 11, 12], "two": [11, 12, 13], "three": [14, 15, 16]})
table = pa.Table.from_pandas(df)
return {
"table": table,
"df": df,
"single_column": {
"columns": ["two"],
"expected_schema": pa.schema([("two", pa.int64())]),
},
"multiple_columns": {
"columns": ["two", "one"],
"expected_schema": pa.schema([("two", pa.int64()), ("one", pa.int64())]),
},
}
@pytest.fixture
def block_slice_data():
"""Test data for block slice operations."""
n = 20
df = pd.DataFrame(
{"one": list(range(n)), "two": ["a"] * n, "three": [np.nan] + [1.5] * (n - 1)}
)
table = pa.Table.from_pandas(df)
empty_df = pd.DataFrame({"one": []})
empty_table = pa.Table.from_pandas(empty_df)
return {
"normal": {"table": table, "df": df, "slice_params": {"a": 5, "b": 10}},
"empty": {"table": empty_table, "slice_params": {"a": 0, "b": 0}},
}
if __name__ == "__main__":
import sys
sys.exit(pytest.main(["-v", __file__]))