Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 16 additions & 7 deletions arango/collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -816,14 +816,15 @@ 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)
query = f"""
FOR doc IN @@collection
{build_filter_conditions(filters)}
{filter_conditions}
LIMIT {skip_val}, {limit_val}
{build_sort_expression(sort)}
RETURN doc
"""
bind_vars = {"@collection": self.name}
bind_vars = {"@collection": self.name, **filter_bind_vars}

request = Request(
method="post",
Expand Down Expand Up @@ -1920,9 +1921,10 @@ def update_match(
# then the collection’s default waitForSync behavior is applied.
sync_val = f", waitForSync: {sync}" if sync is not None else ""

filter_conditions, filter_bind_vars = build_filter_conditions(filters)
query = f"""
FOR doc IN @@collection
{build_filter_conditions(filters)}
{filter_conditions}
{f"LIMIT {limit}" if limit is not None else ""}
UPDATE doc WITH @body IN @@collection
OPTIONS {{ keepNull: @keep_none, mergeObjects: @merge {sync_val} }}
Expand All @@ -1933,6 +1935,7 @@ def update_match(
"body": body,
"keep_none": keep_none,
"merge": merge,
**filter_bind_vars,
}

request = Request(
Expand Down Expand Up @@ -2089,15 +2092,20 @@ def replace_match(
# then the collection’s default waitForSync behavior is applied.
sync_val = f"waitForSync: {sync}" if sync is not None else ""

filter_conditions, filter_bind_vars = build_filter_conditions(filters)
query = f"""
FOR doc IN @@collection
{build_filter_conditions(filters)}
{filter_conditions}
{f"LIMIT {limit}" if limit is not None else ""}
REPLACE doc WITH @body IN @@collection
{f"OPTIONS {{ {sync_val} }}" if sync_val else ""}
""" # noqa: E201 E202

bind_vars = {"@collection": self.name, "body": body}
bind_vars = {
"@collection": self.name,
"body": body,
**filter_bind_vars,
}

request = Request(
method="post",
Expand Down Expand Up @@ -2248,15 +2256,16 @@ def delete_match(
# then the collection’s default waitForSync behavior is applied.
sync_val = f"waitForSync: {sync}" if sync is not None else ""

filter_conditions, filter_bind_vars = build_filter_conditions(filters)
query = f"""
FOR doc IN @@collection
{build_filter_conditions(filters)}
{filter_conditions}
{f"LIMIT {limit}" if limit is not None else ""}
REMOVE doc IN @@collection
{f"OPTIONS {{ {sync_val} }}" if sync_val else ""}
""" # noqa: E201 E202

bind_vars = {"@collection": self.name}
bind_vars = {"@collection": self.name, **filter_bind_vars}

request = Request(
method="post",
Expand Down
35 changes: 25 additions & 10 deletions arango/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@
"is_none_or_str",
]

import json
import logging
from contextlib import contextmanager
from typing import Any, Iterator, Optional, Sequence, Union
from typing import Any, Iterator, Optional, Sequence, Tuple, Union

from arango.exceptions import DocumentParseError, SortValidationError
from arango.typings import Json, Jsons
Expand Down Expand Up @@ -109,23 +108,39 @@ def get_batches(elements: Sequence[Json], batch_size: int) -> Iterator[Sequence[
yield elements[index : index + batch_size]


def build_filter_conditions(filters: Json) -> str:
def build_filter_conditions(filters: Json) -> Tuple[str, Json]:
"""Build a filter condition for an AQL query.

:param filters: Document filters.
:type filters: Dict[str, Any]
:return: The complete AQL filter condition.
:rtype: str
:return: The complete AQL filter condition and its bind variables.
:rtype: tuple[str, dict]
"""
if not filters:
return ""
return "", {}

conditions = []
for k, v in filters.items():
field = k if "." in k else f"`{k}`"
conditions.append(f"doc.{field} == {json.dumps(v)}")
bind_vars = {}
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})"
)

value_var = f"filter_value_{filter_index}"
bind_vars[value_var] = value
conditions.append(f"{field_access} == @{value_var}")

return "FILTER " + " AND ".join(conditions)
return "FILTER " + " AND ".join(conditions), bind_vars


def validate_sort_parameters(sort: Jsons) -> bool:
Expand Down
47 changes: 47 additions & 0 deletions tests/test_document.py
Original file line number Diff line number Diff line change
Expand Up @@ -1278,6 +1278,53 @@ def test_document_find(col, bad_col, docs):
assert len(list(col.find({"foo.bar": "baz"}))) == 1


def test_document_match_with_invalid_field_name(col):
field = "foo`bar"
dotted_field = "foo.bar"
complex_field = "foo.bar`baz.qux`quux"

col.insert_many(
[
{
"_key": "find",
field: "find",
dotted_field: "find",
complex_field: "find",
},
{
"_key": "update",
field: "update",
dotted_field: "update",
complex_field: "update",
},
{
"_key": "replace",
field: "replace",
dotted_field: "replace",
complex_field: "replace",
},
{
"_key": "delete",
field: "delete",
dotted_field: "delete",
complex_field: "delete",
},
]
)
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 col.update_match({field: "update"}, {"updated": True}) == 1
assert col["update"]["updated"] is True

assert col.replace_match({field: "replace"}, {"replaced": True}) == 1
assert col["replace"]["replaced"] is True

assert col.delete_match({field: "delete"}) == 1
assert "delete" not in col


def test_document_find_near(db_version, col, bad_col, docs):
if db_version >= version.parse("4.0.0"):
pytest.skip("Not tested in ArangoDB 4.0 and above")
Expand Down
Loading