193 lines
6.6 KiB
Python
193 lines
6.6 KiB
Python
import pandas as pd
|
|
import pyarrow as pa
|
|
import pytest
|
|
from packaging.version import parse as parse_version
|
|
|
|
import ray
|
|
from ray.data._internal.util import rows_same
|
|
from ray.data._internal.utils.arrow_utils import get_pyarrow_version
|
|
from ray.data.datatype import DataType
|
|
from ray.data.exceptions import UserCodeException
|
|
from ray.data.expressions import col
|
|
from ray.data.tests.conftest import * # noqa
|
|
from ray.tests.conftest import * # noqa
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
get_pyarrow_version() < parse_version("20.0.0"),
|
|
reason="with_column requires PyArrow >= 20.0.0",
|
|
)
|
|
@pytest.mark.parametrize(
|
|
"expr, target_type, expected_rows",
|
|
[
|
|
# Basic type conversions using Ray Data's DataType
|
|
(col("id"), DataType.int64(), [{"id": i, "result": i} for i in range(5)]),
|
|
(
|
|
col("id"),
|
|
DataType.float64(),
|
|
[{"id": i, "result": float(i)} for i in range(5)],
|
|
),
|
|
(
|
|
col("id"),
|
|
DataType.string(),
|
|
[{"id": i, "result": str(i)} for i in range(5)],
|
|
),
|
|
(
|
|
col("id") / 2,
|
|
DataType.int64(),
|
|
[{"id": i, "result": i // 2} for i in range(5)],
|
|
),
|
|
# col("id")/2 uses integer division in expression layer, then cast to float64
|
|
(
|
|
col("id") / 2,
|
|
DataType.float64(),
|
|
[{"id": i, "result": float(i // 2)} for i in range(5)],
|
|
),
|
|
],
|
|
)
|
|
def test_cast_expression_basic(
|
|
ray_start_regular_shared,
|
|
expr,
|
|
target_type,
|
|
expected_rows,
|
|
target_max_block_size_infinite_or_default,
|
|
):
|
|
"""Test basic type casting with cast() method."""
|
|
ds = ray.data.range(5).with_column("result", expr.cast(target_type))
|
|
actual = ds.take_all()
|
|
assert rows_same(pd.DataFrame(actual), pd.DataFrame(expected_rows))
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
get_pyarrow_version() < parse_version("20.0.0"),
|
|
reason="with_column requires PyArrow >= 20.0.0",
|
|
)
|
|
def test_cast_expression_usecase(
|
|
ray_start_regular_shared, target_max_block_size_infinite_or_default
|
|
):
|
|
"""Test the user use case: converting float result from modulo to int64."""
|
|
ds = ray.data.range(10)
|
|
# The modulo operation returns float, cast it to int64
|
|
ds = ds.with_column("part", (col("id") % 2).cast(DataType.int64()))
|
|
actual = ds.take_all()
|
|
expected_rows = [{"id": i, "part": i % 2} for i in range(10)]
|
|
assert rows_same(pd.DataFrame(actual), pd.DataFrame(expected_rows))
|
|
|
|
# Verify the schema shows int64 type
|
|
schema = ds.schema()
|
|
assert "part" in schema.names
|
|
part_type = schema.types[schema.names.index("part")]
|
|
assert part_type == pa.int64()
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
get_pyarrow_version() < parse_version("20.0.0"),
|
|
reason="with_column requires PyArrow >= 20.0.0",
|
|
)
|
|
def test_cast_expression_chained(
|
|
ray_start_regular_shared, target_max_block_size_infinite_or_default
|
|
):
|
|
"""Test that cast() can be chained with other expressions."""
|
|
ds = ray.data.range(5)
|
|
# Cast to float64 then multiply
|
|
ds = ds.with_column("result", col("id").cast(DataType.float64()) * 2.5)
|
|
actual = ds.take_all()
|
|
expected_rows = [{"id": i, "result": i * 2.5} for i in range(5)]
|
|
assert rows_same(pd.DataFrame(actual), pd.DataFrame(expected_rows))
|
|
|
|
# Cast result of arithmetic operation
|
|
ds = ray.data.range(5)
|
|
ds = ds.with_column("result", (col("id") + 1).cast(DataType.string()))
|
|
actual = ds.take_all()
|
|
expected_rows = [{"id": i, "result": str(i + 1)} for i in range(5)]
|
|
assert rows_same(pd.DataFrame(actual), pd.DataFrame(expected_rows))
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
get_pyarrow_version() < parse_version("20.0.0"),
|
|
reason="with_column requires PyArrow >= 20.0.0",
|
|
)
|
|
def test_cast_expression_safe_mode(
|
|
ray_start_regular_shared, target_max_block_size_infinite_or_default
|
|
):
|
|
"""Test that safe=True (default) raises errors on invalid conversions."""
|
|
ds = ray.data.from_items([{"value": "not_a_number"}])
|
|
|
|
# Attempting to cast non-numeric string to int should raise an error
|
|
with pytest.raises((UserCodeException, ValueError, pa.ArrowInvalid)):
|
|
ds.with_column("result", col("value").cast(DataType.int64())).materialize()
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
get_pyarrow_version() < parse_version("20.0.0"),
|
|
reason="with_column requires PyArrow >= 20.0.0",
|
|
)
|
|
def test_cast_expression_invalid_type(
|
|
ray_start_regular_shared, target_max_block_size_infinite_or_default
|
|
):
|
|
"""Test that invalid type targets raise appropriate errors."""
|
|
ds = ray.data.range(5)
|
|
|
|
# Passing a non-DataType target should raise TypeError
|
|
with pytest.raises(
|
|
TypeError, match="target_type must be a ray.data.datatype.DataType"
|
|
):
|
|
ds.with_column("result", col("id").cast("invalid_type")).materialize()
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
get_pyarrow_version() < parse_version("20.0.0"),
|
|
reason="with_column requires PyArrow >= 20.0.0",
|
|
)
|
|
def test_cast_expression_multiple_types(
|
|
ray_start_regular_shared, target_max_block_size_infinite_or_default
|
|
):
|
|
"""Test casting with multiple different target types."""
|
|
ds = ray.data.from_items([{"id": 42, "score": 3.14}])
|
|
|
|
# Cast id to different types
|
|
ds = ds.with_column("id_int", col("id").cast(DataType.int64()))
|
|
ds = ds.with_column("id_float", col("id").cast(DataType.float64()))
|
|
ds = ds.with_column("id_str", col("id").cast(DataType.string()))
|
|
|
|
# Cast score to int (use safe=False to allow float truncation to int)
|
|
ds = ds.with_column("score_int", col("score").cast(DataType.int64(), safe=False))
|
|
|
|
# Use rows_same to compare the full row content (expects DataFrames).
|
|
results = ds.take_all()
|
|
expected = [
|
|
{
|
|
"id": 42,
|
|
"score": 3.14,
|
|
"id_int": 42,
|
|
"id_float": 42.0,
|
|
"id_str": "42",
|
|
"score_int": 3,
|
|
}
|
|
]
|
|
assert rows_same(pd.DataFrame(results), pd.DataFrame(expected))
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
get_pyarrow_version() < parse_version("20.0.0"),
|
|
reason="with_column requires PyArrow >= 20.0.0",
|
|
)
|
|
def test_cast_expression_python_type_datatype_error(
|
|
ray_start_regular_shared, target_max_block_size_infinite_or_default
|
|
):
|
|
"""Test that using Python-type-backed DataType in cast() raises a clear error."""
|
|
# Error is raised at expression build time when cast() is called (not at materialize).
|
|
error_match = "Python-type-backed DataType.*requires.*values"
|
|
with pytest.raises(TypeError, match=error_match):
|
|
col("id").cast(DataType(int))
|
|
with pytest.raises(TypeError, match=error_match):
|
|
col("id").cast(DataType(str))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import sys
|
|
|
|
import pytest
|
|
|
|
sys.exit(pytest.main(["-v", __file__]))
|