This is an automated email from the ASF dual-hosted git repository.
unknowntpo 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 417e146eaa [#8806] feat(client-python): add from_function_arg(s) to
DTOConverters (#8819)
417e146eaa is described below
commit 417e146eaa1c5e65f909d73a81b83e7681dba27f
Author: George T. C. Lai <[email protected]>
AuthorDate: Thu Oct 16 21:57:51 2025 +0800
[#8806] feat(client-python): add from_function_arg(s) to DTOConverters
(#8819)
<!--
1. Title: [#<issue>] <type>(<scope>): <subject>
Examples:
- "[#123] feat(operator): support xxx"
- "[#233] fix: check null before access result in xxx"
- "[MINOR] refactor: fix typo in variable name"
- "[MINOR] docs: fix typo in README"
- "[#255] test: fix flaky test NameOfTheTest"
Reference: https://www.conventionalcommits.org/en/v1.0.0/
2. If the PR is unfinished, please mark this PR as draft.
-->
### What changes were proposed in this pull request?
This PR is aimed at implementing the following two methods in class
`gravitino.dto.util.dto_converters.DTOConverters`
- `from_function_arg`
- `from_function_args`
We can therefore implement the method `from_dto` in the same class.
### Why are the changes needed?
It's required for implementing table operations.
Fix: #8806
### 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]>
---
.../client-python/gravitino/dto/util/__init__.py | 16 +++
.../gravitino/dto/util/dto_converters.py | 78 +++++++++++
.../tests/unittests/dto/util/__init__.py | 16 +++
.../unittests/dto/util/test_dto_converters.py | 148 +++++++++++++++++++++
4 files changed, 258 insertions(+)
diff --git a/clients/client-python/gravitino/dto/util/__init__.py
b/clients/client-python/gravitino/dto/util/__init__.py
new file mode 100644
index 0000000000..13a83393a9
--- /dev/null
+++ b/clients/client-python/gravitino/dto/util/__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/dto/util/dto_converters.py
b/clients/client-python/gravitino/dto/util/dto_converters.py
new file mode 100644
index 0000000000..5394a01677
--- /dev/null
+++ b/clients/client-python/gravitino/dto/util/dto_converters.py
@@ -0,0 +1,78 @@
+# 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 typing import cast
+
+from gravitino.api.rel.expressions.expression import Expression
+from gravitino.api.rel.expressions.function_expression import
FunctionExpression
+from gravitino.api.rel.expressions.literals.literals import Literals
+from gravitino.api.rel.expressions.named_reference import NamedReference
+from gravitino.api.rel.expressions.unparsed_expression import
UnparsedExpression
+from gravitino.api.rel.types.types import Types
+from gravitino.dto.rel.expressions.field_reference_dto import FieldReferenceDTO
+from gravitino.dto.rel.expressions.func_expression_dto import FuncExpressionDTO
+from gravitino.dto.rel.expressions.function_arg import FunctionArg
+from gravitino.dto.rel.expressions.literal_dto import LiteralDTO
+from gravitino.dto.rel.expressions.unparsed_expression_dto import
UnparsedExpressionDTO
+from gravitino.exceptions.base import IllegalArgumentException
+
+
+class DTOConverters:
+ """Utility class for converting between DTOs and domain objects."""
+
+ @staticmethod
+ def from_function_arg(arg: FunctionArg) -> Expression:
+ """Converts a FunctionArg DTO to an Expression.
+
+ Args:
+ arg (FunctionArg): The function argument DTO to be converted.
+
+ Returns:
+ Expression: The expression.
+ """
+ arg_type = arg.arg_type()
+ if arg_type is FunctionArg.ArgType.LITERAL:
+ dto = cast(LiteralDTO, arg)
+ if dto.value() is None or dto.data_type() == Types.NullType.get():
+ return Literals.NULL
+ return Literals.of(dto.value(), dto.data_type())
+ if arg_type is FunctionArg.ArgType.FIELD:
+ dto = cast(FieldReferenceDTO, arg)
+ return NamedReference.field(dto.field_name())
+ if arg_type is FunctionArg.ArgType.FUNCTION:
+ dto = cast(FuncExpressionDTO, arg)
+ return FunctionExpression.of(
+ dto.function_name(),
*DTOConverters.from_function_args(dto.args())
+ )
+ if arg_type is FunctionArg.ArgType.UNPARSED:
+ dto = cast(UnparsedExpressionDTO, arg)
+ return UnparsedExpression.of(dto.unparsed_expression())
+ raise IllegalArgumentException(f"Unsupported expression type: {arg}")
+
+ @staticmethod
+ def from_function_args(args: list[FunctionArg]) -> list[Expression]:
+ """Converts a FunctionArg DTO to an Expression.
+
+ Args:
+ args (list[FunctionArg]): The function argument DTOs to be
converted.
+
+ Returns:
+ list[Expression]: The list of expressions.
+ """
+ if not args:
+ return Expression.EMPTY_EXPRESSION
+ return [DTOConverters.from_function_arg(arg) for arg in args]
diff --git a/clients/client-python/tests/unittests/dto/util/__init__.py
b/clients/client-python/tests/unittests/dto/util/__init__.py
new file mode 100644
index 0000000000..13a83393a9
--- /dev/null
+++ b/clients/client-python/tests/unittests/dto/util/__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/tests/unittests/dto/util/test_dto_converters.py
b/clients/client-python/tests/unittests/dto/util/test_dto_converters.py
new file mode 100644
index 0000000000..a7c685c70b
--- /dev/null
+++ b/clients/client-python/tests/unittests/dto/util/test_dto_converters.py
@@ -0,0 +1,148 @@
+# 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 datetime
+from random import randint, random
+from typing import cast
+from unittest.mock import MagicMock, patch
+
+from gravitino.api.rel.expressions.expression import Expression
+from gravitino.api.rel.expressions.function_expression import
FunctionExpression
+from gravitino.api.rel.expressions.literals.literals import Literals
+from gravitino.api.rel.expressions.named_reference import FieldReference
+from gravitino.api.rel.expressions.unparsed_expression import
UnparsedExpression
+from gravitino.api.rel.types.types import Types
+from gravitino.dto.rel.expressions.field_reference_dto import FieldReferenceDTO
+from gravitino.dto.rel.expressions.func_expression_dto import FuncExpressionDTO
+from gravitino.dto.rel.expressions.function_arg import FunctionArg
+from gravitino.dto.rel.expressions.literal_dto import LiteralDTO
+from gravitino.dto.rel.expressions.unparsed_expression_dto import
UnparsedExpressionDTO
+from gravitino.dto.util.dto_converters import DTOConverters
+from gravitino.exceptions.base import IllegalArgumentException
+
+
+class TestDTOConverters(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls) -> None:
+ random_integer = randint(-100, 100)
+ random_float = round(random() * 100, 2)
+ cls.literals = {
+ Types.NullType.get(): None,
+ Types.BooleanType.get(): True,
+ Types.IntegerType.get(): random_integer,
+ Types.DoubleType.get(): random_float,
+ Types.StringType.get(): "test",
+ Types.FloatType.get(): random_float,
+ Types.ShortType.get(): random_integer,
+ Types.LongType.get(): random_integer,
+ Types.DecimalType.of(10, 2): random_float,
+ Types.DateType.get(): "2025-10-14",
+ Types.TimestampType.without_time_zone(): datetime.fromisoformat(
+ "2025-10-14T00:00:00.000"
+ ),
+ Types.TimeType.get(): "11:34:58.123",
+ Types.BinaryType.get(): bytes("test", "utf-8"),
+ Types.VarCharType.of(10): "test",
+ Types.FixedCharType.of(10): "test",
+ }
+
+ def test_from_function_arg_literal_dto(self):
+ for data_type, value in TestDTOConverters.literals.items():
+ expected = Literals.of(value=value, data_type=data_type)
+ literal_dto = (
+
LiteralDTO.builder().with_data_type(data_type).with_value(value).build()
+ )
+ self.assertTrue(DTOConverters.from_function_arg(literal_dto) ==
expected)
+
+ def test_from_function_arg_func_expression_dto(self):
+ function_name = "test_function"
+ args: list[FunctionArg] = [
+ LiteralDTO.builder()
+ .with_data_type(Types.IntegerType.get())
+ .with_value("-1")
+ .build(),
+ LiteralDTO.builder()
+ .with_data_type(Types.BooleanType.get())
+ .with_value("True")
+ .build(),
+ ]
+ func_expression_dto = (
+ FuncExpressionDTO.builder()
+ .with_function_name(function_name)
+ .with_function_args(args)
+ .build()
+ )
+ expected = FunctionExpression.of(
+ function_name,
+ Literals.of(value="-1", data_type=Types.IntegerType.get()),
+ Literals.of(value="True", data_type=Types.BooleanType.get()),
+ )
+ converted = DTOConverters.from_function_arg(func_expression_dto)
+ self.assertIsInstance(converted, FunctionExpression)
+ converted = cast(FunctionExpression, converted)
+ self.assertEqual(converted.function_name(), expected.function_name())
+ self.assertListEqual(converted.arguments(), expected.arguments())
+
+ def test_from_function_arg_field_reference(self):
+ field_names = [f"field_{i}" for i in range(3)]
+ field_reference_dto = (
+
FieldReferenceDTO.builder().with_field_name(field_name=field_names).build()
+ )
+ expected = FieldReference(field_names=field_names)
+ converted = DTOConverters.from_function_arg(field_reference_dto)
+ self.assertIsInstance(converted, FieldReference)
+ self.assertTrue(converted == expected)
+
+ def test_from_function_arg_unparsed(self):
+ expected = UnparsedExpression.of(unparsed_expression="unparsed")
+ unparsed_expression_dto = (
+
UnparsedExpressionDTO.builder().with_unparsed_expression("unparsed").build()
+ )
+ converted = DTOConverters.from_function_arg(unparsed_expression_dto)
+ self.assertIsInstance(converted, UnparsedExpression)
+ self.assertTrue(converted == expected)
+
+ @patch.object(FunctionArg, "arg_type", return_value="invalid_type")
+ def test_from_function_arg_raises_exception(
+ self, mock_function_arg_arg_type: MagicMock
+ ):
+ with self.assertRaisesRegex(
+ IllegalArgumentException, "Unsupported expression type"
+ ):
+ DTOConverters.from_function_arg(mock_function_arg_arg_type)
+
+ def test_from_function_args(self):
+ args = [
+ LiteralDTO.builder()
+ .with_data_type(Types.IntegerType.get())
+ .with_value("-1")
+ .build(),
+ LiteralDTO.builder()
+ .with_data_type(Types.BooleanType.get())
+ .with_value("True")
+ .build(),
+ ]
+ expected = [
+ Literals.of(value="-1", data_type=Types.IntegerType.get()),
+ Literals.of(value="True", data_type=Types.BooleanType.get()),
+ ]
+ converted = DTOConverters.from_function_args(args)
+ self.assertListEqual(converted, expected)
+ self.assertListEqual(
+ DTOConverters.from_function_args([]), Expression.EMPTY_EXPRESSION
+ )