| ################################################################################ |
| # 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. |
| ################################################################################ |
| |
| import unittest |
| from unittest.mock import Mock, patch |
| |
| import pyarrow as pa |
| |
| from pypaimon.table.data_evolution_merge_into import ( |
| WhenMatched, |
| WhenNotMatched, |
| lit, |
| source_col, |
| target_col, |
| ) |
| from pypaimon.tests.data_evolution_test_helpers import ( |
| BatchModeMixin, |
| DataEvolutionTestBase, |
| StreamModeMixin, |
| ) |
| |
| try: |
| import datafusion # noqa: F401 |
| |
| _HAS_DATAFUSION = True |
| except ImportError: |
| _HAS_DATAFUSION = False |
| |
| _SKIP_CONDITION = not _HAS_DATAFUSION |
| _SKIP_REASON = "pypaimon[sql] is required for condition expressions" |
| |
| |
| class TableMergeIntoTest(BatchModeMixin, DataEvolutionTestBase, unittest.TestCase): |
| |
| def _read_sorted(self, table): |
| return self._read_all(table).sort_by("id").to_pydict() |
| |
| def _read_projected_sorted(self, table, projection): |
| rb = table.new_read_builder().with_projection(projection) |
| return rb.new_read().to_arrow(rb.new_scan().plan().splits()).sort_by( |
| "id" |
| ).to_pydict() |
| |
| def _row_ids_by_id(self, table): |
| rows = self._read_projected_sorted(table, ["id", "_ROW_ID"]) |
| return dict(zip(rows["id"], rows["_ROW_ID"])) |
| |
| def _merge_and_commit(self, table, source, **kwargs): |
| wb = table.new_batch_write_builder() |
| table_update = wb.new_update() |
| msgs = table_update.merge_into(source, **kwargs) |
| commit = wb.new_commit() |
| commit.commit(msgs) |
| commit.close() |
| return msgs |
| |
| def test_when_matched_action_constructors(self): |
| update = WhenMatched.update( |
| {"age": source_col("age")}, |
| condition="s.age > t.age", |
| ) |
| delete = WhenMatched.delete(condition="s.deleted = TRUE") |
| |
| self.assertEqual({"age": source_col("age")}, update.update) |
| self.assertEqual("s.age > t.age", update.condition) |
| self.assertFalse(update.delete) |
| self.assertIsNone(delete.update) |
| self.assertEqual("s.deleted = TRUE", delete.condition) |
| self.assertTrue(delete.delete) |
| |
| def test_ray_merge_rejects_mixed_update_delete_duplicate_row_ids(self): |
| import pypaimon.ray.data_evolution_merge_into as ray_merge |
| |
| with self.assertRaisesRegex(ValueError, "multiple source rows"): |
| ray_merge._validate_disjoint_action_row_ids([7], [7]) |
| |
| def _mock_ray_commit_table(self): |
| table = Mock() |
| write_builder = Mock() |
| table_commit = Mock() |
| table.new_batch_write_builder.return_value = write_builder |
| write_builder.new_commit.return_value = table_commit |
| return table, table_commit |
| |
| def test_ray_execute_validates_mixed_update_delete_duplicate_row_ids(self): |
| import pypaimon.ray.data_evolution_merge_into as ray_merge |
| |
| update_msg = object() |
| delete_msg = object() |
| table, table_commit = self._mock_ray_commit_table() |
| |
| with patch.object( |
| ray_merge, |
| "distributed_update_apply", |
| return_value=([update_msg], 1, [7]) |
| ), patch.object( |
| ray_merge, |
| "distributed_delete_apply", |
| return_value=([delete_msg], 1, [7]) |
| ): |
| with self.assertRaisesRegex(ValueError, "multiple source rows"): |
| ray_merge._execute_and_commit( |
| table=table, |
| update_ds=object(), |
| delete_ds=object(), |
| insert_ds=None, |
| update_cols_union=["age"], |
| base_snapshot=None, |
| num_partitions=1, |
| ray_remote_args=None, |
| concurrency=None, |
| ) |
| table_commit.abort.assert_called_once_with([update_msg, delete_msg]) |
| table_commit.close.assert_called_once_with() |
| |
| def test_ray_execute_aborts_prepared_messages_on_later_branch_failure(self): |
| import pypaimon.ray.data_evolution_merge_into as ray_merge |
| |
| update_msg = object() |
| table, table_commit = self._mock_ray_commit_table() |
| |
| with patch.object( |
| ray_merge, |
| "distributed_update_apply", |
| return_value=([update_msg], 1, []) |
| ), patch.object( |
| ray_merge, |
| "distributed_delete_apply", |
| side_effect=RuntimeError("delete failed") |
| ): |
| with self.assertRaisesRegex(RuntimeError, "delete failed"): |
| ray_merge._execute_and_commit( |
| table=table, |
| update_ds=object(), |
| delete_ds=object(), |
| insert_ds=None, |
| update_cols_union=["age"], |
| base_snapshot=None, |
| num_partitions=1, |
| ray_remote_args=None, |
| concurrency=None, |
| ) |
| table_commit.abort.assert_called_once_with([update_msg]) |
| table_commit.close.assert_called_once_with() |
| table_commit.commit.assert_not_called() |
| |
| def test_ray_execute_aborts_prepared_messages_on_insert_failure(self): |
| import pypaimon.ray.data_evolution_merge_into as ray_merge |
| |
| update_msg = object() |
| table, table_commit = self._mock_ray_commit_table() |
| |
| with patch.object( |
| ray_merge, |
| "distributed_update_apply", |
| return_value=([update_msg], 1, []) |
| ), patch.object( |
| ray_merge, |
| "distributed_write_collect_msgs", |
| side_effect=RuntimeError("insert failed") |
| ): |
| with self.assertRaisesRegex(RuntimeError, "insert failed"): |
| ray_merge._execute_and_commit( |
| table=table, |
| update_ds=object(), |
| delete_ds=None, |
| insert_ds=object(), |
| update_cols_union=["age"], |
| base_snapshot=None, |
| num_partitions=1, |
| ray_remote_args=None, |
| concurrency=None, |
| ) |
| table_commit.abort.assert_called_once_with([update_msg]) |
| table_commit.close.assert_called_once_with() |
| table_commit.commit.assert_not_called() |
| |
| def test_ray_execute_does_not_abort_after_commit_starts(self): |
| import pypaimon.ray.data_evolution_merge_into as ray_merge |
| |
| update_msg = object() |
| table, table_commit = self._mock_ray_commit_table() |
| table_commit.commit.side_effect = RuntimeError("commit failed") |
| |
| with patch.object( |
| ray_merge, |
| "distributed_update_apply", |
| return_value=([update_msg], 1, []) |
| ): |
| with self.assertRaisesRegex(RuntimeError, "commit failed"): |
| ray_merge._execute_and_commit( |
| table=table, |
| update_ds=object(), |
| delete_ds=None, |
| insert_ds=None, |
| update_cols_union=["age"], |
| base_snapshot=None, |
| num_partitions=1, |
| ray_remote_args=None, |
| concurrency=None, |
| ) |
| table_commit.commit.assert_called_once_with([update_msg]) |
| table_commit.abort.assert_not_called() |
| table_commit.close.assert_called_once_with() |
| |
| def test_table_merge_into_updates_and_inserts(self): |
| target = self._create_table() |
| self._write_arrow(target, pa.Table.from_pydict({ |
| "id": pa.array([1, 2], type=pa.int32()), |
| "name": ["Alice", "Bob"], |
| "age": pa.array([25, 30], type=pa.int32()), |
| "city": ["NYC", "LA"], |
| }, schema=self.pa_schema)) |
| |
| source = pa.Table.from_pydict({ |
| "id": pa.array([2, 3], type=pa.int32()), |
| "name": ["Bobby", "Cindy"], |
| "age": pa.array([31, 22], type=pa.int32()), |
| "city": ["LA2", "SF"], |
| }, schema=self.pa_schema) |
| |
| msgs = self._merge_and_commit( |
| target, |
| source, |
| on=["id"], |
| when_matched=[WhenMatched.update("*")], |
| when_not_matched=[WhenNotMatched(insert="*")], |
| ) |
| |
| self.assertTrue(msgs) |
| self.assertEqual( |
| { |
| "id": [1, 2, 3], |
| "name": ["Alice", "Bobby", "Cindy"], |
| "age": [25, 31, 22], |
| "city": ["NYC", "LA2", "SF"], |
| }, |
| self._read_sorted(target), |
| ) |
| |
| def test_table_merge_into_insert_only_on_empty_table(self): |
| target = self._create_table() |
| source = pa.Table.from_pydict({ |
| "id": pa.array([1, 2], type=pa.int32()), |
| "name": ["Alice", "Bob"], |
| "age": pa.array([25, 30], type=pa.int32()), |
| "city": ["NYC", "LA"], |
| }, schema=self.pa_schema) |
| |
| msgs = self._merge_and_commit( |
| target, |
| source, |
| on=["id"], |
| when_not_matched=[WhenNotMatched(insert="*")], |
| ) |
| |
| self.assertTrue(msgs) |
| self.assertEqual( |
| { |
| "id": [1, 2], |
| "name": ["Alice", "Bob"], |
| "age": [25, 30], |
| "city": ["NYC", "LA"], |
| }, |
| self._read_sorted(target), |
| ) |
| |
| def test_table_merge_into_with_renamed_on_key_fills_insert_key(self): |
| target = self._create_table() |
| self._write_arrow(target, pa.Table.from_pydict({ |
| "id": pa.array([1], type=pa.int32()), |
| "name": ["Alice"], |
| "age": pa.array([25], type=pa.int32()), |
| "city": ["NYC"], |
| }, schema=self.pa_schema)) |
| source = pa.Table.from_pydict({ |
| "source_id": pa.array([1, 2], type=pa.int32()), |
| "new_name": ["Alicia", "Bob"], |
| "new_age": pa.array([26, 30], type=pa.int32()), |
| "new_city": ["SF", "LA"], |
| }) |
| |
| self._merge_and_commit( |
| target, |
| source, |
| on={"id": "source_id"}, |
| when_matched=[ |
| WhenMatched.update({ |
| "name": source_col("new_name"), |
| "age": source_col("new_age"), |
| "city": source_col("new_city"), |
| }), |
| ], |
| when_not_matched=[ |
| WhenNotMatched(insert={ |
| "name": source_col("new_name"), |
| "age": source_col("new_age"), |
| "city": source_col("new_city"), |
| }), |
| ], |
| ) |
| |
| self.assertEqual( |
| { |
| "id": [1, 2], |
| "name": ["Alicia", "Bob"], |
| "age": [26, 30], |
| "city": ["SF", "LA"], |
| }, |
| self._read_sorted(target), |
| ) |
| |
| def test_table_merge_into_ignores_target_self_assignment(self): |
| for assignment in [target_col("name"), "t.name"]: |
| with self.subTest(assignment=assignment): |
| target = self._create_table() |
| self._write_arrow(target, pa.Table.from_pydict({ |
| "id": pa.array([1], type=pa.int32()), |
| "name": ["Alice"], |
| "age": pa.array([25], type=pa.int32()), |
| "city": ["NYC"], |
| }, schema=self.pa_schema)) |
| row_ids_before = self._row_ids_by_id(target) |
| source = pa.Table.from_pydict({ |
| "id": pa.array([1], type=pa.int32()), |
| "name": ["Alicia"], |
| "age": pa.array([26], type=pa.int32()), |
| "city": ["SF"], |
| }, schema=self.pa_schema) |
| |
| msgs = self._merge_and_commit( |
| target, |
| source, |
| on=["id"], |
| when_matched=[WhenMatched.update({"name": assignment})], |
| ) |
| |
| self.assertEqual([], msgs) |
| self.assertEqual( |
| { |
| "id": [1], |
| "name": ["Alice"], |
| "age": [25], |
| "city": ["NYC"], |
| }, |
| self._read_sorted(target), |
| ) |
| self.assertEqual(row_ids_before, self._row_ids_by_id(target)) |
| |
| def test_table_merge_into_source_assignment_same_name_is_modified(self): |
| target = self._create_table() |
| self._write_arrow(target, pa.Table.from_pydict({ |
| "id": pa.array([1], type=pa.int32()), |
| "name": ["Alice"], |
| "age": pa.array([25], type=pa.int32()), |
| "city": ["NYC"], |
| }, schema=self.pa_schema)) |
| source = pa.Table.from_pydict({ |
| "id": pa.array([1], type=pa.int32()), |
| "name": ["Alicia"], |
| "age": pa.array([26], type=pa.int32()), |
| "city": ["SF"], |
| }, schema=self.pa_schema) |
| |
| msgs = self._merge_and_commit( |
| target, |
| source, |
| on=["id"], |
| when_matched=[WhenMatched.update({"name": source_col("name")})], |
| ) |
| |
| self.assertTrue(msgs) |
| self.assertEqual(["Alicia"], self._read_sorted(target)["name"]) |
| self.assertEqual([25], self._read_sorted(target)["age"]) |
| |
| def test_table_merge_into_rejects_nested_assignment_key(self): |
| target = self._create_table() |
| self._write_arrow(target, pa.Table.from_pydict({ |
| "id": pa.array([1], type=pa.int32()), |
| "name": ["Alice"], |
| "age": pa.array([25], type=pa.int32()), |
| "city": ["NYC"], |
| }, schema=self.pa_schema)) |
| source = pa.Table.from_pydict({ |
| "id": pa.array([1], type=pa.int32()), |
| "name": ["Alicia"], |
| "age": pa.array([26], type=pa.int32()), |
| "city": ["SF"], |
| }, schema=self.pa_schema) |
| |
| with self.assertRaisesRegex(ValueError, "unknown target column"): |
| target.new_batch_write_builder().new_update().merge_into( |
| source, |
| on=["id"], |
| when_matched=[WhenMatched.update({"name.first": lit("A")})], |
| ) |
| |
| def test_table_merge_into_rejects_partition_column_update(self): |
| target = self._create_table(partition_keys=["city"]) |
| self._write_arrow(target, pa.Table.from_pydict({ |
| "id": pa.array([1], type=pa.int32()), |
| "name": ["Alice"], |
| "age": pa.array([25], type=pa.int32()), |
| "city": ["NYC"], |
| }, schema=self.pa_schema)) |
| source = pa.Table.from_pydict({ |
| "id": pa.array([1], type=pa.int32()), |
| "name": ["Alice"], |
| "age": pa.array([26], type=pa.int32()), |
| "city": ["SF"], |
| }, schema=self.pa_schema) |
| |
| with self.assertRaisesRegex(ValueError, "partition columns"): |
| target.new_batch_write_builder().new_update().merge_into( |
| source, |
| on=["id"], |
| when_matched=[WhenMatched.update({"city": source_col("city")})], |
| ) |
| |
| def test_table_merge_into_preserves_row_ids_for_matched_rows(self): |
| target = self._create_table() |
| self._write_arrow(target, pa.Table.from_pydict({ |
| "id": pa.array([1, 2], type=pa.int32()), |
| "name": ["Alice", "Bob"], |
| "age": pa.array([25, 30], type=pa.int32()), |
| "city": ["NYC", "LA"], |
| }, schema=self.pa_schema)) |
| row_ids_before = self._row_ids_by_id(target) |
| |
| self._merge_and_commit( |
| target, |
| pa.Table.from_pydict({ |
| "id": pa.array([2, 3], type=pa.int32()), |
| "name": ["Bobby", "Cindy"], |
| "age": pa.array([31, 22], type=pa.int32()), |
| "city": ["LA2", "SF"], |
| }, schema=self.pa_schema), |
| on=["id"], |
| when_matched=[WhenMatched.update("*")], |
| when_not_matched=[WhenNotMatched(insert="*")], |
| ) |
| |
| row_ids_after = self._row_ids_by_id(target) |
| self.assertEqual(row_ids_before[1], row_ids_after[1]) |
| self.assertEqual(row_ids_before[2], row_ids_after[2]) |
| self.assertGreater(row_ids_after[3], max(row_ids_before.values())) |
| |
| def test_table_merge_into_delete_matched_rows(self): |
| options = dict(self.table_options) |
| options["deletion-vectors.enabled"] = "true" |
| target = self._create_table(options=options) |
| self._write_arrow(target, pa.Table.from_pydict({ |
| "id": pa.array([1, 2, 3], type=pa.int32()), |
| "name": ["Alice", "Bob", "Charlie"], |
| "age": pa.array([25, 30, 35], type=pa.int32()), |
| "city": ["NYC", "LA", "Chicago"], |
| }, schema=self.pa_schema)) |
| |
| msgs = self._merge_and_commit( |
| target, |
| pa.Table.from_pydict({ |
| "id": pa.array([2, 3], type=pa.int32()), |
| "name": ["ignored", "ignored"], |
| "age": pa.array([99, 99], type=pa.int32()), |
| "city": ["ignored", "ignored"], |
| }, schema=self.pa_schema), |
| on=["id"], |
| when_matched=[WhenMatched.delete()], |
| ) |
| |
| self.assertEqual(1, sum(len(m.index_adds) for m in msgs)) |
| self.assertEqual( |
| { |
| "id": [1], |
| "name": ["Alice"], |
| "age": [25], |
| "city": ["NYC"], |
| }, |
| self._read_sorted(target), |
| ) |
| |
| def test_table_merge_into_second_round_updates_first_round_insert(self): |
| target = self._create_table() |
| self._write_arrow(target, pa.Table.from_pydict({ |
| "id": pa.array([1], type=pa.int32()), |
| "name": ["Alice"], |
| "age": pa.array([25], type=pa.int32()), |
| "city": ["NYC"], |
| }, schema=self.pa_schema)) |
| |
| self._merge_and_commit( |
| target, |
| pa.Table.from_pydict({ |
| "id": pa.array([2], type=pa.int32()), |
| "name": ["Bob"], |
| "age": pa.array([30], type=pa.int32()), |
| "city": ["LA"], |
| }, schema=self.pa_schema), |
| on=["id"], |
| when_not_matched=[WhenNotMatched(insert="*")], |
| ) |
| row_ids_after_insert = self._row_ids_by_id(target) |
| |
| self._merge_and_commit( |
| target, |
| pa.Table.from_pydict({ |
| "id": pa.array([2], type=pa.int32()), |
| "name": ["Bobby"], |
| "age": pa.array([31], type=pa.int32()), |
| "city": ["LA2"], |
| }, schema=self.pa_schema), |
| on=["id"], |
| when_matched=[WhenMatched.update("*")], |
| ) |
| |
| self.assertEqual( |
| { |
| "id": [1, 2], |
| "name": ["Alice", "Bobby"], |
| "age": [25, 31], |
| "city": ["NYC", "LA2"], |
| }, |
| self._read_sorted(target), |
| ) |
| self.assertEqual(row_ids_after_insert, self._row_ids_by_id(target)) |
| |
| def test_table_merge_into_accepts_paimon_source_table(self): |
| target = self._create_table() |
| source = self._create_table() |
| self._write_arrow(target, pa.Table.from_pydict({ |
| "id": pa.array([1], type=pa.int32()), |
| "name": ["Alice"], |
| "age": pa.array([25], type=pa.int32()), |
| "city": ["NYC"], |
| }, schema=self.pa_schema)) |
| self._write_arrow(source, pa.Table.from_pydict({ |
| "id": pa.array([1, 2], type=pa.int32()), |
| "name": ["Alicia", "Bob"], |
| "age": pa.array([26, 30], type=pa.int32()), |
| "city": ["SF", "LA"], |
| }, schema=self.pa_schema)) |
| |
| msgs = self._merge_and_commit( |
| target, |
| source, |
| on=["id"], |
| when_matched=[WhenMatched.update("*")], |
| when_not_matched=[WhenNotMatched(insert="*")], |
| ) |
| |
| self.assertTrue(msgs) |
| self.assertEqual( |
| { |
| "id": [1, 2], |
| "name": ["Alicia", "Bob"], |
| "age": [26, 30], |
| "city": ["SF", "LA"], |
| }, |
| self._read_sorted(target), |
| ) |
| |
| def test_table_merge_into_updates_and_inserts_blob_column(self): |
| blob_schema = pa.schema([ |
| ("id", pa.int32()), |
| ("name", pa.string()), |
| ("payload", pa.large_binary()), |
| ]) |
| target = self._create_table(pa_schema=blob_schema) |
| self._write_arrow(target, pa.Table.from_pydict({ |
| "id": pa.array([1, 2], type=pa.int32()), |
| "name": ["Alice", "Bob"], |
| "payload": pa.array( |
| [b"blob-1", b"blob-2"], type=pa.large_binary() |
| ), |
| }, schema=blob_schema)) |
| |
| msgs = self._merge_and_commit( |
| target, |
| pa.Table.from_pydict({ |
| "id": pa.array([2, 3], type=pa.int32()), |
| "name": ["Bobby", "Cindy"], |
| "payload": pa.array( |
| [b"blob-2-updated", b"blob-3"], type=pa.large_binary() |
| ), |
| }, schema=blob_schema), |
| on=["id"], |
| when_matched=[WhenMatched.update("*")], |
| when_not_matched=[WhenNotMatched(insert="*")], |
| ) |
| |
| self.assertTrue(msgs) |
| self.assertEqual( |
| { |
| "id": [1, 2, 3], |
| "name": ["Alice", "Bobby", "Cindy"], |
| "payload": [b"blob-1", b"blob-2-updated", b"blob-3"], |
| }, |
| self._read_projected_sorted(target, ["id", "name", "payload"]), |
| ) |
| |
| def test_table_merge_into_inserts_null_for_unspecified_blob_column(self): |
| blob_schema = pa.schema([ |
| ("id", pa.int32()), |
| ("name", pa.string()), |
| ("payload", pa.large_binary()), |
| ]) |
| source_schema = pa.schema([ |
| ("id", pa.int32()), |
| ("name", pa.string()), |
| ]) |
| target = self._create_table(pa_schema=blob_schema) |
| |
| msgs = self._merge_and_commit( |
| target, |
| pa.Table.from_pydict({ |
| "id": pa.array([1], type=pa.int32()), |
| "name": ["Alice"], |
| }, schema=source_schema), |
| on=["id"], |
| when_not_matched=[WhenNotMatched(insert={ |
| "id": source_col("id"), |
| "name": source_col("name"), |
| })], |
| ) |
| |
| self.assertTrue(msgs) |
| self.assertEqual( |
| { |
| "id": [1], |
| "name": ["Alice"], |
| "payload": [None], |
| }, |
| self._read_projected_sorted(target, ["id", "name", "payload"]), |
| ) |
| |
| @unittest.skipIf(_SKIP_CONDITION, _SKIP_REASON) |
| def test_table_merge_into_condition_clauses(self): |
| target = self._create_table() |
| self._write_arrow(target, pa.Table.from_pydict({ |
| "id": pa.array([1, 2], type=pa.int32()), |
| "name": ["old_1", "old_2"], |
| "age": pa.array([10, 40], type=pa.int32()), |
| "city": ["NYC", "LA"], |
| }, schema=self.pa_schema)) |
| |
| source = pa.Table.from_pydict({ |
| "id": pa.array([1, 2, 3], type=pa.int32()), |
| "name": ["new_1", "new_2", "new_3"], |
| "age": pa.array([15, 35, 50], type=pa.int32()), |
| "city": ["SF", "SEA", "DAL"], |
| }, schema=self.pa_schema) |
| |
| msgs = self._merge_and_commit( |
| target, |
| source, |
| on=["id"], |
| when_matched=[ |
| WhenMatched.update({"age": lit(99)}, condition="s.age > t.age"), |
| WhenMatched.update({"name": lit("kept")}), |
| ], |
| when_not_matched=[ |
| WhenNotMatched(insert="*", condition="s.age > 45"), |
| ], |
| ) |
| |
| self.assertTrue(msgs) |
| self.assertEqual( |
| { |
| "id": [1, 2, 3], |
| "name": ["old_1", "kept", "new_3"], |
| "age": [99, 40, 50], |
| "city": ["NYC", "LA", "DAL"], |
| }, |
| self._read_sorted(target), |
| ) |
| |
| def test_table_merge_into_duplicate_source_match_rejected(self): |
| target = self._create_table() |
| self._write_arrow(target, pa.Table.from_pydict({ |
| "id": pa.array([1], type=pa.int32()), |
| "name": ["Alice"], |
| "age": pa.array([25], type=pa.int32()), |
| "city": ["NYC"], |
| }, schema=self.pa_schema)) |
| |
| source = pa.Table.from_pydict({ |
| "id": pa.array([1, 1], type=pa.int32()), |
| "name": ["A1", "A2"], |
| "age": pa.array([26, 27], type=pa.int32()), |
| "city": ["SF", "LA"], |
| }, schema=self.pa_schema) |
| |
| with self.assertRaisesRegex(ValueError, "multiple source rows"): |
| target.new_batch_write_builder().new_update().merge_into( |
| source, |
| on=["id"], |
| when_matched=[WhenMatched.update({"age": "s.age"})], |
| ) |
| |
| def test_table_merge_into_self_merge_by_row_id(self): |
| target = self._create_table() |
| self._write_arrow(target, pa.Table.from_pydict({ |
| "id": pa.array([1, 2], type=pa.int32()), |
| "name": ["Alice", "Bob"], |
| "age": pa.array([25, 30], type=pa.int32()), |
| "city": ["NYC", "LA"], |
| }, schema=self.pa_schema)) |
| |
| msgs = self._merge_and_commit( |
| target, |
| target, |
| on=["_ROW_ID"], |
| when_matched=[WhenMatched.update({"name": lit("updated")})], |
| ) |
| |
| self.assertTrue(msgs) |
| self.assertEqual(["updated", "updated"], self._read_sorted(target)["name"]) |
| |
| |
| class StreamTableMergeIntoTest(StreamModeMixin, DataEvolutionTestBase, unittest.TestCase): |
| |
| def _read_sorted(self, table): |
| return self._read_all(table).sort_by("id").to_pydict() |
| |
| def test_stream_table_update_merge_into(self): |
| target = self._create_table() |
| self._write_arrow(target, pa.Table.from_pydict({ |
| "id": pa.array([1], type=pa.int32()), |
| "name": ["Alice"], |
| "age": pa.array([25], type=pa.int32()), |
| "city": ["NYC"], |
| }, schema=self.pa_schema)) |
| |
| source = pa.Table.from_pydict({ |
| "id": pa.array([1, 2], type=pa.int32()), |
| "name": ["Alicia", "Bob"], |
| "age": pa.array([26, 30], type=pa.int32()), |
| "city": ["SF", "LA"], |
| }, schema=self.pa_schema) |
| |
| base_snapshot_id = self._latest_snapshot_id(target) |
| wb = target.new_stream_write_builder() |
| cid = self._next_commit_id() |
| msgs = wb.new_update().merge_into( |
| source, |
| on=["id"], |
| when_matched=[WhenMatched.update("*")], |
| when_not_matched=[WhenNotMatched(insert="*")], |
| commit_identifier=cid, |
| ) |
| commit = wb.new_commit() |
| commit.commit(msgs, cid) |
| commit.close() |
| |
| self._assert_stream_builder_snapshots( |
| target, wb, base_snapshot_id, [cid] |
| ) |
| self.assertEqual( |
| { |
| "id": [1, 2], |
| "name": ["Alicia", "Bob"], |
| "age": [26, 30], |
| "city": ["SF", "LA"], |
| }, |
| self._read_sorted(target), |
| ) |
| |
| def test_stream_table_update_merge_into_multiple_rounds(self): |
| target = self._create_table() |
| self._write_arrow(target, pa.Table.from_pydict({ |
| "id": pa.array([1], type=pa.int32()), |
| "name": ["Alice"], |
| "age": pa.array([25], type=pa.int32()), |
| "city": ["NYC"], |
| }, schema=self.pa_schema)) |
| |
| base_snapshot_id = self._latest_snapshot_id(target) |
| wb = target.new_stream_write_builder() |
| commit = wb.new_commit() |
| |
| cid1 = self._next_commit_id() |
| msgs1 = wb.new_update().merge_into( |
| pa.Table.from_pydict({ |
| "id": pa.array([2], type=pa.int32()), |
| "name": ["Bob"], |
| "age": pa.array([30], type=pa.int32()), |
| "city": ["LA"], |
| }, schema=self.pa_schema), |
| on=["id"], |
| when_not_matched=[WhenNotMatched(insert="*")], |
| commit_identifier=cid1, |
| ) |
| commit.commit(msgs1, cid1) |
| |
| cid2 = self._next_commit_id() |
| msgs2 = wb.new_update().merge_into( |
| pa.Table.from_pydict({ |
| "id": pa.array([2], type=pa.int32()), |
| "name": ["Bobby"], |
| "age": pa.array([31], type=pa.int32()), |
| "city": ["LA2"], |
| }, schema=self.pa_schema), |
| on=["id"], |
| when_matched=[WhenMatched.update("*")], |
| commit_identifier=cid2, |
| ) |
| commit.commit(msgs2, cid2) |
| commit.close() |
| |
| self._assert_stream_builder_snapshots( |
| target, wb, base_snapshot_id, [cid1, cid2] |
| ) |
| self.assertEqual( |
| { |
| "id": [1, 2], |
| "name": ["Alice", "Bobby"], |
| "age": [25, 31], |
| "city": ["NYC", "LA2"], |
| }, |
| self._read_sorted(target), |
| ) |
| |
| |
| if __name__ == "__main__": |
| unittest.main() |