diff --git a/CHANGELOG.md b/CHANGELOG.md index 97cd63f0..8deb20a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ +## v3.4.0 (2022-01-31) +### Feature +* Implement `regexp` operator #297 ([`bf3fe0e`](https://github.com/ral-facilities/datagateway-api/commit/bf3fe0ef2ac582d55dbd881edf6a81a93625ce91)) +* Implement `neq` operator #297 ([`9094bbb`](https://github.com/ral-facilities/datagateway-api/commit/9094bbb894ead20a53fadfd0e24b264af29548b9)) +* Implement `nin` operator #297 ([`00dbba5`](https://github.com/ral-facilities/datagateway-api/commit/00dbba525d5cd86cb5577f3b1621a7042cdd2fa0)) +* Implement `inq` operator #297 ([`fc1cf19`](https://github.com/ral-facilities/datagateway-api/commit/fc1cf194454a4da60652b1f68df278c4624ddc11)) +* Implement `between` operator #297 ([`4736888`](https://github.com/ral-facilities/datagateway-api/commit/4736888bf76cda0dbc00f997443ed565f0f5e760)) + ## v3.3.0 (2022-01-31) ### Feature * Add function to get PaNOSC to ICAT mapping for where filter #260 ([`34b1d81`](https://github.com/ral-facilities/datagateway-api/commit/34b1d819482aa3efdb4f8da321125d3e40d76617)) diff --git a/datagateway_api/src/common/filters.py b/datagateway_api/src/common/filters.py index c99e26eb..bd0e636c 100644 --- a/datagateway_api/src/common/filters.py +++ b/datagateway_api/src/common/filters.py @@ -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]", ) diff --git a/datagateway_api/src/datagateway_api/icat/filters.py b/datagateway_api/src/datagateway_api/icat/filters.py index 9877938a..03d79efd 100644 --- a/datagateway_api/src/datagateway_api/icat/filters.py +++ b/datagateway_api/src/datagateway_api/icat/filters.py @@ -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}%") @@ -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 @@ -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}") @@ -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}'" diff --git a/pyproject.toml b/pyproject.toml index 6fbb6040..a8663bbd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "datagateway-api" -version = "3.3.0" +version = "3.4.0" description = "ICAT API to interface with the DataGateway" license = "Apache-2.0" readme = "README.md" diff --git a/test/datagateway_api/icat/filters/test_where_filter.py b/test/datagateway_api/icat/filters/test_where_filter.py index bc41a5fa..6abaaf00 100644 --- a/test/datagateway_api/icat/filters/test_where_filter.py +++ b/test/datagateway_api/icat/filters/test_where_filter.py @@ -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"), @@ -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( @@ -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")