blob: f58d8876c54879e6010ac13ea39a183d4cd3442e [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.
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()