diff --git a/mealie/schema/response/query_filter.py b/mealie/schema/response/query_filter.py index 875d74ed8..f525356f6 100644 --- a/mealie/schema/response/query_filter.py +++ b/mealie/schema/response/query_filter.py @@ -180,6 +180,9 @@ class QueryFilterBuilderComponent: if v is None: continue + if isinstance(model_attr_type, sqltypes.String): + sanitized_values[i] = v.lower() + if self.relationship is RelationalKeyword.LIKE or self.relationship is RelationalKeyword.NOT_LIKE: if not isinstance(model_attr_type, sqltypes.String): raise ValueError( @@ -336,6 +339,9 @@ class QueryFilterBuilder: def _get_filter_element( component: QueryFilterBuilderComponent, model, model_attr, model_attr_type ) -> sa.ColumnElement: + if isinstance(model_attr_type, sqltypes.String): + model_attr = sa.func.lower(model_attr) + # Keywords if component.relationship is RelationalKeyword.IS: element = model_attr.is_(component.validate(model_attr_type)) @@ -351,9 +357,9 @@ class QueryFilterBuilder: for v in component.validate(model_attr_type): element = sa.and_(element, primary_model_attr.any(model_attr == v)) elif component.relationship is RelationalKeyword.LIKE: - element = model_attr.like(component.validate(model_attr_type)) + element = model_attr.ilike(component.validate(model_attr_type)) elif component.relationship is RelationalKeyword.NOT_LIKE: - element = model_attr.not_like(component.validate(model_attr_type)) + element = model_attr.not_ilike(component.validate(model_attr_type)) # Operators elif component.relationship is RelationalOperator.EQ: diff --git a/tests/unit_tests/repository_tests/test_pagination.py b/tests/unit_tests/repository_tests/test_pagination.py index cdc4f725b..606321789 100644 --- a/tests/unit_tests/repository_tests/test_pagination.py +++ b/tests/unit_tests/repository_tests/test_pagination.py @@ -216,6 +216,29 @@ def test_pagination_filter_basic(query_units: tuple[RepositoryUnit, IngredientUn assert unit_results[0].id == unit_2.id +def test_pagination_filter_string_case_insensitive( + query_units: tuple[RepositoryUnit, IngredientUnit, IngredientUnit, IngredientUnit], +): + units_repo, *units = query_units + target_unit = random.choice(units) + + query = PaginationQuery(page=1, per_page=-1, query_filter=f'name="{target_unit.name.upper()}"') + unit_results = units_repo.page_all(query).items + assert len(unit_results) == 1 + assert unit_results[0].id == target_unit.id + + upper_unit = units_repo.create( + SaveIngredientUnit(name="mIxEd-CaSe uNiT", group_id=units_repo.group_id, use_abbreviation=True) + ) + try: + query = PaginationQuery(page=1, per_page=-1, query_filter=f'name="{upper_unit.name.lower()}"') + unit_results = units_repo.page_all(query).items + assert len(unit_results) == 1 + assert unit_results[0].id == upper_unit.id + finally: + units_repo.delete(upper_unit.id) + + def test_pagination_filter_null(unique_user: TestUser): database = unique_user.repos recipe_not_made_1 = database.recipes.create( @@ -425,6 +448,18 @@ def test_pagination_filter_like(query_units: tuple[RepositoryUnit, IngredientUni assert unit_3.id in result_ids +def test_pagination_filter_like_case_insensitive( + query_units: tuple[RepositoryUnit, IngredientUnit, IngredientUnit, IngredientUnit], +): + units_repo, unit_1, *_ = query_units + + query = PaginationQuery(page=1, per_page=-1, query_filter=r'name LIKE "%EST UNIT 1%"') + unit_results = units_repo.page_all(query).items + + assert len(unit_results) == 1 + assert unit_results[0].id == unit_1.id + + def test_pagination_filter_keyword_namespace_conflict(unique_user: TestUser): database = unique_user.repos recipe_rating_1 = database.recipes.create(