From 1bcd70ef867e35b8e052ee31702baca94aaf666f Mon Sep 17 00:00:00 2001 From: Li Jiajia Date: Tue, 27 Oct 2026 07:26:09 -0700 Subject: [PATCH] [python] Reject operations on the system database As in Java AbstractCatalog and RESTCatalog, create_database, drop_database and alter_database reject the system database sys, and create_table, drop_table, rename_table and alter_table reject tables in it, as Java's checkNotSystemTable does. PyPaimon's filesystem, JDBC and REST catalogs accepted them, so for example create_database('sys') and create_table('sys.t') succeeded, although Java resolves sys.* to global system tables and never sees such tables. --- .../pypaimon/catalog/catalog_utils.py | 9 +++++++- .../pypaimon/catalog/filesystem_catalog.py | 4 +++- .../pypaimon/catalog/jdbc_catalog.py | 4 ++++ .../pypaimon/catalog/rest/rest_catalog.py | 5 ++++- .../tests/catalog_table_guard_test.py | 21 +++++++++++++++++++ 5 files changed, 40 insertions(+), 3 deletions(-) diff --git a/paimon-python/pypaimon/catalog/catalog_utils.py b/paimon-python/pypaimon/catalog/catalog_utils.py index 492d60da9db1..8deb937c1cd3 100644 --- a/paimon-python/pypaimon/catalog/catalog_utils.py +++ b/paimon-python/pypaimon/catalog/catalog_utils.py @@ -21,6 +21,7 @@ from typing import Optional from pypaimon.api.api_response import PagedList, Partition +from pypaimon.catalog.catalog import Catalog from pypaimon.common.identifier import Identifier from pypaimon.table.table import Table @@ -49,9 +50,15 @@ def check_not_branch(identifier: Identifier, method: str) -> None: .format(method, identifier.get_full_name())) +def check_not_system_database(name: str) -> None: + """Mirrors Java ``CatalogUtils.checkNotSystemDatabase``.""" + if name == Catalog.SYSTEM_DATABASE_NAME: + raise ValueError("Can't do operation on system database.") + + def check_not_system_table(identifier: Identifier, method: str) -> None: """Mirrors Java ``CatalogUtils.checkNotSystemTable``.""" - if identifier.is_system_table(): + if identifier.get_database_name() == Catalog.SYSTEM_DATABASE_NAME or identifier.is_system_table(): raise ValueError( "Cannot '{}' for system table '{}', please use data table." .format(method, identifier.get_full_name())) diff --git a/paimon-python/pypaimon/catalog/filesystem_catalog.py b/paimon-python/pypaimon/catalog/filesystem_catalog.py index 76218a8bf838..f33211cbf963 100644 --- a/paimon-python/pypaimon/catalog/filesystem_catalog.py +++ b/paimon-python/pypaimon/catalog/filesystem_catalog.py @@ -21,7 +21,7 @@ from pypaimon.catalog.catalog import Catalog from pypaimon.catalog.catalog_context import CatalogContext from pypaimon.catalog.catalog_environment import CatalogEnvironment -from pypaimon.catalog.catalog_utils import check_not_branch, check_not_system_table +from pypaimon.catalog.catalog_utils import check_not_branch, check_not_system_database, check_not_system_table from pypaimon.catalog.catalog_exception import (BranchAlreadyExistException, BranchNotExistException, DatabaseAlreadyExistException, @@ -87,6 +87,7 @@ def get_database(self, name: str) -> Database: def create_database(self, name: str, ignore_if_exists: bool, properties: Optional[Dict[str, str]] = None) -> None: + check_not_system_database(name) try: self.get_database(name) if not ignore_if_exists: @@ -98,6 +99,7 @@ def create_database(self, name: str, ignore_if_exists: bool, self.file_io.mkdirs(path) def drop_database(self, name: str, ignore_if_not_exists: bool = False, cascade: bool = False) -> None: + check_not_system_database(name) try: self.get_database(name) except DatabaseNotExistException: diff --git a/paimon-python/pypaimon/catalog/jdbc_catalog.py b/paimon-python/pypaimon/catalog/jdbc_catalog.py index 770a9c5f242c..6ada44de64dc 100644 --- a/paimon-python/pypaimon/catalog/jdbc_catalog.py +++ b/paimon-python/pypaimon/catalog/jdbc_catalog.py @@ -26,6 +26,7 @@ from pypaimon.catalog.catalog_environment import CatalogEnvironment from pypaimon.catalog.catalog_utils import ( check_not_branch, + check_not_system_database, check_not_system_table, encoded_branch_name ) @@ -324,6 +325,7 @@ def get_database(self, name: str) -> Database: def create_database(self, name: str, ignore_if_exists: bool, properties: Optional[Dict[str, str]] = None) -> None: + check_not_system_database(name) if self._database_exists(name): if not ignore_if_exists: raise DatabaseAlreadyExistException(name) @@ -337,6 +339,7 @@ def create_database(self, name: str, ignore_if_exists: bool, self._insert_database_properties(name, create_props) def drop_database(self, name: str, ignore_if_not_exists: bool = False, cascade: bool = False) -> None: + check_not_system_database(name) if not self._database_exists(name): if not ignore_if_not_exists: raise DatabaseNotExistException(name) @@ -362,6 +365,7 @@ def drop_database(self, name: str, ignore_if_not_exists: bool = False, cascade: ) def alter_database(self, name: str, changes: List[PropertyChange]) -> None: + check_not_system_database(name) self.get_database(name) set_properties, remove_keys = PropertyChange.get_set_properties_to_remove_keys(changes) current = self._fetch_database_properties(name) diff --git a/paimon-python/pypaimon/catalog/rest/rest_catalog.py b/paimon-python/pypaimon/catalog/rest/rest_catalog.py index 2289eaa17492..0974079c5364 100644 --- a/paimon-python/pypaimon/catalog/rest/rest_catalog.py +++ b/paimon-python/pypaimon/catalog/rest/rest_catalog.py @@ -28,7 +28,7 @@ from pypaimon.catalog.catalog import Catalog from pypaimon.catalog.catalog_context import CatalogContext from pypaimon.catalog.catalog_environment import CatalogEnvironment -from pypaimon.catalog.catalog_utils import check_not_branch, check_not_system_table +from pypaimon.catalog.catalog_utils import check_not_branch, check_not_system_database, check_not_system_table from pypaimon.catalog.catalog_exception import ( ColumnAlreadyExistException, ColumnNotExistException, TableNotExistException, DatabaseAlreadyExistException, @@ -164,6 +164,7 @@ def list_databases_paged(self, max_results: Optional[int] = None, page_token: Op def create_database(self, name: str, ignore_if_exists: bool, properties: Optional[Dict[str, str]] = None) -> None: + check_not_system_database(name) try: self.rest_api.create_database(name, properties) except AlreadyExistsException as e: @@ -187,6 +188,7 @@ def get_database(self, name: str) -> Database: raise DatabaseNoPermissionException(name) from e def drop_database(self, name: str, ignore_if_not_exists: bool = False, cascade: bool = False) -> None: + check_not_system_database(name) if not cascade: try: tables = self.list_tables(name) @@ -209,6 +211,7 @@ def drop_database(self, name: str, ignore_if_not_exists: bool = False, cascade: raise DatabaseNoPermissionException(name) from e def alter_database(self, name: str, changes: List[PropertyChange]) -> None: + check_not_system_database(name) try: set_properties, remove_keys = PropertyChange.get_set_properties_to_remove_keys(changes) self.rest_api.alter_database(name, list(remove_keys), set_properties) diff --git a/paimon-python/pypaimon/tests/catalog_table_guard_test.py b/paimon-python/pypaimon/tests/catalog_table_guard_test.py index 2787b0496330..bf7ed0c8521b 100644 --- a/paimon-python/pypaimon/tests/catalog_table_guard_test.py +++ b/paimon-python/pypaimon/tests/catalog_table_guard_test.py @@ -24,6 +24,8 @@ import pyarrow as pa from pypaimon import CatalogFactory, Schema +from pypaimon.catalog.catalog import Catalog +from pypaimon.catalog.rest.property_change import PropertyChange from pypaimon.common.identifier import Identifier from pypaimon.schema.data_types import AtomicType from pypaimon.schema.schema_change import SchemaChange @@ -79,6 +81,25 @@ def assert_table_guards(test, catalog, database): catalog.drop_table(Identifier(database, 'm2', branch='main'), False) test.assertEqual(catalog.list_tables(database), ['t']) + # As in Java, the sys database and the tables in it cannot be created, dropped or changed. + operations = [ + lambda: catalog.create_database('sys', True), + lambda: catalog.drop_database('sys', True), + lambda: catalog.create_table('sys.t', schema, True), + lambda: catalog.drop_table('sys.t', True), + lambda: catalog.rename_table(name, 'sys.t'), + lambda: catalog.alter_table('sys.t', [add_column]), + ] + if type(catalog).alter_database is not Catalog.alter_database: + operations.append(lambda: catalog.alter_database('sys', [PropertyChange.set_property('k', 'v')])) + for operation in operations: + with test.assertRaises(ValueError): + operation() + with test.assertRaisesRegex(ValueError, "Can't do operation on system database."): + catalog.create_database('sys', False) + test.assertNotIn('sys', catalog.list_databases()) + test.assertEqual(catalog.list_tables(database), ['t']) + class CatalogTableGuardTest(unittest.TestCase):