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 e0572b14a5 [#9548] improvement(python-client): Add TableUpdateRequest 
and TableUpdatesRequest (#9870)
e0572b14a5 is described below

commit e0572b14a5644ddfb432685b572814285c1ac6a5
Author: Lord of Abyss <[email protected]>
AuthorDate: Tue Mar 24 17:11:06 2026 +0800

    [#9548] improvement(python-client): Add TableUpdateRequest and 
TableUpdatesRequest (#9870)
    
    ### What changes were proposed in this pull request?
    
    Add `TableUpdateRequest` and `TableUpdatesRequest`
    
    ### Why are the changes needed?
    
    Fix: #9548
    
    ### Does this PR introduce _any_ user-facing change?
    
    no
    
    ### How was this patch tested?
    
    local unittest
---
 .../gravitino/api/rel/table_change.py              |  26 +-
 .../gravitino/dto/rel/indexes/index_dto.py         |   2 +
 .../gravitino/dto/rel/json_serdes/__init__.py      |  10 +
 .../dto/rel/json_serdes/column_position_serdes.py  |  61 ++
 .../gravitino/dto/requests/table_update_request.py | 745 +++++++++++++++++++++
 .../dto/requests/table_updates_request.py          |  51 ++
 clients/client-python/gravitino/utils/__init__.py  |  10 +-
 .../utils/{__init__.py => string_utils.py}         |  27 +-
 .../dto/requests/test_table_update_request.py      | 649 ++++++++++++++++++
 .../unittests}/json_serdes/__init__.py             |   0
 .../unittests/json_serdes/test_type_serdes.py      | 245 ++++++-
 11 files changed, 1813 insertions(+), 13 deletions(-)

diff --git a/clients/client-python/gravitino/api/rel/table_change.py 
b/clients/client-python/gravitino/api/rel/table_change.py
index 8b79ef0393..c7ec632043 100644
--- a/clients/client-python/gravitino/api/rel/table_change.py
+++ b/clients/client-python/gravitino/api/rel/table_change.py
@@ -35,16 +35,14 @@ class TableChange(ABC):
     """
 
     @staticmethod
-    def rename(new_name: str) -> "RenameTable":
+    def rename(new_name: str, new_schema_name: Optional[str] = None) -> 
"RenameTable":
         """Create a `TableChange` for renaming a table.
 
         Args:
             new_name: The new table name.
-
-        Returns:
-            RenameTable: A `TableChange` for the rename.
+            new_schema_name: The new schema name if cross-schema rename is 
requested.
         """
-        return TableChange.RenameTable(new_name)
+        return TableChange.RenameTable(new_name, new_schema_name)
 
     @staticmethod
     def update_comment(new_comment: str) -> "UpdateComment":
@@ -315,6 +313,9 @@ class TableChange(ABC):
         """A `TableChange` to rename a table."""
 
         _new_name: str = field(metadata=config(field_name="new_name"))
+        _new_schema_name: Optional[str] = field(
+            default=None, metadata=config(field_name="new_schema_name")
+        )
 
         def get_new_name(self) -> str:
             """Retrieves the new name for the table.
@@ -324,17 +325,26 @@ class TableChange(ABC):
             """
             return self._new_name
 
+        def get_new_schema_name(self) -> Optional[str]:
+            """Retrieves the new schema name for the table."""
+            return self._new_schema_name
+
         def __str__(self):
-            return f"RENAMETABLE {self._new_name}"
+            if self._new_schema_name is None:
+                return f"RENAMETABLE {self._new_name}"
+            return f"RENAMETABLE {self._new_schema_name}.{self._new_name}"
 
         def __eq__(self, value: object) -> bool:
             if not isinstance(value, TableChange.RenameTable):
                 return False
             other = cast(TableChange.RenameTable, value)
-            return self._new_name == other.get_new_name()
+            return (
+                self._new_name == other.get_new_name()
+                and self._new_schema_name == other.get_new_schema_name()
+            )
 
         def __hash__(self) -> int:
-            return hash(self._new_name)
+            return hash((self._new_name, self._new_schema_name))
 
     @final
     @dataclass(frozen=True)
diff --git a/clients/client-python/gravitino/dto/rel/indexes/index_dto.py 
b/clients/client-python/gravitino/dto/rel/indexes/index_dto.py
index 9d18f01ff9..4842806e07 100644
--- a/clients/client-python/gravitino/dto/rel/indexes/index_dto.py
+++ b/clients/client-python/gravitino/dto/rel/indexes/index_dto.py
@@ -15,6 +15,8 @@
 # specific language governing permissions and limitations
 # under the License.
 
+from __future__ import annotations
+
 from functools import reduce
 from typing import ClassVar, List, Optional, Dict
 
diff --git a/clients/client-python/gravitino/dto/rel/json_serdes/__init__.py 
b/clients/client-python/gravitino/dto/rel/json_serdes/__init__.py
index 13a83393a9..4646fc5587 100644
--- a/clients/client-python/gravitino/dto/rel/json_serdes/__init__.py
+++ b/clients/client-python/gravitino/dto/rel/json_serdes/__init__.py
@@ -14,3 +14,13 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
+
+from gravitino.dto.rel.json_serdes.column_position_serdes import 
ColumnPositionSerdes
+from gravitino.dto.rel.json_serdes.distribution_serdes import 
DistributionSerDes
+from gravitino.dto.rel.json_serdes.sort_order_serdes import SortOrderSerdes
+
+__all__ = [
+    "ColumnPositionSerdes",
+    "DistributionSerDes",
+    "SortOrderSerdes",
+]
diff --git 
a/clients/client-python/gravitino/dto/rel/json_serdes/column_position_serdes.py 
b/clients/client-python/gravitino/dto/rel/json_serdes/column_position_serdes.py
new file mode 100644
index 0000000000..6312e339ec
--- /dev/null
+++ 
b/clients/client-python/gravitino/dto/rel/json_serdes/column_position_serdes.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.
+
+from typing import Union
+
+from dataclasses_json.core import Json
+
+from gravitino.api.rel.table_change import After, Default, First, TableChange
+from gravitino.api.rel.types.json_serdes.base import JsonSerializable
+
+
+class ColumnPositionSerdes(JsonSerializable[TableChange.ColumnPosition]):
+    """JSON serializer/deserializer for table column positions."""
+
+    _POSITION_FIRST = "first"
+    _POSITION_AFTER = "after"
+    _POSITION_DEFAULT = "default"
+
+    @classmethod
+    def serialize(
+        cls,
+        value: TableChange.ColumnPosition,
+    ) -> Union[str, dict[str, str]]:
+        if isinstance(value, First):
+            return cls._POSITION_FIRST
+        if isinstance(value, After):
+            return {cls._POSITION_AFTER: value.get_column()}
+        if isinstance(value, Default):
+            return cls._POSITION_DEFAULT
+
+        raise ValueError(f"Unknown column position: {value}")
+
+    @classmethod
+    def deserialize(cls, data: Json) -> Union[First, After, Default]:
+        if isinstance(data, str):
+            data = data.lower()
+            if data == cls._POSITION_FIRST:
+                return TableChange.ColumnPosition.first()
+            if data == cls._POSITION_DEFAULT:
+                return TableChange.ColumnPosition.default_pos()
+
+        if isinstance(data, dict):
+            after_column = data.get(cls._POSITION_AFTER)
+            if after_column:
+                return TableChange.ColumnPosition.after(after_column)
+
+        raise ValueError(f"Unknown json column position: {data}")
diff --git 
a/clients/client-python/gravitino/dto/requests/table_update_request.py 
b/clients/client-python/gravitino/dto/requests/table_update_request.py
new file mode 100644
index 0000000000..4114bfbf12
--- /dev/null
+++ b/clients/client-python/gravitino/dto/requests/table_update_request.py
@@ -0,0 +1,745 @@
+# 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
+
+import typing
+from abc import ABC, abstractmethod
+from dataclasses import dataclass, field
+
+from dataclasses_json import config, dataclass_json
+
+from gravitino.api.rel.expressions.expression import Expression
+from gravitino.api.rel.indexes.index import Index
+from gravitino.api.rel.table_change import (
+    DeleteColumn,
+    RenameColumn,
+    TableChange,
+    UpdateColumnAutoIncrement,
+    UpdateColumnComment,
+    UpdateColumnDefaultValue,
+    UpdateColumnNullability,
+    UpdateColumnPosition,
+    UpdateColumnType,
+)
+from gravitino.api.rel.types.json_serdes import TypeSerdes
+from gravitino.api.rel.types.type import Type
+from gravitino.dto.rel.expressions.json_serdes.column_default_value_serdes 
import (
+    ColumnDefaultValueSerdes,
+)
+from gravitino.dto.rel.indexes.json_serdes.index_serdes import IndexSerdes
+from gravitino.dto.rel.json_serdes.column_position_serdes import 
ColumnPositionSerdes
+from gravitino.rest.rest_message import RESTRequest
+from gravitino.utils import StringUtils
+from gravitino.utils.precondition import Precondition
+
+from ...api.rel.table_change import AddColumn
+
+
+@dataclass_json
+@dataclass
+class TableUpdateRequestBase(RESTRequest, ABC):
+    """Base class for all table update requests."""
+
+    _type: str = field(init=False, metadata=config(field_name="@type"))
+
+    @abstractmethod
+    def table_change(self) -> TableChange:
+        """Convert to table change operation"""
+        pass
+
+
+class TableUpdateRequest:
+    """Namespace for all table update request types."""
+
+    @dataclass_json
+    @dataclass
+    class RenameTableRequest(TableUpdateRequestBase):
+        """
+        Update request to rename a table
+        """
+
+        _new_name: str = field(metadata=config(field_name="newName"))
+        _new_schema_name: typing.Optional[str] = field(
+            default=None,
+            metadata=config(
+                field_name="newSchemaName",
+                exclude=lambda value: value is None,
+            ),
+        )
+
+        def __post_init__(self) -> None:
+            self._type = "rename"
+
+        def validate(self) -> None:
+            """
+            Validate the request.
+
+            Raises:
+                ValueError: If the request is invalid, this exception is 
thrown.
+            """
+            Precondition.check_string_not_empty(
+                self._new_name,
+                '"newName" field is required and cannot be empty',
+            )
+
+        @property
+        def new_name(self) -> str:
+            return self._new_name
+
+        @property
+        def new_schema_name(self) -> typing.Optional[str]:
+            return self._new_schema_name
+
+        def table_change(self) -> TableChange.RenameTable:
+            return TableChange.rename(self._new_name, self._new_schema_name)
+
+    @dataclass_json
+    @dataclass
+    class UpdateTableCommentRequest(TableUpdateRequestBase):
+        """
+        Update request to change a table comment
+        """
+
+        _new_comment: str = field(metadata=config(field_name="newComment"))
+
+        def __post_init__(self) -> None:
+            self._type = "updateComment"
+
+        def validate(self) -> None:
+            """
+            Validate the request.
+
+            Raises:
+                ValueError: If the request is invalid, this exception is 
thrown.
+            """
+            # Validates the fields of the request. Always pass.
+            pass
+
+        @property
+        def new_comment(self) -> str:
+            return self._new_comment
+
+        def table_change(self) -> TableChange.UpdateComment:
+            return TableChange.update_comment(self._new_comment)
+
+    @dataclass_json
+    @dataclass
+    class SetTablePropertyRequest(TableUpdateRequestBase):
+        """
+        Update request to set a table property
+        """
+
+        _prop: str = field(metadata=config(field_name="property"))
+        _prop_value: str = field(metadata=config(field_name="value"))
+
+        def __post_init__(self) -> None:
+            self._type = "setProperty"
+
+        def validate(self) -> None:
+            """
+            Validate the request.
+
+            Raises:
+                ValueError: If the request is invalid, this exception is 
thrown.
+            """
+            Precondition.check_string_not_empty(
+                self._prop,
+                '"property" field is required',
+            )
+
+            Precondition.check_string_not_empty(
+                self._prop_value,
+                '"value" field is required',
+            )
+
+        @property
+        def prop(self) -> str:
+            return self._prop
+
+        @property
+        def prop_value(self) -> str:
+            return self._prop_value
+
+        def table_change(self) -> TableChange.SetProperty:
+            return TableChange.set_property(self._prop, self._prop_value)
+
+    @dataclass_json
+    @dataclass
+    class RemoveTablePropertyRequest(TableUpdateRequestBase):
+        """
+        Update request to remove a table property
+        """
+
+        _property: str = field(metadata=config(field_name="property"))
+
+        def __post_init__(self) -> None:
+            self._type = "removeProperty"
+
+        def validate(self) -> None:
+            """
+            Validates the request.
+
+            Raises:
+                ValueError: If the request is invalid, this exception is 
thrown.
+            """
+            Precondition.check_string_not_empty(
+                self._property,
+                '"property" field is required',
+            )
+
+        @property
+        def property(self) -> str:
+            return self._property
+
+        def table_change(self) -> TableChange.RemoveProperty:
+            return TableChange.remove_property(self._property)
+
+    @dataclass_json
+    @dataclass
+    # pylint: disable=too-many-instance-attributes
+    class AddTableColumnRequest(TableUpdateRequestBase):
+        """Represents a request to add a column to a table."""
+
+        _field_name: list[str] = field(metadata=config(field_name="fieldName"))
+        _data_type: Type = field(
+            metadata=config(
+                field_name="type",
+                encoder=TypeSerdes.serialize,
+                decoder=TypeSerdes.deserialize,
+            )
+        )
+        _comment: typing.Optional[str] = 
field(metadata=config(field_name="comment"))
+        _position: typing.Optional[TableChange.ColumnPosition] = field(
+            metadata=config(
+                field_name="position",
+                encoder=ColumnPositionSerdes.serialize,
+                decoder=ColumnPositionSerdes.deserialize,
+            )
+        )
+        _default_value: typing.Optional[Expression] = field(
+            metadata=config(
+                field_name="defaultValue",
+                encoder=ColumnDefaultValueSerdes.serialize,
+                decoder=ColumnDefaultValueSerdes.deserialize,
+                exclude=lambda v: v is None,
+            )
+        )
+        _nullable: bool = field(default=True, 
metadata=config(field_name="nullable"))
+        _auto_increment: bool = field(
+            default=False, metadata=config(field_name="autoIncrement")
+        )
+
+        def __post_init__(self) -> None:
+            self._type = "addColumn"
+
+        def validate(self) -> None:
+            """
+            Validates the request.
+
+            Raises:
+                ValueError: If the request is invalid, this exception is 
thrown.
+            """
+            Precondition.check_argument(
+                self._field_name,
+                "Field name must be specified",
+            )
+            Precondition.check_argument(
+                all(StringUtils.is_not_blank(name) for name in 
self._field_name),
+                'elements in "field_name" cannot be empty',
+            )
+            Precondition.check_argument(
+                self._data_type is not None,
+                '"type" field is required and cannot be empty',
+            )
+
+        @property
+        def field_name(self) -> list[str]:
+            return self._field_name
+
+        @property
+        def data_type(self) -> Type:
+            return self._data_type
+
+        @property
+        def comment(self) -> typing.Optional[str]:
+            return self._comment
+
+        @property
+        def position(self) -> typing.Optional[TableChange.ColumnPosition]:
+            return self._position
+
+        @property
+        def default_value(self) -> typing.Optional[Expression]:
+            return self._default_value
+
+        @property
+        def is_nullable(self) -> bool:
+            return self._nullable
+
+        @property
+        def is_auto_increment(self) -> bool:
+            return self._auto_increment
+
+        def table_change(self) -> AddColumn:
+            return TableChange.add_column(
+                self._field_name,
+                self._data_type,
+                self._comment,
+                self._position,
+                self._nullable,
+                self._auto_increment,
+                self._default_value,
+            )
+
+    @dataclass_json
+    @dataclass
+    class RenameTableColumnRequest(TableUpdateRequestBase):
+        """Represents a request to rename a column of a table."""
+
+        _old_field_name: list[str] = 
field(metadata=config(field_name="oldFieldName"))
+        _new_field_name: str = 
field(metadata=config(field_name="newFieldName"))
+
+        def __post_init__(self) -> None:
+            self._type = "renameColumn"
+
+        def validate(self) -> None:
+            """
+            Validate the request.
+
+            Raises:
+                ValueError: If the request is invalid, this exception is 
thrown.
+            """
+            Precondition.check_argument(
+                self._old_field_name,
+                '"old_field_name" field is required and must contain at least 
one element',
+            )
+            Precondition.check_argument(
+                all(StringUtils.is_not_blank(name) for name in 
self._old_field_name),
+                'elements in "old_field_name" cannot be empty',
+            )
+            Precondition.check_string_not_empty(
+                self._new_field_name,
+                '"newFieldName" field is required and cannot be empty',
+            )
+
+        def table_change(self) -> RenameColumn:
+            return TableChange.rename_column(self._old_field_name, 
self._new_field_name)
+
+    @dataclass_json
+    @dataclass
+    class UpdateTableColumnDefaultValueRequest(TableUpdateRequestBase):
+        """Represents a request to update the default value of a column of a 
table."""
+
+        _field_name: list[str] = field(metadata=config(field_name="fieldName"))
+        _new_default_value: Expression = field(
+            metadata=config(
+                field_name="newDefaultValue",
+                encoder=ColumnDefaultValueSerdes.serialize,
+                decoder=ColumnDefaultValueSerdes.deserialize,
+            )
+        )
+
+        def __post_init__(self) -> None:
+            self._type = "updateColumnDefaultValue"
+
+        def validate(self) -> None:
+            """
+            Validate the request.
+
+            Raises:
+                ValueError: If the request is invalid, this exception is 
thrown.
+            """
+            Precondition.check_argument(
+                self._field_name,
+                '"field_name" field is required and must contain at least one 
element',
+            )
+            Precondition.check_argument(
+                all(StringUtils.is_not_blank(name) for name in 
self._field_name),
+                'elements in "field_name" cannot be empty',
+            )
+            Precondition.check_argument(
+                self._new_default_value is not None
+                and self._new_default_value != Expression.EMPTY_EXPRESSION,
+                '"newDefaultValue" field is required and cannot be empty',
+            )
+
+        @property
+        def field_name(self) -> list[str]:
+            return self._field_name
+
+        @property
+        def new_default_value(self) -> Expression:
+            return self._new_default_value
+
+        def table_change(self) -> UpdateColumnDefaultValue:
+            return TableChange.update_column_default_value(
+                self._field_name, self._new_default_value
+            )
+
+    @dataclass_json
+    @dataclass
+    class UpdateTableColumnTypeRequest(TableUpdateRequestBase):
+        """Represents a request to update the type of a column of a table."""
+
+        _field_name: list[str] = field(metadata=config(field_name="fieldName"))
+        _new_type: Type = field(
+            metadata=config(
+                field_name="newType",
+                encoder=TypeSerdes.serialize,
+                decoder=TypeSerdes.deserialize,
+            )
+        )
+
+        def __post_init__(self) -> None:
+            self._type = "updateColumnType"
+
+        def validate(self) -> None:
+            """
+            Validate the request.
+
+            Raises:
+                ValueError: If the request is invalid, this exception is 
thrown.
+            """
+            Precondition.check_argument(
+                self._field_name,
+                '"field_name" field is required and must contain at least one 
element',
+            )
+            Precondition.check_argument(
+                all(StringUtils.is_not_blank(name) for name in 
self._field_name),
+                'elements in "field_name" cannot be empty',
+            )
+            Precondition.check_argument(
+                self._new_type is not None,
+                '"newType" field is required and cannot be null',
+            )
+
+        @property
+        def field_name(self) -> list[str]:
+            return self._field_name
+
+        @property
+        def new_type(self) -> Type:
+            return self._new_type
+
+        def table_change(self) -> UpdateColumnType:
+            return TableChange.update_column_type(self._field_name, 
self._new_type)
+
+    @dataclass_json
+    @dataclass
+    class UpdateTableColumnCommentRequest(TableUpdateRequestBase):
+        """
+        Represents a request to update the comment of a column of a table.
+        """
+
+        _field_name: list[str] = field(metadata=config(field_name="fieldName"))
+        _new_comment: str = field(metadata=config(field_name="newComment"))
+
+        def __post_init__(self) -> None:
+            self._type = "updateColumnComment"
+
+        def validate(self) -> None:
+            """
+            Validate the request.
+
+            Raises:
+                ValueError: If the request is invalid, this exception is 
thrown.
+            """
+            Precondition.check_argument(
+                self._field_name,
+                '"field_name" field is required and must contain at least one 
element',
+            )
+            Precondition.check_argument(
+                all(StringUtils.is_not_blank(name) for name in 
self._field_name),
+                'elements in "field_name" cannot be empty',
+            )
+            Precondition.check_string_not_empty(
+                self._new_comment,
+                '"newComment" field is required and cannot be empty',
+            )
+
+        @property
+        def field_name(self) -> list[str]:
+            return self._field_name
+
+        @property
+        def new_comment(self) -> str:
+            return self._new_comment
+
+        def table_change(self) -> UpdateColumnComment:
+            return TableChange.update_column_comment(
+                self._field_name, self._new_comment
+            )
+
+    @dataclass_json
+    @dataclass
+    class UpdateTableColumnPositionRequest(TableUpdateRequestBase):
+        """Represents a request to update the position of a column of a 
table."""
+
+        _field_name: list[str] = field(metadata=config(field_name="fieldName"))
+        _new_position: TableChange.ColumnPosition = field(
+            metadata=config(
+                field_name="newPosition",
+                encoder=ColumnPositionSerdes.serialize,
+                decoder=ColumnPositionSerdes.deserialize,
+            )
+        )
+
+        def __post_init__(self) -> None:
+            self._type = "updateColumnPosition"
+
+        def validate(self) -> None:
+            """
+            Validate the request.
+
+            Raises:
+                ValueError: If the request is invalid, this exception is 
thrown.
+            """
+            Precondition.check_argument(
+                self._field_name,
+                '"field_name" field is required and must contain at least one 
element',
+            )
+            Precondition.check_argument(
+                all(StringUtils.is_not_blank(name) for name in 
self._field_name),
+                'elements in "field_name" cannot be empty',
+            )
+            Precondition.check_argument(
+                self._new_position is not None,
+                '"newPosition" field is required and cannot be null',
+            )
+
+        @property
+        def field_name(self) -> list[str]:
+            return self._field_name
+
+        @property
+        def new_position(self) -> TableChange.ColumnPosition:
+            return self._new_position
+
+        def table_change(self) -> UpdateColumnPosition:
+            return TableChange.update_column_position(
+                self._field_name, self._new_position
+            )
+
+    @dataclass_json
+    @dataclass
+    class UpdateTableColumnNullabilityRequest(TableUpdateRequestBase):
+        """
+        Represents a request to update the nullability of a column of a table.
+        """
+
+        _field_name: list[str] = field(metadata=config(field_name="fieldName"))
+        _nullable: bool = field(metadata=config(field_name="nullable"))
+
+        def __post_init__(self) -> None:
+            self._type = "updateColumnNullability"
+
+        def validate(self) -> None:
+            """
+            Validate the request.
+
+            Raises:
+                ValueError: If the request is invalid, this exception is 
thrown.
+            """
+            Precondition.check_argument(
+                self._field_name,
+                '"field_name" field is required and must contain at least one 
element',
+            )
+            Precondition.check_argument(
+                all(StringUtils.is_not_blank(name) for name in 
self._field_name),
+                'elements in "field_name" cannot be empty',
+            )
+
+        @property
+        def field_name(self) -> list[str]:
+            return self._field_name
+
+        @property
+        def nullable(self) -> bool:
+            return self._nullable
+
+        def table_change(self) -> UpdateColumnNullability:
+            return TableChange.update_column_nullability(
+                self._field_name, self._nullable
+            )
+
+    @dataclass_json
+    @dataclass
+    class DeleteTableColumnRequest(TableUpdateRequestBase):
+        """
+        Represents a request to delete a column from a table.
+        """
+
+        _field_name: list[str] = field(metadata=config(field_name="fieldName"))
+        _if_exists: bool = field(metadata=config(field_name="ifExists"))
+
+        def __post_init__(self) -> None:
+            self._type = "deleteColumn"
+
+        def validate(self) -> None:
+            """
+            Validate the request.
+
+            Raises:
+                ValueError: If the request is invalid, this exception is 
thrown.
+            """
+            Precondition.check_argument(
+                self._field_name,
+                '"field_name" field is required and must contain at least one 
element',
+            )
+            Precondition.check_argument(
+                all(StringUtils.is_not_blank(name) for name in 
self._field_name),
+                'elements in "field_name" cannot be empty',
+            )
+
+        @property
+        def field_name(self) -> list[str]:
+            return self._field_name
+
+        @property
+        def if_exists(self) -> bool:
+            return self._if_exists
+
+        def table_change(self) -> DeleteColumn:
+            return TableChange.delete_column(self._field_name, self._if_exists)
+
+    @dataclass_json
+    @dataclass
+    class AddTableIndexRequest(TableUpdateRequestBase):
+        """
+        Represents a request to add an index to a table.
+        """
+
+        _index: Index = field(
+            metadata=config(
+                field_name="index",
+                encoder=IndexSerdes.serialize,
+                decoder=IndexSerdes.deserialize,
+            )
+        )
+
+        def __post_init__(self) -> None:
+            self._type = "addTableIndex"
+
+        def validate(self) -> None:
+            """
+            Validate the request.
+
+            Raises:
+                ValueError: If the request is invalid, this exception is 
thrown.
+            """
+            Precondition.check_argument(self._index is not None, "Index cannot 
be null")
+            Precondition.check_argument(
+                self._index.type() is not None,
+                "Index type cannot be null",
+            )
+            Precondition.check_string_not_empty(
+                self._index.name(),
+                '"name" field is required',
+            )
+            Precondition.check_argument(
+                self._index.field_names() is not None
+                and len(self._index.field_names()) > 0,
+                "The index must be set with corresponding column names",
+            )
+
+        @property
+        def index(self) -> Index:
+            return self._index
+
+        def table_change(self) -> TableChange.AddIndex:
+            return TableChange.AddIndex(
+                self._index.type(), self._index.name(), 
self._index.field_names()
+            )
+
+    @dataclass_json
+    @dataclass
+    class DeleteTableIndexRequest(TableUpdateRequestBase):
+        """
+        Represents a request to delete an index from a table.
+        """
+
+        _name: str = field(metadata=config(field_name="name"))
+        _if_exists: bool = field(metadata=config(field_name="ifExists"))
+
+        def __post_init__(self) -> None:
+            self._type = "deleteTableIndex"
+
+        def validate(self) -> None:
+            """
+            Validate the request.
+
+            Raises:
+                ValueError: If the request is invalid, this exception is 
thrown.
+            """
+            Precondition.check_string_not_empty(
+                self._name,
+                '"name" field is required',
+            )
+
+        @property
+        def name(self) -> str:
+            return self._name
+
+        @property
+        def if_exists(self) -> bool:
+            return self._if_exists
+
+        def table_change(self) -> TableChange.DeleteIndex:
+            return TableChange.delete_index(self._name, self._if_exists)
+
+    @dataclass_json
+    @dataclass
+    class UpdateColumnAutoIncrementRequest(TableUpdateRequestBase):
+        """
+        Represents a request to update a column autoIncrement from a table.
+        """
+
+        _field_name: list[str] = field(metadata=config(field_name="fieldName"))
+        _auto_increment: bool = 
field(metadata=config(field_name="autoIncrement"))
+
+        def __post_init__(self) -> None:
+            self._type = "updateColumnAutoIncrement"
+
+        def validate(self) -> None:
+            """
+            Validate the request.
+
+            Raises:
+                ValueError: If the request is invalid, this exception is 
thrown.
+            """
+            Precondition.check_argument(
+                self._field_name,
+                '"field_name" field is required and must contain at least one 
element',
+            )
+            Precondition.check_argument(
+                all(StringUtils.is_not_blank(name) for name in 
self._field_name),
+                'elements in "field_name" cannot be empty',
+            )
+
+        @property
+        def field_name(self) -> list[str]:
+            return self._field_name
+
+        @property
+        def auto_increment(self) -> bool:
+            return self._auto_increment
+
+        def table_change(self) -> UpdateColumnAutoIncrement:
+            return TableChange.update_column_auto_increment(
+                self._field_name, self._auto_increment
+            )
diff --git 
a/clients/client-python/gravitino/dto/requests/table_updates_request.py 
b/clients/client-python/gravitino/dto/requests/table_updates_request.py
new file mode 100644
index 0000000000..a8b91ab868
--- /dev/null
+++ b/clients/client-python/gravitino/dto/requests/table_updates_request.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.
+
+from __future__ import annotations
+
+from dataclasses import dataclass, field
+
+from dataclasses_json import config
+
+from gravitino.dto.requests.table_update_request import TableUpdateRequest
+from gravitino.rest.rest_message import RESTRequest
+from gravitino.utils.precondition import Precondition
+
+
+@dataclass
+class TableUpdatesRequest(RESTRequest):
+    """Represents a request to update a table."""
+
+    _updates: list[TableUpdateRequest] = field(
+        metadata=config(field_name="updates"), default_factory=list
+    )
+
+    def __init__(self, updates: list[TableUpdateRequest]) -> None:
+        self._updates = updates
+
+    def validate(self) -> None:
+        """Validates the request.
+
+        Raises:
+            ValueError: If the request is invalid.
+        """
+        Precondition.check_argument(
+            self._updates is not None and len(self._updates) > 0,
+            "Updates cannot be empty",
+        )
+        for update_request in self._updates:
+            update_request.validate()
diff --git a/clients/client-python/gravitino/utils/__init__.py 
b/clients/client-python/gravitino/utils/__init__.py
index 091797d89a..4b8c6f02cf 100644
--- a/clients/client-python/gravitino/utils/__init__.py
+++ b/clients/client-python/gravitino/utils/__init__.py
@@ -15,6 +15,12 @@
 # specific language governing permissions and limitations
 # under the License.
 
-from gravitino.utils.http_client import Response, HTTPClient, unpack
+from gravitino.utils.http_client import HTTPClient, Response, unpack
+from gravitino.utils.string_utils import StringUtils
 
-__all__ = ["Response", "HTTPClient", "unpack"]
+__all__ = [
+    "Response",
+    "HTTPClient",
+    "unpack",
+    "StringUtils",
+]
diff --git a/clients/client-python/gravitino/utils/__init__.py 
b/clients/client-python/gravitino/utils/string_utils.py
similarity index 53%
copy from clients/client-python/gravitino/utils/__init__.py
copy to clients/client-python/gravitino/utils/string_utils.py
index 091797d89a..dab2a71726 100644
--- a/clients/client-python/gravitino/utils/__init__.py
+++ b/clients/client-python/gravitino/utils/string_utils.py
@@ -15,6 +15,29 @@
 # specific language governing permissions and limitations
 # under the License.
 
-from gravitino.utils.http_client import Response, HTTPClient, unpack
 
-__all__ = ["Response", "HTTPClient", "unpack"]
+class StringUtils:
+    @classmethod
+    def is_blank(cls, s: str) -> bool:
+        """Checks if a string is blank (null, empty, or only whitespace).
+
+        Args:
+            s: The string to check.
+
+        Returns:
+            True if the string is blank, False otherwise.
+        """
+        return s is None or s.strip() == ""
+
+    @classmethod
+    def is_not_blank(cls, s: str) -> bool:
+        """
+        Checks if a string is not blank (not null, not empty, and not only 
whitespace).
+
+        Args:
+            s (str): The string to check.
+
+        Returns:
+            bool: True if the string is not blank, False otherwise.
+        """
+        return not cls.is_blank(s)
diff --git 
a/clients/client-python/tests/unittests/dto/requests/test_table_update_request.py
 
b/clients/client-python/tests/unittests/dto/requests/test_table_update_request.py
new file mode 100644
index 0000000000..cce1b6617d
--- /dev/null
+++ 
b/clients/client-python/tests/unittests/dto/requests/test_table_update_request.py
@@ -0,0 +1,649 @@
+# 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 as _json
+import unittest
+
+from gravitino.api.rel.expressions.expression import Expression
+from gravitino.api.rel.indexes.index import Index
+from gravitino.api.rel.indexes.indexes import Indexes
+from gravitino.api.rel.table_change import TableChange
+from gravitino.api.rel.types.types import Types
+from gravitino.dto.rel.expressions.literal_dto import LiteralDTO
+from gravitino.dto.requests.table_update_request import TableUpdateRequest
+
+
+class TestTableUpdateRequest(unittest.TestCase):
+    def test_rename_table_request_validate(self) -> None:
+        invalid_request = TableUpdateRequest.RenameTableRequest("")
+
+        with self.assertRaises(ValueError):
+            invalid_request.validate()
+
+    def test_rename_table_request_serialize(self) -> None:
+        request = TableUpdateRequest.RenameTableRequest("newTable")
+        json_str = _json.dumps(
+            {
+                "@type": "rename",
+                "newName": "newTable",
+            },
+            ensure_ascii=False,
+        )
+
+        self.assertEqual(json_str, request.to_json())
+
+    def test_rename_table_request_serialize_with_new_schema_name(self) -> None:
+        request = TableUpdateRequest.RenameTableRequest("newTable", 
"newSchema")
+        json_str = _json.dumps(
+            {
+                "@type": "rename",
+                "newName": "newTable",
+                "newSchemaName": "newSchema",
+            },
+            ensure_ascii=False,
+        )
+
+        self.assertEqual(json_str, request.to_json())
+
+    def test_update_table_comment_request_serialize(self) -> None:
+        request = TableUpdateRequest.UpdateTableCommentRequest("new comment")
+        json_str = _json.dumps(
+            {
+                "@type": "updateComment",
+                "newComment": "new comment",
+            },
+            ensure_ascii=False,
+        )
+
+        self.assertEqual(json_str, request.to_json())
+
+    def test_set_table_property_request_validate(self) -> None:
+        invalid_request1 = TableUpdateRequest.SetTablePropertyRequest("", 
"value")
+        invalid_request2 = TableUpdateRequest.SetTablePropertyRequest("key", 
"")
+
+        with self.assertRaises(ValueError):
+            invalid_request1.validate()
+
+        with self.assertRaises(ValueError):
+            invalid_request2.validate()
+
+    def test_set_table_property_request_serialize(self) -> None:
+        request = TableUpdateRequest.SetTablePropertyRequest("key", "value1")
+        json_str = _json.dumps(
+            {
+                "@type": "setProperty",
+                "property": "key",
+                "value": "value1",
+            },
+            ensure_ascii=False,
+        )
+
+        self.assertEqual(json_str, request.to_json())
+
+    def test_remove_table_property_request_validate(self) -> None:
+        invalid_request = TableUpdateRequest.RemoveTablePropertyRequest("")
+
+        with self.assertRaises(ValueError):
+            invalid_request.validate()
+
+    def test_remove_table_property_request_serialize(self) -> None:
+        request = TableUpdateRequest.RemoveTablePropertyRequest("prop1")
+        json_str = _json.dumps(
+            {
+                "@type": "removeProperty",
+                "property": "prop1",
+            },
+            ensure_ascii=False,
+        )
+
+        self.assertEqual(json_str, request.to_json())
+
+    def test_add_table_column_request_validate(self) -> None:
+        invalid_request = TableUpdateRequest.AddTableColumnRequest(
+            [],
+            Types.StringType.get(),
+            "comment",
+            TableChange.ColumnPosition.after("afterColumn"),
+            LiteralDTO.builder()
+            .with_data_type(Types.StringType.get())
+            .with_value("hello")
+            .build(),
+            False,
+            False,
+        )
+
+        with self.assertRaises(ValueError):
+            invalid_request.validate()
+
+        invalid_request = TableUpdateRequest.AddTableColumnRequest(
+            [" ", "column"],
+            Types.StringType.get(),
+            "comment",
+            TableChange.ColumnPosition.after("afterColumn"),
+            LiteralDTO.builder()
+            .with_data_type(Types.StringType.get())
+            .with_value("hello")
+            .build(),
+            False,
+            False,
+        )
+        with self.assertRaises(ValueError):
+            invalid_request.validate()
+
+        invalid_request = TableUpdateRequest.AddTableColumnRequest(
+            ["column"],
+            None,
+            "comment",
+            TableChange.ColumnPosition.after("afterColumn"),
+            LiteralDTO.builder()
+            .with_data_type(Types.StringType.get())
+            .with_value("hello")
+            .build(),
+            False,
+            False,
+        )
+        with self.assertRaises(ValueError):
+            invalid_request.validate()
+
+    def test_add_table_column_request_serialize(self) -> None:
+        request = TableUpdateRequest.AddTableColumnRequest(
+            ["column"],
+            Types.StringType.get(),
+            "comment",
+            TableChange.ColumnPosition.after("afterColumn"),
+            LiteralDTO.builder()
+            .with_data_type(Types.StringType.get())
+            .with_value("hello")
+            .build(),
+            False,
+            False,
+        )
+        json_str = _json.dumps(
+            {
+                "@type": "addColumn",
+                "fieldName": ["column"],
+                "type": "string",
+                "comment": "comment",
+                "position": {
+                    "after": "afterColumn",
+                },
+                "defaultValue": {
+                    "type": "literal",
+                    "dataType": "string",
+                    "value": "hello",
+                },
+                "nullable": False,
+                "autoIncrement": False,
+            }
+        )
+        self.assertEqual(json_str, request.to_json())
+
+        request = TableUpdateRequest.AddTableColumnRequest(
+            ["column"],
+            Types.StringType.get(),
+            None,
+            TableChange.ColumnPosition.after("afterColumn"),
+            LiteralDTO.builder()
+            .with_data_type(Types.StringType.get())
+            .with_value("hello")
+            .build(),
+            False,
+            False,
+        )
+        json_str = _json.dumps(
+            {
+                "@type": "addColumn",
+                "fieldName": ["column"],
+                "type": "string",
+                "comment": None,
+                "position": {
+                    "after": "afterColumn",
+                },
+                "defaultValue": {
+                    "type": "literal",
+                    "dataType": "string",
+                    "value": "hello",
+                },
+                "nullable": False,
+                "autoIncrement": False,
+            }
+        )
+
+        self.assertEqual(json_str, request.to_json())
+
+        request = TableUpdateRequest.AddTableColumnRequest(
+            ["column"],
+            Types.StringType.get(),
+            "comment",
+            TableChange.ColumnPosition.after("afterColumn"),
+            None,
+            False,
+            False,
+        )
+        json_str = _json.dumps(
+            {
+                "@type": "addColumn",
+                "fieldName": ["column"],
+                "type": "string",
+                "comment": "comment",
+                "position": {
+                    "after": "afterColumn",
+                },
+                "nullable": False,
+                "autoIncrement": False,
+            }
+        )
+
+        self.assertEqual(json_str, request.to_json())
+
+    def test_rename_table_column_request_validate(self) -> None:
+        invalid_request1 = TableUpdateRequest.RenameTableColumnRequest([], 
"new_column")
+        invalid_request2 = TableUpdateRequest.RenameTableColumnRequest(
+            ["old_column"], ""
+        )
+
+        with self.assertRaises(ValueError):
+            invalid_request1.validate()
+
+        with self.assertRaises(ValueError):
+            invalid_request2.validate()
+
+    def test_rename_table_column_request_serialize(self) -> None:
+        request = TableUpdateRequest.RenameTableColumnRequest(
+            ["oldColumn"], "newColumn"
+        )
+        json_str = _json.dumps(
+            {
+                "@type": "renameColumn",
+                "oldFieldName": ["oldColumn"],
+                "newFieldName": "newColumn",
+            },
+            ensure_ascii=False,
+        )
+
+        self.assertEqual(json_str, request.to_json())
+
+    def test_update_table_column_default_value_request_validate(self) -> None:
+        invalid_request = 
TableUpdateRequest.UpdateTableColumnDefaultValueRequest(
+            ["key"], Expression.EMPTY_EXPRESSION
+        )
+        with self.assertRaises(ValueError):
+            invalid_request.validate()
+
+        invalid_request = 
TableUpdateRequest.UpdateTableColumnDefaultValueRequest(
+            [],
+            LiteralDTO.builder()
+            .with_data_type(Types.DateType.get())
+            .with_value("2023-04-01")
+            .build(),
+        )
+
+        with self.assertRaises(ValueError):
+            invalid_request.validate()
+
+        invalid_request = 
TableUpdateRequest.UpdateTableColumnDefaultValueRequest(
+            [" ", "key"],
+            LiteralDTO.builder()
+            .with_data_type(Types.DateType.get())
+            .with_value("2023-04-01")
+            .build(),
+        )
+        with self.assertRaises(ValueError):
+            invalid_request.validate()
+
+        invalid_request = 
TableUpdateRequest.UpdateTableColumnDefaultValueRequest(
+            ["key"],
+            None,
+        )
+
+        with self.assertRaises(ValueError):
+            invalid_request.validate()
+
+    def test_update_table_column_default_value_request_serialize(self) -> None:
+        request = TableUpdateRequest.UpdateTableColumnDefaultValueRequest(
+            ["key"],
+            LiteralDTO.builder()
+            .with_data_type(Types.DateType.get())
+            .with_value("2023-04-01")
+            .build(),
+        )
+        json_str = _json.dumps(
+            {
+                "@type": "updateColumnDefaultValue",
+                "fieldName": ["key"],
+                "newDefaultValue": {
+                    "type": "literal",
+                    "dataType": "date",
+                    "value": "2023-04-01",
+                },
+            }
+        )
+
+        self.assertEqual(json_str, request.to_json())
+
+    def test_update_table_column_comment_request_validate(self) -> None:
+        invalid_request1 = TableUpdateRequest.UpdateTableColumnCommentRequest(
+            [], "new comment"
+        )
+        invalid_request2 = TableUpdateRequest.UpdateTableColumnCommentRequest(
+            [" ", "column2"], "new comment"
+        )
+        invalid_request3 = TableUpdateRequest.UpdateTableColumnCommentRequest(
+            ["column"], ""
+        )
+
+        with self.assertRaises(ValueError):
+            invalid_request1.validate()
+
+        with self.assertRaises(ValueError):
+            invalid_request2.validate()
+
+        with self.assertRaises(ValueError):
+            invalid_request3.validate()
+
+    def test_update_table_column_comment_request_serialize(self) -> None:
+        request = TableUpdateRequest.UpdateTableColumnCommentRequest(
+            ["column"], "new comment"
+        )
+        json_str = _json.dumps(
+            {
+                "@type": "updateColumnComment",
+                "fieldName": ["column"],
+                "newComment": "new comment",
+            },
+            ensure_ascii=False,
+        )
+
+        self.assertEqual(json_str, request.to_json())
+
+    def test_update_table_column_nullability_request_validate(self) -> None:
+        invalid_request1 = 
TableUpdateRequest.UpdateTableColumnNullabilityRequest(
+            [], True
+        )
+        invalid_request2 = 
TableUpdateRequest.UpdateTableColumnNullabilityRequest(
+            [" ", "column2"], False
+        )
+
+        with self.assertRaises(ValueError):
+            invalid_request1.validate()
+
+        with self.assertRaises(ValueError):
+            invalid_request2.validate()
+
+    def test_update_table_column_nullability_request_serialize(self) -> None:
+        request = TableUpdateRequest.UpdateTableColumnNullabilityRequest(
+            ["column"], False
+        )
+        json_str = _json.dumps(
+            {
+                "@type": "updateColumnNullability",
+                "fieldName": ["column"],
+                "nullable": False,
+            },
+            ensure_ascii=False,
+        )
+
+        self.assertEqual(json_str, request.to_json())
+
+    def test_delete_column_request_validate(self) -> None:
+        invalid_request1 = TableUpdateRequest.DeleteTableColumnRequest([], 
False)
+        invalid_request2 = TableUpdateRequest.DeleteTableColumnRequest([" ", " 
"], True)
+
+        with self.assertRaises(ValueError):
+            invalid_request1.validate()
+
+        with self.assertRaises(ValueError):
+            invalid_request2.validate()
+
+    def test_delete_column_request_serialize(self) -> None:
+        request = TableUpdateRequest.DeleteTableColumnRequest(
+            [
+                "column1",
+                "column2",
+            ],
+            True,
+        )
+        json_str = _json.dumps(
+            {
+                "@type": "deleteColumn",
+                "fieldName": ["column1", "column2"],
+                "ifExists": True,
+            },
+            ensure_ascii=False,
+        )
+
+        self.assertEqual(json_str, request.to_json())
+
+    def test_add_table_index_request_validate(self) -> None:
+        invalid_request1 = TableUpdateRequest.AddTableIndexRequest(
+            Indexes.of(
+                Index.IndexType.UNIQUE_KEY,
+                "",
+                [["column1"]],
+            )
+        )
+        invalid_request2 = TableUpdateRequest.AddTableIndexRequest(
+            Indexes.of(
+                Index.IndexType.UNIQUE_KEY,
+                "index_name",
+                [],
+            )
+        )
+
+        with self.assertRaises(ValueError):
+            invalid_request1.validate()
+
+        with self.assertRaises(ValueError):
+            invalid_request2.validate()
+
+    def test_add_table_index_request_serialize(self) -> None:
+        request = TableUpdateRequest.AddTableIndexRequest(
+            Indexes.of(
+                Index.IndexType.PRIMARY_KEY,
+                Indexes.DEFAULT_MYSQL_PRIMARY_KEY_NAME,
+                [["column1"]],
+            )
+        )
+        json_str = _json.dumps(
+            {
+                "@type": "addTableIndex",
+                "index": {
+                    "indexType": Index.IndexType.PRIMARY_KEY,
+                    "name": Indexes.DEFAULT_MYSQL_PRIMARY_KEY_NAME,
+                    "fieldNames": [["column1"]],
+                },
+            }
+        )
+        self.assertEqual(json_str, request.to_json())
+
+    def test_delete_table_index_request_validate(self) -> None:
+        invalid_request = TableUpdateRequest.DeleteTableIndexRequest("", True)
+
+        with self.assertRaises(ValueError):
+            invalid_request.validate()
+
+    def test_delete_table_index_request_serialize(self) -> None:
+        request = TableUpdateRequest.DeleteTableIndexRequest("uk_2", True)
+        json_str = _json.dumps(
+            {
+                "@type": "deleteTableIndex",
+                "name": "uk_2",
+                "ifExists": True,
+            },
+            ensure_ascii=False,
+        )
+
+        self.assertEqual(json_str, request.to_json())
+
+    def test_update_table_column_position_request_validate(self) -> None:
+        invalid_request = TableUpdateRequest.UpdateTableColumnPositionRequest(
+            [], TableChange.ColumnPosition.first()
+        )
+
+        with self.assertRaises(ValueError):
+            invalid_request.validate()
+
+        invalid_request = TableUpdateRequest.UpdateTableColumnPositionRequest(
+            ["column", " "], TableChange.ColumnPosition.first()
+        )
+
+        with self.assertRaises(ValueError):
+            invalid_request.validate()
+
+        invalid_request = TableUpdateRequest.UpdateTableColumnPositionRequest(
+            ["column"], None
+        )
+
+        with self.assertRaises(ValueError):
+            invalid_request.validate()
+
+    def test_update_table_column_position_request_serialize(self) -> None:
+        request1 = TableUpdateRequest.UpdateTableColumnPositionRequest(
+            ["column"], TableChange.ColumnPosition.first()
+        )
+        json_str1 = _json.dumps(
+            {
+                "@type": "updateColumnPosition",
+                "fieldName": ["column"],
+                "newPosition": "first",
+            },
+            ensure_ascii=False,
+        )
+
+        self.assertEqual(json_str1, request1.to_json())
+
+        request2 = TableUpdateRequest.UpdateTableColumnPositionRequest(
+            ["column"], TableChange.ColumnPosition.default_pos()
+        )
+        json_str2 = _json.dumps(
+            {
+                "@type": "updateColumnPosition",
+                "fieldName": ["column"],
+                "newPosition": "default",
+            }
+        )
+
+        self.assertEqual(json_str2, request2.to_json())
+
+        request3 = TableUpdateRequest.UpdateTableColumnPositionRequest(
+            ["column"], TableChange.ColumnPosition.after("another_column")
+        )
+        json_str3 = _json.dumps(
+            {
+                "@type": "updateColumnPosition",
+                "fieldName": ["column"],
+                "newPosition": {"after": "another_column"},
+            }
+        )
+
+        self.assertEqual(json_str3, request3.to_json())
+
+    def test_update_column_auto_increment_request_validate(self) -> None:
+        invalid_request1 = 
TableUpdateRequest.UpdateColumnAutoIncrementRequest([], True)
+        invalid_request2 = TableUpdateRequest.UpdateColumnAutoIncrementRequest(
+            [" ", "column2"], False
+        )
+
+        with self.assertRaises(ValueError):
+            invalid_request1.validate()
+
+        with self.assertRaises(ValueError):
+            invalid_request2.validate()
+
+    def test_update_column_auto_increment_request_serialize(self) -> None:
+        request = TableUpdateRequest.UpdateColumnAutoIncrementRequest(
+            [
+                "column1",
+                "column2",
+            ],
+            False,
+        )
+        json_str = _json.dumps(
+            {
+                "@type": "updateColumnAutoIncrement",
+                "fieldName": ["column1", "column2"],
+                "autoIncrement": False,
+            }
+        )
+        self.assertEqual(json_str, request.to_json())
+
+    def test_update_table_column_type_request_validate(self) -> None:
+        invalid_request = TableUpdateRequest.UpdateTableColumnTypeRequest(
+            [], Types.StringType.get()
+        )
+        with self.assertRaises(ValueError):
+            invalid_request.validate()
+
+        invalid_request = TableUpdateRequest.UpdateTableColumnTypeRequest(
+            [" ", "column"], Types.StringType.get()
+        )
+        with self.assertRaises(ValueError):
+            invalid_request.validate()
+
+        invalid_request = TableUpdateRequest.UpdateTableColumnTypeRequest(
+            ["column"], None
+        )
+
+        with self.assertRaises(ValueError):
+            invalid_request.validate()
+
+    def test_update_table_column_type_request_serialize(self) -> None:
+        request1 = TableUpdateRequest.UpdateTableColumnTypeRequest(
+            ["column1"], Types.StringType.get()
+        )
+
+        json_str1 = _json.dumps(
+            {
+                "@type": "updateColumnType",
+                "fieldName": ["column1"],
+                "newType": "string",
+            }
+        )
+        self.assertEqual(json_str1, request1.to_json())
+
+        request2 = TableUpdateRequest.UpdateTableColumnTypeRequest(
+            ["column2"],
+            Types.StructType.of(
+                Types.StructType.Field.not_null_field("id", 
Types.IntegerType.get()),
+                Types.StructType.Field.not_null_field(
+                    "name", Types.StringType.get(), "name field"
+                ),
+            ),
+        )
+        json_str2 = _json.dumps(
+            {
+                "@type": "updateColumnType",
+                "fieldName": ["column2"],
+                "newType": {
+                    "type": "struct",
+                    "fields": [
+                        {
+                            "name": "id",
+                            "type": "integer",
+                            "nullable": False,
+                        },
+                        {
+                            "name": "name",
+                            "type": "string",
+                            "nullable": False,
+                            "comment": "name field",
+                        },
+                    ],
+                },
+            }
+        )
+        self.assertEqual(json_str2, request2.to_json())
diff --git a/clients/client-python/gravitino/dto/rel/json_serdes/__init__.py 
b/clients/client-python/tests/unittests/json_serdes/__init__.py
similarity index 100%
copy from clients/client-python/gravitino/dto/rel/json_serdes/__init__.py
copy to clients/client-python/tests/unittests/json_serdes/__init__.py
diff --git 
a/clients/client-python/tests/unittests/json_serdes/test_type_serdes.py 
b/clients/client-python/tests/unittests/json_serdes/test_type_serdes.py
index 10f5f77eac..f20e55cb9d 100644
--- a/clients/client-python/tests/unittests/json_serdes/test_type_serdes.py
+++ b/clients/client-python/tests/unittests/json_serdes/test_type_serdes.py
@@ -19,10 +19,23 @@ import random
 import unittest
 from itertools import combinations, product
 
+from gravitino.api.rel.expressions.expression import Expression
+from gravitino.api.rel.indexes.index import Index
+from gravitino.api.rel.indexes.indexes import Indexes
+from gravitino.api.rel.table_change import After, Default, First
 from gravitino.api.rel.types.json_serdes import TypeSerdes
 from gravitino.api.rel.types.json_serdes._helper.serdes_utils import 
SerdesUtils
 from gravitino.api.rel.types.type import PrimitiveType
 from gravitino.api.rel.types.types import Types
+from gravitino.dto.rel.expressions.field_reference_dto import FieldReferenceDTO
+from gravitino.dto.rel.expressions.func_expression_dto import FuncExpressionDTO
+from gravitino.dto.rel.expressions.json_serdes.column_default_value_serdes 
import (
+    ColumnDefaultValueSerdes,
+)
+from gravitino.dto.rel.expressions.literal_dto import LiteralDTO
+from gravitino.dto.rel.expressions.unparsed_expression_dto import 
UnparsedExpressionDTO
+from gravitino.dto.rel.indexes.json_serdes.index_serdes import IndexSerdes
+from gravitino.dto.rel.json_serdes.column_position_serdes import 
ColumnPositionSerdes
 from gravitino.exceptions.base import IllegalArgumentException
 
 
@@ -57,7 +70,7 @@ class TestTypeSerdes(unittest.TestCase):
 
     def test_serialize_primitive_and_none_type(self):
         for simple_string, type_ in self._primitive_and_none_types.items():
-            self.assertEqual(TypeSerdes.serialize(data_type=type_), 
simple_string)
+            self.assertEqual(TypeSerdes.serialize(type_), simple_string)
 
     def test_serialize_struct_type_of_primitive_and_none_types(self):
         types = self._primitive_and_none_types.values()
@@ -394,3 +407,233 @@ class TestTypeSerdes(unittest.TestCase):
         self.assertEqual(deserialized, timestamp_tz_without_precision)
         self.assertFalse(deserialized.has_precision_set())
         self.assertTrue(deserialized.has_time_zone())
+
+    def test_column_default_value_encoder_none(self) -> None:
+        self.assertIsNone(ColumnDefaultValueSerdes.serialize(None))
+        self.assertIsNone(
+            ColumnDefaultValueSerdes.serialize(Expression.EMPTY_EXPRESSION)
+        )
+
+    def test_column_default_value_encoder_with_literal(self) -> None:
+        literal = (
+            LiteralDTO.builder()
+            .with_data_type(Types.DateType.get())
+            .with_value("2023-04-01")
+            .build()
+        )
+
+        serialized = ColumnDefaultValueSerdes.serialize(literal)
+        expected = {
+            "type": "literal",
+            "dataType": "date",
+            "value": "2023-04-01",
+        }
+        self.assertEqual(expected, serialized)
+
+    def test_column_default_value_encoder_with_field(self) -> None:
+        field = (
+            FieldReferenceDTO.builder().with_field_name(["field1", 
"field2"]).build()
+        )
+
+        serialized = ColumnDefaultValueSerdes.serialize(field)
+        expected = {
+            "type": "field",
+            "fieldName": ["field1", "field2"],
+        }
+        self.assertEqual(expected, serialized)
+
+    def test_column_default_value_encoder_with_function(self) -> None:
+        arg1 = FieldReferenceDTO.builder().with_field_name(["dt"]).build()
+        arg2 = (
+            LiteralDTO.builder()
+            .with_data_type(Types.StringType.get())
+            .with_value("Asia/Shanghai")
+            .build()
+        )
+        to_date_func = (
+            FuncExpressionDTO.builder()
+            .with_function_name("toDate")
+            .with_function_args([arg1, arg2])
+            .build()
+        )
+
+        serialized = ColumnDefaultValueSerdes.serialize(to_date_func)
+        expected = {
+            "type": "function",
+            "funcName": "toDate",
+            "funcArgs": [
+                {
+                    "type": "field",
+                    "fieldName": ["dt"],
+                },
+                {
+                    "type": "literal",
+                    "dataType": "string",
+                    "value": "Asia/Shanghai",
+                },
+            ],
+        }
+        self.assertEqual(expected, serialized)
+
+    def test_column_default_value_encoder_with_unparsed(self) -> None:
+        unparsed = (
+            
UnparsedExpressionDTO.builder().with_unparsed_expression("customer").build()
+        )
+        serialized = ColumnDefaultValueSerdes.serialize(unparsed)
+        expected = {
+            "type": "unparsed",
+            "unparsedExpression": "customer",
+        }
+        self.assertEqual(expected, serialized)
+
+    def test_column_default_value_decoder_with_none(self) -> None:
+        self.assertEqual(
+            Expression.EMPTY_EXPRESSION, 
ColumnDefaultValueSerdes.deserialize(None)
+        )
+
+    def test_column_default_value_decoder_with_literal(self) -> None:
+        json_str = {
+            "type": "literal",
+            "dataType": "string",
+            "value": "Asia/Shanghai",
+        }
+        expr: LiteralDTO = ColumnDefaultValueSerdes.deserialize(json_str)
+        self.assertEqual("Asia/Shanghai", expr.value())
+        self.assertEqual(expr.data_type(), Types.StringType.get())
+
+    def test_column_default_value_decoder_with_field(self) -> None:
+        json_str = {
+            "type": "field",
+            "fieldName": ["field1", "field2"],
+        }
+        expr: FieldReferenceDTO = 
ColumnDefaultValueSerdes.deserialize(json_str)
+        self.assertEqual(expr.field_name(), ["field1", "field2"])
+
+    def test_column_default_value_decoder_with_function(self) -> None:
+        json_str = {
+            "type": "function",
+            "funcName": "toDate",
+            "funcArgs": [
+                {
+                    "type": "field",
+                    "fieldName": ["dt"],
+                },
+                {
+                    "type": "literal",
+                    "dataType": "string",
+                    "value": "Asia/Shanghai",
+                },
+            ],
+        }
+
+        expr: FuncExpressionDTO = 
ColumnDefaultValueSerdes.deserialize(json_str)
+        self.assertEqual("toDate", expr.function_name())
+        self.assertEqual(2, len(expr.args()))
+
+    def test_column_default_value_decoder_with_unparsed(self) -> None:
+        json_str = {"type": "unparsed", "unparsedExpression": "unparsed 
expression"}
+
+        expr: UnparsedExpressionDTO = 
ColumnDefaultValueSerdes.deserialize(json_str)
+        self.assertEqual(expr.unparsed_expression(), "unparsed expression")
+
+    def test_column_position_serializer(self) -> None:
+        with self.assertRaises(ValueError):
+            ColumnPositionSerdes.serialize(None)
+
+        self.assertEqual(
+            ColumnPositionSerdes.serialize(First()),
+            "first",
+        )
+
+        self.assertEqual(
+            ColumnPositionSerdes.serialize(After("colA")),
+            {"after": "colA"},
+        )
+
+        self.assertEqual(
+            ColumnPositionSerdes.serialize(Default()),
+            "default",
+        )
+
+    def test_column_position_deserializer(self) -> None:
+        with self.assertRaises(ValueError):
+            ColumnPositionSerdes.deserialize(None)
+
+        self.assertIsInstance(
+            ColumnPositionSerdes.deserialize("first"),
+            First,
+        )
+
+        self.assertIsInstance(
+            ColumnPositionSerdes.deserialize("FIRST"),
+            First,
+        )
+
+        self.assertIsInstance(
+            ColumnPositionSerdes.deserialize({"after": "colA"}),
+            After,
+        )
+
+        self.assertEqual(
+            ColumnPositionSerdes.deserialize({"after": "colA"}).get_column(),
+            "colA",
+        )
+
+        self.assertIsInstance(
+            ColumnPositionSerdes.deserialize("default"),
+            Default,
+        )
+
+        self.assertIsInstance(
+            ColumnPositionSerdes.deserialize("DEFAULT"),
+            Default,
+        )
+
+    def test_table_index_serializer(self) -> None:
+        index_obj = Indexes.create_mysql_primary_key([["a", "b"]])
+        serialized = IndexSerdes.serialize(index_obj)
+        expected = {
+            "indexType": "PRIMARY_KEY",
+            "name": Indexes.DEFAULT_MYSQL_PRIMARY_KEY_NAME,
+            "fieldNames": [["a", "b"]],
+        }
+        self.assertEqual(serialized, expected)
+
+        index_obj = Indexes.of(Index.IndexType.PRIMARY_KEY, None, [["a", "b"]])
+        serialized = IndexSerdes.serialize(index_obj)
+        expected = {
+            "indexType": "PRIMARY_KEY",
+            "fieldNames": [["a", "b"]],
+        }
+        self.assertEqual(serialized, expected)
+
+        index_obj = Indexes.unique("uk_1", [["a", "b"]])
+        serialized = IndexSerdes.serialize(index_obj)
+        expected = {
+            "indexType": "UNIQUE_KEY",
+            "name": "uk_1",
+            "fieldNames": [["a", "b"]],
+        }
+        self.assertEqual(expected, serialized)
+
+    def test_table_index_deserialize(self) -> None:
+        data = {
+            "indexType": "PRIMARY_KEY",
+            "name": "idx_test",
+            "fieldNames": ["a", "b"],
+        }
+
+        result = IndexSerdes.deserialize(data)
+        self.assertEqual(result.name(), "idx_test")
+        self.assertEqual(result.type(), Index.IndexType.PRIMARY_KEY)
+        self.assertEqual(result.field_names(), ["a", "b"])
+
+        data = {
+            "indexType": "PRIMARY_KEY",
+            "fieldNames": ["a", "b"],
+        }
+
+        result = IndexSerdes.deserialize(data)
+        self.assertIsNone(result.name())
+        self.assertEqual(result.type(), Index.IndexType.PRIMARY_KEY)
+        self.assertEqual(result.field_names(), ["a", "b"])

Reply via email to