This is an automated email from the ASF dual-hosted git repository.
liuxun 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 8924ba5f2f [#5202] feat(client-python): Support Column and its default
value part1 - Column (#6795)
8924ba5f2f is described below
commit 8924ba5f2fe4e3a320edc6db2dd339272fee50d4
Author: George T. C. Lai <[email protected]>
AuthorDate: Thu Apr 3 08:27:25 2025 +0800
[#5202] feat(client-python): Support Column and its default value part1 -
Column (#6795)
### What changes were proposed in this pull request?
This is the first part (totally 4 planned) of the implementation to the
following classes from Java to support Column and its default value,
including:
- Column
### Why are the changes needed?
We need to support Column and its default value in python client.
#5202
### Does this PR introduce _any_ user-facing change?
No
### How was this patch tested?
Unit tests
---------
Signed-off-by: George T. C. Lai <[email protected]>
---
clients/client-python/gravitino/api/column.py | 224 +++++++++++++++++++++
.../client-python/tests/unittests/test_column.py | 122 +++++++++++
2 files changed, 346 insertions(+)
diff --git a/clients/client-python/gravitino/api/column.py
b/clients/client-python/gravitino/api/column.py
new file mode 100644
index 0000000000..e1d782732f
--- /dev/null
+++ b/clients/client-python/gravitino/api/column.py
@@ -0,0 +1,224 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+
+from __future__ import annotations
+
+from abc import ABC, abstractmethod
+from typing import Optional
+
+from gravitino.api.expressions.expression import Expression
+from gravitino.api.expressions.function_expression import FunctionExpression
+from gravitino.api.tag.supports_tags import SupportsTags
+from gravitino.api.types.type import Type
+from gravitino.exceptions.base import UnsupportedOperationException
+from gravitino.utils.precondition import Precondition
+
+
+class Column(ABC):
+ """An interface representing a column of a `Table`.
+
+ It defines basic properties of a column, such as name and data type.
+
+ Catalog implementation needs to implement it. They should consume it in
APIs like
+ `TableCatalog.createTable(NameIdentifier, List[Column], str, Dict)`, and
report it in
+ `Table.columns()` a default value and a generation expression.
+ """
+
+ DEFAULT_VALUE_NOT_SET = Expression.EMPTY_EXPRESSION
+ """A default value that indicates the default value is not set. This is
used in `default_value()`"""
+
+ DEFAULT_VALUE_OF_CURRENT_TIMESTAMP: Expression = FunctionExpression.of(
+ "current_timestamp"
+ )
+ """
+ A default value that indicates the default value will be set to the
current timestamp.
+ This is used in `default_value()`
+ """
+
+ @abstractmethod
+ def name(self) -> str:
+ """Get the name of this column.
+
+ Returns:
+ str: The name of this column.
+ """
+ pass
+
+ @abstractmethod
+ def data_type(self) -> Type:
+ """Get the name of this column.
+
+ Returns:
+ Type: The data type of this column.
+ """
+ pass
+
+ @abstractmethod
+ def comment(self) -> Optional[str]:
+ """Get the comment of this column.
+
+ Returns:
+ Optional[str]: The comment of this column, `None` if not specified.
+ """
+ pass
+
+ @abstractmethod
+ def nullable(self) -> bool:
+ """Indicate if this column may produce null values.
+
+ Returns:
+ bool: `True` if this column may produce null values. Default is
`True`.
+ """
+ return True
+
+ @abstractmethod
+ def auto_increment(self) -> bool:
+ """Indicate if this column is an auto-increment column.
+
+ Returns:
+ bool: `True` if this column is an auto-increment column. Default
is `False`.
+ """
+ return False
+
+ @abstractmethod
+ def default_value(self) -> Expression:
+ """Get the default value of this column
+
+ Returns:
+ Expression:
+ The default value of this column,
`Column.DEFAULT_VALUE_NOT_SET` if not specified.
+ """
+ pass
+
+ def supports_tags(self) -> SupportsTags:
+ """Return the `SupportsTags` if the column supports tag operations.
+
+ Raises:
+ UnsupportedOperationException: if the column does not support tag
operations.
+
+ Returns:
+ SupportsTags: the `SupportsTags` if the column supports tag
operations.
+ """
+ raise UnsupportedOperationException("Column does not support tag
operations.")
+
+ @staticmethod
+ def of(
+ name: str,
+ data_type: Type,
+ comment: Optional[str] = None,
+ nullable: bool = True,
+ auto_increment: bool = False,
+ default_value: Optional[Expression] = None,
+ ) -> ColumnImpl:
+ """Create a `Column` instance.
+
+ Args:
+ name (str):
+ The name of the column.
+ data_type (Type):
+ The data type of the column.
+ comment (Optional[str], optional):
+ The comment of the column. Defaults to `None`.
+ nullable (bool, optional):
+ `True` if the column may produce null values. Defaults to
`True`.
+ auto_increment (bool, optional):
+ `True` if the column is an auto-increment column. Defaults to
`False`.
+ default_value (Optional[Expression], optional):
+ The default value of this column,
`Column.DEFAULT_VALUE_NOT_SET` if `None`. Defaults to `None`.
+
+ Returns:
+ ColumnImpl: A `Column` instance.
+ """
+ return ColumnImpl(
+ name=name,
+ data_type=data_type,
+ comment=comment,
+ nullable=nullable,
+ auto_increment=auto_increment,
+ default_value=(
+ Column.DEFAULT_VALUE_NOT_SET if default_value is None else
default_value
+ ),
+ )
+
+
+class ColumnImpl(Column):
+ """The implementation of `Column` for users to use API."""
+
+ def __init__(
+ self,
+ name: str,
+ data_type: Type,
+ comment: Optional[str],
+ nullable: bool,
+ auto_increment: bool,
+ default_value: Optional[Expression],
+ ):
+ Precondition.check_string_not_empty(name, "Column name cannot be null")
+ Precondition.check_argument(
+ data_type is not None, "Column data type cannot be null"
+ )
+ self._name = name
+ self._data_type = data_type
+ self._comment = comment
+ self._nullable = nullable
+ self._auto_increment = auto_increment
+ self._default_value = default_value
+
+ def name(self) -> str:
+ return self._name
+
+ def data_type(self) -> Type:
+ return self._data_type
+
+ def comment(self) -> Optional[str]:
+ return self._comment
+
+ def nullable(self) -> bool:
+ return self._nullable
+
+ def auto_increment(self) -> bool:
+ return self._auto_increment
+
+ def default_value(self) -> Expression:
+ return self._default_value
+
+ def __eq__(self, other: ColumnImpl) -> bool:
+ if not isinstance(other, ColumnImpl):
+ return False
+ return all(
+ [
+ self._name == other.name(),
+ self._data_type == other.data_type(),
+ self._comment == other.comment(),
+ self._nullable == other.nullable(),
+ self._auto_increment == other.auto_increment(),
+ self._default_value == other.default_value(),
+ ]
+ )
+
+ def __hash__(self):
+ return hash(
+ (
+ self._name,
+ self._data_type,
+ self._comment,
+ self._nullable,
+ self._auto_increment,
+ tuple(self._default_value),
+ )
+ )
diff --git a/clients/client-python/tests/unittests/test_column.py
b/clients/client-python/tests/unittests/test_column.py
new file mode 100644
index 0000000000..672ce5d82d
--- /dev/null
+++ b/clients/client-python/tests/unittests/test_column.py
@@ -0,0 +1,122 @@
+# 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 unittest.mock import Mock
+
+from gravitino.api.column import Column, ColumnImpl
+from gravitino.api.expressions.expression import Expression
+from gravitino.api.expressions.function_expression import FunctionExpression
+from gravitino.api.types.type import Type
+from gravitino.exceptions.base import (
+ IllegalArgumentException,
+ UnsupportedOperationException,
+)
+
+
+class TestColumn(unittest.TestCase):
+ def setUp(self):
+ # Create mock Type for testing
+ self.mock_type = Mock(spec=Type)
+
+ def test_column_factory_method(self):
+ """Test the Column.of() factory method."""
+
+ column = Column.of("test_column", self.mock_type)
+
+ self.assertIsInstance(column, ColumnImpl)
+ self.assertEqual("test_column", column.name())
+ self.assertEqual(self.mock_type, column.data_type())
+ self.assertIsNone(column.comment())
+ self.assertTrue(column.nullable())
+ self.assertFalse(column.auto_increment())
+ self.assertEqual(Column.DEFAULT_VALUE_NOT_SET, column.default_value())
+
+ def test_column_factory_with_all_params(self):
+ """Test the Column.of() factory method with all parameters."""
+
+ default_value = Mock(spec=Expression)
+ column = Column.of(
+ name="test_column",
+ data_type=self.mock_type,
+ comment="Test comment",
+ nullable=False,
+ auto_increment=True,
+ default_value=default_value,
+ )
+
+ self.assertEqual("test_column", column.name())
+ self.assertEqual(self.mock_type, column.data_type())
+ self.assertEqual("Test comment", column.comment())
+ self.assertFalse(column.nullable())
+ self.assertTrue(column.auto_increment())
+ self.assertEqual(default_value, column.default_value())
+
+ def test_column_equality(self):
+ """Test equality comparison."""
+ default_value = Mock(spec=Expression)
+
+ col1 = Column.of("test", self.mock_type, "comment", False, True,
default_value)
+ col2 = Column.of("test", self.mock_type, "comment", False, True,
default_value)
+ col3 = Column.of("different", self.mock_type)
+
+ self.assertEqual(col1, col2)
+ self.assertNotEqual(col1, col3)
+ self.assertNotEqual(col1, "not_a_column")
+
+ def test_column_hash(self):
+ """Test hash implementation.
+
+ Same columns should have same hash.
+ """
+ col1 = Column.of("test", self.mock_type, "comment", False, True)
+ col2 = Column.of("test", self.mock_type, "comment", False, True)
+ col3 = Column.of("different", self.mock_type)
+
+ self.assertEqual(hash(col1), hash(col2))
+ self.assertNotEqual(hash(col1), hash(col3))
+
+ def test_supports_tags_raises_exception(self):
+ """Test that supports_tags raises `UnsupportedOperationException`."""
+
+ column = Column.of("test", self.mock_type)
+
+ with self.assertRaises(UnsupportedOperationException):
+ column.supports_tags()
+
+ def test_default_value_constants(self):
+ """Test default value constants."""
+
+ self.assertEqual(Expression.EMPTY_EXPRESSION,
Column.DEFAULT_VALUE_NOT_SET)
+ self.assertIsInstance(
+ Column.DEFAULT_VALUE_OF_CURRENT_TIMESTAMP, FunctionExpression
+ )
+
+ def test_empty_name_validation(self):
+ """Test validation for empty name to raise
`IllegalArgumentException`."""
+
+ with self.assertRaises(IllegalArgumentException):
+ Column.of("", self.mock_type)
+
+ with self.assertRaises(IllegalArgumentException):
+ Column.of(" ", self.mock_type)
+
+ def test_none_data_type_validation(self):
+ """Test validation for None data type to raise
`IllegalArgumentException`."""
+
+ with self.assertRaises(IllegalArgumentException):
+ Column.of("test", None)