Opened 3 weeks ago
Closed 9 days ago
#37432 closed Bug (fixed)
QuerySet.select_for_update() after union() and friends is silently ignored
| Reported by: | Alex Henman | Owned by: | Alex Henman |
|---|---|---|---|
| Component: | Database layer (models, ORM) | Version: | dev |
| Severity: | Normal | Keywords: | |
| Cc: | Triage Stage: | Ready for checkin | |
| Has patch: | yes | Needs documentation: | no |
| Needs tests: | no | Patch needs improvement: | no |
| Easy pickings: | no | UI/UX: | no |
Description
select_for_update() doesn't raise an error when called after union(), intersection() or difference(). Instead, the locking is silently dropped:
- the generated SQL contains no FOR UPDATE clause, so no rows are locked;
- the "select_for_update cannot be used outside of a transaction."
TransactionManagementErroris never raised, because the compiler only performs that check for non-combined queries.
For example, on PostgreSQL:
from django.db import transaction qs = Number.objects.filter(num__lt=5) qs.select_for_update() # outside a transaction, as expected: TransactionManagementError: select_for_update cannot be used outside of a transaction. qs.union(qs).select_for_update() # no error, query runs <QuerySet [<Number: 0>, <Number: 4>, <Number: 3>, <Number: 2>, <Number: 1>]> with transaction.atomic(): print(qs.union(qs).select_for_update().query) # NOTE: there's no FOR UPDATE (SELECT "queries_number"."id" AS "col1", "queries_number"."num" AS "col2", "queries_number"."other_num" AS "col3", "queries_number"."another_num" AS "col4" FROM "queries_number" WHERE "queries_number"."num" < 5) UNION (SELECT "queries_number"."id" AS "col1", "queries_number"."num" AS "col2", "queries_number"."other_num" AS "col3", "queries_number"."another_num" AS "col4" FROM "queries_number" WHERE "queries_number"."num" < 5)
The union() docs say that only slicing, count(), exists(), order_by(), and values()/values_list() are allowed on the resulting QuerySet. Other unsupported methods already raise NotSupportedError ("Calling QuerySet.filter() after union() is not supported.") following #27995, #31148, and #31580, but select_for_update was never added to that list. PostgreSQL rejects FOR UPDATE on UNION/INTERSECT/EXCEPT and their inputs, and Oracle and SQLite don’t support it either. So raising NotSupportedError like the other methods seems the right fix. I think that’s a one-line self._not_support_combined_queries("select_for_update") in QuerySet.select_for_update().
Here's an example failing test based on the others in test_qs_combinators:
def test_select_for_update_unsupported_on_combined_qs(self): qs = Number.objects.all() msg = "Calling QuerySet.select_for_update() after %s() is not supported." combinators = ["union"] if connection.features.supports_select_difference: combinators.append("difference") if connection.features.supports_select_intersection: combinators.append("intersection") for combinator in combinators: with self.subTest(combinator=combinator): with self.assertRaisesMessage(NotSupportedError, msg % combinator): getattr(qs, combinator)(qs).select_for_update()
I had a hunt for any other methods that might be missing this check and the only one flagged up was the internal complex_filter. I'm not really familiar with this method but it would be simple enough to add the same check there if it's considered necessary.
Happy to create the patch as outlined above if that seems like a sensible fix.
Change History (9)
comment:1 by , 3 weeks ago
| Has patch: | set |
|---|
comment:2 by , 3 weeks ago
| Has patch: | unset |
|---|---|
| Triage Stage: | Unreviewed → Accepted |
HimanshuVishwakarma please avoid eagerly submitting LLM generated patch ripping away reporter work while there is clearly still an open discussion about how to proceed, it might be well intended but it's not helping at all.
Hello Alex,
Your report and proposed solution makes sense, please assign the ticket to you and submit a PR for it. FWIW I've confirmed that the other ways that select_for_update() and combination methods can interact are already raising
qs.select_for_update().union(other_qs)qs.union(other_qs.select_for_update())
comment:3 by , 3 weeks ago
| Has patch: | set |
|---|
comment:4 by , 3 weeks ago
| Owner: | set to |
|---|---|
| Status: | new → assigned |
comment:5 by , 3 weeks ago
| Has patch: | unset |
|---|---|
| Owner: | changed from to |
Same comment made to HimanshuVishwakarma applies to to you Chethan; give the reporter a moment to claim the ticket before poaching it from them.
I'll assign the ticket to you Alex to prevent further drive by poaching, feel free to de-assign if you'd rather someone else address the problem.
comment:6 by , 2 weeks ago
| Has patch: | set |
|---|
comment:8 by , 2 weeks ago
| Triage Stage: | Accepted → Ready for checkin |
|---|
Hi everyone,
I have implemented a fix to raise NotSupportedError when select_for_update() is called after query combinators like union(), intersection(), and difference(), preventing the locking clause from being silently dropped.
I've also added unit tests covering these scenarios.
PR: https://github.com/django/django/pull/22160