This is an automated email from the ASF dual-hosted git repository.
JingsongLi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git
The following commit(s) were added to refs/heads/master by this push:
new 158982cd1e [python][ray] Fix update_by_row_id failure on empty
datasets (#9217)
158982cd1e is described below
commit 158982cd1eebfbc60a3395f2a7b628ad66a88905
Author: XiaoHongbo <[email protected]>
AuthorDate: Sat Aug 15 20:18:00 2026 +0800
[python][ray] Fix update_by_row_id failure on empty datasets (#9217)
---
.github/workflows/paimon-python-checks.yml | 9 ++++++---
.../pypaimon/ray/data_evolution_merge_join.py | 17 ++++++++++++++++-
.../tests/ray_data_evolution_merge_into_test.py | 18 ++++++++++++++++++
.../pypaimon/tests/ray_update_by_row_id_test.py | 20 ++++++++++++++++++++
4 files changed, 60 insertions(+), 4 deletions(-)
diff --git a/.github/workflows/paimon-python-checks.yml
b/.github/workflows/paimon-python-checks.yml
index 9cc772dd99..6e56db488d 100755
--- a/.github/workflows/paimon-python-checks.yml
+++ b/.github/workflows/paimon-python-checks.yml
@@ -300,13 +300,16 @@ jobs:
echo "=========================================="
echo "Testing Ray version compatibility"
echo "=========================================="
- for ray_version in 2.44.0 2.48.0 2.53.0; do
+ for ray_version in 2.44.0 2.48.0 2.50.1 2.53.0; do
echo "Testing Ray version: $ray_version"
python -m pip install --no-cache-dir -q ray==$ray_version
python -c "import ray; print(f'Ray version: {ray.__version__}')"
python -c "from packaging.version import parse; import ray; assert
parse(ray.__version__) == parse('$ray_version'), f'Expected Ray $ray_version,
got {ray.__version__}'"
- python -m pytest pypaimon/tests/ray_data_test.py::RayDataTest \
- pypaimon/tests/ray_range_join_test.py::RayRangeJoinTest -v
--tb=short || {
+ tests="pypaimon/tests/ray_data_test.py::RayDataTest
pypaimon/tests/ray_range_join_test.py::RayRangeJoinTest"
+ if [ "$ray_version" = "2.50.1" ]; then
+ tests="$tests
pypaimon/tests/ray_update_by_row_id_test.py::RayUpdateByRowIdTest::test_empty_dataset_after_transform_is_noop"
+ fi
+ python -m pytest $tests -v --tb=short || {
echo "Tests failed for Ray $ray_version"; python -m pip
uninstall -y ray; exit 1;
}
python -m pip uninstall -y ray
diff --git a/paimon-python/pypaimon/ray/data_evolution_merge_join.py
b/paimon-python/pypaimon/ray/data_evolution_merge_join.py
index 359af8e6ef..4a0dd74b35 100644
--- a/paimon-python/pypaimon/ray/data_evolution_merge_join.py
+++ b/paimon-python/pypaimon/ray/data_evolution_merge_join.py
@@ -503,6 +503,7 @@ def distributed_update_apply(
precomputed_info_ref = ray.put(files_info)
frid_col = "_FIRST_ROW_ID"
+ sentinel_row_id = -1
captured_sorted = sorted_first_row_ids
captured_sorted_arr = np.asarray(captured_sorted, dtype=np.int64)
valid_ranges = planner.valid_row_id_ranges
@@ -544,11 +545,25 @@ def distributed_update_apply(
map_kwargs = _map_kwargs(ray_remote_args)
with_frid = update_ds.map_batches(_assign_frid, **map_kwargs)
+ from pypaimon.schema.data_types import PyarrowFieldParser
+ target_pa =
PyarrowFieldParser.from_paimon_schema(table.table_schema.fields)
+ update_schema = build_update_schema(target_pa, cols, row_id_name)
+ # Ray drops the schema of an empty shuffle input. A negative row id keeps
+ # the group-by schema in a separate group that is never written.
+ sentinel = pa.Table.from_arrays(
+ [pa.array([sentinel_row_id], type=pa.int64())]
+ + [pa.nulls(1, type=update_schema.field(col).type) for col in cols],
+ schema=update_schema,
+ ).append_column(
+ frid_col, pa.array([sentinel_row_id], type=pa.int64())
+ )
+ with_frid = with_frid.union(ray.data.from_arrow(sentinel))
+
captured_table = table
captured_cols = cols
def _apply_group(group: pa.Table) -> pa.Table:
- if group.num_rows == 0:
+ if group.column(frid_col)[0].as_py() == sentinel_row_id:
return pa.Table.from_pydict({
"msgs_blob": pa.array([], type=pa.binary()),
"n_updated": pa.array([], type=pa.int64()),
diff --git a/paimon-python/pypaimon/tests/ray_data_evolution_merge_into_test.py
b/paimon-python/pypaimon/tests/ray_data_evolution_merge_into_test.py
index cddb11ac37..2e1e8b8e44 100644
--- a/paimon-python/pypaimon/tests/ray_data_evolution_merge_into_test.py
+++ b/paimon-python/pypaimon/tests/ray_data_evolution_merge_into_test.py
@@ -849,6 +849,24 @@ class RayDataEvolutionMergeIntoTest(unittest.TestCase):
self.assertEqual(self._snapshot_id(target), before)
+ def test_matched_update_with_no_matches_is_noop(self):
+ target = self._create_table()
+ self._write(target, self._source(ids=(1,)))
+ before = self._snapshot_id(target)
+
+ result = merge_into(
+ target=target,
+ source=self._source(ids=(2,)),
+ catalog_options=self.catalog_options,
+ on=['id'],
+ when_matched=[WhenMatched.update('*')],
+ num_partitions=_TEST_NUM_PARTITIONS,
+ )
+
+ self.assertEqual(result['num_matched'], 0)
+ self.assertEqual(self._snapshot_id(target), before)
+ self.assertEqual(self._read_sorted(target)['id'], [1])
+
def test_matched_on_partitioned_table(self):
pt_schema = pa.schema([
('pt', pa.string()),
diff --git a/paimon-python/pypaimon/tests/ray_update_by_row_id_test.py
b/paimon-python/pypaimon/tests/ray_update_by_row_id_test.py
index 613fa50d03..801c35218a 100644
--- a/paimon-python/pypaimon/tests/ray_update_by_row_id_test.py
+++ b/paimon-python/pypaimon/tests/ray_update_by_row_id_test.py
@@ -130,6 +130,26 @@ class RayUpdateByRowIdTest(unittest.TestCase):
self.assertEqual(got[21], 999)
self.assertTrue(all(v == 0 for k, v in got.items() if k != 21))
+ def test_empty_dataset_after_transform_is_noop(self):
+ target = self._create()
+ self._write(target, pa.Table.from_pydict(
+ {"id": [1], "name": ["a"], "age": [1]}, schema=self.pa_schema))
+ rid = self._rowid_by_id(target)
+ source = ray.data.from_arrow(pa.table({
+ "_ROW_ID": pa.array([rid[1]], pa.int64()),
+ "age": pa.array([9], pa.int32()),
+ })).map_batches(
+ lambda batch: batch.slice(0, 0),
+ batch_format="pyarrow",
+ )
+
+ self.assertEqual(
+ update_by_row_id(
+ target, source, self.catalog_options, update_cols=["age"]),
+ {"num_updated": 0},
+ )
+ self.assertEqual(self._read(target).column("age").to_pylist(), [1])
+
def test_pins_base_snapshot_for_conflict_detection(self):
# The update pins its base snapshot and threads it to
distributed_update_apply,
# which uses it for commit-time conflict detection against concurrent
writers.