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"])