diff --git a/autosubmit_api/database/queries.py b/autosubmit_api/database/queries.py index 55cca9ea7698701d3048407a167709e19b5615ab..6c16b970b76e128cc7136de5bea4e39c3de44eed 100644 --- a/autosubmit_api/database/queries.py +++ b/autosubmit_api/database/queries.py @@ -1,18 +1,31 @@ from typing import Optional from pyparsing import Any -from sqlalchemy import Column, select, or_ +from sqlalchemy import Column, Select, select, or_ from autosubmit_api.database import tables +def wildcard_search(query: str, column: Column) -> str: + """ + Replace * to % for wildcard search and seek if the query is negated + """ + # Replace * to % for wildcard search + query = query.replace("*", "%") + # Check if the query is negated + if query.startswith("!"): + return column.not_like(query[1:]) + return column.like(query) + + def generate_query_listexp_extended( query: str = None, only_active: bool = False, owner: str = None, exp_type: str = None, autosubmit_version: str = None, + hpc: str = None, order_by: str = None, order_desc: bool = False, -): +) -> Select: """ Query listexp without accessing the view with status and total/completed jobs. """ @@ -42,7 +55,7 @@ def generate_query_listexp_extended( if query: filter_stmts.append( or_( - tables.experiment_table.c.name.like(f"{query}%"), + tables.experiment_table.c.name.like(f"%{query}%"), tables.experiment_table.c.description.like(f"%{query}%"), tables.details_table.c.user.like(f"%{query}%"), ) @@ -52,7 +65,7 @@ def generate_query_listexp_extended( filter_stmts.append(tables.experiment_status_table.c.status == "RUNNING") if owner: - filter_stmts.append(tables.details_table.c.user == owner) + filter_stmts.append(wildcard_search(owner, tables.details_table.c.user)) if exp_type == "test": filter_stmts.append(tables.experiment_table.c.name.like("t%")) @@ -64,9 +77,14 @@ def generate_query_listexp_extended( if autosubmit_version: filter_stmts.append( - tables.experiment_table.c.autosubmit_version == autosubmit_version + wildcard_search( + autosubmit_version, tables.experiment_table.c.autosubmit_version + ) ) + if hpc: + filter_stmts.append(wildcard_search(hpc, tables.details_table.c.hpc)) + statement = statement.where(*filter_stmts) # Order by diff --git a/autosubmit_api/models/requests.py b/autosubmit_api/models/requests.py index d0b53cc50787515484929361e771e8b8803983a4..8857e38403f34c7b32d2d678a410369fde66a510 100644 --- a/autosubmit_api/models/requests.py +++ b/autosubmit_api/models/requests.py @@ -10,6 +10,7 @@ class ExperimentsSearchRequest(BaseModel): owner: Optional[str] = None exp_type: Optional[Literal["test", "operational", "experiment"]] = None autosubmit_version: Optional[str] = None + hpc: Optional[str] = None order_by: Optional[Literal["expid", "created", "description"]] = None order_desc: bool = True diff --git a/autosubmit_api/routers/v4/experiments.py b/autosubmit_api/routers/v4/experiments.py index 05225f713c21c039efbbb9902b7331cc22878b3e..a70762be3ba24e103e07122871054a30a6d8a7e6 100644 --- a/autosubmit_api/routers/v4/experiments.py +++ b/autosubmit_api/routers/v4/experiments.py @@ -68,6 +68,7 @@ async def search_experiments( owner=query_params.owner, exp_type=query_params.exp_type, autosubmit_version=query_params.autosubmit_version, + hpc=query_params.hpc, order_by=query_params.order_by, order_desc=query_params.order_desc, ) diff --git a/tests/test_repository.py b/tests/test_repository.py new file mode 100644 index 0000000000000000000000000000000000000000..08408bf9717ea9fbff2a96a77cfaf3e56cb2b5c6 --- /dev/null +++ b/tests/test_repository.py @@ -0,0 +1,128 @@ +from typing import Any, Dict, List +from autosubmit_api.database.queries import generate_query_listexp_extended +import pytest + +BASE_FROM = ( + "FROM experiment LEFT OUTER JOIN details ON experiment.id = details.exp_id " + "LEFT OUTER JOIN experiment_status ON experiment.id = experiment_status.exp_id" +) + + +@pytest.mark.parametrize( + "query_args, expected_in_query, expected_params", + [ + # Test the basic query generation + (dict(), [BASE_FROM], {}), + # Test the query generation with a search query + ( + dict(query="test"), + [ + BASE_FROM, + "experiment.name LIKE :name_1", + "experiment.description LIKE :description_1", + 'details."user" LIKE :user_1', + ], + {"name_1": "%test%", "description_1": "%test%", "user_1": "%test%"}, + ), + # Test the query generation with active filter + ( + dict(only_active=True), + [BASE_FROM, "experiment_status.status = :status_1"], + {"status_1": "RUNNING"}, + ), + # Test the query generation with owner filter + ( + dict(owner="test"), + [BASE_FROM, 'details."user" LIKE :user_1'], + {"user_1": "test"}, + ), + # Test the query generation with experiment type filter + ( + dict(exp_type="test"), + [BASE_FROM, "experiment.name LIKE :name_1"], + {"name_1": "t%"}, + ), + ( + dict(exp_type="operational"), + [BASE_FROM, "experiment.name LIKE :name_1"], + {"name_1": "o%"}, + ), + ( + dict(exp_type="experiment"), + [ + BASE_FROM, + "experiment.name NOT LIKE :name_1", + "experiment.name NOT LIKE :name_2", + ], + {"name_1": "t%", "name_2": "o%"}, + ), + # Test the query generation with autosubmit version filter + ( + dict(autosubmit_version="1.0"), + [BASE_FROM, "experiment.autosubmit_version LIKE :autosubmit_version_1"], + {"autosubmit_version_1": "1.0"}, + ), + # Test the query generation with hpc filter + ( + dict(hpc="MN5"), + [BASE_FROM, "details.hpc LIKE :hpc_1"], + {"hpc_1": "MN5"}, + ), + # Test the query generation with order by + ( + dict(order_by="expid"), + [BASE_FROM, "ORDER BY experiment.name"], + {}, + ), + ( + dict(order_by="expid", order_desc=True), + [BASE_FROM, "ORDER BY experiment.name DESC"], + {}, + ), + # Test wildcard search query + ( + dict(owner="foo*bar"), + [BASE_FROM, 'details."user" LIKE :user_1'], + {"user_1": "foo%bar"}, + ), + ( + dict(owner="!foo*bar*baz"), + [BASE_FROM, 'details."user" NOT LIKE :user_1'], + {"user_1": "foo%bar%baz"}, + ), + ( + dict(autosubmit_version="3.*.0"), + [BASE_FROM, "experiment.autosubmit_version LIKE :autosubmit_version_1"], + {"autosubmit_version_1": "3.%.0"}, + ), + ( + dict(autosubmit_version="!3.*.0"), + [BASE_FROM, "experiment.autosubmit_version NOT LIKE :autosubmit_version_1"], + {"autosubmit_version_1": "3.%.0"}, + ), + ( + dict(owner="!foo*bar*baz", autosubmit_version="3.*.0", hpc="MN*"), + [ + BASE_FROM, + 'details."user" NOT LIKE :user_1', + "experiment.autosubmit_version LIKE :autosubmit_version_1", + "details.hpc LIKE :hpc_1", + ], + {"user_1": "foo%bar%baz", "autosubmit_version_1": "3.%.0", "hpc_1": "MN%"}, + ), + ], +) +def test_experiment_search_query_generator( + query_args: Dict[str, Any], + expected_in_query: List[str], + expected_params: Dict[str, str], +): + query = generate_query_listexp_extended(**query_args) + + for expected in expected_in_query: + assert expected in str(query) + + query_params = query.compile().params + + for param, value in expected_params.items(): + assert query_params[param] == value