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 69a9472a64 [python] Make schema management branch-aware (#8829)
69a9472a64 is described below
commit 69a9472a641d25ada1666ac54f01ad437184458a
Author: zhoulii <[email protected]>
AuthorDate: Thu Jul 23 21:11:10 2026 +0800
[python] Make schema management branch-aware (#8829)
---
.../pypaimon/catalog/filesystem_catalog.py | 49 +++++++++++++---------
paimon-python/pypaimon/table/file_store_table.py | 10 +++--
.../tests/filesystem_catalog_branch_test.py | 20 +++++++++
.../pypaimon/tests/rest/rest_branch_test.py | 14 +++++++
.../pypaimon/tests/table/file_store_table_test.py | 6 ++-
5 files changed, 74 insertions(+), 25 deletions(-)
diff --git a/paimon-python/pypaimon/catalog/filesystem_catalog.py
b/paimon-python/pypaimon/catalog/filesystem_catalog.py
index 5c85c27ca8..76ba5e7798 100644
--- a/paimon-python/pypaimon/catalog/filesystem_catalog.py
+++ b/paimon-python/pypaimon/catalog/filesystem_catalog.py
@@ -21,23 +21,22 @@ from pypaimon.api.api_response import GetTagResponse,
PagedList
from pypaimon.catalog.catalog import Catalog
from pypaimon.catalog.catalog_context import CatalogContext
from pypaimon.catalog.catalog_environment import CatalogEnvironment
-from pypaimon.catalog.catalog_exception import (
- BranchAlreadyExistException,
- BranchNotExistException,
- DatabaseAlreadyExistException,
- DatabaseNotExistException,
- TableAlreadyExistException,
- TableNotExistException,
- TagAlreadyExistException,
- TagNotExistException,
-)
+from pypaimon.catalog.catalog_exception import (BranchAlreadyExistException,
+ BranchNotExistException,
+ DatabaseAlreadyExistException,
+ DatabaseNotExistException,
+ TableAlreadyExistException,
+ TableNotExistException,
+ TagAlreadyExistException,
+ TagNotExistException)
from pypaimon.catalog.database import Database
+from pypaimon.common.file_io import FileIO
+from pypaimon.common.identifier import Identifier
from pypaimon.common.options import Options
from pypaimon.common.options.config import CatalogOptions
from pypaimon.common.options.core_options import CoreOptions
-from pypaimon.common.file_io import FileIO
-from pypaimon.common.identifier import Identifier
-from pypaimon.common.time_utils import duration_to_iso8601,
local_datetime_to_system_zone_millis
+from pypaimon.common.time_utils import (duration_to_iso8601,
+ local_datetime_to_system_zone_millis)
from pypaimon.filesystem.caching_file_io import CachingFileIO
from pypaimon.schema.schema_change import SchemaChange
from pypaimon.schema.schema_manager import SchemaManager
@@ -143,7 +142,8 @@ class FileSystemCatalog(Catalog):
table_schema = self.get_table_schema(identifier)
# Create catalog environment for filesystem catalog
- from pypaimon.catalog.filesystem_catalog_loader import
FileSystemCatalogLoader
+ from pypaimon.catalog.filesystem_catalog_loader import \
+ FileSystemCatalogLoader
catalog_environment = CatalogEnvironment(
identifier=identifier,
uuid=None,
@@ -190,7 +190,11 @@ class FileSystemCatalog(Catalog):
def get_table_schema(self, identifier: Identifier):
table_path = self.get_table_path(identifier)
- table_schema = SchemaManager(self.file_io, table_path).latest()
+ table_schema = SchemaManager(
+ self.file_io,
+ table_path,
+ branch=identifier.get_branch_name_or_default(),
+ ).latest()
if table_schema is None:
raise TableNotExistException(identifier)
return table_schema
@@ -219,7 +223,11 @@ class FileSystemCatalog(Catalog):
return
table_path = self.get_table_path(identifier)
- schema_manager = SchemaManager(self.file_io, table_path)
+ schema_manager = SchemaManager(
+ self.file_io,
+ table_path,
+ branch=identifier.get_branch_name_or_default(),
+ )
try:
schema_manager.commit_changes(changes)
except Exception as e:
@@ -254,7 +262,7 @@ class FileSystemCatalog(Catalog):
def drop_table(self, identifier: Union[str, Identifier],
ignore_if_not_exists: bool = False):
if not isinstance(identifier, Identifier):
identifier = Identifier.from_string(identifier)
-
+
# Check if table exists
try:
self.get_table(identifier)
@@ -262,7 +270,7 @@ class FileSystemCatalog(Catalog):
if not ignore_if_not_exists:
raise
return
-
+
# Delete the table directory
table_path = self.get_table_path(identifier)
self.file_io.delete(table_path, True)
@@ -286,9 +294,9 @@ class FileSystemCatalog(Catalog):
page_token: Optional[str] = None,
partition_name_pattern: Optional[str] = None,
):
- from pypaimon.api.api_response import Partition, PagedList
- from pypaimon.manifest.manifest_list_manager import ManifestListManager
+ from pypaimon.api.api_response import PagedList, Partition
from pypaimon.manifest.manifest_file_manager import ManifestFileManager
+ from pypaimon.manifest.manifest_list_manager import ManifestListManager
if not isinstance(identifier, Identifier):
identifier = Identifier.from_string(identifier)
@@ -345,6 +353,7 @@ class FileSystemCatalog(Catalog):
# Apply pattern filter with proper regex escaping
if partition_name_pattern:
import re
+
# Escape special regex chars except '*', then replace '*' with '.*'
escaped_pattern = re.escape(partition_name_pattern).replace(r'\*',
'.*')
regex = re.compile(escaped_pattern)
diff --git a/paimon-python/pypaimon/table/file_store_table.py
b/paimon-python/pypaimon/table/file_store_table.py
index 8edbc14021..a1de56cb99 100644
--- a/paimon-python/pypaimon/table/file_store_table.py
+++ b/paimon-python/pypaimon/table/file_store_table.py
@@ -60,8 +60,8 @@ class FileStoreTable(Table):
self.is_primary_key_table = bool(self.primary_keys)
self.total_buckets = self.options.bucket()
- current_branch = self.options.branch()
- self.schema_manager = SchemaManager(file_io, table_path,
branch=current_branch)
+ self.schema_manager = SchemaManager(
+ file_io, table_path, branch=self.current_branch())
@classmethod
def from_path(cls, table_path: str) -> 'FileStoreTable':
@@ -118,7 +118,8 @@ class FileStoreTable(Table):
# If catalog environment has a catalog loader, use CatalogBranchManager
catalog_loader = self.catalog_environment.catalog_loader
if catalog_loader is not None and
self.catalog_environment.supports_version_management:
- from pypaimon.branch.catalog_branch_manager import
CatalogBranchManager
+ from pypaimon.branch.catalog_branch_manager import \
+ CatalogBranchManager
return CatalogBranchManager(
catalog_loader,
self.identifier
@@ -453,7 +454,8 @@ class FileStoreTable(Table):
def create_global_index(self, index_column, index_type: str = "btree",
partition_filter=None, partitions=None,
options: Optional[dict] = None) -> int:
- from pypaimon.globalindex.create_global_index import
create_global_index
+ from pypaimon.globalindex.create_global_index import \
+ create_global_index
return create_global_index(
self,
index_column,
diff --git a/paimon-python/pypaimon/tests/filesystem_catalog_branch_test.py
b/paimon-python/pypaimon/tests/filesystem_catalog_branch_test.py
index 2129f811fa..4bf22b85aa 100644
--- a/paimon-python/pypaimon/tests/filesystem_catalog_branch_test.py
+++ b/paimon-python/pypaimon/tests/filesystem_catalog_branch_test.py
@@ -36,6 +36,8 @@ from pypaimon.catalog.catalog_exception import
(BranchAlreadyExistException,
TableNotExistException,
TagNotExistException)
from pypaimon.common.identifier import Identifier
+from pypaimon.schema.data_types import AtomicType
+from pypaimon.schema.schema_change import SchemaChange
class FileSystemCatalogBranchCRUDTest(unittest.TestCase):
@@ -77,6 +79,24 @@ class FileSystemCatalogBranchCRUDTest(unittest.TestCase):
self.catalog.create_branch(self.identifier, "b1")
self.assertEqual(self.catalog.list_branches(self.identifier), ["b1"])
+ def test_alter_table_isolated_to_branch(self):
+ self.catalog.create_branch(self.identifier, "b1")
+ branch_identifier = Identifier(
+ self.identifier.get_database_name(),
+ self.identifier.get_table_name(),
+ branch="b1",
+ )
+
+ self.catalog.alter_table(
+ branch_identifier,
+ [SchemaChange.add_column("branch_col", AtomicType("STRING"))],
+ )
+
+ self.assertNotIn(
+ "branch_col", self.catalog.get_table(self.identifier).field_names)
+ self.assertIn(
+ "branch_col",
self.catalog.get_table(branch_identifier).field_names)
+
def test_create_branch_duplicate_raises(self):
self.catalog.create_branch(self.identifier, "b1")
with self.assertRaises(BranchAlreadyExistException) as cm:
diff --git a/paimon-python/pypaimon/tests/rest/rest_branch_test.py
b/paimon-python/pypaimon/tests/rest/rest_branch_test.py
index 9658c4ee3e..3b9eb80ff2 100644
--- a/paimon-python/pypaimon/tests/rest/rest_branch_test.py
+++ b/paimon-python/pypaimon/tests/rest/rest_branch_test.py
@@ -51,6 +51,20 @@ class RESTCatalogBranchCRUDTest(RESTBaseTest):
self.rest_catalog.create_branch(identifier, "b1")
self.assertEqual(self.rest_catalog.list_branches(identifier), ["b1"])
+ def test_branch_table_uses_branch_schema_manager(self):
+ identifier = self._identifier()
+ self.rest_catalog.create_branch(identifier, "b1")
+ branch_identifier = Identifier(
+ identifier.get_database_name(),
+ identifier.get_table_name(),
+ branch="b1",
+ )
+
+ table = self.rest_catalog.get_table(branch_identifier)
+
+ self.assertEqual(table.current_branch(), "b1")
+ self.assertEqual(table.schema_manager.branch, "b1")
+
def test_create_branch_duplicate_raises(self):
identifier = self._identifier()
self.rest_catalog.create_branch(identifier, "b1")
diff --git a/paimon-python/pypaimon/tests/table/file_store_table_test.py
b/paimon-python/pypaimon/tests/table/file_store_table_test.py
index 2e093f4e4a..9690a550b2 100644
--- a/paimon-python/pypaimon/tests/table/file_store_table_test.py
+++ b/paimon-python/pypaimon/tests/table/file_store_table_test.py
@@ -112,7 +112,7 @@ class FileStoreTableTest(unittest.TestCase):
"""Test consumer_manager when branch is encoded in the identifier."""
branch_name = "feature_branch"
- # Create a regular table; the branch is supplied later via the
+ # Create a regular table and branch, then select the branch via the
# branch-encoded identifier (Java-aligned routing).
schema = Schema.from_pyarrow_schema(
self.pa_schema,
@@ -120,6 +120,7 @@ class FileStoreTableTest(unittest.TestCase):
options={CoreOptions.BUCKET.key(): "2"},
)
self.catalog.create_table('default.test_branch_table', schema, True)
+ self.catalog.create_branch('default.test_branch_table', branch_name)
# Access the table with a branch-encoded identifier.
branch_table = self.catalog.get_table(
@@ -267,6 +268,8 @@ class FileStoreTableTest(unittest.TestCase):
options={CoreOptions.BUCKET.key(): "2"},
)
self.catalog.create_table('default.test_changelog_branch_table',
schema, False)
+ self.catalog.create_branch(
+ 'default.test_changelog_branch_table', branch_name)
branch_table = self.catalog.get_table(
'default.test_changelog_branch_table$branch_{}'.format(branch_name))
@@ -316,6 +319,7 @@ class FileStoreTableTest(unittest.TestCase):
options={CoreOptions.BUCKET.key(): "2"},
)
self.catalog.create_table('default.test_current_branch', schema, False)
+ self.catalog.create_branch('default.test_current_branch', branch_name)
branch_table = self.catalog.get_table(
'default.test_current_branch$branch_{}'.format(branch_name))
self.assertEqual(branch_table.current_branch(), branch_name)