From 32cdc6206a4608b3458dba022daf0aa1fd287000 Mon Sep 17 00:00:00 2001 From: Deniz Alpaslan Date: Sun, 26 Jan 2025 18:24:52 +0000 Subject: [PATCH 1/7] add support for sort in collection.find function --- .gitignore | 3 +++ arango/collection.py | 7 ++++++- arango/utils.py | 39 +++++++++++++++++++++++++++++++++++++++ docs/document.rst | 6 ++++++ tests/test_document.py | 20 ++++++++++++++++++++ 5 files changed, 74 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index c6ef2445..4fa6f46d 100644 --- a/.gitignore +++ b/.gitignore @@ -124,3 +124,6 @@ arango/version.py # test results *_results.txt + +# devcontainers +.devcontainer diff --git a/arango/collection.py b/arango/collection.py index 446200fb..829b6e3a 100644 --- a/arango/collection.py +++ b/arango/collection.py @@ -50,11 +50,13 @@ from arango.typings import Fields, Headers, Json, Jsons, Params from arango.utils import ( build_filter_conditions, + build_sort_expression, get_batches, get_doc_id, is_none_or_bool, is_none_or_int, is_none_or_str, + validate_sort_parameters, ) @@ -753,6 +755,7 @@ def find( skip: Optional[int] = None, limit: Optional[int] = None, allow_dirty_read: bool = False, + sort: Sequence[Json] = [], ) -> Result[Cursor]: """Return all documents that match the given filters. @@ -771,6 +774,8 @@ def find( assert isinstance(filters, dict), "filters must be a dict" assert is_none_or_int(skip), "skip must be a non-negative int" assert is_none_or_int(limit), "limit must be a non-negative int" + if sort: + validate_sort_parameters(sort) skip_val = skip if skip is not None else 0 limit_val = limit if limit is not None else "null" @@ -778,9 +783,9 @@ def find( FOR doc IN @@collection {build_filter_conditions(filters)} LIMIT {skip_val}, {limit_val} + {build_sort_expression(sort)} RETURN doc """ - bind_vars = {"@collection": self.name} request = Request( diff --git a/arango/utils.py b/arango/utils.py index 541f9d0c..0b5add50 100644 --- a/arango/utils.py +++ b/arango/utils.py @@ -126,3 +126,42 @@ def build_filter_conditions(filters: Json) -> str: conditions.append(f"doc.{field} == {json.dumps(v)}") return "FILTER " + " AND ".join(conditions) + + +def validate_sort_parameters(sort: Sequence[Json]) -> bool: + """Validate sort parameters for an AQL query. + + :param sort: Document sort parameters. + :type sort: Sequence[Json] + :return: Validation success. + :rtype: bool + :raise arango.exceptions.DocumentGetError: If sort parameters are invalid. + """ + assert isinstance(sort, Sequence) + for param in sort: + if "sort_by" not in param or "sort_order" not in param: + raise DocumentParseError( + "Each sort parameter must have 'sort_by' and 'sort_order'." + ) + if param["sort_order"].upper() not in ["ASC", "DESC"]: + raise DocumentParseError("'sort_order' must be either 'ASC' or 'DESC'") + return True + + +def build_sort_expression(sort: Sequence[Json]) -> str: + """Build a sort condition for an AQL query. + + :param sort: Document sort parameters. + :type sort: Sequence[Json] + :return: The complete AQL sort condition. + :rtype: str + """ + if not sort: + return "" + + sort_chunks = [] + for sort_param in sort: + chunk = f"doc.{sort_param['sort_by']} {sort_param['sort_order']}" + sort_chunks.append(chunk) + + return "SORT " + ", ".join(sort_chunks) diff --git a/docs/document.rst b/docs/document.rst index 62ad0886..0f0d7d10 100644 --- a/docs/document.rst +++ b/docs/document.rst @@ -103,6 +103,12 @@ Standard documents are managed via collection API wrapper: assert student['GPA'] == 3.6 assert student['last'] == 'Kim' + # Retrieve one or more matching documents, sorted by a field. + for student in students.find({'first': 'John'}, sort=[{'sort_by': 'GPA', 'sort_order': 'DESC'}]): + assert student['_key'] == 'john' + assert student['GPA'] == 3.6 + assert student['last'] == 'Kim' + # Retrieve a document by key. students.get('john') diff --git a/tests/test_document.py b/tests/test_document.py index 37599507..7cb0a435 100644 --- a/tests/test_document.py +++ b/tests/test_document.py @@ -1162,6 +1162,26 @@ def test_document_find(col, bad_col, docs): # Set up test documents col.import_bulk(docs) + # Test find with sort expression (single field) + found = list(col.find({}, sort=[{"sort_by": "text", "sort_order": "ASC"}])) + assert len(found) == 6 + assert found[0]["text"] == "bar" + assert found[-1]["text"] == "foo" + + # Test find with sort expression (multiple fields) + found = list( + col.find( + {}, + sort=[ + {"sort_by": "text", "sort_order": "ASC"}, + {"sort_by": "val", "sort_order": "DESC"}, + ], + ) + ) + assert len(found) == 6 + assert found[0]["val"] == 6 + assert found[-1]["val"] == 1 + # Test find (single match) with default options found = list(col.find({"val": 2})) assert len(found) == 1 From 107df529b51c9905a7ce9ec0dd31787d5182da96 Mon Sep 17 00:00:00 2001 From: Deniz Alpaslan Date: Thu, 30 Jan 2025 11:00:07 +0000 Subject: [PATCH 2/7] update sort parameter type. add SortValidationError as custom exception --- arango/collection.py | 5 ++++- arango/exceptions.py | 7 +++++++ arango/utils.py | 6 +++--- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/arango/collection.py b/arango/collection.py index 829b6e3a..ba229244 100644 --- a/arango/collection.py +++ b/arango/collection.py @@ -720,6 +720,7 @@ def all( :return: Document cursor. :rtype: arango.cursor.Cursor :raise arango.exceptions.DocumentGetError: If retrieval fails. + :raise arango.exceptions.SortValidationError: If sort parameters are invalid. """ assert is_none_or_int(skip), "skip must be a non-negative int" assert is_none_or_int(limit), "limit must be a non-negative int" @@ -755,7 +756,7 @@ def find( skip: Optional[int] = None, limit: Optional[int] = None, allow_dirty_read: bool = False, - sort: Sequence[Json] = [], + sort: Jsons = [], ) -> Result[Cursor]: """Return all documents that match the given filters. @@ -767,6 +768,8 @@ def find( :type limit: int | None :param allow_dirty_read: Allow reads from followers in a cluster. :type allow_dirty_read: bool + :param sort: Document sort parameters + :type sort: Jsons :return: Document cursor. :rtype: arango.cursor.Cursor :raise arango.exceptions.DocumentGetError: If retrieval fails. diff --git a/arango/exceptions.py b/arango/exceptions.py index 28295b2b..29bcdc17 100644 --- a/arango/exceptions.py +++ b/arango/exceptions.py @@ -1074,3 +1074,10 @@ class JWTRefreshError(ArangoClientError): class JWTExpiredError(ArangoClientError): """JWT token has expired.""" + + +################################### +# Parameter Validation Exceptions # +################################### +class SortValidationError(ArangoClientError): + """Invalid sort parameters.""" diff --git a/arango/utils.py b/arango/utils.py index 0b5add50..0a088bb9 100644 --- a/arango/utils.py +++ b/arango/utils.py @@ -12,7 +12,7 @@ from typing import Any, Iterator, Sequence, Union from arango.exceptions import DocumentParseError -from arango.typings import Json +from arango.typings import Json, Jsons @contextmanager @@ -148,11 +148,11 @@ def validate_sort_parameters(sort: Sequence[Json]) -> bool: return True -def build_sort_expression(sort: Sequence[Json]) -> str: +def build_sort_expression(sort: Jsons) -> str: """Build a sort condition for an AQL query. :param sort: Document sort parameters. - :type sort: Sequence[Json] + :type sort: Jsons :return: The complete AQL sort condition. :rtype: str """ From 1149f01dc0f102678821d9b36e68ed0318a13d68 Mon Sep 17 00:00:00 2001 From: Deniz Alpaslan Date: Thu, 30 Jan 2025 23:26:43 +0300 Subject: [PATCH 3/7] Update arango/collection.py Co-authored-by: Alex Petenchea --- arango/collection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arango/collection.py b/arango/collection.py index ba229244..9c016633 100644 --- a/arango/collection.py +++ b/arango/collection.py @@ -769,7 +769,7 @@ def find( :param allow_dirty_read: Allow reads from followers in a cluster. :type allow_dirty_read: bool :param sort: Document sort parameters - :type sort: Jsons + :type sort: Jsons | None :return: Document cursor. :rtype: arango.cursor.Cursor :raise arango.exceptions.DocumentGetError: If retrieval fails. From df2ab2f15492d8a3f153c206f0f347a17230169d Mon Sep 17 00:00:00 2001 From: Deniz Alpaslan Date: Thu, 30 Jan 2025 23:26:53 +0300 Subject: [PATCH 4/7] Update arango/collection.py Co-authored-by: Alex Petenchea --- arango/collection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arango/collection.py b/arango/collection.py index 9c016633..9481db81 100644 --- a/arango/collection.py +++ b/arango/collection.py @@ -756,7 +756,7 @@ def find( skip: Optional[int] = None, limit: Optional[int] = None, allow_dirty_read: bool = False, - sort: Jsons = [], + sort: Optional[Jsons] = None, ) -> Result[Cursor]: """Return all documents that match the given filters. From 71776c3f5baf81f440caf1095658758cc6d19e4d Mon Sep 17 00:00:00 2001 From: Deniz Alpaslan Date: Thu, 30 Jan 2025 20:39:47 +0000 Subject: [PATCH 5/7] update utils.py and collection.py to raise SortValidationError --- arango/collection.py | 1 + arango/utils.py | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/arango/collection.py b/arango/collection.py index 9481db81..ecb81d35 100644 --- a/arango/collection.py +++ b/arango/collection.py @@ -773,6 +773,7 @@ def find( :return: Document cursor. :rtype: arango.cursor.Cursor :raise arango.exceptions.DocumentGetError: If retrieval fails. + :raise arango.exceptions.SortValidationError: If sort parameters are invalid. """ assert isinstance(filters, dict), "filters must be a dict" assert is_none_or_int(skip), "skip must be a non-negative int" diff --git a/arango/utils.py b/arango/utils.py index 0a088bb9..caa8dd41 100644 --- a/arango/utils.py +++ b/arango/utils.py @@ -11,7 +11,7 @@ from contextlib import contextmanager from typing import Any, Iterator, Sequence, Union -from arango.exceptions import DocumentParseError +from arango.exceptions import DocumentParseError, SortValidationError from arango.typings import Json, Jsons @@ -135,16 +135,16 @@ def validate_sort_parameters(sort: Sequence[Json]) -> bool: :type sort: Sequence[Json] :return: Validation success. :rtype: bool - :raise arango.exceptions.DocumentGetError: If sort parameters are invalid. + :raise arango.exceptions.SortValidationError: If sort parameters are invalid. """ assert isinstance(sort, Sequence) for param in sort: if "sort_by" not in param or "sort_order" not in param: - raise DocumentParseError( + raise SortValidationError( "Each sort parameter must have 'sort_by' and 'sort_order'." ) if param["sort_order"].upper() not in ["ASC", "DESC"]: - raise DocumentParseError("'sort_order' must be either 'ASC' or 'DESC'") + raise SortValidationError("'sort_order' must be either 'ASC' or 'DESC'") return True From 856743a4dd64215d184a583ba0e56c1bd8c88a54 Mon Sep 17 00:00:00 2001 From: Deniz Alpaslan Date: Thu, 30 Jan 2025 20:45:31 +0000 Subject: [PATCH 6/7] update utils.py for build_sort_expression to accept Jsons or None --- arango/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/arango/utils.py b/arango/utils.py index caa8dd41..0d128db3 100644 --- a/arango/utils.py +++ b/arango/utils.py @@ -148,11 +148,11 @@ def validate_sort_parameters(sort: Sequence[Json]) -> bool: return True -def build_sort_expression(sort: Jsons) -> str: +def build_sort_expression(sort: Jsons | None) -> str: """Build a sort condition for an AQL query. :param sort: Document sort parameters. - :type sort: Jsons + :type sort: Jsons | None :return: The complete AQL sort condition. :rtype: str """ From bedc4d46565c221cdb56e0c2a5715d3e5064b7dd Mon Sep 17 00:00:00 2001 From: Alex Petenchea Date: Fri, 31 Jan 2025 12:49:50 +0200 Subject: [PATCH 7/7] Update arango/collection.py --- arango/collection.py | 1 - 1 file changed, 1 deletion(-) diff --git a/arango/collection.py b/arango/collection.py index ecb81d35..e2dfcd2a 100644 --- a/arango/collection.py +++ b/arango/collection.py @@ -720,7 +720,6 @@ def all( :return: Document cursor. :rtype: arango.cursor.Cursor :raise arango.exceptions.DocumentGetError: If retrieval fails. - :raise arango.exceptions.SortValidationError: If sort parameters are invalid. """ assert is_none_or_int(skip), "skip must be a non-negative int" assert is_none_or_int(limit), "limit must be a non-negative int"