blob: c127fe271aabb32d360c972d7e82f36c92527a6d [file]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from unittest.mock import Mock, patch
import pyarrow as pa
import pytest
import ray.cloudpickle
from ray.data import DataContext
from pypaimon import CatalogFactory, Schema
from pypaimon.read.datasource.ray_datasource import RayDatasource
from pypaimon.read.datasource.split_provider import CatalogSplitProvider
from pypaimon.read.table_read import TableRead
@pytest.fixture
def catalog_options(tmp_path):
options = {"warehouse": str(tmp_path)}
catalog = CatalogFactory.create(options)
catalog.create_database("default", True)
data = pa.table({"id": list(range(5))})
catalog.create_table("default.data", Schema.from_pyarrow_schema(data.schema), False)
table = catalog.get_table("default.data")
builder = table.new_batch_write_builder()
writer = builder.new_write()
commit = builder.new_commit()
try:
writer.write_arrow(data)
commit.commit(writer.prepare_commit())
finally:
writer.close()
commit.close()
return options
@pytest.mark.parametrize("limit,task_limit,expected", [
(None, None, 5), (2, None, 2), (None, 2, 2),
(4, 2, 2), (2, 4, 2), (0, None, 0), (None, 0, 0),
(10, None, 5),
])
def test_task_metadata_matches_limited_rows(catalog_options, limit, task_limit, expected):
source = RayDatasource(CatalogSplitProvider(
"default.data", catalog_options, limit=limit))
tasks = source.get_read_tasks(
1, per_task_row_limit=task_limit, data_context=DataContext.get_current())
# The native planner can return no splits for a zero scan limit.
rows = 0
for task in tasks:
task = ray.cloudpickle.loads(ray.cloudpickle.dumps(task))
task_rows = sum(block.num_rows for block in task())
assert task.metadata.num_rows == task_rows
rows += task_rows
assert rows == expected
def test_filter_keeps_unknown_row_count_with_limit(catalog_options):
table = CatalogFactory.create(catalog_options).get_table("default.data")
predicate = table.new_read_builder().new_predicate_builder().greater_than("id", 1)
source = RayDatasource(CatalogSplitProvider(
"default.data", catalog_options, predicate=predicate, limit=2))
task = source.get_read_tasks(1, per_task_row_limit=1)[0]
assert task.metadata.num_rows is None
assert sum(block.num_rows for block in task()) == 1
def test_ray_limit_is_applied_inside_worker_reader(catalog_options):
source = RayDatasource(CatalogSplitProvider("default.data", catalog_options, limit=4))
task = source.get_read_tasks(1, per_task_row_limit=2)[0]
original = TableRead.to_arrow_batch_reader
limits = []
def track_reader(read, splits):
limits.append(read.limit)
return original(read, splits)
with patch.object(TableRead, "to_arrow_batch_reader", track_reader):
assert sum(block.num_rows for block in task()) == 2
assert limits == [2]
@pytest.mark.parametrize("finish", ["exhausted", "cancelled", "failed", "empty"])
@pytest.mark.parametrize("fail_close", [False, True])
def test_worker_closes_batch_reader(catalog_options, finish, fail_close):
source = RayDatasource(CatalogSplitProvider("default.data", catalog_options))
task = source.get_read_tasks(1)[0]
batch = pa.record_batch([pa.array([0, 1])], names=["id"])
read_error = RuntimeError("read failed")
close_error = OSError("close failed") if fail_close else None
batches = [batch, StopIteration()]
if finish == "failed":
batches = [batch, read_error]
elif finish == "empty":
batches = [StopIteration()]
reader = Mock()
reader.read_next_batch.side_effect = batches
reader.close.side_effect = close_error
with patch.object(TableRead, "to_arrow_batch_reader", return_value=reader):
blocks = task.read_fn()
if finish == "cancelled":
assert next(blocks).num_rows == 2
consume = blocks.close
else:
consume = lambda: list(blocks)
expected_error = read_error if finish == "failed" else close_error
if expected_error is not None:
with pytest.raises(type(expected_error)) as exc_info:
consume()
assert exc_info.value is expected_error
else:
result = consume()
if finish != "cancelled":
assert sum(block.num_rows for block in result) == (0 if finish == "empty" else 2)
reader.close.assert_called_once_with()