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

jshao pushed a commit to branch branch-gvfs-fuse-dev
in repository https://gitbox.apache.org/repos/asf/gravitino.git

commit a606c1b3a4953117ddbc767f493e43caa8ccff1c
Author: SophieTech88 <[email protected]>
AuthorDate: Wed Dec 4 18:14:47 2024 -0800

    [#5201] feat(client-python): Implement expressions in python client (#5646)
    
    ### What changes were proposed in this pull request?
    Implement expression from java, including:
    - Expression.java
    - FunctionExpression.java
    - NamedReference.java
    - UnparsedExpression.java
    - literals/
    
    convert to python client, and add unit test for each class.
    
    ### Why are the changes needed?
    
    We need to support the expressions in python client
    
    Fix: #5201
    
    ### Does this PR introduce _any_ user-facing change?
    No
    
    ### How was this patch tested?
    Need to pass all unit tests.
    
    ---------
    
    Co-authored-by: Xun <[email protected]>
---
 .../gravitino/api/expressions/__init__.py          |  16 +++
 .../gravitino/api/expressions/expression.py        |  51 ++++++++
 .../api/expressions/function_expression.py         |  92 ++++++++++++++
 .../gravitino/api/expressions/literals/__init__.py |  16 +++
 .../gravitino/api/expressions/literals/literal.py  |  43 +++++++
 .../gravitino/api/expressions/literals/literals.py | 137 +++++++++++++++++++++
 .../gravitino/api/expressions/named_reference.py   |  86 +++++++++++++
 .../api/expressions/unparsed_expression.py         |  77 ++++++++++++
 .../client-python/gravitino/api/types/__init__.py  |  16 +++
 .../gravitino/api/{ => types}/type.py              |   0
 .../gravitino/api/{ => types}/types.py             |   2 +-
 .../tests/unittests/test_expressions.py            |  61 +++++++++
 .../tests/unittests/test_function_expression.py    |  62 ++++++++++
 .../client-python/tests/unittests/test_literals.py |  95 ++++++++++++++
 .../tests/unittests/test_named_reference.py        |  39 ++++++
 .../client-python/tests/unittests/test_types.py    |   2 +-
 .../tests/unittests/test_unparsed_expression.py    |  34 +++++
 17 files changed, 827 insertions(+), 2 deletions(-)

diff --git a/clients/client-python/gravitino/api/expressions/__init__.py 
b/clients/client-python/gravitino/api/expressions/__init__.py
new file mode 100644
index 000000000..13a83393a
--- /dev/null
+++ b/clients/client-python/gravitino/api/expressions/__init__.py
@@ -0,0 +1,16 @@
+# 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.
diff --git a/clients/client-python/gravitino/api/expressions/expression.py 
b/clients/client-python/gravitino/api/expressions/expression.py
new file mode 100644
index 000000000..41669042c
--- /dev/null
+++ b/clients/client-python/gravitino/api/expressions/expression.py
@@ -0,0 +1,51 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from __future__ import annotations
+from abc import ABC, abstractmethod
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from gravitino.api.expressions.named_reference import NamedReference
+
+
+class Expression(ABC):
+    """Base class of the public logical expression API."""
+
+    EMPTY_EXPRESSION: list[Expression] = []
+    """
+    `EMPTY_EXPRESSION` is only used as an input when the default `children` 
method builds the result.
+    """
+
+    EMPTY_NAMED_REFERENCE: list[NamedReference] = []
+    """
+    `EMPTY_NAMED_REFERENCE` is only used as an input when the default 
`references` method builds
+    the result array to avoid repeatedly allocating an empty array.
+    """
+
+    @abstractmethod
+    def children(self) -> list[Expression]:
+        """Returns a list of the children of this node. Children should not 
change."""
+        pass
+
+    def references(self) -> list[NamedReference]:
+        """Returns a list of fields or columns that are referenced by this 
expression."""
+
+        ref_set: set[NamedReference] = set()
+        for child in self.children():
+            ref_set.update(child.references())
+        return list(ref_set)
diff --git 
a/clients/client-python/gravitino/api/expressions/function_expression.py 
b/clients/client-python/gravitino/api/expressions/function_expression.py
new file mode 100644
index 000000000..7664cf9bf
--- /dev/null
+++ b/clients/client-python/gravitino/api/expressions/function_expression.py
@@ -0,0 +1,92 @@
+# 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 abstractmethod
+
+from gravitino.api.expressions.expression import Expression
+
+
+class FunctionExpression(Expression):
+    """
+    The interface of a function expression. A function expression is an 
expression that takes a
+    function name and a list of arguments.
+    """
+
+    @staticmethod
+    def of(function_name: str, *arguments: Expression) -> FuncExpressionImpl:
+        """
+        Creates a new FunctionExpression with the given function name.
+        If no arguments are provided, it uses an empty expression.
+
+        :param function_name: The name of the function.
+        :param arguments: The arguments to the function (optional).
+        :return: The created FunctionExpression.
+        """
+        arguments = list(arguments) if arguments else 
Expression.EMPTY_EXPRESSION
+        return FuncExpressionImpl(function_name, arguments)
+
+    @abstractmethod
+    def function_name(self) -> str:
+        """Returns the function name."""
+
+    @abstractmethod
+    def arguments(self) -> list[Expression]:
+        """Returns the arguments passed to the function."""
+
+    def children(self) -> list[Expression]:
+        """Returns the arguments as children."""
+        return self.arguments()
+
+
+class FuncExpressionImpl(FunctionExpression):
+    """
+    A concrete implementation of the FunctionExpression interface.
+    """
+
+    _function_name: str
+    _arguments: list[Expression]
+
+    def __init__(self, function_name: str, arguments: list[Expression]):
+        super().__init__()
+        self._function_name = function_name
+        self._arguments = arguments
+
+    def function_name(self) -> str:
+        return self._function_name
+
+    def arguments(self) -> list[Expression]:
+        return self._arguments
+
+    def __str__(self) -> str:
+        if not self._arguments:
+            return f"{self._function_name}()"
+        arguments_str = ", ".join(map(str, self._arguments))
+        return f"{self._function_name}({arguments_str})"
+
+    def __eq__(self, other: FuncExpressionImpl) -> bool:
+        if self is other:
+            return True
+        if other is None or self.__class__ is not other.__class__:
+            return False
+        return (
+            self._function_name == other.function_name()
+            and self._arguments == other.arguments()
+        )
+
+    def __hash__(self) -> int:
+        return hash((self._function_name, tuple(self._arguments)))
diff --git 
a/clients/client-python/gravitino/api/expressions/literals/__init__.py 
b/clients/client-python/gravitino/api/expressions/literals/__init__.py
new file mode 100644
index 000000000..13a83393a
--- /dev/null
+++ b/clients/client-python/gravitino/api/expressions/literals/__init__.py
@@ -0,0 +1,16 @@
+# 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.
diff --git 
a/clients/client-python/gravitino/api/expressions/literals/literal.py 
b/clients/client-python/gravitino/api/expressions/literals/literal.py
new file mode 100644
index 000000000..676b9ef4c
--- /dev/null
+++ b/clients/client-python/gravitino/api/expressions/literals/literal.py
@@ -0,0 +1,43 @@
+# 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 abstractmethod
+from typing import List, TypeVar, Generic
+
+from gravitino.api.expressions.expression import Expression
+from gravitino.api.types.type import Type
+
+T = TypeVar("T")
+
+
+class Literal(Generic[T], Expression):
+    """
+    Represents a constant literal value in the public expression API.
+    """
+
+    @abstractmethod
+    def value(self) -> T:
+        """The literal value."""
+        raise NotImplementedError("Subclasses must implement the `value` 
method.")
+
+    @abstractmethod
+    def data_type(self) -> Type:
+        """The data type of the literal."""
+        raise NotImplementedError("Subclasses must implement the `data_type` 
method.")
+
+    def children(self) -> List[Expression]:
+        return Expression.EMPTY_EXPRESSION
diff --git 
a/clients/client-python/gravitino/api/expressions/literals/literals.py 
b/clients/client-python/gravitino/api/expressions/literals/literals.py
new file mode 100644
index 000000000..c4d07338b
--- /dev/null
+++ b/clients/client-python/gravitino/api/expressions/literals/literals.py
@@ -0,0 +1,137 @@
+# 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 decimal
+from typing import TypeVar
+from datetime import date, time, datetime
+
+from gravitino.api.expressions.literals.literal import Literal
+from gravitino.api.types.type import Type
+from gravitino.api.types.types import Types
+
+T = TypeVar("T")
+
+
+class LiteralImpl(Literal[T]):
+    """Creates a literal with the given type value."""
+
+    _value: T
+    _data_type: Type
+
+    def __init__(self, value: T, data_type: Type):
+        self._value = value
+        self._data_type = data_type
+
+    def value(self) -> T:
+        return self._value
+
+    def data_type(self) -> Type:
+        return self._data_type
+
+    def __eq__(self, other: object) -> bool:
+        if not isinstance(other, LiteralImpl):
+            return False
+        return (self._value == other._value) and (self._data_type == 
other._data_type)
+
+    def __hash__(self):
+        return hash((self._value, self._data_type))
+
+    def __str__(self):
+        return f"LiteralImpl(value={self._value}, data_type={self._data_type})"
+
+
+class Literals:
+    """The helper class to create literals to pass into Apache Gravitino."""
+
+    NULL = LiteralImpl(None, Types.NullType.get())
+
+    @staticmethod
+    def of(value: T, data_type: Type) -> Literal[T]:
+        return LiteralImpl(value, data_type)
+
+    @staticmethod
+    def boolean_literal(value: bool) -> LiteralImpl[bool]:
+        return LiteralImpl(value, Types.BooleanType.get())
+
+    @staticmethod
+    def byte_literal(value: str) -> LiteralImpl[str]:
+        return LiteralImpl(value, Types.ByteType.get())
+
+    @staticmethod
+    def unsigned_byte_literal(value: str) -> LiteralImpl[str]:
+        return LiteralImpl(value, Types.ByteType.unsigned())
+
+    @staticmethod
+    def short_literal(value: int) -> LiteralImpl[int]:
+        return LiteralImpl(value, Types.ShortType.get())
+
+    @staticmethod
+    def unsigned_short_literal(value: int) -> LiteralImpl[int]:
+        return LiteralImpl(value, Types.ShortType.unsigned())
+
+    @staticmethod
+    def integer_literal(value: int) -> LiteralImpl[int]:
+        return LiteralImpl(value, Types.IntegerType.get())
+
+    @staticmethod
+    def unsigned_integer_literal(value: int) -> LiteralImpl[int]:
+        return LiteralImpl(value, Types.IntegerType.unsigned())
+
+    @staticmethod
+    def long_literal(value: int) -> LiteralImpl[int]:
+        return LiteralImpl(value, Types.LongType.get())
+
+    @staticmethod
+    def unsigned_long_literal(value: int) -> LiteralImpl[int]:
+        return LiteralImpl(value, Types.LongType.unsigned())
+
+    @staticmethod
+    def float_literal(value: float) -> LiteralImpl[float]:
+        return LiteralImpl(value, Types.FloatType.get())
+
+    @staticmethod
+    def double_literal(value: float) -> LiteralImpl[float]:
+        return LiteralImpl(value, Types.DoubleType.get())
+
+    @staticmethod
+    def decimal_literal(value: decimal.Decimal) -> 
LiteralImpl[decimal.Decimal]:
+        precision: int = len(value.as_tuple().digits)
+        scale: int = -value.as_tuple().exponent
+        return LiteralImpl(value, Types.DecimalType.of(max(precision, scale), 
scale))
+
+    @staticmethod
+    def date_literal(value: date) -> Literal[date]:
+        return LiteralImpl(value, Types.DateType.get())
+
+    @staticmethod
+    def time_literal(value: time) -> Literal[time]:
+        return Literals.of(value, Types.TimeType.get())
+
+    @staticmethod
+    def timestamp_literal(value: datetime) -> Literal[datetime]:
+        return Literals.of(value, Types.TimestampType.without_time_zone())
+
+    @staticmethod
+    def timestamp_literal_from_string(value: str) -> Literal[datetime]:
+        return Literals.timestamp_literal(datetime.fromisoformat(value))
+
+    @staticmethod
+    def string_literal(value: str) -> Literal[str]:
+        return LiteralImpl(value, Types.StringType.get())
+
+    @staticmethod
+    def varchar_literal(length: int, value: str) -> Literal[str]:
+        return LiteralImpl(value, Types.VarCharType.of(length))
diff --git a/clients/client-python/gravitino/api/expressions/named_reference.py 
b/clients/client-python/gravitino/api/expressions/named_reference.py
new file mode 100644
index 000000000..3b766b4ac
--- /dev/null
+++ b/clients/client-python/gravitino/api/expressions/named_reference.py
@@ -0,0 +1,86 @@
+# 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 gravitino.api.expressions.expression import Expression
+
+
+class NamedReference(Expression):
+    """
+    Represents a field or column reference in the public logical expression 
API.
+    """
+
+    @staticmethod
+    def field(field_name: list[str]) -> FieldReference:
+        """
+        Returns a FieldReference for the given field name(s). The array of 
field name(s) is
+        used to reference nested fields. For example, if we have a struct 
column named "student" with a
+        data type of StructType{"name": StringType, "age": IntegerType}, we 
can reference the field
+        "name" by calling field("student", "name").
+
+        @param field_name the field name(s)
+        @return a FieldReference for the given field name(s)
+        """
+        return FieldReference(field_name)
+
+    @staticmethod
+    def field_from_column(column_name: str) -> FieldReference:
+        """Returns a FieldReference for the given column name."""
+        return FieldReference([column_name])
+
+    def field_name(self) -> list[str]:
+        """
+        Returns the referenced field name as a list of string parts.
+        Must be implemented by subclasses.
+        """
+        raise NotImplementedError("Subclasses must implement this method.")
+
+    def children(self) -> list[Expression]:
+        """Named references do not have children."""
+        return Expression.EMPTY_EXPRESSION
+
+    def references(self) -> list[NamedReference]:
+        """Named references reference themselves."""
+        return [self]
+
+
+class FieldReference(NamedReference):
+    """
+    A NamedReference that references a field or column.
+    """
+
+    _field_names: list[str]
+
+    def __init__(self, field_names: list[str]):
+        super().__init__()
+        self._field_names = field_names
+
+    def field_name(self) -> list[str]:
+        return self._field_names
+
+    def __eq__(self, other: object) -> bool:
+        if isinstance(other, FieldReference):
+            return self._field_names == other._field_names
+        return False
+
+    def __hash__(self) -> int:
+        return hash(tuple(self._field_names))
+
+    def __str__(self) -> str:
+        """Returns the string representation of the field reference."""
+        return ".".join(self._field_names)
diff --git 
a/clients/client-python/gravitino/api/expressions/unparsed_expression.py 
b/clients/client-python/gravitino/api/expressions/unparsed_expression.py
new file mode 100644
index 000000000..55ca32756
--- /dev/null
+++ b/clients/client-python/gravitino/api/expressions/unparsed_expression.py
@@ -0,0 +1,77 @@
+# 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 gravitino.api.expressions.expression import Expression
+
+
+class UnparsedExpression(Expression):
+    """
+    Represents an expression that is not parsed yet.
+    The parsed expression is represented by FunctionExpression, literal.py, or 
NamedReference.
+    """
+
+    def unparsed_expression(self) -> str:
+        """
+        Returns the unparsed expression as a string.
+        """
+        raise NotImplementedError("Subclasses must implement this method.")
+
+    def children(self) -> list[Expression]:
+        """
+        Unparsed expressions do not have children.
+        """
+        return Expression.EMPTY_EXPRESSION
+
+    @staticmethod
+    def of(unparsed_expression: str) -> UnparsedExpressionImpl:
+        """
+        Creates a new UnparsedExpression with the given unparsed expression.
+
+
+        :param unparsed_expression: The unparsed expression as a string.
+        :return: The created UnparsedExpression.
+        """
+        return UnparsedExpressionImpl(unparsed_expression)
+
+
+class UnparsedExpressionImpl(UnparsedExpression):
+    """
+    An implementation of the UnparsedExpression interface.
+    """
+
+    def __init__(self, unparsed_expression: str):
+        super().__init__()
+        self._unparsed_expression = unparsed_expression
+
+    def unparsed_expression(self) -> str:
+        return self._unparsed_expression
+
+    def __eq__(self, other: object) -> bool:
+        if isinstance(other, UnparsedExpressionImpl):
+            return self._unparsed_expression == other._unparsed_expression
+        return False
+
+    def __hash__(self) -> int:
+        return hash(self._unparsed_expression)
+
+    def __str__(self) -> str:
+        """
+        Returns the string representation of the unparsed expression.
+        """
+        return 
f"UnparsedExpressionImpl{{unparsedExpression='{self._unparsed_expression}'}}"
diff --git a/clients/client-python/gravitino/api/types/__init__.py 
b/clients/client-python/gravitino/api/types/__init__.py
new file mode 100644
index 000000000..13a83393a
--- /dev/null
+++ b/clients/client-python/gravitino/api/types/__init__.py
@@ -0,0 +1,16 @@
+# 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.
diff --git a/clients/client-python/gravitino/api/type.py 
b/clients/client-python/gravitino/api/types/type.py
similarity index 100%
rename from clients/client-python/gravitino/api/type.py
rename to clients/client-python/gravitino/api/types/type.py
diff --git a/clients/client-python/gravitino/api/types.py 
b/clients/client-python/gravitino/api/types/types.py
similarity index 99%
rename from clients/client-python/gravitino/api/types.py
rename to clients/client-python/gravitino/api/types/types.py
index b82ac2b68..63684211a 100644
--- a/clients/client-python/gravitino/api/types.py
+++ b/clients/client-python/gravitino/api/types/types.py
@@ -123,7 +123,7 @@ class Types:
             return cls(True)
 
         @classmethod
-        def unsigned(cls):
+        def unsigned(cls) -> "ShortType":
             return cls(False)
 
         def name(self) -> Name:
diff --git a/clients/client-python/tests/unittests/test_expressions.py 
b/clients/client-python/tests/unittests/test_expressions.py
new file mode 100644
index 000000000..6054c1fde
--- /dev/null
+++ b/clients/client-python/tests/unittests/test_expressions.py
@@ -0,0 +1,61 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import unittest
+from typing import List
+from gravitino.api.expressions.expression import Expression
+from gravitino.api.expressions.named_reference import NamedReference
+
+
+class MockExpression(Expression):
+    """Mock implementation of the Expression class for testing."""
+
+    def __init__(
+        self, children: List[Expression] = None, references: 
List[NamedReference] = None
+    ):
+        self._children = children if children else []
+        self._references = references if references else []
+
+    def children(self) -> List[Expression]:
+        return self._children
+
+    def references(self) -> List[NamedReference]:
+        if self._references:
+            return self._references
+        return super().references()
+
+
+class TestExpression(unittest.TestCase):
+    def test_empty_expression(self):
+        expr = MockExpression()
+        self.assertEqual(expr.children(), [])
+        self.assertEqual(expr.references(), [])
+
+    def test_expression_with_references(self):
+        ref = NamedReference.field(["student", "name"])
+        child = MockExpression(references=[ref])
+        expr = MockExpression(children=[child])
+        self.assertEqual(expr.children(), [child])
+        self.assertEqual(expr.references(), [ref])
+
+    def test_multiple_children(self):
+        ref1 = NamedReference.field(["student", "name"])
+        ref2 = NamedReference.field(["student", "age"])
+        child1 = MockExpression(references=[ref1])
+        child2 = MockExpression(references=[ref2])
+        expr = MockExpression(children=[child1, child2])
+        self.assertCountEqual(expr.references(), [ref1, ref2])
diff --git a/clients/client-python/tests/unittests/test_function_expression.py 
b/clients/client-python/tests/unittests/test_function_expression.py
new file mode 100644
index 000000000..deaa2089e
--- /dev/null
+++ b/clients/client-python/tests/unittests/test_function_expression.py
@@ -0,0 +1,62 @@
+# 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.expressions.function_expression import (
+    FunctionExpression,
+    FuncExpressionImpl,
+)
+from gravitino.api.expressions.expression import Expression
+
+
+class MockExpression(Expression):
+    """Mock implementation of the Expression class for testing."""
+
+    def children(self):
+        return []
+
+    def references(self):
+        return []
+
+    def __str__(self):
+        return "MockExpression()"
+
+
+class TestFunctionExpression(unittest.TestCase):
+    def test_function_without_arguments(self):
+        func = FuncExpressionImpl("SUM", [])
+        self.assertEqual(func.function_name(), "SUM")
+        self.assertEqual(func.arguments(), [])
+        self.assertEqual(str(func), "SUM()")
+
+    def test_function_with_arguments(self):
+        arg1 = MockExpression()
+        arg2 = MockExpression()
+        func = FuncExpressionImpl("SUM", [arg1, arg2])
+        self.assertEqual(func.function_name(), "SUM")
+        self.assertEqual(func.arguments(), [arg1, arg2])
+        self.assertEqual(str(func), "SUM(MockExpression(), MockExpression())")
+
+    def test_function_equality(self):
+        func1 = FuncExpressionImpl("SUM", [])
+        func2 = FuncExpressionImpl("SUM", [])
+        self.assertEqual(func1, func2)
+        self.assertEqual(hash(func1), hash(func2))
+
+    def test_function_of_static_method(self):
+        func = FunctionExpression.of("SUM", MockExpression())
+        self.assertEqual(func.function_name(), "SUM")
diff --git a/clients/client-python/tests/unittests/test_literals.py 
b/clients/client-python/tests/unittests/test_literals.py
new file mode 100644
index 000000000..d9c96b7ba
--- /dev/null
+++ b/clients/client-python/tests/unittests/test_literals.py
@@ -0,0 +1,95 @@
+# 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 datetime import date, time, datetime
+from decimal import Decimal
+
+from gravitino.api.expressions.literals.literals import Literals
+from gravitino.api.types.types import Types
+
+
+class TestLiterals(unittest.TestCase):
+    def test_null_literal(self):
+        null_val = Literals.NULL
+        self.assertEqual(null_val.value(), None)
+        self.assertEqual(null_val.data_type(), Types.NullType.get())
+
+    def test_boolean_literal(self):
+        bool_val = Literals.boolean_literal(True)
+        self.assertEqual(bool_val.value(), True)
+        self.assertEqual(bool_val.data_type(), Types.BooleanType.get())
+
+    def test_integer_literal(self):
+        int_val = Literals.integer_literal(42)
+        self.assertEqual(int_val.value(), 42)
+        self.assertEqual(int_val.data_type(), Types.IntegerType.get())
+
+    def test_string_literal(self):
+        str_val = Literals.string_literal("Hello World")
+        self.assertEqual(str_val.value(), "Hello World")
+        self.assertEqual(str_val.data_type(), Types.StringType.get())
+
+    def test_decimal_literal(self):
+        decimal_val = Literals.decimal_literal(Decimal("0.00"))
+        self.assertEqual(decimal_val.value(), Decimal("0.00"))
+        self.assertEqual(decimal_val.data_type(), Types.DecimalType.of(2, 2))
+
+    def test_date_literal(self):
+        date_val = Literals.date_literal(date(2023, 1, 1))
+        self.assertEqual(date_val.value(), date(2023, 1, 1))
+        self.assertEqual(date_val.data_type(), Types.DateType.get())
+
+    def test_time_literal(self):
+        time_val = Literals.time_literal(time(12, 30, 45))
+        self.assertEqual(time_val.value(), time(12, 30, 45))
+        self.assertEqual(time_val.data_type(), Types.TimeType.get())
+
+    def test_timestamp_literal(self):
+        timestamp_val = Literals.timestamp_literal(datetime(2023, 1, 1, 12, 
30, 45))
+        self.assertEqual(timestamp_val.value(), datetime(2023, 1, 1, 12, 30, 
45))
+        self.assertEqual(
+            timestamp_val.data_type(), Types.TimestampType.without_time_zone()
+        )
+
+    def test_timestamp_literal_from_string(self):
+        timestamp_val = 
Literals.timestamp_literal_from_string("2023-01-01T12:30:45")
+        self.assertEqual(timestamp_val.value(), datetime(2023, 1, 1, 12, 30, 
45))
+        self.assertEqual(
+            timestamp_val.data_type(), Types.TimestampType.without_time_zone()
+        )
+
+    def test_varchar_literal(self):
+        varchar_val = Literals.varchar_literal(10, "Test String")
+        self.assertEqual(varchar_val.value(), "Test String")
+        self.assertEqual(varchar_val.data_type(), Types.VarCharType.of(10))
+
+    def test_equality(self):
+        int_val1 = Literals.integer_literal(42)
+        int_val2 = Literals.integer_literal(42)
+        int_val3 = Literals.integer_literal(10)
+        self.assertTrue(int_val1 == int_val2)
+        self.assertFalse(int_val1 == int_val3)
+
+    def test_hash(self):
+        int_val1 = Literals.integer_literal(42)
+        int_val2 = Literals.integer_literal(42)
+        self.assertEqual(hash(int_val1), hash(int_val2))
+
+    def test_unequal_literals(self):
+        int_val = Literals.integer_literal(42)
+        str_val = Literals.string_literal("Hello")
+        self.assertFalse(int_val == str_val)
diff --git a/clients/client-python/tests/unittests/test_named_reference.py 
b/clients/client-python/tests/unittests/test_named_reference.py
new file mode 100644
index 000000000..a9942aec7
--- /dev/null
+++ b/clients/client-python/tests/unittests/test_named_reference.py
@@ -0,0 +1,39 @@
+# 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.expressions.named_reference import NamedReference, 
FieldReference
+
+
+class TestNamedReference(unittest.TestCase):
+    def test_field_reference_creation(self):
+        field = FieldReference(["student", "name"])
+        self.assertEqual(field.field_name(), ["student", "name"])
+        self.assertEqual(str(field), "student.name")
+
+    def test_field_reference_equality(self):
+        field1 = FieldReference(["student", "name"])
+        field2 = FieldReference(["student", "name"])
+        self.assertEqual(field1, field2)
+        self.assertEqual(hash(field1), hash(field2))
+
+    def test_named_reference_static_methods(self):
+        ref = NamedReference.field(["student", "name"])
+        self.assertEqual(ref.field_name(), ["student", "name"])
+
+        ref2 = NamedReference.field_from_column("student")
+        self.assertEqual(ref2.field_name(), ["student"])
diff --git a/clients/client-python/tests/unittests/test_types.py 
b/clients/client-python/tests/unittests/test_types.py
index e241b420a..bf5685c4a 100644
--- a/clients/client-python/tests/unittests/test_types.py
+++ b/clients/client-python/tests/unittests/test_types.py
@@ -17,7 +17,7 @@
 
 import unittest
 
-from gravitino.api.types import Types, Name
+from gravitino.api.types.types import Types, Name
 
 
 class TestTypes(unittest.TestCase):
diff --git a/clients/client-python/tests/unittests/test_unparsed_expression.py 
b/clients/client-python/tests/unittests/test_unparsed_expression.py
new file mode 100644
index 000000000..809caf67d
--- /dev/null
+++ b/clients/client-python/tests/unittests/test_unparsed_expression.py
@@ -0,0 +1,34 @@
+# 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.expressions.unparsed_expression import 
UnparsedExpressionImpl
+
+
+class TestUnparsedExpression(unittest.TestCase):
+    def test_unparsed_expression_creation(self):
+        expr = UnparsedExpressionImpl("some_expression")
+        self.assertEqual(expr.unparsed_expression(), "some_expression")
+        self.assertEqual(
+            str(expr), 
"UnparsedExpressionImpl{unparsedExpression='some_expression'}"
+        )
+
+    def test_unparsed_expression_equality(self):
+        expr1 = UnparsedExpressionImpl("some_expression")
+        expr2 = UnparsedExpressionImpl("some_expression")
+        self.assertEqual(expr1, expr2)
+        self.assertEqual(hash(expr1), hash(expr2))


Reply via email to