I just ran into #34603 (~Q() incorrectly interpreted as full rather than empty) – Django again today. The issue is closed as “wontfix” and I have a hard time understanding the benefits of the current behavior. It leads to subtle bugs in the (~100k LOC) application I’m working on from time to time. There are workarounds, but these need to be remembered by members of the team to be applied in every case.
The behavior I’m talking about, in short, is this. Given:
q1 = Q(field=1)
q2 = Q()
qs.filter(q1)
will return the inverse ofqs.filter(~q1)
.qs.filter(q2)
will not return the inverse ofqs.filter(~q2)
. Instead, it will return the same set of rows.
There are a lot of places where we construct Q
objects incrementally, often passing them as arguments to functions. We often start with an “empty” Q()
object (for example from a default argument of a wrapper function), adding and removing subsets of data using q |= Q(...)
and q &= ~Q(...)
. When looking at the code, it’s not always clear which Q
object might be the empty Q
object in some cases and which can’t. The issue is that code using a Q
object might only work correctly when that Q
object is never the empty Q
object.
I never came across a situation where the current behavior lead to the expected result, only situations where the it resulted in a bug (e.g. returning all rows instead of no rows after removing all permissions from an access token).
Examples that I have seen of code that behaves unexpectedly:
# Start with no selected rows.
q = ~Q()
if condition_a:
q |= Q(...)
if condition_b:
q |= Q(...)
# condition_a and condition_b should dictate which rows to include.
# Instead, when all conditions are False, all rows are included.
qs.filter(q)
qs.filter(q1 & ~q2)
# Refactoring the above code to this will subtly break it:
qs.filter(q1).exclude(~q2)
# Each condition selects a set of rows. We want to return the union of all those rows.
conditions = [Q(...) for i in ...]
# When the list of conditions is empty, all rows are returned instead.
qs.filter(reduce(operator.or_, conditions, Q())
Has anyone of you ever found the current behavior useful or expected?
Background
Q() objects states:
A
Q()
object represents an SQL condition that can be used in database-related operations.
Conditions in SQL follow boolean logic, so I’d expect Q
objects to do the same. I mean, imagine a = not (foo and bar)
working correctly but a = not True
resulting in True
…