From 15fd0ce9b34d9d3c3a5bfcd56a2f21d8dabb5695 Mon Sep 17 00:00:00 2001 From: Alex Petenchea Date: Mon, 24 Aug 2026 22:11:55 +0800 Subject: [PATCH 1/2] Adding nested field test --- tests/test_document.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_document.py b/tests/test_document.py index aaa5b921..15e05af6 100644 --- a/tests/test_document.py +++ b/tests/test_document.py @@ -1309,11 +1309,16 @@ def test_document_match_with_invalid_field_name(col): dotted_field: "delete", complex_field: "delete", }, + { + "_key": "nested", + "foo": {"bar`baz": {"qux`quux": "nested"}}, + }, ] ) assert [doc["_key"] for doc in col.find({field: "find"})] == ["find"] assert [doc["_key"] for doc in col.find({dotted_field: "find"})] == ["find"] assert [doc["_key"] for doc in col.find({complex_field: "find"})] == ["find"] + assert [doc["_key"] for doc in col.find({complex_field: "nested"})] == ["nested"] assert col.update_match({field: "update"}, {"updated": True}) == 1 assert col["update"]["updated"] is True From 7fdcf73a84f3a8c3de312e513efc2f02e45f8c66 Mon Sep 17 00:00:00 2001 From: Alex Petenchea Date: Mon, 24 Aug 2026 22:27:52 +0800 Subject: [PATCH 2/2] Adding sort expression fix --- arango/collection.py | 9 +++++-- arango/utils.py | 57 ++++++++++++++++++++++++++---------------- tests/test_document.py | 4 +++ 3 files changed, 47 insertions(+), 23 deletions(-) diff --git a/arango/collection.py b/arango/collection.py index f7f56313..70ca49b0 100644 --- a/arango/collection.py +++ b/arango/collection.py @@ -817,14 +817,19 @@ def find( skip_val = skip if skip is not None else 0 limit_val = limit if limit is not None else "null" filter_conditions, filter_bind_vars = build_filter_conditions(filters) + sort_expression, sort_bind_vars = build_sort_expression(sort) query = f""" FOR doc IN @@collection {filter_conditions} LIMIT {skip_val}, {limit_val} - {build_sort_expression(sort)} + {sort_expression} RETURN doc """ - bind_vars = {"@collection": self.name, **filter_bind_vars} + bind_vars = { + "@collection": self.name, + **filter_bind_vars, + **sort_bind_vars, + } request = Request( method="post", diff --git a/arango/utils.py b/arango/utils.py index f2139e11..14521ee2 100644 --- a/arango/utils.py +++ b/arango/utils.py @@ -108,6 +108,23 @@ def get_batches(elements: Sequence[Json], batch_size: int) -> Iterator[Sequence[ yield elements[index : index + batch_size] +def _build_attribute_expression(field: str, prefix: str, bind_vars: Json) -> str: + """Build a bind-safe AQL document attribute expression.""" + bind_vars[prefix] = field + field_access = f"doc[@{prefix}]" + + if "." not in field: + return field_access + + nested_access = "doc" + for field_index, field_part in enumerate(field.split(".")): + field_var = f"{prefix}_{field_index}" + bind_vars[field_var] = field_part + nested_access += f"[@{field_var}]" + + return f"(HAS(doc, @{prefix}) ? {field_access} : {nested_access})" + + def build_filter_conditions(filters: Json) -> Tuple[str, Json]: """Build a filter condition for an AQL query. @@ -120,21 +137,13 @@ def build_filter_conditions(filters: Json) -> Tuple[str, Json]: return "", {} conditions = [] - bind_vars = {} + bind_vars: Json = {} for filter_index, (field, value) in enumerate(filters.items()): - field_access = "doc" - for field_index, field_part in enumerate(field.split(".")): - field_var = f"filter_field_{filter_index}_{field_index}" - bind_vars[field_var] = field_part - field_access += f"[@{field_var}]" - - if "." in field: - full_field_var = f"filter_field_{filter_index}" - bind_vars[full_field_var] = field - field_access = ( - f"(HAS(doc, @{full_field_var}) " - f"? doc[@{full_field_var}] : {field_access})" - ) + field_access = _build_attribute_expression( + field, + f"filter_field_{filter_index}", + bind_vars, + ) value_var = f"filter_value_{filter_index}" bind_vars[value_var] = value @@ -163,20 +172,26 @@ def validate_sort_parameters(sort: Jsons) -> bool: return True -def build_sort_expression(sort: Optional[Jsons]) -> str: +def build_sort_expression(sort: Optional[Jsons]) -> Tuple[str, Json]: """Build a sort condition for an AQL query. :param sort: Document sort parameters. :type sort: Jsons | None - :return: The complete AQL sort condition. - :rtype: str + :return: The complete AQL sort condition and its bind variables. + :rtype: tuple[str, dict] """ if not sort: - return "" + return "", {} sort_chunks = [] - for sort_param in sort: - chunk = f"doc.{sort_param['sort_by']} {sort_param['sort_order']}" + bind_vars: Json = {} + for sort_index, sort_param in enumerate(sort): + field_access = _build_attribute_expression( + sort_param["sort_by"], + f"sort_field_{sort_index}", + bind_vars, + ) + chunk = f"{field_access} {sort_param['sort_order'].upper()}" sort_chunks.append(chunk) - return "SORT " + ", ".join(sort_chunks) + return "SORT " + ", ".join(sort_chunks), bind_vars diff --git a/tests/test_document.py b/tests/test_document.py index 15e05af6..07f01f4f 100644 --- a/tests/test_document.py +++ b/tests/test_document.py @@ -1319,6 +1319,10 @@ def test_document_match_with_invalid_field_name(col): assert [doc["_key"] for doc in col.find({dotted_field: "find"})] == ["find"] assert [doc["_key"] for doc in col.find({complex_field: "find"})] == ["find"] assert [doc["_key"] for doc in col.find({complex_field: "nested"})] == ["nested"] + assert [ + doc["_key"] + for doc in col.find({}, sort=[{"sort_by": complex_field, "sort_order": "ASC"}]) + ] == ["delete", "find", "nested", "replace", "update"] assert col.update_match({field: "update"}, {"updated": True}) == 1 assert col["update"]["updated"] is True