Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Fixed
- ``QuerySet.count()`` now matches the limited query result for the LIMIT/OFFSET edge cases: it returns ``0`` (instead of a negative number) when ``offset()`` exceeds the total row count, and ``0`` (instead of the total) for ``limit(0)``. (#2208)
- Field declarations on models now resolve to their concrete type (e.g. ``CharField[str]``) in Pyright/Pylance instead of ``Field[Unknown]``; the ``Field.__new__`` type-check stub now returns ``Self``. (#2216)
- ``TransactionContext`` now returns a ``TransactionalDBClient`` instead of a raw database connection. This change gives the correct inferred type for the transaction context. (#2232)
- ``QuerySet.distinct().count()`` no longer counts rows duplicated by a join (e.g. filtering on a m2m relation); it now counts distinct primary keys and matches the number of rows the query returns. (#2178)


1.1.7
Expand Down
13 changes: 13 additions & 0 deletions tests/test_queryset.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
MinRelation,
Node,
Reporter,
Team,
Tournament,
Tree,
)
Expand Down Expand Up @@ -227,6 +228,18 @@ async def test_distinct(db, intfields_data):
]


@pytest.mark.asyncio
async def test_distinct_count_with_m2m_join(db):
tournament = await Tournament.create(name="Tournament")
event = await Event.create(name="Event", tournament=tournament)
for name in ("Team A", "Team B", "Team C"):
await event.participants.add(await Team.create(name=name))

queryset = Event.filter(participants__name__in=["Team A", "Team B", "Team C"]).distinct()
assert len(await queryset) == 1
assert await queryset.count() == 1


@pytest.mark.asyncio
async def test_limit_offset_values_list(db, intfields_data):
# Test limit/offset/ordering values_list
Expand Down
22 changes: 18 additions & 4 deletions tortoise/queryset.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@
from pypika_tortoise import JoinType, Order, Table
from pypika_tortoise.analytics import Count
from pypika_tortoise.functions import Cast
from pypika_tortoise.functions import Count as DistinctCount
from pypika_tortoise.queries import QueryBuilder, _SetOperation
from pypika_tortoise.terms import Case, Field, Star, Term, ValueWrapper
from pypika_tortoise.terms import Case, Field, Function, Star, Term, ValueWrapper

from tortoise.backends.base.client import BaseDBAsyncClient, Capabilities
from tortoise.exceptions import (
Expand Down Expand Up @@ -806,6 +807,7 @@ def count(self) -> CountQuery:
offset=self._offset,
force_indexes=self._force_indexes,
use_indexes=self._use_indexes,
distinct=self._distinct,
)

def exists(self) -> ExistsQuery:
Expand Down Expand Up @@ -1520,6 +1522,7 @@ class CountQuery(AwaitableQuery):
"_offset",
"_force_indexes",
"_use_indexes",
"_distinct",
)

def __init__(
Expand All @@ -1533,6 +1536,7 @@ def __init__(
offset: int | None,
force_indexes: set[str],
use_indexes: set[str],
distinct: bool = False,
) -> None:
super().__init__(model)
self._q_objects = q_objects
Expand All @@ -1543,13 +1547,23 @@ def __init__(
self._db = db
self._force_indexes = force_indexes
self._use_indexes = use_indexes
self._distinct = distinct

def _make_query(self) -> None:
self.query = copy(self.model._meta.basequery)
self.resolve_filters()
count_term = Count(Star())
if self.query._groupbys:
count_term = count_term.over()
count_term: Function
if self._distinct and not self.query._groupbys:
# ``DISTINCT`` on the outer SELECT does not apply to ``COUNT(*)``,
# so rows duplicated by a join would still be counted. Count the
# distinct primary keys instead.
count_term = DistinctCount(
self.model._meta.basetable[self.model._meta.db_pk_column]
).distinct()
else:
count_term = Count(Star())
if self.query._groupbys:
count_term = count_term.over()

# remove annotations
self.query._selects = []
Expand Down