""" Various complex queries that have been problematic in the past. """ import datetime from django.db import models from django.db.models.query import Q class Tag(models.Model): name = models.CharField(max_length=10) parent = models.ForeignKey('self', blank=True, null=True) def __unicode__(self): return self.name class Note(models.Model): note = models.CharField(max_length=100) misc = models.CharField(max_length=10) class Meta: ordering = ['note'] def __unicode__(self): return self.note class ExtraInfo(models.Model): info = models.CharField(max_length=100) note = models.ForeignKey(Note) class Meta: ordering = ['info'] def __unicode__(self): return self.info class Author(models.Model): name = models.CharField(max_length=10) num = models.IntegerField(unique=True) extra = models.ForeignKey(ExtraInfo) def __unicode__(self): return self.name class Item(models.Model): name = models.CharField(max_length=10) created = models.DateTimeField() tags = models.ManyToManyField(Tag, blank=True, null=True) creator = models.ForeignKey(Author) note = models.ForeignKey(Note) class Meta: ordering = ['-note', 'name'] def __unicode__(self): return self.name class Report(models.Model): name = models.CharField(max_length=10) creator = models.ForeignKey(Author, to_field='num') def __unicode__(self): return self.name class Ranking(models.Model): rank = models.IntegerField() author = models.ForeignKey(Author) class Meta: # A complex ordering specification. Should stress the system a bit. ordering = ('author__extra__note', 'author__name', 'rank') def __unicode__(self): return '%d: %s' % (self.rank, self.author.name) class Cover(models.Model): title = models.CharField(max_length=50) item = models.ForeignKey(Item) class Meta: ordering = ['item'] def __unicode__(self): return self.title class Number(models.Model): num = models.IntegerField() def __unicode__(self): return unicode(self.num) # Some funky cross-linked models for testing a couple of infinite recursion # cases. class X(models.Model): y = models.ForeignKey('Y') class Y(models.Model): x1 = models.ForeignKey(X, related_name='y1') # Some models with a cycle in the default ordering. This would be bad if we # didn't catch the infinite loop. class LoopX(models.Model): y = models.ForeignKey('LoopY') class Meta: ordering = ['y'] class LoopY(models.Model): x = models.ForeignKey(LoopX) class Meta: ordering = ['x'] __test__ = {'API_TESTS':""" >>> t1 = Tag(name='t1') >>> t1.save() >>> t2 = Tag(name='t2', parent=t1) >>> t2.save() >>> t3 = Tag(name='t3', parent=t1) >>> t3.save() >>> t4 = Tag(name='t4', parent=t3) >>> t4.save() >>> t5 = Tag(name='t5', parent=t3) >>> t5.save() >>> n1 = Note(note='n1', misc='foo') >>> n1.save() >>> n2 = Note(note='n2', misc='bar') >>> n2.save() >>> n3 = Note(note='n3', misc='foo') >>> n3.save() Create these out of order so that sorting by 'id' will be different to sorting by 'info'. Helps detect some problems later. >>> e2 = ExtraInfo(info='e2', note=n2) >>> e2.save() >>> e1 = ExtraInfo(info='e1', note=n1) >>> e1.save() >>> a1 = Author(name='a1', num=1001, extra=e1) >>> a1.save() >>> a2 = Author(name='a2', num=2002, extra=e1) >>> a2.save() >>> a3 = Author(name='a3', num=3003, extra=e2) >>> a3.save() >>> a4 = Author(name='a4', num=4004, extra=e2) >>> a4.save() >>> time1 = datetime.datetime(2007, 12, 19, 22, 25, 0) >>> time2 = datetime.datetime(2007, 12, 19, 21, 0, 0) >>> time3 = datetime.datetime(2007, 12, 20, 22, 25, 0) >>> time4 = datetime.datetime(2007, 12, 20, 21, 0, 0) >>> i1 = Item(name='one', created=time1, creator=a1, note=n3) >>> i1.save() >>> i1.tags = [t1, t2] >>> i2 = Item(name='two', created=time2, creator=a2, note=n2) >>> i2.save() >>> i2.tags = [t1, t3] >>> i3 = Item(name='three', created=time3, creator=a2, note=n3) >>> i3.save() >>> i4 = Item(name='four', created=time4, creator=a4, note=n3) >>> i4.save() >>> i4.tags = [t4] >>> r1 = Report(name='r1', creator=a1) >>> r1.save() >>> r2 = Report(name='r2', creator=a3) >>> r2.save() Ordering by 'rank' gives us rank2, rank1, rank3. Ordering by the Meta.ordering will be rank3, rank2, rank1. >>> rank1 = Ranking(rank=2, author=a2) >>> rank1.save() >>> rank2 = Ranking(rank=1, author=a3) >>> rank2.save() >>> rank3 = Ranking(rank=3, author=a1) >>> rank3.save() >>> c1 = Cover(title="first", item=i4) >>> c1.save() >>> c2 = Cover(title="second", item=i2) >>> c2.save() >>> n1 = Number(num=4) >>> n1.save() >>> n2 = Number(num=8) >>> n2.save() >>> n3 = Number(num=12) >>> n3.save() Bug #1050 >>> Item.objects.filter(tags__isnull=True) [] >>> Item.objects.filter(tags__id__isnull=True) [] Bug #1801 >>> Author.objects.filter(item=i2) [] >>> Author.objects.filter(item=i3) [] >>> Author.objects.filter(item=i2) & Author.objects.filter(item=i3) [] Bug #2306 Checking that no join types are "left outer" joins. >>> query = Item.objects.filter(tags=t2).query >>> query.LOUTER not in [x[2] for x in query.alias_map.values()] True >>> Item.objects.filter(Q(tags=t1)).order_by('name') [, ] >>> Item.objects.filter(Q(tags=t1)).filter(Q(tags=t2)) [] Each filter call is processed "at once" against a single table, so this is different from the previous example as it tries to find tags that are two things at once (rather than two tags). >>> Item.objects.filter(Q(tags=t1) & Q(tags=t2)) [] >>> qs = Author.objects.filter(ranking__rank=2, ranking__id=rank1.id) >>> list(qs) [] >>> qs.query.count_active_tables() 2 >>> qs = Author.objects.filter(ranking__rank=2).filter(ranking__id=rank1.id) >>> qs.query.count_active_tables() 3 Bug #4464 >>> Item.objects.filter(tags=t1).filter(tags=t2) [] >>> Item.objects.filter(tags__in=[t1, t2]).distinct().order_by('name') [, ] >>> Item.objects.filter(tags__in=[t1, t2]).filter(tags=t3) [] Bug #2080, #3592 >>> Author.objects.filter(item__name='one') | Author.objects.filter(name='a3') [, ] >>> Author.objects.filter(Q(item__name='one') | Q(name='a3')) [, ] >>> Author.objects.filter(Q(name='a3') | Q(item__name='one')) [, ] >>> Author.objects.filter(Q(item__name='three') | Q(report__name='r3')) [] Bug #4289 A slight variation on the above theme: restricting the choices by the lookup constraints. >>> Number.objects.filter(num__lt=4) [] >>> Number.objects.filter(num__gt=8, num__lt=12) [] >>> Number.objects.filter(num__gt=8, num__lt=13) [] >>> Number.objects.filter(Q(num__lt=4) | Q(num__gt=8, num__lt=12)) [] >>> Number.objects.filter(Q(num__gt=8, num__lt=12) | Q(num__lt=4)) [] >>> Number.objects.filter(Q(num__gt=8) & Q(num__lt=12) | Q(num__lt=4)) [] >>> Number.objects.filter(Q(num__gt=7) & Q(num__lt=12) | Q(num__lt=4)) [] Bug #6074 Merging two empty result sets shouldn't leave a queryset with no constraints (which would match everything). >>> Author.objects.filter(Q(id__in=[])) [] >>> Author.objects.filter(Q(id__in=[])|Q(id__in=[])) [] Bug #1878, #2939 >>> Item.objects.values('creator').distinct().count() 3 # Create something with a duplicate 'name' so that we can test multi-column # cases (which require some tricky SQL transformations under the covers). >>> xx = Item(name='four', created=time1, creator=a2, note=n1) >>> xx.save() >>> Item.objects.exclude(name='two').values('creator', 'name').distinct().count() 4 >>> xx.delete() Bug #2253 >>> q1 = Item.objects.order_by('name') >>> q2 = Item.objects.filter(id=i1.id) >>> q1 [, , , ] >>> q2 [] >>> (q1 | q2).order_by('name') [, , , ] >>> (q1 & q2).order_by('name') [] # FIXME: This is difficult to fix and very much an edge case, so punt for now. # # This is related to the order_by() tests, below, but the old bug exhibited # # itself here (q2 was pulling too many tables into the combined query with the # # new ordering, but only because we have evaluated q2 already). # >>> len((q1 & q2).order_by('name').query.tables) # 1 >>> q1 = Item.objects.filter(tags=t1) >>> q2 = Item.objects.filter(note=n3, tags=t2) >>> q3 = Item.objects.filter(creator=a4) >>> ((q1 & q2) | q3).order_by('name') [, ] Bugs #4088, #4306 >>> Report.objects.filter(creator=1001) [] >>> Report.objects.filter(creator__num=1001) [] >>> Report.objects.filter(creator__id=1001) [] >>> Report.objects.filter(creator__id=a1.id) [] >>> Report.objects.filter(creator__name='a1') [] Bug #4510 >>> Author.objects.filter(report__name='r1') [] Bug #5324, #6704 >>> Item.objects.filter(tags__name='t4') [] >>> Item.objects.exclude(tags__name='t4').order_by('name').distinct() [, , ] >>> Item.objects.exclude(tags__name='t4').order_by('name').distinct().reverse() [, , ] >>> Author.objects.exclude(item__name='one').distinct().order_by('name') [, , ] # Excluding across a m2m relation when there is more than one related object # associated was problematic. >>> Item.objects.exclude(tags__name='t1').order_by('name') [, ] >>> Item.objects.exclude(tags__name='t1').exclude(tags__name='t4') [] # Excluding from a relation that cannot be NULL should not use outer joins. >>> query = Item.objects.exclude(creator__in=[a1, a2]).query >>> query.LOUTER not in [x[2] for x in query.alias_map.values()] True Similarly, when one of the joins cannot possibly, ever, involve NULL values (Author -> ExtraInfo, in the following), it should never be promoted to a left outer join. So hte following query should only involve one "left outer" join (Author -> Item is 0-to-many). >>> qs = Author.objects.filter(id=a1.id).filter(Q(extra__note=n1)|Q(item__note=n3)) >>> len([x[2] for x in qs.query.alias_map.values() if x[2] == query.LOUTER]) 1 The previous changes shouldn't affect nullable foreign key joins. >>> Tag.objects.filter(parent__isnull=True).order_by('name') [] >>> Tag.objects.exclude(parent__isnull=True).order_by('name') [, , , ] >>> Tag.objects.exclude(Q(parent__name='t1') | Q(parent__isnull=True)).order_by('name') [, ] >>> Tag.objects.exclude(Q(parent__isnull=True) | Q(parent__name='t1')).order_by('name') [, ] >>> Tag.objects.exclude(Q(parent__parent__isnull=True)).order_by('name') [, ] >>> Tag.objects.filter(~Q(parent__parent__isnull=True)).order_by('name') [, ] Bug #2091 >>> t = Tag.objects.get(name='t4') >>> Item.objects.filter(tags__in=[t]) [] Combining querysets built on different models should behave in a well-defined fashion. We raise an error. >>> Author.objects.all() & Tag.objects.all() Traceback (most recent call last): ... AssertionError: Cannot combine queries on two different base models. >>> Author.objects.all() | Tag.objects.all() Traceback (most recent call last): ... AssertionError: Cannot combine queries on two different base models. Bug #3141 >>> Author.objects.extra(select={'foo': '1'}).count() 4 Bug #2400 >>> Author.objects.filter(item__isnull=True) [] >>> Tag.objects.filter(item__isnull=True) [] Bug #2496 >>> Item.objects.extra(tables=['queries_author']).select_related().order_by('name')[:1] [] Bug #2076 # Ordering on related tables should be possible, even if the table is not # otherwise involved. >>> Item.objects.order_by('note__note', 'name') [, , , ] # Ordering on a related field should use the remote model's default ordering as # a final step. >>> Author.objects.order_by('extra', '-name') [, , , ] # Using remote model default ordering can span multiple models (in this case, # Cover is ordered by Item's default, which uses Note's default). >>> Cover.objects.all() [, ] # If you're not careful, it's possible to introduce infinite loops via default # ordering on foreign keys in a cycle. We detect that. >>> LoopX.objects.all() Traceback (most recent call last): ... FieldError: Infinite loop caused by ordering. # ... but you can still order in a non-recursive fashion amongst linked fields # (the previous test failed because the default ordering was recursive). >>> LoopX.objects.all().order_by('y__x__id') [] # If the remote model does not have a default ordering, we order by its 'id' # field. >>> Item.objects.order_by('creator', 'name') [, , , ] # Cross model ordering is possible in Meta, too. >>> Ranking.objects.all() [, , ] >>> Ranking.objects.all().order_by('rank') [, , ] # Ordering by a many-valued attribute (e.g. a many-to-many or reverse # ForeignKey) is legal, but the results might not make sense. That isn't # Django's problem. Garbage in, garbage out. >>> Item.objects.all().order_by('tags', 'id') [, , , , ] # If we replace the default ordering, Django adjusts the required tables # automatically. Item normally requires a join with Note to do the default # ordering, but that isn't needed here. >>> qs = Item.objects.order_by('name') >>> qs [, , , ] >>> len(qs.query.tables) 1 # Ordering of extra() pieces is possible, too and you can mix extra fields and # model fields in the ordering. >>> Ranking.objects.extra(tables=['django_site'], order_by=['-django_site.id', 'rank']) [, , ] >>> qs = Ranking.objects.extra(select={'good': 'rank > 2'}) >>> [o.good for o in qs.extra(order_by=('-good',))] == [True, False, False] True >>> qs.extra(order_by=('-good', 'id')) [, , ] Bugs #2874, #3002 >>> qs = Item.objects.select_related().order_by('note__note', 'name') >>> list(qs) [, , , ] # This is also a good select_related() test because there are multiple Note # entries in the SQL. The two Note items should be different. >>> qs[0].note, qs[0].creator.extra.note (, ) Bug #3037 >>> Item.objects.filter(Q(creator__name='a3', name='two')|Q(creator__name='a4', name='four')) [] Bug #5321 >>> Note.objects.values('misc').distinct().order_by('note', '-misc') [{'misc': u'foo'}, {'misc': u'bar'}] Bug #4358 If you don't pass any fields to values(), relation fields are returned as "foo_id" keys, not "foo". For consistency, you should be able to pass "foo_id" in the fields list and have it work, too. We actually allow both "foo" and "foo_id". # The *_id version is returned by default. >>> 'note_id' in ExtraInfo.objects.values()[0] True # You can also pass it in explicitly. >>> ExtraInfo.objects.values('note_id') [{'note_id': 1}, {'note_id': 2}] # ...or use the field name. >>> ExtraInfo.objects.values('note') [{'note': 1}, {'note': 2}] Bug #5261 >>> Note.objects.exclude(Q()) [, , ] Bug #3045, #3288 Once upon a time, select_related() with circular relations would loop infinitely if you forgot to specify "depth". Now we set an arbitrary default upper bound. >>> X.objects.all() [] >>> X.objects.select_related() [] Bug #3739 The all() method on querysets returns a copy of the queryset. >>> q1 = Item.objects.order_by('name') >>> id(q1) == id(q1.all()) False Bug #2902 Parameters can be given to extra_select, *if* you use a SortedDict. (First we need to know which order the keys fall in "naturally" on your system, so we can put things in the wrong way around from normal. A normal dict would thus fail.) >>> from django.utils.datastructures import SortedDict >>> s = [('a', '%s'), ('b', '%s')] >>> params = ['one', 'two'] >>> if {'a': 1, 'b': 2}.keys() == ['a', 'b']: ... s.reverse() ... params.reverse() # This slightly odd comparison works aorund the fact that PostgreSQL will # return 'one' and 'two' as strings, not Unicode objects. It's a side-effect of # using constants here and not a real concern. >>> d = Item.objects.extra(select=SortedDict(s), params=params).values('a', 'b')[0] >>> d == {'a': u'one', 'b': u'two'} True # Order by the number of tags attached to an item. >>> l = Item.objects.extra(select={'count': 'select count(*) from queries_item_tags where queries_item_tags.item_id = queries_item.id'}).order_by('-count') >>> [o.count for o in l] [2, 2, 1, 0] Bug #6154 Multiple filter statements are joined using "AND" all the time. >>> Author.objects.filter(id=a1.id).filter(Q(extra__note=n1)|Q(item__note=n3)) [] >>> Author.objects.filter(Q(extra__note=n1)|Q(item__note=n3)).filter(id=a1.id) [] Bug #6180, #6203 >>> Item.objects.count() 4 >>> Item.objects.dates('created', 'month').count() 1 >>> Item.objects.dates('created', 'day').count() 2 >>> len(Item.objects.dates('created', 'day')) 2 >>> Item.objects.dates('created', 'day')[0] datetime.datetime(2007, 12, 19, 0, 0) Test that parallel iterators work. >>> qs = Tag.objects.all() >>> i1, i2 = iter(qs), iter(qs) >>> i1.next(), i1.next() (, ) >>> i2.next(), i2.next(), i2.next() (, , ) >>> i1.next() We can do slicing beyond what is currently in the result cache, too. # FIXME!! This next test causes really weird PostgreSQL behaviour, but it's # only apparent much later when the full test suite runs. I don't understand # what's going on here yet. # We need to mess with the implemenation internals a bit here to decrease the # cache fill size so that we don't read all the results at once. >>> from django.db.models import query >>> query.ITER_CHUNK_SIZE = 2 >>> qs = Tag.objects.all() # Fill the cache with the first chunk. >>> bool(qs) True >>> len(qs._result_cache) 2 # Query beyond the end of the cache and check that it is filled out as required. >>> qs[4] >>> len(qs._result_cache) 5 # But querying beyond the end of the result set will fail. >>> qs[100] Traceback (most recent call last): ... IndexError: ... """}