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

jshao 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 bcb8eb126e [#9386] feat(python-client): Add minimum viable integration 
tests for relational table (#9396)
bcb8eb126e is described below

commit bcb8eb126ec77d829416272b66d29f74423a3a64
Author: George T. C. Lai <[email protected]>
AuthorDate: Thu Dec 25 10:55:21 2025 +0800

    [#9386] feat(python-client): Add minimum viable integration tests for 
relational table (#9396)
    
    ### What changes were proposed in this pull request?
    
    We shall add integration tests for the least implementation of
    relational table.
    
    ### Why are the changes needed?
    
    This PR added the following classes.
    
    - `TableCreateRequest`
    - `TableResponse`
    - integration tests of partition operations for relational table.
    
    Fix: #9386
    
    ### Does this PR introduce _any_ user-facing change?
    
    No
    
    ### How was this patch tested?
    
    Unit tests and integration tests
    
    ---------
    
    Signed-off-by: George T. C. Lai <[email protected]>
---
 .../client-python/gravitino/dto/rel/column_dto.py  |   2 +-
 .../dto/requests/add_partitions_request.py         |  15 ++-
 clients/client-python/gravitino/rest/rest_utils.py |   3 +-
 .../tests/integration/test_relational_table.py     | 150 +++++++++++++++++++++
 .../client-python/tests/unittests/test_requests.py |  59 +++++++-
 5 files changed, 222 insertions(+), 7 deletions(-)

diff --git a/clients/client-python/gravitino/dto/rel/column_dto.py 
b/clients/client-python/gravitino/dto/rel/column_dto.py
index 521348c7c0..87e30b14a4 100644
--- a/clients/client-python/gravitino/dto/rel/column_dto.py
+++ b/clients/client-python/gravitino/dto/rel/column_dto.py
@@ -50,7 +50,7 @@ class ColumnDTO(Column, DataClassJsonMixin):
     )
     """The data type of the column."""
 
-    _comment: str = field(metadata=config(field_name="comment"))
+    _comment: Optional[str] = field(default=None, 
metadata=config(field_name="comment"))
     """The comment associated with the column."""
 
     _default_value: Optional[Union[Expression, List[Expression]]] = field(
diff --git 
a/clients/client-python/gravitino/dto/requests/add_partitions_request.py 
b/clients/client-python/gravitino/dto/requests/add_partitions_request.py
index e54b9f9200..bbf450d9f6 100644
--- a/clients/client-python/gravitino/dto/requests/add_partitions_request.py
+++ b/clients/client-python/gravitino/dto/requests/add_partitions_request.py
@@ -19,6 +19,9 @@ from dataclasses import dataclass, field
 
 from dataclasses_json import config
 
+from gravitino.dto.rel.partitions.json_serdes.partition_dto_serdes import (
+    PartitionDTOSerdes,
+)
 from gravitino.dto.rel.partitions.partition_dto import PartitionDTO
 from gravitino.rest.rest_message import RESTRequest
 from gravitino.utils.precondition import Precondition
@@ -28,7 +31,17 @@ from gravitino.utils.precondition import Precondition
 class AddPartitionsRequest(RESTRequest):
     """Request to add partitions to a table."""
 
-    _partitions: list[PartitionDTO] = 
field(metadata=config(field_name="partitions"))
+    _partitions: list[PartitionDTO] = field(
+        metadata=config(
+            field_name="partitions",
+            encoder=lambda items: [
+                PartitionDTOSerdes.serialize(item) for item in items
+            ],
+            decoder=lambda values: [
+                PartitionDTOSerdes.deserialize(value) for value in values
+            ],
+        )
+    )
 
     def validate(self):
         Precondition.check_argument(
diff --git a/clients/client-python/gravitino/rest/rest_utils.py 
b/clients/client-python/gravitino/rest/rest_utils.py
index 58c4d55b7a..a760f36756 100644
--- a/clients/client-python/gravitino/rest/rest_utils.py
+++ b/clients/client-python/gravitino/rest/rest_utils.py
@@ -16,6 +16,7 @@
 # under the License.
 
 import urllib.parse
+
 from gravitino.exceptions.base import IllegalArgumentException
 
 
@@ -23,4 +24,4 @@ def encode_string(to_encode: str):
     if to_encode is None:
         raise IllegalArgumentException("Invalid string to encode: None")
 
-    return urllib.parse.quote(to_encode, encoding="utf-8")
+    return urllib.parse.quote(to_encode, safe="", encoding="utf-8")
diff --git a/clients/client-python/tests/integration/test_relational_table.py 
b/clients/client-python/tests/integration/test_relational_table.py
new file mode 100644
index 0000000000..c696cdd374
--- /dev/null
+++ b/clients/client-python/tests/integration/test_relational_table.py
@@ -0,0 +1,150 @@
+# 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 logging
+from datetime import date
+from random import randint
+
+from gravitino import (
+    Catalog,
+    GravitinoAdminClient,
+    GravitinoClient,
+    NameIdentifier,
+)
+from gravitino.api.rel.expressions.literals.literals import Literals
+from gravitino.api.rel.partitions.partitions import Partitions
+from gravitino.api.rel.types.types import Types
+from gravitino.dto.rel.column_dto import ColumnDTO
+from gravitino.dto.rel.partitioning.identity_partitioning_dto import (
+    IdentityPartitioningDTO,
+)
+from tests.integration.containers.hdfs_container import HDFSContainer
+from tests.integration.integration_test_env import IntegrationTestEnv
+
+logger = logging.getLogger(__name__)
+
+
+class TestRelationalTable(IntegrationTestEnv):
+    METALAKE_NAME: str = "TestRelationalTable_metalake" + str(randint(1, 
10000))
+    CATALOG_NAME: str = "relational_catalog"
+    CATALOG_PROVIDER: str = "hive"
+    SCHEMA_NAME: str = "test_schema"
+    TABLE_NAME: str = "test_table"
+    TABLE_IDENT: NameIdentifier = NameIdentifier.of(SCHEMA_NAME, TABLE_NAME)
+
+    @classmethod
+    def setUpClass(cls):
+        super().setUpClass()
+        cls.hdfs_container: HDFSContainer = HDFSContainer()
+        hive_metastore_uri = f"thrift://{cls.hdfs_container.get_ip()}:9083"
+        logger.info("Started Hive container with metastore URI: %s", 
hive_metastore_uri)
+        cls.gravitino_admin_client = 
GravitinoAdminClient(uri="http://localhost:8090";)
+        cls.gravitino_admin_client.create_metalake(
+            cls.METALAKE_NAME,
+            comment="Test metalake for relational catalog",
+            properties={},
+        )
+        cls.gravitino_client: GravitinoClient = GravitinoClient(
+            uri="http://localhost:8090";, metalake_name=cls.METALAKE_NAME
+        )
+        cls.catalog = cls.gravitino_client.create_catalog(
+            name=cls.CATALOG_NAME,
+            catalog_type=Catalog.Type.RELATIONAL,
+            provider=cls.CATALOG_PROVIDER,
+            comment="Test relational catalog",
+            properties={"metastore.uris": hive_metastore_uri},
+        )
+        cls.schema = cls.catalog.as_schemas().create_schema(
+            schema_name=cls.SCHEMA_NAME,
+            comment="Test schema",
+            properties={},
+        )
+        cls.relational_catalog = cls.catalog.as_table_catalog()
+        cls.relational_table = cls.relational_catalog.create_table(
+            identifier=cls.TABLE_IDENT,
+            columns=[
+                ColumnDTO.builder()
+                .with_name("dt")
+                .with_data_type(Types.DateType.get())
+                .build(),
+                ColumnDTO.builder()
+                .with_name("country")
+                .with_data_type(Types.StringType.get())
+                .build(),
+            ],
+            partitioning=[
+                IdentityPartitioningDTO("dt"),
+                IdentityPartitioningDTO("country"),
+            ],
+        )
+
+    @classmethod
+    def tearDownClass(cls):
+        try:
+            cls.catalog.as_schemas().drop_schema(
+                schema_name=cls.SCHEMA_NAME, cascade=True
+            )
+            cls.gravitino_client.drop_catalog(name=cls.CATALOG_NAME, 
force=True)
+            cls.gravitino_admin_client.drop_metalake(name=cls.METALAKE_NAME, 
force=True)
+        except Exception as e:  # pylint: disable=broad-exception-caught
+            logger.warning("Failed to clean up class-level resources: %s", e)
+
+        # Clean up the HDFS/Hive container
+        if cls.hdfs_container:
+            try:
+                cls.hdfs_container.close()
+            except Exception as e:  # pylint: disable=broad-exception-caught
+                logger.warning("Failed to clean up HDFS container: %s", e)
+
+        super().tearDownClass()
+
+    def test_relational_table_partition_ops(self):
+        """Tests add/get/list/drop partition and list partition names of a 
relational table."""
+        relational_table = self.relational_catalog.load_table(self.TABLE_IDENT)
+
+        # Tests list partition names
+        partition_names = relational_table.list_partition_names()
+        self.assertEqual(len(partition_names), 0)
+
+        # Tests add partition
+        new_partition = relational_table.add_partition(
+            Partitions.identity(
+                name="dt=2025-12-03/country=us",
+                field_names=[["dt"], ["country"]],
+                values=[
+                    Literals.date_literal(date.fromisoformat("2025-12-03")),
+                    Literals.string_literal("us"),
+                ],
+            )
+        )
+        partition_names = relational_table.list_partition_names()
+        self.assertEqual(len(partition_names), 1)
+
+        # Tests list partitions
+        partitions = relational_table.list_partitions()
+        self.assertEqual(len(partitions), 1)
+        self.assertEqual(partitions[0], new_partition)
+
+        # Tests get partition
+        partition = relational_table.get_partition(new_partition.name())
+        self.assertEqual(new_partition, partition)
+
+        # Tests drop partition
+        result = relational_table.drop_partition(new_partition.name())
+        self.assertTrue(result)
+        partition_names = relational_table.list_partition_names()
+        self.assertEqual(len(partition_names), 0)
diff --git a/clients/client-python/tests/unittests/test_requests.py 
b/clients/client-python/tests/unittests/test_requests.py
index 66d799a71b..f3c2b19d1f 100644
--- a/clients/client-python/tests/unittests/test_requests.py
+++ b/clients/client-python/tests/unittests/test_requests.py
@@ -26,15 +26,66 @@ from gravitino.exceptions.base import 
IllegalArgumentException
 
 class TestRequests(unittest.TestCase):
     def test_add_partitions_request(self):
-        partitions = ["p202508_California"]
-        json_str = json.dumps({"partitions": partitions})
+        json_str = """
+            {
+                "partitions": [
+                    {
+                        "type": "identity",
+                        "name": "partition_1",
+                        "fieldNames": [["id"]],
+                        "values": [
+                            {
+                                "type": "literal",
+                                "dataType": "integer",
+                                "value": "0"
+                            }
+                        ],
+                        "properties": {
+                            "key1": "value1",
+                            "key2": "value2"
+                        }
+                    }
+                ]
+            }
+        """
+        partitions = json.loads(json_str)
         req = AddPartitionsRequest.from_json(json_str)
         req_dict = cast(dict, req.to_dict())
-        self.assertListEqual(req_dict["partitions"], partitions)
+        self.assertListEqual(req_dict["partitions"], partitions["partitions"])
 
+        multiple_partitions_json = """
+            {
+                "partitions": [
+                    {
+                        "type": "identity",
+                        "name": "partition_1",
+                        "fieldNames": [["id"]],
+                        "values": [
+                            {
+                                "type": "literal",
+                                "dataType": "integer",
+                                "value": "0"
+                            }
+                        ]
+                    },
+                    {
+                        "type": "identity",
+                        "name": "partition_2",
+                        "fieldNames": [["id"]],
+                        "values": [
+                            {
+                                "type": "literal",
+                                "dataType": "integer",
+                                "value": "1"
+                            }
+                        ]
+                    }
+                ]
+            }
+        """
         exceptions = {
             "partitions must not be null": '{"partitions": null}',
-            "Haven't yet implemented multiple partitions": '{"partitions": 
["p1", "p2"]}',
+            "Haven't yet implemented multiple partitions": 
multiple_partitions_json,
         }
         for exception_str, json_str in exceptions.items():
             with self.assertRaisesRegex(IllegalArgumentException, 
exception_str):

Reply via email to