Ticket #6422: distinct_on.8.diff

File distinct_on.8.diff, 12.0 KB (added by Ramiro Morales, 13 years ago)

Patch with docs and tests updates

  • AUTHORS

    diff --git a/AUTHORS b/AUTHORS
    a b  
    202202    Marc Garcia <marc.garcia@accopensys.com>
    203203    Andy Gayton <andy-django@thecablelounge.com>
    204204    geber@datacollect.com
     205    Jeffrey Gelens <jeffrey@gelens.org>
    205206    Baishampayan Ghose
    206207    Joshua Ginsberg <jag@flowtheory.net>
    207208    Dimitris Glezos <dimitris@glezos.com>
  • django/db/backends/__init__.py

    diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py
    a b  
    376376    supports_stddev = None
    377377    can_introspect_foreign_keys = None
    378378
     379    # Support for the DISTINCT ON clause
     380    can_distinct_on_fields = False
     381
    379382    def __init__(self, connection):
    380383        self.connection = connection
    381384
     
    529532        """
    530533        raise NotImplementedError('Full-text search is not implemented for this database backend')
    531534
     535    def distinct(self, fields):
     536        """
     537        Returns an SQL DISTINCT clause which removes duplicate rows from the
     538        result set. If any fields are given, only the given fields are being
     539        checked for duplicates.
     540        """
     541        if fields:
     542            raise NotImplementedError('DISTINCT ON fields is not supported by this database backend')
     543        else:
     544            return 'DISTINCT'
     545
    532546    def last_executed_query(self, cursor, sql, params):
    533547        """
    534548        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
    a b  
    7676    has_select_for_update_nowait = True
    7777    has_bulk_insert = True
    7878    supports_tablespaces = True
     79    can_distinct_on_fields = True
    7980
    8081class DatabaseWrapper(BaseDatabaseWrapper):
    8182    vendor = 'postgresql'
  • 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
    a b  
    179179
    180180        return 63
    181181
     182    def distinct(self, fields):
     183        if fields:
     184            fields_sql = []
     185
     186            for field in fields:
     187                fields_sql.append(
     188                    self.quote_name(field.model._meta.db_table) + "." + \
     189                    self.quote_name(field.column)
     190                )
     191
     192            return 'DISTINCT ON (%s)' % ', '.join(fields_sql)
     193        else:
     194            return 'DISTINCT'
     195
    182196    def last_executed_query(self, cursor, sql, params):
    183197        # http://initd.org/psycopg/docs/cursor.html#cursor.query
    184198        # 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
    a b  
    738738        obj.query.add_ordering(*field_names)
    739739        return obj
    740740
    741     def distinct(self, true_or_false=True):
     741    def distinct(self, *field_names):
    742742        """
    743743        Returns a new QuerySet instance that will select only distinct results.
    744744        """
    745745        obj = self._clone()
    746         obj.query.distinct = true_or_false
     746        obj.query.add_distinct_fields(field_names)
     747        obj.query.distinct = True
     748
    747749        return obj
    748750
    749751    def extra(self, select=None, where=None, params=None, tables=None,
     
    11661168        """
    11671169        return self
    11681170
    1169     def distinct(self, true_or_false=True):
     1171    def distinct(self, fields=None):
    11701172        """
    11711173        Always returns EmptyQuerySet.
    11721174        """
  • django/db/models/sql/compiler.py

    diff --git a/django/db/models/sql/compiler.py b/django/db/models/sql/compiler.py
    a b  
    7676            params.extend(val[1])
    7777
    7878        result = ['SELECT']
     79
    7980        if self.query.distinct:
    80             result.append('DISTINCT')
     81            result.append(self.connection.ops.distinct(self.query.distinct_fields))
     82
    8183        result.append(', '.join(out_cols + self.query.ordering_aliases))
    8284
    8385        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
    a b  
    126126        self.order_by = []
    127127        self.low_mark, self.high_mark = 0, None  # Used for offset/limit
    128128        self.distinct = False
     129        self.distinct_fields = None
    129130        self.select_for_update = False
    130131        self.select_for_update_nowait = False
    131132        self.select_related = False
     
    264265        obj.order_by = self.order_by[:]
    265266        obj.low_mark, obj.high_mark = self.low_mark, self.high_mark
    266267        obj.distinct = self.distinct
     268        obj.distinct_fields = self.distinct_fields
    267269        obj.select_for_update = self.select_for_update
    268270        obj.select_for_update_nowait = self.select_for_update_nowait
    269271        obj.select_related = self.select_related
     
    392394        Performs a COUNT() query using the current filter constraints.
    393395        """
    394396        obj = self.clone()
    395         if len(self.select) > 1 or self.aggregate_select:
     397        if len(self.select) > 1 or self.aggregate_select or (self.distinct and self.distinct_fields):
    396398            # If a select clause exists, then the query has already started to
    397399            # specify the columns that are to be returned.
    398400            # In this case, we need to use a subquery to evaluate the count.
     
    15951597        self.select = []
    15961598        self.select_fields = []
    15971599
     1600    def add_distinct_fields(self, field_names):
     1601        self.distinct_fields = []
     1602        options = self.get_meta()
     1603
     1604        for name in field_names:
     1605            field, source, opts, join_list, last, _ = self.setup_joins(
     1606                name.split(LOOKUP_SEP), options, self.get_initial_alias(), False)
     1607            self.distinct_fields.append(field)
     1608
    15981609    def add_fields(self, field_names, allow_m2m=True):
    15991610        """
    16001611        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
    a b  
    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.
     
    374374    :meth:`values()` together, be careful when ordering by fields not in the
    375375    :meth:`values()` call.
    376376
     377.. versionadded:: 1.4
     378
     379The possibility to pass positional arguments (``*fields``) is new in Django 1.4.
     380They are names of fields to which the ``DISTINCT`` should be limited. This
     381translates to a ``SELECT DISTINCT ON`` SQL query.
     382
     383.. note::
     384    Note that the ability to specify field names is only available in PostgreSQL.
     385
     386.. note::
     387    When fields names are given, you will have to add an :meth:`order_by`
     388    call with the same field names as the leftmost arguments.
     389
     390Examples::
     391
     392    >>> Author.objects.distinct()
     393    [...]
     394
     395    >>> Entry.objects.order_by('pub_date').distinct('pub_date')
     396    [...]
     397
     398    >>> Entry.objects.order_by('blog').distinct('blog')
     399    [...]
     400
     401    >>> Entry.objects.order_by('author', 'pub_date').distinct('author', 'pub_date')
     402    [...]
     403
     404    >>> Entry.objects.order_by('blog__name', 'mod_date').distinct('blog__name', 'mod_date')
     405    [...]
     406
     407    >>> Entry.objects.order_by('author', 'pub_date').distinct('author')
     408    [...]
     409
    377410values
    378411~~~~~~
    379412
  • tests/regressiontests/queries/models.py

    diff --git a/tests/regressiontests/queries/models.py b/tests/regressiontests/queries/models.py
    a b  
    209209    name = models.CharField("Name", max_length=20)
    210210    greatest_fan = models.ForeignKey("Fan", null=True, unique=True)
    211211
     212    def __unicode__(self):
     213        return self.name
     214
    212215class TvChef(Celebrity):
    213216    pass
    214217
     
    344347    def __unicode__(self):
    345348        return "one2one " + self.new_name
    346349
     350class Staff(models.Model):
     351    name = models.CharField(max_length=50)
     352    organisation = models.CharField(max_length=100)
     353    tags = models.ManyToManyField(Tag, through='StaffTag')
     354
     355    def __unicode__(self):
     356        return self.name
     357
     358class StaffTag(models.Model):
     359    staff = models.ForeignKey(Staff)
     360    tag = models.ForeignKey(Tag)
     361
     362    def __unicode__(self):
     363        return u"%s -> %s" % (self.tag, self.staff)
  • tests/regressiontests/queries/tests.py

    diff --git a/tests/regressiontests/queries/tests.py b/tests/regressiontests/queries/tests.py
    a b  
    1818    ManagedModel, Member, NamedCategory, Note, Number, Plaything, PointerA,
    1919    Ranking, Related, Report, ReservedName, Tag, TvChef, Valid, X, Food, Eaten,
    2020    Node, ObjectA, ObjectB, ObjectC, CategoryItem, SimpleCategory,
    21     SpecialCategory, OneToOneCategory)
     21    SpecialCategory, OneToOneCategory, Staff, StaffTag)
    2222
    2323
    2424class BaseQuerysetTest(TestCase):
     
    17391739        t4 = Tag.objects.create(name='t4', parent=t3)
    17401740        t5 = Tag.objects.create(name='t5', parent=t3)
    17411741
     1742        p1_o1 = Staff.objects.create(name="p1", organisation="o1")
     1743        p2_o1 = Staff.objects.create(name="p2", organisation="o1")
     1744        p3_o1 = Staff.objects.create(name="p3", organisation="o1")
     1745        p1_o2 = Staff.objects.create(name="p1", organisation="o2")
     1746
     1747        StaffTag.objects.create(staff=p1_o1, tag=t1)
     1748        StaffTag.objects.create(staff=p1_o1, tag=t1)
     1749
     1750        celeb1 = Celebrity.objects.create(name="c1")
     1751        celeb2 = Celebrity.objects.create(name="c2")
     1752
     1753        self.fan1 = Fan.objects.create(fan_of=celeb1)
     1754        self.fan2 = Fan.objects.create(fan_of=celeb1)
     1755        self.fan3 = Fan.objects.create(fan_of=celeb2)
     1756
    17421757    # In Python 2.6 beta releases, exceptions raised in __len__ are swallowed
    17431758    # (Python issue 1242657), so these cases return an empty list, rather than
    17441759    # raising an exception. Not a lot we can do about that, unfortunately, due to
     
    18101825            2500
    18111826        )
    18121827
     1828    @skipUnlessDBFeature('can_distinct_on_fields')
     1829    def test_ticket6422(self):
     1830        """QuerySet.distinct('field', ...) works"""
     1831        # (qset, expected) tuples
     1832        qsets = (
     1833            (
     1834                Staff.objects.distinct().order_by('name'),
     1835                ['<Staff: p1>', '<Staff: p1>', '<Staff: p2>', '<Staff: p3>'],
     1836            ),
     1837            (
     1838                Staff.objects.distinct('name').order_by('name'),
     1839                ['<Staff: p1>', '<Staff: p2>', '<Staff: p3>'],
     1840            ),
     1841            (
     1842                Staff.objects.distinct('organisation').order_by('organisation', 'name'),
     1843                ['<Staff: p1>', '<Staff: p1>'],
     1844            ),
     1845            (
     1846                Staff.objects.distinct('name', 'organisation').order_by('name', 'organisation'),
     1847                ['<Staff: p1>', '<Staff: p1>', '<Staff: p2>', '<Staff: p3>'],
     1848            ),
     1849            (
     1850                Celebrity.objects.filter(fan__in=[self.fan1, self.fan2, self.fan3]).\
     1851                    distinct('name').order_by('name'),
     1852                ['<Celebrity: c1>', '<Celebrity: c2>'],
     1853            ),
     1854            (
     1855                StaffTag.objects.distinct('staff','tag'),
     1856                ['<StaffTag: t1 -> p1>'],
     1857            ),
     1858            (
     1859                Tag.objects.order_by('parent__pk', 'pk').distinct('parent'),
     1860                ['<Tag: t2>', '<Tag: t4>', '<Tag: t1>'],
     1861            ),
     1862            (
     1863                StaffTag.objects.select_related('staff').distinct('staff__name').order_by('staff__name'),
     1864                ['<StaffTag: t1 -> p1>'],
     1865            ),
     1866        )
     1867
     1868        for qset, expected in qsets:
     1869            self.assertQuerysetEqual(qset, expected)
     1870            self.assertEqual(qset.count(), len(expected))
     1871
     1872        # and check the fieldlookup
     1873        self.assertRaises(
     1874            FieldError,
     1875            lambda: Staff.objects.distinct('shrubbery')
     1876        )
     1877
     1878
    18131879class UnionTests(unittest.TestCase):
    18141880    """
    18151881    Tests for the union of two querysets. Bug #12252.
Back to Top