| # 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 os |
| import tempfile |
| import unittest |
| |
| from pypaimon.branch.branch_manager import BranchManager |
| from pypaimon.common.identifier import DEFAULT_MAIN_BRANCH |
| from pypaimon.common.file_io import FileIO |
| from pypaimon.common.options.core_options import CoreOptions |
| from pypaimon.common.options.options import Options |
| from pypaimon.schema.data_types import (ArrayType, AtomicType, DataField, |
| MapType, RowType) |
| from pypaimon.schema.schema import Schema |
| from pypaimon.schema.schema_change import SetOption |
| from pypaimon.schema.schema_manager import SchemaManager |
| from pypaimon.schema.table_schema import TableSchema |
| |
| |
| class TestSchemaManagerBranch(unittest.TestCase): |
| """Test cases for SchemaManager branch support.""" |
| |
| def setUp(self): |
| """Set up test fixtures.""" |
| self.temp_dir = tempfile.mkdtemp() |
| self.file_io = FileIO.get(self.temp_dir, Options({})) |
| self.table_path = f"{self.temp_dir}/test_db.db/test_table" |
| |
| def tearDown(self): |
| """Clean up test fixtures.""" |
| import shutil |
| if hasattr(self, 'temp_dir') and os.path.exists(self.temp_dir): |
| shutil.rmtree(self.temp_dir, ignore_errors=True) |
| |
| def test_default_branch(self): |
| """Test that SchemaManager uses default branch by default.""" |
| schema_manager = SchemaManager(self.file_io, self.table_path) |
| self.assertEqual(schema_manager.branch, DEFAULT_MAIN_BRANCH) |
| # branch path should be table path for main branch |
| self.assertEqual(schema_manager.branch_path, self.table_path) |
| |
| def test_custom_branch(self): |
| """Test that SchemaManager can be created with a custom branch.""" |
| branch_name = "feature-branch" |
| schema_manager = SchemaManager(self.file_io, self.table_path, branch_name) |
| self.assertEqual(schema_manager.branch, branch_name) |
| # branch path should include branch name |
| expected_branch_path = BranchManager.branch_path( |
| self.table_path, branch_name |
| ) |
| self.assertEqual(schema_manager.branch_path, expected_branch_path) |
| |
| def test_copy_with_branch(self): |
| """Test that SchemaManager.copy_with_branch creates a new manager for a different branch.""" |
| # Create manager with main branch |
| main_manager = SchemaManager(self.file_io, self.table_path) |
| self.assertEqual(main_manager.branch, DEFAULT_MAIN_BRANCH) |
| |
| # Create manager for feature branch |
| feature_branch = "feature-branch" |
| feature_manager = main_manager.copy_with_branch(feature_branch) |
| self.assertEqual(feature_manager.branch, feature_branch) |
| # Original manager should still be on main branch |
| self.assertEqual(main_manager.branch, DEFAULT_MAIN_BRANCH) |
| |
| def test_branch_path_for_main(self): |
| """Test branch_path property for main branch.""" |
| schema_manager = SchemaManager(self.file_io, self.table_path) |
| self.assertEqual(schema_manager.branch_path, self.table_path) |
| |
| def test_branch_path_for_custom_branch(self): |
| """Test branch_path property for custom branch.""" |
| branch_name = "test-branch" |
| schema_manager = SchemaManager(self.file_io, self.table_path, branch_name) |
| expected_path = f"{self.table_path}/branch/branch-{branch_name}" |
| self.assertEqual(schema_manager.branch_path, expected_path) |
| |
| |
| class TestBlobPartitionValidation(unittest.TestCase): |
| |
| def setUp(self): |
| self.temp_dir = tempfile.mkdtemp() |
| self.file_io = FileIO.get(self.temp_dir, Options({})) |
| |
| def tearDown(self): |
| import shutil |
| shutil.rmtree(self.temp_dir, ignore_errors=True) |
| |
| @staticmethod |
| def _schema(blob_type): |
| return Schema( |
| fields=[ |
| DataField(0, "id", AtomicType("INT")), |
| DataField(1, "payload", blob_type), |
| ], |
| partition_keys=["payload"], |
| options={ |
| "row-tracking.enabled": "true", |
| "data-evolution.enabled": "true", |
| }, |
| ) |
| |
| def test_create_table_rejects_blob_partition_key(self): |
| blob_types = [ |
| AtomicType("BLOB"), |
| ArrayType(True, AtomicType("BLOB")), |
| MapType( |
| True, |
| AtomicType("STRING", False), |
| AtomicType("BLOB"), |
| ), |
| ] |
| for index, blob_type in enumerate(blob_types): |
| with self.subTest(blob_type=blob_type): |
| manager = SchemaManager( |
| self.file_io, |
| f"{self.temp_dir}/test_db.db/create_{index}", |
| ) |
| with self.assertRaisesRegex( |
| ValueError, |
| "can not be part of partition keys", |
| ): |
| manager.create_table(self._schema(blob_type)) |
| |
| def test_commit_rejects_array_blob_partition_key(self): |
| manager = SchemaManager( |
| self.file_io, |
| f"{self.temp_dir}/test_db.db/commit", |
| ) |
| table_schema = TableSchema.from_schema( |
| 0, |
| self._schema(ArrayType(True, AtomicType("BLOB"))), |
| ) |
| with self.assertRaisesRegex( |
| ValueError, |
| "can not be part of partition keys", |
| ): |
| manager.commit(table_schema) |
| |
| def test_create_table_allows_unsupported_map_blob_key(self): |
| manager = SchemaManager( |
| self.file_io, |
| f"{self.temp_dir}/test_db.db/unsupported_map_key", |
| ) |
| schema = Schema( |
| fields=[ |
| DataField(0, "id", AtomicType("INT")), |
| DataField( |
| 1, |
| "payload", |
| MapType( |
| True, |
| AtomicType("BOOLEAN", False), |
| AtomicType("BLOB"), |
| ), |
| ), |
| ], |
| options={ |
| "row-tracking.enabled": "true", |
| "data-evolution.enabled": "true", |
| }, |
| ) |
| |
| table_schema = manager.create_table(schema) |
| self.assertEqual(table_schema.fields[1].type, schema.fields[1].type) |
| |
| def test_create_table_rejects_unsupported_nested_blob(self): |
| nested_types = [ |
| MapType(True, AtomicType("BLOB", False), AtomicType("INT")), |
| RowType( |
| True, |
| [DataField(2, "nested_blob", AtomicType("BLOB"))], |
| ), |
| ArrayType(True, ArrayType(True, AtomicType("BLOB"))), |
| MapType( |
| True, |
| AtomicType("STRING", False), |
| ArrayType(True, AtomicType("BLOB")), |
| ), |
| ] |
| |
| for index, nested_type in enumerate(nested_types): |
| with self.subTest(nested_type=nested_type): |
| manager = SchemaManager( |
| self.file_io, |
| f"{self.temp_dir}/test_db.db/nested_blob_{index}", |
| ) |
| schema = Schema( |
| fields=[ |
| DataField(0, "id", AtomicType("INT")), |
| DataField(1, "payload", nested_type), |
| ], |
| ) |
| with self.assertRaisesRegex( |
| ValueError, |
| "unsupported nested BLOB type", |
| ): |
| manager.create_table(schema) |
| |
| def test_create_table_still_rejects_primary_key_map_blob(self): |
| manager = SchemaManager( |
| self.file_io, |
| f"{self.temp_dir}/test_db.db/pk_map_blob", |
| ) |
| schema = Schema( |
| fields=[ |
| DataField(0, "id", AtomicType("INT", False)), |
| DataField( |
| 1, |
| "payload", |
| MapType( |
| True, |
| AtomicType("STRING", False), |
| AtomicType("BLOB"), |
| ), |
| ), |
| ], |
| primary_keys=["id"], |
| options={ |
| "row-tracking.enabled": "true", |
| "data-evolution.enabled": "true", |
| }, |
| ) |
| |
| with self.assertRaisesRegex( |
| ValueError, |
| "MAP<X, BLOB> type is not supported with primary key", |
| ): |
| manager.create_table(schema) |
| |
| |
| class TestOptionValidation(unittest.TestCase): |
| |
| def setUp(self): |
| self.temp_dir = tempfile.mkdtemp() |
| self.file_io = FileIO.get(self.temp_dir, Options({})) |
| self.table_path = f"{self.temp_dir}/test_db.db/test_table" |
| |
| def tearDown(self): |
| import shutil |
| shutil.rmtree(self.temp_dir, ignore_errors=True) |
| |
| @staticmethod |
| def _schema(options=None): |
| return Schema( |
| fields=[DataField(0, "id", AtomicType("INT"))], |
| options=options or {}, |
| ) |
| |
| def test_create_rejects_invalid_target_file_row_num(self): |
| key = CoreOptions.TARGET_FILE_ROW_NUM.key() |
| max_value = CoreOptions.TARGET_FILE_ROW_NUM.default_value() |
| manager = SchemaManager(self.file_io, self.table_path) |
| |
| invalid_values = [ |
| ("0", "should be at least 1"), |
| (str(max_value + 1), f"should be at most {max_value}"), |
| ] |
| for value, message in invalid_values: |
| with self.subTest(value=value): |
| with self.assertRaisesRegex(ValueError, f"{key} {message}"): |
| manager.create_table(self._schema({key: value})) |
| |
| def test_alter_rejects_invalid_target_file_row_num(self): |
| key = CoreOptions.TARGET_FILE_ROW_NUM.key() |
| max_value = CoreOptions.TARGET_FILE_ROW_NUM.default_value() |
| manager = SchemaManager(self.file_io, self.table_path) |
| manager.create_table(self._schema()) |
| |
| invalid_values = [ |
| ("-1", "should be at least 1"), |
| (str(max_value + 1), f"should be at most {max_value}"), |
| ] |
| for value, message in invalid_values: |
| with self.subTest(value=value): |
| with self.assertRaisesRegex(RuntimeError, f"{key} {message}"): |
| manager.commit_changes([SetOption(key, value)]) |
| |
| self.assertEqual(manager.latest().id, 0) |
| self.assertNotIn(key, manager.latest().options) |
| |
| |
| if __name__ == '__main__': |
| unittest.main() |