This is an automated email from the ASF dual-hosted git repository.

fokko pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg-python.git


The following commit(s) were added to refs/heads/main by this push:
     new 33bfbc88 Add `namespace_exists` method (#2972)
33bfbc88 is described below

commit 33bfbc8807279c77563351ec1c4d0b044871440b
Author: Alex Stephen <[email protected]>
AuthorDate: Wed Jan 28 13:17:05 2026 -0800

    Add `namespace_exists` method (#2972)
    
    <!--
    Thanks for opening a pull request!
    -->
    
    <!-- In the case this PR will resolve an issue, please replace
    ${GITHUB_ISSUE_ID} below with the actual Github issue id. -->
    Closes #2969
    
    # Rationale for this change
    We should make `namespace_exists` available on every catalog.
    
    ## Are these changes tested?
    Includes unit tests. Many catalogs have a private method doing this
    already. Look at all of the type exceptions being deleted!
    
    ## Are there any user-facing changes?
    - Adds namespace_exists method.
    
    <!-- In the case of user-facing changes, please add the changelog label.
    -->
---
 pyiceberg/catalog/__init__.py           | 26 ++++++++++++++++++++++++++
 pyiceberg/catalog/noop.py               |  3 +++
 pyiceberg/catalog/sql.py                | 20 ++++++++++----------
 tests/catalog/test_catalog_behaviors.py | 24 ++++++++++++------------
 tests/integration/test_catalog.py       |  3 +++
 5 files changed, 54 insertions(+), 22 deletions(-)

diff --git a/pyiceberg/catalog/__init__.py b/pyiceberg/catalog/__init__.py
index 67ca805c..31552155 100644
--- a/pyiceberg/catalog/__init__.py
+++ b/pyiceberg/catalog/__init__.py
@@ -470,6 +470,17 @@ class Catalog(ABC):
             bool: True if the view exists, False otherwise.
         """
 
+    @abstractmethod
+    def namespace_exists(self, namespace: str | Identifier) -> bool:
+        """Check if a namespace exists.
+
+        Args:
+            namespace (str | Identifier): Namespace identifier.
+
+        Returns:
+            bool: True if the namespace exists, False otherwise.
+        """
+
     @abstractmethod
     def register_table(self, identifier: str | Identifier, metadata_location: 
str) -> Table:
         """Register a new table using existing metadata.
@@ -845,6 +856,21 @@ class MetastoreCatalog(Catalog, ABC):
         except NoSuchTableError:
             return False
 
+    def namespace_exists(self, namespace: str | Identifier) -> bool:
+        """Check if a namespace exists.
+
+        Args:
+            namespace (str | Identifier): Namespace identifier.
+
+        Returns:
+            bool: True if the namespace exists, False otherwise.
+        """
+        try:
+            self.load_namespace_properties(namespace)
+            return True
+        except NoSuchNamespaceError:
+            return False
+
     def purge_table(self, identifier: str | Identifier) -> None:
         table = self.load_table(identifier)
         self.drop_table(identifier)
diff --git a/pyiceberg/catalog/noop.py b/pyiceberg/catalog/noop.py
index ac2423c1..0dc6fdb7 100644
--- a/pyiceberg/catalog/noop.py
+++ b/pyiceberg/catalog/noop.py
@@ -122,5 +122,8 @@ class NoopCatalog(Catalog):
     def view_exists(self, identifier: str | Identifier) -> bool:
         raise NotImplementedError
 
+    def namespace_exists(self, namespace: str | Identifier) -> bool:
+        raise NotImplementedError
+
     def drop_view(self, identifier: str | Identifier) -> None:
         raise NotImplementedError
diff --git a/pyiceberg/catalog/sql.py b/pyiceberg/catalog/sql.py
index a2704c3d..dc24abcf 100644
--- a/pyiceberg/catalog/sql.py
+++ b/pyiceberg/catalog/sql.py
@@ -204,7 +204,7 @@ class SqlCatalog(MetastoreCatalog):
 
         namespace_identifier = Catalog.namespace_from(identifier)
         table_name = Catalog.table_name_from(identifier)
-        if not self._namespace_exists(namespace_identifier):
+        if not self.namespace_exists(namespace_identifier):
             raise NoSuchNamespaceError(f"Namespace does not exist: 
{namespace_identifier}")
 
         namespace = Catalog.namespace_to_string(namespace_identifier)
@@ -251,7 +251,7 @@ class SqlCatalog(MetastoreCatalog):
         namespace_tuple = Catalog.namespace_from(identifier)
         namespace = Catalog.namespace_to_string(namespace_tuple)
         table_name = Catalog.table_name_from(identifier)
-        if not self._namespace_exists(namespace):
+        if not self.namespace_exists(namespace):
             raise NoSuchNamespaceError(f"Namespace does not exist: 
{namespace}")
 
         with Session(self.engine) as session:
@@ -361,7 +361,7 @@ class SqlCatalog(MetastoreCatalog):
         to_namespace_tuple = Catalog.namespace_from(to_identifier)
         to_namespace = Catalog.namespace_to_string(to_namespace_tuple)
         to_table_name = Catalog.table_name_from(to_identifier)
-        if not self._namespace_exists(to_namespace):
+        if not self.namespace_exists(to_namespace):
             raise NoSuchNamespaceError(f"Namespace does not exist: 
{to_namespace}")
         with Session(self.engine) as session:
             try:
@@ -495,7 +495,7 @@ class SqlCatalog(MetastoreCatalog):
             metadata=updated_staged_table.metadata, 
metadata_location=updated_staged_table.metadata_location
         )
 
-    def _namespace_exists(self, identifier: str | Identifier) -> bool:
+    def namespace_exists(self, identifier: str | Identifier) -> bool:
         namespace_tuple = Catalog.identifier_to_tuple(identifier)
         namespace = Catalog.namespace_to_string(namespace_tuple, 
NoSuchNamespaceError)
         namespace_starts_with = namespace.replace("!", "!!").replace("_", 
"!_").replace("%", "!%") + ".%"
@@ -537,7 +537,7 @@ class SqlCatalog(MetastoreCatalog):
         Raises:
             NamespaceAlreadyExistsError: If a namespace with the given name 
already exists.
         """
-        if self._namespace_exists(namespace):
+        if self.namespace_exists(namespace):
             raise NamespaceAlreadyExistsError(f"Namespace {namespace} already 
exists")
 
         if not properties:
@@ -565,7 +565,7 @@ class SqlCatalog(MetastoreCatalog):
             NoSuchNamespaceError: If a namespace with the given name does not 
exist.
             NamespaceNotEmptyError: If the namespace is not empty.
         """
-        if not self._namespace_exists(namespace):
+        if not self.namespace_exists(namespace):
             raise NoSuchNamespaceError(f"Namespace does not exist: 
{namespace}")
 
         namespace_str = Catalog.namespace_to_string(namespace)
@@ -593,7 +593,7 @@ class SqlCatalog(MetastoreCatalog):
         Raises:
             NoSuchNamespaceError: If a namespace with the given name does not 
exist.
         """
-        if namespace and not self._namespace_exists(namespace):
+        if namespace and not self.namespace_exists(namespace):
             raise NoSuchNamespaceError(f"Namespace does not exist: 
{namespace}")
 
         namespace = Catalog.namespace_to_string(namespace)
@@ -614,7 +614,7 @@ class SqlCatalog(MetastoreCatalog):
         Raises:
             NoSuchNamespaceError: If a namespace with the given name does not 
exist.
         """
-        if namespace and not self._namespace_exists(namespace):
+        if namespace and not self.namespace_exists(namespace):
             raise NoSuchNamespaceError(f"Namespace does not exist: 
{namespace}")
 
         table_stmt = 
select(IcebergTables.table_namespace).where(IcebergTables.catalog_name == 
self.name)
@@ -656,7 +656,7 @@ class SqlCatalog(MetastoreCatalog):
             NoSuchNamespaceError: If a namespace with the given name does not 
exist.
         """
         namespace_str = Catalog.namespace_to_string(namespace)
-        if not self._namespace_exists(namespace):
+        if not self.namespace_exists(namespace):
             raise NoSuchNamespaceError(f"Namespace {namespace_str} does not 
exists")
 
         stmt = select(IcebergNamespaceProperties).where(
@@ -681,7 +681,7 @@ class SqlCatalog(MetastoreCatalog):
             ValueError: If removals and updates have overlapping keys.
         """
         namespace_str = Catalog.namespace_to_string(namespace)
-        if not self._namespace_exists(namespace):
+        if not self.namespace_exists(namespace):
             raise NoSuchNamespaceError(f"Namespace {namespace_str} does not 
exists")
 
         current_properties = 
self.load_namespace_properties(namespace=namespace)
diff --git a/tests/catalog/test_catalog_behaviors.py 
b/tests/catalog/test_catalog_behaviors.py
index 29689358..21508a8f 100644
--- a/tests/catalog/test_catalog_behaviors.py
+++ b/tests/catalog/test_catalog_behaviors.py
@@ -946,7 +946,7 @@ def test_add_column_with_statement(catalog: Catalog, 
table_schema_simple: Schema
 
 def test_create_namespace(catalog: Catalog, test_namespace: Identifier, 
test_table_properties: dict[str, str]) -> None:
     catalog.create_namespace(test_namespace, test_table_properties)
-    assert catalog._namespace_exists(test_namespace)  # type: 
ignore[attr-defined]
+    assert catalog.namespace_exists(test_namespace)
     assert (Catalog.identifier_to_tuple(test_namespace)[:1]) in 
catalog.list_namespaces()
     assert test_table_properties == 
catalog.load_namespace_properties(test_namespace)
 
@@ -1015,16 +1015,16 @@ def 
test_get_namespace_metadata_raises_error_when_namespace_does_not_exist(catal
 def test_namespace_exists(catalog: Catalog) -> None:
     for ns in [("db1",), ("db1", "ns1"), ("db2", "ns1"), ("db3", "ns1", 
"ns2")]:
         catalog.create_namespace(ns)
-        assert catalog._namespace_exists(ns)  # type: ignore[attr-defined]
+        assert catalog.namespace_exists(ns)
 
     # `db2` exists because `db2.ns1` exists
-    assert catalog._namespace_exists("db2")  # type: ignore[attr-defined]
+    assert catalog.namespace_exists("db2")
     # `db3.ns1` exists because `db3.ns1.ns2` exists
-    assert catalog._namespace_exists("db3.ns1")  # type: ignore[attr-defined]
+    assert catalog.namespace_exists("db3.ns1")
     # make sure '_' is escaped in the query
-    assert not catalog._namespace_exists("db_")  # type: ignore[attr-defined]
+    assert not catalog.namespace_exists("db_")
     # make sure '%' is escaped in the query
-    assert not catalog._namespace_exists("db%")  # type: ignore[attr-defined]
+    assert not catalog.namespace_exists("db%")
 
 
 # Namespace properties
@@ -1064,7 +1064,7 @@ def test_load_empty_namespace_properties(catalog: 
Catalog, test_namespace: Ident
 def test_list_namespaces(catalog: Catalog) -> None:
     namespace_list = ["db", "db.ns1", "db.ns1.ns2", "db.ns2", "db2", 
"db2.ns1", "db%"]
     for namespace in namespace_list:
-        if not catalog._namespace_exists(namespace):  # type: 
ignore[attr-defined]
+        if not catalog.namespace_exists(namespace):
             catalog.create_namespace(namespace)
 
     ns_list = catalog.list_namespaces()
@@ -1084,7 +1084,7 @@ def test_list_namespaces(catalog: Catalog) -> None:
 def test_list_namespaces_fuzzy_match(catalog: Catalog) -> None:
     namespace_list = ["db.ns1", "db.ns1.ns2", "db.ns2", "db.ns1X.ns3", 
"db_.ns1.ns2", "db2.ns1.ns2"]
     for namespace in namespace_list:
-        if not catalog._namespace_exists(namespace):  # type: 
ignore[attr-defined]
+        if not catalog.namespace_exists(namespace):
             catalog.create_namespace(namespace)
 
     assert catalog.list_namespaces("db.ns1") == [("db", "ns1", "ns2")]
@@ -1140,7 +1140,7 @@ def test_update_namespace_metadata(catalog: Catalog, 
test_namespace: Identifier,
     catalog.create_namespace(test_namespace, test_table_properties)
     new_metadata = {"key3": "value3", "key4": "value4"}
     summary = catalog.update_namespace_properties(test_namespace, 
updates=new_metadata)
-    assert catalog._namespace_exists(test_namespace)  # type: 
ignore[attr-defined]
+    assert catalog.namespace_exists(test_namespace)
     assert new_metadata.items() <= 
catalog.load_namespace_properties(test_namespace).items()
     assert summary.removed == []
     assert sorted(summary.updated) == ["key3", "key4"]
@@ -1154,7 +1154,7 @@ def test_update_namespace_metadata_removals(
     new_metadata = {"key3": "value3", "key4": "value4"}
     remove_metadata = {"key1"}
     summary = catalog.update_namespace_properties(test_namespace, 
remove_metadata, new_metadata)
-    assert catalog._namespace_exists(test_namespace)  # type: 
ignore[attr-defined]
+    assert catalog.namespace_exists(test_namespace)
     assert new_metadata.items() <= 
catalog.load_namespace_properties(test_namespace).items()
     assert 
remove_metadata.isdisjoint(catalog.load_namespace_properties(test_namespace).keys())
     assert summary.removed == ["key1"]
@@ -1168,13 +1168,13 @@ def test_update_namespace_metadata_removals(
 def test_drop_namespace(catalog: Catalog, table_schema_nested: Schema, 
test_table_identifier: Identifier) -> None:
     namespace = Catalog.namespace_from(test_table_identifier)
     catalog.create_namespace(namespace)
-    assert catalog._namespace_exists(namespace)  # type: ignore[attr-defined]
+    assert catalog.namespace_exists(namespace)
     catalog.create_table(test_table_identifier, table_schema_nested)
     with pytest.raises(NamespaceNotEmptyError):
         catalog.drop_namespace(namespace)
     catalog.drop_table(test_table_identifier)
     catalog.drop_namespace(namespace)
-    assert not catalog._namespace_exists(namespace)  # type: 
ignore[attr-defined]
+    assert not catalog.namespace_exists(namespace)
 
 
 def test_drop_namespace_raises_error_when_namespace_does_not_exist(catalog: 
Catalog) -> None:
diff --git a/tests/integration/test_catalog.py 
b/tests/integration/test_catalog.py
index 72ef16cf..f9d174dd 100644
--- a/tests/integration/test_catalog.py
+++ b/tests/integration/test_catalog.py
@@ -410,7 +410,10 @@ def test_concurrent_create_transaction(test_catalog: 
Catalog, test_schema: Schem
 @pytest.mark.integration
 @pytest.mark.parametrize("test_catalog", CATALOGS)
 def test_create_namespace(test_catalog: Catalog, database_name: str) -> None:
+    assert not test_catalog.namespace_exists(database_name)
+
     test_catalog.create_namespace(database_name)
+    assert test_catalog.namespace_exists(database_name)
     assert (database_name,) in test_catalog.list_namespaces()
 
 

Reply via email to