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)

Reply via email to