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

mchades pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git


The following commit(s) were added to refs/heads/main by this push:
     new 648fdd7c1a [#12089] improvement(client-python): Implement client-side 
view models (#12090)
648fdd7c1a is described below

commit 648fdd7c1a869f798d05e471420de6cf56e74957
Author: Zhiguo Wu <[email protected]>
AuthorDate: Mon Jul 20 21:22:50 2026 +0800

    [#12089] improvement(client-python): Implement client-side view models 
(#12090)
    
    ### What changes were proposed in this pull request?
    
    - Add `GenericView` as the client-side implementation of `View`.
    - Add DTOs for views and SQL representations.
    - Add polymorphic JSON serialization for view representations.
    - Extend `DTOConverters` with representation conversions.
    - Add unit tests for the new models, serialization, and conversions.
    
    ### Why are the changes needed?
    
    The Python client needs concrete view models and serialization support
    before view catalog operations can consume and return view metadata.
    
    Fix: #12089
    
    ### Does this PR introduce _any_ user-facing change?
    
    No. This PR adds the supporting client-side models. View catalog
    operations will be added separately.
    
    ### How was this patch tested?
    
    Added unit tests covering `GenericView`, view and representation DTOs,
    representation serialization, and DTO conversions. Python client
    formatting, lint, and unit tests pass.
---
 .../client-python/gravitino/client/generic_view.py |  63 +++++
 .../dto/rel/json_serdes/representation_serdes.py   |  49 ++++
 .../gravitino/dto/rel/representation_dto.py        |  30 +++
 .../gravitino/dto/rel/sql_representation_dto.py    |  58 ++++
 .../client-python/gravitino/dto/rel/view_dto.py    | 105 ++++++++
 .../gravitino/dto/util/dto_converters.py           |  54 +++-
 .../dto/rel/test_representation_serdes.py          |  54 ++++
 .../dto/rel/test_sql_representation_dto.py         |  55 ++++
 .../tests/unittests/dto/rel/test_view_dto.py       | 293 +++++++++++++++++++++
 .../unittests/dto/util/test_dto_converters.py      |  52 ++++
 .../tests/unittests/test_generic_view.py           |  61 +++++
 11 files changed, 868 insertions(+), 6 deletions(-)

diff --git a/clients/client-python/gravitino/client/generic_view.py 
b/clients/client-python/gravitino/client/generic_view.py
new file mode 100644
index 0000000000..ce4e2a765c
--- /dev/null
+++ b/clients/client-python/gravitino/client/generic_view.py
@@ -0,0 +1,63 @@
+# 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 Optional
+
+from gravitino.api.audit import Audit
+from gravitino.api.rel.column import Column
+from gravitino.api.rel.representation import Representation
+from gravitino.api.rel.view import View
+from gravitino.dto.rel.view_dto import ViewDTO
+
+
+class GenericView(View):
+    """A generic implementation of the View interface."""
+
+    def __init__(
+        self,
+        view_dto: ViewDTO,
+    ):
+        """Create a GenericView from a ViewDTO.
+
+        Args:
+            view_dto: The view DTO.
+        """
+        self._view_dto = view_dto
+
+    def name(self) -> str:
+        return self._view_dto.name()
+
+    def comment(self) -> Optional[str]:
+        return self._view_dto.comment()
+
+    def columns(self) -> list[Column]:
+        return self._view_dto.columns()
+
+    def representations(self) -> list[Representation]:
+        return self._view_dto.representations()
+
+    def default_catalog(self) -> Optional[str]:
+        return self._view_dto.default_catalog()
+
+    def default_schema(self) -> Optional[str]:
+        return self._view_dto.default_schema()
+
+    def properties(self) -> dict[str, str]:
+        return self._view_dto.properties()
+
+    def audit_info(self) -> Audit:
+        return self._view_dto.audit_info()
diff --git 
a/clients/client-python/gravitino/dto/rel/json_serdes/representation_serdes.py 
b/clients/client-python/gravitino/dto/rel/json_serdes/representation_serdes.py
new file mode 100644
index 0000000000..1414633df2
--- /dev/null
+++ 
b/clients/client-python/gravitino/dto/rel/json_serdes/representation_serdes.py
@@ -0,0 +1,49 @@
+# 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 Optional
+
+from gravitino.api.rel.representation import Representation
+from gravitino.dto.rel.representation_dto import RepresentationDTO
+from gravitino.dto.rel.sql_representation_dto import SQLRepresentationDTO
+from gravitino.exceptions.base import IllegalArgumentException
+from gravitino.utils.precondition import Precondition
+
+
+class RepresentationSerdes:
+    """Serdes for view representation DTOs."""
+
+    @staticmethod
+    def serialize(value: Optional[RepresentationDTO]) -> Optional[dict]:
+        """Encode a representation DTO to a dictionary."""
+        if value is None:
+            return None
+        return value.to_dict()
+
+    @staticmethod
+    def deserialize(value: Optional[dict]) -> Optional[RepresentationDTO]:
+        """Decode a representation DTO from a dictionary."""
+        if value is None:
+            return None
+        Precondition.check_argument(
+            isinstance(value, dict) and len(value) > 0,
+            f"Cannot parse representation from invalid JSON: {value}",
+        )
+        if value.get("type") == Representation.TYPE_SQL:
+            return SQLRepresentationDTO.from_dict(value)
+        raise IllegalArgumentException(
+            f"Unsupported representation type: {value.get('type')}"
+        )
diff --git a/clients/client-python/gravitino/dto/rel/representation_dto.py 
b/clients/client-python/gravitino/dto/rel/representation_dto.py
new file mode 100644
index 0000000000..dab099399a
--- /dev/null
+++ b/clients/client-python/gravitino/dto/rel/representation_dto.py
@@ -0,0 +1,30 @@
+# 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 abc import abstractmethod
+
+from dataclasses_json import DataClassJsonMixin
+
+from gravitino.api.rel.representation import Representation
+
+
+class RepresentationDTO(Representation, DataClassJsonMixin):
+    """DTO base class for view representations."""
+
+    @abstractmethod
+    def validate(self) -> None:
+        """Validates the representation DTO."""
diff --git a/clients/client-python/gravitino/dto/rel/sql_representation_dto.py 
b/clients/client-python/gravitino/dto/rel/sql_representation_dto.py
new file mode 100644
index 0000000000..45f7974b33
--- /dev/null
+++ b/clients/client-python/gravitino/dto/rel/sql_representation_dto.py
@@ -0,0 +1,58 @@
+# 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 dataclasses import dataclass, field
+
+from dataclasses_json import config
+
+from gravitino.api.rel.representation import Representation
+from gravitino.dto.rel.representation_dto import RepresentationDTO
+from gravitino.utils.precondition import Precondition
+
+
+@dataclass
+class SQLRepresentationDTO(RepresentationDTO):
+    """DTO for SQL view representations."""
+
+    _dialect: str = field(metadata=config(field_name="dialect"))
+    _sql: str = field(metadata=config(field_name="sql"))
+    _type: str = field(
+        default=Representation.TYPE_SQL, metadata=config(field_name="type")
+    )
+
+    def type(self) -> str:
+        """Returns the representation type."""
+        return self._type
+
+    def dialect(self) -> str:
+        """Returns the SQL dialect."""
+        return self._dialect
+
+    def sql(self) -> str:
+        """Returns the SQL text."""
+        return self._sql
+
+    def validate(self) -> None:
+        Precondition.check_string_not_empty(
+            self._dialect, '"dialect" field is required and cannot be empty'
+        )
+        Precondition.check_string_not_empty(
+            self._sql, '"sql" field is required and cannot be empty'
+        )
+
+    def __hash__(self) -> int:
+        return hash((self._type, self._dialect, self._sql))
diff --git a/clients/client-python/gravitino/dto/rel/view_dto.py 
b/clients/client-python/gravitino/dto/rel/view_dto.py
new file mode 100644
index 0000000000..3606217cab
--- /dev/null
+++ b/clients/client-python/gravitino/dto/rel/view_dto.py
@@ -0,0 +1,105 @@
+# 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 dataclasses import dataclass, field
+from typing import Optional
+
+from dataclasses_json import DataClassJsonMixin, config
+
+from gravitino.api.rel.column import Column
+from gravitino.api.rel.representation import Representation
+from gravitino.api.rel.view import View
+from gravitino.dto.audit_dto import AuditDTO
+from gravitino.dto.rel.column_dto import ColumnDTO
+from gravitino.dto.rel.json_serdes.representation_serdes import 
RepresentationSerdes
+from gravitino.dto.rel.representation_dto import RepresentationDTO
+from gravitino.dto.util.dto_converters import DTOConverters
+from gravitino.utils.precondition import Precondition
+
+
+@dataclass
+class ViewDTO(View, DataClassJsonMixin):  # pylint: 
disable=too-many-instance-attributes
+    """Represents a View DTO."""
+
+    _name: str = field(metadata=config(field_name="name"))
+    _columns: Optional[list[ColumnDTO]] = field(
+        default=None, metadata=config(field_name="columns")
+    )
+    _representations: Optional[list[RepresentationDTO]] = field(
+        default=None,
+        metadata=config(
+            field_name="representations",
+            encoder=lambda items: [
+                RepresentationSerdes.serialize(item) for item in items
+            ],
+            decoder=lambda values: [
+                RepresentationSerdes.deserialize(value) for value in values
+            ],
+        ),
+    )
+    _audit: Optional[AuditDTO] = field(
+        default=None, metadata=config(field_name="audit")
+    )
+    _comment: Optional[str] = field(default=None, 
metadata=config(field_name="comment"))
+    _default_catalog: Optional[str] = field(
+        default=None, metadata=config(field_name="defaultCatalog")
+    )
+    _default_schema: Optional[str] = field(
+        default=None, metadata=config(field_name="defaultSchema")
+    )
+    _properties: Optional[dict[str, str]] = field(
+        default=None, metadata=config(field_name="properties")
+    )
+
+    def __post_init__(self):
+        if self._columns is None:
+            self._columns = []
+        Precondition.check_string_not_empty(self._name, "name cannot be null 
or empty")
+        Precondition.check_argument(self._audit is not None, "audit cannot be 
null")
+        Precondition.check_argument(
+            self._representations is not None and len(self._representations) > 
0,
+            "representations cannot be null or empty",
+        )
+        for representation in self._representations:
+            Precondition.check_argument(
+                representation is not None, "representation must not be null"
+            )
+            representation.validate()
+
+    def name(self) -> str:
+        return self._name
+
+    def comment(self) -> Optional[str]:
+        return self._comment
+
+    def columns(self) -> list[Column]:
+        return self._columns
+
+    def representations(self) -> list[Representation]:
+        return DTOConverters.from_dtos(self._representations)
+
+    def default_catalog(self) -> Optional[str]:
+        return self._default_catalog
+
+    def default_schema(self) -> Optional[str]:
+        return self._default_schema
+
+    def properties(self) -> dict[str, str]:
+        return self._properties or {}
+
+    def audit_info(self) -> AuditDTO:
+        return self._audit
diff --git a/clients/client-python/gravitino/dto/util/dto_converters.py 
b/clients/client-python/gravitino/dto/util/dto_converters.py
index 1aa128e179..9aedba7f40 100644
--- a/clients/client-python/gravitino/dto/util/dto_converters.py
+++ b/clients/client-python/gravitino/dto/util/dto_converters.py
@@ -41,6 +41,8 @@ from gravitino.api.rel.partitions.identity_partition import 
IdentityPartition
 from gravitino.api.rel.partitions.list_partition import ListPartition
 from gravitino.api.rel.partitions.partition import Partition
 from gravitino.api.rel.partitions.range_partition import RangePartition
+from gravitino.api.rel.representation import Representation
+from gravitino.api.rel.sql_representation import SQLRepresentation
 from gravitino.api.rel.table import Table
 from gravitino.api.rel.table_change import (
     AddColumn,
@@ -87,7 +89,9 @@ from gravitino.dto.rel.partitions.identity_partition_dto 
import IdentityPartitio
 from gravitino.dto.rel.partitions.list_partition_dto import ListPartitionDTO
 from gravitino.dto.rel.partitions.partition_dto import PartitionDTO
 from gravitino.dto.rel.partitions.range_partition_dto import RangePartitionDTO
+from gravitino.dto.rel.representation_dto import RepresentationDTO
 from gravitino.dto.rel.sort_order_dto import SortOrderDTO
+from gravitino.dto.rel.sql_representation_dto import SQLRepresentationDTO
 from gravitino.dto.rel.table_dto import TableDTO
 from gravitino.dto.requests.table_update_request import (
     TableUpdateRequest,
@@ -227,6 +231,19 @@ class DTOConverters:
             DTOConverters.from_function_arg(dto.default_value()),
         )
 
+    @from_dto.register
+    @staticmethod
+    def _(dto: SQLRepresentationDTO) -> SQLRepresentation:
+        """Converts a SQLRepresentationDTO to a SQLRepresentation.
+
+        Args:
+            dto (SQLRepresentationDTO): The SQLRepresentation DTO to be 
converted.
+
+        Returns:
+            SQLRepresentation: The SQLRepresentation.
+        """
+        return SQLRepresentation(_dialect=dto.dialect(), _sql=dto.sql())
+
     @from_dto.register
     @staticmethod
     def _(dto: Partitioning) -> Transform:  # pylint: 
disable=too-many-return-statements
@@ -375,22 +392,25 @@ class DTOConverters:
     @staticmethod
     def from_dtos(dtos: list[SortOrderDTO]) -> list[SortOrder]: ...  # pragma: 
no cover
 
+    @overload
+    @staticmethod
+    def from_dtos(
+        dtos: list[RepresentationDTO],
+    ) -> list[Representation]: ...  # pragma: no cover
+
     @overload
     @staticmethod
     def from_dtos(dtos: list[Partitioning]) -> list[Transform]: ...  # pragma: 
no cover
 
     @staticmethod
     def from_dtos(dtos):
-        """Converts list of `ColumnDTO`, `IndexDTO`, `SortOrderDTO`, or 
`Partitioning`
-        to the corresponding list of `Column`s, `Index`es, `SortOrder`s, or 
`Transform`s.
+        """Converts a list of DTOs to the corresponding API objects.
 
         Args:
-            dtos (list[ColumnDTO] | list[IndexDTO] | list[SortOrderDTO] | 
list[Partitioning]):
-                The DTOs to be converted.
+            dtos: The DTOs to be converted.
 
         Returns:
-            list[Column] | list[Index] | list[SortOrder] | list[Transform]:
-                The list of Columns, Indexes, SortOrders, or Transforms 
depends on the input DTOs.
+            The converted objects.
         """
         if not dtos:
             return []
@@ -557,6 +577,16 @@ class DTOConverters:
             field_names=obj.field_names(),
         )
 
+    @to_dto.register
+    @staticmethod
+    def _(obj: RepresentationDTO) -> RepresentationDTO:
+        return obj
+
+    @to_dto.register
+    @staticmethod
+    def _(obj: SQLRepresentation) -> SQLRepresentationDTO:
+        return SQLRepresentationDTO(_dialect=obj.dialect(), _sql=obj.sql())
+
     @to_dto.register
     @staticmethod
     def _(obj: Partitioning) -> Partitioning:
@@ -646,6 +676,18 @@ class DTOConverters:
     @staticmethod
     def to_dtos(dtos: list[SortOrderDTO]) -> list[SortOrderDTO]: ...  # 
pragma: no cover
 
+    @overload
+    @staticmethod
+    def to_dtos(
+        dtos: list[Representation],
+    ) -> list[RepresentationDTO]: ...  # pragma: no cover
+
+    @overload
+    @staticmethod
+    def to_dtos(
+        dtos: list[RepresentationDTO],
+    ) -> list[RepresentationDTO]: ...  # pragma: no cover
+
     @overload
     @staticmethod
     def to_dtos(dtos: list[Transform]) -> list[Partitioning]: ...  # pragma: 
no cover
diff --git 
a/clients/client-python/tests/unittests/dto/rel/test_representation_serdes.py 
b/clients/client-python/tests/unittests/dto/rel/test_representation_serdes.py
new file mode 100644
index 0000000000..e75fc0829c
--- /dev/null
+++ 
b/clients/client-python/tests/unittests/dto/rel/test_representation_serdes.py
@@ -0,0 +1,54 @@
+# 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 unittest
+
+from gravitino.api.rel.dialects import Dialects
+from gravitino.api.rel.representation import Representation
+from gravitino.dto.rel.json_serdes.representation_serdes import 
RepresentationSerdes
+from gravitino.dto.rel.sql_representation_dto import SQLRepresentationDTO
+from gravitino.exceptions.base import IllegalArgumentException
+
+
+class TestRepresentationSerdes(unittest.TestCase):
+    def test_representation_serdes(self):
+        representation = SQLRepresentationDTO(
+            _dialect=Dialects.TRINO, _sql="SELECT id FROM table"
+        )
+        serialized = RepresentationSerdes.serialize(representation)
+
+        self.assertEqual(Representation.TYPE_SQL, serialized["type"])
+        self.assertEqual(Dialects.TRINO, serialized["dialect"])
+        self.assertEqual("SELECT id FROM table", serialized["sql"])
+        self.assertEqual(representation, 
RepresentationSerdes.deserialize(serialized))
+
+    def test_representation_serdes_none(self):
+        self.assertIsNone(RepresentationSerdes.serialize(None))
+        self.assertIsNone(RepresentationSerdes.deserialize(None))
+
+    def test_representation_serdes_invalid_json(self):
+        for invalid_json in ("invalid", {}):
+            with self.subTest(invalid_json=invalid_json):
+                with self.assertRaisesRegex(
+                    IllegalArgumentException,
+                    "Cannot parse representation from invalid JSON",
+                ):
+                    RepresentationSerdes.deserialize(invalid_json)
+
+    def test_representation_serdes_unknown_type(self):
+        with self.assertRaises(IllegalArgumentException):
+            RepresentationSerdes.deserialize({"type": "unknown"})
diff --git 
a/clients/client-python/tests/unittests/dto/rel/test_sql_representation_dto.py 
b/clients/client-python/tests/unittests/dto/rel/test_sql_representation_dto.py
new file mode 100644
index 0000000000..7cdcb85d09
--- /dev/null
+++ 
b/clients/client-python/tests/unittests/dto/rel/test_sql_representation_dto.py
@@ -0,0 +1,55 @@
+# 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 unittest
+
+from gravitino.api.rel.dialects import Dialects
+from gravitino.api.rel.representation import Representation
+from gravitino.dto.rel.sql_representation_dto import SQLRepresentationDTO
+
+
+class TestSQLRepresentationDTO(unittest.TestCase):
+    def test_sql_representation_dto(self):
+        representation = SQLRepresentationDTO(
+            _dialect=Dialects.TRINO, _sql="SELECT id FROM table"
+        )
+
+        self.assertEqual(Representation.TYPE_SQL, representation.type())
+        self.assertEqual(Dialects.TRINO, representation.dialect())
+        self.assertEqual("SELECT id FROM table", representation.sql())
+        self.assertEqual(
+            representation,
+            SQLRepresentationDTO(_dialect=Dialects.TRINO, _sql="SELECT id FROM 
table"),
+        )
+        self.assertEqual(
+            hash(representation),
+            hash(
+                SQLRepresentationDTO(
+                    _dialect=Dialects.TRINO, _sql="SELECT id FROM table"
+                )
+            ),
+        )
+        self.assertNotEqual(
+            representation,
+            SQLRepresentationDTO(_dialect=Dialects.SPARK, _sql="SELECT id FROM 
table"),
+        )
+
+    def test_sql_representation_dto_validate(self):
+        with self.assertRaises(ValueError):
+            SQLRepresentationDTO(_dialect="", _sql="SELECT 1").validate()
+        with self.assertRaises(ValueError):
+            SQLRepresentationDTO(_dialect=Dialects.TRINO, _sql="").validate()
diff --git a/clients/client-python/tests/unittests/dto/rel/test_view_dto.py 
b/clients/client-python/tests/unittests/dto/rel/test_view_dto.py
new file mode 100644
index 0000000000..c568646594
--- /dev/null
+++ b/clients/client-python/tests/unittests/dto/rel/test_view_dto.py
@@ -0,0 +1,293 @@
+# 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 unittest
+
+from gravitino.api.rel.dialects import Dialects
+from gravitino.api.rel.sql_representation import SQLRepresentation
+from gravitino.api.rel.types.types import Types
+from gravitino.dto.audit_dto import AuditDTO
+from gravitino.dto.rel.column_dto import ColumnDTO
+from gravitino.dto.rel.sql_representation_dto import SQLRepresentationDTO
+from gravitino.dto.rel.view_dto import ViewDTO
+
+
+class TestViewDTO(unittest.TestCase):
+    @staticmethod
+    def _column(name: str = "id") -> ColumnDTO:
+        return ColumnDTO(
+            _name=name,
+            _data_type=Types.IntegerType.get(),
+            _comment="column comment",
+            _nullable=False,
+        )
+
+    @staticmethod
+    def _representation(
+        dialect: str = Dialects.TRINO, sql: str = "SELECT id FROM table"
+    ) -> SQLRepresentationDTO:
+        return SQLRepresentationDTO(_dialect=dialect, _sql=sql)
+
+    def _view_dto(
+        self,
+        name: str = "test_view",
+        columns: list[ColumnDTO] = None,
+        representations: list[SQLRepresentationDTO] = None,
+        comment: str = "view comment",
+        properties: dict[str, str] = None,
+    ) -> ViewDTO:
+        return ViewDTO(
+            _name=name,
+            _columns=columns if columns is not None else [self._column()],
+            _representations=(
+                representations
+                if representations is not None
+                else [self._representation()]
+            ),
+            _comment=comment,
+            _default_catalog="catalog",
+            _default_schema="schema",
+            _properties=properties if properties is not None else {"k1": "v1"},
+            _audit=AuditDTO(
+                "creator", "2022-01-01T00:00:00Z", "modifier", 
"2022-01-02T00:00:00Z"
+            ),
+        )
+
+    def test_view_dto_serialization(self):
+        view = self._view_dto()
+
+        deserialized = ViewDTO.from_json(view.to_json())
+
+        self.assertEqual(view, deserialized)
+        self.assertEqual("test_view", deserialized.name())
+        self.assertEqual("view comment", deserialized.comment())
+        self.assertEqual("catalog", deserialized.default_catalog())
+        self.assertEqual("schema", deserialized.default_schema())
+        self.assertEqual({"k1": "v1"}, deserialized.properties())
+        self.assertEqual("creator", deserialized.audit_info().creator())
+        self.assertEqual(1, len(deserialized.columns()))
+        self.assertEqual(1, len(deserialized.representations()))
+        self.assertIsInstance(deserialized.representations()[0], 
SQLRepresentation)
+        self.assertEqual(
+            "SELECT id FROM table",
+            deserialized.representations()[0].sql(),
+        )
+
+    def test_view_dto_deserialize_from_raw_json(self):
+        raw_json = """
+        {
+            "name": "raw_view",
+            "comment": "raw comment",
+            "columns": [
+                {
+                    "name": "id",
+                    "type": "integer",
+                    "comment": "id column",
+                    "nullable": false,
+                    "autoIncrement": false
+                }
+            ],
+            "representations": [
+                {
+                    "type": "sql",
+                    "dialect": "spark",
+                    "sql": "SELECT id FROM table"
+                }
+            ],
+            "defaultCatalog": "catalog",
+            "defaultSchema": "schema",
+            "properties": {
+                "k1": "v1"
+            },
+            "audit": {
+                "creator": "creator"
+            }
+        }
+        """
+
+        view = ViewDTO.from_json(raw_json)
+
+        self.assertEqual("raw_view", view.name())
+        self.assertEqual("raw comment", view.comment())
+        self.assertEqual("catalog", view.default_catalog())
+        self.assertEqual("schema", view.default_schema())
+        self.assertEqual({"k1": "v1"}, view.properties())
+        self.assertEqual("creator", view.audit_info().creator())
+        self.assertEqual(1, len(view.columns()))
+        self.assertEqual("id", view.columns()[0].name())
+        self.assertEqual(1, len(view.representations()))
+        self.assertIsInstance(view.representations()[0], SQLRepresentation)
+        self.assertEqual("spark", view.representations()[0].dialect())
+        self.assertEqual("SELECT id FROM table", 
view.representations()[0].sql())
+
+    def test_view_dto_defaults(self):
+        view = ViewDTO(
+            _name="test_view",
+            _representations=[self._representation()],
+            _audit=AuditDTO("creator"),
+        )
+
+        self.assertEqual([], view.columns())
+        self.assertEqual([], view.to_dict()["columns"])
+        self.assertEqual({}, view.properties())
+        self.assertIsNone(view.comment())
+        self.assertIsNone(view.default_catalog())
+        self.assertIsNone(view.default_schema())
+
+    def test_view_dto_deserialize_invalid_name(self):
+        raw_json = """
+        {
+            "name": "",
+            "representations": [
+                {
+                    "type": "sql",
+                    "dialect": "spark",
+                    "sql": "SELECT 1"
+                }
+            ],
+            "audit": {
+                "creator": "creator"
+            }
+        }
+        """
+
+        with self.assertRaisesRegex(ValueError, "name cannot be null or 
empty"):
+            ViewDTO.from_json(raw_json)
+
+    def test_view_dto_deserialize_missing_audit(self):
+        raw_json = """
+        {
+            "name": "test_view",
+            "representations": [
+                {
+                    "type": "sql",
+                    "dialect": "spark",
+                    "sql": "SELECT 1"
+                }
+            ]
+        }
+        """
+
+        with self.assertRaisesRegex(ValueError, "audit cannot be null"):
+            ViewDTO.from_json(raw_json)
+
+    def test_view_dto_deserialize_missing_representations(self):
+        raw_json = """
+        {
+            "name": "test_view",
+            "audit": {
+                "creator": "creator"
+            }
+        }
+        """
+
+        with self.assertRaisesRegex(
+            ValueError, "representations cannot be null or empty"
+        ):
+            ViewDTO.from_json(raw_json)
+
+    def test_view_dto_deserialize_invalid_representations(self):
+        raw_json_strings = [
+            """
+            {
+                "name": "test_view",
+                "representations": [],
+                "audit": {
+                    "creator": "creator"
+                }
+            }
+            """,
+            """
+            {
+                "name": "test_view",
+                "representations": [null],
+                "audit": {
+                    "creator": "creator"
+                }
+            }
+            """,
+            """
+            {
+                "name": "test_view",
+                "representations": [
+                    {
+                        "type": "sql",
+                        "dialect": "",
+                        "sql": "SELECT 1"
+                    }
+                ],
+                "audit": {
+                    "creator": "creator"
+                }
+            }
+            """,
+            """
+            {
+                "name": "test_view",
+                "representations": [
+                    {
+                        "type": "sql",
+                        "dialect": "spark",
+                        "sql": ""
+                    }
+                ],
+                "audit": {
+                    "creator": "creator"
+                }
+            }
+            """,
+        ]
+
+        for raw_json in raw_json_strings:
+            with self.assertRaises(ValueError):
+                ViewDTO.from_json(raw_json)
+
+    def test_view_dto_validate_constructor_arguments(self):
+        with self.assertRaises(ValueError):
+            ViewDTO(
+                _name="",
+                _representations=[self._representation()],
+                _audit=AuditDTO("creator"),
+            )
+        with self.assertRaises(ValueError):
+            ViewDTO(_name="test_view", _representations=[], 
_audit=AuditDTO("creator"))
+        with self.assertRaises(ValueError):
+            ViewDTO(
+                _name="test_view", _representations=[None], 
_audit=AuditDTO("creator")
+            )
+        with self.assertRaises(ValueError):
+            ViewDTO(
+                _name="test_view",
+                _representations=[self._representation()],
+                _audit=None,
+            )
+
+    def test_view_dto_sql_for(self):
+        view = self._view_dto(
+            representations=[
+                self._representation(Dialects.TRINO, "SELECT 1"),
+                self._representation(Dialects.SPARK, "SELECT 2"),
+            ]
+        )
+        deserialized = ViewDTO.from_json(view.to_json())
+
+        self.assertEqual("SELECT 1", deserialized.sql_for("trino").sql())
+        self.assertEqual("SELECT 1", deserialized.sql_for("TRINO").sql())
+        self.assertEqual("SELECT 1", deserialized.sql_for("Trino").sql())
+        self.assertEqual("SELECT 2", 
deserialized.sql_for(Dialects.SPARK).sql())
+        self.assertIsNone(deserialized.sql_for("hive"))
+        self.assertIsNone(deserialized.sql_for(None))
diff --git 
a/clients/client-python/tests/unittests/dto/util/test_dto_converters.py 
b/clients/client-python/tests/unittests/dto/util/test_dto_converters.py
index 3918be7658..fb7a0dd492 100644
--- a/clients/client-python/tests/unittests/dto/util/test_dto_converters.py
+++ b/clients/client-python/tests/unittests/dto/util/test_dto_converters.py
@@ -23,6 +23,7 @@ from typing import Dict, cast
 from unittest.mock import MagicMock, patch
 
 from gravitino.api.rel.column import Column
+from gravitino.api.rel.dialects import Dialects
 from gravitino.api.rel.expressions.distributions.distributions import 
Distributions
 from gravitino.api.rel.expressions.distributions.strategy import Strategy
 from gravitino.api.rel.expressions.expression import Expression
@@ -41,6 +42,7 @@ from gravitino.api.rel.indexes.indexes import Indexes
 from gravitino.api.rel.partitions.partition import Partition
 from gravitino.api.rel.partitions.partitions import Partitions
 from gravitino.api.rel.table import Table
+from gravitino.api.rel.sql_representation import SQLRepresentation
 from gravitino.api.rel.types.types import Types
 from gravitino.dto.rel.column_dto import ColumnDTO
 from gravitino.dto.rel.distribution_dto import DistributionDTO
@@ -71,6 +73,7 @@ from gravitino.dto.rel.partitions.identity_partition_dto 
import IdentityPartitio
 from gravitino.dto.rel.partitions.list_partition_dto import ListPartitionDTO
 from gravitino.dto.rel.partitions.range_partition_dto import RangePartitionDTO
 from gravitino.dto.rel.sort_order_dto import SortOrderDTO
+from gravitino.dto.rel.sql_representation_dto import SQLRepresentationDTO
 from gravitino.dto.rel.table_dto import TableDTO
 from gravitino.dto.util.dto_converters import DTOConverters
 from gravitino.exceptions.base import IllegalArgumentException
@@ -261,6 +264,16 @@ class TestDTOConverters(unittest.TestCase):
         converted = DTOConverters.from_dto(sort_order_dto)
         self.assertTrue(converted == expected)
 
+    def test_from_dto_sql_representation(self):
+        representation_dto = SQLRepresentationDTO(Dialects.TRINO, "SELECT 1")
+
+        converted = DTOConverters.from_dto(representation_dto)
+
+        self.assertEqual(
+            SQLRepresentation(Dialects.TRINO, "SELECT 1"),
+            converted,
+        )
+
     def test_from_dto_column(self):
         column_name = "test_column"
         column_data_type = Types.IntegerType.get()
@@ -412,6 +425,20 @@ class TestDTOConverters(unittest.TestCase):
         converted = DTOConverters.from_dtos(sort_order_dtos)
         self.assertListEqual(converted, expected)
 
+    def test_from_dtos_representations(self):
+        representation_dtos = [
+            SQLRepresentationDTO(Dialects.TRINO, "SELECT 1"),
+            SQLRepresentationDTO(Dialects.SPARK, "SELECT 2"),
+        ]
+        expected = [
+            SQLRepresentation(Dialects.TRINO, "SELECT 1"),
+            SQLRepresentation(Dialects.SPARK, "SELECT 2"),
+        ]
+
+        converted = DTOConverters.from_dtos(representation_dtos)
+
+        self.assertListEqual(converted, expected)
+
     def test_from_dtos_partitioning(self):
         field_name = ["score"]
         field_names = [field_name]
@@ -543,6 +570,16 @@ class TestDTOConverters(unittest.TestCase):
         with self.assertRaisesRegex(IllegalArgumentException, "Unsupported 
type"):
             DTOConverters.to_dto("test")
 
+    def test_to_dto_sql_representation(self):
+        representation = SQLRepresentation(Dialects.TRINO, "SELECT 1")
+
+        dto = DTOConverters.to_dto(representation)
+
+        self.assertIsInstance(dto, SQLRepresentationDTO)
+        self.assertEqual(Dialects.TRINO, dto.dialect())
+        self.assertEqual("SELECT 1", dto.sql())
+        self.assertIs(dto, DTOConverters.to_dto(dto))
+
     def test_to_dto_range_partition(self):
         converted = DTOConverters.to_dto(
             Partitions.range(
@@ -733,6 +770,21 @@ class TestDTOConverters(unittest.TestCase):
 
         self.assertListEqual(DTOConverters.to_dtos(converted_dtos), 
converted_dtos)
 
+    def test_to_dtos_representations(self):
+        representations = [
+            SQLRepresentation(Dialects.TRINO, "SELECT 1"),
+            SQLRepresentation(Dialects.SPARK, "SELECT 2"),
+        ]
+        expected_dtos = [
+            SQLRepresentationDTO(Dialects.TRINO, "SELECT 1"),
+            SQLRepresentationDTO(Dialects.SPARK, "SELECT 2"),
+        ]
+
+        converted_dtos = DTOConverters.to_dtos(representations)
+
+        self.assertListEqual(converted_dtos, expected_dtos)
+        self.assertListEqual(DTOConverters.to_dtos(converted_dtos), 
converted_dtos)
+
     def test_to_dtos_single_field_transforms(self):
         converted_dtos = 
DTOConverters.to_dtos(self.single_field_transforms.values())
         for key, converted in zip(self.single_field_transforms.keys(), 
converted_dtos):
diff --git a/clients/client-python/tests/unittests/test_generic_view.py 
b/clients/client-python/tests/unittests/test_generic_view.py
new file mode 100644
index 0000000000..a07c15be05
--- /dev/null
+++ b/clients/client-python/tests/unittests/test_generic_view.py
@@ -0,0 +1,61 @@
+# 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 unittest
+
+from gravitino.api.rel.dialects import Dialects
+from gravitino.client.generic_view import GenericView
+from gravitino.dto.audit_dto import AuditDTO
+from gravitino.dto.rel.sql_representation_dto import SQLRepresentationDTO
+from gravitino.dto.rel.view_dto import ViewDTO
+
+
+class TestGenericView(unittest.TestCase):
+    def _generic_view(
+        self,
+        name: str = "demo_view",
+        comment: str = "comment",
+        sql: str = "SELECT 1",
+        default_catalog: str = "demo_catalog",
+        default_schema: str = "demo_schema",
+    ) -> GenericView:
+        return GenericView(
+            ViewDTO(
+                _name=name,
+                _representations=[
+                    SQLRepresentationDTO(_dialect=Dialects.TRINO, _sql=sql)
+                ],
+                _comment=comment,
+                _default_catalog=default_catalog,
+                _default_schema=default_schema,
+                _properties={"key": "value"},
+                _audit=AuditDTO("creator"),
+            ),
+        )
+
+    def test_generic_view(self) -> None:
+        generic_view = self._generic_view()
+
+        self.assertEqual("demo_view", generic_view.name())
+        self.assertEqual("comment", generic_view.comment())
+        self.assertEqual("demo_catalog", generic_view.default_catalog())
+        self.assertEqual("demo_schema", generic_view.default_schema())
+        self.assertEqual({"key": "value"}, generic_view.properties())
+        self.assertEqual(0, len(generic_view.columns()))
+        self.assertEqual(1, len(generic_view.representations()))
+        self.assertEqual("SELECT 1", 
generic_view.sql_for(Dialects.TRINO).sql())
+        self.assertEqual("creator", generic_view.audit_info().creator())


Reply via email to