fastapi-sqlalchemy-toolkit 0.8.0__tar.gz → 0.8.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/PKG-INFO +1 -1
- {fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/docs/filtering.md +74 -0
- {fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/docs/ru/filtering.md +76 -1
- {fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/fastapi_sqlalchemy_toolkit/model_manager.py +94 -12
- {fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/pyproject.toml +1 -1
- {fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/tests/test_public_methods.py +270 -9
- {fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/.flake8 +0 -0
- {fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/.github/workflows/docker-test.yml +0 -0
- {fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/.github/workflows/python-publish.yml +0 -0
- {fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/.gitignore +0 -0
- {fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/.pylintrc +0 -0
- {fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/AUTHORS +0 -0
- {fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/LICENSE +0 -0
- {fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/Makefile +0 -0
- {fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/README.md +0 -0
- {fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/docs/benefits.md +0 -0
- {fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/docs/db_validation.md +0 -0
- {fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/docs/extension.md +0 -0
- {fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/docs/index.md +0 -0
- {fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/docs/ru/benefits.md +0 -0
- {fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/docs/ru/db_validation.md +0 -0
- {fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/docs/ru/extension.md +0 -0
- {fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/docs/ru/index.md +0 -0
- {fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/docs/ru/sorting.md +0 -0
- {fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/docs/ru/transactions.md +0 -0
- {fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/docs/ru/usage.md +0 -0
- {fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/docs/ru/utils.md +0 -0
- {fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/docs/sorting.md +0 -0
- {fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/docs/transactions.md +0 -0
- {fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/docs/usage.md +0 -0
- {fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/docs/utils.md +0 -0
- {fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/examples/app/.env +0 -0
- {fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/examples/app/__init__.py +0 -0
- {fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/examples/app/api/__init__.py +0 -0
- {fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/examples/app/api/api.py +0 -0
- {fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/examples/app/api/deps.py +0 -0
- {fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/examples/app/api/endpoints/__init__.py +0 -0
- {fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/examples/app/api/endpoints/child.py +0 -0
- {fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/examples/app/config.py +0 -0
- {fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/examples/app/db.py +0 -0
- {fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/examples/app/main.py +0 -0
- {fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/examples/app/managers.py +0 -0
- {fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/examples/app/models.py +0 -0
- {fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/examples/app/schemas.py +0 -0
- {fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/fastapi_sqlalchemy_toolkit/__init__.py +0 -0
- {fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/fastapi_sqlalchemy_toolkit/filters.py +0 -0
- {fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/fastapi_sqlalchemy_toolkit/ordering.py +0 -0
- {fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/fastapi_sqlalchemy_toolkit/utils.py +0 -0
- {fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/mkdocs.yml +0 -0
- {fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/poetry.lock +0 -0
- {fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/requirements/base.txt +0 -0
- {fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/requirements/docs.txt +0 -0
- {fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/requirements/lint.txt +0 -0
- {fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/requirements/test.txt +0 -0
- {fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/tests/Dockerfile +0 -0
- {fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/tests/__init__.py +0 -0
- {fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/tests/conftest.py +0 -0
- {fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/tests/db.py +0 -0
- {fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/tests/docker-compose.yml +0 -0
- {fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/tests/models.py +0 -0
|
@@ -206,6 +206,80 @@ Unlike the `list` method, the `filter` method:
|
|
|
206
206
|
2. Does not have the `filter_expressions` parameter, i.e., it will not perform `join`,
|
|
207
207
|
necessary for filtering by fields of related models.
|
|
208
208
|
|
|
209
|
+
### Filtering with optional expressions
|
|
210
|
+
|
|
211
|
+
The `optional_where` parameter of the `list` and `paginated_list` methods accepts
|
|
212
|
+
SQLAlchemy expressions that may contain `None` values. Filters with a `None` value
|
|
213
|
+
are automatically skipped.
|
|
214
|
+
|
|
215
|
+
This is useful in list API endpoints where filtering is optional — if the query parameter
|
|
216
|
+
is not provided (i.e., its value is `None`), the filter is not applied.
|
|
217
|
+
|
|
218
|
+
The `optional_where` parameter supports three kinds of expressions:
|
|
219
|
+
|
|
220
|
+
**Case 1: Simple expression** (`MyModel.field == value`)
|
|
221
|
+
|
|
222
|
+
If `value` is `None`, the filter is skipped entirely. Otherwise it is applied as-is.
|
|
223
|
+
|
|
224
|
+
```python
|
|
225
|
+
@router.get("/parents")
|
|
226
|
+
async def get_parents(
|
|
227
|
+
session: Session,
|
|
228
|
+
title: str | None = None,
|
|
229
|
+
) -> list[ParentListSchema]:
|
|
230
|
+
return await parent_manager.list(
|
|
231
|
+
session,
|
|
232
|
+
optional_where=(Parent.title == title),
|
|
233
|
+
)
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
`GET /parents` — no filter applied, all `Parent` objects are returned.
|
|
237
|
+
|
|
238
|
+
`GET /parents?title=foo` — only `Parent` objects with `title = 'foo'` are returned.
|
|
239
|
+
|
|
240
|
+
**Case 2: Function or operator expression** (`func.date(MyModel.field) == value`, `MyModel.field.ilike(value)`)
|
|
241
|
+
|
|
242
|
+
Same behaviour as Case 1 — if `value` is `None`, the filter is skipped.
|
|
243
|
+
|
|
244
|
+
```python
|
|
245
|
+
@router.get("/parents")
|
|
246
|
+
async def get_parents(
|
|
247
|
+
session: Session,
|
|
248
|
+
created_at_date: date | None = None,
|
|
249
|
+
) -> list[ParentListSchema]:
|
|
250
|
+
return await parent_manager.list(
|
|
251
|
+
session,
|
|
252
|
+
optional_where=(func.date(Parent.created_at) == created_at_date),
|
|
253
|
+
)
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
**Case 3: Compound expression with `&` or `|`** (`(expr1) & (expr2)`, `(expr1) | (expr2)`)
|
|
257
|
+
|
|
258
|
+
Sub-expressions whose values are `None` are excluded. The remaining sub-expressions
|
|
259
|
+
are combined using the original operator (`&` or `|`). If all values are `None`,
|
|
260
|
+
the filter is skipped entirely.
|
|
261
|
+
|
|
262
|
+
```python
|
|
263
|
+
@router.get("/parents")
|
|
264
|
+
async def get_parents(
|
|
265
|
+
session: Session,
|
|
266
|
+
title: str | None = None,
|
|
267
|
+
slug: str | None = None,
|
|
268
|
+
) -> list[ParentListSchema]:
|
|
269
|
+
return await parent_manager.list(
|
|
270
|
+
session,
|
|
271
|
+
optional_where=(Parent.title == title) & (Parent.slug == slug),
|
|
272
|
+
)
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
`GET /parents` — no filter applied, all `Parent` objects are returned.
|
|
276
|
+
|
|
277
|
+
`GET /parents?title=foo` — only the `title` filter is applied.
|
|
278
|
+
|
|
279
|
+
`GET /parents?title=foo&slug=bar` — both filters are applied with `AND`.
|
|
280
|
+
|
|
281
|
+
> **Note**: nesting of compound expressions (e.g. `(a & b) | c`) is not supported.
|
|
282
|
+
|
|
209
283
|
### Filtering by `null` via API
|
|
210
284
|
|
|
211
285
|
If in a list API endpoint, you need to be able to filter the field value
|
|
@@ -77,7 +77,9 @@ WHERE child.slug = :slug_1
|
|
|
77
77
|
а не только те, у которых `slug is null`. Поэтому метод `list` (`paginated_list`) отбрасывает фильтрацию
|
|
78
78
|
по этому параметру, если его значение не передано.
|
|
79
79
|
|
|
80
|
-
##
|
|
80
|
+
## Фильтрация с выражениями
|
|
81
|
+
|
|
82
|
+
### filter_expressions
|
|
81
83
|
|
|
82
84
|
Чтобы использовать фильтрацию не только по точному соответствию атрибуту модели,
|
|
83
85
|
в методах `list` и `paginated_list` можно передать параметр `filter_expressions`.
|
|
@@ -182,6 +184,79 @@ WHERE lower(parent.title) LIKE lower(:title_1)
|
|
|
182
184
|
**Важно**: работает только для моделей, напрямую связанных с основной, и только тогда, когда
|
|
183
185
|
эти модели связывает единственный внешний ключ.
|
|
184
186
|
|
|
187
|
+
### optional_where
|
|
188
|
+
|
|
189
|
+
Параметр `optional_where` методов `list` и `paginated_list` принимает выражения SQLAlchemy,
|
|
190
|
+
в которых значения фильтров могут быть `None`. Фильтры со значением `None` автоматически пропускаются.
|
|
191
|
+
|
|
192
|
+
Это удобно использовать в списочных API эндпоинтах, где фильтрация необязательна —
|
|
193
|
+
если параметр запроса не передан (т. е. его значение `None`), фильтр не применяется.
|
|
194
|
+
|
|
195
|
+
Параметр `optional_where` поддерживает три вида выражений:
|
|
196
|
+
|
|
197
|
+
**Кейс 1: Простое выражение** (`MyModel.field == value`)
|
|
198
|
+
|
|
199
|
+
Если `value` равно `None`, фильтр пропускается. Иначе применяется как есть.
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
@router.get("/parents")
|
|
203
|
+
async def get_parents(
|
|
204
|
+
session: Session,
|
|
205
|
+
title: str | None = None,
|
|
206
|
+
) -> list[ParentListSchema]:
|
|
207
|
+
return await parent_manager.list(
|
|
208
|
+
session,
|
|
209
|
+
optional_where=(Parent.title == title),
|
|
210
|
+
)
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Запрос `GET /parents` — фильтр не применяется, возвращаются все объекты `Parent`.
|
|
214
|
+
|
|
215
|
+
Запрос `GET /parents?title=foo` — возвращаются только объекты `Parent` с `title = 'foo'`.
|
|
216
|
+
|
|
217
|
+
**Кейс 2: Выражение с функцией или оператором** (`func.date(MyModel.field) == value`, `MyModel.field.ilike(value)`)
|
|
218
|
+
|
|
219
|
+
Аналогично кейсу 1 — если `value` равно `None`, фильтр пропускается.
|
|
220
|
+
|
|
221
|
+
```python
|
|
222
|
+
@router.get("/parents")
|
|
223
|
+
async def get_parents(
|
|
224
|
+
session: Session,
|
|
225
|
+
created_at_date: date | None = None,
|
|
226
|
+
) -> list[ParentListSchema]:
|
|
227
|
+
return await parent_manager.list(
|
|
228
|
+
session,
|
|
229
|
+
optional_where=(func.date(Parent.created_at) == created_at_date),
|
|
230
|
+
)
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
**Кейс 3: Составное выражение через `&` или `|`** (`(expr1) & (expr2)`, `(expr1) | (expr2)`)
|
|
234
|
+
|
|
235
|
+
Части выражения, значения которых равны `None`, исключаются. Оставшиеся части объединяются
|
|
236
|
+
с использованием исходного оператора (`&` или `|`). Если все значения `None`,
|
|
237
|
+
фильтр пропускается полностью.
|
|
238
|
+
|
|
239
|
+
```python
|
|
240
|
+
@router.get("/parents")
|
|
241
|
+
async def get_parents(
|
|
242
|
+
session: Session,
|
|
243
|
+
title: str | None = None,
|
|
244
|
+
slug: str | None = None,
|
|
245
|
+
) -> list[ParentListSchema]:
|
|
246
|
+
return await parent_manager.list(
|
|
247
|
+
session,
|
|
248
|
+
optional_where=(Parent.title == title) & (Parent.slug == slug),
|
|
249
|
+
)
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Запрос `GET /parents` — фильтр не применяется, возвращаются все объекты `Parent`.
|
|
253
|
+
|
|
254
|
+
Запрос `GET /parents?title=foo` — применяется только фильтр по `title`.
|
|
255
|
+
|
|
256
|
+
Запрос `GET /parents?title=foo&slug=bar` — применяются оба фильтра через `AND`.
|
|
257
|
+
|
|
258
|
+
> **Примечание**: вложенные составные выражения (например, `(a & b) | c`) не поддерживаются.
|
|
259
|
+
|
|
185
260
|
## Фильтрация без дополнительной обработки
|
|
186
261
|
|
|
187
262
|
Для фильтрации без дополнительной обработки в методах `list` и `paginated_list` можно
|
|
@@ -16,6 +16,7 @@ from sqlalchemy import (
|
|
|
16
16
|
delete,
|
|
17
17
|
func,
|
|
18
18
|
insert,
|
|
19
|
+
or_,
|
|
19
20
|
select,
|
|
20
21
|
update,
|
|
21
22
|
)
|
|
@@ -25,7 +26,8 @@ from sqlalchemy.orm import DeclarativeBase, contains_eager, load_only
|
|
|
25
26
|
from sqlalchemy.orm.attributes import InstrumentedAttribute
|
|
26
27
|
from sqlalchemy.orm.relationships import Relationship
|
|
27
28
|
from sqlalchemy.sql import Select
|
|
28
|
-
from sqlalchemy.sql.elements import UnaryExpression
|
|
29
|
+
from sqlalchemy.sql.elements import Null, UnaryExpression
|
|
30
|
+
from sqlalchemy.sql.expression import BinaryExpression, ColumnElement
|
|
29
31
|
from sqlalchemy.sql.functions import Function
|
|
30
32
|
from sqlalchemy.sql.schema import ScalarElementColumnDefault
|
|
31
33
|
from sqlalchemy.sql.selectable import Exists
|
|
@@ -524,12 +526,14 @@ class ModelManager(Generic[ModelT, CreateSchemaT, UpdateSchemaT]):
|
|
|
524
526
|
self,
|
|
525
527
|
session: AsyncSession,
|
|
526
528
|
order_by: InstrumentedAttribute | UnaryExpression | None = ...,
|
|
527
|
-
filter_expressions: dict[InstrumentedAttribute | Callable, Any]
|
|
529
|
+
filter_expressions: dict[InstrumentedAttribute | Callable | ColumnElement, Any]
|
|
530
|
+
| None = ...,
|
|
528
531
|
nullable_filter_expressions: (
|
|
529
532
|
dict[InstrumentedAttribute | Callable, Any] | None
|
|
530
533
|
) = ...,
|
|
531
534
|
options: List[Any] | Any | None = ...,
|
|
532
535
|
where: Any | None = ...,
|
|
536
|
+
optional_where: Any | None = ...,
|
|
533
537
|
base_stmt: None = ...,
|
|
534
538
|
transformer: Callable | None = ...,
|
|
535
539
|
**simple_filters: Any,
|
|
@@ -540,12 +544,14 @@ class ModelManager(Generic[ModelT, CreateSchemaT, UpdateSchemaT]):
|
|
|
540
544
|
self,
|
|
541
545
|
session: AsyncSession,
|
|
542
546
|
order_by: InstrumentedAttribute | UnaryExpression | None = ...,
|
|
543
|
-
filter_expressions: dict[InstrumentedAttribute | Callable, Any]
|
|
547
|
+
filter_expressions: dict[InstrumentedAttribute | Callable | ColumnElement, Any]
|
|
548
|
+
| None = ...,
|
|
544
549
|
nullable_filter_expressions: (
|
|
545
550
|
dict[InstrumentedAttribute | Callable, Any] | None
|
|
546
551
|
) = ...,
|
|
547
552
|
options: List[Any] | Any | None = ...,
|
|
548
553
|
where: Any | None = ...,
|
|
554
|
+
optional_where: Any | None = ...,
|
|
549
555
|
base_stmt: Select = ...,
|
|
550
556
|
transformer: Callable | None = ...,
|
|
551
557
|
**simple_filters: Any,
|
|
@@ -555,12 +561,14 @@ class ModelManager(Generic[ModelT, CreateSchemaT, UpdateSchemaT]):
|
|
|
555
561
|
self,
|
|
556
562
|
session: AsyncSession,
|
|
557
563
|
order_by: InstrumentedAttribute | UnaryExpression | None = None,
|
|
558
|
-
filter_expressions: dict[InstrumentedAttribute | Callable, Any]
|
|
564
|
+
filter_expressions: dict[InstrumentedAttribute | Callable | ColumnElement, Any]
|
|
565
|
+
| None = None,
|
|
559
566
|
nullable_filter_expressions: (
|
|
560
567
|
dict[InstrumentedAttribute | Callable, Any] | None
|
|
561
568
|
) = None,
|
|
562
569
|
options: List[Any] | Any | None = None,
|
|
563
570
|
where: Any | None = None,
|
|
571
|
+
optional_where: Any | None = None,
|
|
564
572
|
base_stmt: Select | None = None,
|
|
565
573
|
transformer: Callable | None = None,
|
|
566
574
|
**simple_filters: Any,
|
|
@@ -586,6 +594,12 @@ class ModelManager(Generic[ModelT, CreateSchemaT, UpdateSchemaT]):
|
|
|
586
594
|
|
|
587
595
|
:param where: выражение, которое будет передано в метод .where() SQLAlchemy
|
|
588
596
|
|
|
597
|
+
:param optional_where: выражение SQLAlchemy, в котором фильтры со значением None
|
|
598
|
+
пропускаются. Поддерживает простые выражения (MyModel.field == value),
|
|
599
|
+
выражения с функциями (func.date(MyModel.field) == value) и составные
|
|
600
|
+
выражения через & или | ((expr1) & (expr2)). См. раздел "фильтрация"
|
|
601
|
+
в документации.
|
|
602
|
+
|
|
589
603
|
:param base_stmt: объект Select для SQL запроса. Если передан, то метод вернёт
|
|
590
604
|
страницу Row, а не ModelT.
|
|
591
605
|
Примечание: фильтрация и сортировка по связанным моделям скорее всего
|
|
@@ -618,11 +632,18 @@ class ModelManager(Generic[ModelT, CreateSchemaT, UpdateSchemaT]):
|
|
|
618
632
|
)
|
|
619
633
|
|
|
620
634
|
for filter_expression, value in filter_expressions.items():
|
|
621
|
-
if isinstance(
|
|
635
|
+
if isinstance(
|
|
636
|
+
filter_expression,
|
|
637
|
+
InstrumentedAttribute | Function | BinaryExpression | ColumnElement,
|
|
638
|
+
):
|
|
622
639
|
stmt = stmt.filter(filter_expression == value)
|
|
623
640
|
else:
|
|
624
641
|
stmt = stmt.filter(filter_expression(value))
|
|
625
642
|
|
|
643
|
+
processed_optional_where = self.handle_optional_where(optional_where)
|
|
644
|
+
if processed_optional_where is not None:
|
|
645
|
+
stmt = stmt.where(processed_optional_where)
|
|
646
|
+
|
|
626
647
|
return await paginate(session, stmt, transformer=transformer)
|
|
627
648
|
|
|
628
649
|
@overload
|
|
@@ -716,12 +737,14 @@ class ModelManager(Generic[ModelT, CreateSchemaT, UpdateSchemaT]):
|
|
|
716
737
|
self,
|
|
717
738
|
session: AsyncSession,
|
|
718
739
|
order_by: InstrumentedAttribute | UnaryExpression | None = ...,
|
|
719
|
-
filter_expressions: dict[InstrumentedAttribute | Callable, Any]
|
|
740
|
+
filter_expressions: dict[InstrumentedAttribute | Callable | ColumnElement, Any]
|
|
741
|
+
| None = ...,
|
|
720
742
|
nullable_filter_expressions: (
|
|
721
743
|
dict[InstrumentedAttribute | Callable, Any] | None
|
|
722
744
|
) = ...,
|
|
723
745
|
options: List[Any] | Any | None = ...,
|
|
724
746
|
where: Any | None = ...,
|
|
747
|
+
optional_where: Any | None = ...,
|
|
725
748
|
base_stmt: None = ...,
|
|
726
749
|
limit: int | None = ...,
|
|
727
750
|
offset: int | None = ...,
|
|
@@ -735,12 +758,14 @@ class ModelManager(Generic[ModelT, CreateSchemaT, UpdateSchemaT]):
|
|
|
735
758
|
self,
|
|
736
759
|
session: AsyncSession,
|
|
737
760
|
order_by: InstrumentedAttribute | UnaryExpression | None = ...,
|
|
738
|
-
filter_expressions: dict[InstrumentedAttribute | Callable, Any]
|
|
761
|
+
filter_expressions: dict[InstrumentedAttribute | Callable | ColumnElement, Any]
|
|
762
|
+
| None = ...,
|
|
739
763
|
nullable_filter_expressions: (
|
|
740
764
|
dict[InstrumentedAttribute | Callable, Any] | None
|
|
741
765
|
) = ...,
|
|
742
766
|
options: List[Any] | Any | None = ...,
|
|
743
767
|
where: Any | None = ...,
|
|
768
|
+
optional_where: Any | None = ...,
|
|
744
769
|
base_stmt: Select = ...,
|
|
745
770
|
limit: int | None = ...,
|
|
746
771
|
offset: int | None = ...,
|
|
@@ -753,12 +778,14 @@ class ModelManager(Generic[ModelT, CreateSchemaT, UpdateSchemaT]):
|
|
|
753
778
|
self,
|
|
754
779
|
session: AsyncSession,
|
|
755
780
|
order_by: InstrumentedAttribute | UnaryExpression | None = None,
|
|
756
|
-
filter_expressions: dict[InstrumentedAttribute | Callable, Any]
|
|
781
|
+
filter_expressions: dict[InstrumentedAttribute | Callable | ColumnElement, Any]
|
|
782
|
+
| None = None,
|
|
757
783
|
nullable_filter_expressions: (
|
|
758
784
|
dict[InstrumentedAttribute | Callable, Any] | None
|
|
759
785
|
) = None,
|
|
760
786
|
options: List[Any] | Any | None = None,
|
|
761
787
|
where: Any | None = None,
|
|
788
|
+
optional_where: Any | None = None,
|
|
762
789
|
base_stmt: Select | None = None,
|
|
763
790
|
limit: int | None = None,
|
|
764
791
|
offset: int | None = None,
|
|
@@ -787,6 +814,12 @@ class ModelManager(Generic[ModelT, CreateSchemaT, UpdateSchemaT]):
|
|
|
787
814
|
|
|
788
815
|
:param where: выражение, которое будет передано в метод .where() SQLAlchemy
|
|
789
816
|
|
|
817
|
+
:param optional_where: выражение SQLAlchemy, в котором фильтры со значением None
|
|
818
|
+
пропускаются. Поддерживает простые выражения (MyModel.field == value),
|
|
819
|
+
выражения с функциями (func.date(MyModel.field) == value) и составные
|
|
820
|
+
выражения через & или | ((expr1) & (expr2)). См. раздел "фильтрация"
|
|
821
|
+
в документации.
|
|
822
|
+
|
|
790
823
|
:param unique: определяет необходимость вызова метода .unique()
|
|
791
824
|
у результата SQLAlchemy
|
|
792
825
|
|
|
@@ -830,11 +863,18 @@ class ModelManager(Generic[ModelT, CreateSchemaT, UpdateSchemaT]):
|
|
|
830
863
|
)
|
|
831
864
|
|
|
832
865
|
for filter_expression, value in filter_expressions.items():
|
|
833
|
-
if isinstance(
|
|
866
|
+
if isinstance(
|
|
867
|
+
filter_expression,
|
|
868
|
+
InstrumentedAttribute | BinaryExpression | ColumnElement,
|
|
869
|
+
):
|
|
834
870
|
stmt = stmt.filter(filter_expression == value)
|
|
835
871
|
else:
|
|
836
872
|
stmt = stmt.filter(filter_expression(value))
|
|
837
873
|
|
|
874
|
+
processed_optional_where = self.handle_optional_where(optional_where)
|
|
875
|
+
if processed_optional_where is not None:
|
|
876
|
+
stmt = stmt.where(processed_optional_where)
|
|
877
|
+
|
|
838
878
|
result = await session.execute(stmt)
|
|
839
879
|
|
|
840
880
|
if base_stmt is None:
|
|
@@ -1075,7 +1115,7 @@ class ModelManager(Generic[ModelT, CreateSchemaT, UpdateSchemaT]):
|
|
|
1075
1115
|
def get_joins(
|
|
1076
1116
|
self,
|
|
1077
1117
|
base_query: Select,
|
|
1078
|
-
filter_expressions: dict[InstrumentedAttribute | Callable, Any],
|
|
1118
|
+
filter_expressions: dict[InstrumentedAttribute | Callable | ColumnElement, Any],
|
|
1079
1119
|
options: List[Any] | None = None,
|
|
1080
1120
|
order_by: InstrumentedAttribute | UnaryExpression | None = None,
|
|
1081
1121
|
) -> Select:
|
|
@@ -1108,6 +1148,8 @@ class ModelManager(Generic[ModelT, CreateSchemaT, UpdateSchemaT]):
|
|
|
1108
1148
|
model = filter_expression.parent._identity_class
|
|
1109
1149
|
elif isinstance(filter_expression, Function):
|
|
1110
1150
|
model = filter_expression.entity_namespace
|
|
1151
|
+
elif isinstance(filter_expression, ColumnElement):
|
|
1152
|
+
model = self.model # Not supported
|
|
1111
1153
|
else:
|
|
1112
1154
|
model = filter_expression.__self__.parent._identity_class
|
|
1113
1155
|
if model != self.model:
|
|
@@ -1157,7 +1199,7 @@ class ModelManager(Generic[ModelT, CreateSchemaT, UpdateSchemaT]):
|
|
|
1157
1199
|
|
|
1158
1200
|
@staticmethod
|
|
1159
1201
|
def handle_filter_expressions(
|
|
1160
|
-
filter_expressions: dict[InstrumentedAttribute | Callable, Any],
|
|
1202
|
+
filter_expressions: dict[InstrumentedAttribute | Callable | ColumnElement, Any],
|
|
1161
1203
|
) -> None:
|
|
1162
1204
|
for filter_expression, value in filter_expressions.copy().items():
|
|
1163
1205
|
if value is None:
|
|
@@ -1169,7 +1211,9 @@ class ModelManager(Generic[ModelT, CreateSchemaT, UpdateSchemaT]):
|
|
|
1169
1211
|
|
|
1170
1212
|
@staticmethod
|
|
1171
1213
|
def handle_nullable_filter_expressions(
|
|
1172
|
-
nullable_filter_expressions: dict[
|
|
1214
|
+
nullable_filter_expressions: dict[
|
|
1215
|
+
InstrumentedAttribute | Callable | ColumnElement, Any
|
|
1216
|
+
],
|
|
1173
1217
|
) -> None:
|
|
1174
1218
|
for filter_expression, value in nullable_filter_expressions.copy().items():
|
|
1175
1219
|
if value in null_query_values:
|
|
@@ -1191,6 +1235,44 @@ class ModelManager(Generic[ModelT, CreateSchemaT, UpdateSchemaT]):
|
|
|
1191
1235
|
related_pk = _get_model_pk(related_model)
|
|
1192
1236
|
return relationship.any(related_pk.in_(value))
|
|
1193
1237
|
|
|
1238
|
+
@staticmethod
|
|
1239
|
+
def handle_optional_where(optional_where: Any) -> Any:
|
|
1240
|
+
"""
|
|
1241
|
+
Обрабатывает выражение optional_where, пропуская фильтры, значения которых None.
|
|
1242
|
+
|
|
1243
|
+
Поддерживает:
|
|
1244
|
+
1. Простые выражения вида MyModel.field == value
|
|
1245
|
+
2. Выражения с функциями/операторами вида func.date(MyModel.field) == value
|
|
1246
|
+
3. Составные выражения вида (expr1) & (expr2) или (expr1) | (expr2)
|
|
1247
|
+
(без вложенности)
|
|
1248
|
+
|
|
1249
|
+
Если value равно None, фильтр не применяется.
|
|
1250
|
+
В составных выражениях исключаются части с value == None,
|
|
1251
|
+
при этом оператор & или | сохраняется.
|
|
1252
|
+
"""
|
|
1253
|
+
if optional_where is None:
|
|
1254
|
+
return None
|
|
1255
|
+
|
|
1256
|
+
# compound expression (& or |) — has .clauses attribute
|
|
1257
|
+
if hasattr(optional_where, "clauses"):
|
|
1258
|
+
remaining = [
|
|
1259
|
+
clause
|
|
1260
|
+
for clause in optional_where.clauses
|
|
1261
|
+
if not isinstance(clause.right, Null)
|
|
1262
|
+
]
|
|
1263
|
+
if not remaining:
|
|
1264
|
+
return None
|
|
1265
|
+
if len(remaining) == 1:
|
|
1266
|
+
return remaining[0]
|
|
1267
|
+
if optional_where.operator.__name__ == "and_":
|
|
1268
|
+
return and_(*remaining)
|
|
1269
|
+
return or_(*remaining)
|
|
1270
|
+
|
|
1271
|
+
# simple binary expression
|
|
1272
|
+
if isinstance(getattr(optional_where, "right", None), Null):
|
|
1273
|
+
return None
|
|
1274
|
+
return optional_where
|
|
1275
|
+
|
|
1194
1276
|
def assemble_stmt(
|
|
1195
1277
|
self,
|
|
1196
1278
|
base_stmt: Select | None = None,
|
{fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/tests/test_public_methods.py
RENAMED
|
@@ -31,9 +31,9 @@ async def test_get(session: AsyncSession):
|
|
|
31
31
|
category_to_check = await session.execute(
|
|
32
32
|
select(Category).where(Category.title == "test-get-category-title")
|
|
33
33
|
)
|
|
34
|
-
assert
|
|
35
|
-
|
|
36
|
-
)
|
|
34
|
+
assert (
|
|
35
|
+
category == category_to_check.scalars().first()
|
|
36
|
+
), "Got not equal to object in database"
|
|
37
37
|
|
|
38
38
|
nonexistent = await session.execute(
|
|
39
39
|
select(Category).where(Category.title == "nonexistent-test-get-category-title")
|
|
@@ -279,9 +279,9 @@ async def test_create(session: AsyncSession):
|
|
|
279
279
|
category_to_check = await session.execute(
|
|
280
280
|
select(Category).where(Category.title == "test-create-category-title")
|
|
281
281
|
)
|
|
282
|
-
assert
|
|
283
|
-
|
|
284
|
-
)
|
|
282
|
+
assert (
|
|
283
|
+
created == category_to_check.scalars().first()
|
|
284
|
+
), "Created not equal to object in database"
|
|
285
285
|
|
|
286
286
|
|
|
287
287
|
async def test_create_unique_filed_validation(session: AsyncSession):
|
|
@@ -339,9 +339,9 @@ async def test_update(session: AsyncSession):
|
|
|
339
339
|
category_to_check = await session.execute(
|
|
340
340
|
select(Category).where(Category.title == "UPDATED-test-update-category-title")
|
|
341
341
|
)
|
|
342
|
-
assert
|
|
343
|
-
|
|
344
|
-
)
|
|
342
|
+
assert (
|
|
343
|
+
updated == category_to_check.scalars().first()
|
|
344
|
+
), "Updated not equal to object in database"
|
|
345
345
|
|
|
346
346
|
|
|
347
347
|
async def test_update_unique_filed_validation(session: AsyncSession):
|
|
@@ -729,6 +729,267 @@ async def test_list_with_where(session: AsyncSession):
|
|
|
729
729
|
assert parent.title == same_title
|
|
730
730
|
|
|
731
731
|
|
|
732
|
+
# ########################################################################
|
|
733
|
+
# Tests for optional_where parameter
|
|
734
|
+
# ########################################################################
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
async def test_list_with_optional_where_case1_applied(session: AsyncSession):
|
|
738
|
+
"""Case 1: simple expression, value is not None — filter is applied."""
|
|
739
|
+
target_title = "optional-where-title-target"
|
|
740
|
+
await session.execute(
|
|
741
|
+
insert(Parent),
|
|
742
|
+
[
|
|
743
|
+
{
|
|
744
|
+
"title": target_title,
|
|
745
|
+
"slug": "optional-where-slug1",
|
|
746
|
+
},
|
|
747
|
+
{
|
|
748
|
+
"title": "optional-where-title-other",
|
|
749
|
+
"slug": "optional-where-slug2",
|
|
750
|
+
},
|
|
751
|
+
],
|
|
752
|
+
)
|
|
753
|
+
await session.commit()
|
|
754
|
+
|
|
755
|
+
parents = await parent_manager.list(
|
|
756
|
+
session=session,
|
|
757
|
+
optional_where=(Parent.title == target_title),
|
|
758
|
+
)
|
|
759
|
+
assert len(parents) == 1
|
|
760
|
+
assert parents[0].title == target_title
|
|
761
|
+
|
|
762
|
+
|
|
763
|
+
async def test_list_with_optional_where_case1_skipped(session: AsyncSession):
|
|
764
|
+
"""Case 1: simple expression, value is None — filter is skipped (all returned)."""
|
|
765
|
+
await session.execute(
|
|
766
|
+
insert(Parent),
|
|
767
|
+
[
|
|
768
|
+
{
|
|
769
|
+
"title": "optional-where-title-1",
|
|
770
|
+
"slug": "optional-where-slug1",
|
|
771
|
+
},
|
|
772
|
+
{
|
|
773
|
+
"title": "optional-where-title-2",
|
|
774
|
+
"slug": "optional-where-slug2",
|
|
775
|
+
},
|
|
776
|
+
],
|
|
777
|
+
)
|
|
778
|
+
await session.commit()
|
|
779
|
+
|
|
780
|
+
none_value = None
|
|
781
|
+
parents = await parent_manager.list(
|
|
782
|
+
session=session,
|
|
783
|
+
optional_where=(Parent.title == none_value),
|
|
784
|
+
)
|
|
785
|
+
assert len(parents) == 2
|
|
786
|
+
|
|
787
|
+
|
|
788
|
+
async def test_list_with_optional_where_case2_applied(session: AsyncSession):
|
|
789
|
+
"""Case 2: func expression, value is not None — filter is applied."""
|
|
790
|
+
from sqlalchemy import func
|
|
791
|
+
|
|
792
|
+
target_slug = "optional-where-func-slug1"
|
|
793
|
+
await session.execute(
|
|
794
|
+
insert(Parent),
|
|
795
|
+
[
|
|
796
|
+
{
|
|
797
|
+
"title": "optional-where-func-title-1",
|
|
798
|
+
"slug": target_slug,
|
|
799
|
+
},
|
|
800
|
+
{
|
|
801
|
+
"title": "optional-where-func-title-2",
|
|
802
|
+
"slug": "optional-where-func-slug2",
|
|
803
|
+
},
|
|
804
|
+
],
|
|
805
|
+
)
|
|
806
|
+
await session.commit()
|
|
807
|
+
|
|
808
|
+
# Use func.lower() on the slug to simulate a function expression
|
|
809
|
+
parents = await parent_manager.list(
|
|
810
|
+
session=session,
|
|
811
|
+
optional_where=(func.lower(Parent.slug) == target_slug),
|
|
812
|
+
)
|
|
813
|
+
assert len(parents) == 1
|
|
814
|
+
assert parents[0].slug == target_slug
|
|
815
|
+
|
|
816
|
+
|
|
817
|
+
async def test_list_with_optional_where_case2_skipped(session: AsyncSession):
|
|
818
|
+
"""Case 2: func expression, value is None — filter is skipped (all returned)."""
|
|
819
|
+
from sqlalchemy import func
|
|
820
|
+
|
|
821
|
+
await session.execute(
|
|
822
|
+
insert(Parent),
|
|
823
|
+
[
|
|
824
|
+
{
|
|
825
|
+
"title": "optional-where-func-title-1",
|
|
826
|
+
"slug": "optional-where-func-slug1",
|
|
827
|
+
},
|
|
828
|
+
{
|
|
829
|
+
"title": "optional-where-func-title-2",
|
|
830
|
+
"slug": "optional-where-func-slug2",
|
|
831
|
+
},
|
|
832
|
+
],
|
|
833
|
+
)
|
|
834
|
+
await session.commit()
|
|
835
|
+
|
|
836
|
+
none_value = None
|
|
837
|
+
parents = await parent_manager.list(
|
|
838
|
+
session=session,
|
|
839
|
+
optional_where=(func.lower(Parent.slug) == none_value),
|
|
840
|
+
)
|
|
841
|
+
assert len(parents) == 2
|
|
842
|
+
|
|
843
|
+
|
|
844
|
+
async def test_list_with_optional_where_case3_and_both_applied(session: AsyncSession):
|
|
845
|
+
"""Case 3: compound & expression, both values not None — both filters applied."""
|
|
846
|
+
target_title = "optional-where-and-title"
|
|
847
|
+
target_slug = "optional-where-and-slug1"
|
|
848
|
+
await session.execute(
|
|
849
|
+
insert(Parent),
|
|
850
|
+
[
|
|
851
|
+
{
|
|
852
|
+
"title": target_title,
|
|
853
|
+
"slug": target_slug,
|
|
854
|
+
},
|
|
855
|
+
{
|
|
856
|
+
"title": target_title,
|
|
857
|
+
"slug": "optional-where-and-slug2",
|
|
858
|
+
},
|
|
859
|
+
{
|
|
860
|
+
"title": "optional-where-and-title-other",
|
|
861
|
+
"slug": "optional-where-and-slug3",
|
|
862
|
+
},
|
|
863
|
+
],
|
|
864
|
+
)
|
|
865
|
+
await session.commit()
|
|
866
|
+
|
|
867
|
+
parents = await parent_manager.list(
|
|
868
|
+
session=session,
|
|
869
|
+
optional_where=(Parent.title == target_title) & (Parent.slug == target_slug),
|
|
870
|
+
)
|
|
871
|
+
assert len(parents) == 1
|
|
872
|
+
assert parents[0].title == target_title
|
|
873
|
+
assert parents[0].slug == target_slug
|
|
874
|
+
|
|
875
|
+
|
|
876
|
+
async def test_list_with_optional_where_case3_and_one_none(session: AsyncSession):
|
|
877
|
+
"""Case 3: compound & expression, one value is None — only non-None filter applied."""
|
|
878
|
+
target_title = "optional-where-one-none-title"
|
|
879
|
+
await session.execute(
|
|
880
|
+
insert(Parent),
|
|
881
|
+
[
|
|
882
|
+
{
|
|
883
|
+
"title": target_title,
|
|
884
|
+
"slug": "optional-where-one-none-slug1",
|
|
885
|
+
},
|
|
886
|
+
{
|
|
887
|
+
"title": target_title,
|
|
888
|
+
"slug": "optional-where-one-none-slug2",
|
|
889
|
+
},
|
|
890
|
+
{
|
|
891
|
+
"title": "optional-where-one-none-title-other",
|
|
892
|
+
"slug": "optional-where-one-none-slug3",
|
|
893
|
+
},
|
|
894
|
+
],
|
|
895
|
+
)
|
|
896
|
+
await session.commit()
|
|
897
|
+
|
|
898
|
+
none_value = None
|
|
899
|
+
parents = await parent_manager.list(
|
|
900
|
+
session=session,
|
|
901
|
+
optional_where=(Parent.title == target_title) & (Parent.slug == none_value),
|
|
902
|
+
)
|
|
903
|
+
assert len(parents) == 2
|
|
904
|
+
for parent in parents:
|
|
905
|
+
assert parent.title == target_title
|
|
906
|
+
|
|
907
|
+
|
|
908
|
+
async def test_list_with_optional_where_case3_all_none(session: AsyncSession):
|
|
909
|
+
"""Case 3: compound expression, all values are None — filter skipped (all returned)."""
|
|
910
|
+
await session.execute(
|
|
911
|
+
insert(Parent),
|
|
912
|
+
[
|
|
913
|
+
{
|
|
914
|
+
"title": "optional-where-all-none-title-1",
|
|
915
|
+
"slug": "optional-where-all-none-slug1",
|
|
916
|
+
},
|
|
917
|
+
{
|
|
918
|
+
"title": "optional-where-all-none-title-2",
|
|
919
|
+
"slug": "optional-where-all-none-slug2",
|
|
920
|
+
},
|
|
921
|
+
],
|
|
922
|
+
)
|
|
923
|
+
await session.commit()
|
|
924
|
+
|
|
925
|
+
none_value = None
|
|
926
|
+
parents = await parent_manager.list(
|
|
927
|
+
session=session,
|
|
928
|
+
optional_where=(Parent.title == none_value) & (Parent.slug == none_value),
|
|
929
|
+
)
|
|
930
|
+
assert len(parents) == 2
|
|
931
|
+
|
|
932
|
+
|
|
933
|
+
async def test_list_with_optional_where_case3_or_both_applied(session: AsyncSession):
|
|
934
|
+
"""Case 3: compound | expression, both values not None — both filters applied as OR."""
|
|
935
|
+
title_a = "optional-where-or-title-a"
|
|
936
|
+
title_b = "optional-where-or-title-b"
|
|
937
|
+
await session.execute(
|
|
938
|
+
insert(Parent),
|
|
939
|
+
[
|
|
940
|
+
{
|
|
941
|
+
"title": title_a,
|
|
942
|
+
"slug": "optional-where-or-slug1",
|
|
943
|
+
},
|
|
944
|
+
{
|
|
945
|
+
"title": title_b,
|
|
946
|
+
"slug": "optional-where-or-slug2",
|
|
947
|
+
},
|
|
948
|
+
{
|
|
949
|
+
"title": "optional-where-or-title-other",
|
|
950
|
+
"slug": "optional-where-or-slug3",
|
|
951
|
+
},
|
|
952
|
+
],
|
|
953
|
+
)
|
|
954
|
+
await session.commit()
|
|
955
|
+
|
|
956
|
+
parents = await parent_manager.list(
|
|
957
|
+
session=session,
|
|
958
|
+
optional_where=(Parent.title == title_a) | (Parent.title == title_b),
|
|
959
|
+
)
|
|
960
|
+
assert len(parents) == 2
|
|
961
|
+
titles = {p.title for p in parents}
|
|
962
|
+
assert title_a in titles
|
|
963
|
+
assert title_b in titles
|
|
964
|
+
|
|
965
|
+
|
|
966
|
+
async def test_list_with_optional_where_case3_or_one_none(session: AsyncSession):
|
|
967
|
+
"""Case 3: compound | expression, one value is None — only non-None filter applied."""
|
|
968
|
+
target_title = "optional-where-or-none-title"
|
|
969
|
+
await session.execute(
|
|
970
|
+
insert(Parent),
|
|
971
|
+
[
|
|
972
|
+
{
|
|
973
|
+
"title": target_title,
|
|
974
|
+
"slug": "optional-where-or-none-slug1",
|
|
975
|
+
},
|
|
976
|
+
{
|
|
977
|
+
"title": "optional-where-or-none-title-other",
|
|
978
|
+
"slug": "optional-where-or-none-slug2",
|
|
979
|
+
},
|
|
980
|
+
],
|
|
981
|
+
)
|
|
982
|
+
await session.commit()
|
|
983
|
+
|
|
984
|
+
none_value = None
|
|
985
|
+
parents = await parent_manager.list(
|
|
986
|
+
session=session,
|
|
987
|
+
optional_where=(Parent.title == target_title) | (Parent.slug == none_value),
|
|
988
|
+
)
|
|
989
|
+
assert len(parents) == 1
|
|
990
|
+
assert parents[0].title == target_title
|
|
991
|
+
|
|
992
|
+
|
|
732
993
|
async def test_create_unique_constraint_validation(session: AsyncSession):
|
|
733
994
|
await parent_manager.create(
|
|
734
995
|
session=session,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/docs/ru/db_validation.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/docs/ru/transactions.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/examples/app/__init__.py
RENAMED
|
File without changes
|
{fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/examples/app/api/__init__.py
RENAMED
|
File without changes
|
{fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/examples/app/api/api.py
RENAMED
|
File without changes
|
{fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/examples/app/api/deps.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/examples/app/config.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/examples/app/managers.py
RENAMED
|
File without changes
|
{fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/examples/app/models.py
RENAMED
|
File without changes
|
{fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/examples/app/schemas.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_sqlalchemy_toolkit-0.8.0 → fastapi_sqlalchemy_toolkit-0.8.2}/tests/docker-compose.yml
RENAMED
|
File without changes
|
|
File without changes
|