#24431: Combining extra(), annotate() or distinct() and count() can generate
invalid SQL
-------------------------------------+-------------------------------------
     Reporter:  briankrznarich       |                    Owner:  nobody
         Type:  Bug                  |                   Status:  new
    Component:  Database layer       |                  Version:  1.8beta1
  (models, ORM)                      |
     Severity:  Release blocker      |               Resolution:
     Keywords:  extra distinct       |             Triage Stage:  Accepted
  aggregate count annotate           |
    Has patch:  0                    |      Needs documentation:  0
  Needs tests:  0                    |  Patch needs improvement:  0
Easy pickings:  1                    |                    UI/UX:  0
-------------------------------------+-------------------------------------

Comment (by briankrznarich):

 Thanks for the quick response.  I've been poking at this trying to
 understand completely what's going on, and now I'm afraid that the
 examples I've given may simply have undefined results (which happened to
 work out in 1.7, but not 1.8).

 In both of my examples, I relied on select_related() to ensure that the
 tables I needed for the extra() call would be available.  However,
 select_related() does not make that guarantee.    I've found an even
 simpler example that fails(and others have historically noted similar
 problems):

 {{{#!python
 
Permission.objects.all().select_related('content_type').extra(select={'foo':"django_content_type.app_label"}).order_by().values('foo')
 }}}

 This fails with a SQL generation error, since values() drops the contents
 of select_related.   The empty .order_by() is needed to remove the default
 ordering of the Permission class, which references ContentType explicitly:
 {{{#!python
    class Meta:
         ordering = ('content_type__app_label', 'content_type__model',
                     'codename')
 }}}
 If I leave the default ordering in, the command succeeds, since the
 ordering forces the join to occur.

 Here the query works until values() is called.  The ValuesQuerySet class
 clearly references bug #3358 right before discarding select_related.  Bug
 #3358 says it's fixed, but the source code does not agree.
 {{{#!python
 class ValuesQuerySet(QuerySet):
     def __init__(self, *args, **kwargs):
         super(ValuesQuerySet, self).__init__(*args, **kwargs)
         # select_related isn't supported in values(). (FIXME -#3358)
         self.query.select_related = False

 }}}
 Commenting out the line simply leads to other problems.

 It seems the real issue is that it isn't obvious how to ensure that a
 required JOIN will actually occur for extra() if your queryset is subject
 to modification by code you did not write.  In our case, tastypie is free
 to append .count(), .distinct(), and .values() to whatever we might
 construct.   The one technique that seems to work is to apply a filter()
 against the needed table with a clause that is always true.  For example,
 in the Permission example above:
 {{{#!python
 
Permission.objects.all().filter(content_type__app_label__isnull=False).select_related('content_type').extra(select={'foo':"django_content_type.app_label"}).order_by().values('foo')
 }}}
 Here I've verified that a guaranteed NOT NULL field is, in fact, not null.
 Any decent database should manage that rather quickly.  This is the
 technique I have applied to our source code as a workaround.

 .order_by() is not even reliable for this purpose, as a distinct() applied
 later will discard the order_by clause.
 Counting works on this query:
 {{{#!python
 
Permission.objects.all().extra(select={'foo':"django_content_type.app_label"}).order_by('content_type__app_label').count()
 }}}
 Distinct works, no problem
 {{{#!python
 
Permission.objects.all().extra(select={'foo':"django_content_type.app_label"}).order_by('content_type__app_label').distinct()
 }}}
 However, distinct()+count() fails.  The distinct() is performed in a
 subselect with the ORDER BY clause discarded.  This is in the same block
 of code as my original ticket description, but the fix is not so straight
 forward.  PostgreSQL has no problem with ORDER BY in subselects, but other
 DBs certainly do (SQL Server, for instance).
 {{{#!python
 
Permission.objects.all().extra(select={'foo':"django_content_type.app_label"}).order_by('content_type__app_label').distinct().count()
 }}}

 I think the correct fix in the long term(if it is fixed, I know extra() is
 a little hacky to begin with) is for extra() to have a mechanism to
 specify the simple JOINs it requires.  That's not easy pickings anymore.
 In the interim, the filter() trick seems like the safe bet.  Perhaps the
 documentation could use some comments on the unreliability of order_by and
 select_related with regards to extra().

--
Ticket URL: <https://code.djangoproject.com/ticket/24431#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 post to this group, send email to [email protected].
To view this discussion on the web visit 
https://groups.google.com/d/msgid/django-updates/072.80981a6739ff133c61c88e6a904a04e4%40djangoproject.com.
For more options, visit https://groups.google.com/d/optout.

Reply via email to