#36388 closed Bug (fixed)
QuerySet.union no longer supports empty args
| Reported by: | Antoine Humeau | Owned by: | Colleen Dunlap |
|---|---|---|---|
| Component: | Database layer (models, ORM) | Version: | 5.2 |
| Severity: | Release blocker | Keywords: | |
| Cc: | Simon Charette | 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
Hello there,
Up until Django 5.1 (included), it was possible to use the union queryset method with an empty arg list, example:
User.objects.union()
This example is obviously useless but the following pattern would be equivalent and would be more reallistic:
def unite_querysets(querysets):
queryset, *other_querysets = querysets
queryset = queryset.union(*other_querysets)
Since Django 5.2, that triggers the following exception:
AttributeError: 'NoneType' object has no attribute 'elide_empty'
The line that triggers the exception is the following: https://github.com/django/django/blob/5.2.1/django/db/models/sql/compiler.py#L616
empty_compiler is never initialized.
I am not sure this really qualifies as a bug, it might be a unsupported usage, still it is a regression.
Change History (17)
comment:1 by , 6 months ago
| Cc: | added |
|---|
comment:2 by , 6 months ago
| Component: | Uncategorized → Database layer (models, ORM) |
|---|---|
| Severity: | Normal → Release blocker |
| Triage Stage: | Unreviewed → Accepted |
Marking as a release blocker but willing to downgrade
comment:4 by , 6 months ago
Thanks for the report and triage folks. I think it's worth fixing by having QuerySet.union return self if not other_qs given it's a non-invasive patch and dealing with empty sequence in a different manner would be annoyance.
comment:5 by , 6 months ago
| Owner: | set to |
|---|---|
| Status: | new → assigned |
comment:6 by , 6 months ago
| Has patch: | set |
|---|
comment:7 by , 6 months ago
| Patch needs improvement: | set |
|---|
Left some comments on the PR, the proposed approach generates an unnecessary UNION against itself that causes the improper results to be returned when all is used as they are not deduped.
comment:8 by , 6 months ago
| Patch needs improvement: | unset |
|---|
Hi Simon! I responded. It's already returning self and it's still breaking. Also do I make my PR pointing to the 5.2 branch or main?
comment:9 by , 6 months ago
| Patch needs improvement: | set |
|---|
comment:10 by , 6 months ago
It should point at main which your new PR does, thank you for that.
Please have a look at this comment. The fact the tests are passing is not a good indicative that the right thing is happening here as the adjusted test I provided demonstrates (it does a UNION against itself which is not safe when all is used).
I still believe that the right way of fixing this is to adjust QuerySet.union to return self when not other_qs as the compilation layer is not equipped to deal with an empty combination of queries and it's just faster to avoid the query cloning if we can.
comment:11 by , 6 months ago
is that not what this is doing already? https://github.com/django/django/blob/0b2ed4f7c8396c8d9aa8428a40e6b25c31312889/django/db/models/query.py#L1548
comment:12 by , 6 months ago
ok nevermind I'm rereading this and it looks like I may have misinterpreted...
comment:13 by , 6 months ago
No, the branch you linked is gated by a if isinstance(self, EmptyQuerySet) and qs is a list of all non-empty querysets. Django is able to detect when a queryset cannot match any rows (the most common example is filter(pk=[])) and the logic you linked deals with this case.
What I mean is that if the *other_qs args capture is empty then you can immediately return as some_set OR *other_sets means some_set.
comment:15 by , 6 months ago
| Patch needs improvement: | unset |
|---|---|
| Triage Stage: | Accepted → Ready for checkin |
git bisect to 9cb8baa0c4fa2c10789c5c8b65f4465932d4d172
Possible regression test
tests/queries/test_qs_combinators.py
However, I agree that this usage isn't supported. Perhaps a breaking change note is enough here? We might want a nicer error also