Skip to content

Commit a9eefad

Browse files
authored
Merge branch 'develop' into fix/recorder-pk-deprecation-warning
2 parents 84e0a7b + 5ec4cfa commit a9eefad

8 files changed

Lines changed: 218 additions & 161 deletions

File tree

CHANGELOG.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ Added
2121
Fixed
2222
^^^^^
2323
- ``MigrationRecorder`` now uses parameterized queries; fixes MariaDB/MySQL rejecting ISO-8601 ``applied_at`` values. (#2132)
24-
- ``MigrationRecorder`` no longer emits tortoise's own ``pk`` field ``DeprecationWarning`` when applying migrations; it now builds its bookkeeping model with ``primary_key=True``.
24+
- ``MigrationRecorder`` no longer emits tortoise's own ``pk`` field ``DeprecationWarning`` when applying migrations; it now builds its bookkeeping model with ``primary_key=True``. (#2203)
25+
- ``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)
26+
- 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)
2527

2628
1.1.7
2729
-----

tests/backends/test_db_url.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
_postgres_scheme_engines = {
99
"postgres": "tortoise.backends.asyncpg",
10+
"postgresql": "tortoise.backends.asyncpg",
1011
"asyncpg": "tortoise.backends.asyncpg",
1112
"psycopg": "tortoise.backends.psycopg",
1213
}

tests/test_q.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,17 @@ def test_q_equality():
129129
assert complex_q1 == complex_q2
130130

131131

132+
def test_q_inequality():
133+
assert Q(moo="cow") != Q(moo="bull")
134+
assert Q(moo="cow") != Q(moo="cow", join_type=Q.OR)
135+
assert Q(moo="cow") != "not a q"
136+
137+
138+
def test_q_unhashable():
139+
with pytest.raises(TypeError, match="unhashable type"):
140+
hash(Q(moo="cow"))
141+
142+
132143
# =============================================================================
133144
# Tests for Q object resolution (requires database for model resolution)
134145
# =============================================================================

tests/test_queryset.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,14 @@ async def test_limit_count(db, intfields_data):
8888
assert await IntFields.all().limit(10).count() == 10
8989

9090

91+
@pytest.mark.asyncio
92+
async def test_limit_zero_count(db, intfields_data):
93+
# limit(0) means zero rows, so count() must be 0 (not the total), matching
94+
# the actual limited query.
95+
assert await IntFields.all().limit(0).count() == 0
96+
assert await IntFields.all().limit(0).count() == len(await IntFields.all().limit(0))
97+
98+
9199
@pytest.mark.asyncio
92100
async def test_limit_negative(db, intfields_data):
93101
with pytest.raises(ParamsError, match="Limit should be non-negative number"):
@@ -106,6 +114,14 @@ async def test_offset_count(db, intfields_data):
106114
assert await IntFields.all().offset(10).count() == 20
107115

108116

117+
@pytest.mark.asyncio
118+
async def test_offset_count_beyond_total(db, intfields_data):
119+
# An offset past the total must report 0, not a negative count (the SQL
120+
# LIMIT/OFFSET would return zero rows).
121+
assert await IntFields.all().offset(100).count() == 0
122+
assert await IntFields.all().offset(100).count() == len(await IntFields.all().offset(100))
123+
124+
109125
@pytest.mark.asyncio
110126
async def test_offset_negative(db, intfields_data):
111127
with pytest.raises(ParamsError, match="Offset should be non-negative number"):

tortoise/backends/base/config_generator.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from tortoise.exceptions import ConfigurationError
1010

1111
urlparse.uses_netloc.append("postgres")
12+
urlparse.uses_netloc.append("postgresql")
1213
urlparse.uses_netloc.append("asyncpg")
1314
urlparse.uses_netloc.append("psycopg")
1415
urlparse.uses_netloc.append("sqlite")
@@ -127,6 +128,8 @@
127128
}
128129
# Create an alias for backwards compatibility
129130
DB_LOOKUP["postgres"] = DB_LOOKUP["asyncpg"]
131+
# "postgresql" is the scheme accepted by libpq and pydantic's PostgresDsn
132+
DB_LOOKUP["postgresql"] = DB_LOOKUP["asyncpg"]
130133

131134

132135
def _quote_url_userinfo(db_url: str) -> str:
@@ -211,7 +214,7 @@ def expand_db_url(db_url: str, testing: bool = False) -> dict:
211214
# asyncpg accepts None for password, but aiomysql not
212215
params[vmap["password"]] = (
213216
None
214-
if (not url.password and db_backend in {"postgres", "asyncpg", "psycopg"})
217+
if (not url.password and db_backend in {"postgres", "postgresql", "asyncpg", "psycopg"})
215218
else urlparse.unquote(url.password or "")
216219
)
217220

tortoise/fields/base.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@
1919

2020
if sys.version_info >= (3, 11):
2121
from enum import StrEnum
22+
from typing import Self
2223
else: # pragma: no cover
24+
from typing_extensions import Self
2325

2426
class StrEnum(str, Enum):
2527
__str__ = str.__str__
@@ -202,7 +204,7 @@ def function_cast(self, term: Term) -> Term:
202204
# These methods are just to make IDE/Linters happy:
203205
if TYPE_CHECKING:
204206

205-
def __new__(cls, *args: Any, **kwargs: Any) -> Field[VALUE]:
207+
def __new__(cls, *args: Any, **kwargs: Any) -> Self:
206208
return super().__new__(cls)
207209

208210
@overload

tortoise/queryset.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1571,8 +1571,13 @@ async def _execute(self) -> int:
15711571
_, result = await self._db.execute_query(*self.query.get_parameterized_sql())
15721572
if not result:
15731573
return 0
1574-
count = list(dict(result[0]).values())[0] - self._offset
1575-
if self._limit and count > self._limit:
1574+
# COUNT(*) ignores LIMIT/OFFSET, so the offset is applied here. Clamp at
1575+
# 0: when the offset is past the total, SQL would return 0 rows, not a
1576+
# negative count.
1577+
count = max(0, list(dict(result[0]).values())[0] - self._offset)
1578+
# Use ``is not None`` so an explicit ``limit(0)`` clamps to 0 instead of
1579+
# being treated as "no limit" by a truthiness check.
1580+
if self._limit is not None and count > self._limit:
15761581
return self._limit
15771582
return count
15781583

0 commit comments

Comments
 (0)