From 80c15af9b37b557b2a2330f8eadb66107375db4d Mon Sep 17 00:00:00 2001 From: solvin-ai-teammate Date: Wed, 11 Mar 2026 12:15:32 +0200 Subject: [PATCH 1/3] fix(queryset): use subquery for DELETE/UPDATE filtering by related fields --- tests/test_queryset.py | 22 ++++++++++++++++++++++ tortoise/queryset.py | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/tests/test_queryset.py b/tests/test_queryset.py index 513979035..6478c5282 100644 --- a/tests/test_queryset.py +++ b/tests/test_queryset.py @@ -478,6 +478,28 @@ async def test_delete_limit_order_by(db, intfields_data): with pytest.raises(DoesNotExist): await IntFields.get(intnum=97) +@pytest.mark.asyncio +async def test_delete_filter_with_foreign_key(db): + author = await Author.create(name="test") + await Book.create(name="book1", author=author, rating=5.0) + await Book.create(name="book2", author=author, rating=4.0) + + # This is the failing query + await Book.filter(author__name="test").delete() + + assert await Book.all().count() == 0 + + +@pytest.mark.asyncio +async def test_update_filter_with_foreign_key(db): + author = await Author.create(name="test") + await Book.create(name="book1", author=author, rating=5.0) + + await Book.filter(author__name="test").update(rating=1.0) + + book = await Book.first() + assert book.rating == 1.0 + @pytest.mark.asyncio async def test_async_iter(db, intfields_data): diff --git a/tortoise/queryset.py b/tortoise/queryset.py index 0cfd2fc52..cb0bade7a 100644 --- a/tortoise/queryset.py +++ b/tortoise/queryset.py @@ -1301,6 +1301,26 @@ def _make_query(self) -> None: 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._db.query_class.from_(table).select(table[pk_column]) + subquery._wheres = self.query._wheres + subquery._havings = self.query._havings + subquery._joins = self.query._joins + if hasattr(self.query, "_limit"): + subquery._limit = self.query._limit + if hasattr(self.query, "_orderbys"): + subquery._orderbys = self.query._orderbys + + # 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]) + + self.query = self._db.query_class.update(table) + self.query = self.query.where(table[pk_column].isin(wrapper)) + for key, value in self.update_kwargs.items(): field_object = self.model._meta.fields_map.get(key) if not field_object: @@ -1383,6 +1403,21 @@ 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]) + + # To avoid MySQL Error 1093, we wrap the subquery in another SELECT + # To avoid MySQL Error 1235, the outer SELECT shouldn't have LIMIT + # We use the connection's query class directly to avoid carrying over + # the base table into the FROM clause. + wrapper = self._db.query_class.from_(subquery.as_("_t")).select(Table("_t")[pk_column]) + + 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 From fc545b7ca0f21c3bb6f3aabcdf135d7718add28a Mon Sep 17 00:00:00 2001 From: NoySolvin Date: Wed, 11 Mar 2026 12:40:48 +0200 Subject: [PATCH 2/3] update changelog --- CHANGELOG.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 5c448bdd0..f2aff4cc6 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -8,6 +8,13 @@ Changelog 1.1 === +1.1.7 +----- + +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. (#283) + 1.1.6 ----- From b45306503ff65f89d8e8dc845765f6183d5b2a85 Mon Sep 17 00:00:00 2001 From: NoySolvin Date: Thu, 12 Mar 2026 11:39:38 +0200 Subject: [PATCH 3/3] style: fix formatting --- tests/test_queryset.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_queryset.py b/tests/test_queryset.py index 6478c5282..744a40139 100644 --- a/tests/test_queryset.py +++ b/tests/test_queryset.py @@ -478,6 +478,7 @@ async def test_delete_limit_order_by(db, intfields_data): with pytest.raises(DoesNotExist): await IntFields.get(intnum=97) + @pytest.mark.asyncio async def test_delete_filter_with_foreign_key(db): author = await Author.create(name="test")