diff --git a/examples/fastapi_filter_sqlalchemy.py b/examples/fastapi_filter_sqlalchemy.py index 54d43907..85cb9842 100644 --- a/examples/fastapi_filter_sqlalchemy.py +++ b/examples/fastapi_filter_sqlalchemy.py @@ -101,6 +101,8 @@ class UserFilter(Filter): See: https://github.com/tiangolo/fastapi/issues/4700 for why we need to wrap `Query` in `Field`. """ + age__in: Optional[List[int]] = None + age__between: Optional[List[List[int]]] = None order_by: List[str] = ["age"] search: Optional[str] = None diff --git a/fastapi_filter/contrib/sqlalchemy/filter.py b/fastapi_filter/contrib/sqlalchemy/filter.py index 34879de6..42d0cf14 100644 --- a/fastapi_filter/contrib/sqlalchemy/filter.py +++ b/fastapi_filter/contrib/sqlalchemy/filter.py @@ -39,6 +39,7 @@ def _backward_compatible_value_for_like_and_ilike(value: str): "isnull": lambda value: ("is_", None) if value is True else ("is_not", None), "lt": lambda value: ("__lt__", value), "lte": lambda value: ("__le__", value), + "between": lambda value: ("between", (value[0], value[1])), "like": lambda value: ("like", _backward_compatible_value_for_like_and_ilike(value)), "ilike": lambda value: ("ilike", _backward_compatible_value_for_like_and_ilike(value)), # XXX(arthurio): Mysql excludes None values when using `in` or `not in` filters. @@ -89,19 +90,17 @@ class Direction(str, Enum): @field_validator("*", mode="before") def split_str(cls, value, field: ValidationInfo): - if ( - field.field_name is not None - and ( + if field.field_name is not None: + if ( field.field_name == cls.Constants.ordering_field_name or field.field_name.endswith("__in") or field.field_name.endswith("__not_in") - ) - and isinstance(value, str) - ): - if not value: - # Empty string should return [] not [''] - return [] - return list(value.split(",")) + or field.field_name.endswith("__between") + ) and isinstance(value, str): + if not value: + # Empty string should return [] not [''] + return [] + return list(value.split(",")) return value def filter(self, query: Union[Query, Select]): @@ -124,7 +123,10 @@ def filter(self, query: Union[Query, Select]): query = query.filter(or_(*search_filters)) else: model_field = getattr(self.Constants.model, field_name) - query = query.filter(getattr(model_field, operator)(value)) + if isinstance(value, tuple): + query = query.filter(getattr(model_field, operator)(*value)) + else: + query = query.filter(getattr(model_field, operator)(value)) return query diff --git a/tests/test_sqlalchemy/conftest.py b/tests/test_sqlalchemy/conftest.py index f250b9f7..fc746347 100644 --- a/tests/test_sqlalchemy/conftest.py +++ b/tests/test_sqlalchemy/conftest.py @@ -274,6 +274,7 @@ class UserFilter(Filter): # type: ignore[misc, valid-type] age__gt: Optional[int] = None age__gte: Optional[int] = None age__in: Optional[List[int]] = None + age__between: Optional[List[int]] = None address: Optional[AddressFilter] = FilterDepends( # type: ignore[valid-type] with_prefix("address", AddressFilter), by_alias=True ) diff --git a/tests/test_sqlalchemy/test_filter.py b/tests/test_sqlalchemy/test_filter.py index 3217a60b..0ac77013 100644 --- a/tests/test_sqlalchemy/test_filter.py +++ b/tests/test_sqlalchemy/test_filter.py @@ -27,6 +27,8 @@ [{"address": {"country__not_in": ["France"]}}, 3], [{"age__in": "1"}, 1], [{"age__in": "21,33"}, 3], + [{"age__between": [21, 33]}, 3], + [{"age__between": "21,33"}, 3], [{"address": {"country__not_in": "France"}}, 3], [{"address": {"street__isnull": True}}, 2], [{"address": {"city__in": ["Nantes", "Denver"]}}, 3],