This is an automated email from the ASF dual-hosted git repository.
jshao pushed a commit to branch branch-1.0
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/branch-1.0 by this push:
new 31ff8be118 [#8274] Update Python Client to support time/timestamp
precision (#8462)
31ff8be118 is described below
commit 31ff8be118b42017f48e0b02e0d03a781f022b7d
Author: github-actions[bot]
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Mon Sep 8 13:04:46 2025 +0800
[#8274] Update Python Client to support time/timestamp precision (#8462)
### Why are the changes needed?
Fix: #8274
### Does this PR introduce _any_ user-facing change?
No
### How was this patch tested?
UT already added.
Signed-off-by: zacsun <[email protected]>
Co-authored-by: predator4ann <[email protected]>
---
.../api/types/json_serdes/_helper/serdes_utils.py | 16 +++
clients/client-python/gravitino/api/types/type.py | 12 +-
clients/client-python/gravitino/api/types/types.py | 151 ++++++++++++++++++---
clients/client-python/gravitino/utils/serdes.py | 5 +
.../unittests/json_serdes/test_type_serdes.py | 71 ++++++++++
.../tests/unittests/rel/test_types.py | 61 +++++++++
6 files changed, 293 insertions(+), 23 deletions(-)
diff --git
a/clients/client-python/gravitino/api/types/json_serdes/_helper/serdes_utils.py
b/clients/client-python/gravitino/api/types/json_serdes/_helper/serdes_utils.py
index 6aaa12a45e..cab0ebbacc 100644
---
a/clients/client-python/gravitino/api/types/json_serdes/_helper/serdes_utils.py
+++
b/clients/client-python/gravitino/api/types/json_serdes/_helper/serdes_utils.py
@@ -201,6 +201,22 @@ class SerdesUtils(SerdesUtilsBase):
if varchar_matched:
return Types.VarCharType.of(length=int(varchar_matched.group(1)))
+ time_matched = cls.TIME_PATTERN.match(type_string)
+ if time_matched:
+ return Types.TimeType.of(precision=int(time_matched.group(1)))
+
+ timestamp_matched = cls.TIMESTAMP_PATTERN.match(type_string)
+ if timestamp_matched:
+ return Types.TimestampType.without_time_zone(
+ precision=int(timestamp_matched.group(1))
+ )
+
+ timestamp_tz_matched = cls.TIMESTAMP_TZ_PATTERN.match(type_string)
+ if timestamp_tz_matched:
+ return Types.TimestampType.with_time_zone(
+ precision=int(timestamp_tz_matched.group(1))
+ )
+
return Types.UnparsedType.of(type_string)
@classmethod
diff --git a/clients/client-python/gravitino/api/types/type.py
b/clients/client-python/gravitino/api/types/type.py
index 9b089ea187..bbc467a05e 100644
--- a/clients/client-python/gravitino/api/types/type.py
+++ b/clients/client-python/gravitino/api/types/type.py
@@ -146,7 +146,17 @@ class NumericType(PrimitiveType, ABC):
class DateTimeType(PrimitiveType, ABC):
"""Base class for all date/time types."""
- pass
+ # Indicates that precision for the date/time type was not explicitly set
by the user.
+ # The value should be converted to the catalog's default precision.
+ DATE_TIME_PRECISION_NOT_SET = -1
+
+ # Represents the minimum precision range for timestamp, time and other
date/time types.
+ # The minimum precision is 0, which means second-level precision.
+ MIN_ALLOWED_PRECISION = 0
+
+ # Represents the maximum precision allowed for timestamp, time and other
date/time types.
+ # The maximum precision is 12, which means picosecond-level precision.
+ MAX_ALLOWED_PRECISION = 12
class IntervalType(PrimitiveType, ABC):
diff --git a/clients/client-python/gravitino/api/types/types.py
b/clients/client-python/gravitino/api/types/types.py
index 6e82725713..9549c1ec5e 100644
--- a/clients/client-python/gravitino/api/types/types.py
+++ b/clients/client-python/gravitino/api/types/types.py
@@ -322,60 +322,167 @@ class Types:
class TimeType(DateTimeType):
_instance: Types.TimeType = None
+ _precision: int
- def __new__(cls):
- if cls._instance is None:
- cls._instance = super(Types.TimeType, cls).__new__(cls)
- cls._instance.__init__()
- return cls._instance
+ def __new__(cls, precision: int = None):
+ if precision is None:
+ # Forward compatibility: Use singleton when there is no
precision parameter
+ if cls._instance is None:
+ cls._instance = super(Types.TimeType, cls).__new__(cls)
+ cls._instance.__init__()
+ return cls._instance
+ # Create a new instance when the precision parameter is present
+ return super(Types.TimeType, cls).__new__(cls)
+
+ def __init__(self, precision: int = None):
+ if hasattr(self, "_initialized"):
+ return
+ super().__init__()
+ self._precision = (
+ precision if precision is not None else
self.DATE_TIME_PRECISION_NOT_SET
+ )
+ self._initialized = True
@classmethod
def get(cls) -> Types.TimeType:
+ """Returns the default TimeType instance (without precision)"""
return cls()
+ @classmethod
+ def of(cls, precision: int) -> Types.TimeType:
+ """Create a TimeType instance with the specified precision"""
+ if not (
+ cls.MIN_ALLOWED_PRECISION <= precision <=
cls.MAX_ALLOWED_PRECISION
+ ):
+ raise ValueError(
+ f"precision must be in range "
+ f"[{cls.MIN_ALLOWED_PRECISION},
{cls.MAX_ALLOWED_PRECISION}]: "
+ f"precision: {precision}"
+ )
+ return cls(precision)
+
+ def precision(self) -> int:
+ """Returns the precision of the time type"""
+ return self._precision
+
+ def has_precision_set(self) -> bool:
+ """Returns whether the time type has precision set"""
+ return self._precision != self.DATE_TIME_PRECISION_NOT_SET
+
def name(self) -> Name:
return Name.TIME
def simple_string(self) -> str:
- return "time"
+ return f"time({self._precision})" if self.has_precision_set() else
"time"
+
+ def __eq__(self, other):
+ if not isinstance(other, Types.TimeType):
+ return False
+ return self._precision == other._precision
+
+ def __hash__(self):
+ return hash(self._precision)
class TimestampType(DateTimeType):
_instance_with_tz: Types.TimestampType = None
_instance_without_tz: Types.TimestampType = None
_with_time_zone: bool
+ _precision: int
- 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
+ def __new__(cls, with_time_zone: bool, precision: int = None):
+ if precision is None:
+ # Use singleton when there is no precision parameter
+ 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
+ # Create a new instance when the precision parameter is present
+ return super(Types.TimestampType, cls).__new__(cls)
+
+ def __init__(self, with_time_zone: bool, precision: int = None):
+ if hasattr(self, "_initialized"):
+ return
+ super().__init__()
+ self._with_time_zone = with_time_zone
+ self._precision = (
+ precision if precision is not None else
self.DATE_TIME_PRECISION_NOT_SET
+ )
+ self._initialized = True
@classmethod
- def with_time_zone(cls) -> Types.TimestampType:
+ def with_time_zone(cls, precision: int = None) -> Types.TimestampType:
+ """Create TimestampType with Timezone"""
+ if precision is not None:
+ if not (
+ cls.MIN_ALLOWED_PRECISION <= precision <=
cls.MAX_ALLOWED_PRECISION
+ ):
+ raise ValueError(
+ f"precision must be in range "
+ f"[{cls.MIN_ALLOWED_PRECISION},
{cls.MAX_ALLOWED_PRECISION}]: "
+ f"precision: {precision}"
+ )
+ return cls(True, precision)
return cls(True)
@classmethod
- def without_time_zone(cls) -> Types.TimestampType:
+ def without_time_zone(cls, precision: int = None) ->
Types.TimestampType:
+ """Create TimestampType without Timezone"""
+ if precision is not None:
+ if not (
+ cls.MIN_ALLOWED_PRECISION <= precision <=
cls.MAX_ALLOWED_PRECISION
+ ):
+ raise ValueError(
+ f"precision must be in range "
+ f"[{cls.MIN_ALLOWED_PRECISION},
{cls.MAX_ALLOWED_PRECISION}]: "
+ f"precision: {precision}"
+ )
+ return cls(False, precision)
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:
+ """Returns whether the timestamp type has a timezone"""
return self._with_time_zone
+ def precision(self) -> int:
+ """Returns the precision of the timestamp type"""
+ return self._precision
+
+ def has_precision_set(self) -> bool:
+ """Returns whether the precision is set for the timestamp type"""
+ return self._precision != self.DATE_TIME_PRECISION_NOT_SET
+
def name(self) -> Name:
return Name.TIMESTAMP
def simple_string(self) -> str:
+ if self.has_precision_set():
+ return (
+ f"timestamp_tz({self._precision})"
+ if self._with_time_zone
+ else f"timestamp({self._precision})"
+ )
return "timestamp_tz" if self._with_time_zone else "timestamp"
+ def __eq__(self, other):
+ if not isinstance(other, Types.TimestampType):
+ return False
+ return (
+ self._with_time_zone == other._with_time_zone
+ and self._precision == other._precision
+ )
+
+ def __hash__(self):
+ return hash((self._with_time_zone, self._precision))
+
class IntervalYearType(IntervalType):
"""The interval year type in Gravitino."""
diff --git a/clients/client-python/gravitino/utils/serdes.py
b/clients/client-python/gravitino/utils/serdes.py
index 8554e65cc4..b862ba9259 100644
--- a/clients/client-python/gravitino/utils/serdes.py
+++ b/clients/client-python/gravitino/utils/serdes.py
@@ -87,6 +87,11 @@ class SerdesUtilsBase:
FIXED_PATTERN: Final[Pattern[str]] = re.compile(r"fixed\(\s*(\d+)\s*\)")
FIXEDCHAR_PATTERN: Final[Pattern[str]] = re.compile(r"char\(\s*(\d+)\s*\)")
VARCHAR_PATTERN: Final[Pattern[str]] =
re.compile(r"varchar\(\s*(\d+)\s*\)")
+ TIME_PATTERN: Final[Pattern[str]] = re.compile(r"time\(\s*(\d+)\s*\)")
+ TIMESTAMP_PATTERN: Final[Pattern[str]] =
re.compile(r"timestamp\(\s*(\d+)\s*\)")
+ TIMESTAMP_TZ_PATTERN: Final[Pattern[str]] = re.compile(
+ r"timestamp_tz\(\s*(\d+)\s*\)"
+ )
TYPES: Final[Mapping] = MappingProxyType(
{
type_instance.simple_string(): type_instance
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 8d71ab9d45..7ef195b649 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
@@ -46,6 +46,12 @@ class TestTypeSerdes(unittest.TestCase):
"fixed(10)": Types.FixedType.of(10),
"char(10)": Types.FixedCharType.of(10),
"varchar(10)": Types.VarCharType.of(10),
+ "time(6)": Types.TimeType.of(6),
+ "time(3)": Types.TimeType.of(3),
+ "timestamp(6)": Types.TimestampType.without_time_zone(6),
+ "timestamp(0)": Types.TimestampType.without_time_zone(0),
+ "timestamp_tz(6)": Types.TimestampType.with_time_zone(6),
+ "timestamp_tz(3)": Types.TimestampType.with_time_zone(3),
},
}
@@ -323,3 +329,68 @@ class TestTypeSerdes(unittest.TestCase):
TypeSerdes.deserialize,
data=invalid_data,
)
+
+ def test_time_type_precision_serialization(self):
+ """Test the serialization and deserialization of time type precision"""
+ # Test TimeType with precision
+ time_with_precision = Types.TimeType.of(6)
+ serialized = TypeSerdes.serialize(time_with_precision)
+ self.assertEqual(serialized, "time(6)")
+
+ deserialized = TypeSerdes.deserialize(serialized)
+ self.assertEqual(deserialized, time_with_precision)
+ self.assertTrue(deserialized.has_precision_set())
+ self.assertEqual(deserialized.precision(), 6)
+
+ # Test TimestampType with precision (without timezone)
+ timestamp_with_precision = Types.TimestampType.without_time_zone(3)
+ serialized = TypeSerdes.serialize(timestamp_with_precision)
+ self.assertEqual(serialized, "timestamp(3)")
+
+ deserialized = TypeSerdes.deserialize(serialized)
+ self.assertEqual(deserialized, timestamp_with_precision)
+ self.assertTrue(deserialized.has_precision_set())
+ self.assertEqual(deserialized.precision(), 3)
+ self.assertFalse(deserialized.has_time_zone())
+
+ # Test TimestampType with precision (with timezone)
+ timestamp_tz_with_precision = Types.TimestampType.with_time_zone(9)
+ serialized = TypeSerdes.serialize(timestamp_tz_with_precision)
+ self.assertEqual(serialized, "timestamp_tz(9)")
+
+ deserialized = TypeSerdes.deserialize(serialized)
+ self.assertEqual(deserialized, timestamp_tz_with_precision)
+ self.assertTrue(deserialized.has_precision_set())
+ self.assertEqual(deserialized.precision(), 9)
+ self.assertTrue(deserialized.has_time_zone())
+
+ def test_backward_compatibility(self):
+ """Test forward compatibility - Time types without precision should
work properly"""
+ # Test TimeType without precision
+ time_without_precision = Types.TimeType.get()
+ serialized = TypeSerdes.serialize(time_without_precision)
+ self.assertEqual(serialized, "time")
+
+ deserialized = TypeSerdes.deserialize(serialized)
+ self.assertEqual(deserialized, time_without_precision)
+ self.assertFalse(deserialized.has_precision_set())
+
+ # Test TimestampType without precision (without timezone)
+ timestamp_without_precision = Types.TimestampType.without_time_zone()
+ serialized = TypeSerdes.serialize(timestamp_without_precision)
+ self.assertEqual(serialized, "timestamp")
+
+ deserialized = TypeSerdes.deserialize(serialized)
+ self.assertEqual(deserialized, timestamp_without_precision)
+ self.assertFalse(deserialized.has_precision_set())
+ self.assertFalse(deserialized.has_time_zone())
+
+ # Test TimestampType without precision (with timezone)
+ timestamp_tz_without_precision = Types.TimestampType.with_time_zone()
+ serialized = TypeSerdes.serialize(timestamp_tz_without_precision)
+ self.assertEqual(serialized, "timestamp_tz")
+
+ deserialized = TypeSerdes.deserialize(serialized)
+ self.assertEqual(deserialized, timestamp_tz_without_precision)
+ self.assertFalse(deserialized.has_precision_set())
+ self.assertTrue(deserialized.has_time_zone())
diff --git a/clients/client-python/tests/unittests/rel/test_types.py
b/clients/client-python/tests/unittests/rel/test_types.py
index 2acdbcf4ea..c742281e7e 100644
--- a/clients/client-python/tests/unittests/rel/test_types.py
+++ b/clients/client-python/tests/unittests/rel/test_types.py
@@ -91,17 +91,78 @@ class TestTypes(unittest.TestCase):
self.assertEqual(instance.simple_string(), "date")
def test_time_type(self):
+ # Test TimeType without precision
instance: Types.TimeType = Types.TimeType.get()
self.assertEqual(instance.name(), Name.TIME)
self.assertEqual(instance.simple_string(), "time")
+ self.assertFalse(instance.has_precision_set())
+ self.assertEqual(
+ instance.precision(), Types.TimeType.DATE_TIME_PRECISION_NOT_SET
+ )
+
+ # Test TimeType with precision
+ time_with_precision = Types.TimeType.of(6)
+ self.assertEqual(time_with_precision.name(), Name.TIME)
+ self.assertEqual(time_with_precision.simple_string(), "time(6)")
+ self.assertTrue(time_with_precision.has_precision_set())
+ self.assertEqual(time_with_precision.precision(), 6)
+
+ # Test Precision Range Verification
+ with self.assertRaises(ValueError):
+ Types.TimeType.of(-1)
+ with self.assertRaises(ValueError):
+ Types.TimeType.of(13)
+
+ time_with_precision_2 = Types.TimeType.of(6)
+ self.assertEqual(time_with_precision, time_with_precision_2)
+ self.assertEqual(hash(time_with_precision),
hash(time_with_precision_2))
+
+ time_different_precision = Types.TimeType.of(3)
+ self.assertNotEqual(time_with_precision, time_different_precision)
def test_timestamp_type(self):
+ # Test TimestampType without precision
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")
+ self.assertFalse(instance_with_tz.has_precision_set())
+ self.assertFalse(instance_without_tz.has_precision_set())
+
+ # Test TimestampType with precision (with timezone)
+ timestamp_tz_with_precision = Types.TimestampType.with_time_zone(6)
+ self.assertTrue(timestamp_tz_with_precision.has_time_zone())
+ self.assertTrue(timestamp_tz_with_precision.has_precision_set())
+ self.assertEqual(timestamp_tz_with_precision.precision(), 6)
+ self.assertEqual(timestamp_tz_with_precision.simple_string(),
"timestamp_tz(6)")
+
+ # Test TimestampType with precision (without timezone)
+ timestamp_with_precision = Types.TimestampType.without_time_zone(3)
+ self.assertFalse(timestamp_with_precision.has_time_zone())
+ self.assertTrue(timestamp_with_precision.has_precision_set())
+ self.assertEqual(timestamp_with_precision.precision(), 3)
+ self.assertEqual(timestamp_with_precision.simple_string(),
"timestamp(3)")
+
+ # Test Precision Range Verification
+ with self.assertRaises(ValueError):
+ Types.TimestampType.with_time_zone(-1)
+ with self.assertRaises(ValueError):
+ Types.TimestampType.without_time_zone(13)
+
+ timestamp_tz_with_precision_2 = Types.TimestampType.with_time_zone(6)
+ self.assertEqual(timestamp_tz_with_precision,
timestamp_tz_with_precision_2)
+ self.assertEqual(
+ hash(timestamp_tz_with_precision),
hash(timestamp_tz_with_precision_2)
+ )
+
+ # Different precision or timezone should not be equal
+ timestamp_different_precision = Types.TimestampType.with_time_zone(3)
+ self.assertNotEqual(timestamp_tz_with_precision,
timestamp_different_precision)
+
+ timestamp_different_tz = Types.TimestampType.without_time_zone(6)
+ self.assertNotEqual(timestamp_tz_with_precision,
timestamp_different_tz)
def test_interval_types(self):
year_instance: Types.IntervalYearType = Types.IntervalYearType.get()