Ticket #6422: distinct_on.2.diff

File distinct_on.2.diff, 11.9 KB (added by Taylor Mitchell, 13 years ago)

Add (failing) test for M2M intermediate model

  • django/db/backends/__init__.py

    diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py
    index b64fb01..ef301b7 100644
    a b class BaseDatabaseFeatures(object):  
    342342    supports_stddev = None
    343343    can_introspect_foreign_keys = None
    344344
     345    # Support for the DISTINCT ON clause
     346    can_distinct_on_fields = False
     347
    345348    def __init__(self, connection):
    346349        self.connection = connection
    347350
    class BaseDatabaseOperations(object):  
    495498        """
    496499        raise NotImplementedError('Full-text search is not implemented for this database backend')
    497500
     501    def distinct(self, db_table, fields):
     502        """
     503        Returns an SQL DISTINCT clause which removes duplicate rows from the
     504        result set. If any fields are given, only the given fields are being
     505        checked for duplicates.
     506        """
     507        if fields:
     508            raise NotImplementedError('DISTINCT ON fields is not supported by this database backend')
     509        else:
     510            return 'DISTINCT'
     511
    498512    def last_executed_query(self, cursor, sql, params):
    499513        """
    500514        Returns a string of the query last executed by the given cursor, with
  • django/db/backends/postgresql_psycopg2/base.py

    diff --git a/django/db/backends/postgresql_psycopg2/base.py b/django/db/backends/postgresql_psycopg2/base.py
    index 67e2877..db7acc5 100644
    a b class DatabaseFeatures(BaseDatabaseFeatures):  
    7272    can_defer_constraint_checks = True
    7373    has_select_for_update = True
    7474    has_select_for_update_nowait = True
     75    can_distinct_on_fields = True
    7576
    7677
    7778class DatabaseWrapper(BaseDatabaseWrapper):
  • django/db/backends/postgresql_psycopg2/operations.py

    diff --git a/django/db/backends/postgresql_psycopg2/operations.py b/django/db/backends/postgresql_psycopg2/operations.py
    index 3315913..e24b0aa 100644
    a b class DatabaseOperations(BaseDatabaseOperations):  
    201201
    202202        return 63
    203203
     204    def distinct(self, db_table, fields):
     205        if fields:
     206            table_name = self.quote_name(db_table)
     207            fields = [table_name + "." + self.quote_name(field) for field in fields]
     208            return 'DISTINCT ON (%s)' % ', '.join(fields)
     209        else:
     210            return 'DISTINCT'
     211
    204212    def last_executed_query(self, cursor, sql, params):
    205213        # http://initd.org/psycopg/docs/cursor.html#cursor.query
    206214        # The query attribute is a Psycopg extension to the DB API 2.0.
  • django/db/models/query.py

    diff --git a/django/db/models/query.py b/django/db/models/query.py
    index 6a6a829..9e90c9a 100644
    a b class QuerySet(object):  
    668668        obj.query.add_ordering(*field_names)
    669669        return obj
    670670
    671     def distinct(self, true_or_false=True):
     671    def distinct(self, *field_names):
    672672        """
    673673        Returns a new QuerySet instance that will select only distinct results.
    674674        """
    675675        obj = self._clone()
    676         obj.query.distinct = true_or_false
     676        obj.query.add_distinct_fields(field_names)
     677        obj.query.distinct = True
     678
    677679        return obj
    678680
    679681    def extra(self, select=None, where=None, params=None, tables=None,
    class EmptyQuerySet(QuerySet):  
    10931095        """
    10941096        return self
    10951097
    1096     def distinct(self, true_or_false=True):
     1098    def distinct(self, fields=None):
    10971099        """
    10981100        Always returns EmptyQuerySet.
    10991101        """
  • django/db/models/sql/compiler.py

    diff --git a/django/db/models/sql/compiler.py b/django/db/models/sql/compiler.py
    index 841ec12..9d22f4a 100644
    a b class SQLCompiler(object):  
    7474            params.extend(val[1])
    7575
    7676        result = ['SELECT']
     77
    7778        if self.query.distinct:
    78             result.append('DISTINCT')
     79            distinct_sql = self.connection.ops.distinct(
     80                self.query.model._meta.db_table, self.query.distinct_fields)
     81            result.append(distinct_sql)
     82
    7983        result.append(', '.join(out_cols + self.query.ordering_aliases))
    8084
    8185        result.append('FROM')
  • django/db/models/sql/query.py

    diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py
    index 99663b6..662fc25 100644
    a b class Query(object):  
    125125        self.order_by = []
    126126        self.low_mark, self.high_mark = 0, None  # Used for offset/limit
    127127        self.distinct = False
     128        self.distinct_fields = None
    128129        self.select_for_update = False
    129130        self.select_for_update_nowait = False
    130131        self.select_related = False
    class Query(object):  
    256257        obj.order_by = self.order_by[:]
    257258        obj.low_mark, obj.high_mark = self.low_mark, self.high_mark
    258259        obj.distinct = self.distinct
     260        obj.distinct_fields = self.distinct_fields
    259261        obj.select_for_update = self.select_for_update
    260262        obj.select_for_update_nowait = self.select_for_update_nowait
    261263        obj.select_related = self.select_related
    class Query(object):  
    384386        Performs a COUNT() query using the current filter constraints.
    385387        """
    386388        obj = self.clone()
    387         if len(self.select) > 1 or self.aggregate_select:
     389        if len(self.select) > 1 or self.aggregate_select or (self.distinct and self.distinct_fields):
    388390            # If a select clause exists, then the query has already started to
    389391            # specify the columns that are to be returned.
    390392            # In this case, we need to use a subquery to evaluate the count.
    class Query(object):  
    15561558        self.select = []
    15571559        self.select_fields = []
    15581560
     1561    def add_distinct_fields(self, field_names):
     1562        self.distinct_fields = []
     1563        opts = self.get_meta()
     1564
     1565        for name in field_names:
     1566            field, source, opts, join_list, last, _ = self.setup_joins(
     1567                name.split(LOOKUP_SEP), opts, self.get_initial_alias(), False)
     1568            self.distinct_fields.append(field.column)
     1569
    15591570    def add_fields(self, field_names, allow_m2m=True):
    15601571        """
    15611572        Adds the given (model) fields to the select set. The field names are
  • docs/ref/models/querysets.txt

    diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt
    index 2bd813d..9172569 100644
    a b Though you usually won't create one manually -- you'll go through a  
    139139        clause or a default ordering on the model. ``False`` otherwise.
    140140
    141141    .. attribute:: db
    142    
     142
    143143        The database that will be used if this query is executed now.
    144144
    145145    .. note::
    undefined afterward).  
    345345distinct
    346346~~~~~~~~
    347347
    348 .. method:: distinct()
     348.. method:: distinct(*fields)
    349349
    350350Returns a new ``QuerySet`` that uses ``SELECT DISTINCT`` in its SQL query. This
    351351eliminates duplicate rows from the query results.
    don't introduce the possibility of duplicate result rows. However, if your  
    356356query spans multiple tables, it's possible to get duplicate results when a
    357357``QuerySet`` is evaluated. That's when you'd use ``distinct()``.
    358358
     359.. versionadded:: 1.4
     360   ``distinct()`` takes optional positional arguments, ``*fields``, which specify
     361   field names to which the ``DISTINCT`` should be limited. This translates to
     362   a ``SELECT DISTINCT ON`` SQL query. Note that this ``DISTINCT ON`` query is
     363   only available in PostgreSQL.
     364
    359365.. note::
    360366    Any fields used in an :meth:`order_by` call are included in the SQL
    361367    ``SELECT`` columns. This can sometimes lead to unexpected results when
  • tests/regressiontests/queries/models.py

    diff --git a/tests/regressiontests/queries/models.py b/tests/regressiontests/queries/models.py
    index d1e5e6e..9cf3a09 100644
    a b class Celebrity(models.Model):  
    208208    name = models.CharField("Name", max_length=20)
    209209    greatest_fan = models.ForeignKey("Fan", null=True, unique=True)
    210210
     211    def __unicode__(self):
     212        return self.name
     213
    211214class TvChef(Celebrity):
    212215    pass
    213216
    class ObjectC(models.Model):  
    317320
    318321    def __unicode__(self):
    319322       return self.name
     323
     324
     325class Staff(models.Model):
     326    name = models.CharField(max_length=50)
     327    organisation = models.CharField(max_length=100)
     328    tags = models.ManyToManyField(Tag, through='StaffTag')
     329
     330    def __unicode__(self):
     331        return self.name
     332
     333class StaffTag(models.Model):
     334    staff = models.ForeignKey(Staff)
     335    tag = models.ForeignKey(Tag)
     336
     337    def __unicode__(self):
     338        return u"%s -> %s" % (self.tag, self.staff)
     339
  • tests/regressiontests/queries/tests.py

    diff --git a/tests/regressiontests/queries/tests.py b/tests/regressiontests/queries/tests.py
    index 31856ba..619f755 100644
    a b from models import (Annotation, Article, Author, Celebrity, Child, Cover, Detail  
    1515    DumbCategory, ExtraInfo, Fan, Item, LeafA, LoopX, LoopZ, ManagedModel,
    1616    Member, NamedCategory, Note, Number, Plaything, PointerA, Ranking, Related,
    1717    Report, ReservedName, Tag, TvChef, Valid, X, Food, Eaten, Node, ObjectA, ObjectB,
    18     ObjectC)
     18    ObjectC, Staff, StaffTag)
    1919
    2020
    2121class BaseQuerysetTest(TestCase):
    class ConditionalTests(BaseQuerysetTest):  
    16061606        t4 = Tag.objects.create(name='t4', parent=t3)
    16071607        t5 = Tag.objects.create(name='t5', parent=t3)
    16081608
     1609        p1_o1 = Staff.objects.create(name="p1", organisation="o1")
     1610        p2_o1 = Staff.objects.create(name="p2", organisation="o1")
     1611        p3_o1 = Staff.objects.create(name="p3", organisation="o1")
     1612        p1_o2 = Staff.objects.create(name="p1", organisation="o2")
     1613
     1614        StaffTag.objects.create(staff=p1_o1, tag=t1)
     1615        StaffTag.objects.create(staff=p1_o1, tag=t1)
     1616
     1617        celeb1 = Celebrity.objects.create(name="c1")
     1618        celeb2 = Celebrity.objects.create(name="c2")
     1619
     1620        self.fan1 = Fan.objects.create(fan_of=celeb1)
     1621        self.fan2 = Fan.objects.create(fan_of=celeb1)
     1622        self.fan3 = Fan.objects.create(fan_of=celeb2)
     1623
    16091624    # In Python 2.6 beta releases, exceptions raised in __len__ are swallowed
    16101625    # (Python issue 1242657), so these cases return an empty list, rather than
    16111626    # raising an exception. Not a lot we can do about that, unfortunately, due to
    class ConditionalTests(BaseQuerysetTest):  
    16771692            2500
    16781693        )
    16791694
     1695    @skipUnlessDBFeature('can_distinct_on_fields')
     1696    def test_ticket6422(self):
     1697        # (qset, expected) tuples
     1698        qsets = (
     1699            (
     1700                Staff.objects.distinct().order_by('name'),
     1701                ['<Staff: p1>', '<Staff: p1>', '<Staff: p2>', '<Staff: p3>'],
     1702            ),
     1703            (
     1704                Staff.objects.distinct('name').order_by('name'),
     1705                ['<Staff: p1>', '<Staff: p2>', '<Staff: p3>'],
     1706            ),
     1707            (
     1708                Staff.objects.distinct('organisation').order_by('organisation', 'name'),
     1709                ['<Staff: p1>', '<Staff: p1>'],
     1710            ),
     1711            (
     1712                Staff.objects.distinct('name', 'organisation').order_by('name', 'organisation'),
     1713                ['<Staff: p1>', '<Staff: p1>', '<Staff: p2>', '<Staff: p3>'],
     1714            ),
     1715            (
     1716                Celebrity.objects.filter(fan__in=[self.fan1, self.fan2, self.fan3]).\
     1717                    distinct('name').order_by('name'),
     1718                ['<Celebrity: c1>', '<Celebrity: c2>'],
     1719            ),
     1720            (
     1721                StaffTag.objects.distinct('staff','tag'),
     1722                ['<StaffTag: t1 -> p1>'],
     1723            ),
     1724        )
     1725
     1726        for qset, expected in qsets:
     1727            self.assertQuerysetEqual(qset, expected)
     1728            self.assertEqual(qset.count(), len(expected))
     1729
     1730        # and check the fieldlookup
     1731        self.assertRaises(
     1732            FieldError,
     1733            lambda: Staff.objects.distinct('shrubbery')
     1734        )
     1735
     1736
    16801737class UnionTests(unittest.TestCase):
    16811738    """
    16821739    Tests for the union of two querysets. Bug #12252.
Back to Top