Merge pull request #589 from erichaydel/588-fix-queryset-count-bug

Fix collerek/ormar#588 Bug in queryset count() method
This commit is contained in:
collerek
2022-03-28 11:56:43 +02:00
committed by GitHub
11 changed files with 114 additions and 71 deletions

View File

@ -601,7 +601,7 @@ metadata.drop_all(engine)
* `prefetch_related(related: Union[List, str]) -> QuerySet` * `prefetch_related(related: Union[List, str]) -> QuerySet`
* `limit(limit_count: int) -> QuerySet` * `limit(limit_count: int) -> QuerySet`
* `offset(offset: int) -> QuerySet` * `offset(offset: int) -> QuerySet`
* `count() -> int` * `count(distinct: bool = True) -> int`
* `exists() -> bool` * `exists() -> bool`
* `max(columns: List[str]) -> Any` * `max(columns: List[str]) -> Any`
* `min(columns: List[str]) -> Any` * `min(columns: List[str]) -> Any`

View File

@ -499,11 +499,19 @@ Returns a bool value to confirm if there are rows matching the given criteria
#### count #### count
```python ```python
| async count() -> int | async count(distinct: bool = True) -> int
``` ```
Returns number of rows matching the given criteria Returns number of rows matching the given criteria
(applied with `filter` and `exclude` if set before). (applied with `filter` and `exclude` if set before).
If `distinct` is `True` (the default), this will return the number of primary rows selected. If `False`,
the count will be the total number of rows returned
(including extra rows for `one-to-many` or `many-to-many` left `select_related` table joins).
`False` is the legacy (buggy) behavior for workflows that depend on it.
**Arguments**:
- `distinct` (`bool`): flag if the primary table rows should be distinct or not
**Returns**: **Returns**:
@ -865,4 +873,3 @@ Bulk operations do not send signals.
- `objects` (`List[Model]`): list of ormar models - `objects` (`List[Model]`): list of ormar models
- `columns` (`List[str]`): list of columns to update - `columns` (`List[str]`): list of columns to update

View File

@ -150,14 +150,22 @@ Actual call delegated to QuerySet.
#### count #### count
```python ```python
| async count() -> int | async count(distinct: bool = True) -> int
``` ```
Returns number of rows matching the given criteria Returns number of rows matching the given criteria
(applied with `filter` and `exclude` if set before). (applied with `filter` and `exclude` if set before).
If `distinct` is `True` (the default), this will return the number of primary rows selected. If `False`,
the count will be the total number of rows returned
(including extra rows for `one-to-many` or `many-to-many` left `select_related` table joins).
`False` is the legacy (buggy) behavior for workflows that depend on it.
Actual call delegated to QuerySet. Actual call delegated to QuerySet.
**Arguments**:
- `distinct` (`bool`): flag if the primary table rows should be distinct or not
**Returns**: **Returns**:
`int`: number of rows `int`: number of rows
@ -773,4 +781,3 @@ Actual call delegated to QuerySet.
**Returns**: **Returns**:
`QuerysetProxy`: QuerysetProxy `QuerysetProxy`: QuerysetProxy

View File

@ -610,7 +610,7 @@ metadata.drop_all(engine)
* `prefetch_related(related: Union[List, str]) -> QuerySet` * `prefetch_related(related: Union[List, str]) -> QuerySet`
* `limit(limit_count: int) -> QuerySet` * `limit(limit_count: int) -> QuerySet`
* `offset(offset: int) -> QuerySet` * `offset(offset: int) -> QuerySet`
* `count() -> int` * `count(distinct: bool = True) -> int`
* `exists() -> bool` * `exists() -> bool`
* `max(columns: List[str]) -> Any` * `max(columns: List[str]) -> Any`
* `min(columns: List[str]) -> Any` * `min(columns: List[str]) -> Any`

View File

@ -3,7 +3,7 @@
Currently 6 aggregation functions are supported. Currently 6 aggregation functions are supported.
* `count() -> int` * `count(distinct: bool = True) -> int`
* `exists() -> bool` * `exists() -> bool`
* `sum(columns) -> Any` * `sum(columns) -> Any`
* `avg(columns) -> Any` * `avg(columns) -> Any`
@ -12,7 +12,7 @@ Currently 6 aggregation functions are supported.
* `QuerysetProxy` * `QuerysetProxy`
* `QuerysetProxy.count()` method * `QuerysetProxy.count(distinct=True)` method
* `QuerysetProxy.exists()` method * `QuerysetProxy.exists()` method
* `QuerysetProxy.sum(columns)` method * `QuerysetProxy.sum(columns)` method
* `QuerysetProxy.avg(columns)` method * `QuerysetProxy.avg(columns)` method
@ -22,9 +22,13 @@ Currently 6 aggregation functions are supported.
## count ## count
`count() -> int` `count(distinct: bool = True) -> int`
Returns number of rows matching the given criteria (i.e. applied with `filter` and `exclude`) Returns number of rows matching the given criteria (i.e. applied with `filter` and `exclude`).
If `distinct` is `True` (the default), this will return the number of primary rows selected. If `False`,
the count will be the total number of rows returned
(including extra rows for `one-to-many` or `many-to-many` left `select_related` table joins).
`False` is the legacy (buggy) behavior for workflows that depend on it.
```python ```python
class Book(ormar.Model): class Book(ormar.Model):
@ -322,4 +326,3 @@ objects from other side of the relation.
!!!tip !!!tip
To read more about `QuerysetProxy` visit [querysetproxy][querysetproxy] section To read more about `QuerysetProxy` visit [querysetproxy][querysetproxy] section

View File

@ -188,12 +188,12 @@ Instead of ormar models return raw data in form list of dictionaries or tuples.
### [Aggregated functions](./aggregations.md) ### [Aggregated functions](./aggregations.md)
* `count() -> int` * `count(distinct: bool = True) -> int`
* `exists() -> bool` * `exists() -> bool`
* `QuerysetProxy` * `QuerysetProxy`
* `QuerysetProxy.count()` method * `QuerysetProxy.count(distinct=True)` method
* `QuerysetProxy.exists()` method * `QuerysetProxy.exists()` method
!!!tip !!!tip

View File

@ -274,7 +274,7 @@ With exclude_fields() you can select subset of model columns that will be exclud
### count ### count
`count() -> int` `count(distinct: bool = True) -> int`
Returns number of rows matching the given criteria (i.e. applied with filter and exclude) Returns number of rows matching the given criteria (i.e. applied with filter and exclude)

View File

@ -26,7 +26,7 @@ class QuerySetProtocol(Protocol): # pragma: nocover
async def exists(self) -> bool: async def exists(self) -> bool:
... ...
async def count(self) -> int: async def count(self, distinct: bool = True) -> int:
... ...
async def clear(self) -> int: async def clear(self) -> int:

View File

@ -678,16 +678,25 @@ class QuerySet(Generic[T]):
expr = sqlalchemy.exists(expr).select() expr = sqlalchemy.exists(expr).select()
return await self.database.fetch_val(expr) return await self.database.fetch_val(expr)
async def count(self) -> int: async def count(self, distinct: bool = True) -> int:
""" """
Returns number of rows matching the given criteria Returns number of rows matching the given criteria
(applied with `filter` and `exclude` if set before). (applied with `filter` and `exclude` if set before).
If `distinct` is `True` (the default), this will return the number of primary rows selected. If `False`,
the count will be the total number of rows returned
(including extra rows for `one-to-many` or `many-to-many` left `select_related` table joins).
`False` is the legacy (buggy) behavior for workflows that depend on it.
:param distinct: flag if the primary table rows should be distinct or not
:return: number of rows :return: number of rows
:rtype: int :rtype: int
""" """
expr = self.build_select_expression().alias("subquery_for_count") expr = self.build_select_expression().alias("subquery_for_count")
expr = sqlalchemy.func.count().select().select_from(expr) expr = sqlalchemy.func.count().select().select_from(expr)
if distinct:
expr_distinct = expr.group_by(self.model_meta.pkname).alias("subquery_for_group")
expr = sqlalchemy.func.count().select().select_from(expr_distinct)
return await self.database.fetch_val(expr) return await self.database.fetch_val(expr)
async def _query_aggr_function(self, func_name: str, columns: List) -> Any: async def _query_aggr_function(self, func_name: str, columns: List) -> Any:

View File

@ -193,17 +193,22 @@ class QuerysetProxy(Generic[T]):
""" """
return await self.queryset.exists() return await self.queryset.exists()
async def count(self) -> int: async def count(self, distinct: bool = True) -> int:
""" """
Returns number of rows matching the given criteria Returns number of rows matching the given criteria
(applied with `filter` and `exclude` if set before). (applied with `filter` and `exclude` if set before).
If `distinct` is `True` (the default), this will return the number of primary rows selected. If `False`,
the count will be the total number of rows returned
(including extra rows for `one-to-many` or `many-to-many` left `select_related` table joins).
`False` is the legacy (buggy) behavior for workflows that depend on it.
Actual call delegated to QuerySet. Actual call delegated to QuerySet.
:param distinct: flag if the primary table rows should be distinct or not
:return: number of rows :return: number of rows
:rtype: int :rtype: int
""" """
return await self.queryset.count() return await self.queryset.count(distinct=distinct)
async def max(self, columns: Union[str, List[str]]) -> Any: # noqa: A003 async def max(self, columns: Union[str, List[str]]) -> Any: # noqa: A003
""" """

View File

@ -175,3 +175,15 @@ async def test_queryset_method():
assert await author.books.max(["year", "title"]) == dict( assert await author.books.max(["year", "title"]) == dict(
year=1930, title="Book 3" year=1930, title="Book 3"
) )
@pytest.mark.asyncio
async def test_count_method():
async with database:
await sample_data()
count = await Author.objects.select_related("books").count()
assert count == 1
# The legacy functionality
count = await Author.objects.select_related("books").count(distinct=False)
assert count == 3