| # 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 patch |
| |
| import pyarrow as pa |
| |
| from pypaimon import Schema |
| from pypaimon.catalog.catalog_exception import (DatabaseAlreadyExistException, |
| DatabaseNotExistException, |
| TableAlreadyExistException, |
| TableNotExistException) |
| from pypaimon.common.options import Options |
| from pypaimon.common.options.config import OssOptions |
| from pypaimon.filesystem.pyarrow_file_io import PyArrowFileIO |
| from pypaimon.tests.py36.pyarrow_compat import table_sort_by |
| from pypaimon.tests.rest.rest_base_test import RESTBaseTest |
| |
| |
| class AOSimpleTest(RESTBaseTest): |
| def setUp(self): |
| super().setUp() |
| self.pa_schema = pa.schema([ |
| ('user_id', pa.int64()), |
| ('item_id', pa.int64()), |
| ('behavior', pa.string()), |
| ('dt', pa.string()), |
| ]) |
| self.data = { |
| 'user_id': [2, 4, 6, 8, 10], |
| 'item_id': [1001, 1002, 1003, 1004, 1005], |
| 'behavior': ['a', 'b', 'c', 'd', 'e'], |
| 'dt': ['2000-10-10', '2025-08-10', '2025-08-11', '2025-08-12', '2025-08-13'] |
| } |
| self.expected = pa.Table.from_pydict(self.data, schema=self.pa_schema) |
| |
| def test_with_shard_ao_unaware_bucket(self): |
| schema = Schema.from_pyarrow_schema(self.pa_schema, partition_keys=['dt']) |
| self.rest_catalog.create_table('default.test_with_shard_ao_unaware_bucket', schema, False) |
| table = self.rest_catalog.get_table('default.test_with_shard_ao_unaware_bucket') |
| write_builder = table.new_batch_write_builder() |
| # first write |
| table_write = write_builder.new_write() |
| table_commit = write_builder.new_commit() |
| data1 = { |
| 'user_id': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], |
| 'item_id': [1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008, 1009, 1010, 1011, 1012, 1013, 1014], |
| 'behavior': ['a', 'b', 'c', None, 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm'], |
| 'dt': ['p1', 'p1', 'p2', 'p1', 'p2', 'p1', 'p2', 'p1', 'p2', 'p1', 'p2', 'p1', 'p2', 'p1'], |
| } |
| pa_table = pa.Table.from_pydict(data1, schema=self.pa_schema) |
| table_write.write_arrow(pa_table) |
| table_commit.commit(table_write.prepare_commit()) |
| table_write.close() |
| table_commit.close() |
| # second write |
| table_write = write_builder.new_write() |
| table_commit = write_builder.new_commit() |
| data2 = { |
| 'user_id': [5, 6, 7, 8, 18], |
| 'item_id': [1005, 1006, 1007, 1008, 1018], |
| 'behavior': ['e', 'f', 'g', 'h', 'z'], |
| 'dt': ['p2', 'p1', 'p2', 'p2', 'p1'], |
| } |
| pa_table = pa.Table.from_pydict(data2, schema=self.pa_schema) |
| table_write.write_arrow(pa_table) |
| table_commit.commit(table_write.prepare_commit()) |
| table_write.close() |
| table_commit.close() |
| |
| read_builder = table.new_read_builder() |
| table_read = read_builder.new_read() |
| splits = read_builder.new_scan().with_shard(2, 3).plan().splits() |
| actual = table_sort_by(table_read.to_arrow(splits), 'user_id') |
| expected = pa.Table.from_pydict({ |
| 'user_id': [5, 7, 8, 9, 11, 13], |
| 'item_id': [1005, 1007, 1008, 1009, 1011, 1013], |
| 'behavior': ['e', 'g', 'h', 'h', 'j', 'l'], |
| 'dt': ['p2', 'p2', 'p2', 'p2', 'p2', 'p2'], |
| }, schema=self.pa_schema) |
| self.assertEqual(actual, expected) |
| |
| # Get the three actual tables |
| splits1 = read_builder.new_scan().with_shard(0, 3).plan().splits() |
| actual1 = table_sort_by(table_read.to_arrow(splits1), 'user_id') |
| splits2 = read_builder.new_scan().with_shard(1, 3).plan().splits() |
| actual2 = table_sort_by(table_read.to_arrow(splits2), 'user_id') |
| splits3 = read_builder.new_scan().with_shard(2, 3).plan().splits() |
| actual3 = table_sort_by(table_read.to_arrow(splits3), 'user_id') |
| |
| # Concatenate the three tables |
| actual = table_sort_by(pa.concat_tables([actual1, actual2, actual3]), 'user_id') |
| expected = table_sort_by(self._read_test_table(read_builder), 'user_id') |
| self.assertEqual(actual, expected) |
| |
| def test_with_shard_ao_fixed_bucket(self): |
| schema = Schema.from_pyarrow_schema(self.pa_schema, partition_keys=['dt'], |
| options={'bucket': '5', 'bucket-key': 'item_id'}) |
| self.rest_catalog.create_table('default.test_with_slice_ao_fixed_bucket', schema, False) |
| table = self.rest_catalog.get_table('default.test_with_slice_ao_fixed_bucket') |
| write_builder = table.new_batch_write_builder() |
| # first write |
| table_write = write_builder.new_write() |
| table_commit = write_builder.new_commit() |
| data1 = { |
| 'user_id': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], |
| 'item_id': [1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008, 1009, 1010, 1011, 1012, 1013, 1014], |
| 'behavior': ['a', 'b', 'c', None, 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm'], |
| 'dt': ['p1', 'p1', 'p2', 'p1', 'p2', 'p1', 'p2', 'p1', 'p2', 'p1', 'p2', 'p1', 'p2', 'p1'], |
| } |
| pa_table = pa.Table.from_pydict(data1, schema=self.pa_schema) |
| table_write.write_arrow(pa_table) |
| table_commit.commit(table_write.prepare_commit()) |
| table_write.close() |
| table_commit.close() |
| # second write |
| table_write = write_builder.new_write() |
| table_commit = write_builder.new_commit() |
| data2 = { |
| 'user_id': [5, 6, 7, 8], |
| 'item_id': [1005, 1006, 1007, 1008], |
| 'behavior': ['e', 'f', 'g', 'h'], |
| 'dt': ['p2', 'p1', 'p2', 'p2'], |
| } |
| pa_table = pa.Table.from_pydict(data2, schema=self.pa_schema) |
| table_write.write_arrow(pa_table) |
| table_commit.commit(table_write.prepare_commit()) |
| table_write.close() |
| table_commit.close() |
| |
| read_builder = table.new_read_builder() |
| table_read = read_builder.new_read() |
| splits = read_builder.new_scan().with_shard(0, 3).plan().splits() |
| actual = table_sort_by(table_read.to_arrow(splits), 'user_id') |
| expected = pa.Table.from_pydict({ |
| 'user_id': [1, 2, 3, 4, 5, 13], |
| 'item_id': [1001, 1002, 1003, 1004, 1005, 1013], |
| 'behavior': ['a', 'b', 'c', None, 'd', 'l'], |
| 'dt': ['p1', 'p1', 'p2', 'p1', 'p2', 'p2'], |
| }, schema=self.pa_schema) |
| self.assertEqual(actual, expected) |
| |
| # Get the three actual tables |
| splits1 = read_builder.new_scan().with_shard(0, 3).plan().splits() |
| actual1 = table_sort_by(table_read.to_arrow(splits1), 'user_id') |
| splits2 = read_builder.new_scan().with_shard(1, 3).plan().splits() |
| actual2 = table_sort_by(table_read.to_arrow(splits2), 'user_id') |
| splits3 = read_builder.new_scan().with_shard(2, 3).plan().splits() |
| actual3 = table_sort_by(table_read.to_arrow(splits3), 'user_id') |
| |
| # Concatenate the three tables |
| actual = table_sort_by(pa.concat_tables([actual1, actual2, actual3]), 'user_id') |
| expected = table_sort_by(self._read_test_table(read_builder), 'user_id') |
| self.assertEqual(actual, expected) |
| |
| def test_shard_single_partition(self): |
| """Test sharding with single partition - tests _filter_by_shard with simple data""" |
| schema = Schema.from_pyarrow_schema(self.pa_schema, partition_keys=['dt']) |
| self.rest_catalog.create_table('default.test_shard_single_partition', schema, False) |
| table = self.rest_catalog.get_table('default.test_shard_single_partition') |
| write_builder = table.new_batch_write_builder() |
| |
| # Write data with single partition |
| table_write = write_builder.new_write() |
| table_commit = write_builder.new_commit() |
| data = { |
| 'user_id': [1, 2, 3, 4, 5, 6], |
| 'item_id': [1001, 1002, 1003, 1004, 1005, 1006], |
| 'behavior': ['a', 'b', 'c', 'd', 'e', 'f'], |
| 'dt': ['p1', 'p1', 'p1', 'p1', 'p1', 'p1'], |
| } |
| pa_table = pa.Table.from_pydict(data, schema=self.pa_schema) |
| table_write.write_arrow(pa_table) |
| table_commit.commit(table_write.prepare_commit()) |
| table_write.close() |
| table_commit.close() |
| |
| read_builder = table.new_read_builder() |
| table_read = read_builder.new_read() |
| |
| # Test first shard (0, 2) - should get first 3 rows |
| splits = read_builder.new_scan().with_shard(0, 2).plan().splits() |
| actual = table_sort_by(table_read.to_arrow(splits), 'user_id') |
| expected = pa.Table.from_pydict({ |
| 'user_id': [1, 2, 3], |
| 'item_id': [1001, 1002, 1003], |
| 'behavior': ['a', 'b', 'c'], |
| 'dt': ['p1', 'p1', 'p1'], |
| }, schema=self.pa_schema) |
| self.assertEqual(actual, expected) |
| |
| # Test second shard (1, 2) - should get last 3 rows |
| splits = read_builder.new_scan().with_shard(1, 2).plan().splits() |
| actual = table_sort_by(table_read.to_arrow(splits), 'user_id') |
| expected = pa.Table.from_pydict({ |
| 'user_id': [4, 5, 6], |
| 'item_id': [1004, 1005, 1006], |
| 'behavior': ['d', 'e', 'f'], |
| 'dt': ['p1', 'p1', 'p1'], |
| }, schema=self.pa_schema) |
| self.assertEqual(actual, expected) |
| |
| def test_shard_uneven_distribution(self): |
| """Test sharding with uneven row distribution across shards""" |
| schema = Schema.from_pyarrow_schema(self.pa_schema, partition_keys=['dt']) |
| self.rest_catalog.create_table('default.test_shard_uneven', schema, False) |
| table = self.rest_catalog.get_table('default.test_shard_uneven') |
| write_builder = table.new_batch_write_builder() |
| |
| # Write data with 7 rows (not evenly divisible by 3) |
| table_write = write_builder.new_write() |
| table_commit = write_builder.new_commit() |
| data = { |
| 'user_id': [1, 2, 3, 4, 5, 6, 7], |
| 'item_id': [1001, 1002, 1003, 1004, 1005, 1006, 1007], |
| 'behavior': ['a', 'b', 'c', 'd', 'e', 'f', 'g'], |
| 'dt': ['p1', 'p1', 'p1', 'p1', 'p1', 'p1', 'p1'], |
| } |
| pa_table = pa.Table.from_pydict(data, schema=self.pa_schema) |
| table_write.write_arrow(pa_table) |
| table_commit.commit(table_write.prepare_commit()) |
| table_write.close() |
| table_commit.close() |
| |
| read_builder = table.new_read_builder() |
| table_read = read_builder.new_read() |
| |
| # Test sharding into 3 parts: 2, 2, 3 rows |
| splits = read_builder.new_scan().with_shard(0, 3).plan().splits() |
| actual1 = table_sort_by(table_read.to_arrow(splits), 'user_id') |
| expected1 = pa.Table.from_pydict({ |
| 'user_id': [1, 2, 3], |
| 'item_id': [1001, 1002, 1003], |
| 'behavior': ['a', 'b', 'c'], |
| 'dt': ['p1', 'p1', 'p1'], |
| }, schema=self.pa_schema) |
| self.assertEqual(actual1, expected1) |
| |
| splits = read_builder.new_scan().with_shard(1, 3).plan().splits() |
| actual2 = table_sort_by(table_read.to_arrow(splits), 'user_id') |
| expected2 = pa.Table.from_pydict({ |
| 'user_id': [4, 5], |
| 'item_id': [1004, 1005], |
| 'behavior': ['d', 'e'], |
| 'dt': ['p1', 'p1'], |
| }, schema=self.pa_schema) |
| self.assertEqual(actual2, expected2) |
| |
| splits = read_builder.new_scan().with_shard(2, 3).plan().splits() |
| actual3 = table_sort_by(table_read.to_arrow(splits), 'user_id') |
| expected3 = pa.Table.from_pydict({ |
| 'user_id': [6, 7], |
| 'item_id': [1006, 1007], |
| 'behavior': ['f', 'g'], |
| 'dt': ['p1', 'p1'], |
| }, schema=self.pa_schema) |
| self.assertEqual(actual3, expected3) |
| |
| def test_shard_many_small_shards(self): |
| """Test sharding with many small shards""" |
| schema = Schema.from_pyarrow_schema(self.pa_schema, partition_keys=['dt']) |
| self.rest_catalog.create_table('default.test_shard_many_small', schema, False) |
| table = self.rest_catalog.get_table('default.test_shard_many_small') |
| write_builder = table.new_batch_write_builder() |
| |
| table_write = write_builder.new_write() |
| table_commit = write_builder.new_commit() |
| data = { |
| 'user_id': [1, 2, 3, 4, 5, 6], |
| 'item_id': [1001, 1002, 1003, 1004, 1005, 1006], |
| 'behavior': ['a', 'b', 'c', 'd', 'e', 'f'], |
| 'dt': ['p1', 'p1', 'p1', 'p1', 'p1', 'p1'], |
| } |
| pa_table = pa.Table.from_pydict(data, schema=self.pa_schema) |
| table_write.write_arrow(pa_table) |
| table_commit.commit(table_write.prepare_commit()) |
| table_write.close() |
| table_commit.close() |
| |
| read_builder = table.new_read_builder() |
| table_read = read_builder.new_read() |
| |
| # Test with 6 shards (one row per shard) |
| for i in range(6): |
| splits = read_builder.new_scan().with_shard(i, 6).plan().splits() |
| actual = table_read.to_arrow(splits) |
| self.assertEqual(len(actual), 1) |
| self.assertEqual(actual['user_id'][0].as_py(), i + 1) |
| |
| def test_shard_boundary_conditions(self): |
| """Test sharding boundary conditions with edge cases""" |
| schema = Schema.from_pyarrow_schema(self.pa_schema, partition_keys=['dt']) |
| self.rest_catalog.create_table('default.test_shard_boundary', schema, False) |
| table = self.rest_catalog.get_table('default.test_shard_boundary') |
| write_builder = table.new_batch_write_builder() |
| |
| table_write = write_builder.new_write() |
| table_commit = write_builder.new_commit() |
| data = { |
| 'user_id': [1, 2, 3, 4, 5], |
| 'item_id': [1001, 1002, 1003, 1004, 1005], |
| 'behavior': ['a', 'b', 'c', 'd', 'e'], |
| 'dt': ['p1', 'p1', 'p1', 'p1', 'p1'], |
| } |
| pa_table = pa.Table.from_pydict(data, schema=self.pa_schema) |
| table_write.write_arrow(pa_table) |
| table_commit.commit(table_write.prepare_commit()) |
| table_write.close() |
| table_commit.close() |
| |
| read_builder = table.new_read_builder() |
| table_read = read_builder.new_read() |
| |
| # Test first shard (0, 4) - should get 1 row (5//4 = 1) |
| splits = read_builder.new_scan().with_shard(0, 4).plan().splits() |
| actual = table_read.to_arrow(splits) |
| self.assertEqual(len(actual), 2) |
| |
| # Test middle shard (1, 4) - should get 1 row |
| splits = read_builder.new_scan().with_shard(1, 4).plan().splits() |
| actual = table_read.to_arrow(splits) |
| self.assertEqual(len(actual), 1) |
| |
| # Test last shard (3, 4) - should get 2 rows (remainder goes to last shard) |
| splits = read_builder.new_scan().with_shard(3, 4).plan().splits() |
| actual = table_read.to_arrow(splits) |
| self.assertEqual(len(actual), 1) |
| |
| def test_create_drop_database_table(self): |
| # test create database |
| self.rest_catalog.create_database("db1", False) |
| |
| with self.assertRaises(DatabaseAlreadyExistException) as context: |
| self.rest_catalog.create_database("db1", False) |
| |
| self.assertEqual("db1", context.exception.database) |
| |
| try: |
| self.rest_catalog.create_database("db1", True) |
| except DatabaseAlreadyExistException: |
| self.fail("create_database with ignore_if_exists=True should not raise DatabaseAlreadyExistException") |
| |
| # test create table |
| self.rest_catalog.create_table("db1.tbl1", |
| Schema.from_pyarrow_schema(self.pa_schema, partition_keys=['dt']), |
| False) |
| with self.assertRaises(TableAlreadyExistException) as context: |
| self.rest_catalog.create_table("db1.tbl1", |
| Schema.from_pyarrow_schema(self.pa_schema, partition_keys=['dt']), |
| False) |
| self.assertEqual("db1.tbl1", context.exception.identifier.get_full_name()) |
| |
| try: |
| self.rest_catalog.create_table("db1.tbl1", |
| Schema.from_pyarrow_schema(self.pa_schema, partition_keys=['dt']), |
| True) |
| except TableAlreadyExistException: |
| self.fail("create_table with ignore_if_exists=True should not raise TableAlreadyExistException") |
| |
| # test drop table |
| self.rest_catalog.drop_table("db1.tbl1", False) |
| with self.assertRaises(TableNotExistException) as context: |
| self.rest_catalog.drop_table("db1.tbl1", False) |
| self.assertEqual("db1.tbl1", context.exception.identifier.get_full_name()) |
| |
| try: |
| self.rest_catalog.drop_table("db1.tbl1", True) |
| except TableNotExistException: |
| self.fail("drop_table with ignore_if_not_exists=True should not raise TableNotExistException") |
| |
| # test drop database |
| self.rest_catalog.drop_database("db1", False) |
| with self.assertRaises(DatabaseNotExistException) as context: |
| self.rest_catalog.drop_database("db1", False) |
| self.assertEqual("db1", context.exception.database) |
| |
| try: |
| self.rest_catalog.drop_database("db1", True) |
| except DatabaseNotExistException: |
| self.fail("drop_database with ignore_if_not_exists=True should not raise DatabaseNotExistException") |
| |
| def test_initialize_oss_fs_pyarrow_lt_7(self): |
| props = { |
| OssOptions.OSS_ACCESS_KEY_ID.key(): "AKID", |
| OssOptions.OSS_ACCESS_KEY_SECRET.key(): "SECRET", |
| OssOptions.OSS_SECURITY_TOKEN.key(): "TOKEN", |
| OssOptions.OSS_REGION.key(): "cn-hangzhou", |
| OssOptions.OSS_ENDPOINT.key(): "oss-cn-hangzhou.aliyuncs.com", |
| } |
| |
| with patch("pypaimon.common.file_io.pyarrow.__version__", "6.0.0"), \ |
| patch("pyarrow.fs.S3FileSystem") as mock_s3fs: |
| PyArrowFileIO("oss://oss-bucket/paimon-database/paimon-table", Options(props)) |
| mock_s3fs.assert_called_once_with(access_key="AKID", |
| secret_key="SECRET", |
| session_token="TOKEN", |
| region="cn-hangzhou", |
| endpoint_override="oss-bucket." + props[OssOptions.OSS_ENDPOINT.key()]) |
| PyArrowFileIO("oss://oss-bucket.endpoint/paimon-database/paimon-table", Options(props)) |
| mock_s3fs.assert_called_with(access_key="AKID", |
| secret_key="SECRET", |
| session_token="TOKEN", |
| region="cn-hangzhou", |
| endpoint_override="oss-bucket." + props[OssOptions.OSS_ENDPOINT.key()]) |
| PyArrowFileIO("oss://access_id:secret_key@Endpoint/oss-bucket/paimon-database/paimon-table", Options(props)) |
| mock_s3fs.assert_called_with(access_key="AKID", |
| secret_key="SECRET", |
| session_token="TOKEN", |
| region="cn-hangzhou", |
| endpoint_override="oss-bucket." + props[OssOptions.OSS_ENDPOINT.key()]) |
| |
| def test_multi_prepare_commit_ao(self): |
| schema = Schema.from_pyarrow_schema(self.pa_schema, partition_keys=['dt']) |
| self.rest_catalog.create_table('default.test_append_only_parquet', schema, False) |
| table = self.rest_catalog.get_table('default.test_append_only_parquet') |
| write_builder = table.new_stream_write_builder() |
| |
| table_write = write_builder.new_write() |
| table_commit = write_builder.new_commit() |
| # write 1 |
| data1 = { |
| 'user_id': [1, 2, 3, 4], |
| 'item_id': [1001, 1002, 1003, 1004], |
| 'behavior': ['a', 'b', 'c', None], |
| 'dt': ['p1', 'p1', 'p2', 'p1'], |
| } |
| pa_table = pa.Table.from_pydict(data1, schema=self.pa_schema) |
| table_write.write_arrow(pa_table) |
| table_write.prepare_commit(0) |
| # write 2 |
| data2 = { |
| 'user_id': [5, 6, 7, 8], |
| 'item_id': [1005, 1006, 1007, 1008], |
| 'behavior': ['e', 'f', 'g', 'h'], |
| 'dt': ['p2', 'p1', 'p2', 'p2'], |
| } |
| pa_table = pa.Table.from_pydict(data2, schema=self.pa_schema) |
| table_write.write_arrow(pa_table) |
| table_write.prepare_commit(1) |
| # write 3 |
| data3 = { |
| 'user_id': [9, 10], |
| 'item_id': [1009, 1010], |
| 'behavior': ['i', 'j'], |
| 'dt': ['p2', 'p1'], |
| } |
| pa_table = pa.Table.from_pydict(data3, schema=self.pa_schema) |
| table_write.write_arrow(pa_table) |
| cm = table_write.prepare_commit(2) |
| # commit |
| table_commit.commit(cm, 2) |
| table_write.close() |
| table_commit.close() |
| self.assertEqual(2, table_write.file_store_write.commit_identifier) |
| |
| read_builder = table.new_read_builder() |
| table_read = read_builder.new_read() |
| splits = read_builder.new_scan().plan().splits() |
| actual = table_sort_by(table_read.to_arrow(splits), 'user_id') |
| expected = pa.Table.from_pydict({ |
| 'user_id': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], |
| 'item_id': [1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008, 1009, 1010], |
| 'behavior': ['a', 'b', 'c', None, 'e', 'f', 'g', 'h', 'i', 'j'], |
| 'dt': ['p1', 'p1', 'p2', 'p1', 'p2', 'p1', 'p2', 'p2', 'p2', 'p1'] |
| }, schema=self.pa_schema) |
| self.assertEqual(expected, actual) |