Skip to content

Commit

Permalink
Merge pull request #303 from ral-facilities/implement-search-api-wher…
Browse files Browse the repository at this point in the history
…e-filter-operators-#297

Implement Search API WHERE Filter Operators
  • Loading branch information
MRichards99 authored Jan 31, 2022
2 parents 6c9780a + 92918ac commit 4946d8e
Show file tree
Hide file tree
Showing 3 changed files with 64 additions and 12 deletions.
11 changes: 8 additions & 3 deletions datagateway_api/src/common/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,16 @@ def __init__(self, field, value, operation):
self.value = value
self.operation = operation

if self.operation == "in":
if self.operation in ["in", "nin", "inq", "between"]:
if not isinstance(self.value, list):
raise BadRequestError(
"When using the 'in' operation for a WHERE filter, the values must"
" be in a list format e.g. [1, 2, 3]",
f"When using the {self.operation} operation for a WHERE filter, the"
f" values must be in a list format e.g. [1, 2]",
)
if self.operation == "between" and len(self.value) != 2:
raise BadRequestError(
"When using the 'between' operation for a WHERE filter, the list"
"must contain two values e.g. [1, 2]",
)


Expand Down
26 changes: 22 additions & 4 deletions datagateway_api/src/datagateway_api/icat/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def create_filter(self):
log.info("Creating condition for ICAT where filter")
if self.operation == "eq":
where_filter = self.create_condition(self.field, "=", self.value)
elif self.operation == "ne":
elif self.operation in ["ne", "neq"]:
where_filter = self.create_condition(self.field, "!=", self.value)
elif self.operation == "like":
where_filter = self.create_condition(self.field, "like", f"%{self.value}%")
Expand All @@ -75,7 +75,7 @@ def create_filter(self):
where_filter = self.create_condition(self.field, ">", self.value)
elif self.operation == "gte":
where_filter = self.create_condition(self.field, ">=", self.value)
elif self.operation == "in":
elif self.operation in ["in", "inq"]:
# Convert self.value into a string with brackets equivalent to tuple format.
# Cannot convert straight to tuple as single element tuples contain a
# trailing comma which Python ICAT/JPQL doesn't accept
Expand All @@ -88,6 +88,25 @@ def create_filter(self):
self.value = "(NULL)"

where_filter = self.create_condition(self.field, "in", self.value)
elif self.operation == "nin":
# Convert self.value into a string with brackets equivalent to tuple format.
# Cannot convert straight to tuple as single element tuples contain a
# trailing comma which Python ICAT/JPQL doesn't accept
self.value = str(self.value).replace("[", "(").replace("]", ")")

# DataGateway Search can send requests with blank lists. Adding NULL to the
# filter prevents the API from returning a 500. An empty list will be
# returned instead, equivalent to the DB backend
if self.value == "()":
self.value = "(NULL)"

where_filter = self.create_condition(self.field, "not in", self.value)
elif self.operation == "between":
where_filter = self.create_condition(
self.field, "between", f"'{self.value[0]}' and '{self.value[1]}'",
)
elif self.operation == "regexp":
where_filter = self.create_condition(self.field, "regexp", self.value)
else:
raise FilterError(f"Bad operation given to where filter: {self.operation}")

Expand Down Expand Up @@ -116,8 +135,7 @@ def create_condition(attribute_name, operator, value):
# distinct filter is used in a request
jpql_value = (
f"{value}"
if operator == "in"
or operator == "!="
if operator in ("in", "not in", "!=", "between")
or str(value).startswith("UPPER")
or "o." in str(value)
else f"'{value}'"
Expand Down
39 changes: 34 additions & 5 deletions test/datagateway_api/icat/filters/test_where_filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ class TestICATWhereFilter:
"operation, value, expected_condition_value",
[
pytest.param("eq", 5, ["%s = '5'"], id="equal"),
pytest.param("ne", 5, ["%s != 5"], id="not equal"),
pytest.param("ne", 5, ["%s != 5"], id="not equal (ne)"),
pytest.param("neq", 5, ["%s != 5"], id="not equal (neq)"),
pytest.param("like", 5, ["%s like '%%5%%'"], id="like"),
pytest.param("ilike", 5, ["UPPER(%s) like UPPER('%%5%%')"], id="ilike"),
pytest.param("nlike", 5, ["%s not like '%%5%%'"], id="not like"),
Expand All @@ -21,8 +22,20 @@ class TestICATWhereFilter:
pytest.param("lte", 5, ["%s <= '5'"], id="less than or equal"),
pytest.param("gt", 5, ["%s > '5'"], id="greater than"),
pytest.param("gte", 5, ["%s >= '5'"], id="greater than or equal"),
pytest.param("in", [1, 2, 3, 4], ["%s in (1, 2, 3, 4)"], id="in a list"),
pytest.param("in", [], ["%s in (NULL)"], id="empty list"),
pytest.param(
"in", [1, 2, 3, 4], ["%s in (1, 2, 3, 4)"], id="in a list (in)",
),
pytest.param("in", [], ["%s in (NULL)"], id="in empty list (in)"),
pytest.param(
"inq", [1, 2, 3, 4], ["%s in (1, 2, 3, 4)"], id="in a list (inq)",
),
pytest.param("inq", [], ["%s in (NULL)"], id="in empty list (inq)"),
pytest.param(
"nin", [1, 2, 3, 4], ["%s not in (1, 2, 3, 4)"], id="not in a list",
),
pytest.param("nin", [], ["%s not in (NULL)"], id="not in empty list"),
pytest.param("between", [1, 2], ["%s between '1' and '2'"], id="between"),
pytest.param("regexp", "^Test", ["%s regexp '^Test'"], id="regexp"),
],
)
def test_valid_operations(
Expand All @@ -33,9 +46,25 @@ def test_valid_operations(

assert icat_query.conditions == {"id": expected_condition_value}

def test_invalid_in_operation(self, icat_query):
@pytest.mark.parametrize(
"operation, value",
[
pytest.param("in", "1, 2, 3, 4, 5", id="in a list (in)"),
pytest.param("inq", "1, 2, 3, 4, 5", id="in a list (inq)"),
pytest.param("nin", "1, 2, 3, 4, 5", id="nin"),
pytest.param("between", "1, 2, 3, 4, 5", id="between - string value"),
pytest.param("between", [], id="between - empty list"),
pytest.param(
"between", [1], id="between - list with less than two elements",
),
pytest.param(
"between", [1, 2, 3], id="between - list with more than two elements",
),
],
)
def test_invalid_operations_raise_bad_request_error(self, operation, value):
with pytest.raises(BadRequestError):
PythonICATWhereFilter("id", "1, 2, 3, 4, 5", "in")
PythonICATWhereFilter("id", value, operation)

def test_invalid_operation(self, icat_query):
test_filter = PythonICATWhereFilter("id", 10, "non")
Expand Down

0 comments on commit 4946d8e

Please sign in to comment.