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