From 4dbb967982edb6699ef05909ec3f632f07de311c Mon Sep 17 00:00:00 2001 From: Gabriel Igliozzi Date: Fri, 8 May 2026 11:56:26 +0200 Subject: [PATCH 1/4] View api --- pyiceberg/view/__init__.py | 47 ++++++++++++++++++++++++++++++++++---- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/pyiceberg/view/__init__.py b/pyiceberg/view/__init__.py index 4ddb21a112..c244566953 100644 --- a/pyiceberg/view/__init__.py +++ b/pyiceberg/view/__init__.py @@ -16,12 +16,11 @@ # under the License. from __future__ import annotations -from typing import ( - Any, -) +from typing import Any +from pyiceberg.schema import Schema from pyiceberg.typedef import Identifier -from pyiceberg.view.metadata import ViewMetadata +from pyiceberg.view.metadata import SQLViewRepresentation, ViewHistoryEntry, ViewMetadata, ViewVersion class View: @@ -42,6 +41,46 @@ def name(self) -> Identifier: """Return the identifier of this view.""" return self._identifier + def schema(self) -> Schema: + """Return the schema for this view.""" + return next(schema for schema in self.metadata.schemas if schema.schema_id == self.current_version().schema_id) + + def schemas(self) -> dict[int, Schema]: + """Return the schemas for this view.""" + return {schema.schema_id: schema for schema in self.metadata.schemas} + + def current_version(self) -> ViewVersion: + """Get the version of this view.""" + return next(version for version in self.metadata.versions if version.version_id == self.metadata.current_version_id) + + def versions(self) -> list[ViewVersion]: + """Get the versions of this view.""" + return self.metadata.versions + + def version(self, version_id: int) -> ViewVersion: + """Get the version in this view by ID.""" + return next(version for version in self.metadata.versions if version.version_id == version_id) + + def history(self) -> list[ViewHistoryEntry]: + """Get the version of this history view.""" + return self.metadata.version_log + + def properties(self) -> dict[str, str]: + """Return a map of string properties for this view.""" + return self.metadata.properties + + def location(self) -> str: + """Return the view's base location.""" + return self.metadata.location + + def uuid(self) -> str: + """Return the view's UUID.""" + return self.metadata.view_uuid + + def sql_for(self, dialect: str) -> SQLViewRepresentation: + """Return the view representation for the sql dialect.""" + return next(repr.root for repr in self.current_version().representations if repr.root.dialect == dialect) + def __eq__(self, other: Any) -> bool: """Return the equality of two instances of the View class.""" return self.name() == other.name() and self.metadata == other.metadata if isinstance(other, View) else False From 20dad7a2a7c14e2fd172f8226e51553181812451 Mon Sep 17 00:00:00 2001 From: Gabriel Igliozzi Date: Fri, 8 May 2026 12:20:10 +0200 Subject: [PATCH 2/4] tests --- tests/conftest.py | 37 ++++++++++++++ tests/test_view.py | 120 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 157 insertions(+) create mode 100644 tests/test_view.py diff --git a/tests/conftest.py b/tests/conftest.py index d1a9f92886..102a38f9bf 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1174,6 +1174,43 @@ def example_view_metadata_v1() -> dict[str, Any]: } +@pytest.fixture +def example_view_metadata_v1_multiple_versions() -> dict[str, Any]: + return { + "view-uuid": "a20125c8-7284-442c-9aea-15fee620737c", + "format-version": 1, + "location": "s3://bucket/test/location/test_view", + "current-version-id": 2, + "versions": [ + { + "version-id": 1, + "timestamp-ms": 1602638573874, + "schema-id": 1, + "summary": {}, + "representations": [{"type": "sql", "sql": "SELECT 1", "dialect": "spark"}], + "default-namespace": ["default"], + }, + { + "version-id": 2, + "timestamp-ms": 1602638573875, + "schema-id": 2, + "summary": {}, + "representations": [{"type": "sql", "sql": "SELECT 2", "dialect": "spark"}], + "default-namespace": ["default"], + }, + ], + "schemas": [ + {"type": "struct", "schema-id": 1, "fields": [{"id": 1, "name": "a", "required": True, "type": "long"}]}, + {"type": "struct", "schema-id": 2, "fields": [{"id": 2, "name": "b", "required": True, "type": "string"}]}, + ], + "version-log": [ + {"timestamp-ms": 1602638573874, "version-id": 1}, + {"timestamp-ms": 1602638573875, "version-id": 2}, + ], + "properties": {}, + } + + @pytest.fixture def example_table_metadata_v3() -> dict[str, Any]: return EXAMPLE_TABLE_METADATA_V3 diff --git a/tests/test_view.py b/tests/test_view.py new file mode 100644 index 0000000000..bb1759fbf6 --- /dev/null +++ b/tests/test_view.py @@ -0,0 +1,120 @@ +# 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. +from typing import Any + +import pytest + +from pyiceberg.schema import Schema +from pyiceberg.view import View +from pyiceberg.view.metadata import SQLViewRepresentation, ViewHistoryEntry, ViewMetadata, ViewVersion + + +@pytest.fixture +def view(example_view_metadata_v1: dict[str, Any]) -> View: + metadata = ViewMetadata.model_validate(example_view_metadata_v1) + return View(("default", "test_view"), metadata) + + +def test_view_schema(view: View) -> None: + schema = view.schema() + assert isinstance(schema, Schema) + assert schema.schema_id == 1 + assert len(schema.fields) == 3 + assert schema.find_field("x") is not None + assert schema.find_field("y") is not None + assert schema.find_field("z") is not None + + +def test_view_schemas(view: View) -> None: + schemas = view.schemas() + assert isinstance(schemas, dict) + assert len(schemas) == 1 + assert 1 in schemas + assert isinstance(schemas[1], Schema) + + +def test_view_current_version(view: View) -> None: + version = view.current_version() + assert isinstance(version, ViewVersion) + assert version.version_id == 1 + assert version.schema_id == 1 + + +def test_view_versions(view: View) -> None: + versions = view.versions() + assert len(versions) == 1 + assert isinstance(versions[0], ViewVersion) + assert versions[0].version_id == 1 + + +def test_view_version_by_id(view: View) -> None: + version = view.version(1) + assert isinstance(version, ViewVersion) + assert version.version_id == 1 + assert version == view.current_version() + + +def test_view_history(view: View) -> None: + history = view.history() + assert len(history) == 1 + assert isinstance(history[0], ViewHistoryEntry) + assert history[0].version_id == 1 + assert history[0].timestamp_ms == 1602638573874 + + +def test_view_properties(view: View) -> None: + assert view.properties() == {"comment": "this is a test view"} + + +def test_view_location(view: View) -> None: + assert view.location() == "s3://bucket/test/location/test_view" + + +def test_view_uuid(view: View) -> None: + assert view.uuid() == "a20125c8-7284-442c-9aea-15fee620737c" + + +def test_view_sql_for_dialect(view: View) -> None: + repr = view.sql_for("spark") + assert isinstance(repr, SQLViewRepresentation) + assert repr.dialect == "spark" + assert repr.sql == "SELECT * FROM prod.db.table" + + +def test_view_schemas_multiple(example_view_metadata_v1_multiple_versions: dict[str, Any]) -> None: + view = View(("default", "test_view"), ViewMetadata.model_validate(example_view_metadata_v1_multiple_versions)) + schemas = view.schemas() + assert len(schemas) == 2 + assert 1 in schemas + assert 2 in schemas + assert view.schema().schema_id == 2 + + +def test_view_versions_multiple(example_view_metadata_v1_multiple_versions: dict[str, Any]) -> None: + view = View(("default", "test_view"), ViewMetadata.model_validate(example_view_metadata_v1_multiple_versions)) + assert len(view.versions()) == 2 + assert view.current_version().version_id == 2 + + +def test_view_version_unknown_id(view: View) -> None: + with pytest.raises(StopIteration): + view.version(999) + + +def test_view_sql_for_unknown_dialect(view: View) -> None: + with pytest.raises(StopIteration): + view.sql_for("trino") From fb6bed4c6c0a14bdf2ae3a14255c882e0e6cc889 Mon Sep 17 00:00:00 2001 From: Gabriel Igliozzi Date: Fri, 8 May 2026 22:42:22 +0200 Subject: [PATCH 3/4] fix: return uuid type --- pyiceberg/view/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyiceberg/view/__init__.py b/pyiceberg/view/__init__.py index c244566953..9e1c05770d 100644 --- a/pyiceberg/view/__init__.py +++ b/pyiceberg/view/__init__.py @@ -17,6 +17,7 @@ from __future__ import annotations from typing import Any +from uuid import UUID from pyiceberg.schema import Schema from pyiceberg.typedef import Identifier @@ -73,9 +74,9 @@ def location(self) -> str: """Return the view's base location.""" return self.metadata.location - def uuid(self) -> str: + def uuid(self) -> UUID: """Return the view's UUID.""" - return self.metadata.view_uuid + return UUID(self.metadata.view_uuid) def sql_for(self, dialect: str) -> SQLViewRepresentation: """Return the view representation for the sql dialect.""" From 4dc01aa79f0c013e22ef6366f8ba7e738e90a5ab Mon Sep 17 00:00:00 2001 From: Gabriel Igliozzi Date: Fri, 8 May 2026 22:49:05 +0200 Subject: [PATCH 4/4] compare uuid == uuid not str --- tests/test_view.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_view.py b/tests/test_view.py index bb1759fbf6..b4a684f636 100644 --- a/tests/test_view.py +++ b/tests/test_view.py @@ -15,6 +15,7 @@ # specific language governing permissions and limitations # under the License. from typing import Any +from uuid import UUID import pytest @@ -85,7 +86,7 @@ def test_view_location(view: View) -> None: def test_view_uuid(view: View) -> None: - assert view.uuid() == "a20125c8-7284-442c-9aea-15fee620737c" + assert view.uuid() == UUID("a20125c8-7284-442c-9aea-15fee620737c") def test_view_sql_for_dialect(view: View) -> None: