"""Integration tests for struct namespace expressions. These tests require Ray and test end-to-end struct namespace expression evaluation. """ import pandas as pd import pyarrow as pa import pytest from packaging import version import ray from ray.data._internal.util import rows_same from ray.data.expressions import col from ray.data.tests.conftest import * # noqa from ray.tests.conftest import * # noqa pytestmark = pytest.mark.skipif( version.parse(pa.__version__) < version.parse("19.0.0"), reason="Namespace expressions tests require PyArrow >= 19.0", ) def _create_dataset(items_data, dataset_format, arrow_table=None): if dataset_format == "arrow": if arrow_table is not None: ds = ray.data.from_arrow(arrow_table) else: table = pa.Table.from_pylist(items_data) ds = ray.data.from_arrow(table) elif dataset_format == "pandas": if arrow_table is not None: df = arrow_table.to_pandas() else: df = pd.DataFrame(items_data) ds = ray.data.from_blocks([df]) return ds DATASET_FORMATS = ["pandas", "arrow"] @pytest.mark.parametrize("dataset_format", DATASET_FORMATS) class TestStructNamespace: """Tests for struct namespace operations.""" def test_struct_bracket_bool_index_raises(self, dataset_format): """Test struct[bool] raises TypeError instead of being treated as int.""" del dataset_format # Unused, required by class-level parametrization. with pytest.raises( TypeError, match="Struct indices must be strings or integers" ): col("user").struct[True] @pytest.mark.parametrize("bad_index", ["1", True]) def test_struct_field_by_index_non_integer_raises(self, dataset_format, bad_index): """Test struct.field_by_index() rejects non-integer indices.""" del dataset_format # Unused, required by class-level parametrization. with pytest.raises(TypeError, match="Struct field index must be an integer"): col("user").struct.field_by_index(bad_index) def test_struct_field_by_index_negative_raises(self, dataset_format): """Test struct.field_by_index() rejects negative indices.""" del dataset_format # Unused, required by class-level parametrization. with pytest.raises( ValueError, match="Struct field index must be non-negative, got -1" ): col("user").struct.field_by_index(-1) with pytest.raises( ValueError, match="Struct field index must be non-negative, got -1" ): col("user").struct[-1] def test_struct_field(self, ray_start_regular_shared, dataset_format): """Test struct.field() extracts field.""" arrow_table = pa.table( { "user": pa.array( [ {"name": "Alice", "age": 30}, {"name": "Bob", "age": 25}, ], type=pa.struct( [ pa.field("name", pa.string()), pa.field("age", pa.int32()), ] ), ) } ) items_data = [ {"user": {"name": "Alice", "age": 30}}, {"user": {"name": "Bob", "age": 25}}, ] ds = _create_dataset(items_data, dataset_format, arrow_table) result = ds.with_column("age", col("user").struct.field("age")).to_pandas() expected = pd.DataFrame( { "user": [{"name": "Alice", "age": 30}, {"name": "Bob", "age": 25}], "age": [30, 25], } ) assert rows_same(result, expected) def test_struct_bracket(self, ray_start_regular_shared, dataset_format): """Test struct['field'] bracket notation.""" arrow_table = pa.table( { "user": pa.array( [ {"name": "Alice", "age": 30}, {"name": "Bob", "age": 25}, ], type=pa.struct( [ pa.field("name", pa.string()), pa.field("age", pa.int32()), ] ), ) } ) items_data = [ {"user": {"name": "Alice", "age": 30}}, {"user": {"name": "Bob", "age": 25}}, ] ds = _create_dataset(items_data, dataset_format, arrow_table) result = ds.with_column("name", col("user").struct["name"]).to_pandas() expected = pd.DataFrame( { "user": [{"name": "Alice", "age": 30}, {"name": "Bob", "age": 25}], "name": ["Alice", "Bob"], } ) assert rows_same(result, expected) def test_struct_field_by_index(self, ray_start_regular_shared, dataset_format): """Test struct.field_by_index() extracts field by position.""" if dataset_format == "pandas": pytest.skip( "Index-based struct access requires stable Arrow struct field ordering." ) arrow_table = pa.table( { "user": pa.array( [ {"name": "Alice", "age": 30}, {"name": "Bob", "age": 25}, ], type=pa.struct( [ pa.field("name", pa.string()), pa.field("age", pa.int32()), ] ), ) } ) items_data = [ {"user": {"name": "Alice", "age": 30}}, {"user": {"name": "Bob", "age": 25}}, ] ds = _create_dataset(items_data, dataset_format, arrow_table) result = ds.with_column("age", col("user").struct.field_by_index(1)).to_pandas() expected = pd.DataFrame( { "user": [{"name": "Alice", "age": 30}, {"name": "Bob", "age": 25}], "age": [30, 25], } ) assert rows_same(result, expected) def test_struct_bracket_with_index(self, ray_start_regular_shared, dataset_format): """Test struct[index] bracket notation.""" if dataset_format == "pandas": pytest.skip( "Index-based struct access requires stable Arrow struct field ordering." ) arrow_table = pa.table( { "user": pa.array( [ {"name": "Alice", "age": 30}, {"name": "Bob", "age": 25}, ], type=pa.struct( [ pa.field("name", pa.string()), pa.field("age", pa.int32()), ] ), ) } ) items_data = [ {"user": {"name": "Alice", "age": 30}}, {"user": {"name": "Bob", "age": 25}}, ] ds = _create_dataset(items_data, dataset_format, arrow_table) result = ds.with_column("name", col("user").struct[0]).to_pandas() expected = pd.DataFrame( { "user": [{"name": "Alice", "age": 30}, {"name": "Bob", "age": 25}], "name": ["Alice", "Bob"], } ) assert rows_same(result, expected) def test_struct_nested_field(self, ray_start_regular_shared, dataset_format): """Test nested struct field access with .field().""" arrow_table = pa.table( { "user": pa.array( [ {"name": "Alice", "address": {"city": "NYC", "zip": "10001"}}, {"name": "Bob", "address": {"city": "LA", "zip": "90001"}}, ], type=pa.struct( [ pa.field("name", pa.string()), pa.field( "address", pa.struct( [ pa.field("city", pa.string()), pa.field("zip", pa.string()), ] ), ), ] ), ) } ) items_data = [ {"user": {"name": "Alice", "address": {"city": "NYC", "zip": "10001"}}}, {"user": {"name": "Bob", "address": {"city": "LA", "zip": "90001"}}}, ] ds = _create_dataset(items_data, dataset_format, arrow_table) result = ds.with_column( "city", col("user").struct.field("address").struct.field("city") ).to_pandas() expected = pd.DataFrame( { "user": [ {"name": "Alice", "address": {"city": "NYC", "zip": "10001"}}, {"name": "Bob", "address": {"city": "LA", "zip": "90001"}}, ], "city": ["NYC", "LA"], } ) assert rows_same(result, expected) def test_struct_nested_bracket(self, ray_start_regular_shared, dataset_format): """Test nested struct field access with brackets.""" arrow_table = pa.table( { "user": pa.array( [ {"name": "Alice", "address": {"city": "NYC", "zip": "10001"}}, {"name": "Bob", "address": {"city": "LA", "zip": "90001"}}, ], type=pa.struct( [ pa.field("name", pa.string()), pa.field( "address", pa.struct( [ pa.field("city", pa.string()), pa.field("zip", pa.string()), ] ), ), ] ), ) } ) items_data = [ {"user": {"name": "Alice", "address": {"city": "NYC", "zip": "10001"}}}, {"user": {"name": "Bob", "address": {"city": "LA", "zip": "90001"}}}, ] ds = _create_dataset(items_data, dataset_format, arrow_table) result = ds.with_column( "zip", col("user").struct["address"].struct["zip"] ).to_pandas() expected = pd.DataFrame( { "user": [ {"name": "Alice", "address": {"city": "NYC", "zip": "10001"}}, {"name": "Bob", "address": {"city": "LA", "zip": "90001"}}, ], "zip": ["10001", "90001"], } ) assert rows_same(result, expected) if __name__ == "__main__": import sys sys.exit(pytest.main(["-v", __file__]))