#32554: Add Q.empty(), Q.TRUE, Q.FALSE, Q.any(), and Q.all()
-------------------------------------+-------------------------------------
     Reporter:  jonathan-golorry     |                    Owner:  jonathan-
                                     |  golorry
         Type:  New feature          |                   Status:  closed
    Component:  Database layer       |                  Version:  dev
  (models, ORM)                      |
     Severity:  Normal               |               Resolution:  wontfix
     Keywords:  Q objects, any, all  |             Triage Stage:
                                     |  Unreviewed
    Has patch:  1                    |      Needs documentation:  1
  Needs tests:  0                    |  Patch needs improvement:  0
Easy pickings:  0                    |                    UI/UX:  0
-------------------------------------+-------------------------------------

Comment (by jonathan-golorry):

 Replying to [comment:4 Mariusz Felisiak]:
 > Thanks for this ticket, however I have mixed-feelings. Django API is
 already massive. IMO new hooks will make `Q` expressions more confusing
 and are not necessary per se. `Q.any()` and `Q.all()` can be replaced with
 `reduce()` (that's how we do this in Django itself, see
 
[https://github.com/django/django/blob/7c08f26bf0439c1ed593b51b51ad847f7e262bc1/django/contrib/admin/options.py#L1032
 an example]). I don't see why the new API should be preferred over an
 example with `reduce()` in
 [https://docs.djangoproject.com/en/dev/topics/db/queries/#complex-lookups-
 with-q-objects docs]. It has already been
 [https://github.com/django/django/pull/13798#issuecomment-767492096
 pointed out] that we need a good example of building `Q()` dynamically.

 `reduce` isn't great. A common mistake I see in accepted answers on stack
 overflow is that people forget to address empty iterators (potentially
 leaking data due to empty `Q()` objects).
 https://stackoverflow.com/questions/13076822/django-dynamically-filtering-
 with-q-objects
 https://stackoverflow.com/questions/852414/how-to-dynamically-compose-an-
 or-query-filter-in-django

 `reduce` turns that into a TypeError, which is better than leaking data,
 but handling the case properly is still a pain. It's hard to explain why
 some initializers work as defaults and others don't.
 {{{
 reduce(operator.and_, iterator, initializer=~Q(pk__in=[])  # "all",
 defaulting to everything
 reduce(operator.or_, iterator, initializer=Q(pk__in=[])    # "any",
 defaulting to nothing
 reduce(operator.and_, iterator, initializer=Q(pk__in=[])   # always
 returns nothing
 reduce(operator.or_, iterator, initializer=~Q(pk__in=[])   # always
 returns everything
 }}}
 That's ignoring the absolute foot-gun that is `initializer=Q()`. You need
 to add logically meaningless expressions to make the queries safe.
 {{{
 reduce(operator.and_, iterator, initializer=Q()) | Q(pk__in=[])  # "all",
 defaulting to nothing
 reduce(operator.or_, iterator, initializer=Q()) & ~Q(pk__in=[])  # "or",
 defaulting to everything
 }}}

 Using manual checks to avoid needing initializer isn't great because of
 edge cases around `bool(Q(Q()))` and `iterator=[Q()]`.

 > About `Q.TRUE` and `Q.FALSE`, these constants are really tricky and
 niche. I don't believe that folks will not how to use them properly (I
 don't see immediate and wide use). Moreover, I'm not sure why Django
 should recommend the `pk__in=[]` lookup (and not e.g. `pk=-1`), everything
 depends on the context. It is so niche that folks should choose on their
 own.

 The main argument for having `Q.TRUE` and `Q.FALSE` is to make the above
 code examples more readable. The logic of ANDing with TRUE is already
 weird enough that it's easy to miss a `~`.

 The query optimizer can handle `pk__in=[]`, not the others.
 {{{
 Article.objects.filter(Q(pk__in=[]))  # doesn't hit DB
 Article.objects.filter(Q(pk=None))    # hits DB and returns nothing
 Article.objects.none()                # doesn't hit DB
 Article.objects.filter(Q())           # returns everything
 }}}

 > I strongly believe that we need a good example of building `Q()`
 dynamically with the existing API, rather than adding yet another method
 to do the same.

 I think empty `Q()` objects are a significant security risk. The code
 required to handle them properly is unintuitive and difficult to read. If
 we just documented a good example, it would need to sufficiently explain
 all these risks.

-- 
Ticket URL: <https://code.djangoproject.com/ticket/32554#comment:5>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.

-- 
You received this message because you are subscribed to the Google Groups 
"Django updates" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To view this discussion on the web visit 
https://groups.google.com/d/msgid/django-updates/074.a7b4ab419b4d8365f8182f66cfd1a8b7%40djangoproject.com.

Reply via email to