chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:17:40 +08:00
commit f1825c8ceb
10096 changed files with 2364182 additions and 0 deletions
@@ -0,0 +1,209 @@
"""Integration tests for arithmetic expression operations.
These tests require Ray and test end-to-end arithmetic expression evaluation.
"""
import math
import pandas as pd
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.expressions import col, lit
from ray.data.tests.conftest import * # noqa
from ray.tests.conftest import * # noqa
pytestmark = pytest.mark.skipif(
get_pyarrow_version() < parse_version("20.0.0"),
reason="Expression integration tests require PyArrow >= 20.0.0",
)
class TestArithmeticIntegration:
"""Integration tests for arithmetic expressions with Ray Dataset."""
def test_arithmetic_with_dataset(self, ray_start_regular_shared):
"""Test arithmetic expressions work correctly with Ray Dataset."""
ds = ray.data.from_items(
[
{"price": 10.0, "quantity": 2},
{"price": 20.0, "quantity": 3},
{"price": 15.0, "quantity": 4},
]
)
result = ds.with_column("total", col("price") * col("quantity")).to_pandas()
expected = pd.DataFrame(
{
"price": [10.0, 20.0, 15.0],
"quantity": [2, 3, 4],
"total": [20.0, 60.0, 60.0],
}
)
assert rows_same(result, expected)
def test_chained_arithmetic_with_dataset(self, ray_start_regular_shared):
"""Test chained arithmetic expressions with Ray Dataset."""
ds = ray.data.from_items(
[
{"a": 10, "b": 5},
{"a": 20, "b": 3},
]
)
result = (
ds.with_column("sum", col("a") + col("b"))
.with_column("diff", col("a") - col("b"))
.with_column("product", col("a") * col("b"))
.to_pandas()
)
expected = pd.DataFrame(
{
"a": [10, 20],
"b": [5, 3],
"sum": [15, 23],
"diff": [5, 17],
"product": [50, 60],
}
)
assert rows_same(result, expected)
def test_floor_division_with_dataset(self, ray_start_regular_shared):
"""Test floor division operations with Ray Dataset."""
ds = ray.data.range(5)
result = ds.with_column("result", col("id") // 2).to_pandas()
expected = pd.DataFrame({"id": [0, 1, 2, 3, 4], "result": [0, 0, 1, 1, 2]})
assert rows_same(result, expected)
def test_literal_floor_division_with_dataset(self, ray_start_regular_shared):
"""Test literal floor division by expression with Ray Dataset."""
ds = ray.data.range(5)
result = ds.with_column("result", lit(10) // (col("id") + 2)).to_pandas()
expected = pd.DataFrame({"id": [0, 1, 2, 3, 4], "result": [5, 3, 2, 2, 1]})
assert rows_same(result, expected)
@pytest.mark.parametrize(
"expr_factory,expected_values",
[
pytest.param(lambda: col("value").ceil(), [-1, 0, 0, 1, 2], id="ceil"),
pytest.param(lambda: col("value").floor(), [-2, -1, 0, 0, 1], id="floor"),
pytest.param(lambda: col("value").round(), [-2, 0, 0, 0, 2], id="round"),
pytest.param(lambda: col("value").trunc(), [-1, 0, 0, 0, 1], id="trunc"),
],
)
def test_rounding_with_dataset(
self, ray_start_regular_shared, expr_factory, expected_values
):
"""Test rounding operations with Ray Dataset."""
values = [-1.75, -0.25, 0.0, 0.25, 1.75]
ds = ray.data.from_items([{"value": v} for v in values])
result = ds.with_column("result", expr_factory()).to_pandas()
expected = pd.DataFrame({"value": values, "result": expected_values})
assert rows_same(result, expected)
@pytest.mark.parametrize(
"expr_factory,expected_fn",
[
pytest.param(lambda: col("value").ln(), math.log, id="ln"),
pytest.param(lambda: col("value").log10(), math.log10, id="log10"),
pytest.param(lambda: col("value").log2(), math.log2, id="log2"),
pytest.param(lambda: col("value").exp(), math.exp, id="exp"),
],
)
def test_logarithmic_with_dataset(
self, ray_start_regular_shared, expr_factory, expected_fn
):
"""Test logarithmic operations with Ray Dataset."""
values = [1.0, math.e, 10.0, 4.0]
ds = ray.data.from_items([{"value": v} for v in values])
expected_values = [expected_fn(v) for v in values]
result = ds.with_column("result", expr_factory()).to_pandas()
expected = pd.DataFrame({"value": values, "result": expected_values})
assert rows_same(result, expected)
@pytest.mark.parametrize(
"expr_factory,expected_fn",
[
pytest.param(lambda: col("value").sin(), math.sin, id="sin"),
pytest.param(lambda: col("value").cos(), math.cos, id="cos"),
pytest.param(lambda: col("value").tan(), math.tan, id="tan"),
pytest.param(lambda: col("value").atan(), math.atan, id="atan"),
],
)
def test_trigonometric_with_dataset(
self, ray_start_regular_shared, expr_factory, expected_fn
):
"""Test trigonometric operations with Ray Dataset."""
values = [0.0, math.pi / 6, math.pi / 4, math.pi / 3]
ds = ray.data.from_items([{"value": v} for v in values])
expected_values = [expected_fn(v) for v in values]
result = ds.with_column("result", expr_factory()).to_pandas()
expected = pd.DataFrame({"value": values, "result": expected_values})
assert rows_same(result, expected)
@pytest.mark.parametrize(
"test_data,expr_factory,expected_results",
[
pytest.param(
[{"x": 5}, {"x": -3}, {"x": 0}],
lambda: col("x").negate(),
[-5, 3, 0],
id="negate",
),
pytest.param(
[{"x": 5}, {"x": -3}, {"x": 0}],
lambda: col("x").sign(),
[1, -1, 0],
id="sign",
),
pytest.param(
[{"x": 5}, {"x": -3}, {"x": 0}],
lambda: col("x").abs(),
[5, 3, 0],
id="abs",
),
pytest.param(
[{"x": 2}, {"x": 3}, {"x": 4}],
lambda: col("x").power(2),
[4, 9, 16],
id="power_int",
),
pytest.param(
[{"x": 4}, {"x": 9}, {"x": 16}],
lambda: col("x").power(0.5),
[2.0, 3.0, 4.0],
id="power_sqrt",
),
],
)
def test_arithmetic_helpers_with_dataset(
self, ray_start_regular_shared, test_data, expr_factory, expected_results
):
"""Test arithmetic helper operations with Ray Dataset."""
ds = ray.data.from_items(test_data)
result = ds.with_column("result", expr_factory()).to_pandas()
expected = pd.DataFrame(test_data)
expected["result"] = expected_results
assert rows_same(result, expected)
def test_age_group_calculation_with_dataset(self, ray_start_regular_shared):
"""Test floor division for grouping values (e.g., age into decades)."""
test_data = [
{"age": 25},
{"age": 17},
{"age": 30},
]
ds = ray.data.from_items(test_data)
result = ds.with_column("age_group", col("age") // 10 * 10).to_pandas()
expected = pd.DataFrame({"age": [25, 17, 30], "age_group": [20, 10, 30]})
assert rows_same(result, expected)
if __name__ == "__main__":
import sys
sys.exit(pytest.main(["-v", __file__]))
@@ -0,0 +1,147 @@
"""Integration tests for boolean/logical expression operations.
These tests require Ray and test end-to-end boolean expression evaluation.
"""
import pandas as pd
import pytest
from packaging.version import parse as parse_version
import ray
from ray.data._internal.utils.arrow_utils import get_pyarrow_version
from ray.data.expressions import col, lit
from ray.data.tests.conftest import * # noqa
from ray.tests.conftest import * # noqa
pytestmark = pytest.mark.skipif(
get_pyarrow_version() < parse_version("20.0.0"),
reason="Expression integration tests require PyArrow >= 20.0.0",
)
class TestBooleanIntegration:
"""Integration tests for boolean expressions with Ray Dataset."""
def test_boolean_filter_with_dataset(self, ray_start_regular_shared):
"""Test boolean expressions used for filtering with Ray Dataset."""
ds = ray.data.from_items(
[
{"age": 17, "is_student": True, "name": "Alice"},
{"age": 21, "is_student": True, "name": "Bob"},
{"age": 25, "is_student": False, "name": "Charlie"},
{"age": 30, "is_student": False, "name": "Diana"},
]
)
# Add boolean columns using expressions
result = (
ds.with_column("is_adult", col("age") >= 18)
.with_column("adult_student", (col("age") >= 18) & col("is_student"))
.with_column("minor_or_student", (col("age") < 18) | col("is_student"))
.to_pandas()
)
expected = pd.DataFrame(
{
"age": [17, 21, 25, 30],
"is_student": [True, True, False, False],
"name": ["Alice", "Bob", "Charlie", "Diana"],
"is_adult": [False, True, True, True],
"adult_student": [False, True, False, False],
"minor_or_student": [True, True, False, False],
}
)
pd.testing.assert_frame_equal(result, expected, check_dtype=False)
def test_complex_boolean_with_dataset(self, ray_start_regular_shared):
"""Test complex boolean expressions with Ray Dataset."""
ds = ray.data.from_items(
[
{"score": 85, "passed": True, "bonus": False},
{"score": 70, "passed": True, "bonus": True},
{"score": 45, "passed": False, "bonus": False},
]
)
# Complex: (score > 80) OR (passed AND bonus)
result = ds.with_column(
"eligible", (col("score") > 80) | (col("passed") & col("bonus"))
).to_pandas()
expected = pd.DataFrame(
{
"score": [85, 70, 45],
"passed": [True, True, False],
"bonus": [False, True, False],
"eligible": [True, True, False],
}
)
pd.testing.assert_frame_equal(result, expected, check_dtype=False)
def test_logical_not_with_dataset(self, ray_start_regular_shared):
"""Test logical NOT operation with Ray Dataset."""
ds = ray.data.range(5)
result = ds.with_column("result", ~(col("id") == 2)).to_pandas()
expected = pd.DataFrame(
{"id": [0, 1, 2, 3, 4], "result": [True, True, False, True, True]}
)
pd.testing.assert_frame_equal(result, expected, check_dtype=False)
@pytest.mark.parametrize(
"expression_factory,expected_results,test_id",
[
pytest.param(
lambda: (col("age") > 18) & (col("country") == "USA"),
[True, False, False],
"complex_and",
),
pytest.param(
lambda: (col("age") < 18) | (col("country") == "USA"),
[True, True, False],
"complex_or",
),
pytest.param(
lambda: ~((col("age") < 25) & (col("country") != "USA")),
[True, False, True],
"complex_not",
),
pytest.param(
lambda: (col("age") >= 21)
& (col("score") >= 10)
& col("active").is_not_null()
& (col("active") == lit(True)),
[True, False, False],
"eligibility_flag",
),
],
)
def test_complex_boolean_expressions_with_dataset(
self, ray_start_regular_shared, expression_factory, expected_results, test_id
):
"""Test complex boolean expressions with Ray Dataset."""
test_data = [
{"age": 25, "country": "USA", "active": True, "score": 20},
{"age": 17, "country": "Canada", "active": False, "score": 10},
{"age": 30, "country": "UK", "active": None, "score": 20},
]
ds = ray.data.from_items(test_data)
expression = expression_factory()
result = ds.with_column("result", expression).to_pandas()
expected = pd.DataFrame(
{
"age": [25, 17, 30],
"country": ["USA", "Canada", "UK"],
"active": [True, False, None],
"score": [20, 10, 20],
"result": expected_results,
}
)
pd.testing.assert_frame_equal(result, expected, check_dtype=False)
if __name__ == "__main__":
import sys
sys.exit(pytest.main(["-v", __file__]))
@@ -0,0 +1,192 @@
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__]))
@@ -0,0 +1,78 @@
"""Integration tests for comparison expression operations.
These tests require Ray and test end-to-end comparison expression evaluation.
"""
import pandas as pd
import pytest
from packaging.version import parse as parse_version
import ray
from ray.data._internal.utils.arrow_utils import get_pyarrow_version
from ray.data.expressions import col
from ray.data.tests.conftest import * # noqa
from ray.tests.conftest import * # noqa
pytestmark = pytest.mark.skipif(
get_pyarrow_version() < parse_version("20.0.0"),
reason="Expression integration tests require PyArrow >= 20.0.0",
)
class TestComparisonIntegration:
"""Integration tests for comparison expressions with Ray Dataset."""
def test_comparison_with_dataset(self, ray_start_regular_shared):
"""Test comparison expressions work correctly with Ray Dataset."""
ds = ray.data.from_items(
[
{"age": 17, "name": "Alice"},
{"age": 21, "name": "Bob"},
{"age": 25, "name": "Charlie"},
{"age": 18, "name": "Diana"},
]
)
result = ds.with_column("is_adult", col("age") >= 18).to_pandas()
expected = pd.DataFrame(
{
"age": [17, 21, 25, 18],
"name": ["Alice", "Bob", "Charlie", "Diana"],
"is_adult": [False, True, True, True],
}
)
pd.testing.assert_frame_equal(result, expected, check_dtype=False)
def test_multiple_comparisons_with_dataset(self, ray_start_regular_shared):
"""Test multiple comparison expressions with Ray Dataset."""
ds = ray.data.from_items(
[
{"score": 45, "passing": 50},
{"score": 75, "passing": 50},
{"score": 50, "passing": 50},
]
)
result = (
ds.with_column("passed", col("score") >= col("passing"))
.with_column("failed", col("score") < col("passing"))
.with_column("borderline", col("score") == col("passing"))
.to_pandas()
)
expected = pd.DataFrame(
{
"score": [45, 75, 50],
"passing": [50, 50, 50],
"passed": [False, True, True],
"failed": [True, False, False],
"borderline": [False, False, True],
}
)
pd.testing.assert_frame_equal(result, expected, check_dtype=False)
if __name__ == "__main__":
import sys
sys.exit(pytest.main(["-v", __file__]))
@@ -0,0 +1,63 @@
"""Integration tests for array namespace expressions.
These tests require Ray and test end-to-end array 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 _make_fixed_size_list_table() -> pa.Table:
values = pa.array([1, 2, 3, 4, 5, 6], type=pa.int64())
fixed = pa.FixedSizeListArray.from_arrays(values, list_size=2)
return pa.Table.from_arrays([fixed], names=["features"])
def test_arr_to_list_fixed_size(ray_start_regular_shared):
table = _make_fixed_size_list_table()
ds = ray.data.from_arrow(table)
result = (
ds.with_column("features", col("features").arr.to_list())
.select_columns(["features"])
.to_pandas()
)
expected = pd.DataFrame(
[
{"features": [1, 2]},
{"features": [3, 4]},
{"features": [5, 6]},
]
)
assert rows_same(result, expected)
def test_arr_to_list_invalid_dtype_raises(ray_start_regular_shared):
ds = ray.data.from_items([{"value": 1}, {"value": 2}])
with pytest.raises(
(ray.exceptions.RayTaskError, ray.exceptions.UserCodeException)
) as exc_info:
ds.with_column("value_list", col("value").arr.to_list()).to_pandas()
assert "to_list() can only be called on list-like columns" in str(exc_info.value)
if __name__ == "__main__":
import sys
sys.exit(pytest.main(["-v", __file__]))
@@ -0,0 +1,81 @@
"""Integration tests for datetime namespace expressions.
These tests require Ray and test end-to-end datetime namespace expression evaluation.
"""
import datetime
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",
)
class TestDatetimeNamespace:
"""Tests for datetime namespace operations."""
def test_datetime_namespace_all_operations(self, ray_start_regular_shared):
"""Test all datetime namespace operations on a datetime column."""
ts = datetime.datetime(2024, 1, 2, 10, 30, 0)
ds = ray.data.from_items([{"ts": ts}])
result_ds = (
ds.with_column("year", col("ts").dt.year())
.with_column("month", col("ts").dt.month())
.with_column("day", col("ts").dt.day())
.with_column("hour", col("ts").dt.hour())
.with_column("minute", col("ts").dt.minute())
.with_column("second", col("ts").dt.second())
.with_column("date_str", col("ts").dt.strftime("%Y-%m-%d"))
.with_column("ts_floor", col("ts").dt.floor("day"))
.with_column("ts_ceil", col("ts").dt.ceil("day"))
.with_column("ts_round", col("ts").dt.round("day"))
.drop_columns(["ts"])
)
actual = result_ds.to_pandas()
expected = pd.DataFrame(
[
{
"year": 2024,
"month": 1,
"day": 2,
"hour": 10,
"minute": 30,
"second": 0,
"date_str": "2024-01-02",
"ts_floor": pd.Timestamp("2024-01-02"),
"ts_ceil": pd.Timestamp("2024-01-03"),
# round("day") rounds to nearest day; 10:30 < 12:00 so rounds down
"ts_round": pd.Timestamp("2024-01-02"),
}
]
)
assert rows_same(actual, expected)
def test_dt_namespace_invalid_dtype_raises(self, ray_start_regular_shared):
"""Test that dt namespace on non-datetime column raises an error."""
ds = ray.data.from_items([{"value": 1}])
with pytest.raises(Exception):
ds.with_column("year", col("value").dt.year()).to_pandas()
if __name__ == "__main__":
import sys
sys.exit(pytest.main(["-v", __file__]))
@@ -0,0 +1,183 @@
"""Integration tests for list namespace expressions.
These tests require Ray and test end-to-end list 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.exceptions import RayTaskError
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 TestListNamespace:
"""Tests for list namespace operations."""
def test_list_len(self, ray_start_regular_shared, dataset_format):
"""Test list.len() returns length of each list."""
data = [
{"items": [1, 2, 3]},
{"items": [4, 5]},
{"items": []},
]
ds = _create_dataset(data, dataset_format)
result = ds.with_column("len", col("items").list.len()).to_pandas()
expected = pd.DataFrame(
{
"items": [[1, 2, 3], [4, 5], []],
"len": [3, 2, 0],
}
)
assert rows_same(result, expected)
def test_list_get(self, ray_start_regular_shared, dataset_format):
"""Test list.get() extracts element at index."""
data = [
{"items": [10, 20, 30]},
{"items": [40, 50, 60]},
]
ds = _create_dataset(data, dataset_format)
result = ds.with_column("first", col("items").list.get(0)).to_pandas()
expected = pd.DataFrame(
{
"items": [[10, 20, 30], [40, 50, 60]],
"first": [10, 40],
}
)
assert rows_same(result, expected)
def test_list_bracket_index(self, ray_start_regular_shared, dataset_format):
"""Test list[i] bracket notation for element access."""
data = [{"items": [10, 20, 30]}]
ds = _create_dataset(data, dataset_format)
result = ds.with_column("elem", col("items").list[1]).to_pandas()
expected = pd.DataFrame(
{
"items": [[10, 20, 30]],
"elem": [20],
}
)
assert rows_same(result, expected)
def test_list_with_arithmetic(self, ray_start_regular_shared, dataset_format):
"""Test list operations combined with arithmetic."""
data = [{"items": [1, 2, 3]}]
ds = _create_dataset(data, dataset_format)
result = ds.with_column("len_plus_one", col("items").list.len() + 1).to_pandas()
expected = pd.DataFrame({"items": [[1, 2, 3]], "len_plus_one": [4]})
assert rows_same(result, expected)
def test_list_sort(self, ray_start_regular_shared, dataset_format):
"""Test list.sort() sorts each list with custom options."""
data = [
{"items": [3, 1, 2]},
{"items": [None, 4, 2]},
]
ds = _create_dataset(data, dataset_format)
method = col("items").list.sort(order="descending", null_placement="at_start")
result = ds.with_column("sorted", method).to_pandas()
expected = pd.DataFrame(
{
"items": [[3, 1, 2], [None, 4, 2]],
"sorted": [[3, 2, 1], [None, 4, 2]],
}
)
assert rows_same(result, expected)
def test_list_flatten(self, ray_start_regular_shared, dataset_format):
"""Test list.flatten() removes one nesting level."""
data = [
{"items": [[1, 2], [3]]},
{"items": [[], [4, 5]]},
]
ds = _create_dataset(data, dataset_format)
result = ds.with_column("flattened", col("items").list.flatten()).to_pandas()
expected = pd.DataFrame(
{
"items": [[[1, 2], [3]], [[], [4, 5]]],
"flattened": [[1, 2, 3], [4, 5]],
}
)
assert rows_same(result, expected)
def test_list_flatten_requires_nested_lists(
self, ray_start_regular_shared, dataset_format
):
"""list.flatten() should raise if elements aren't lists."""
data = [{"items": [1, 2]}, {"items": [3, 4]}]
ds = _create_dataset(data, dataset_format)
with pytest.raises(RayTaskError):
ds.with_column("flattened", col("items").list.flatten()).materialize()
def test_list_flatten_large_list_type(
self, ray_start_regular_shared, dataset_format
):
"""Flatten should preserve LargeList type when present."""
if dataset_format != "arrow":
pytest.skip("LargeList type only available via Arrow tables.")
arrow_type = pa.large_list(pa.list_(pa.int64()))
table = pa.Table.from_arrays(
[
pa.array(
[
[[1, 2], [3]],
[[], [4, 5]],
],
type=arrow_type,
)
],
names=["items"],
)
ds = _create_dataset(None, dataset_format, arrow_table=table)
result = ds.with_column("flattened", col("items").list.flatten())
arrow_refs = result.to_arrow_refs()
tables = ray.get(arrow_refs)
result_table = pa.concat_tables(tables) if len(tables) > 1 else tables[0]
flattened_type = result_table.schema.field("flattened").type
assert flattened_type == pa.large_list(pa.int64())
expected = pa.Table.from_arrays(
[
pa.array([[1, 2, 3], [4, 5]], type=pa.large_list(pa.int64())),
],
names=["flattened"],
)
assert result_table.select(["flattened"]).combine_chunks().equals(expected)
if __name__ == "__main__":
import sys
sys.exit(pytest.main(["-v", __file__]))
@@ -0,0 +1,150 @@
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__]))
@@ -0,0 +1,328 @@
"""Integration tests for string namespace expressions.
These tests require Ray and test end-to-end string 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)
@pytest.mark.parametrize(
"method_name,input_values,expected_results",
[
("len", ["Alice", "Bob"], [5, 3]),
("byte_len", ["ABC"], [3]),
],
)
class TestStringLength:
"""Tests for string length operations."""
def test_string_length(
self,
ray_start_regular_shared,
dataset_format,
method_name,
input_values,
expected_results,
):
"""Test string length methods."""
data = [{"name": v} for v in input_values]
ds = _create_dataset(data, dataset_format)
method = getattr(col("name").str, method_name)
result = ds.with_column("result", method()).to_pandas()
expected = pd.DataFrame({"name": input_values, "result": expected_results})
assert rows_same(result, expected)
@pytest.mark.parametrize("dataset_format", DATASET_FORMATS)
@pytest.mark.parametrize(
"method_name,input_values,expected_values",
[
("upper", ["alice", "bob"], ["ALICE", "BOB"]),
("lower", ["ALICE", "BOB"], ["alice", "bob"]),
("capitalize", ["alice", "bob"], ["Alice", "Bob"]),
("title", ["alice smith", "bob jones"], ["Alice Smith", "Bob Jones"]),
("swapcase", ["AlIcE"], ["aLiCe"]),
],
)
class TestStringCase:
"""Tests for string case conversion."""
def test_string_case(
self,
ray_start_regular_shared,
dataset_format,
method_name,
input_values,
expected_values,
):
"""Test string case conversion methods."""
data = [{"name": v} for v in input_values]
ds = _create_dataset(data, dataset_format)
method = getattr(col("name").str, method_name)
result = ds.with_column("result", method()).to_pandas()
expected = pd.DataFrame({"name": input_values, "result": expected_values})
assert rows_same(result, expected)
@pytest.mark.parametrize("dataset_format", DATASET_FORMATS)
@pytest.mark.parametrize(
"method_name,input_values,expected_results",
[
("is_alpha", ["abc", "abc123", "123"], [True, False, False]),
("is_alnum", ["abc123", "abc-123"], [True, False]),
("is_digit", ["123", "12a"], [True, False]),
("is_space", [" ", " a "], [True, False]),
("is_lower", ["abc", "Abc"], [True, False]),
("is_upper", ["ABC", "Abc"], [True, False]),
("is_ascii", ["hello", "hello😊"], [True, False]),
],
)
class TestStringPredicates:
"""Tests for string predicate methods (is_*)."""
def test_string_predicate(
self,
ray_start_regular_shared,
dataset_format,
method_name,
input_values,
expected_results,
):
"""Test string predicate methods."""
data = [{"val": v} for v in input_values]
ds = _create_dataset(data, dataset_format)
method = getattr(col("val").str, method_name)
result = ds.with_column("result", method()).to_pandas()
expected = pd.DataFrame({"val": input_values, "result": expected_results})
assert rows_same(result, expected)
@pytest.mark.parametrize("dataset_format", DATASET_FORMATS)
@pytest.mark.parametrize(
"method_name,method_args,input_values,expected_values",
[
("strip", (), [" hello ", " world "], ["hello", "world"]),
("strip", ("x",), ["xxxhelloxxx"], ["hello"]),
("lstrip", (), [" hello "], ["hello "]),
("rstrip", (), [" hello "], [" hello"]),
],
)
class TestStringTrimming:
"""Tests for string trimming operations."""
def test_string_trimming(
self,
ray_start_regular_shared,
dataset_format,
method_name,
method_args,
input_values,
expected_values,
):
"""Test string trimming methods."""
data = [{"val": v} for v in input_values]
ds = _create_dataset(data, dataset_format)
method = getattr(col("val").str, method_name)
result = ds.with_column("result", method(*method_args)).to_pandas()
expected = pd.DataFrame({"val": input_values, "result": expected_values})
assert rows_same(result, expected)
@pytest.mark.parametrize("dataset_format", DATASET_FORMATS)
@pytest.mark.parametrize(
"method_name,method_kwargs,expected_value",
[
("pad", {"width": 5, "fillchar": "*", "side": "right"}, "hi***"),
("pad", {"width": 5, "fillchar": "*", "side": "left"}, "***hi"),
("pad", {"width": 6, "fillchar": "*", "side": "both"}, "**hi**"),
("lpad", {"width": 5, "padding": "*"}, "***hi"),
("rpad", {"width": 5, "padding": "*"}, "hi***"),
("center", {"width": 6, "padding": "*"}, "**hi**"),
],
)
class TestStringPadding:
"""Tests for string padding operations."""
def test_string_padding(
self,
ray_start_regular_shared,
dataset_format,
method_name,
method_kwargs,
expected_value,
):
"""Test string padding methods."""
data = [{"val": "hi"}]
ds = _create_dataset(data, dataset_format)
method = getattr(col("val").str, method_name)
result = ds.with_column("result", method(**method_kwargs)).to_pandas()
expected = pd.DataFrame({"val": ["hi"], "result": [expected_value]})
assert rows_same(result, expected)
@pytest.mark.parametrize("dataset_format", DATASET_FORMATS)
@pytest.mark.parametrize(
"method_name,method_args,method_kwargs,input_values,expected_results",
[
("starts_with", ("A",), {}, ["Alice", "Bob", "Alex"], [True, False, True]),
("starts_with", ("A",), {"ignore_case": True}, ["alice", "bob"], [True, False]),
("ends_with", ("e",), {}, ["Alice", "Bob"], [True, False]),
("contains", ("li",), {}, ["Alice", "Bob", "Charlie"], [True, False, True]),
("find", ("i",), {}, ["Alice", "Bob"], [2, -1]),
("count", ("a",), {}, ["banana", "apple"], [3, 1]),
("match", ("Al%",), {}, ["Alice", "Bob", "Alex"], [True, False, True]),
],
)
class TestStringSearch:
"""Tests for string searching operations."""
def test_string_search(
self,
ray_start_regular_shared,
dataset_format,
method_name,
method_args,
method_kwargs,
input_values,
expected_results,
):
"""Test string searching methods."""
data = [{"val": v} for v in input_values]
ds = _create_dataset(data, dataset_format)
method = getattr(col("val").str, method_name)
result = ds.with_column(
"result", method(*method_args, **method_kwargs)
).to_pandas()
expected = pd.DataFrame({"val": input_values, "result": expected_results})
assert rows_same(result, expected)
@pytest.mark.parametrize("dataset_format", DATASET_FORMATS)
class TestStringTransform:
"""Tests for string transformation operations."""
def test_reverse(self, ray_start_regular_shared, dataset_format):
"""Test str.reverse() reverses strings."""
data = [{"val": "hello"}, {"val": "world"}]
ds = _create_dataset(data, dataset_format)
result = ds.with_column("rev", col("val").str.reverse()).to_pandas()
expected = pd.DataFrame({"val": ["hello", "world"], "rev": ["olleh", "dlrow"]})
assert rows_same(result, expected)
def test_slice(self, ray_start_regular_shared, dataset_format):
"""Test str.slice() extracts substring."""
data = [{"val": "hello"}]
ds = _create_dataset(data, dataset_format)
result = ds.with_column("sliced", col("val").str.slice(1, 4)).to_pandas()
expected = pd.DataFrame({"val": ["hello"], "sliced": ["ell"]})
assert rows_same(result, expected)
def test_replace(self, ray_start_regular_shared, dataset_format):
"""Test str.replace() replaces substring."""
data = [{"val": "hello world"}]
ds = _create_dataset(data, dataset_format)
result = ds.with_column(
"replaced", col("val").str.replace("world", "universe")
).to_pandas()
expected = pd.DataFrame(
{"val": ["hello world"], "replaced": ["hello universe"]}
)
assert rows_same(result, expected)
def test_replace_with_max(self, ray_start_regular_shared, dataset_format):
"""Test str.replace() with max_replacements."""
data = [{"val": "aaa"}]
ds = _create_dataset(data, dataset_format)
result = ds.with_column(
"replaced", col("val").str.replace("a", "X", max_replacements=2)
).to_pandas()
expected = pd.DataFrame({"val": ["aaa"], "replaced": ["XXa"]})
assert rows_same(result, expected)
def test_repeat(self, ray_start_regular_shared, dataset_format):
"""Test str.repeat() repeats strings."""
data = [{"val": "A"}]
ds = _create_dataset(data, dataset_format)
result = ds.with_column("repeated", col("val").str.repeat(3)).to_pandas()
expected = pd.DataFrame({"val": ["A"], "repeated": ["AAA"]})
assert rows_same(result, expected)
def test_string_with_comparison(self, ray_start_regular_shared, dataset_format):
"""Test string operations combined with comparison."""
data = [{"name": "Alice"}, {"name": "Bo"}]
ds = _create_dataset(data, dataset_format)
result = ds.with_column("long_name", col("name").str.len() > 3).to_pandas()
expected = pd.DataFrame({"name": ["Alice", "Bo"], "long_name": [True, False]})
assert rows_same(result, expected)
def test_multiple_string_operations(self, ray_start_regular_shared, dataset_format):
"""Test multiple namespace operations in single pipeline."""
data = [{"name": "alice"}]
ds = _create_dataset(data, dataset_format)
result = (
ds.with_column("upper", col("name").str.upper())
.with_column("len", col("name").str.len())
.with_column("starts_a", col("name").str.starts_with("a"))
.to_pandas()
)
expected = pd.DataFrame(
{
"name": ["alice"],
"upper": ["ALICE"],
"len": [5],
"starts_a": [True],
}
)
assert rows_same(result, expected)
if __name__ == "__main__":
import sys
sys.exit(pytest.main(["-v", __file__]))
@@ -0,0 +1,313 @@
"""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__]))
@@ -0,0 +1,287 @@
"""Integration tests for predicate expression operations.
These tests require Ray and test end-to-end predicate expression evaluation.
"""
import pandas as pd
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.expressions import col
from ray.data.tests.conftest import * # noqa
from ray.tests.conftest import * # noqa
pytestmark = pytest.mark.skipif(
get_pyarrow_version() < parse_version("20.0.0"),
reason="Expression integration tests require PyArrow >= 20.0.0",
)
class TestPredicateIntegration:
"""Integration tests for predicate expressions with Ray Dataset."""
def test_null_predicates_with_dataset(self, ray_start_regular_shared):
"""Test null predicate expressions with Ray Dataset."""
ds = ray.data.from_items(
[
{"value": 10, "name": "Alice"},
{"value": None, "name": "Bob"},
{"value": 30, "name": None},
{"value": None, "name": None},
]
)
result = (
ds.with_column("value_is_null", col("value").is_null())
.with_column("name_not_null", col("name").is_not_null())
.with_column(
"both_present", col("value").is_not_null() & col("name").is_not_null()
)
.to_pandas()
)
expected = pd.DataFrame(
{
"value": [10, None, 30, None],
"name": ["Alice", "Bob", None, None],
"value_is_null": [False, True, False, True],
"name_not_null": [True, True, False, False],
"both_present": [True, False, False, False],
}
)
assert rows_same(result, expected)
def test_membership_predicates_with_dataset(self, ray_start_regular_shared):
"""Test membership predicate expressions with Ray Dataset."""
ds = ray.data.from_items(
[
{"status": "active", "category": "A"},
{"status": "inactive", "category": "B"},
{"status": "pending", "category": "A"},
{"status": "deleted", "category": "C"},
]
)
result = (
ds.with_column(
"is_valid_status", col("status").is_in(["active", "pending"])
)
.with_column("not_deleted", col("status").not_in(["deleted"]))
.with_column("category_a", col("category").is_in(["A"]))
.to_pandas()
)
expected = pd.DataFrame(
{
"status": ["active", "inactive", "pending", "deleted"],
"category": ["A", "B", "A", "C"],
"is_valid_status": [True, False, True, False],
"not_deleted": [True, True, True, False],
"category_a": [True, False, True, False],
}
)
pd.testing.assert_frame_equal(result, expected, check_dtype=False)
@pytest.mark.parametrize(
"test_data,expression,expected_results,test_id",
[
pytest.param(
[{"value": 1}, {"value": None}, {"value": 3}],
col("value").is_null(),
[False, True, False],
"is_null_with_actual_nulls",
),
pytest.param(
[{"value": 1}, {"value": None}, {"value": 3}],
col("value").is_not_null(),
[True, False, True],
"is_not_null_with_actual_nulls",
),
pytest.param(
[{"value": 1}, {"value": 2}, {"value": 3}],
col("value").is_in([1, 3]),
[True, False, True],
"isin_operation",
),
pytest.param(
[{"value": 1}, {"value": 2}, {"value": 3}],
col("value").not_in([1, 3]),
[False, True, False],
"not_in_operation",
),
pytest.param(
[{"name": "Alice"}, {"name": "Bob"}, {"name": "Charlie"}],
col("name") == "Bob",
[False, True, False],
"string_equality",
),
pytest.param(
[{"name": "Alice"}, {"name": "Bob"}, {"name": "Charlie"}],
col("name") != "Bob",
[True, False, True],
"string_not_equal",
),
pytest.param(
[{"name": "included"}, {"name": "excluded"}, {"name": None}],
col("name").is_not_null() & (col("name") != "excluded"),
[True, False, False],
"string_filter",
),
],
)
def test_null_and_membership_with_dataset(
self, ray_start_regular_shared, test_data, expression, expected_results, test_id
):
"""Test null checking and membership operations with Ray Dataset."""
ds = ray.data.from_items(test_data)
result = ds.with_column("result", expression).to_pandas()
expected_data = {}
for key in test_data[0].keys():
expected_data[key] = [row[key] for row in test_data]
expected_data["result"] = expected_results
expected = pd.DataFrame(expected_data)
assert rows_same(result, expected)
@pytest.mark.parametrize(
"filter_expr,test_data,expected_flags,test_id",
[
pytest.param(
col("age") >= 21,
[
{"age": 20, "name": "Alice"},
{"age": 21, "name": "Bob"},
{"age": 25, "name": "Charlie"},
],
[False, True, True],
"age_filter",
),
pytest.param(
col("score") > 50,
[
{"score": 30, "status": "fail"},
{"score": 50, "status": "pass"},
{"score": 70, "status": "pass"},
],
[False, False, True],
"score_filter",
),
pytest.param(
(col("age") >= 18) & col("active"),
[
{"age": 17, "active": True},
{"age": 18, "active": False},
{"age": 25, "active": True},
],
[False, False, True],
"complex_and_filter",
),
pytest.param(
(col("status") == "approved") | (col("priority") == "high"),
[
{"status": "pending", "priority": "low"},
{"status": "approved", "priority": "low"},
{"status": "pending", "priority": "high"},
],
[False, True, True],
"complex_or_filter",
),
pytest.param(
col("value").is_not_null() & (col("value") > 0),
[{"value": None}, {"value": -5}, {"value": 10}],
[False, False, True],
"null_aware_filter",
),
pytest.param(
col("name").is_not_null() & (col("name") != "excluded"),
[{"name": "included"}, {"name": "excluded"}, {"name": None}],
[True, False, False],
"string_filter",
),
pytest.param(
col("category").is_in(["A", "B"]),
[
{"category": "A"},
{"category": "B"},
{"category": "C"},
{"category": "D"},
],
[True, True, False, False],
"membership_filter",
),
pytest.param(
(col("score") >= 50) & (col("grade") != "F"),
[
{"score": 45, "grade": "F"},
{"score": 55, "grade": "D"},
{"score": 75, "grade": "B"},
{"score": 30, "grade": "F"},
],
[False, True, True, False],
"nested_filters",
),
],
)
def test_filter_expressions_with_dataset(
self, ray_start_regular_shared, filter_expr, test_data, expected_flags, test_id
):
"""Test filter expressions with Ray Dataset."""
ds = ray.data.from_items(test_data)
result = ds.with_column("is_filtered", filter_expr).to_pandas()
expected = pd.DataFrame(test_data)
expected["is_filtered"] = expected_flags
assert rows_same(result, expected)
def test_filter_in_pipeline_with_dataset(self, ray_start_regular_shared):
"""Test filter expressions in a data processing pipeline."""
test_data = [
{"product": "A", "quantity": 10, "price": 100, "region": "North"},
{"product": "B", "quantity": 5, "price": 200, "region": "South"},
{"product": "C", "quantity": 20, "price": 50, "region": "North"},
{"product": "D", "quantity": 15, "price": 75, "region": "East"},
{"product": "E", "quantity": 3, "price": 300, "region": "West"},
]
ds = ray.data.from_items(test_data)
result = (
ds.with_column("revenue", col("quantity") * col("price"))
.with_column("is_high_value", col("revenue") >= 1000)
.with_column("is_bulk_order", col("quantity") >= 10)
.with_column("is_premium", col("price") >= 100)
.with_column(
"needs_special_handling",
(col("is_high_value")) | (col("is_bulk_order") & col("is_premium")),
)
.with_column("is_north_region", col("region") == "North")
.to_pandas()
)
expected = pd.DataFrame(
{
"product": ["A", "B", "C", "D", "E"],
"quantity": [10, 5, 20, 15, 3],
"price": [100, 200, 50, 75, 300],
"region": ["North", "South", "North", "East", "West"],
"revenue": [1000, 1000, 1000, 1125, 900],
"is_high_value": [True, True, True, True, False],
"is_bulk_order": [True, False, True, True, False],
"is_premium": [True, True, False, False, True],
"needs_special_handling": [True, True, True, True, False],
"is_north_region": [True, False, True, False, False],
}
)
assert rows_same(result, expected)
if __name__ == "__main__":
import sys
sys.exit(pytest.main(["-v", __file__]))