Skip to content

fix(queryset): use subquery for DELETE/UPDATE filtering by related fields#2139

Open
noy-solvin wants to merge 3 commits intotortoise:developfrom
noy-solvin:fix/queryset
Open

fix(queryset): use subquery for DELETE/UPDATE filtering by related fields#2139
noy-solvin wants to merge 3 commits intotortoise:developfrom
noy-solvin:fix/queryset

Conversation

@noy-solvin
Copy link

Description

Modified DeleteQuery._make_query() and UpdateQuery._make_query() in tortoise/queryset.py to use a subquery pattern - WHERE id IN (SELECT id FROM (SELECT id FROM table JOIN ... WHERE ...) AS _t).
Also preserved LIMIT and ORDER BY clauses within the internal subquery.

Motivation and Context

DELETE and UPDATE queries were failing when filtering by related fields (foreign keys) because the engine was trying to use JOINs, which MySQL and SQLite don't support for these operations.
closes #283

How Has This Been Tested?

Added test_delete_filter_with_foreign_key and test_update_filter_with_foreign_key to the test suite.
Verified that the full regression suite (1899 tests) passes. The fix was also tested manually.

Full transparency: this fix was generated using Solvin, an AI coding agent my team is building. Reviewed and tested manually before submitting. I'd love your feedback. The fix was fully tested manually by me prior to submitting this PR.

Checklist:

  • My code follows the code style of this project.
  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • I have added the changelog accordingly.
  • I have read the CONTRIBUTING document.
  • I have added tests to cover my changes.
  • All new and existing tests passed.

@codspeed-hq
Copy link

codspeed-hq bot commented Mar 12, 2026

Merging this PR will not alter performance

✅ 24 untouched benchmarks


Comparing noy-solvin:fix/queryset (b453065) with develop (5e65d83)

Open in CodSpeed


# 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])
Copy link
Member

Choose a reason for hiding this comment

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

We should use this hack only where it is neccessary, you can do somthing like this:

  if self.capabilities.dialect == "mysql":
      # MySQL Error 1093: wrap in an extra SELECT to decouple target table reference
      final = self._db.query_class.from_(subquery.as_("_t")).select(Table("_t")[pk_column])
  else:
      # PostgreSQL, SQLite, etc. can use the subquery directly
      final = subquery

Comment on lines +1307 to +1315
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
Copy link
Member

Choose a reason for hiding this comment

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

Is there reason why you was able to cleanly make subquery based on query in delete, but wasn't able to do it in update?
Also - regarding "hasattr" calls - is there any case where there are no such fields on query? As far as I see - they are always set in init

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

book = await Book.first()
assert book.rating == 1.0
Copy link
Member

Choose a reason for hiding this comment

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

Let's add some test, that tests behavior in cases where there are limit and order by clauses, to see that subquery will update/delete only items that fall into subquery

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Delete with related field query fails due to invalid SQL generation

3 participants