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
11 changes: 11 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@ Changelog
1.1
===

1.1.9
-----

Fixed
^^^^^
- Fixed DELETE and UPDATE queries failing when filtering by related fields (foreign keys). Using a subquery pattern instead of JOIN for compatibility with MySQL and SQLite. (#2139)

Added
^^^^^
- Tests for model validators. (#2137)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove this, as it is unrelated to this PR.


1.1.8
-----

Expand Down
101 changes: 101 additions & 0 deletions tests/test_queryset.py
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,38 @@ async def test_delete_limit_order_by(db, intfields_data):
await IntFields.get(intnum=97)


@pytest.mark.asyncio
async def test_delete_filter_with_foreign_key(db):
author = await Author.create(name="test")
Comment thread
noy-solvin marked this conversation as resolved.
await Book.create(name="book1", author=author, rating=5.0)
await Book.create(name="book2", author=author, rating=4.0)

author2 = await Author.create(name="test2")
await Book.create(name="book3", author=author2, rating=5.0)

# This is the failing query

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove this comment line.

await Book.filter(author__name="test").delete()

assert await Book.all().count() == 1


@pytest.mark.asyncio
async def test_update_filter_with_foreign_key(db):
author = await Author.create(name="test")
Comment thread
noy-solvin marked this conversation as resolved.
await Book.create(name="book1", author=author, rating=5.0)

author2 = await Author.create(name="test2")
await Book.create(name="book2", author=author2, rating=5.0)

await Book.filter(author__name="test").update(rating=1.0)

book = await Book.get(name="book1")
assert book.rating == 1.0
Comment thread
noy-solvin marked this conversation as resolved.

book2 = await Book.get(name="book2")
assert book2.rating == 5.0


@pytest.mark.asyncio
async def test_async_iter(db, intfields_data):
counter = 0
Expand Down Expand Up @@ -1156,3 +1188,72 @@ async def test_union_with_annotate_raises(db):

with pytest.raises(ParamsError, match="Union queries do not support annotations"):
await qs1.union(qs2)


@pytest.mark.asyncio
async def test_update_limit_order_by_with_join(db):
old_cap_val = Event._meta.db.capabilities.support_update_limit_order_by
object.__setattr__(Event._meta.db.capabilities, "_mutable", True)
Event._meta.db.capabilities.support_update_limit_order_by = True
try:
t1 = await Tournament.create(name="T1")
e1 = await Event.create(name="E1", tournament=t1)
e2 = await Event.create(name="E2", tournament=t1)

updated = (
await Event.filter(tournament__name="T1")
.order_by("event_id")
.limit(1)
.update(name="E1_updated")
)
assert updated == 1

await e1.refresh_from_db()
await e2.refresh_from_db()
assert e1.name == "E1_updated"
assert e2.name == "E2"
finally:
Event._meta.db.capabilities.support_update_limit_order_by = old_cap_val
object.__setattr__(Event._meta.db.capabilities, "_mutable", False)


@pytest.mark.asyncio
async def test_delete_limit_order_by_with_join(db):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are this test and the one above related to this PR? If not, please move them to a separate PR. If they are related, please use a TestClass to reduce duplicated code.

old_cap_val = Event._meta.db.capabilities.support_update_limit_order_by
object.__setattr__(Event._meta.db.capabilities, "_mutable", True)
Event._meta.db.capabilities.support_update_limit_order_by = True
try:
t1 = await Tournament.create(name="T1")
await Event.create(name="E1", tournament=t1)
await Event.create(name="E2", tournament=t1)

deleted = await Event.filter(tournament__name="T1").order_by("event_id").limit(1).delete()
assert deleted == 1

count = await Event.all().count()
assert count == 1
finally:
Event._meta.db.capabilities.support_update_limit_order_by = old_cap_val
object.__setattr__(Event._meta.db.capabilities, "_mutable", False)


def test_update_query_postgres_dialect_coverage(db):
from tortoise.backends.base.client import Capabilities

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't need to be inside a function — please move it to the top of the file.


q = Event.filter(tournament__name="T1").limit(1).update(name="E1_updated")
q.capabilities = Capabilities("postgres", support_update_limit_order_by=False)
sql = q.sql()
assert "IN (SELECT " in sql
assert '"_t"' not in sql
assert "`_t`" not in sql


def test_delete_query_postgres_dialect_coverage(db):
from tortoise.backends.base.client import Capabilities

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also move it to the top.


q = Event.filter(tournament__name="T1").delete()
q.capabilities = Capabilities("postgres", support_update_limit_order_by=False)
sql = q.sql()
assert "IN (SELECT " in sql
assert '"_t"' not in sql
assert "`_t`" not in sql
43 changes: 42 additions & 1 deletion tortoise/queryset.py
Original file line number Diff line number Diff line change
Expand Up @@ -1349,12 +1349,37 @@ def __init__(

def _make_query(self) -> None:
table = self.model._meta.basetable
self.query = self._db.query_class.update(table)
self.query = copy(self.model._meta.basequery)
if self.capabilities.support_update_limit_order_by and self._limit:
self.query._limit = self.query._wrapper_cls(self._limit)
self.resolve_ordering(self.model, table, self._orderings, self._annotations)

self.resolve_filters()
if self._joined_tables:
# If we have joins, we must use a subquery for update
# because standard UPDATE does not support JOINs on many DBs.
pk_column = self.model._meta.db_pk_column
subquery = self.query.select(table[pk_column])

if self.capabilities.dialect == "mysql":
# To avoid MySQL Error 1093, we wrap the subquery in another SELECT
# To avoid MySQL Error 1235, the outer SELECT shouldn't have LIMIT
wrapper = self._db.query_class.from_(subquery.as_("_t")).select(
Table("_t")[pk_column]
)
else:
wrapper = subquery

self.query = self._db.query_class.update(table)
self.query = self.query.where(table[pk_column].isin(wrapper))

else:
update_query = self._db.query_class.update(table)
update_query._wheres = self.query._wheres
update_query._limit = self.query._limit
update_query._orderbys = self.query._orderbys
self.query = update_query

for key, value in self.update_kwargs.items():
field_object = self.model._meta.fields_map.get(key)
if not field_object:
Expand Down Expand Up @@ -1437,6 +1462,22 @@ def _make_query(self) -> None:
annotations=self._annotations,
)
self.resolve_filters()
if self._joined_tables:
# If we have joins, we must use a subquery for deletion
# because standard DELETE FROM does not support JOINs.
pk_column = self.model._meta.db_pk_column
subquery = self.query.select(self.model._meta.basetable[pk_column])

if self.capabilities.dialect == "mysql":
wrapper = self._db.query_class.from_(subquery.as_("_t")).select(
Table("_t")[pk_column]
)
else:
wrapper = subquery

self.query = copy(self.model._meta.basequery)
self.query = self.query.where(self.model._meta.basetable[pk_column].isin(wrapper))

self.query._delete_from = True
return

Expand Down
Loading