Skip to content

Commit 6ba1471

Browse files
committed
Merge branch 'develop' of github.com:tortoise/tortoise-orm into sort-imports
2 parents 0ff989f + bc4f971 commit 6ba1471

12 files changed

Lines changed: 275 additions & 165 deletions

File tree

CHANGELOG.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ Added
2121
Fixed
2222
^^^^^
2323
- ``MigrationRecorder`` now uses parameterized queries; fixes MariaDB/MySQL rejecting ISO-8601 ``applied_at`` values. (#2132)
24+
- ``db_default`` on ``ForeignKeyField``/``OneToOneField`` now propagates to the underlying ``<fk>_id`` column, so ``CREATE TABLE`` emits the ``DEFAULT`` clause for FK columns. (#2199)
25+
- ``MigrationRecorder`` no longer emits tortoise's own ``pk`` field ``DeprecationWarning`` when applying migrations; it now builds its bookkeeping model with ``primary_key=True``. (#2203)
26+
- ``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)
27+
- 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)
2428

2529
1.1.7
2630
-----

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/migrations/test_recorder.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import warnings
2+
13
import pytest
24

35
from tortoise.backends.base.client import Capabilities
@@ -126,6 +128,22 @@ async def test_recorder_sqlite_placeholders() -> None:
126128
assert '"name" = ?' in delete_query
127129

128130

131+
def test_recorder_model_emits_no_field_deprecation_warnings() -> None:
132+
"""Regression: the migration recorder must not trip tortoise's own
133+
``pk``/``index`` field deprecation warnings. It builds its bookkeeping
134+
model internally, so downstream projects can't silence them.
135+
"""
136+
with warnings.catch_warnings(record=True) as caught:
137+
# Reset filters so the repo-wide ``ignore:`pk` deprecation`` filter in
138+
# pyproject.toml doesn't mask a regression here.
139+
warnings.simplefilter("always")
140+
MigrationRecorder(FakeConnection("sqlite"))
141+
142+
messages = [str(w.message) for w in caught if issubclass(w.category, DeprecationWarning)]
143+
assert not any("`pk` is deprecated" in m for m in messages), messages
144+
assert not any("`index` is deprecated" in m for m in messages), messages
145+
146+
129147
@pytest.mark.asyncio
130148
async def test_recorder_oracle_placeholders() -> None:
131149
connection = FakeConnection("oracle")

tests/migrations/test_schema_editor_sql.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,3 +249,36 @@ class Meta:
249249
sql = client.executed[0]
250250
assert 'CREATE TABLE "widget"' in sql
251251
assert "DEFAULT 'active'" in sql
252+
253+
254+
@pytest.mark.asyncio
255+
async def test_create_model_includes_db_default_on_fk() -> None:
256+
"""CreateModel should include DEFAULT clause for FK columns with db_default."""
257+
258+
class Dc(Model):
259+
id = fields.IntField(primary_key=True)
260+
261+
class Meta:
262+
table = "dc"
263+
app = "models"
264+
265+
class App(Model):
266+
id = fields.IntField(primary_key=True)
267+
dc: fields.ForeignKeyRelation[Dc] = fields.ForeignKeyField("models.Dc", db_default=2)
268+
269+
class Meta:
270+
table = "app"
271+
app = "models"
272+
273+
init_apps(Dc, App)
274+
275+
client = FakeClient("sql")
276+
editor = TestSchemaEditor(client)
277+
278+
await editor.create_model(App)
279+
280+
assert len(client.executed) == 1
281+
sql = client.executed[0]
282+
assert 'CREATE TABLE "app"' in sql
283+
assert '"dc_id"' in sql
284+
assert "DEFAULT 2" in sql

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/apps.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ def init_fk_o2o_field(model: type[Model], field: str, is_o2o: bool = False) -> N
192192
key_field = f"{field}_id"
193193
key_fk_object.reference = fk_object
194194
key_fk_object.source_field = fk_object.source_field or key_field
195-
for attr in ("index", "default", "null", "generated", "description"):
195+
for attr in ("index", "default", "null", "generated", "description", "db_default"):
196196
setattr(key_fk_object, attr, getattr(fk_object, attr))
197197
if is_o2o:
198198
key_fk_object.pk = fk_object.pk

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/migrations/recorder.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def _placeholder(self, pos: int) -> str:
3535

3636
def _make_model(self, table_name: str) -> type[Model]:
3737
class MigrationRecord(Model):
38-
id = fields.IntField(pk=True)
38+
id = fields.IntField(primary_key=True)
3939
app = fields.CharField(max_length=255)
4040
name = fields.CharField(max_length=255)
4141
applied_at = fields.DatetimeField()

0 commit comments

Comments
 (0)