fix(queryset): use subquery for DELETE/UPDATE filtering by related fields#2139
fix(queryset): use subquery for DELETE/UPDATE filtering by related fields#2139noy-solvin wants to merge 4 commits into
Conversation
|
|
||
| # 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]) |
There was a problem hiding this comment.
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
| 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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
|
|
||
| @pytest.mark.asyncio | ||
| async def test_delete_filter_with_foreign_key(db): | ||
| author = await Author.create(name="test") |
There was a problem hiding this comment.
Would be better to create one more author and verify it not deleted
|
|
||
| @pytest.mark.asyncio | ||
| async def test_update_filter_with_foreign_key(db): | ||
| author = await Author.create(name="test") |
There was a problem hiding this comment.
Create more author and book and check it update the right one
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: