From 39259909cda174abc0d775fba8655c0773009226 Mon Sep 17 00:00:00 2001 From: "nhuan.bc" Date: Tue, 28 Jul 2026 01:02:27 +0700 Subject: [PATCH 1/6] Implement list_views for hive catalog --- pyiceberg/catalog/__init__.py | 1 + pyiceberg/catalog/hive.py | 24 +++++++++++++++++++++++- tests/catalog/test_hive.py | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 1 deletion(-) diff --git a/pyiceberg/catalog/__init__.py b/pyiceberg/catalog/__init__.py index 95ceaa539f..bba3244e72 100644 --- a/pyiceberg/catalog/__init__.py +++ b/pyiceberg/catalog/__init__.py @@ -96,6 +96,7 @@ LOCATION = "location" EXTERNAL_TABLE = "EXTERNAL_TABLE" BOTOCORE_SESSION = "botocore_session" +ICEBERG_VIEW = "iceberg-view" TABLE_METADATA_FILE_NAME_REGEX = re.compile( r""" diff --git a/pyiceberg/catalog/hive.py b/pyiceberg/catalog/hive.py index 181f9d4661..032499b85c 100644 --- a/pyiceberg/catalog/hive.py +++ b/pyiceberg/catalog/hive.py @@ -59,6 +59,7 @@ METADATA_LOCATION, TABLE_TYPE, URI, + ICEBERG_VIEW, MetastoreCatalog, PropertiesUpdateSummary, ) @@ -480,7 +481,28 @@ def register_table(self, identifier: str | Identifier, metadata_location: str, o @override def list_views(self, namespace: str | Identifier) -> list[Identifier]: - raise NotImplementedError + """List Iceberg views under the given namespace in the catalog. + + When the database doesn't exist, it will just return an empty list. + + Args: + namespace: Database to list. + + Returns: + List[Identifier]: list of views identifiers. + + Raises: + NoSuchNamespaceError: If a namespace with the given name does not exist, or the identifier is invalid. + """ + database_name = self.identifier_to_database(namespace, NoSuchNamespaceError) + with self._client as open_client: + return [ + (database_name, table.tableName) + for table in open_client.get_table_objects_by_name( + dbname=database_name, tbl_names=open_client.get_all_tables(db_name=database_name) + ) + if table.parameters.get(TABLE_TYPE, "").lower() == ICEBERG_VIEW + ] @override def view_exists(self, identifier: str | Identifier) -> bool: diff --git a/tests/catalog/test_hive.py b/tests/catalog/test_hive.py index 09bb5ab920..7ad919fc4e 100644 --- a/tests/catalog/test_hive.py +++ b/tests/catalog/test_hive.py @@ -1144,6 +1144,38 @@ def test_create_database_already_exists() -> None: assert "Database default already exists" in str(exc_info.value) +def test_list_views(hive_table: HiveTable) -> None: + catalog = HiveCatalog(HIVE_CATALOG_NAME, uri=HIVE_METASTORE_FAKE_URL) + + tbl1 = deepcopy(hive_table) + tbl1.tableName = "table1" + tbl1.dbName = "database" + tbl1.parameters["table_type"] = "iceberg-view" + tbl2 = deepcopy(hive_table) + tbl2.tableName = "table2" + tbl2.dbName = "database" + tbl2.parameters["table_type"] = "iceberg-view" + tbl3 = deepcopy(hive_table) + tbl3.tableName = "table3" + tbl3.dbName = "database" + tbl3.parameters["table_type"] = "iceberg" + tbl4 = deepcopy(hive_table) + tbl4.tableName = "table4" + tbl4.dbName = "database" + tbl4.parameters.pop("table_type") + + catalog._client = MagicMock() + catalog._client.__enter__().get_all_tables.return_value = ["table1", "table2", "table3", "table4"] + catalog._client.__enter__().get_table_objects_by_name.return_value = [tbl1, tbl2, tbl3, tbl4] + + got_tables = catalog.list_views("database") + assert got_tables == [("database", "table1"), ("database", "table2")] + catalog._client.__enter__().get_all_tables.assert_called_with(db_name="database") + catalog._client.__enter__().get_table_objects_by_name.assert_called_with( + dbname="database", tbl_names=["table1", "table2", "table3", "table4"] + ) + + def test_load_namespace_properties(hive_database: HiveDatabase) -> None: catalog = HiveCatalog(HIVE_CATALOG_NAME, uri=HIVE_METASTORE_FAKE_URL) From 134eded13e9df2bb1d461a68617e430cd06593f5 Mon Sep 17 00:00:00 2001 From: "nhuan.bc" Date: Tue, 28 Jul 2026 01:15:42 +0700 Subject: [PATCH 2/6] Fix ruff --- pyiceberg/catalog/hive.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyiceberg/catalog/hive.py b/pyiceberg/catalog/hive.py index 032499b85c..1f539e1411 100644 --- a/pyiceberg/catalog/hive.py +++ b/pyiceberg/catalog/hive.py @@ -55,11 +55,11 @@ from pyiceberg.catalog import ( EXTERNAL_TABLE, ICEBERG, + ICEBERG_VIEW, LOCATION, METADATA_LOCATION, TABLE_TYPE, URI, - ICEBERG_VIEW, MetastoreCatalog, PropertiesUpdateSummary, ) From a45c830e727db4d21594513e6f41c9c1e54c47e3 Mon Sep 17 00:00:00 2001 From: NHUANBC Date: Tue, 28 Jul 2026 18:23:47 +0700 Subject: [PATCH 3/6] Raise database not exisits and unit test --- pyiceberg/catalog/hive.py | 21 +++++++++++---------- tests/catalog/test_hive.py | 12 ++++++++++++ 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/pyiceberg/catalog/hive.py b/pyiceberg/catalog/hive.py index 1f539e1411..d2787f5b1f 100644 --- a/pyiceberg/catalog/hive.py +++ b/pyiceberg/catalog/hive.py @@ -483,8 +483,6 @@ def register_table(self, identifier: str | Identifier, metadata_location: str, o def list_views(self, namespace: str | Identifier) -> list[Identifier]: """List Iceberg views under the given namespace in the catalog. - When the database doesn't exist, it will just return an empty list. - Args: namespace: Database to list. @@ -495,14 +493,17 @@ def list_views(self, namespace: str | Identifier) -> list[Identifier]: NoSuchNamespaceError: If a namespace with the given name does not exist, or the identifier is invalid. """ database_name = self.identifier_to_database(namespace, NoSuchNamespaceError) - with self._client as open_client: - return [ - (database_name, table.tableName) - for table in open_client.get_table_objects_by_name( - dbname=database_name, tbl_names=open_client.get_all_tables(db_name=database_name) - ) - if table.parameters.get(TABLE_TYPE, "").lower() == ICEBERG_VIEW - ] + try: + with self._client as open_client: + return [ + (database_name, table.tableName) + for table in open_client.get_table_objects_by_name( + dbname=database_name, tbl_names=open_client.get_all_tables(db_name=database_name) + ) + if table.parameters.get(TABLE_TYPE, "").lower() == ICEBERG_VIEW + ] + except (MetaException, NoSuchObjectException) as e: + raise NoSuchNamespaceError(f"Database does not exists: {database_name}") from e @override def view_exists(self, identifier: str | Identifier) -> bool: diff --git a/tests/catalog/test_hive.py b/tests/catalog/test_hive.py index 7ad919fc4e..84c2bf6572 100644 --- a/tests/catalog/test_hive.py +++ b/tests/catalog/test_hive.py @@ -1176,6 +1176,18 @@ def test_list_views(hive_table: HiveTable) -> None: ) +def test_list_views_to_namespace_does_not_exists(hive_table: HiveTable) -> None: + catalog = HiveCatalog(HIVE_CATALOG_NAME, uri=HIVE_METASTORE_FAKE_URL) + + catalog._client = MagicMock() + catalog._client.__enter__().get_all_tables.side_effect = NoSuchObjectException(message="does_not_exists") + + with pytest.raises(NoSuchNamespaceError) as exc_info: + catalog.list_views("database") + + assert "Database does not exists: database" in str(exc_info.value) + + def test_load_namespace_properties(hive_database: HiveDatabase) -> None: catalog = HiveCatalog(HIVE_CATALOG_NAME, uri=HIVE_METASTORE_FAKE_URL) From 664ee3b53a71cef90a69d14b23a50fbfdda9d5d8 Mon Sep 17 00:00:00 2001 From: "nhuan.bc" Date: Tue, 28 Jul 2026 23:36:19 +0700 Subject: [PATCH 4/6] Add integration test for HiveCatalog.list_views --- tests/integration/test_hive_migration.py | 55 ++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/tests/integration/test_hive_migration.py b/tests/integration/test_hive_migration.py index 51386d56c4..3af509fd13 100644 --- a/tests/integration/test_hive_migration.py +++ b/tests/integration/test_hive_migration.py @@ -21,6 +21,7 @@ from pyspark.sql import SparkSession from pyiceberg.catalog import Catalog +from pyiceberg.catalog.hive import HiveCatalog @pytest.mark.integration @@ -81,3 +82,57 @@ def test_migrate_table( assert tbl.scan(row_filter="dt == '2023-01-01'").to_arrow().column(0).combine_chunks().tolist() == [4, 5, 6] assert tbl.scan(row_filter="dt == '2022-01-01'").to_arrow().column(0).combine_chunks().tolist() == [1, 2, 3] assert tbl.scan(row_filter="dt < '2022-02-01'").to_arrow().column(0).combine_chunks().tolist() == [1, 2, 3] + + +@pytest.mark.integration +def test_list_views( + session_catalog_hive: HiveCatalog, + spark: SparkSession, +) -> None: + """ + Verify that a view created by Spark through the Iceberg Hive catalog + can be discovered by PyIceberg HiveCatalog.list_views(). + + The test also verifies that: + - Iceberg tables are not returned as views. + - Multiple views are returned. + - Returned identifiers use the expected (namespace, view_name) format. + """ + suffix = int(time.time()) + catalog_name = "hive" + namespace = "default" + table_name = f"table_{suffix}" + first_view_name = f"first_view_{suffix}" + second_view_name = f"second_view_{suffix}" + table_identifier = f"{catalog_name}.{namespace}.{table_name}" + first_view_identifier = f"{catalog_name}.{namespace}.{first_view_name}" + second_view_identifier = f"{catalog_name}.{namespace}.{second_view_name}" + + spark.sql(f""" + CREATE TABLE {table_identifier} ( + id INTEGER, + name STRING, + dt DATE + ) + USING iceberg + """) + + spark.sql(f""" + CREATE VIEW {first_view_identifier} AS + SELECT id, name + FROM {table_identifier} + """) + + spark.sql(f""" + CREATE VIEW {second_view_identifier} AS + SELECT id, name, dt + FROM {table_identifier} + """) + + views = set(session_catalog_hive.list_views(namespace)) + + assert (namespace, first_view_name) in views + assert (namespace, second_view_name) in views + + # A table in the same namespace must not be returned as a view. + assert (namespace, table_name) not in views From 28d607253ad396d87ecb7eaf1a546dd28ec8368e Mon Sep 17 00:00:00 2001 From: "nhuan.bc" Date: Wed, 29 Jul 2026 00:19:41 +0700 Subject: [PATCH 5/6] Move the integration test for HiveCatalog.list_views to the common suite --- tests/integration/test_catalog.py | 56 ++++++++++++++++++++++++ tests/integration/test_hive_migration.py | 55 ----------------------- 2 files changed, 56 insertions(+), 55 deletions(-) diff --git a/tests/integration/test_catalog.py b/tests/integration/test_catalog.py index 457f5e90c2..e8e5d7e788 100644 --- a/tests/integration/test_catalog.py +++ b/tests/integration/test_catalog.py @@ -16,12 +16,14 @@ # under the License. import os +import time import uuid from collections.abc import Generator from pathlib import Path, PosixPath from typing import Any import pytest +from pyspark.sql import SparkSession from pytest_lazy_fixtures import lf from pyiceberg.catalog import Catalog, MetastoreCatalog, load_catalog @@ -917,3 +919,57 @@ def test_load_missing_table(test_catalog: Catalog, database_name: str, table_nam with pytest.raises(NoSuchTableError): test_catalog.load_table(identifier) + + +@pytest.mark.integration +def test_list_views( + session_catalog_hive: HiveCatalog, + spark: SparkSession, +) -> None: + """ + Verify that a view created by Spark through the Iceberg Hive catalog + can be discovered by PyIceberg HiveCatalog.list_views(). + + The test also verifies that: + - Iceberg tables are not returned as views. + - Multiple views are returned. + - Returned identifiers use the expected (namespace, view_name) format. + """ + suffix = int(time.time()) + catalog_name = "hive" + namespace = "default" + table_name = f"table_{suffix}" + first_view_name = f"first_view_{suffix}" + second_view_name = f"second_view_{suffix}" + table_identifier = f"{catalog_name}.{namespace}.{table_name}" + first_view_identifier = f"{catalog_name}.{namespace}.{first_view_name}" + second_view_identifier = f"{catalog_name}.{namespace}.{second_view_name}" + + spark.sql(f""" + CREATE TABLE {table_identifier} ( + id INTEGER, + name STRING, + dt DATE + ) + USING iceberg + """) + + spark.sql(f""" + CREATE VIEW {first_view_identifier} AS + SELECT id, name + FROM {table_identifier} + """) + + spark.sql(f""" + CREATE VIEW {second_view_identifier} AS + SELECT id, name, dt + FROM {table_identifier} + """) + + views = set(session_catalog_hive.list_views(namespace)) + + assert (namespace, first_view_name) in views + assert (namespace, second_view_name) in views + + # A table in the same namespace must not be returned as a view. + assert (namespace, table_name) not in views diff --git a/tests/integration/test_hive_migration.py b/tests/integration/test_hive_migration.py index 3af509fd13..51386d56c4 100644 --- a/tests/integration/test_hive_migration.py +++ b/tests/integration/test_hive_migration.py @@ -21,7 +21,6 @@ from pyspark.sql import SparkSession from pyiceberg.catalog import Catalog -from pyiceberg.catalog.hive import HiveCatalog @pytest.mark.integration @@ -82,57 +81,3 @@ def test_migrate_table( assert tbl.scan(row_filter="dt == '2023-01-01'").to_arrow().column(0).combine_chunks().tolist() == [4, 5, 6] assert tbl.scan(row_filter="dt == '2022-01-01'").to_arrow().column(0).combine_chunks().tolist() == [1, 2, 3] assert tbl.scan(row_filter="dt < '2022-02-01'").to_arrow().column(0).combine_chunks().tolist() == [1, 2, 3] - - -@pytest.mark.integration -def test_list_views( - session_catalog_hive: HiveCatalog, - spark: SparkSession, -) -> None: - """ - Verify that a view created by Spark through the Iceberg Hive catalog - can be discovered by PyIceberg HiveCatalog.list_views(). - - The test also verifies that: - - Iceberg tables are not returned as views. - - Multiple views are returned. - - Returned identifiers use the expected (namespace, view_name) format. - """ - suffix = int(time.time()) - catalog_name = "hive" - namespace = "default" - table_name = f"table_{suffix}" - first_view_name = f"first_view_{suffix}" - second_view_name = f"second_view_{suffix}" - table_identifier = f"{catalog_name}.{namespace}.{table_name}" - first_view_identifier = f"{catalog_name}.{namespace}.{first_view_name}" - second_view_identifier = f"{catalog_name}.{namespace}.{second_view_name}" - - spark.sql(f""" - CREATE TABLE {table_identifier} ( - id INTEGER, - name STRING, - dt DATE - ) - USING iceberg - """) - - spark.sql(f""" - CREATE VIEW {first_view_identifier} AS - SELECT id, name - FROM {table_identifier} - """) - - spark.sql(f""" - CREATE VIEW {second_view_identifier} AS - SELECT id, name, dt - FROM {table_identifier} - """) - - views = set(session_catalog_hive.list_views(namespace)) - - assert (namespace, first_view_name) in views - assert (namespace, second_view_name) in views - - # A table in the same namespace must not be returned as a view. - assert (namespace, table_name) not in views From f0cf0b19cee427ba0e4b1c1d3d3e5cc049fc9a83 Mon Sep 17 00:00:00 2001 From: "nhuan.bc" Date: Wed, 29 Jul 2026 00:37:19 +0700 Subject: [PATCH 6/6] Create test_hive_catalog.py for hive integration test only --- tests/integration/test_catalog.py | 56 ------------------- tests/integration/test_hive_catalog.py | 76 ++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 56 deletions(-) create mode 100644 tests/integration/test_hive_catalog.py diff --git a/tests/integration/test_catalog.py b/tests/integration/test_catalog.py index e8e5d7e788..457f5e90c2 100644 --- a/tests/integration/test_catalog.py +++ b/tests/integration/test_catalog.py @@ -16,14 +16,12 @@ # under the License. import os -import time import uuid from collections.abc import Generator from pathlib import Path, PosixPath from typing import Any import pytest -from pyspark.sql import SparkSession from pytest_lazy_fixtures import lf from pyiceberg.catalog import Catalog, MetastoreCatalog, load_catalog @@ -919,57 +917,3 @@ def test_load_missing_table(test_catalog: Catalog, database_name: str, table_nam with pytest.raises(NoSuchTableError): test_catalog.load_table(identifier) - - -@pytest.mark.integration -def test_list_views( - session_catalog_hive: HiveCatalog, - spark: SparkSession, -) -> None: - """ - Verify that a view created by Spark through the Iceberg Hive catalog - can be discovered by PyIceberg HiveCatalog.list_views(). - - The test also verifies that: - - Iceberg tables are not returned as views. - - Multiple views are returned. - - Returned identifiers use the expected (namespace, view_name) format. - """ - suffix = int(time.time()) - catalog_name = "hive" - namespace = "default" - table_name = f"table_{suffix}" - first_view_name = f"first_view_{suffix}" - second_view_name = f"second_view_{suffix}" - table_identifier = f"{catalog_name}.{namespace}.{table_name}" - first_view_identifier = f"{catalog_name}.{namespace}.{first_view_name}" - second_view_identifier = f"{catalog_name}.{namespace}.{second_view_name}" - - spark.sql(f""" - CREATE TABLE {table_identifier} ( - id INTEGER, - name STRING, - dt DATE - ) - USING iceberg - """) - - spark.sql(f""" - CREATE VIEW {first_view_identifier} AS - SELECT id, name - FROM {table_identifier} - """) - - spark.sql(f""" - CREATE VIEW {second_view_identifier} AS - SELECT id, name, dt - FROM {table_identifier} - """) - - views = set(session_catalog_hive.list_views(namespace)) - - assert (namespace, first_view_name) in views - assert (namespace, second_view_name) in views - - # A table in the same namespace must not be returned as a view. - assert (namespace, table_name) not in views diff --git a/tests/integration/test_hive_catalog.py b/tests/integration/test_hive_catalog.py new file mode 100644 index 0000000000..cd0e56f422 --- /dev/null +++ b/tests/integration/test_hive_catalog.py @@ -0,0 +1,76 @@ +# 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 time + +import pytest +from pyspark.sql import SparkSession + +from pyiceberg.catalog.hive import HiveCatalog + + +@pytest.mark.integration +def test_list_views( + session_catalog_hive: HiveCatalog, + spark: SparkSession, +) -> None: + """ + Verify that a view created by Spark through the Iceberg Hive catalog + can be discovered by PyIceberg HiveCatalog.list_views(). + + The test also verifies that: + - Iceberg tables are not returned as views. + - Multiple views are returned. + - Returned identifiers use the expected (namespace, view_name) format. + """ + suffix = int(time.time()) + catalog_name = "hive" + namespace = "default" + table_name = f"table_{suffix}" + first_view_name = f"first_view_{suffix}" + second_view_name = f"second_view_{suffix}" + table_identifier = f"{catalog_name}.{namespace}.{table_name}" + first_view_identifier = f"{catalog_name}.{namespace}.{first_view_name}" + second_view_identifier = f"{catalog_name}.{namespace}.{second_view_name}" + + spark.sql(f""" + CREATE TABLE {table_identifier} ( + id INTEGER, + name STRING, + dt DATE + ) + USING iceberg + """) + + spark.sql(f""" + CREATE VIEW {first_view_identifier} AS + SELECT id, name + FROM {table_identifier} + """) + + spark.sql(f""" + CREATE VIEW {second_view_identifier} AS + SELECT id, name, dt + FROM {table_identifier} + """) + + views = set(session_catalog_hive.list_views(namespace)) + + assert (namespace, first_view_name) in views + assert (namespace, second_view_name) in views + + # A table in the same namespace must not be returned as a view. + assert (namespace, table_name) not in views