172 lines
4.9 KiB
Python
172 lines
4.9 KiB
Python
import os
|
|
|
|
import pandas as pd
|
|
import pyarrow as pa
|
|
import pytest
|
|
from packaging.version import parse as parse_version
|
|
|
|
import ray
|
|
from ray.data import Schema
|
|
from ray.data._internal.util import rows_same
|
|
from ray.data._internal.utils.arrow_utils import get_pyarrow_version
|
|
from ray.data.tests.conftest import * # noqa
|
|
from ray.data.tests.mock_http_server import * # noqa
|
|
from ray.tests.conftest import * # noqa
|
|
|
|
# deltalake's write_deltalake requires pyarrow >= 15 for the Arrow C Stream interface.
|
|
_pa_version = get_pyarrow_version()
|
|
assert _pa_version is not None, "pyarrow must be installed to run these tests"
|
|
|
|
pytestmark = pytest.mark.skipif(
|
|
_pa_version < parse_version("15.0.0"),
|
|
reason="deltalake write_deltalake requires pyarrow >= 15.0",
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"batch_size",
|
|
[1, 100],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
"write_mode",
|
|
["append", "overwrite"],
|
|
)
|
|
def test_delta_read_basic(tmp_path, batch_size, write_mode):
|
|
from deltalake import write_deltalake
|
|
|
|
# Parse the data path.
|
|
path = os.path.join(tmp_path, "tmp_test_delta")
|
|
|
|
# Create a sample Delta Lake table
|
|
df = pd.DataFrame(
|
|
{"x": [42] * batch_size, "y": ["a"] * batch_size, "z": [3.14] * batch_size}
|
|
)
|
|
table = pa.Table.from_pandas(df)
|
|
if write_mode == "append":
|
|
write_deltalake(path, table, mode=write_mode)
|
|
write_deltalake(path, table, mode=write_mode)
|
|
expected = pd.concat([df, df], ignore_index=True)
|
|
elif write_mode == "overwrite":
|
|
write_deltalake(path, table, mode=write_mode)
|
|
expected = df
|
|
else:
|
|
raise ValueError(f"Unexpected write_mode: {write_mode}")
|
|
|
|
# Read the Delta Lake table
|
|
ds = ray.data.read_delta(path)
|
|
|
|
assert ds.schema() == Schema(
|
|
pa.schema(
|
|
{
|
|
"x": pa.int64(),
|
|
"y": pa.string(),
|
|
"z": pa.float64(),
|
|
}
|
|
)
|
|
)
|
|
assert rows_same(ds.to_pandas(), expected)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"columns, expected_columns",
|
|
[
|
|
(["a", "c"], ["a", "c"]),
|
|
(["b"], ["b"]),
|
|
(["a", "b", "c"], ["a", "b", "c"]),
|
|
],
|
|
)
|
|
def test_delta_read_column_selection(tmp_path, columns, expected_columns):
|
|
from deltalake import write_deltalake
|
|
|
|
path = os.path.join(tmp_path, "tmp_test_delta_cols")
|
|
df = pd.DataFrame({"a": [1, 2, 3], "b": ["x", "y", "z"], "c": [1.0, 2.0, 3.0]})
|
|
write_deltalake(path, pa.Table.from_pandas(df))
|
|
|
|
ds = ray.data.read_delta(path, columns=columns)
|
|
expected = df[expected_columns]
|
|
|
|
assert ds.schema().names == expected_columns
|
|
assert rows_same(ds.to_pandas(), expected)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"version, expected_data",
|
|
[
|
|
(0, {"x": [1, 2]}),
|
|
(1, {"x": [3, 4, 5]}),
|
|
(None, {"x": [3, 4, 5]}),
|
|
],
|
|
)
|
|
def test_delta_read_version(tmp_path, version, expected_data):
|
|
from deltalake import write_deltalake
|
|
|
|
path = os.path.join(tmp_path, "tmp_test_delta_version")
|
|
write_deltalake(path, pa.table({"x": [1, 2]}))
|
|
write_deltalake(path, pa.table({"x": [3, 4, 5]}), mode="overwrite")
|
|
|
|
ds = ray.data.read_delta(path, version=version)
|
|
expected = pd.DataFrame(expected_data)
|
|
|
|
assert rows_same(ds.to_pandas(), expected)
|
|
|
|
|
|
def test_delta_read_schema_evolution(tmp_path):
|
|
"""Older files missing newer columns should be null-filled."""
|
|
from deltalake import write_deltalake
|
|
|
|
path = os.path.join(tmp_path, "tmp_test_delta_schema_evo")
|
|
|
|
write_deltalake(path, pa.table({"x": [1, 2]}))
|
|
write_deltalake(
|
|
path,
|
|
pa.table({"x": [3, 4], "y": ["a", "b"]}),
|
|
mode="append",
|
|
schema_mode="merge", # pyrefly: ignore[unexpected-keyword]
|
|
)
|
|
|
|
ds = ray.data.read_delta(path)
|
|
expected = pd.DataFrame(
|
|
{"x": [1, 2, 3, 4], "y": [None, None, "a", "b"]},
|
|
)
|
|
# Match the Arrow-backed null sentinel produced by ``to_pandas()``.
|
|
expected["y"] = expected["y"].astype("string")
|
|
|
|
assert rows_same(ds.to_pandas(), expected)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"storage_options",
|
|
[{}, None],
|
|
)
|
|
def test_delta_read_storage_options(tmp_path, storage_options):
|
|
"""Verify that storage_options are forwarded to DeltaTable."""
|
|
from deltalake import write_deltalake
|
|
|
|
path = os.path.join(tmp_path, "tmp_test_delta_storage_opts")
|
|
df = pd.DataFrame({"x": [1, 2, 3]})
|
|
write_deltalake(path, pa.Table.from_pandas(df))
|
|
|
|
ds = ray.data.read_delta(path, storage_options=storage_options)
|
|
assert rows_same(ds.to_pandas(), df)
|
|
|
|
|
|
def test_delta_read_empty_table(tmp_path):
|
|
from deltalake import write_deltalake
|
|
|
|
path = os.path.join(tmp_path, "tmp_test_delta_empty")
|
|
write_deltalake(path, pa.table({"x": pa.array([], type=pa.int64())}))
|
|
|
|
ds = ray.data.read_delta(path)
|
|
assert ds.count() == 0
|
|
|
|
|
|
def test_delta_read_rejects_multiple_paths():
|
|
with pytest.raises(ValueError, match="Only a single Delta Lake table path"):
|
|
ray.data.read_delta(["path1", "path2"])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import sys
|
|
|
|
sys.exit(pytest.main(["-v", __file__]))
|