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

mchades 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 fabdbc9aa [#5200]Update the Column data type for python client (#5354)
fabdbc9aa is described below

commit fabdbc9aa424aa76bf6109244b64c40e7acad934
Author: SophieTech88 <[email protected]>
AuthorDate: Wed Nov 6 08:04:06 2024 -0600

    [#5200]Update the Column data type for python client (#5354)
    
    ### What changes were proposed in this pull request?
    1. Implement type.java as type.py in Python client
    2.  Implement types.java as types.py in Python client
    3.  Create a test_types.py in unit tests.
    
    ### Why are the changes needed?
    We need to support the column data type in python client
    
    Fix: #5200
    
    ### Does this PR introduce _any_ user-facing change?
    No
    
    ### How was this patch tested?
    Need to add the unit tests
    
    ---------
    
    Co-authored-by: Xun <[email protected]>
---
 clients/client-python/gravitino/api/type.py        |  180 ++++
 clients/client-python/gravitino/api/types.py       | 1106 ++++++++++++++++++++
 .../tests/unittests/test_gvfs_with_local.py        |    1 +
 .../client-python/tests/unittests/test_types.py    |  195 ++++
 docs/manage-relational-metadata-using-gravitino.md |    2 +-
 5 files changed, 1483 insertions(+), 1 deletion(-)

diff --git a/clients/client-python/gravitino/api/type.py 
b/clients/client-python/gravitino/api/type.py
new file mode 100644
index 000000000..9b089ea18
--- /dev/null
+++ b/clients/client-python/gravitino/api/type.py
@@ -0,0 +1,180 @@
+# 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 abc import ABC, abstractmethod
+from enum import Enum
+
+
+class Name(Enum):
+    """
+    The root type name of this type, representing all data types supported.
+    """
+
+    BOOLEAN = "BOOLEAN"
+    """ The boolean type. """
+
+    BYTE = "BYTE"
+    """ The byte type. """
+
+    SHORT = "SHORT"
+    """ The short type. """
+
+    INTEGER = "INTEGER"
+    """ The integer type. """
+
+    LONG = "LONG"
+    """ The long type. """
+
+    FLOAT = "FLOAT"
+    """ The float type. """
+
+    DOUBLE = "DOUBLE"
+    """ The double type. """
+
+    DECIMAL = "DECIMAL"
+    """ The decimal type. """
+
+    DATE = "DATE"
+    """ The date type. """
+
+    TIME = "TIME"
+    """ The time type. """
+
+    TIMESTAMP = "TIMESTAMP"
+    """ The timestamp type. """
+
+    INTERVAL_YEAR = "INTERVAL_YEAR"
+    """ The interval year type. """
+
+    INTERVAL_DAY = "INTERVAL_DAY"
+    """ The interval day type. """
+
+    STRING = "STRING"
+    """ The string type. """
+
+    VARCHAR = "VARCHAR"
+    """ The varchar type. """
+
+    FIXEDCHAR = "FIXEDCHAR"
+    """ The char type with fixed length. """
+
+    UUID = "UUID"
+    """ The UUID type. """
+
+    FIXED = "FIXED"
+    """ The binary type with fixed length. """
+
+    BINARY = "BINARY"
+    """ The binary type with variable length. The length is specified in the 
type itself. """
+
+    STRUCT = "STRUCT"
+    """
+    The struct type.
+    A struct type is a complex type that contains a set of named fields, each 
with a type, 
+    and optionally a comment.
+    """
+
+    LIST = "LIST"
+    """
+    The list type.
+    A list type is a complex type that contains a set of elements, each with 
the same type.
+    """
+
+    MAP = "MAP"
+    """
+    The map type.
+    A map type is a complex type that contains a set of key-value pairs, each 
with a key type 
+    and a value type.
+    """
+
+    UNION = "UNION"
+    """
+    The union type.
+    A union type is a complex type that contains a set of types.
+    """
+
+    NULL = "NULL"
+    """ The null type. A null type represents a value that is null. """
+
+    UNPARSED = "UNPARSED"
+    """ The unparsed type. An unparsed type represents an unresolvable type. 
"""
+
+    EXTERNAL = "EXTERNAL"
+    """ The external type. An external type represents a type that is not 
supported. """
+
+
+# Define the Type interface (abstract base class)
+class Type(ABC):
+    @abstractmethod
+    def name(self) -> Name:
+        """Returns the generic name of the type."""
+        pass
+
+    @abstractmethod
+    def simple_string(self) -> str:
+        """Returns a readable string representation of the type."""
+        pass
+
+
+# Define base classes
+class PrimitiveType(Type, ABC):
+    """Base class for all primitive types."""
+
+    pass
+
+
+class NumericType(PrimitiveType, ABC):
+    """Base class for all numeric types."""
+
+    pass
+
+
+class DateTimeType(PrimitiveType, ABC):
+    """Base class for all date/time types."""
+
+    pass
+
+
+class IntervalType(PrimitiveType, ABC):
+    """Base class for all interval types."""
+
+    pass
+
+
+class ComplexType(Type, ABC):
+    """Base class for all complex types, including struct, list, map, and 
union."""
+
+    pass
+
+
+class IntegralType(NumericType, ABC):
+    """Base class for all integral types."""
+
+    _signed: bool
+
+    def __init__(self, signed: bool):
+        self._signed = signed
+
+    def signed(self) -> bool:
+        """Returns True if the integer type is signed, False otherwise."""
+        return self._signed
+
+
+class FractionType(NumericType, ABC):
+    """Base class for all fractional types."""
+
+    pass
diff --git a/clients/client-python/gravitino/api/types.py 
b/clients/client-python/gravitino/api/types.py
new file mode 100644
index 000000000..b82ac2b68
--- /dev/null
+++ b/clients/client-python/gravitino/api/types.py
@@ -0,0 +1,1106 @@
+# 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.
+# pylint: disable=C0302
+from __future__ import annotations
+from typing import List
+from .type import (
+    Type,
+    Name,
+    PrimitiveType,
+    IntegralType,
+    FractionType,
+    DateTimeType,
+    IntervalType,
+    ComplexType,
+)
+
+
+class Types:
+    """The helper class for Type. It contains all built-in types and provides 
utility methods."""
+
+    class NullType(Type):
+        """The data type representing `NULL` values."""
+
+        _instance: "NullType" = None
+
+        def __new__(cls):
+            if cls._instance is None:
+                cls._instance = super(Types.NullType, cls).__new__(cls)
+            return cls._instance
+
+        @classmethod
+        def get(cls) -> "NullType":
+            return cls()
+
+        def name(self) -> Name:
+            return Name.NULL
+
+        def simple_string(self) -> str:
+            return "null"
+
+    class BooleanType(PrimitiveType):
+        """The boolean type in Gravitino."""
+
+        _instance: "BooleanType" = None
+
+        def __new__(cls):
+            if cls._instance is None:
+                cls._instance = super(Types.BooleanType, cls).__new__(cls)
+            return cls._instance
+
+        @classmethod
+        def get(cls) -> "BooleanType":
+            return cls()
+
+        def name(self) -> Name:
+            return Name.BOOLEAN
+
+        def simple_string(self) -> str:
+            return "boolean"
+
+    class ByteType(IntegralType):
+        """The byte type in Gravitino."""
+
+        _instance: "ByteType" = None
+        _unsigned_instance: "ByteType" = None
+
+        def __new__(cls, signed: bool = True):
+            if signed:
+                if cls._instance is None:
+                    cls._instance = super(Types.ByteType, cls).__new__(cls)
+                    cls._instance.__init__(signed)
+                return cls._instance
+            if cls._unsigned_instance is None:
+                cls._unsigned_instance = super(Types.ByteType, 
cls).__new__(cls)
+                cls._unsigned_instance.__init__(signed)
+            return cls._unsigned_instance
+
+        @classmethod
+        def get(cls) -> "ByteType":
+            return cls(True)
+
+        @classmethod
+        def unsigned(cls) -> "ByteType":
+            return cls(False)
+
+        def name(self) -> Name:
+            return Name.BYTE
+
+        def simple_string(self) -> str:
+            return "byte" if self.signed() else "byte unsigned"
+
+    class ShortType(IntegralType):
+        _instance: "ShortType" = None
+        _unsigned_instance: "ShortType" = None
+
+        def __new__(cls, signed=True):
+            if signed:
+                if cls._instance is None:
+                    cls._instance = super(Types.ShortType, cls).__new__(cls)
+                    cls._instance.__init__(signed)
+                return cls._instance
+            if cls._unsigned_instance is None:
+                cls._unsigned_instance = super(Types.ShortType, 
cls).__new__(cls)
+                cls._unsigned_instance.__init__(signed)
+            return cls._unsigned_instance
+
+        @classmethod
+        def get(cls) -> "ShortType":
+            return cls(True)
+
+        @classmethod
+        def unsigned(cls):
+            return cls(False)
+
+        def name(self) -> Name:
+            return Name.SHORT
+
+        def simple_string(self) -> str:
+            return "short" if self.signed() else "short unsigned"
+
+    class IntegerType(IntegralType):
+        _instance: "IntegerType" = None
+        _unsigned_instance: "IntegerType" = None
+
+        def __new__(cls, signed=True):
+            if signed:
+                if cls._instance is None:
+                    cls._instance = super(Types.IntegerType, cls).__new__(cls)
+                    cls._instance.__init__(signed)
+                return cls._instance
+            if cls._unsigned_instance is None:
+                cls._unsigned_instance = super(Types.IntegerType, 
cls).__new__(cls)
+                cls._unsigned_instance.__init__(signed)
+            return cls._unsigned_instance
+
+        @classmethod
+        def get(cls) -> "IntegerType":
+            return cls(True)
+
+        @classmethod
+        def unsigned(cls):
+            return cls(False)
+
+        def name(self) -> Name:
+            return Name.INTEGER
+
+        def simple_string(self) -> str:
+            return "integer" if self.signed() else "integer unsigned"
+
+    class LongType(IntegralType):
+        _instance: "LongType" = None
+        _unsigned_instance: "LongType" = None
+
+        def __new__(cls, signed=True):
+            if signed:
+                if cls._instance is None:
+                    cls._instance = super(Types.LongType, cls).__new__(cls)
+                    cls._instance.__init__(signed)
+                return cls._instance
+            if cls._unsigned_instance is None:
+                cls._unsigned_instance = super(Types.LongType, 
cls).__new__(cls)
+                cls._unsigned_instance.__init__(signed)
+            return cls._unsigned_instance
+
+        @classmethod
+        def get(cls) -> "LongType":
+            return cls(True)
+
+        @classmethod
+        def unsigned(cls):
+            return cls(False)
+
+        def name(self) -> Name:
+            return Name.LONG
+
+        def simple_string(self) -> str:
+            return "long" if self.signed() else "long unsigned"
+
+    class FloatType(FractionType):
+        _instance: "FloatType" = None
+
+        def __new__(cls):
+            if cls._instance is None:
+                cls._instance = super(Types.FloatType, cls).__new__(cls)
+                cls._instance.__init__()
+            return cls._instance
+
+        @classmethod
+        def get(cls) -> "FloatType":
+            return cls()
+
+        def name(self) -> Name:
+            return Name.FLOAT
+
+        def simple_string(self) -> str:
+            return "float"
+
+    class DoubleType(FractionType):
+        _instance: "DoubleType" = None
+
+        def __new__(cls):
+            if cls._instance is None:
+                cls._instance = super(Types.DoubleType, cls).__new__(cls)
+                cls._instance.__init__()
+            return cls._instance
+
+        @classmethod
+        def get(cls) -> "DoubleType":
+            return cls()
+
+        def name(self) -> Name:
+            return Name.DOUBLE
+
+        def simple_string(self) -> str:
+            return "double"
+
+    class DecimalType(FractionType):
+        """The decimal type in Gravitino."""
+
+        MAX_PRECISION = 38
+        _precision: int
+        _scale: int
+
+        def __init__(self, precision: int, scale: int):
+            """
+            Args:
+                precision: The precision of the decimal type.
+                scale: The scale of the decimal type.
+            """
+            super().__init__()
+            self.check_precision_scale(precision, scale)
+            self._precision = precision
+            self._scale = scale
+
+        @staticmethod
+        def check_precision_scale(precision: int, scale: int):
+            """
+            Ensures the precision and scale values are within valid range.
+
+            Args:
+                precision: The precision of the decimal.
+                scale: The scale of the decimal.
+            """
+            if not 1 <= precision <= Types.DecimalType.MAX_PRECISION:
+                raise ValueError(
+                    f"Decimal precision must be in range [1, 38]: {precision}"
+                )
+            if not 0 <= scale <= precision:
+                raise ValueError(
+                    f"Decimal scale must be in range [0, precision 
({precision})]: {scale}"
+                )
+
+        @classmethod
+        def of(cls, precision: int, scale: int) -> "DecimalType":
+            return cls(precision, scale)
+
+        def name(self) -> Name:
+            return Name.DECIMAL
+
+        def precision(self) -> int:
+            return self._precision
+
+        def scale(self) -> int:
+            return self._scale
+
+        def simple_string(self) -> str:
+            return f"decimal({self._precision},{self._scale})"
+
+        def __eq__(self, other):
+            """
+            Compares two DecimalType objects for equality.
+
+            Args:
+                other: The other DecimalType to compare with.
+
+            Returns:
+                True if both objects have the same precision and scale, False 
otherwise.
+            """
+            if not isinstance(other, Types.DecimalType):
+                return False
+            return self._precision == other._precision and self._scale == 
other._scale
+
+        def __hash__(self):
+            return hash((self._precision, self._scale))
+
+    class DateType(DateTimeType):
+        """The date time type in Gravitino."""
+
+        _instance: "DateType" = None
+
+        def __new__(cls):
+            if cls._instance is None:
+                cls._instance = super(Types.DateType, cls).__new__(cls)
+                cls._instance.__init__()
+            return cls._instance
+
+        @classmethod
+        def get(cls) -> "DateType":
+            return cls()
+
+        def name(self) -> Name:
+            return Name.DATE
+
+        def simple_string(self) -> str:
+            return "date"
+
+    class TimeType(DateTimeType):
+        _instance: "TimeType" = None
+
+        def __new__(cls):
+            if cls._instance is None:
+                cls._instance = super(Types.TimeType, cls).__new__(cls)
+                cls._instance.__init__()
+            return cls._instance
+
+        @classmethod
+        def get(cls) -> "TimeType":
+            return cls()
+
+        def name(self) -> Name:
+            return Name.TIME
+
+        def simple_string(self) -> str:
+            return "time"
+
+    class TimestampType(DateTimeType):
+        _instance_with_tz: "TimestampType" = None
+        _instance_without_tz: "TimestampType" = None
+        _with_time_zone: bool
+
+        def __new__(cls, with_time_zone: bool):
+            if with_time_zone:
+                if cls._instance_with_tz is None:
+                    cls._instance_with_tz = super(Types.TimestampType, 
cls).__new__(cls)
+                    cls._instance_with_tz.__init__(with_time_zone)
+                return cls._instance_with_tz
+            if cls._instance_without_tz is None:
+                cls._instance_without_tz = super(Types.TimestampType, 
cls).__new__(cls)
+                cls._instance_without_tz.__init__(with_time_zone)
+            return cls._instance_without_tz
+
+        @classmethod
+        def with_time_zone(cls) -> "TimestampType":
+            return cls(True)
+
+        @classmethod
+        def without_time_zone(cls) -> "TimestampType":
+            return cls(False)
+
+        def __init__(self, with_time_zone: bool):
+            self._with_time_zone = with_time_zone
+            super().__init__()
+
+        def has_time_zone(self) -> bool:
+            return self._with_time_zone
+
+        def name(self) -> Name:
+            return Name.TIMESTAMP
+
+        def simple_string(self) -> str:
+            return "timestamp_tz" if self._with_time_zone else "timestamp"
+
+    class IntervalYearType(IntervalType):
+        """The interval year type in Gravitino."""
+
+        _instance: "IntervalYearType" = None
+
+        def __new__(cls):
+            if cls._instance is None:
+                cls._instance = super(Types.IntervalYearType, cls).__new__(cls)
+                cls._instance.__init__()
+            return cls._instance
+
+        @classmethod
+        def get(cls) -> "IntervalYearType":
+            return cls()
+
+        def name(self) -> Name:
+            return Name.INTERVAL_YEAR
+
+        def simple_string(self) -> str:
+            return "interval_year"
+
+    class IntervalDayType(IntervalType):
+        """The interval day type in Gravitino."""
+
+        _instance: "IntervalDayType" = None
+
+        def __new__(cls):
+            if cls._instance is None:
+                cls._instance = super(Types.IntervalDayType, cls).__new__(cls)
+                cls._instance.__init__()
+            return cls._instance
+
+        @classmethod
+        def get(cls) -> "IntervalDayType":
+            return cls()
+
+        def name(self) -> Name:
+            return Name.INTERVAL_DAY
+
+        def simple_string(self) -> str:
+            return "interval_day"
+
+    class StringType(PrimitiveType):
+        """The string type in Gravitino, equivalent to varchar(MAX),
+        which the MAX is determined by the underlying catalog."""
+
+        _instance: "StringType" = None
+
+        def __new__(cls):
+            if cls._instance is None:
+                cls._instance = super(Types.StringType, cls).__new__(cls)
+                cls._instance.__init__()
+            return cls._instance
+
+        @classmethod
+        def get(cls) -> "StringType":
+            return cls()
+
+        def name(self) -> Name:
+            return Name.STRING
+
+        def simple_string(self) -> str:
+            return "string"
+
+    class UUIDType(PrimitiveType):
+        """The uuid type in Gravitino."""
+
+        _instance: "UUIDType" = None
+
+        def __new__(cls):
+            if cls._instance is None:
+                cls._instance = super(Types.UUIDType, cls).__new__(cls)
+                cls._instance.__init__()
+            return cls._instance
+
+        @classmethod
+        def get(cls) -> "UUIDType":
+            return cls()
+
+        def name(self) -> Name:
+            return Name.UUID
+
+        def simple_string(self) -> str:
+            return "uuid"
+
+    class FixedType(PrimitiveType):
+        """Fixed-length byte array type, if you want to use variable-length
+        byte array, use BinaryType instead."""
+
+        _length: int
+
+        def __init__(self, length: int):
+            """
+            Initializes the FixedType with the given length.
+
+            Args:
+                length: The length of the fixed type.
+            """
+            self._length = length
+
+        @classmethod
+        def of(cls, length: int) -> "FixedType":
+            """
+            Args:
+                length: The length of the fixed type.
+
+            Returns:
+                 A FixedType instance with the given length.
+            """
+            return cls(length)
+
+        def name(self) -> Name:
+            return Name.FIXED
+
+        def length(self) -> int:
+            return self._length
+
+        def simple_string(self) -> str:
+            return f"fixed({self._length})"
+
+        def __eq__(self, other):
+            """
+            Compares two FixedType objects for equality.
+
+            Args:
+                other: The other FixedType object to compare with.
+
+            Returns:
+                True if both FixedType objects have the same length, False 
otherwise.
+            """
+            if not isinstance(other, Types.FixedType):
+                return False
+            return self._length == other._length
+
+        def __hash__(self):
+            return hash(self._length)
+
+    class VarCharType(PrimitiveType):
+        """The varchar type in Gravitino."""
+
+        _length: int
+
+        def __init__(self, length: int):
+            self._length = length
+
+        @classmethod
+        def of(cls, length: int) -> "VarCharType":
+            return cls(length)
+
+        def name(self) -> Name:
+            return Name.VARCHAR
+
+        def length(self) -> int:
+            return self._length
+
+        def simple_string(self) -> str:
+            return f"varchar({self._length})"
+
+        def __eq__(self, other):
+            """
+            Compares two VarCharType objects for equality.
+
+            Args:
+                other: The other VarCharType object to compare with.
+
+            Returns:
+                True if both VarCharType objects have the same length, False 
otherwise.
+            """
+            if isinstance(other, Types.VarCharType):
+                return self._length == other._length
+            return False
+
+        def __hash__(self):
+            return hash(self._length)
+
+    class FixedCharType(PrimitiveType):
+        """The fixed char type in Gravitino."""
+
+        _length: int
+
+        def __init__(self, length: int):
+            self._length = length
+
+        @classmethod
+        def of(cls, length: int) -> "FixedCharType":
+            return cls(length)
+
+        def name(self) -> Name:
+            return Name.FIXEDCHAR
+
+        def length(self) -> int:
+            return self._length
+
+        def simple_string(self) -> str:
+            return f"char({self._length})"
+
+        def __eq__(self, other):
+            if not isinstance(other, Types.FixedCharType):
+                return False
+            return self._length == other._length
+
+        def __hash__(self):
+            return hash(self._length)
+
+    class BinaryType(PrimitiveType):
+        _instance: "BinaryType" = None
+
+        def __new__(cls):
+            if cls._instance is None:
+                cls._instance = super(Types.BinaryType, cls).__new__(cls)
+                cls._instance.__init__()
+            return cls._instance
+
+        @classmethod
+        def get(cls) -> "BinaryType":
+            return cls()
+
+        def name(self) -> Name:
+            return Name.BINARY
+
+        def simple_string(self) -> str:
+            return "binary"
+
+    class StructType(ComplexType):
+        """The struct type in Gravitino.
+        Note, this type is not supported in the current version of 
Gravitino."""
+
+        _fields: List["Field"]
+
+        def __init__(self, fields: List["Field"]):
+            if not fields or len(fields) == 0:
+                raise ValueError("fields cannot be null or empty")
+            self._fields = fields
+
+        @classmethod
+        def of(cls, *fields) -> "StructType":
+            """
+            Args:
+                fields: The fields of the struct type.
+
+            Returns:
+                A StructType instance with the given fields.
+            """
+            return cls(fields)
+
+        def fields(self) -> List["Field"]:
+            return self._fields
+
+        def name(self) -> Name:
+            return Name.STRUCT
+
+        def simple_string(self) -> str:
+            return (
+                f"struct<{', '.join(field.simple_string() for field in 
self._fields)}>"
+            )
+
+        def __eq__(self, other):
+            """
+            Compares two StructType objects for equality.
+
+            Args:
+                other: The other StructType object to compare with.
+
+            Returns:
+                True if both StructType objects have the same fields, False 
otherwise.
+            """
+            if isinstance(other, Types.StructType):
+                return self._fields == other._fields
+            return False
+
+        def __hash__(self):
+            return hash(tuple(self._fields))
+
+        class Field:
+            _name: str
+            _type: Type
+            _nullable: bool
+            _comment: str
+
+            def __init__(
+                self, name: str, field_type: Type, nullable: bool, comment: str
+            ) -> None:
+                """
+                Initializes the Field with the given name, type, nullable 
flag, and comment.
+
+                Args:
+                    name: The name of the field.
+                    field_type: The type of the field.
+                    nullable: Whether the field is nullable.
+                    comment: The comment of the field (optional).
+                """
+                if name is None:
+                    raise ValueError("name cannot be null")
+                if type is None:
+                    raise ValueError("type cannot be null")
+                self._name = name
+                self._type = field_type
+                self._nullable = nullable
+                self._comment = comment
+
+            @classmethod
+            def not_null_field(
+                cls, name: str, field_type: Type, comment: str = None
+            ) -> "Field":
+                """
+                Args:
+                    name: The name of the field.
+                    field_type: The type of the field.
+                    comment: The comment of the field (optional).
+                """
+                return cls(name, field_type, False, comment)
+
+            @classmethod
+            def nullable_field(
+                cls, name: str, field_type: Type, comment: str = None
+            ) -> "Field":
+                """
+                Args:
+                    name: The name of the field.
+                    field_type: The type of the field.
+                    comment: The comment of the field (optional).
+
+                Returns:
+                    A nullable Field instance with the given name, field_type, 
and comment.
+                """
+                return cls(name, field_type, True, comment)
+
+            def name(self):
+                return self._name
+
+            def type(self):
+                return self._type
+
+            def nullable(self):
+                return self._nullable
+
+            def comment(self):
+                return self._comment
+
+            def __eq__(self, other):
+                """
+                Compares two Field objects for equality.
+
+                Args:
+                    other: The other Field object to compare with.
+
+                Returns:
+                    True if both Field objects have the same attributes, False 
otherwise.
+                """
+                if isinstance(other, Types.StructType.Field):
+                    return (
+                        self._name == other._name
+                        and self._type == other._type
+                        and self._nullable == other._nullable
+                        and self._comment == other._comment
+                    )
+                return False
+
+            def __hash__(self):
+                return hash((self._name, self._type, self._nullable))
+
+            def simple_string(self) -> str:
+                nullable_str = "NULL" if self._nullable else "NOT NULL"
+                comment_str = f" COMMENT '{self._comment}'" if self._comment 
else ""
+                return f"{self._name}: {self._type.simple_string()} 
{nullable_str}{comment_str}"
+
+    class ListType(ComplexType):
+        """A list type. Note, this type is not supported in the current 
version of Gravitino."""
+
+        _element_type: Type
+        _element_nullable: bool
+
+        def __init__(self, element_type: Type, element_nullable: bool):
+            """
+            Create a new ListType with the given element type and the type is 
nullable.
+
+            Args:
+                element_type: The element type of the list.
+                element_nullable: Whether the element of the list is nullable.
+            """
+            if element_type is None:
+                raise ValueError("element_type cannot be null")
+            self._element_type = element_type
+            self._element_nullable = element_nullable
+
+        @classmethod
+        def nullable(cls, element_type: Type) -> "ListType":
+            """
+            Create a new ListType with the given element type and the type is 
nullable.
+
+            Args:
+                element_type: The element type of the list.
+
+            Returns:
+                A new ListType instance.
+            """
+            return cls.of(element_type, True)
+
+        @classmethod
+        def not_null(cls, element_type: Type) -> "ListType":
+            """
+            Create a new ListType with the given element type.
+
+            Args:
+                element_type: The element type of the list.
+
+            Returns:
+                A new ListType instance.
+            """
+            return cls.of(element_type, False)
+
+        @classmethod
+        def of(cls, element_type: Type, element_nullable: bool) -> "ListType":
+            """
+            Create a new ListType with the given element type and whether the 
element is nullable.
+
+            Args:
+                element_type: The element type of the list.
+                element_nullable: Whether the element of the list is nullable.
+
+            Returns
+                A new ListType instance.
+            """
+            return cls(element_type, element_nullable)
+
+        def element_type(self) -> Type:
+            return self._element_type
+
+        def element_nullable(self) -> bool:
+            return self._element_nullable
+
+        def name(self) -> Name:
+            return Name.LIST
+
+        def simple_string(self) -> str:
+            return (
+                f"list<{self._element_type.simple_string()}>"
+                if self._element_nullable
+                else f"list<{self._element_type.simple_string()}, NOT NULL>"
+            )
+
+        def __eq__(self, other):
+            if not isinstance(other, Types.ListType):
+                return (
+                    self._element_nullable == other.element_nullable()
+                    and self._element_type == other.element_type()
+                )
+            return False
+
+        def __hash__(self):
+            return hash((self._element_type, self._element_nullable))
+
+    class MapType(ComplexType):
+        """The map type in Gravitino. Note, this type is not supported in the 
current version of Gravitino."""
+
+        _key_type: Type
+        _value_type: Type
+        _value_nullable: bool
+
+        def __init__(self, key_type: Type, value_type: Type, value_nullable: 
bool):
+            """
+            Create a new MapType with the given key type, value type and the 
value is nullable.
+
+            Args:
+                key_type: The key type of the map.
+                value_type: The value type of the map.
+                value_nullable: Whether the value of the map is nullable.
+            """
+            self._key_type = key_type
+            self._value_type = value_type
+            self._value_nullable = value_nullable
+
+        @classmethod
+        def value_nullable(cls, key_type: Type, value_type: Type) -> "MapType":
+            """
+            Create a new MapType with the given key type, value type, and the 
value is nullable.
+
+            Args:
+                key_type: The key type of the map.
+                value_type: The value type of the map.
+
+            Returns:
+                A new MapType instance.
+            """
+            return cls.of(key_type, value_type, True)
+
+        @classmethod
+        def value_not_null(cls, key_type: Type, value_type: Type) -> "MapType":
+            """
+            Create a new MapType with the given key type, value type, and the 
value is not nullable.
+
+            Args:
+                key_type: The key type of the map.
+                value_type: The value type of the map.
+
+            Returns:
+                A new MapType instance.
+            """
+            return cls.of(key_type, value_type, False)
+
+        @classmethod
+        def of(
+            cls, key_type: Type, value_type: Type, value_nullable: bool
+        ) -> "MapType":
+            """
+            Create a new MapType with the given key type, value type, and 
whether the value is nullable.
+
+            Args:
+                key_type: The key type of the map.
+                value_type: The value type of the map.
+                value_nullable: Whether the value of the map is nullable.
+
+            Returns:
+                A new MapType instance.
+            """
+            return cls(key_type, value_type, value_nullable)
+
+        def key_type(self) -> Type:
+            return self._key_type
+
+        def value_type(self) -> Type:
+            return self._value_type
+
+        def is_value_nullable(self) -> bool:
+            return self._value_nullable
+
+        def name(self) -> Name:
+            return Name.MAP
+
+        def simple_string(self) -> str:
+            return f"map<{self._key_type.simple_string()}, 
{self._value_type.simple_string()}>"
+
+        def __eq__(self, other):
+            """
+            Compares two MapType objects for equality.
+
+            Args:
+                other The other MapType object to compare with.
+
+            Returns:
+                True if both MapType objects have the same key type, value 
type, and nullability, False otherwise.
+            """
+            if isinstance(other, Types.MapType):
+                return (
+                    self._value_nullable == other._value_nullable
+                    and self._key_type == other._key_type
+                    and self._value_type == other._value_type
+                )
+            return False
+
+        def __hash__(self):
+            return hash((self._key_type, self._value_type, 
self._value_nullable))
+
+    class UnionType(ComplexType):
+        """The union type in Gravitino. Note, this type is not supported in 
the current version of Gravitino."""
+
+        _types: list[Type]
+
+        def __init__(self, types: list[Type]):
+            """
+            Create a new UnionType with the given types.
+
+            Args:
+                types The types of the union.
+            """
+            self._types = types
+
+        @classmethod
+        def of(cls, *types: Type) -> "UnionType":
+            """
+            Create a new UnionType with the given types.
+
+            Args:
+                types: The types of the union.
+
+            Returns:
+                A new UnionType instance.
+            """
+            return Types.UnionType(list(types))
+
+        def types(self) -> list:
+            return self._types
+
+        def name(self) -> Name:
+            return Name.UNION
+
+        def simple_string(self) -> str:
+            return f"union<{', '.join(t.simple_string() for t in 
self._types)}>"
+
+        def __eq__(self, other):
+            """
+            Compares two UnionType objects for equality.
+
+            Args:
+                other The other UnionType object to compare with.
+
+            Returns:
+                True if both UnionType objects have the same types, False 
otherwise.
+            """
+            if not isinstance(other, Types.UnionType):
+                return self._types == other.types()
+            return False
+
+        def __hash__(self):
+            return hash(tuple(self._types))
+
+    class UnparsedType(Type):
+        """Represents a type that is not parsed yet. The parsed type is 
represented by other types of types."""
+
+        _unparsed_type: str
+
+        def __init__(self, unparsed_type: str):
+            """
+            Initializes an unparsed_type instance.
+
+            Args:
+                unparsed_type: The unparsed type as a string.
+            """
+            self._unparsed_type = unparsed_type
+
+        @classmethod
+        def of(cls, unparsed_type: str) -> "UnparsedType":
+            """
+            Creates a new unparsed_type with the given unparsed type.
+
+            Args:
+                unparsed_type The unparsed type.
+
+            Returns
+                A new unparsed_type instance.
+            """
+            return cls(unparsed_type)
+
+        def unparsed_type(self) -> str:
+            return self._unparsed_type
+
+        def name(self) -> Name:
+            return Name.UNPARSED
+
+        def simple_string(self) -> str:
+            return f"unparsed({self._unparsed_type})"
+
+        def __eq__(self, other):
+            """
+            Compares two unparsed_type objects for equality.
+
+            Args:
+                other: The other unparsed_type object to compare with.
+
+            Returns:
+                True if both unparsed_type objects have the same unparsed type 
string, False otherwise.
+            """
+            if not isinstance(other, Types.UnparsedType):
+                return self._unparsed_type == other.unparsed_type()
+            return False
+
+        def __hash__(self):
+            return hash(self._unparsed_type)
+
+        def __str__(self):
+            return self._unparsed_type
+
+    class ExternalType(Type):
+        """Represents a type that is defined in an external catalog."""
+
+        _catalog_string: str
+
+        def __init__(self, catalog_string: str):
+            """
+            Initializes an ExternalType instance.
+
+            Args:
+                catalog_string The string representation of this type in the 
catalog.
+            """
+            self._catalog_string = catalog_string
+
+        @classmethod
+        def of(cls, catalog_string: str) -> "ExternalType":
+            """
+            Creates a new ExternalType with the given catalog string.
+
+            Args:
+                catalog_string The string representation of this type in the 
catalog.
+
+            Returns:
+                A new ExternalType instance.
+            """
+            return cls(catalog_string)
+
+        def catalog_string(self) -> str:
+            return self._catalog_string
+
+        def name(self) -> Name:
+            return Name.EXTERNAL
+
+        def simple_string(self) -> str:
+            return f"external({self._catalog_string})"
+
+        def __eq__(self, other):
+            """
+            Compares two ExternalType objects for equality.
+
+            Args:
+                other: The other ExternalType object to compare with.
+
+            Returns:
+                True if both ExternalType objects have the same catalog 
string, False otherwise.
+            """
+            if not isinstance(other, Types.ExternalType):
+                return False
+            return self._catalog_string == other._catalog_string
+
+        def __hash__(self):
+            return hash(self._catalog_string)
+
+        def __str__(self):
+            return self.simple_string()
+
+    @staticmethod
+    def allow_auto_increment(data_type: Type) -> bool:
+        """
+        Checks if the given data type is allowed to be an auto-increment 
column.
+
+        Args:
+            data_type The data type to check.
+
+        Returns:
+            True if the given data type is allowed to be an auto-increment 
column, False otherwise.
+        """
+        return isinstance(data_type, (Types.IntegerType, Types.LongType))
diff --git a/clients/client-python/tests/unittests/test_gvfs_with_local.py 
b/clients/client-python/tests/unittests/test_gvfs_with_local.py
index b4ce39e57..6e8e20502 100644
--- a/clients/client-python/tests/unittests/test_gvfs_with_local.py
+++ b/clients/client-python/tests/unittests/test_gvfs_with_local.py
@@ -49,6 +49,7 @@ from tests.unittests.auth.mock_base import (
 )
 
 
+# pylint: disable=C0302
 def generate_unique_random_string(length):
     characters = string.ascii_letters + string.digits
     random_string = "".join(random.sample(characters, length))
diff --git a/clients/client-python/tests/unittests/test_types.py 
b/clients/client-python/tests/unittests/test_types.py
new file mode 100644
index 000000000..e241b420a
--- /dev/null
+++ b/clients/client-python/tests/unittests/test_types.py
@@ -0,0 +1,195 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import unittest
+
+from gravitino.api.types import Types, Name
+
+
+class TestTypes(unittest.TestCase):
+
+    def test_null_type(self):
+        instance: Types.NullType = Types.NullType.get()
+        self.assertIsInstance(instance, Types.NullType)
+        self.assertEqual(instance.name(), Name.NULL)
+        self.assertEqual(instance.simple_string(), "null")
+        self.assertIs(instance, Types.NullType.get())  # Singleton check
+
+    def test_boolean_type(self):
+        instance: Types.BooleanType = Types.BooleanType.get()
+        self.assertIsInstance(instance, Types.BooleanType)
+        self.assertEqual(instance.name(), Name.BOOLEAN)
+        self.assertEqual(instance.simple_string(), "boolean")
+        self.assertIs(instance, Types.BooleanType.get())  # Singleton check
+
+    def test_byte_type(self):
+        signed_instance: Types.ByteType = Types.ByteType.get()
+        unsigned_instance = Types.ByteType.unsigned()
+        self.assertIsInstance(signed_instance, Types.ByteType)
+        self.assertEqual(signed_instance.name(), Name.BYTE)
+        self.assertEqual(signed_instance.simple_string(), "byte")
+        self.assertEqual(unsigned_instance.simple_string(), "byte unsigned")
+
+    def test_short_type(self):
+        signed_instance: Types.ShortType = Types.ShortType.get()
+        unsigned_instance = Types.ShortType.unsigned()
+        self.assertIsInstance(signed_instance, Types.ShortType)
+        self.assertEqual(signed_instance.simple_string(), "short")
+        self.assertEqual(unsigned_instance.simple_string(), "short unsigned")
+
+    def test_integer_type(self):
+        signed_instance: Types.IntegerType = Types.IntegerType.get()
+        unsigned_instance = Types.IntegerType.unsigned()
+        self.assertIsInstance(signed_instance, Types.IntegerType)
+        self.assertEqual(signed_instance.simple_string(), "integer")
+        self.assertEqual(unsigned_instance.simple_string(), "integer unsigned")
+
+    def test_long_type(self):
+        signed_instance: Types.LongType = Types.LongType.get()
+        unsigned_instance = Types.LongType.unsigned()
+        self.assertIsInstance(signed_instance, Types.LongType)
+        self.assertEqual(signed_instance.simple_string(), "long")
+        self.assertEqual(unsigned_instance.simple_string(), "long unsigned")
+
+    def test_float_type(self):
+        instance: Types.FloatType = Types.FloatType.get()
+        self.assertEqual(instance.name(), Name.FLOAT)
+        self.assertEqual(instance.simple_string(), "float")
+
+    def test_double_type(self):
+        instance: Types.DoubleType = Types.DoubleType.get()
+        self.assertEqual(instance.name(), Name.DOUBLE)
+        self.assertEqual(instance.simple_string(), "double")
+
+    def test_decimal_type(self):
+        instance: Types.DecimalType = Types.DecimalType.of(10, 2)
+        self.assertEqual(instance.name(), Name.DECIMAL)
+        self.assertEqual(instance.precision(), 10)
+        self.assertEqual(instance.scale(), 2)
+        self.assertEqual(instance.simple_string(), "decimal(10,2)")
+        with self.assertRaises(ValueError):
+            Types.DecimalType.of(39, 2)  # Precision out of range
+        with self.assertRaises(ValueError):
+            Types.DecimalType.of(10, 11)  # Scale out of range
+
+    def test_date_type(self):
+        instance: Types.DateType = Types.DateType.get()
+        self.assertEqual(instance.name(), Name.DATE)
+        self.assertEqual(instance.simple_string(), "date")
+
+    def test_time_type(self):
+        instance: Types.TimeType = Types.TimeType.get()
+        self.assertEqual(instance.name(), Name.TIME)
+        self.assertEqual(instance.simple_string(), "time")
+
+    def test_timestamp_type(self):
+        instance_with_tz = Types.TimestampType.with_time_zone()
+        instance_without_tz = Types.TimestampType.without_time_zone()
+        self.assertTrue(instance_with_tz.has_time_zone())
+        self.assertFalse(instance_without_tz.has_time_zone())
+        self.assertEqual(instance_with_tz.simple_string(), "timestamp_tz")
+        self.assertEqual(instance_without_tz.simple_string(), "timestamp")
+
+    def test_interval_types(self):
+        year_instance: Types.IntervalYearType = Types.IntervalYearType.get()
+        day_instance: Types.IntervalDayType = Types.IntervalDayType.get()
+        self.assertEqual(year_instance.name(), Name.INTERVAL_YEAR)
+        self.assertEqual(day_instance.name(), Name.INTERVAL_DAY)
+        self.assertEqual(year_instance.simple_string(), "interval_year")
+        self.assertEqual(day_instance.simple_string(), "interval_day")
+
+    def test_string_type(self):
+        instance: Types.StringType = Types.StringType.get()
+        self.assertEqual(instance.name(), Name.STRING)
+        self.assertEqual(instance.simple_string(), "string")
+
+    def test_uuid_type(self):
+        instance: Types.UUIDType = Types.UUIDType.get()
+        self.assertEqual(instance.name(), Name.UUID)
+        self.assertEqual(instance.simple_string(), "uuid")
+
+    def test_fixed_type(self):
+        instance: Types.FixedType = Types.FixedType.of(5)
+        self.assertEqual(instance.name(), Name.FIXED)
+        self.assertEqual(instance.length(), 5)
+        self.assertEqual(instance.simple_string(), "fixed(5)")
+
+    def test_varchar_type(self):
+        instance: Types.VarCharType = Types.VarCharType.of(10)
+        self.assertEqual(instance.name(), Name.VARCHAR)
+        self.assertEqual(instance.length(), 10)
+        self.assertEqual(instance.simple_string(), "varchar(10)")
+
+    def test_fixed_char_type(self):
+        instance: Types.FixedCharType = Types.FixedCharType.of(3)
+        self.assertEqual(instance.name(), Name.FIXEDCHAR)
+        self.assertEqual(instance.length(), 3)
+        self.assertEqual(instance.simple_string(), "char(3)")
+
+    def test_binary_type(self):
+        instance: Types.BinaryType = Types.BinaryType.get()
+        self.assertEqual(instance.name(), Name.BINARY)
+        self.assertEqual(instance.simple_string(), "binary")
+
+    def test_struct_type(self):
+        field1: Types.StructType.Field = Types.StructType.Field(
+            "name", Types.StringType.get(), True, "User's name"
+        )
+        field2: Types.StructType.Field = Types.StructType.Field(
+            "age", Types.IntegerType.get(), False, "User's age"
+        )
+        struct: Types.StructType = Types.StructType.of(field1, field2)
+        self.assertEqual(
+            struct.simple_string(),
+            "struct<name: string NULL COMMENT 'User's name', age: integer NOT 
NULL COMMENT 'User's age'>",
+        )
+
+    def test_list_type(self):
+        instance: Types.ListType = Types.ListType.of(Types.StringType.get(), 
True)
+        self.assertEqual(instance.name(), Name.LIST)
+        self.assertTrue(instance.element_nullable())
+        self.assertEqual(instance.simple_string(), "list<string>")
+
+    def test_map_type(self):
+        instance: Types.MapType = Types.MapType.of(
+            Types.StringType.get(), Types.IntegerType.get(), True
+        )
+        self.assertEqual(instance.name(), Name.MAP)
+        self.assertTrue(instance.is_value_nullable())
+        self.assertEqual(instance.simple_string(), "map<string, integer>")
+
+    def test_union_type(self):
+        instance: Types.UnionType = Types.UnionType.of(
+            Types.StringType.get(), Types.IntegerType.get()
+        )
+        self.assertEqual(instance.name(), Name.UNION)
+        self.assertEqual(instance.simple_string(), "union<string, integer>")
+
+    def test_unparsed_type(self):
+        instance: Types.UnparsedType = Types.UnparsedType.of("custom_type")
+        self.assertEqual(instance.name(), Name.UNPARSED)
+        self.assertEqual(instance.simple_string(), "unparsed(custom_type)")
+
+    def test_external_type(self):
+        instance: Types.ExternalType = Types.ExternalType.of("external_type")
+        self.assertEqual(instance.name(), Name.EXTERNAL)
+        self.assertEqual(instance.simple_string(), "external(external_type)")
+
+    def test_auto_increment_check(self):
+        self.assertTrue(Types.allow_auto_increment(Types.IntegerType.get()))
+        self.assertTrue(Types.allow_auto_increment(Types.LongType.get()))
+        self.assertFalse(Types.allow_auto_increment(Types.StringType.get()))
diff --git a/docs/manage-relational-metadata-using-gravitino.md 
b/docs/manage-relational-metadata-using-gravitino.md
index ef35e182e..280793e69 100644
--- a/docs/manage-relational-metadata-using-gravitino.md
+++ b/docs/manage-relational-metadata-using-gravitino.md
@@ -873,7 +873,7 @@ In order to create a table, you need to provide the 
following information:
 
 The following types that Gravitino supports:
 
-| Type                      | Java                                             
                        | JSON                                                  
                                                                               
| Description                                                                   
                                                                                
             |
+| Type                      | Java / Python                                    
                        | JSON                                                  
                                                                               
| Description                                                                   
                                                                                
             |
 
|---------------------------|--------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
 | Boolean                   | `Types.BooleanType.get()`                        
                        | `boolean`                                             
                                                                               
| Boolean type                                                                  
                                                                                
             |
 | Byte                      | `Types.ByteType.get()`                           
                        | `byte`                                                
                                                                               
| Byte type, indicates a numerical value of 1 byte                              
                                                                                
             |

Reply via email to