Django

Code

Changeset 6510

Show
Ignore:
Timestamp:
10/14/07 17:38:54 (9 months ago)
Author:
mtredinnick
Message:

queryset-refactor: Fixed a large bag of order_by() problems.

This also picked up a small bug in some twisted select_related() handling.

Introduces a new syntax for cross-model ordering: foobarbaz, using field
names, instead of a strange combination of table names and field names. This
might turn out to be backwards compatible (although the old syntax leads to
bugs and is not to be recommended).

Still to come: fixes for extra() handling, since the new syntax can't handle
that and doc updates.

Things are starting to get a bit slow here, so we might eventually have to
remove ordering by many-many and other multiple-result fields, since they don't
make a lot of sense in any case. For now, it's legal.

Refs #2076, #2874, #3002 (although the admin bit doesn't work yet).

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • django/branches/queryset-refactor/django/core/management/validation.py

    r6339 r6510  
    191191                if opts.order_with_respect_to and field_name == '_order': 
    192192                    continue 
    193                 if '.' in field_name: continue # Skip ordering in the format 'table.field'. 
     193                # Skip ordering in the format field1__field2 (FIXME: checking 
     194                # this format would be nice, but it's a little fiddly). 
     195                if '_' in field_name: 
     196                    continue 
    194197                try: 
    195198                    opts.get_field(field_name, many_to_many=False) 
  • django/branches/queryset-refactor/django/db/models/sql/query.py

    r6504 r6510  
    99 
    1010import copy 
     11import re 
    1112 
    1213from django.utils import tree 
     
    3637LOOKUP_SEP = '__' 
    3738 
    38 # Constants to make looking up tuple values clearerer
     39# Constants to make looking up tuple values clearer
    3940# Join lists 
    4041TABLE_NAME = 0 
     
    4445LHS_JOIN_COL = 4 
    4546RHS_JOIN_COL = 5 
    46 # Alias maps lists 
     47# Alias map lists 
    4748ALIAS_TABLE = 0 
    4849ALIAS_REFCOUNT = 1 
     
    5455SINGLE = 'single' 
    5556NONE = None 
     57 
     58ORDER_PATTERN = re.compile(r'\?|[-+]?\w+$') 
    5659 
    5760class Query(object): 
     
    9295        self.extra_where = [] 
    9396        self.extra_params = [] 
     97        self.extra_order_by = [] 
    9498 
    9599    def __str__(self): 
     
    141145        obj.extra_where = self.extra_where[:] 
    142146        obj.extra_params = self.extra_params[:] 
     147        obj.extra_order_by = self.extra_order_by[:] 
    143148        obj.__dict__.update(kwargs) 
    144149        return obj 
     
    190195        """ 
    191196        self.pre_sql_setup() 
     197        out_cols = self.get_columns() 
     198        ordering = self.get_ordering() 
     199        # This must come after 'select' and 'ordering' -- see docstring of 
     200        # get_from_clause() for details. 
     201        from_, f_params = self.get_from_clause() 
     202        where, w_params = self.where.as_sql() 
     203 
    192204        result = ['SELECT'] 
    193205        if self.distinct: 
    194206            result.append('DISTINCT') 
    195         out_cols = self.get_columns() 
    196207        result.append(', '.join(out_cols)) 
    197208 
    198209        result.append('FROM') 
    199         from_, f_params = self.get_from_clause() 
    200210        result.extend(from_) 
    201211        params = list(f_params) 
    202212 
    203         where, w_params = self.where.as_sql() 
    204         params.extend(w_params) 
    205213        if where: 
    206214            result.append('WHERE %s' % where) 
     
    211219                result.append('AND') 
    212220            result.append(' AND'.join(self.extra_where)) 
     221        params.extend(w_params) 
    213222 
    214223        if self.group_by: 
     
    216225            result.append('GROUP BY %s' % ', '.join(grouping)) 
    217226 
    218         ordering = self.get_ordering() 
    219227        if ordering: 
    220228            result.append('ORDER BY %s' % ', '.join(ordering)) 
     
    313321        # the current ordering is used. 
    314322        self.order_by = rhs.order_by and rhs.order_by[:] or self.order_by 
     323        self.extra_order_by = (rhs.extra_order_by and rhs.extra_order_by[:] or 
     324                self.extra_order_by) 
    315325 
    316326    def pre_sql_setup(self): 
    317327        """ 
    318         Does any necessary class setup prior to producing SQL. This is for 
    319         things that can't necessarily be done in __init__. 
     328        Does any necessary class setup immediately prior to producing SQL. This 
     329        is for things that can't necessarily be done in __init__ because we 
     330        might not have all the pieces in place at that time. 
    320331        """ 
    321332        if not self.tables: 
     
    357368        be included. Sub-classes, can override this to create a from-clause via 
    358369        a "select", for example (e.g. CountQuery). 
     370 
     371        This should only be called after any SQL construction methods that 
     372        might change the tables we need. This means the select columns and 
     373        ordering must be done first. 
    359374        """ 
    360375        result = [] 
    361376        qn = self.quote_name_unless_alias 
     377        first = True 
    362378        for alias in self.tables: 
    363379            if not self.alias_map[alias][ALIAS_REFCOUNT]: 
     
    371387                            qn(lhs_col), qn(alias), qn(col))) 
    372388            else: 
    373                 result.append('%s%s' % (qn(name), alias_str)) 
     389                connector = not first and ', ' or '' 
     390                result.append('%s%s%s' % (connector, qn(name), alias_str)) 
     391            first = False 
    374392        extra_tables = [] 
    375393        for t in self.extra_tables: 
    376394            alias, created = self.table_alias(t) 
    377395            if created: 
    378                 result.append(', %s' % alias) 
     396                connector = not first and ', ' or '' 
     397                result.append('%s%s' % (connector, alias)) 
     398                first = False 
    379399        return result, [] 
    380400 
     
    397417        """ 
    398418        Returns a tuple representing the SQL elements in the "order by" clause. 
    399         """ 
    400         if self.order_by is None: 
     419 
     420        Determining the ordering SQL can change the tables we need to include, 
     421        so this should be run *before* get_from_clause(). 
     422        """ 
     423        if self.extra_order_by: 
     424            ordering = self.extra_order_by 
     425        elif self.order_by is None: 
    401426            ordering = [] 
    402427        else: 
     428            # Note that self.order_by can be empty in two ways: [] ("use the 
     429            # default"), which is handled here, and None ("no ordering"), which 
     430            # is handled in the previous test. 
    403431            ordering = self.order_by or self.model._meta.ordering 
    404432        qn = self.quote_name_unless_alias 
    405         opts = self.model._meta 
    406433        result = [] 
    407434        for field in ordering: 
     
    417444                result.append('%s %s' % (field, order)) 
    418445                continue 
    419             if field[0] == '-': 
    420                 col = field[1:] 
    421                 order = 'DESC' 
     446            if '.' in field: 
     447                # This came in through an extra(ordering=...) addition. Pass it 
     448                # on verbatim, after mapping the table name to an alias, if 
     449                # necessary. 
     450                col, order = get_order_dir(field) 
     451                table, col = col.split('.', 1) 
     452                result.append('%s.%s %s' % (qn(self.table_alias(table)[0]), col, 
     453                        order)) 
     454            elif get_order_dir(field)[0] not in self.extra_select: 
     455                # 'col' is of the form 'field' or 'field1__field2' or 
     456                # 'field1__field2__field', etc. 
     457                for table, col, order in self.find_ordering_name(field, 
     458                        self.model._meta): 
     459                    result.append('%s.%s %s' % (qn(table), qn(col), order)) 
    422460            else: 
    423                 col = field 
    424                 order = 'ASC' 
    425             if '.' in col: 
    426                 table, col = col.split('.', 1) 
    427                 table = '%s.' % qn(self.table_alias(table)[0]) 
    428             elif col not in self.extra_select: 
    429                 # Use the root model's database table as the referenced table. 
    430                 table = '%s.' % qn(self.tables[0]) 
    431             else: 
    432                 table = '' 
    433             result.append('%s%s %s' % (table, 
    434                     qn(orderfield_to_column(col, opts)), order)) 
     461                col, order = get_order_dir(field) 
     462                result.append('%s %s' % (qn(col), order)) 
    435463        return result 
     464 
     465    def find_ordering_name(self, name, opts, alias=None, default_order='ASC'): 
     466        """ 
     467        Returns the table alias (the name might not be unambiguous, the alias 
     468        will be) and column name for ordering by the given 'name' parameter. 
     469        The 'name' is of the form 'field1__field2__...__fieldN'. 
     470        """ 
     471        name, order = get_order_dir(name, default_order) 
     472        pieces = name.split(LOOKUP_SEP) 
     473        if not alias: 
     474            alias = self.join((None, opts.db_table, None, None)) 
     475        for elt in pieces: 
     476            joins, opts, unused1, field, col, unused2 = \ 
     477                    self.get_next_join(elt, opts, alias, False) 
     478            if joins: 
     479                alias = joins[-1] 
     480        col = col or field.column 
     481 
     482        # If we get to this point and the field is a relation to another model, 
     483        # append the default ordering for that model. 
     484        if joins and opts.ordering: 
     485            results = [] 
     486            for item in opts.ordering: 
     487                results.extend(self.find_ordering_name(item, opts, alias, 
     488                        order)) 
     489            return results 
     490 
     491        if alias: 
     492            # We have to do the same "final join" optimisation as in 
     493            # add_filter, since the final column might not otherwise be part of 
     494            # the select set (so we can't order on it). 
     495            join = self.alias_map[alias][ALIAS_JOIN] 
     496            if col == join[RHS_JOIN_COL]: 
     497                self.unref_alias(alias) 
     498                alias = join[LHS_ALIAS] 
     499                col = join[LHS_JOIN_COL] 
     500        return [(alias, col, order)] 
    436501 
    437502    def table_alias(self, table_name, create=False): 
     
    558623                    f.rel.get_related_field().column), exclusions=used) 
    559624            used.append(alias) 
    560             self.select.extend([(table, f2.column) 
     625            self.select.extend([(alias, f2.column) 
    561626                    for f2 in f.rel.to._meta.fields]) 
    562627            self.fill_related_selections(f.rel.to._meta, alias, cur_depth + 1, 
     
    803868        corresponding to column positions in the 'select' list. 
    804869        """ 
     870        errors = [] 
     871        for item in ordering: 
     872            if not ORDER_PATTERN.match(item): 
     873                errors.append(item) 
     874        if errors: 
     875            raise TypeError('Invalid order_by arguments: %s' % errors) 
    805876        self.order_by.extend(ordering) 
    806877 
     
    814885        else: 
    815886            self.order_by = [] 
     887        self.extra_order_by = [] 
    816888 
    817889    def add_count_column(self): 
     
    10441116        return ['(%s) AS A1' % result], params 
    10451117 
     1118    def get_ordering(self): 
     1119        return () 
     1120 
    10461121def find_field(name, field_list, related_query): 
    10471122    """ 
     
    10781153            + field_choices(opts.fields, False)) 
    10791154 
    1080 def orderfield_to_column(name, opts): 
    1081     """ 
    1082     For a field name specified in an "order by" clause, returns the database 
    1083     column name. If 'name' is not a field in the current model, it is returned 
    1084     unchanged. 
    1085     """ 
    1086     try: 
    1087         return opts.get_field(name, False).column 
    1088     except FieldDoesNotExist: 
    1089         return name 
    1090  
     1155def get_order_dir(field, default='ASC'): 
     1156    """ 
     1157    Returns the field name and direction for an order specification. For 
     1158    example, '-foo' is returned as ('foo', 'DESC'). 
     1159 
     1160    The 'default' param is used to indicate which way no prefix (or a '+' 
     1161    prefix) should sort. The '-' prefix always sorts the opposite way. 
     1162    """ 
     1163    dirn = {'ASC': ('ASC', 'DESC'), 'DESC': ('DESC', 'ASC')}[default] 
     1164    if field[0] == '-': 
     1165        return field[1:], dirn[1] 
     1166    return field, dirn[0] 
     1167 
  • django/branches/queryset-refactor/tests/modeltests/reserved_names/models.py

    r5876 r6510  
    4646>>> print v.where 
    47472005-01-01 
    48 >>> Thing.objects.order_by('select.when') 
    49 [<Thing: a>, <Thing: h>] 
    5048 
    5149>>> Thing.objects.dates('where', 'year') 
  • django/branches/queryset-refactor/tests/regressiontests/queries/models.py

    r6504 r6510  
    11""" 
    2 Various combination queries that have been problematic in the past. 
     2Various complex queries that have been problematic in the past. 
    33""" 
    44 
     
    1313        return self.name 
    1414 
     15class Note(models.Model): 
     16    note = models.CharField(maxlength=100) 
     17 
     18    class Meta: 
     19        ordering = ['note'] 
     20 
     21    def __unicode__(self): 
     22        return self.note 
     23 
     24class ExtraInfo(models.Model): 
     25    info = models.CharField(maxlength=100) 
     26    note = models.ForeignKey(Note) 
     27 
     28    class Meta: 
     29        ordering = ['info'] 
     30 
     31    def __unicode__(self): 
     32        return self.info 
     33 
    1534class Author(models.Model): 
    1635    name = models.CharField(maxlength=10) 
    1736    num = models.IntegerField(unique=True) 
     37    extra = models.ForeignKey(ExtraInfo) 
    1838 
    1939    def __unicode__(self): 
     
    2444    tags = models.ManyToManyField(Tag, blank=True, null=True) 
    2545    creator = models.ForeignKey(Author) 
     46    note = models.ForeignKey(Note) 
     47 
     48    class Meta: 
     49        ordering = ['-note', 'name'] 
    2650 
    2751    def __unicode__(self): 
     
    3559        return self.name 
    3660 
     61class Ranking(models.Model): 
     62    rank = models.IntegerField() 
     63    author = models.ForeignKey(Author) 
     64 
     65    class Meta: 
     66        # A complex ordering specification. Should stress the system a bit. 
     67        ordering = ('author__extra__note', 'author__name', 'rank') 
     68 
     69    def __unicode__(self): 
     70        return '%d: %s' % (self.rank, self.author.name) 
     71 
     72class Cover(models.Model): 
     73    title = models.CharField(maxlength=50) 
     74    item = models.ForeignKey(Item) 
     75 
     76    class Meta: 
     77        ordering = ['item'] 
     78 
     79    def __unicode__(self): 
     80        return self.title 
    3781 
    3882__test__ = {'API_TESTS':""" 
     
    4892>>> t5.save() 
    4993 
    50  
    51 >>> a1 = Author(name='a1', num=1001) 
     94>>> n1 = Note(note='n1') 
     95>>> n1.save() 
     96>>> n2 = Note(note='n2') 
     97>>> n2.save() 
     98>>> n3 = Note(note='n3') 
     99>>> n3.save() 
     100 
     101Create these out of order so that sorting by 'id' will be different to sorting 
     102by 'info'. Helps detect some problems later. 
     103>>> e2 = ExtraInfo(info='e2', note=n2) 
     104>>> e2.save() 
     105>>> e1 = ExtraInfo(info='e1', note=n1) 
     106>>> e1.save() 
     107 
     108>>> a1 = Author(name='a1', num=1001, extra=e1) 
    52109>>> a1.save() 
    53 >>> a2 = Author(name='a2', num=2002
     110>>> a2 = Author(name='a2', num=2002, extra=e1
    54111>>> a2.save() 
    55 >>> a3 = Author(name='a3', num=3003
     112>>> a3 = Author(name='a3', num=3003, extra=e2
    56113>>> a3.save() 
    57 >>> a4 = Author(name='a4', num=4004
     114>>> a4 = Author(name='a4', num=4004, extra=e2
    58115>>> a4.save() 
    59116 
    60 >>> i1 = Item(name='one', creator=a1
     117>>> i1 = Item(name='one', creator=a1, note=n3
    61118>>> i1.save() 
    62119>>> i1.tags = [t1, t2] 
    63 >>> i2 = Item(name='two', creator=a2
     120>>> i2 = Item(name='two', creator=a2, note=n2
    64121>>> i2.save() 
    65122>>> i2.tags = [t1, t3] 
    66 >>> i3 = Item(name='three', creator=a2
     123>>> i3 = Item(name='three', creator=a2, note=n3
    67124>>> i3.save() 
    68 >>> i4 = Item(name='four', creator=a4
     125>>> i4 = Item(name='four', creator=a4, note=n3
    69126>>> i4.save() 
    70127>>> i4.tags = [t4] 
     
    74131>>> r2 = Report(name='r2', creator=a3) 
    75132>>> r2.save() 
     133 
     134Ordering by 'rank' gives us rank2, rank1, rank3. Ordering by the Meta.ordering 
     135will be rank3, rank2, rank1. 
     136>>> rank1 = Ranking(rank=2, author=a2) 
     137>>> rank1.save() 
     138>>> rank2 = Ranking(rank=1, author=a3) 
     139>>> rank2.save() 
     140>>> rank3 = Ranking(rank=3, author=a1) 
     141>>> rank3.save() 
     142 
     143>>> c1 = Cover(title="first", item=i4) 
     144>>> c1.save() 
     145>>> c2 = Cover(title="second", item=i2) 
     146>>> c2.save() 
    76147 
    77148Bug #1050 
     
    120191# Create something with a duplicate 'name' so that we can test multi-column 
    121192# cases (which require some tricky SQL transformations under the covers). 
    122 >>> xx = Item(name='four', creator=a2
     193>>> xx = Item(name='four', creator=a2, note=n1
    123194>>> xx.save() 
    124195>>> Item.objects.exclude(name='two').values('creator', 'name').distinct().count() 
     
    207278>>> Item.objects.extra(tables=['queries_author']).select_related().order_by('name')[:1] 
    208279[<Item: four>] 
     280 
     281Bug #2076 
     282# Ordering on related tables should be possible, even if the table is not 
     283# otherwise involved. 
     284>>> Item.objects.order_by('note__note', 'name') 
     285[<Item: two>, <Item: four>, <Item: one>, <Item: three>] 
     286 
     287# Ordering on a related field should use the remote model's default ordering as 
     288# a final step. 
     289>>> Author.objects.order_by('extra', '-name') 
     290[<Author: a2>, <Author: a1>, <Author: a4>, <Author: a3>] 
     291 
     292# If the remote model does not have a default ordering, we order by its 'id' 
     293# field. 
     294>>> Item.objects.order_by('creator', 'name') 
     295[<Item: one>, <Item: three>, <Item: two>, <Item: four>] 
     296 
     297# Cross model ordering is possible in Meta, too. 
     298>>> Ranking.objects.all() 
     299[<Ranking: 3: a1>, <Ranking: 2: a2>, <Ranking: 1: a3>] 
     300>>> Ranking.objects.all().order_by('rank') 
     301[<Ranking: 1: a3>, <Ranking: 2: a2>, <Ranking: 3: a1>] 
     302 
     303>>> Cover.objects.all() 
     304[<Cover: first>, <Cover: second>] 
     305 
     306Bugs #2874, #3002 
     307>>> qs = Item.objects.select_related().order_by('note__note', 'name') 
     308>>> list(qs) 
     309[<Item: two>, <Item: four>, <Item: one>, <Item: three>] 
     310 
     311# This is also a good select_related() test because there are multiple Note 
     312# entries in the SQL. The two Note items should be different. 
     313>>> qs[0].note, qs[0].creator.extra.note 
     314(<Note: n2>, <Note: n1>) 
    209315"""} 
    210316