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

@ -334,7 +334,7 @@ async def delete():
async def joins(): async def joins():
# Tho join two models use select_related # Tho join two models use select_related
# Django style # Django style
book = await Book.objects.select_related("author").get(title="The Hobbit") book = await Book.objects.select_related("author").get(title="The Hobbit")
# Python style # Python style
@ -348,7 +348,7 @@ async def joins():
# By default you also get a second side of the relation # By default you also get a second side of the relation
# constructed as lowercase source model name +'s' (books in this case) # constructed as lowercase source model name +'s' (books in this case)
# you can also provide custom name with parameter related_name # you can also provide custom name with parameter related_name
# Django style # Django style
author = await Author.objects.select_related("books").all(name="J.R.R. Tolkien") author = await Author.objects.select_related("books").all(name="J.R.R. Tolkien")
# Python style # Python style
@ -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
@ -260,11 +268,11 @@ List of related models is cleared before the call.
**Arguments**: **Arguments**:
- `kwargs`: - `kwargs`:
**Returns**: **Returns**:
`_asyncio.Future`: `_asyncio.Future`:
<a name="relations.querysetproxy.QuerysetProxy.get_or_none"></a> <a name="relations.querysetproxy.QuerysetProxy.get_or_none"></a>
#### get\_or\_none #### get\_or\_none
@ -773,4 +781,3 @@ Actual call delegated to QuerySet.
**Returns**: **Returns**:
`QuerysetProxy`: QuerysetProxy `QuerysetProxy`: QuerysetProxy

View File

@ -22,7 +22,7 @@
### Overview ### Overview
The `ormar` package is an async mini ORM for Python, with support for **Postgres, The `ormar` package is an async mini ORM for Python, with support for **Postgres,
MySQL**, and **SQLite**. MySQL**, and **SQLite**.
The main benefits of using `ormar` are: The main benefits of using `ormar` are:
@ -31,7 +31,7 @@ The main benefits of using `ormar` are:
The goal was to create a simple ORM that can be **used directly (as request and response models) with [`fastapi`][fastapi]** that bases it's data validation on pydantic. The goal was to create a simple ORM that can be **used directly (as request and response models) with [`fastapi`][fastapi]** that bases it's data validation on pydantic.
Ormar - apart from the obvious "ORM" in name - gets its name from _ormar_ in Swedish which means _snakes_, and _ormar(e)_ in Croatian which means _cabinet_. Ormar - apart from the obvious "ORM" in name - gets its name from _ormar_ in Swedish which means _snakes_, and _ormar(e)_ in Croatian which means _cabinet_.
And what's a better name for python ORM than snakes cabinet :) And what's a better name for python ORM than snakes cabinet :)
@ -82,7 +82,7 @@ Ormar is built with:
`ormar` is built as open-source software and will remain completely free (MIT license). `ormar` is built as open-source software and will remain completely free (MIT license).
As I write open-source code to solve everyday problems in my work or to promote and build strong python As I write open-source code to solve everyday problems in my work or to promote and build strong python
community you can say thank you and buy me a coffee or sponsor me with a monthly amount to help ensure my work remains free and maintained. community you can say thank you and buy me a coffee or sponsor me with a monthly amount to help ensure my work remains free and maintained.
<a aria-label="Sponsor collerek" href="https://github.com/sponsors/collerek" style="text-decoration: none; color: #c9d1d9 !important;"> <a aria-label="Sponsor collerek" href="https://github.com/sponsors/collerek" style="text-decoration: none; color: #c9d1d9 !important;">
@ -99,7 +99,7 @@ community you can say thank you and buy me a coffee or sponsor me with a monthly
padding: 10px; padding: 10px;
line-height: 0px; line-height: 0px;
height: 40px; height: 40px;
"> ">
<svg aria-hidden="true" viewBox="0 0 16 16" height="16" width="16" style="fill: #db61a2"> <svg aria-hidden="true" viewBox="0 0 16 16" height="16" width="16" style="fill: #db61a2">
<path fill-rule="evenodd" d="M4.25 2.5c-1.336 0-2.75 1.164-2.75 3 0 2.15 1.58 4.144 3.365 5.682A20.565 20.565 0 008 13.393a20.561 20.561 0 003.135-2.211C12.92 9.644 14.5 7.65 14.5 5.5c0-1.836-1.414-3-2.75-3-1.373 0-2.609.986-3.029 2.456a.75.75 0 01-1.442 0C6.859 3.486 5.623 2.5 4.25 2.5zM8 14.25l-.345.666-.002-.001-.006-.003-.018-.01a7.643 7.643 0 01-.31-.17 22.075 22.075 0 01-3.434-2.414C2.045 10.731 0 8.35 0 5.5 0 2.836 2.086 1 4.25 1 5.797 1 7.153 1.802 8 3.02 8.847 1.802 10.203 1 11.75 1 13.914 1 16 2.836 16 5.5c0 2.85-2.045 5.231-3.885 6.818a22.08 22.08 0 01-3.744 2.584l-.018.01-.006.003h-.002L8 14.25zm0 0l.345.666a.752.752 0 01-.69 0L8 14.25z"></path> <path fill-rule="evenodd" d="M4.25 2.5c-1.336 0-2.75 1.164-2.75 3 0 2.15 1.58 4.144 3.365 5.682A20.565 20.565 0 008 13.393a20.561 20.561 0 003.135-2.211C12.92 9.644 14.5 7.65 14.5 5.5c0-1.836-1.414-3-2.75-3-1.373 0-2.609.986-3.029 2.456a.75.75 0 01-1.442 0C6.859 3.486 5.623 2.5 4.25 2.5zM8 14.25l-.345.666-.002-.001-.006-.003-.018-.01a7.643 7.643 0 01-.31-.17 22.075 22.075 0 01-3.434-2.414C2.045 10.731 0 8.35 0 5.5 0 2.836 2.086 1 4.25 1 5.797 1 7.153 1.802 8 3.02 8.847 1.802 10.203 1 11.75 1 13.914 1 16 2.836 16 5.5c0 2.85-2.045 5.231-3.885 6.818a22.08 22.08 0 01-3.744 2.584l-.018.01-.006.003h-.002L8 14.25zm0 0l.345.666a.752.752 0 01-.69 0L8 14.25z"></path>
</svg> </svg>
@ -141,30 +141,30 @@ engine = sqlalchemy.create_engine(DATABASE_URL)
metadata.create_all(engine) metadata.create_all(engine)
``` ```
For a sample configuration of alembic and more information regarding migrations and For a sample configuration of alembic and more information regarding migrations and
database creation visit [migrations][migrations] documentation section. database creation visit [migrations][migrations] documentation section.
### Package versions ### Package versions
**ormar is still under development:** **ormar is still under development:**
We recommend pinning any dependencies (with i.e. `ormar~=0.9.1`) We recommend pinning any dependencies (with i.e. `ormar~=0.9.1`)
`ormar` also follows the release numeration that breaking changes bump the major number, `ormar` also follows the release numeration that breaking changes bump the major number,
while other changes and fixes bump minor number, so with the latter you should be safe to while other changes and fixes bump minor number, so with the latter you should be safe to
update, yet always read the [releases][releases] docs before. update, yet always read the [releases][releases] docs before.
`example: (0.5.2 -> 0.6.0 - breaking, 0.5.2 -> 0.5.3 - non breaking)`. `example: (0.5.2 -> 0.6.0 - breaking, 0.5.2 -> 0.5.3 - non breaking)`.
### Asynchronous Python ### Asynchronous Python
Note that `ormar` is an asynchronous ORM, which means that you have to `await` the calls to Note that `ormar` is an asynchronous ORM, which means that you have to `await` the calls to
the methods, that are scheduled for execution in an event loop. Python has a builtin module the methods, that are scheduled for execution in an event loop. Python has a builtin module
[`asyncio`][asyncio] that allows you to do just that. [`asyncio`][asyncio] that allows you to do just that.
Note that most "normal" python interpreters do not allow execution of `await` Note that most "normal" python interpreters do not allow execution of `await`
outside of a function (because you actually schedule this function for delayed execution outside of a function (because you actually schedule this function for delayed execution
and don't get the result immediately). and don't get the result immediately).
In a modern web framework (like `fastapi`), the framework will handle this for you, but if In a modern web framework (like `fastapi`), the framework will handle this for you, but if
you plan to do this on your own you need to perform this manually like described in the you plan to do this on your own you need to perform this manually like described in the
quick start below. quick start below.
### Quick Start ### Quick Start
@ -343,7 +343,7 @@ async def delete():
async def joins(): async def joins():
# Tho join two models use select_related # Tho join two models use select_related
# Django style # Django style
book = await Book.objects.select_related("author").get(title="The Hobbit") book = await Book.objects.select_related("author").get(title="The Hobbit")
# Python style # Python style
@ -357,7 +357,7 @@ async def joins():
# By default you also get a second side of the relation # By default you also get a second side of the relation
# constructed as lowercase source model name +'s' (books in this case) # constructed as lowercase source model name +'s' (books in this case)
# you can also provide custom name with parameter related_name # you can also provide custom name with parameter related_name
# Django style # Django style
author = await Author.objects.select_related("books").all(name="J.R.R. Tolkien") author = await Author.objects.select_related("books").all(name="J.R.R. Tolkien")
# Python style # Python style
@ -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`
@ -670,10 +670,10 @@ All fields are required unless one of the following is set:
* `sql_nullable` - Used to set different setting for pydantic and the database. Sets the default to `nullable` value. Read the fields common parameters for details. * `sql_nullable` - Used to set different setting for pydantic and the database. Sets the default to `nullable` value. Read the fields common parameters for details.
* `default` - Set a default value for the field. **Not available for relation fields** * `default` - Set a default value for the field. **Not available for relation fields**
* `server_default` - Set a default value for the field on server side (like sqlalchemy's `func.now()`). **Not available for relation fields** * `server_default` - Set a default value for the field on server side (like sqlalchemy's `func.now()`). **Not available for relation fields**
* `primary key` with `autoincrement` - When a column is set to primary key and autoincrement is set on this column. * `primary key` with `autoincrement` - When a column is set to primary key and autoincrement is set on this column.
Autoincrement is set by default on int primary keys. Autoincrement is set by default on int primary keys.
* `pydantic_only` - Field is available only as normal pydantic field, not stored in the database. * `pydantic_only` - Field is available only as normal pydantic field, not stored in the database.
### Available signals ### Available signals
Signals allow to trigger your function for a given event on a given Model. Signals allow to trigger your function for a given event on a given Model.

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,19 +12,23 @@ 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
* `QuerysetProxy.min(column)` method * `QuerysetProxy.min(column)` method
* `QuerysetProxy.max(columns)` method * `QuerysetProxy.max(columns)` method
## 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):
@ -84,7 +88,7 @@ Returns sum value of columns for rows matching the given criteria (applied with
You can pass one or many column names including related columns. You can pass one or many column names including related columns.
As of now each column passed is aggregated separately (so `sum(col1+col2)` is not possible, As of now each column passed is aggregated separately (so `sum(col1+col2)` is not possible,
you can have `sum(col1, col2)` and later add 2 returned sums in python) you can have `sum(col1, col2)` and later add 2 returned sums in python)
You cannot `sum` non numeric columns. You cannot `sum` non numeric columns.
@ -138,7 +142,7 @@ Returns avg value of columns for rows matching the given criteria (applied with
You can pass one or many column names including related columns. You can pass one or many column names including related columns.
As of now each column passed is aggregated separately (so `sum(col1+col2)` is not possible, As of now each column passed is aggregated separately (so `sum(col1+col2)` is not possible,
you can have `sum(col1, col2)` and later add 2 returned sums in python) you can have `sum(col1, col2)` and later add 2 returned sums in python)
You cannot `avg` non numeric columns. You cannot `avg` non numeric columns.
@ -193,7 +197,7 @@ Returns min value of columns for rows matching the given criteria (applied with
You can pass one or many column names including related columns. You can pass one or many column names including related columns.
As of now each column passed is aggregated separately (so `sum(col1+col2)` is not possible, As of now each column passed is aggregated separately (so `sum(col1+col2)` is not possible,
you can have `sum(col1, col2)` and later add 2 returned sums in python) you can have `sum(col1, col2)` and later add 2 returned sums in python)
If you aggregate on one column, the single value is directly returned as a result If you aggregate on one column, the single value is directly returned as a result
@ -241,7 +245,7 @@ Returns min value of columns for rows matching the given criteria (applied with
You can pass one or many column names including related columns. You can pass one or many column names including related columns.
As of now each column passed is aggregated separately (so `sum(col1+col2)` is not possible, As of now each column passed is aggregated separately (so `sum(col1+col2)` is not possible,
you can have `sum(col1, col2)` and later add 2 returned sums in python) you can have `sum(col1, col2)` and later add 2 returned sums in python)
If you aggregate on one column, the single value is directly returned as a result If you aggregate on one column, the single value is directly returned as a result
@ -292,7 +296,7 @@ select related etc related models directly from parent model.
Works exactly the same as [count](./#count) function above but allows you to select columns from related Works exactly the same as [count](./#count) function above but allows you to select columns from related
objects from other side of the relation. 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
### exists ### exists
@ -320,6 +324,5 @@ objects from other side of the relation.
Works exactly the same as [max](./#max) function above but allows you to select maximum of columns from related Works exactly the same as [max](./#max) function above but allows you to select maximum of columns from related
objects from other side of the relation. 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

@ -8,7 +8,7 @@ and it's options.
Most of the methods are also available through many to many relations and on reverse Most of the methods are also available through many to many relations and on reverse
foreign key relations through `QuerysetProxy` interface. foreign key relations through `QuerysetProxy` interface.
!!!info !!!info
To see which relations are supported and how to construct relations To see which relations are supported and how to construct relations
visit [relations][relations]. visit [relations][relations].
@ -34,12 +34,12 @@ To read more about any specific section or function please refer to the details
* `Model.upsert()` method * `Model.upsert()` method
* `Model.save_related()` method * `Model.save_related()` method
* `QuerysetProxy` * `QuerysetProxy`
* `QuerysetProxy.create(**kwargs)` method * `QuerysetProxy.create(**kwargs)` method
* `QuerysetProxy.get_or_create(**kwargs)` method * `QuerysetProxy.get_or_create(**kwargs)` method
* `QuerysetProxy.update_or_create(**kwargs)` method * `QuerysetProxy.update_or_create(**kwargs)` method
!!!tip !!!tip
To read more about any or all of those functions visit [create](./create.md) section. To read more about any or all of those functions visit [create](./create.md) section.
@ -62,7 +62,7 @@ To read more about any specific section or function please refer to the details
* `QuerysetProxy.get_or_create(**kwargs)` method * `QuerysetProxy.get_or_create(**kwargs)` method
* `QuerysetProxy.first()` method * `QuerysetProxy.first()` method
* `QuerysetProxy.all(**kwargs)` method * `QuerysetProxy.all(**kwargs)` method
!!!tip !!!tip
To read more about any or all of those functions visit [read](./read.md) section. To read more about any or all of those functions visit [read](./read.md) section.
@ -96,7 +96,7 @@ Instead of ormar models return raw data in form list of dictionaries or tuples.
* `QuerysetProxy` * `QuerysetProxy`
* `QuerysetProxy.update_or_create(**kwargs)` method * `QuerysetProxy.update_or_create(**kwargs)` method
!!!tip !!!tip
To read more about any or all of those functions visit [update](./update.md) section. To read more about any or all of those functions visit [update](./update.md) section.
@ -112,7 +112,7 @@ Instead of ormar models return raw data in form list of dictionaries or tuples.
* `QuerysetProxy` * `QuerysetProxy`
* `QuerysetProxy.remove()` method * `QuerysetProxy.remove()` method
* `QuerysetProxy.clear()` method * `QuerysetProxy.clear()` method
!!!tip !!!tip
To read more about any or all of those functions visit [delete](./delete.md) section. To read more about any or all of those functions visit [delete](./delete.md) section.
@ -129,7 +129,7 @@ Instead of ormar models return raw data in form list of dictionaries or tuples.
* `QuerysetProxy` * `QuerysetProxy`
* `QuerysetProxy.select_related(related: Union[List, str])` method * `QuerysetProxy.select_related(related: Union[List, str])` method
* `QuerysetProxy.prefetch_related(related: Union[List, str])` method * `QuerysetProxy.prefetch_related(related: Union[List, str])` method
!!!tip !!!tip
To read more about any or all of those functions visit [joins and subqueries](./joins-and-subqueries.md) section. To read more about any or all of those functions visit [joins and subqueries](./joins-and-subqueries.md) section.
@ -152,7 +152,7 @@ Instead of ormar models return raw data in form list of dictionaries or tuples.
* `QuerysetProxy.get_or_none(**kwargs)` method * `QuerysetProxy.get_or_none(**kwargs)` method
* `QuerysetProxy.get_or_create(**kwargs)` method * `QuerysetProxy.get_or_create(**kwargs)` method
* `QuerysetProxy.all(**kwargs)` method * `QuerysetProxy.all(**kwargs)` method
!!!tip !!!tip
To read more about any or all of those functions visit [filtering and sorting](./filter-and-sort.md) section. To read more about any or all of those functions visit [filtering and sorting](./filter-and-sort.md) section.
@ -165,7 +165,7 @@ Instead of ormar models return raw data in form list of dictionaries or tuples.
* `QuerysetProxy` * `QuerysetProxy`
* `QuerysetProxy.fields(columns: Union[List, str, set, dict])` method * `QuerysetProxy.fields(columns: Union[List, str, set, dict])` method
* `QuerysetProxy.exclude_fields(columns: Union[List, str, set, dict])` method * `QuerysetProxy.exclude_fields(columns: Union[List, str, set, dict])` method
!!!tip !!!tip
To read more about any or all of those functions visit [selecting columns](./select-columns.md) section. To read more about any or all of those functions visit [selecting columns](./select-columns.md) section.
@ -182,22 +182,22 @@ Instead of ormar models return raw data in form list of dictionaries or tuples.
* `QuerysetProxy.paginate(page: int)` method * `QuerysetProxy.paginate(page: int)` method
* `QuerysetProxy.limit(limit_count: int)` method * `QuerysetProxy.limit(limit_count: int)` method
* `QuerysetProxy.offset(offset: int)` method * `QuerysetProxy.offset(offset: int)` method
!!!tip !!!tip
To read more about any or all of those functions visit [pagination](./pagination-and-rows-number.md) section. To read more about any or all of those functions visit [pagination](./pagination-and-rows-number.md) section.
### [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
To read more about any or all of those functions visit [aggregations](./aggregations.md) section. To read more about any or all of those functions visit [aggregations](./aggregations.md) section.
[relations]: ../relations/index.md [relations]: ../relations/index.md

View File

@ -6,35 +6,35 @@ But at the same time it exposes subset of QuerySet API, so you can filter, creat
!!!note !!!note
By default exposed QuerySet is already filtered to return only `Models` related to parent `Model`. By default exposed QuerySet is already filtered to return only `Models` related to parent `Model`.
So if you issue `post.categories.all()` you will get all categories related to that post, not all in table. So if you issue `post.categories.all()` you will get all categories related to that post, not all in table.
!!!note !!!note
Note that when accessing QuerySet API methods through QuerysetProxy you don't Note that when accessing QuerySet API methods through QuerysetProxy you don't
need to use `objects` attribute like in normal queries. need to use `objects` attribute like in normal queries.
So note that it's `post.categories.all()` and **not** `post.categories.objects.all()`. So note that it's `post.categories.all()` and **not** `post.categories.objects.all()`.
To learn more about available QuerySet methods visit [queries][queries] To learn more about available QuerySet methods visit [queries][queries]
!!!warning !!!warning
Querying related models from ManyToMany cleans list of related models loaded on parent model: Querying related models from ManyToMany cleans list of related models loaded on parent model:
Example: `post.categories.first()` will set post.categories to list of 1 related model -> the one returned by first() Example: `post.categories.first()` will set post.categories to list of 1 related model -> the one returned by first()
Example 2: if post has 4 categories so `len(post.categories) == 4` calling `post.categories.limit(2).all()` Example 2: if post has 4 categories so `len(post.categories) == 4` calling `post.categories.limit(2).all()`
-> will load only 2 children and now `assert len(post.categories) == 2` -> will load only 2 children and now `assert len(post.categories) == 2`
This happens for all QuerysetProxy methods returning data: `get`, `all` and `first` and in `get_or_create` if model already exists. This happens for all QuerysetProxy methods returning data: `get`, `all` and `first` and in `get_or_create` if model already exists.
Note that value returned by `create` or created in `get_or_create` and `update_or_create` Note that value returned by `create` or created in `get_or_create` and `update_or_create`
if model does not exist will be added to relation list (not clearing it). if model does not exist will be added to relation list (not clearing it).
## Read data from database ## Read data from database
### get ### get
`get(**kwargs): -> Model` `get(**kwargs): -> Model`
To grab just one of related models filtered by name you can use `get(**kwargs)` method. To grab just one of related models filtered by name you can use `get(**kwargs)` method.
@ -67,7 +67,7 @@ Tries to get a row meeting the criteria and if NoMatch exception is raised it cr
`all(**kwargs) -> List[Optional["Model"]]` `all(**kwargs) -> List[Optional["Model"]]`
To get a list of related models use `all()` method. To get a list of related models use `all()` method.
Note that you can filter the queryset, select related, exclude fields etc. like in normal query. Note that you can filter the queryset, select related, exclude fields etc. like in normal query.
@ -88,7 +88,7 @@ assert news_posts[0].author == guido
### create ### create
`create(**kwargs): -> Model` `create(**kwargs): -> Model`
Create related `Model` directly from parent `Model`. Create related `Model` directly from parent `Model`.
@ -105,7 +105,7 @@ assert len(await post.categories.all()) == 2
Read more in queries documentation [create][create] Read more in queries documentation [create][create]
For `ManyToMany` relations there is an additional functionality of passing parameters For `ManyToMany` relations there is an additional functionality of passing parameters
that will be used to create a through model if you declared additional fields on explicitly that will be used to create a through model if you declared additional fields on explicitly
provided Through model. provided Through model.
Given sample like this: Given sample like this:
@ -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)
@ -309,4 +309,4 @@ Returns a bool value to confirm if there are rows matching the given criteria (a
[exists]: ../queries/aggregations.md#exists [exists]: ../queries/aggregations.md#exists
[fields]: ../queries/select-columns.md#fields [fields]: ../queries/select-columns.md#fields
[exclude_fields]: ../queries/select-columns.md#exclude_fields [exclude_fields]: ../queries/select-columns.md#exclude_fields
[order_by]: ../queries/filter-and-sort.md#order_by [order_by]: ../queries/filter-and-sort.md#order_by

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