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 2787179a4d [#5202] feat(client-python): Add ColumnDTO related classes 
(#7357)
2787179a4d is described below

commit 2787179a4dc876d1fc0e3a199334af5ae5757354
Author: George T. C. Lai <[email protected]>
AuthorDate: Tue Jun 24 14:02:33 2025 +0800

    [#5202] feat(client-python): Add ColumnDTO related classes (#7357)
    
    ### What changes were proposed in this pull request?
    
    This is the second part (totally 4 planned) of implementation to the
    following classes from Java to support Column and its default value,
    including:
    
    - ColumnDTO
    - LiteralDTO
    - FunctionArg
    - PartitionUtils
    
    We implemented the above four classes in one single PR since they have
    tight dependency to each other.
    
    **NOTE** that we haven't implemented the serdes for
    `ColumnDTO.default_value` which will be included in the future PR. Its
    serdes now are dummy ones to always serialize `ColumnDTO.default_value`
    to `None` and deserialize `ColiumnDTO.default_value` as
    `Column.DEFAULT_VALUE_NOT_SET`.
    
    ### Why are the changes needed?
    
    We need to support Column and its default value in python client.
    
    #5202
    
    ### Does this PR introduce _any_ user-facing change?
    
    No
    
    ### How was this patch tested?
    
    Unit tests
    
    ---------
    
    Signed-off-by: George T. C. Lai <[email protected]>
---
 .../client-python/gravitino/dto/rel/__init__.py    |  16 ++
 .../client-python/gravitino/dto/rel/column_dto.py  | 161 +++++++++++++++++
 .../gravitino/dto/rel/expressions/__init__.py      |  16 ++
 .../gravitino/dto/rel/expressions/function_arg.py  |  73 ++++++++
 .../gravitino/dto/rel/expressions/literal_dto.py   |  75 ++++++++
 .../gravitino/dto/rel/partition_utils.py           |  54 ++++++
 .../client-python/tests/unittests/dto/__init__.py  |  16 ++
 .../tests/unittests/dto/rel/__init__.py            |  16 ++
 .../tests/unittests/dto/rel/test_column_dto.py     | 195 +++++++++++++++++++++
 .../tests/unittests/dto/rel/test_function_arg.py   |  51 ++++++
 .../tests/unittests/dto/rel/test_literal_dto.py    |  48 +++++
 .../unittests/dto/rel/test_partition_utils.py      |  56 ++++++
 12 files changed, 777 insertions(+)

diff --git a/clients/client-python/gravitino/dto/rel/__init__.py 
b/clients/client-python/gravitino/dto/rel/__init__.py
new file mode 100644
index 0000000000..13a83393a9
--- /dev/null
+++ b/clients/client-python/gravitino/dto/rel/__init__.py
@@ -0,0 +1,16 @@
+# 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.
diff --git a/clients/client-python/gravitino/dto/rel/column_dto.py 
b/clients/client-python/gravitino/dto/rel/column_dto.py
new file mode 100644
index 0000000000..8fa38474ad
--- /dev/null
+++ b/clients/client-python/gravitino/dto/rel/column_dto.py
@@ -0,0 +1,161 @@
+# 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 __future__ import annotations
+
+from dataclasses import dataclass, field
+from typing import List, Optional, Union, cast
+
+from dataclasses_json import DataClassJsonMixin, config
+
+from gravitino.api.column import Column
+from gravitino.api.expressions.expression import Expression
+from gravitino.api.types.json_serdes.type_serdes import TypeSerdes
+from gravitino.api.types.type import Type
+from gravitino.api.types.types import Types
+from gravitino.dto.rel.expressions.literal_dto import LiteralDTO
+from gravitino.utils.precondition import Precondition
+
+
+@dataclass
+class ColumnDTO(Column, DataClassJsonMixin):
+    """Represents a Model DTO (Data Transfer Object)."""
+
+    _name: str = field(metadata=config(field_name="name"))
+    """The name of the column."""
+
+    _data_type: Type = field(
+        metadata=config(
+            field_name="type",
+            encoder=TypeSerdes.serialize,
+            decoder=TypeSerdes.deserialize,
+        )
+    )
+    """The data type of the column."""
+
+    _comment: str = field(metadata=config(field_name="comment"))
+    """The comment associated with the column."""
+
+    # TODO: We shall specify encoder/decoder in the future PR. They're now 
dummy serdes.
+    _default_value: Optional[Union[Expression, List[Expression]]] = field(
+        default_factory=lambda: Column.DEFAULT_VALUE_NOT_SET,
+        metadata=config(
+            field_name="defaultValue",
+            encoder=lambda _: None,
+            decoder=lambda _: Column.DEFAULT_VALUE_NOT_SET,
+            exclude=lambda value: value is None
+            or value is Column.DEFAULT_VALUE_NOT_SET,
+        ),
+    )
+    """The default value of the column."""
+
+    _nullable: bool = field(default=True, 
metadata=config(field_name="nullable"))
+    """Whether the column value can be null."""
+
+    _auto_increment: bool = field(
+        default=False, metadata=config(field_name="autoIncrement")
+    )
+    """Whether the column is an auto-increment column."""
+
+    def name(self) -> str:
+        return self._name
+
+    def data_type(self) -> Type:
+        return self._data_type
+
+    def comment(self) -> str:
+        return self._comment
+
+    def nullable(self) -> bool:
+        return self._nullable
+
+    def auto_increment(self) -> bool:
+        return self._auto_increment
+
+    def default_value(self) -> Union[Expression, List[Expression]]:
+        return self._default_value
+
+    def validate(self) -> None:
+        Precondition.check_string_not_empty(
+            self._name, "Column name cannot be null or empty."
+        )
+        Precondition.check_argument(
+            self._data_type is not None, "Column data type cannot be null."
+        )
+        non_nullable_condition = (
+            not self._nullable
+            and isinstance(self._default_value, LiteralDTO)
+            and cast(LiteralDTO, self._default_value).data_type()
+            == Types.NullType.get()
+        )
+        Precondition.check_argument(
+            not non_nullable_condition,
+            f"Column cannot be non-nullable with a null default value: 
{self._name}.",
+        )
+
+    @classmethod
+    def builder(
+        cls,
+        name: str,
+        data_type: Type,
+        comment: str,
+        nullable: bool = True,
+        auto_increment: bool = False,
+        default_value: Optional[Expression] = None,
+    ) -> ColumnDTO:
+        Precondition.check_argument(name is not None, "Column name cannot be 
null")
+        Precondition.check_argument(
+            data_type is not None, "Column data type cannot be null"
+        )
+        return ColumnDTO(
+            _name=name,
+            _data_type=data_type,
+            _comment=comment,
+            _nullable=nullable,
+            _auto_increment=auto_increment,
+            _default_value=(
+                Column.DEFAULT_VALUE_NOT_SET if default_value is None else 
default_value
+            ),
+        )
+
+    def __eq__(self, other: ColumnDTO) -> bool:
+        if not isinstance(other, ColumnDTO):
+            return False
+        return (
+            self._name == other._name
+            and self._data_type == other._data_type
+            and self._comment == other._comment
+            and self._nullable == other._nullable
+            and self._auto_increment == other._auto_increment
+            and self._default_value == other._default_value
+        )
+
+    def __hash__(self) -> int:
+        return hash(
+            (
+                self._name,
+                self._data_type,
+                self._comment,
+                self._nullable,
+                self._auto_increment,
+                (
+                    None
+                    if self._default_value is Column.DEFAULT_VALUE_NOT_SET
+                    else self._default_value
+                ),
+            )
+        )
diff --git a/clients/client-python/gravitino/dto/rel/expressions/__init__.py 
b/clients/client-python/gravitino/dto/rel/expressions/__init__.py
new file mode 100644
index 0000000000..13a83393a9
--- /dev/null
+++ b/clients/client-python/gravitino/dto/rel/expressions/__init__.py
@@ -0,0 +1,16 @@
+# 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.
diff --git 
a/clients/client-python/gravitino/dto/rel/expressions/function_arg.py 
b/clients/client-python/gravitino/dto/rel/expressions/function_arg.py
new file mode 100644
index 0000000000..5c53a365cd
--- /dev/null
+++ b/clients/client-python/gravitino/dto/rel/expressions/function_arg.py
@@ -0,0 +1,73 @@
+# 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 __future__ import annotations
+
+from abc import abstractmethod
+from enum import Enum, unique
+from typing import TYPE_CHECKING, ClassVar, List
+
+from gravitino.api.expressions.expression import Expression
+from gravitino.dto.rel.partition_utils import PartitionUtils
+
+if TYPE_CHECKING:
+    from gravitino.dto.rel.column_dto import ColumnDTO
+
+
+class FunctionArg(Expression):
+    """An argument of a function."""
+
+    EMPTY_ARGS: ClassVar[List[FunctionArg]] = []
+
+    @abstractmethod
+    def arg_type(self) -> ArgType:
+        """Arguments type of the function.
+
+        Returns:
+            ArgType: The type of this argument.
+        """
+        pass
+
+    def validate(self, columns: List[ColumnDTO]) -> None:
+        """Validates the function argument.
+
+        Args:
+            columns (List[ColumnDTO]): The columns of the table.
+
+        Raises:
+            IllegalArgumentException: If the function argument is invalid.
+        """
+        validate_field_existence = PartitionUtils.validate_field_existence
+        for ref in self.references():
+            validate_field_existence(columns, ref.field_name())
+
+    @unique
+    class ArgType(str, Enum):
+        """The type of the argument.
+
+        The supported types are:
+
+        - `LITERAL`: A literal argument.
+        - `FIELD`: A field argument.
+        - `FUNCTION`: A function argument.
+        - `UNPARSED`: An unparsed argument.
+        """
+
+        LITERAL = "literal"
+        FIELD = "field"
+        FUNCTION = "function"
+        UNPARSED = "unparsed"
diff --git a/clients/client-python/gravitino/dto/rel/expressions/literal_dto.py 
b/clients/client-python/gravitino/dto/rel/expressions/literal_dto.py
new file mode 100644
index 0000000000..4e0ba4bbc4
--- /dev/null
+++ b/clients/client-python/gravitino/dto/rel/expressions/literal_dto.py
@@ -0,0 +1,75 @@
+# 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 __future__ import annotations
+
+from typing import TYPE_CHECKING, ClassVar
+
+from gravitino.api.expressions.literals.literal import Literal
+from gravitino.api.types.types import Types
+from gravitino.dto.rel.expressions.function_arg import FunctionArg
+
+if TYPE_CHECKING:
+    from gravitino.api.types.type import Type
+
+
+class LiteralDTO(Literal[str], FunctionArg):
+    """Represents a Literal Data Transfer Object (DTO) that implements the 
Literal interface."""
+
+    NULL: ClassVar[LiteralDTO]
+    """An instance of LiteralDTO with a value of "NULL" and a data type of 
Types.NullType.get()."""
+
+    def __init__(self, value: str, data_type: Type):
+        self._value = value
+        self._data_type = data_type
+
+    def value(self) -> str:
+        """The literal value.
+
+        Returns:
+            str: The value of the literal.
+        """
+        return self._value
+
+    def data_type(self) -> Type:
+        """The data type of the literal.
+
+        Returns:
+            Type: The data type of the literal.
+        """
+        return self._data_type
+
+    def arg_type(self) -> FunctionArg.ArgType:
+        return self.ArgType.LITERAL
+
+    def __eq__(self, other: object) -> bool:
+        if not isinstance(other, LiteralDTO):
+            return False
+        return (
+            self._data_type == other.data_type()
+            and self._value == other.value()
+            and self.arg_type() == other.arg_type()
+        )
+
+    def __hash__(self) -> int:
+        return hash((self.arg_type(), self._data_type, self._value))
+
+    def __str__(self) -> str:
+        return f"LiteralDTO(value='{self._value}', 
data_type={self._data_type})"
+
+
+LiteralDTO.NULL = LiteralDTO("NULL", Types.NullType.get())
diff --git a/clients/client-python/gravitino/dto/rel/partition_utils.py 
b/clients/client-python/gravitino/dto/rel/partition_utils.py
new file mode 100644
index 0000000000..9629761dba
--- /dev/null
+++ b/clients/client-python/gravitino/dto/rel/partition_utils.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.
+
+from typing import TYPE_CHECKING, List
+
+from gravitino.utils.precondition import Precondition
+
+if TYPE_CHECKING:
+    from gravitino.dto.rel.column_dto import ColumnDTO
+
+
+class PartitionUtils:
+    """Validates the existence of the partition field in the table."""
+
+    @staticmethod
+    def validate_field_existence(
+        columns: List["ColumnDTO"], field_name: List[str]
+    ) -> None:
+        """Validates the existence of the partition field in the table.
+
+        Args:
+            columns (List[ColumnDTO]): The columns of the table.
+            field_name (List[str]): The name of the field to validate.
+
+        Raises:
+            IllegalArgumentException:
+                If the field does not exist in the table, this exception is 
thrown.
+        """
+        Precondition.check_argument(
+            columns is not None and len(columns) > 0, "columns cannot be null 
or empty"
+        )
+        # TODO: Need to consider the case sensitivity issues. To be optimized.
+        partition_column = [
+            c for c in columns if c.name().lower() == field_name[0].lower()
+        ]
+
+        Precondition.check_argument(
+            len(partition_column) == 1, f"Field '{field_name[0]}' not found in 
table"
+        )
+        # TODO: should validate nested fieldName after column type support 
namedStruct
diff --git a/clients/client-python/tests/unittests/dto/__init__.py 
b/clients/client-python/tests/unittests/dto/__init__.py
new file mode 100644
index 0000000000..13a83393a9
--- /dev/null
+++ b/clients/client-python/tests/unittests/dto/__init__.py
@@ -0,0 +1,16 @@
+# 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.
diff --git a/clients/client-python/tests/unittests/dto/rel/__init__.py 
b/clients/client-python/tests/unittests/dto/rel/__init__.py
new file mode 100644
index 0000000000..13a83393a9
--- /dev/null
+++ b/clients/client-python/tests/unittests/dto/rel/__init__.py
@@ -0,0 +1,16 @@
+# 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.
diff --git a/clients/client-python/tests/unittests/dto/rel/test_column_dto.py 
b/clients/client-python/tests/unittests/dto/rel/test_column_dto.py
new file mode 100644
index 0000000000..99cf3fb621
--- /dev/null
+++ b/clients/client-python/tests/unittests/dto/rel/test_column_dto.py
@@ -0,0 +1,195 @@
+# 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 json
+import unittest
+
+from gravitino.api.column import Column
+from gravitino.api.types.json_serdes import TypeSerdes
+from gravitino.api.types.json_serdes._helper.serdes_utils import SerdesUtils
+from gravitino.api.types.types import Types
+from gravitino.dto.rel.column_dto import ColumnDTO
+from gravitino.dto.rel.expressions.literal_dto import LiteralDTO
+from gravitino.exceptions.base import IllegalArgumentException
+
+
+class TestColumnDTO(unittest.TestCase):
+    @classmethod
+    def setUpClass(cls):
+        cls._supported_types = [
+            *SerdesUtils.TYPES.values(),
+            Types.DecimalType.of(10, 2),
+            Types.FixedType.of(10),
+            Types.FixedCharType.of(10),
+            Types.VarCharType.of(10),
+            Types.StructType(
+                fields=[
+                    Types.StructType.Field.not_null_field(
+                        name=f"field_{field_idx}",
+                        field_type=type_,
+                        comment=f"comment {field_idx}" if field_idx % 2 == 0 
else "",
+                    )
+                    for type_, field_idx in zip(
+                        SerdesUtils.TYPES.values(),
+                        range(len(SerdesUtils.TYPES.values())),
+                    )
+                ]
+            ),
+            Types.UnionType.of(Types.DoubleType.get(), Types.FloatType.get()),
+            Types.ListType.of(
+                element_type=Types.StringType.get(), element_nullable=False
+            ),
+            Types.MapType.of(
+                key_type=Types.StringType.get(),
+                value_type=Types.StringType.get(),
+                value_nullable=False,
+            ),
+            Types.ExternalType.of(catalog_string="external_type"),
+            Types.UnparsedType.of(unparsed_type="unparsed_type"),
+        ]
+        cls._string_columns = [
+            ColumnDTO.builder(
+                name=f"column_{idx}",
+                data_type=Types.StringType.get(),
+                comment=f"column_{idx} comment",
+            )
+            for idx in range(3)
+        ]
+
+    def test_column_dto_equality(self):
+        column_dto_1 = self._string_columns[1]
+        column_dto_2 = self._string_columns[2]
+        self.assertNotEqual(column_dto_1, column_dto_2)
+        self.assertEqual(column_dto_1, column_dto_1)
+
+    def test_column_dto_hash(self):
+        column_dto_1 = self._string_columns[1]
+        column_dto_2 = self._string_columns[2]
+        column_dto_dict = {column_dto_1: "column_1", column_dto_2: "column_2"}
+        self.assertEqual("column_1", column_dto_dict.get(column_dto_1))
+        self.assertNotEqual("column_1", column_dto_dict.get(column_dto_2))
+
+    def test_column_dto_builder(self):
+        ColumnDTO.builder(
+            name="",
+            data_type=Types.StringType.get(),
+            comment="comment",
+            default_value=LiteralDTO(
+                value="default_value", data_type=Types.StringType.get()
+            ),
+        )
+
+        with self.assertRaisesRegex(
+            IllegalArgumentException,
+            "Column name cannot be null",
+        ):
+            ColumnDTO.builder(
+                name=None,
+                data_type=Types.StringType.get(),
+                comment="comment",
+                default_value=LiteralDTO(
+                    value="default_value", data_type=Types.StringType.get()
+                ),
+            )
+
+        with self.assertRaisesRegex(
+            IllegalArgumentException,
+            "Column data type cannot be null",
+        ):
+            ColumnDTO.builder(
+                name="column",
+                data_type=None,
+                comment="comment",
+                default_value=LiteralDTO(
+                    value="default_value", data_type=Types.StringType.get()
+                ),
+            )
+
+    def test_column_dto_violate_non_nullable(self):
+        column_dto = ColumnDTO.builder(
+            name="column_name",
+            data_type=Types.StringType.get(),
+            comment="comment",
+            nullable=False,
+            default_value=LiteralDTO(value="None", 
data_type=Types.NullType.get()),
+        )
+        with self.assertRaisesRegex(
+            IllegalArgumentException,
+            "Column cannot be non-nullable with a null default value",
+        ):
+            column_dto.validate()
+
+    def test_column_dto_default_value_not_set(self):
+        column_dto = ColumnDTO.builder(
+            name="column_name",
+            data_type=Types.StringType.get(),
+            comment="comment",
+        )
+        self.assertEqual(column_dto.name(), "column_name")
+        self.assertEqual(column_dto.nullable(), True)
+        self.assertEqual(column_dto.auto_increment(), False)
+        self.assertEqual(column_dto.comment(), "comment")
+        self.assertEqual(column_dto.default_value(), 
Column.DEFAULT_VALUE_NOT_SET)
+
+    def test_column_dto_serialize_with_default_value_not_set(self):
+        """Test if `default_value` is excluded after having been serialized 
when its
+        value is `Column.DEFAULT_VALUE_NOT_SET`
+        """
+
+        expected_dict = {
+            "name": "",
+            "type": "",
+            "comment": "",
+            "nullable": False,
+            "autoIncrement": False,
+        }
+        for supported_type in self._supported_types:
+            column_dto = ColumnDTO.builder(
+                name=str(supported_type.name()),
+                data_type=supported_type,
+                comment=supported_type.simple_string(),
+            )
+            expected_dict["name"] = str(supported_type.name())
+            expected_dict["type"] = TypeSerdes.serialize(supported_type)
+            expected_dict["comment"] = supported_type.simple_string()
+            expected_dict["nullable"] = True
+            expected_dict["autoIncrement"] = False
+
+            serialized_dict = json.loads(column_dto.to_json())
+            self.assertDictEqual(serialized_dict, expected_dict)
+
+    def test_column_dto_deserialize_with_default_value_not_set(self):
+        """Test if we can deserialize a valid JSON document of `ColumnDTO` 
with missing
+        `default_value` as a `ColumnDTO` instance with 
`default_value=Column.DEFAULT_VALUE_NOT_SET`
+        """
+
+        for supported_type in self._supported_types:
+            column_dto = ColumnDTO.builder(
+                name=str(supported_type.name()),
+                data_type=supported_type,
+                comment=supported_type.simple_string(),
+                nullable=True,
+                auto_increment=False,
+            )
+            serialized_json = column_dto.to_json()
+            deserialized_column_dto = ColumnDTO.from_json(serialized_json)
+            deserialized_json = deserialized_column_dto.to_json()
+
+            self.assertIs(
+                deserialized_column_dto.default_value(), 
Column.DEFAULT_VALUE_NOT_SET
+            )
+            self.assertEqual(serialized_json, deserialized_json)
diff --git a/clients/client-python/tests/unittests/dto/rel/test_function_arg.py 
b/clients/client-python/tests/unittests/dto/rel/test_function_arg.py
new file mode 100644
index 0000000000..9ab4f938f1
--- /dev/null
+++ b/clients/client-python/tests/unittests/dto/rel/test_function_arg.py
@@ -0,0 +1,51 @@
+# 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.types.types import Types
+from gravitino.dto.rel.column_dto import ColumnDTO
+from gravitino.dto.rel.expressions.function_arg import FunctionArg
+from gravitino.dto.rel.expressions.literal_dto import LiteralDTO
+
+
+class TestFunctionArg(unittest.TestCase):
+    def setUp(self) -> None:
+        self._data_types = [
+            Types.StringType.get(),
+            Types.IntegerType.get(),
+            Types.DateType.get(),
+        ]
+        self._column_names = [f"column{i}" for i in 
range(len(self._data_types))]
+        self._columns = [
+            ColumnDTO.builder(
+                name=column_name,
+                data_type=data_type,
+                comment=f"{column_name} comment",
+                nullable=False,
+            )
+            for column_name, data_type in zip(self._column_names, 
self._data_types)
+        ]
+
+    def test_function_arg(self):
+        self.assertEqual(FunctionArg.EMPTY_ARGS, [])
+
+    def test_function_arg_validate(self):
+        LiteralDTO(data_type=Types.StringType.get(), value="test").validate(
+            columns=self._columns
+        )
+        # TODO: add unit test for FunctionArg with children
diff --git a/clients/client-python/tests/unittests/dto/rel/test_literal_dto.py 
b/clients/client-python/tests/unittests/dto/rel/test_literal_dto.py
new file mode 100644
index 0000000000..a6e00e9bbc
--- /dev/null
+++ b/clients/client-python/tests/unittests/dto/rel/test_literal_dto.py
@@ -0,0 +1,48 @@
+# 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.types.types import Types
+from gravitino.dto.rel.expressions.literal_dto import LiteralDTO
+
+
+class TestLiteralDTO(unittest.TestCase):
+    def setUp(self):
+        self._literal_dto = LiteralDTO(data_type=Types.IntegerType.get(), 
value="-1")
+
+    def test_literal_dto(self):
+        self.assertEqual(self._literal_dto.value(), "-1")
+        self.assertEqual(self._literal_dto.data_type(), 
Types.IntegerType.get())
+
+    def test_literal_dto_to_string(self):
+        expected_str = f"LiteralDTO(value='{self._literal_dto.value()}', 
data_type={self._literal_dto.data_type()})"
+        self.assertEqual(str(self._literal_dto), expected_str)
+
+    def test_literal_dto_null(self):
+        self.assertEqual(
+            LiteralDTO.NULL, LiteralDTO(data_type=Types.NullType.get(), 
value="NULL")
+        )
+
+    def test_literal_dto_hash(self):
+        second_literal_dto: LiteralDTO = LiteralDTO(
+            data_type=Types.IntegerType.get(), value="2"
+        )
+        literal_dto_dict = {self._literal_dto: "test1", second_literal_dto: 
"test2"}
+
+        self.assertEqual("test1", literal_dto_dict.get(self._literal_dto))
+        self.assertNotEqual("test2", literal_dto_dict.get(self._literal_dto))
diff --git 
a/clients/client-python/tests/unittests/dto/rel/test_partition_utils.py 
b/clients/client-python/tests/unittests/dto/rel/test_partition_utils.py
new file mode 100644
index 0000000000..6bd47cc50e
--- /dev/null
+++ b/clients/client-python/tests/unittests/dto/rel/test_partition_utils.py
@@ -0,0 +1,56 @@
+# 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.types.types import Types
+from gravitino.dto.rel.column_dto import ColumnDTO
+from gravitino.dto.rel.partition_utils import PartitionUtils
+from gravitino.exceptions.base import IllegalArgumentException
+
+
+class TestPartitionUtils(unittest.TestCase):
+    def setUp(self) -> None:
+        self._data_types = [
+            Types.StringType.get(),
+            Types.IntegerType.get(),
+            Types.DateType.get(),
+        ]
+        self._column_names = [f"column{i}" for i in 
range(len(self._data_types))]
+        self._columns = [
+            ColumnDTO.builder(
+                name=column_name,
+                data_type=data_type,
+                comment=f"{column_name} comment",
+                nullable=False,
+            )
+            for column_name, data_type in zip(self._column_names, 
self._data_types)
+        ]
+
+    def test_partition_utils_validate_field_existence(self):
+        for column_name in self._column_names:
+            PartitionUtils.validate_field_existence(
+                columns=self._columns, field_name=[column_name]
+            )
+
+    def test_partition_utils_validate_field_existence_with_empty_columns(self):
+        with self.assertRaises(IllegalArgumentException):
+            PartitionUtils.validate_field_existence([], self._column_names)
+
+    def test_partition_utils_validate_field_existence_not_found(self):
+        with self.assertRaises(IllegalArgumentException):
+            PartitionUtils.validate_field_existence(self._columns, 
["fake_column"])


Reply via email to