This is an automated email from the ASF dual-hosted git repository.
fanng 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 08573d17cd [#5730] feat(client-python): Add sorts expression (#5879)
08573d17cd is described below
commit 08573d17cd7e482234f5b6fc0dfa0878bf37e2d3
Author: SophieTech88 <[email protected]>
AuthorDate: Tue Jan 7 02:54:17 2025 -0800
[#5730] feat(client-python): Add sorts expression (#5879)
### What changes were proposed in this pull request?
Implement sorts expression in python client, add unit test.
### Why are the changes needed?
We need to support the sorts expressions in python client
Fix: #5730
### 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]>
Co-authored-by: Xun <[email protected]>
---
.../gravitino/api/expressions/sorts/__init__.py | 16 +++
.../api/expressions/sorts/null_ordering.py | 35 ++++++
.../api/expressions/sorts/sort_direction.py | 73 +++++++++++++
.../gravitino/api/expressions/sorts/sort_order.py | 45 ++++++++
.../gravitino/api/expressions/sorts/sort_orders.py | 94 ++++++++++++++++
.../tests/unittests/rel/test_sorts.py | 118 +++++++++++++++++++++
6 files changed, 381 insertions(+)
diff --git a/clients/client-python/gravitino/api/expressions/sorts/__init__.py
b/clients/client-python/gravitino/api/expressions/sorts/__init__.py
new file mode 100644
index 0000000000..13a83393a9
--- /dev/null
+++ b/clients/client-python/gravitino/api/expressions/sorts/__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/sorts/null_ordering.py
b/clients/client-python/gravitino/api/expressions/sorts/null_ordering.py
new file mode 100644
index 0000000000..a65a6efc97
--- /dev/null
+++ b/clients/client-python/gravitino/api/expressions/sorts/null_ordering.py
@@ -0,0 +1,35 @@
+# 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 enum import Enum
+
+
+class NullOrdering(Enum):
+ """A null order used in sorting expressions."""
+
+ NULLS_FIRST: str = "nulls_first"
+ """Nulls appear before non-nulls. For ascending order, this means nulls
appear at the beginning."""
+
+ NULLS_LAST: str = "nulls_last"
+ """Nulls appear after non-nulls. For ascending order, this means nulls
appear at the end."""
+
+ def __str__(self) -> str:
+ if self == NullOrdering.NULLS_FIRST:
+ return "nulls_first"
+ if self == NullOrdering.NULLS_LAST:
+ return "nulls_last"
+
+ raise ValueError(f"Unexpected null order: {self}")
diff --git
a/clients/client-python/gravitino/api/expressions/sorts/sort_direction.py
b/clients/client-python/gravitino/api/expressions/sorts/sort_direction.py
new file mode 100644
index 0000000000..23694b019c
--- /dev/null
+++ b/clients/client-python/gravitino/api/expressions/sorts/sort_direction.py
@@ -0,0 +1,73 @@
+# 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 enum import Enum
+from gravitino.api.expressions.sorts.null_ordering import NullOrdering
+
+
+class SortDirection(Enum):
+ """A sort direction used in sorting expressions.
+ Each direction has a default null ordering that is implied if no null
ordering is specified explicitly.
+ """
+
+ ASCENDING = ("asc", NullOrdering.NULLS_FIRST)
+ """Ascending sort direction. Nulls appear first. For ascending order, this
means nulls appear at the beginning."""
+
+ DESCENDING = ("desc", NullOrdering.NULLS_LAST)
+ """Descending sort direction. Nulls appear last. For ascending order, this
means nulls appear at the end."""
+
+ def __init__(self, direction: str, default_null_ordering: NullOrdering):
+ self._direction = direction
+ self._default_null_ordering = default_null_ordering
+
+ def direction(self) -> str:
+ return self._direction
+
+ def default_null_ordering(self) -> NullOrdering:
+ """
+ Returns the default null ordering to use if no null ordering is
specified explicitly.
+
+ Returns:
+ NullOrdering: The default null ordering.
+ """
+ return self._default_null_ordering
+
+ def __str__(self) -> str:
+ if self == SortDirection.ASCENDING:
+ return SortDirection.ASCENDING.direction()
+ if self == SortDirection.DESCENDING:
+ return SortDirection.DESCENDING.direction()
+
+ raise ValueError(f"Unexpected sort direction: {self}")
+
+ @staticmethod
+ def from_string(direction: str):
+ """
+ Returns the SortDirection from the string representation.
+
+ Args:
+ direction: The string representation of the sort direction.
+
+ Returns:
+ SortDirection: The corresponding SortDirection.
+ """
+ direction = direction.lower()
+ if direction == SortDirection.ASCENDING.direction():
+ return SortDirection.ASCENDING
+ if direction == SortDirection.DESCENDING.direction():
+ return SortDirection.DESCENDING
+
+ raise ValueError(f"Unexpected sort direction: {direction}")
diff --git
a/clients/client-python/gravitino/api/expressions/sorts/sort_order.py
b/clients/client-python/gravitino/api/expressions/sorts/sort_order.py
new file mode 100644
index 0000000000..ae7a1bb27b
--- /dev/null
+++ b/clients/client-python/gravitino/api/expressions/sorts/sort_order.py
@@ -0,0 +1,45 @@
+# 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 ABC, abstractmethod
+from typing import List
+
+from gravitino.api.expressions.expression import Expression
+from gravitino.api.expressions.sorts.null_ordering import NullOrdering
+from gravitino.api.expressions.sorts.sort_direction import SortDirection
+
+
+class SortOrder(Expression, ABC):
+ """Represents a sort order in the public expression API."""
+
+ @abstractmethod
+ def expression(self) -> Expression:
+ """Returns the sort expression."""
+ pass
+
+ @abstractmethod
+ def direction(self) -> SortDirection:
+ """Returns the sort direction."""
+ pass
+
+ @abstractmethod
+ def null_ordering(self) -> NullOrdering:
+ """Returns the null ordering."""
+ pass
+
+ def children(self) -> List[Expression]:
+ """Returns the children expressions of this sort order."""
+ return [self.expression()]
diff --git
a/clients/client-python/gravitino/api/expressions/sorts/sort_orders.py
b/clients/client-python/gravitino/api/expressions/sorts/sort_orders.py
new file mode 100644
index 0000000000..9deaa4bacd
--- /dev/null
+++ b/clients/client-python/gravitino/api/expressions/sorts/sort_orders.py
@@ -0,0 +1,94 @@
+# 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 List
+
+from gravitino.api.expressions.expression import Expression
+from gravitino.api.expressions.sorts.null_ordering import NullOrdering
+from gravitino.api.expressions.sorts.sort_direction import SortDirection
+from gravitino.api.expressions.sorts.sort_order import SortOrder
+
+
+class SortImpl(SortOrder):
+
+ def __init__(
+ self,
+ expression: Expression,
+ direction: SortDirection,
+ null_ordering: NullOrdering,
+ ):
+ """Initialize the SortImpl object."""
+ self._expression = expression
+ self._direction = direction
+ self._null_ordering = null_ordering
+
+ def expression(self) -> Expression:
+ return self._expression
+
+ def direction(self) -> SortDirection:
+ return self._direction
+
+ def null_ordering(self) -> NullOrdering:
+ return self._null_ordering
+
+ def __eq__(self, other: object) -> bool:
+ """Check if two SortImpl instances are equal."""
+ if not isinstance(other, SortImpl):
+ return False
+ return (
+ self.expression() == other.expression()
+ and self.direction() == other.direction()
+ and self.null_ordering() == other.null_ordering()
+ )
+
+ def __hash__(self) -> int:
+ """Generate a hash for a SortImpl instance."""
+ return hash((self.expression(), self.direction(),
self.null_ordering()))
+
+ def __str__(self) -> str:
+ """Provide a string representation of the SortImpl object."""
+ return (
+ f"SortImpl(expression={self._expression}, "
+ f"direction={self._direction},
null_ordering={self._null_ordering})"
+ )
+
+
+class SortOrders:
+ """Helper methods to create SortOrders to pass into Apache Gravitino."""
+
+ # NONE is used to indicate that there is no sort order
+ NONE: List[SortOrder] = []
+
+ @staticmethod
+ def ascending(expression: Expression) -> SortImpl:
+ """Creates a sort order with ascending direction and nulls first."""
+ return SortOrders.of(expression, SortDirection.ASCENDING)
+
+ @staticmethod
+ def descending(expression: Expression) -> SortImpl:
+ """Creates a sort order with descending direction and nulls last."""
+ return SortOrders.of(expression, SortDirection.DESCENDING)
+
+ @staticmethod
+ def of(
+ expression: Expression,
+ direction: SortDirection,
+ null_ordering: NullOrdering = None,
+ ) -> SortImpl:
+ """Creates a sort order with the given direction and optionally
specified null ordering."""
+ if null_ordering is None:
+ null_ordering = direction.default_null_ordering()
+ return SortImpl(expression, direction, null_ordering)
diff --git a/clients/client-python/tests/unittests/rel/test_sorts.py
b/clients/client-python/tests/unittests/rel/test_sorts.py
new file mode 100644
index 0000000000..7116add4de
--- /dev/null
+++ b/clients/client-python/tests/unittests/rel/test_sorts.py
@@ -0,0 +1,118 @@
+# 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 MagicMock
+
+from gravitino.api.expressions.function_expression import FunctionExpression
+from gravitino.api.expressions.named_reference import NamedReference
+from gravitino.api.expressions.sorts.sort_direction import SortDirection
+from gravitino.api.expressions.sorts.null_ordering import NullOrdering
+from gravitino.api.expressions.sorts.sort_orders import SortImpl, SortOrders
+from gravitino.api.expressions.expression import Expression
+
+
+class TestSortOrder(unittest.TestCase):
+ def test_sort_direction_from_string(self):
+ self.assertEqual(SortDirection.from_string("asc"),
SortDirection.ASCENDING)
+ self.assertEqual(SortDirection.from_string("desc"),
SortDirection.DESCENDING)
+ with self.assertRaises(ValueError):
+ SortDirection.from_string("invalid")
+
+ def test_null_ordering(self):
+ self.assertEqual(str(NullOrdering.NULLS_FIRST), "nulls_first")
+ self.assertEqual(str(NullOrdering.NULLS_LAST), "nulls_last")
+
+ def test_sort_impl_initialization(self):
+ mock_expression = MagicMock(spec=Expression)
+ sort_impl = SortImpl(
+ expression=mock_expression,
+ direction=SortDirection.ASCENDING,
+ null_ordering=NullOrdering.NULLS_FIRST,
+ )
+ self.assertEqual(sort_impl.expression(), mock_expression)
+ self.assertEqual(sort_impl.direction(), SortDirection.ASCENDING)
+ self.assertEqual(sort_impl.null_ordering(), NullOrdering.NULLS_FIRST)
+
+ def test_sort_impl_equality(self):
+ mock_expression1 = MagicMock(spec=Expression)
+ mock_expression2 = MagicMock(spec=Expression)
+
+ sort_impl1 = SortImpl(
+ expression=mock_expression1,
+ direction=SortDirection.ASCENDING,
+ null_ordering=NullOrdering.NULLS_FIRST,
+ )
+ sort_impl2 = SortImpl(
+ expression=mock_expression1,
+ direction=SortDirection.ASCENDING,
+ null_ordering=NullOrdering.NULLS_FIRST,
+ )
+ sort_impl3 = SortImpl(
+ expression=mock_expression2,
+ direction=SortDirection.ASCENDING,
+ null_ordering=NullOrdering.NULLS_FIRST,
+ )
+
+ self.assertEqual(sort_impl1, sort_impl2)
+ self.assertNotEqual(sort_impl1, sort_impl3)
+
+ def test_sort_orders(self):
+ mock_expression = MagicMock(spec=Expression)
+ ascending_order = SortOrders.ascending(mock_expression)
+ self.assertEqual(ascending_order.direction(), SortDirection.ASCENDING)
+ self.assertEqual(ascending_order.null_ordering(),
NullOrdering.NULLS_FIRST)
+
+ descending_order = SortOrders.descending(mock_expression)
+ self.assertEqual(descending_order.direction(),
SortDirection.DESCENDING)
+ self.assertEqual(descending_order.null_ordering(),
NullOrdering.NULLS_LAST)
+
+ def test_sort_impl_string_representation(self):
+ mock_expression = MagicMock(spec=Expression)
+ sort_impl = SortImpl(
+ expression=mock_expression,
+ direction=SortDirection.ASCENDING,
+ null_ordering=NullOrdering.NULLS_FIRST,
+ )
+ expected_str = (
+ f"SortImpl(expression={mock_expression}, "
+ f"direction=asc, null_ordering=nulls_first)"
+ )
+ self.assertEqual(str(sort_impl), expected_str)
+
+ def test_sort_order(self):
+ field_reference = NamedReference.field(["field1"])
+ sort_order = SortOrders.of(
+ field_reference, SortDirection.ASCENDING, NullOrdering.NULLS_FIRST
+ )
+
+ self.assertEqual(NullOrdering.NULLS_FIRST, sort_order.null_ordering())
+ self.assertEqual(SortDirection.ASCENDING, sort_order.direction())
+ self.assertIsInstance(sort_order.expression(), NamedReference)
+ self.assertEqual(["field1"], sort_order.expression().field_name())
+
+ date = FunctionExpression.of("date", NamedReference.field(["b"]))
+ sort_order = SortOrders.of(
+ date, SortDirection.DESCENDING, NullOrdering.NULLS_LAST
+ )
+ self.assertEqual(NullOrdering.NULLS_LAST, sort_order.null_ordering())
+ self.assertEqual(SortDirection.DESCENDING, sort_order.direction())
+
+ self.assertIsInstance(sort_order.expression(), FunctionExpression)
+ self.assertEqual("date", sort_order.expression().function_name())
+ self.assertEqual(
+ ["b"],
sort_order.expression().arguments()[0].references()[0].field_name()
+ )