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

151 lines
5.1 KiB
Python

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
pytestmark = pytest.mark.skipif(
version.parse(pa.__version__) < version.parse("19.0.0"),
reason="Namespace expressions tests require PyArrow >= 19.0",
)
@pytest.fixture
def map_dataset():
"""Fixture that creates a dataset backed by an Arrow MapArray column."""
map_items = [
{"attrs": {"color": "red", "size": "M"}},
{"attrs": {"brand": "Ray"}},
]
map_type = pa.map_(pa.string(), pa.string())
arrow_table = pa.table(
{"attrs": pa.array([row["attrs"] for row in map_items], type=map_type)}
)
return ray.data.from_arrow(arrow_table)
def _assert_result(result_df: pd.DataFrame, expected_df: pd.DataFrame, drop_cols: list):
"""Helper to drop columns and assert equality."""
result_df = result_df.drop(columns=drop_cols)
assert rows_same(result_df, expected_df)
class TestMapNamespace:
"""Tests for map namespace operations using the shared map_dataset fixture."""
def test_map_keys(self, map_dataset):
result = map_dataset.with_column("keys", col("attrs").map.keys()).to_pandas()
expected = pd.DataFrame({"keys": [["color", "size"], ["brand"]]})
_assert_result(result, expected, drop_cols=["attrs"])
def test_map_values(self, map_dataset):
result = map_dataset.with_column(
"values", col("attrs").map.values()
).to_pandas()
expected = pd.DataFrame({"values": [["red", "M"], ["Ray"]]})
_assert_result(result, expected, drop_cols=["attrs"])
def test_map_chaining(self, map_dataset):
# map.keys() returns a list, so .list.len() should apply
result = map_dataset.with_column(
"num_keys", col("attrs").map.keys().list.len()
).to_pandas()
expected = pd.DataFrame({"num_keys": [2, 1]})
_assert_result(result, expected, drop_cols=["attrs"])
def test_physical_map_extraction():
"""Test extraction works on List<Struct> (Physical Maps)."""
# Construct List<Struct<k, v>>
struct_type = pa.struct([pa.field("k", pa.string()), pa.field("v", pa.int64())])
list_type = pa.list_(struct_type)
data_py = [[{"k": "a", "v": 1}], [{"k": "b", "v": 2}]]
arrow_table = pa.Table.from_arrays(
[pa.array(data_py, type=list_type)], names=["data"]
)
ds = ray.data.from_arrow(arrow_table)
result = (
ds.with_column("keys", col("data").map.keys())
.with_column("values", col("data").map.values())
.to_pandas()
)
expected = pd.DataFrame(
{
"data": data_py,
"keys": [["a"], ["b"]],
"values": [[1], [2]],
}
)
assert rows_same(result, expected)
def test_map_sliced_offsets():
"""Test extraction works correctly on sliced Arrow arrays (offset > 0)."""
items = [{"m": {"id": i}} for i in range(10)]
map_type = pa.map_(pa.string(), pa.int64())
arrays = pa.array([row["m"] for row in items], type=map_type)
table = pa.Table.from_arrays([arrays], names=["m"])
# Force offsets by slicing the table before ingestion
sliced_table = table.slice(offset=7, length=3)
ds = ray.data.from_arrow(sliced_table)
result = ds.with_column("vals", col("m").map.values()).to_pandas()
expected = pd.DataFrame({"vals": [[7], [8], [9]]})
_assert_result(result, expected, drop_cols=["m"])
def test_map_nulls_and_empty():
"""Test handling of null maps and empty maps."""
items_data = [{"m": {"a": 1}}, {"m": {}}, {"m": None}]
map_type = pa.map_(pa.string(), pa.int64())
arrays = pa.array([row["m"] for row in items_data], type=map_type)
arrow_table = pa.Table.from_arrays([arrays], names=["m"])
ds = ray.data.from_arrow(arrow_table)
rows = (
ds.with_column("keys", col("m").map.keys())
.with_column("values", col("m").map.values())
.take_all()
)
assert list(rows[0]["keys"]) == ["a"] and list(rows[0]["values"]) == [1]
assert len(rows[1]["keys"]) == 0 and len(rows[1]["values"]) == 0
assert rows[2]["keys"] is None and rows[2]["values"] is None
def test_empty_chunked_array():
"""Test extraction works on empty ChunkedArray (zero chunks)."""
from ray.data.namespace_expressions.map_namespace import (
MapComponent,
_extract_map_component,
)
# Create empty ChunkedArray with map type
map_type = pa.map_(pa.string(), pa.int64())
empty_chunked = pa.chunked_array([], type=map_type)
assert empty_chunked.num_chunks == 0
# Extract keys - should return empty ChunkedArray with list<string> type
keys_result = _extract_map_component(empty_chunked, MapComponent.KEYS)
assert len(keys_result) == 0
assert keys_result.type == pa.list_(pa.string())
# Extract values - should return empty ChunkedArray with list<int64> type
values_result = _extract_map_component(empty_chunked, MapComponent.VALUES)
assert len(values_result) == 0
assert values_result.type == pa.list_(pa.int64())
if __name__ == "__main__":
import sys
sys.exit(pytest.main(["-v", __file__]))