Author: ramiro Date: 2011-12-22 12:42:40 -0800 (Thu, 22 Dec 2011) New Revision: 17244
Added: django/trunk/tests/modeltests/distinct_on_fields/ django/trunk/tests/modeltests/distinct_on_fields/__init__.py django/trunk/tests/modeltests/distinct_on_fields/models.py django/trunk/tests/modeltests/distinct_on_fields/tests.py Modified: django/trunk/AUTHORS django/trunk/django/db/backends/__init__.py django/trunk/django/db/backends/postgresql_psycopg2/base.py django/trunk/django/db/backends/postgresql_psycopg2/operations.py django/trunk/django/db/models/query.py django/trunk/django/db/models/sql/compiler.py django/trunk/django/db/models/sql/query.py django/trunk/docs/ref/models/querysets.txt django/trunk/docs/releases/1.4-alpha-1.txt django/trunk/docs/releases/1.4.txt django/trunk/tests/regressiontests/queries/models.py django/trunk/tests/regressiontests/queries/tests.py django/trunk/tests/regressiontests/select_related_regress/tests.py Log: Added support for modifying the effect of ``DISTINCT`` clauses so they only consider some fields (PostgreSQL only). For this, the ``distinct()`` QuerySet method now accepts an optional list of model fields names and generates ``DISTINCT ON`` clauses on these cases. Thanks Jeffrey Gelens and Anssi K?\195?\164?\195?\164ri?\195?\164inen for their work. Fixes #6422. Modified: django/trunk/AUTHORS =================================================================== --- django/trunk/AUTHORS 2011-12-22 20:29:18 UTC (rev 17243) +++ django/trunk/AUTHORS 2011-12-22 20:42:40 UTC (rev 17244) @@ -203,6 +203,7 @@ Marc Garcia <marc.gar...@accopensys.com> Andy Gayton <andy-dja...@thecablelounge.com> ge...@datacollect.com + Jeffrey Gelens <jeff...@gelens.org> Baishampayan Ghose Joshua Ginsberg <j...@flowtheory.net> Dimitris Glezos <dimit...@glezos.com> @@ -269,6 +270,7 @@ jpelle...@gmail.com junzhang...@gmail.com Xia Kai <http://blog.xiaket.org/> + Anssi Kääriäinen Antti Kaihola <http://djangopeople.net/akaihola/> Peter van Kampen Bahadır Kandemir <baha...@pardus.org.tr> Modified: django/trunk/django/db/backends/__init__.py =================================================================== --- django/trunk/django/db/backends/__init__.py 2011-12-22 20:29:18 UTC (rev 17243) +++ django/trunk/django/db/backends/__init__.py 2011-12-22 20:42:40 UTC (rev 17244) @@ -406,6 +406,9 @@ supports_stddev = None can_introspect_foreign_keys = None + # Support for the DISTINCT ON clause + can_distinct_on_fields = False + def __init__(self, connection): self.connection = connection @@ -559,6 +562,17 @@ """ raise NotImplementedError('Full-text search is not implemented for this database backend') + def distinct_sql(self, fields): + """ + Returns an SQL DISTINCT clause which removes duplicate rows from the + result set. If any fields are given, only the given fields are being + checked for duplicates. + """ + if fields: + raise NotImplementedError('DISTINCT ON fields is not supported by this database backend') + else: + return 'DISTINCT' + def last_executed_query(self, cursor, sql, params): """ Returns a string of the query last executed by the given cursor, with Modified: django/trunk/django/db/backends/postgresql_psycopg2/base.py =================================================================== --- django/trunk/django/db/backends/postgresql_psycopg2/base.py 2011-12-22 20:29:18 UTC (rev 17243) +++ django/trunk/django/db/backends/postgresql_psycopg2/base.py 2011-12-22 20:42:40 UTC (rev 17244) @@ -82,6 +82,7 @@ has_select_for_update_nowait = True has_bulk_insert = True supports_tablespaces = True + can_distinct_on_fields = True class DatabaseWrapper(BaseDatabaseWrapper): vendor = 'postgresql' Modified: django/trunk/django/db/backends/postgresql_psycopg2/operations.py =================================================================== --- django/trunk/django/db/backends/postgresql_psycopg2/operations.py 2011-12-22 20:29:18 UTC (rev 17243) +++ django/trunk/django/db/backends/postgresql_psycopg2/operations.py 2011-12-22 20:42:40 UTC (rev 17244) @@ -179,6 +179,12 @@ return 63 + def distinct_sql(self, fields): + if fields: + return 'DISTINCT ON (%s)' % ', '.join(fields) + else: + return 'DISTINCT' + def last_executed_query(self, cursor, sql, params): # http://initd.org/psycopg/docs/cursor.html#cursor.query # The query attribute is a Psycopg extension to the DB API 2.0. Modified: django/trunk/django/db/models/query.py =================================================================== --- django/trunk/django/db/models/query.py 2011-12-22 20:29:18 UTC (rev 17243) +++ django/trunk/django/db/models/query.py 2011-12-22 20:42:40 UTC (rev 17244) @@ -323,6 +323,8 @@ If args is present the expression is passed as a kwarg using the Aggregate object's default alias. """ + if self.query.distinct_fields: + raise NotImplementedError("aggregate() + distinct(fields) not implemented.") for arg in args: kwargs[arg.default_alias] = arg @@ -751,12 +753,14 @@ obj.query.add_ordering(*field_names) return obj - def distinct(self, true_or_false=True): + def distinct(self, *field_names): """ Returns a new QuerySet instance that will select only distinct results. """ + assert self.query.can_filter(), \ + "Cannot create distinct fields once a slice has been taken." obj = self._clone() - obj.query.distinct = true_or_false + obj.query.add_distinct_fields(*field_names) return obj def extra(self, select=None, where=None, params=None, tables=None, @@ -1179,7 +1183,7 @@ """ return self - def distinct(self, true_or_false=True): + def distinct(self, fields=None): """ Always returns EmptyQuerySet. """ Modified: django/trunk/django/db/models/sql/compiler.py =================================================================== --- django/trunk/django/db/models/sql/compiler.py 2011-12-22 20:29:18 UTC (rev 17243) +++ django/trunk/django/db/models/sql/compiler.py 2011-12-22 20:42:40 UTC (rev 17244) @@ -23,6 +23,8 @@ Does any necessary class setup immediately prior to producing SQL. This is for things that can't necessarily be done in __init__ because we might not have all the pieces in place at that time. + # TODO: after the query has been executed, the altered state should be + # cleaned. We are not using a clone() of the query here. """ if not self.query.tables: self.query.join((None, self.query.model._meta.db_table, None, None)) @@ -60,11 +62,19 @@ return '', () self.pre_sql_setup() + # After executing the query, we must get rid of any joins the query + # setup created. So, take note of alias counts before the query ran. + # However we do not want to get rid of stuff done in pre_sql_setup(), + # as the pre_sql_setup will modify query state in a way that forbids + # another run of it. + self.refcounts_before = self.query.alias_refcount.copy() out_cols = self.get_columns(with_col_aliases) ordering, ordering_group_by = self.get_ordering() - # This must come after 'select' and 'ordering' -- see docstring of - # get_from_clause() for details. + distinct_fields = self.get_distinct() + + # This must come after 'select', 'ordering' and 'distinct' -- see + # docstring of get_from_clause() for details. from_, f_params = self.get_from_clause() qn = self.quote_name_unless_alias @@ -76,8 +86,10 @@ params.extend(val[1]) result = ['SELECT'] + if self.query.distinct: - result.append('DISTINCT') + result.append(self.connection.ops.distinct_sql(distinct_fields)) + result.append(', '.join(out_cols + self.query.ordering_aliases)) result.append('FROM') @@ -90,6 +102,9 @@ grouping, gb_params = self.get_grouping() if grouping: + if distinct_fields: + raise NotImplementedError( + "annotate() + distinct(fields) not implemented.") if ordering: # If the backend can't group by PK (i.e., any database # other than MySQL), then any fields mentioned in the @@ -129,6 +144,9 @@ raise DatabaseError('NOWAIT is not supported on this database backend.') result.append(self.connection.ops.for_update_sql(nowait=nowait)) + # Finally do cleanup - get rid of the joins we created above. + self.query.reset_refcounts(self.refcounts_before) + return ' '.join(result), tuple(params) def as_nested_sql(self): @@ -292,6 +310,26 @@ col_aliases.add(field.column) return result, aliases + def get_distinct(self): + """ + Returns a quoted list of fields to use in DISTINCT ON part of the query. + + Note that this method can alter the tables in the query, and thus it + must be called before get_from_clause(). + """ + qn = self.quote_name_unless_alias + qn2 = self.connection.ops.quote_name + result = [] + opts = self.query.model._meta + + for name in self.query.distinct_fields: + parts = name.split(LOOKUP_SEP) + field, col, alias, _, _ = self._setup_joins(parts, opts, None) + col, alias = self._final_join_removal(col, alias) + result.append("%s.%s" % (qn(alias), qn2(col))) + return result + + def get_ordering(self): """ Returns a tuple containing a list representing the SQL elements in the @@ -384,22 +422,8 @@ """ name, order = get_order_dir(name, default_order) pieces = name.split(LOOKUP_SEP) - if not alias: - alias = self.query.get_initial_alias() - field, target, opts, joins, last, extra = self.query.setup_joins(pieces, - opts, alias, False) - alias = joins[-1] - col = target.column - if not field.rel: - # To avoid inadvertent trimming of a necessary alias, use the - # refcount to show that we are referencing a non-relation field on - # the model. - self.query.ref_alias(alias) + field, col, alias, joins, opts = self._setup_joins(pieces, opts, alias) - # Must use left outer joins for nullable fields and their relations. - self.query.promote_alias_chain(joins, - self.query.alias_map[joins[0]][JOIN_TYPE] == self.query.LOUTER) - # If we get to this point and the field is a relation to another model, # append the default ordering for that model. if field.rel and len(joins) > 1 and opts.ordering: @@ -416,11 +440,47 @@ results.extend(self.find_ordering_name(item, opts, alias, order, already_seen)) return results + col, alias = self._final_join_removal(col, alias) + return [(alias, col, order)] + def _setup_joins(self, pieces, opts, alias): + """ + A helper method for get_ordering and get_distinct. This method will + call query.setup_joins, handle refcounts and then promote the joins. + + Note that get_ordering and get_distinct must produce same target + columns on same input, as the prefixes of get_ordering and get_distinct + must match. Executing SQL where this is not true is an error. + """ + if not alias: + alias = self.query.get_initial_alias() + field, target, opts, joins, _, _ = self.query.setup_joins(pieces, + opts, alias, False) + alias = joins[-1] + col = target.column + if not field.rel: + # To avoid inadvertent trimming of a necessary alias, use the + # refcount to show that we are referencing a non-relation field on + # the model. + self.query.ref_alias(alias) + + # Must use left outer joins for nullable fields and their relations. + # Ordering or distinct must not affect the returned set, and INNER + # JOINS for nullable fields could do this. + self.query.promote_alias_chain(joins, + self.query.alias_map[joins[0]][JOIN_TYPE] == self.query.LOUTER) + return field, col, alias, joins, opts + + def _final_join_removal(self, col, alias): + """ + A helper method for get_distinct and get_ordering. This method will + trim extra not-needed joins from the tail of the join chain. + + This is very similar to what is done in trim_joins, but we will + trim LEFT JOINS here. It would be a good idea to consolidate this + method and query.trim_joins(). + """ if alias: - # We have to do the same "final join" optimisation as in - # add_filter, since the final column might not otherwise be part of - # the select set (so we can't order on it). while 1: join = self.query.alias_map[alias] if col != join[RHS_JOIN_COL]: @@ -428,7 +488,7 @@ self.query.unref_alias(alias) alias = join[LHS_ALIAS] col = join[LHS_JOIN_COL] - return [(alias, col, order)] + return col, alias def get_from_clause(self): """ @@ -438,8 +498,8 @@ from-clause via a "select". This should only be called after any SQL construction methods that - might change the tables we need. This means the select columns and - ordering must be done first. + might change the tables we need. This means the select columns, + ordering and distinct must be done first. """ result = [] qn = self.quote_name_unless_alias @@ -984,6 +1044,7 @@ """ if qn is None: qn = self.quote_name_unless_alias + sql = ('SELECT %s FROM (%s) subquery' % ( ', '.join([ aggregate.as_sql(qn, self.connection) Modified: django/trunk/django/db/models/sql/query.py =================================================================== --- django/trunk/django/db/models/sql/query.py 2011-12-22 20:29:18 UTC (rev 17243) +++ django/trunk/django/db/models/sql/query.py 2011-12-22 20:42:40 UTC (rev 17244) @@ -127,6 +127,7 @@ self.order_by = [] self.low_mark, self.high_mark = 0, None # Used for offset/limit self.distinct = False + self.distinct_fields = [] self.select_for_update = False self.select_for_update_nowait = False self.select_related = False @@ -265,6 +266,7 @@ obj.order_by = self.order_by[:] obj.low_mark, obj.high_mark = self.low_mark, self.high_mark obj.distinct = self.distinct + obj.distinct_fields = self.distinct_fields[:] obj.select_for_update = self.select_for_update obj.select_for_update_nowait = self.select_for_update_nowait obj.select_related = self.select_related @@ -298,6 +300,7 @@ else: obj.used_aliases = set() obj.filter_is_sticky = False + obj.__dict__.update(kwargs) if hasattr(obj, '_setup_query'): obj._setup_query() @@ -393,7 +396,7 @@ Performs a COUNT() query using the current filter constraints. """ obj = self.clone() - if len(self.select) > 1 or self.aggregate_select: + if len(self.select) > 1 or self.aggregate_select or (self.distinct and self.distinct_fields): # If a select clause exists, then the query has already started to # specify the columns that are to be returned. # In this case, we need to use a subquery to evaluate the count. @@ -452,6 +455,8 @@ "Cannot combine queries once a slice has been taken." assert self.distinct == rhs.distinct, \ "Cannot combine a unique query with a non-unique query." + assert self.distinct_fields == rhs.distinct_fields, \ + "Cannot combine queries with different distinct fields." self.remove_inherited_models() # Work out how to relabel the rhs aliases, if necessary. @@ -674,9 +679,9 @@ """ Increases the reference count for this alias. """ self.alias_refcount[alias] += 1 - def unref_alias(self, alias): + def unref_alias(self, alias, amount=1): """ Decreases the reference count for this alias. """ - self.alias_refcount[alias] -= 1 + self.alias_refcount[alias] -= amount def promote_alias(self, alias, unconditional=False): """ @@ -705,6 +710,15 @@ if self.promote_alias(alias, must_promote): must_promote = True + def reset_refcounts(self, to_counts): + """ + This method will reset reference counts for aliases so that they match + the value passed in :param to_counts:. + """ + for alias, cur_refcount in self.alias_refcount.copy().items(): + unref_amount = cur_refcount - to_counts.get(alias, 0) + self.unref_alias(alias, unref_amount) + def promote_unused_aliases(self, initial_refcounts, used_aliases): """ Given a "before" copy of the alias_refcounts dictionary (as @@ -832,7 +846,8 @@ def count_active_tables(self): """ Returns the number of tables in this query with a non-zero reference - count. + count. Note that after execution, the reference counts are zeroed, so + tables added in compiler will not be seen by this method. """ return len([1 for count in self.alias_refcount.itervalues() if count]) @@ -1596,6 +1611,13 @@ self.select = [] self.select_fields = [] + def add_distinct_fields(self, *field_names): + """ + Adds and resolves the given fields to the query's "distinct on" clause. + """ + self.distinct_fields = field_names + self.distinct = True + def add_fields(self, field_names, allow_m2m=True): """ Adds the given (model) fields to the select set. The field names are Modified: django/trunk/docs/ref/models/querysets.txt =================================================================== --- django/trunk/docs/ref/models/querysets.txt 2011-12-22 20:29:18 UTC (rev 17243) +++ django/trunk/docs/ref/models/querysets.txt 2011-12-22 20:42:40 UTC (rev 17244) @@ -345,7 +345,7 @@ distinct ~~~~~~~~ -.. method:: distinct() +.. method:: distinct([*fields]) Returns a new ``QuerySet`` that uses ``SELECT DISTINCT`` in its SQL query. This eliminates duplicate rows from the query results. @@ -374,6 +374,43 @@ :meth:`values()` together, be careful when ordering by fields not in the :meth:`values()` call. +.. versionadded:: 1.4 + +The possibility to pass positional arguments (``*fields``) is new in Django 1.4. +They are names of fields to which the ``DISTINCT`` should be limited. This +translates to a ``SELECT DISTINCT ON`` SQL query. A ``DISTINCT ON`` query eliminates +duplicate rows not by comparing all fields in a row, but by comparing only the given +fields. + +.. note:: + Note that the ability to specify field names is only available in PostgreSQL. + +.. note:: + When using the ``DISTINCT ON`` functionality it is required that the columns given + to :meth:`distinct` match the first :meth:`order_by` columns. For example ``SELECT + DISTINCT ON (a)`` gives you the first row for each value in column ``a``. If you + don't specify an order, then you'll get some arbitrary row. + +Examples:: + + >>> Author.objects.distinct() + [...] + + >>> Entry.objects.order_by('pub_date').distinct('pub_date') + [...] + + >>> Entry.objects.order_by('blog').distinct('blog') + [...] + + >>> Entry.objects.order_by('author', 'pub_date').distinct('author', 'pub_date') + [...] + + >>> Entry.objects.order_by('blog__name', 'mod_date').distinct('blog__name', 'mod_date') + [...] + + >>> Entry.objects.order_by('author', 'pub_date').distinct('author') + [...] + values ~~~~~~ Modified: django/trunk/docs/releases/1.4-alpha-1.txt =================================================================== --- django/trunk/docs/releases/1.4-alpha-1.txt 2011-12-22 20:29:18 UTC (rev 17243) +++ django/trunk/docs/releases/1.4-alpha-1.txt 2011-12-22 20:42:40 UTC (rev 17244) @@ -507,6 +507,16 @@ ``pickle.HIGHEST_PROTOCOL`` for better compatibility with the other cache backends. +* Support in the ORM for generating ``SELECT`` queries containing ``DISTINCT ON`` + + The ``distinct()`` ``Queryset`` method now accepts an optional list of model + field names. If specified, then the ``DISTINCT`` statement is limited to these + fields. The PostgreSQL is the only of the database backends shipped with + Django that supports this new functionality. + + For more details, see the documentation for + :meth:`~django.db.models.query.QuerySet.distinct`. + Backwards incompatible changes in 1.4 ===================================== Modified: django/trunk/docs/releases/1.4.txt =================================================================== --- django/trunk/docs/releases/1.4.txt 2011-12-22 20:29:18 UTC (rev 17243) +++ django/trunk/docs/releases/1.4.txt 2011-12-22 20:42:40 UTC (rev 17244) @@ -498,6 +498,16 @@ ``pickle.HIGHEST_PROTOCOL`` for better compatibility with the other cache backends. +* Support in the ORM for generating ``SELECT`` queries containing ``DISTINCT ON`` + + The ``distinct()`` ``Queryset`` method now accepts an optional list of model + field names. If specified, then the ``DISTINCT`` statement is limited to these + fields. The PostgreSQL is the only of the database backends shipped with + Django that supports this new functionality. + + For more details, see the documentation for + :meth:`~django.db.models.query.QuerySet.distinct`. + .. _backwards-incompatible-changes-1.4: Backwards incompatible changes in 1.4 Added: django/trunk/tests/modeltests/distinct_on_fields/__init__.py =================================================================== --- django/trunk/tests/modeltests/distinct_on_fields/__init__.py (rev 0) +++ django/trunk/tests/modeltests/distinct_on_fields/__init__.py 2011-12-22 20:42:40 UTC (rev 17244) @@ -0,0 +1 @@ +# Added: django/trunk/tests/modeltests/distinct_on_fields/models.py =================================================================== --- django/trunk/tests/modeltests/distinct_on_fields/models.py (rev 0) +++ django/trunk/tests/modeltests/distinct_on_fields/models.py 2011-12-22 20:42:40 UTC (rev 17244) @@ -0,0 +1,39 @@ +from django.db import models + +class Tag(models.Model): + name = models.CharField(max_length=10) + parent = models.ForeignKey('self', blank=True, null=True, + related_name='children') + + class Meta: + ordering = ['name'] + + def __unicode__(self): + return self.name + +class Celebrity(models.Model): + name = models.CharField("Name", max_length=20) + greatest_fan = models.ForeignKey("Fan", null=True, unique=True) + + def __unicode__(self): + return self.name + +class Fan(models.Model): + fan_of = models.ForeignKey(Celebrity) + +class Staff(models.Model): + id = models.IntegerField(primary_key=True) + name = models.CharField(max_length=50) + organisation = models.CharField(max_length=100) + tags = models.ManyToManyField(Tag, through='StaffTag') + coworkers = models.ManyToManyField('self') + + def __unicode__(self): + return self.name + +class StaffTag(models.Model): + staff = models.ForeignKey(Staff) + tag = models.ForeignKey(Tag) + + def __unicode__(self): + return u"%s -> %s" % (self.tag, self.staff) Added: django/trunk/tests/modeltests/distinct_on_fields/tests.py =================================================================== --- django/trunk/tests/modeltests/distinct_on_fields/tests.py (rev 0) +++ django/trunk/tests/modeltests/distinct_on_fields/tests.py 2011-12-22 20:42:40 UTC (rev 17244) @@ -0,0 +1,116 @@ +from __future__ import absolute_import, with_statement + +from django.db.models import Max +from django.test import TestCase, skipUnlessDBFeature + +from .models import Tag, Celebrity, Fan, Staff, StaffTag + +class DistinctOnTests(TestCase): + def setUp(self): + t1 = Tag.objects.create(name='t1') + t2 = Tag.objects.create(name='t2', parent=t1) + t3 = Tag.objects.create(name='t3', parent=t1) + t4 = Tag.objects.create(name='t4', parent=t3) + t5 = Tag.objects.create(name='t5', parent=t3) + + p1_o1 = Staff.objects.create(id=1, name="p1", organisation="o1") + p2_o1 = Staff.objects.create(id=2, name="p2", organisation="o1") + p3_o1 = Staff.objects.create(id=3, name="p3", organisation="o1") + p1_o2 = Staff.objects.create(id=4, name="p1", organisation="o2") + p1_o1.coworkers.add(p2_o1, p3_o1) + StaffTag.objects.create(staff=p1_o1, tag=t1) + StaffTag.objects.create(staff=p1_o1, tag=t1) + + celeb1 = Celebrity.objects.create(name="c1") + celeb2 = Celebrity.objects.create(name="c2") + + self.fan1 = Fan.objects.create(fan_of=celeb1) + self.fan2 = Fan.objects.create(fan_of=celeb1) + self.fan3 = Fan.objects.create(fan_of=celeb2) + + @skipUnlessDBFeature('can_distinct_on_fields') + def test_basic_distinct_on(self): + """QuerySet.distinct('field', ...) works""" + # (qset, expected) tuples + qsets = ( + ( + Staff.objects.distinct().order_by('name'), + ['<Staff: p1>', '<Staff: p1>', '<Staff: p2>', '<Staff: p3>'], + ), + ( + Staff.objects.distinct('name').order_by('name'), + ['<Staff: p1>', '<Staff: p2>', '<Staff: p3>'], + ), + ( + Staff.objects.distinct('organisation').order_by('organisation', 'name'), + ['<Staff: p1>', '<Staff: p1>'], + ), + ( + Staff.objects.distinct('name', 'organisation').order_by('name', 'organisation'), + ['<Staff: p1>', '<Staff: p1>', '<Staff: p2>', '<Staff: p3>'], + ), + ( + Celebrity.objects.filter(fan__in=[self.fan1, self.fan2, self.fan3]).\ + distinct('name').order_by('name'), + ['<Celebrity: c1>', '<Celebrity: c2>'], + ), + # Does combining querysets work? + ( + (Celebrity.objects.filter(fan__in=[self.fan1, self.fan2]).\ + distinct('name').order_by('name') + |Celebrity.objects.filter(fan__in=[self.fan3]).\ + distinct('name').order_by('name')), + ['<Celebrity: c1>', '<Celebrity: c2>'], + ), + ( + StaffTag.objects.distinct('staff','tag'), + ['<StaffTag: t1 -> p1>'], + ), + ( + Tag.objects.order_by('parent__pk', 'pk').distinct('parent'), + ['<Tag: t2>', '<Tag: t4>', '<Tag: t1>'], + ), + ( + StaffTag.objects.select_related('staff').distinct('staff__name').order_by('staff__name'), + ['<StaffTag: t1 -> p1>'], + ), + # Fetch the alphabetically first coworker for each worker + ( + (Staff.objects.distinct('id').order_by('id', 'coworkers__name'). + values_list('id', 'coworkers__name')), + ["(1, u'p2')", "(2, u'p1')", "(3, u'p1')", "(4, None)"] + ), + ) + for qset, expected in qsets: + self.assertQuerysetEqual(qset, expected) + self.assertEqual(qset.count(), len(expected)) + + # Combining queries with different distinct_fields is not allowed. + base_qs = Celebrity.objects.all() + self.assertRaisesMessage( + AssertionError, + "Cannot combine queries with different distinct fields.", + lambda: (base_qs.distinct('id') & base_qs.distinct('name')) + ) + + # Test join unreffing + c1 = Celebrity.objects.distinct('greatest_fan__id', 'greatest_fan__fan_of') + self.assertIn('OUTER JOIN', str(c1.query)) + c2 = c1.distinct('pk') + self.assertNotIn('OUTER JOIN', str(c2.query)) + + @skipUnlessDBFeature('can_distinct_on_fields') + def test_distinct_not_implemented_checks(self): + # distinct + annotate not allowed + with self.assertRaises(NotImplementedError): + Celebrity.objects.annotate(Max('id')).distinct('id')[0] + with self.assertRaises(NotImplementedError): + Celebrity.objects.distinct('id').annotate(Max('id'))[0] + + # However this check is done only when the query executes, so you + # can use distinct() to remove the fields before execution. + Celebrity.objects.distinct('id').annotate(Max('id')).distinct()[0] + # distinct + aggregate not allowed + with self.assertRaises(NotImplementedError): + Celebrity.objects.distinct('id').aggregate(Max('id')) + Modified: django/trunk/tests/regressiontests/queries/models.py =================================================================== --- django/trunk/tests/regressiontests/queries/models.py 2011-12-22 20:29:18 UTC (rev 17243) +++ django/trunk/tests/regressiontests/queries/models.py 2011-12-22 20:42:40 UTC (rev 17244) @@ -209,6 +209,9 @@ name = models.CharField("Name", max_length=20) greatest_fan = models.ForeignKey("Fan", null=True, unique=True) + def __unicode__(self): + return self.name + class TvChef(Celebrity): pass @@ -343,4 +346,3 @@ def __unicode__(self): return "one2one " + self.new_name - Modified: django/trunk/tests/regressiontests/queries/tests.py =================================================================== --- django/trunk/tests/regressiontests/queries/tests.py 2011-12-22 20:29:18 UTC (rev 17243) +++ django/trunk/tests/regressiontests/queries/tests.py 2011-12-22 20:42:40 UTC (rev 17244) @@ -234,18 +234,22 @@ ['<Item: four>', '<Item: one>'] ) - # FIXME: This is difficult to fix and very much an edge case, so punt for - # now. This is related to the order_by() tests for ticket #2253, 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). - @unittest.expectedFailure def test_order_by_tables(self): q1 = Item.objects.order_by('name') q2 = Item.objects.filter(id=self.i1.id) list(q2) self.assertEqual(len((q1 & q2).order_by('name').query.tables), 1) + def test_order_by_join_unref(self): + """ + This test is related to the above one, testing that there aren't + old JOINs in the query. + """ + qs = Celebrity.objects.order_by('greatest_fan__fan_of') + self.assertIn('OUTER JOIN', str(qs.query)) + qs = qs.order_by('id') + self.assertNotIn('OUTER JOIN', str(qs.query)) + def test_tickets_4088_4306(self): self.assertQuerysetEqual( Report.objects.filter(creator=1001), @@ -1728,7 +1732,7 @@ class ConditionalTests(BaseQuerysetTest): - """Tests whose execution depend on dfferent environment conditions like + """Tests whose execution depend on different environment conditions like Python version or DB backend features""" def setUp(self): @@ -1739,6 +1743,7 @@ t4 = Tag.objects.create(name='t4', parent=t3) t5 = Tag.objects.create(name='t5', parent=t3) + # In Python 2.6 beta releases, exceptions raised in __len__ are swallowed # (Python issue 1242657), so these cases return an empty list, rather than # raising an exception. Not a lot we can do about that, unfortunately, due to @@ -1810,6 +1815,7 @@ 2500 ) + class UnionTests(unittest.TestCase): """ Tests for the union of two querysets. Bug #12252. Modified: django/trunk/tests/regressiontests/select_related_regress/tests.py =================================================================== --- django/trunk/tests/regressiontests/select_related_regress/tests.py 2011-12-22 20:29:18 UTC (rev 17243) +++ django/trunk/tests/regressiontests/select_related_regress/tests.py 2011-12-22 20:42:40 UTC (rev 17244) @@ -40,9 +40,9 @@ self.assertEqual([(c.id, unicode(c.start), unicode(c.end)) for c in connections], [(c1.id, u'router/4', u'switch/7'), (c2.id, u'switch/7', u'server/1')]) - # This final query should only join seven tables (port, device and building - # twice each, plus connection once). - self.assertEqual(connections.query.count_active_tables(), 7) + # This final query should only have seven tables (port, device and building + # twice each, plus connection once). Thus, 6 joins plus the FROM table. + self.assertEqual(str(connections.query).count(" JOIN "), 6) def test_regression_8106(self): -- You received this message because you are subscribed to the Google Groups "Django updates" group. To post to this group, send email to django-updates@googlegroups.com. To unsubscribe from this group, send email to django-updates+unsubscr...@googlegroups.com. For more options, visit this group at http://groups.google.com/group/django-updates?hl=en.