diff --git a/django/contrib/admin/views/main.py b/django/contrib/admin/views/main.py index 2b7a690a83..515dea56a1 100644 --- a/django/contrib/admin/views/main.py +++ b/django/contrib/admin/views/main.py @@ -716,24 +716,9 @@ class ChangeList(object): qs = qs.select_related() break - # Calculate lookup_order_field. - # If the order-by field is a field with a relationship, order by the - # value in the related table. - lookup_order_field = self.order_field - try: - f = self.lookup_opts.get_field(self.order_field, many_to_many=False) - except models.FieldDoesNotExist: - pass - else: - if isinstance(f.rel, models.OneToOneRel): - # For OneToOneFields, don't try to order by the related object's ordering criteria. - pass - elif isinstance(f.rel, models.ManyToOneRel): - rel_ordering = f.rel.to._meta.ordering and f.rel.to._meta.ordering[0] or f.rel.to._meta.pk.column - lookup_order_field = '%s.%s' % (f.rel.to._meta.db_table, rel_ordering) - # Set ordering. - qs = qs.order_by((self.order_type == 'desc' and '-' or '') + lookup_order_field) + if self.order_field: + qs = qs.order_by('%s%s' % ((self.order_type == 'desc' and '-' or ''), self.order_field)) # Apply keyword searches. def construct_search(field_name): diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py index 8917fc3b23..063c0ae411 100644 --- a/django/db/backends/__init__.py +++ b/django/db/backends/__init__.py @@ -51,6 +51,7 @@ class BaseDatabaseFeatures(object): uses_case_insensitive_names = False uses_custom_query_class = False empty_fetchmany_value = [] + update_can_self_select = True class BaseDatabaseOperations(object): """ diff --git a/django/db/backends/mysql/base.py b/django/db/backends/mysql/base.py index 17aa6f13bf..da57ecbe47 100644 --- a/django/db/backends/mysql/base.py +++ b/django/db/backends/mysql/base.py @@ -63,6 +63,7 @@ class DatabaseFeatures(BaseDatabaseFeatures): autoindexes_primary_keys = False inline_fk_references = False empty_fetchmany_value = () + update_can_self_select = False class DatabaseOperations(BaseDatabaseOperations): def date_extract_sql(self, lookup_type, field_name): diff --git a/django/db/backends/mysql_old/base.py b/django/db/backends/mysql_old/base.py index efbfeeafc5..0ae114d176 100644 --- a/django/db/backends/mysql_old/base.py +++ b/django/db/backends/mysql_old/base.py @@ -67,6 +67,7 @@ class DatabaseFeatures(BaseDatabaseFeatures): autoindexes_primary_keys = False inline_fk_references = False empty_fetchmany_value = () + update_can_self_select = False class DatabaseOperations(BaseDatabaseOperations): def date_extract_sql(self, lookup_type, field_name): diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py index 3c9169d53b..bb2191e3a4 100644 --- a/django/db/models/fields/__init__.py +++ b/django/db/models/fields/__init__.py @@ -9,6 +9,7 @@ except ImportError: from django.db import get_creation_module from django.db.models import signals +from django.db.models.query_utils import QueryWrapper from django.dispatch import dispatcher from django.conf import settings from django.core import validators @@ -226,6 +227,9 @@ class Field(object): def get_db_prep_lookup(self, lookup_type, value): "Returns field's value prepared for database lookup." + if hasattr(value, 'as_sql'): + sql, params = value.as_sql() + return QueryWrapper(('(%s)' % sql), params) if lookup_type in ('exact', 'regex', 'iregex', 'gt', 'gte', 'lt', 'lte', 'month', 'day', 'search'): return [value] elif lookup_type in ('range', 'in'): diff --git a/django/db/models/query.py b/django/db/models/query.py index e1e2bb19f2..3011b9d9a6 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -284,6 +284,7 @@ class QuerySet(object): query = self.query.clone(sql.UpdateQuery) query.add_update_values(kwargs) query.execute_sql(None) + transaction.commit_unless_managed() self._result_cache = None update.alters_data = True @@ -496,9 +497,6 @@ class ValuesQuerySet(QuerySet): # QuerySet.clone() will also set up the _fields attribute with the # names of the model fields to select. - def __iter__(self): - return self.iterator() - def iterator(self): self.query.trim_extra_select(self.extra_names) names = self.query.extra_select.keys() + self.field_names diff --git a/django/db/models/sql/constants.py b/django/db/models/sql/constants.py index 3075817385..129a592b31 100644 --- a/django/db/models/sql/constants.py +++ b/django/db/models/sql/constants.py @@ -28,7 +28,7 @@ NULLABLE = 6 MULTI = 'multi' SINGLE = 'single' -ORDER_PATTERN = re.compile(r'\?|[-+]?\w+$') +ORDER_PATTERN = re.compile(r'\?|[-+]?[.\w]+$') ORDER_DIR = { 'ASC': ('ASC', 'DESC'), 'DESC': ('DESC', 'ASC')} diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py index 59b2ebdd68..bfed984953 100644 --- a/django/db/models/sql/query.py +++ b/django/db/models/sql/query.py @@ -895,9 +895,15 @@ class Query(object): Add a single filter to the query. The 'filter_expr' is a pair: (filter_string, value). E.g. ('name__contains', 'fred') - If 'negate' is True, this is an exclude() filter. If 'trim' is True, we - automatically trim the final join group (used internally when - constructing nested queries). + If 'negate' is True, this is an exclude() filter. It's important to + note that this method does not negate anything in the where-clause + object when inserting the filter constraints. This is because negated + filters often require multiple calls to add_filter() and the negation + should only happen once. So the caller is responsible for this (the + caller will normally be add_q(), so that as an example). + + If 'trim' is True, we automatically trim the final join group (used + internally when constructing nested queries). If 'can_reuse' is a set, we are processing a component of a multi-component filter (e.g. filter(Q1, Q2)). In this case, 'can_reuse' @@ -1001,7 +1007,6 @@ class Query(object): self.where.add((alias, col, field, lookup_type, value), connector) if negate: - self.where.negate() for alias in join_list: self.promote_alias(alias) if final > 1 and lookup_type != 'isnull': @@ -1039,12 +1044,12 @@ class Query(object): self.where.start_subtree(connector) self.add_q(child, used_aliases) self.where.end_subtree() - if q_object.negated: - self.where.children[-1].negate() else: self.add_filter(child, connector, q_object.negated, can_reuse=used_aliases) connector = q_object.connector + if q_object.negated: + self.where.negate() if subtree: self.where.end_subtree() diff --git a/django/db/models/sql/subqueries.py b/django/db/models/sql/subqueries.py index 382e6e94ff..7385cd00e1 100644 --- a/django/db/models/sql/subqueries.py +++ b/django/db/models/sql/subqueries.py @@ -95,7 +95,7 @@ class UpdateQuery(Query): def _setup_query(self): """ - Runs on initialisation and after cloning. Any attributes that would + Runs on initialization and after cloning. Any attributes that would normally be set in __init__ should go in here, instead, so that they are also set up after a clone() call. """ @@ -159,20 +159,37 @@ class UpdateQuery(Query): # from other tables. query = self.clone(klass=Query) query.bump_prefix() - query.select = [] query.extra_select = {} - query.add_fields([query.model._meta.pk.name]) + first_table = query.tables[0] + if query.alias_refcount[first_table] == 1: + # We can remove one table from the inner query. + query.unref_alias(first_table) + for i in xrange(1, len(query.tables)): + table = query.tables[i] + if query.alias_refcount[table]: + break + join_info = query.alias_map[table] + query.select = [(join_info[RHS_ALIAS], join_info[RHS_JOIN_COL])] + must_pre_select = False + else: + query.select = [] + query.add_fields([query.model._meta.pk.name]) + must_pre_select = not self.connection.features.update_can_self_select # Now we adjust the current query: reset the where clause and get rid # of all the tables we don't need (since they're in the sub-select). self.where = self.where_class() - if self.related_updates: + if self.related_updates or must_pre_select: + # Either we're using the idents in multiple update queries (so + # don't want them to change), or the db backend doesn't support + # selecting from the updating table (e.g. MySQL). idents = [] for rows in query.execute_sql(MULTI): idents.extend([r[0] for r in rows]) self.add_filter(('pk__in', idents)) self.related_ids = idents else: + # The fast path. Filters and updates in one query. self.add_filter(('pk__in', query)) for alias in self.tables[1:]: self.alias_refcount[alias] = 0 @@ -349,6 +366,7 @@ class DateQuery(Query): self.connection.ops.date_trunc_sql) self.select = [select] self.select_fields = [None] + self.select_related = False # See #7097. self.distinct = True self.order_by = order == 'ASC' and [1] or [-1] diff --git a/docs/db-api.txt b/docs/db-api.txt index 6585890c72..6299f3497d 100644 --- a/docs/db-api.txt +++ b/docs/db-api.txt @@ -527,7 +527,7 @@ applied to a query, not even the default ordering, call ``order_by()`` with no parameters. **New in Django development version:** The syntax for ordering across related -models has changed. See the `Django 0.96 documentation`_ for the old behaviour. +models has changed. See the `Django 0.96 documentation`_ for the old behavior. .. _Django 0.96 documentation: http://www.djangoproject.com/documentation/0.96/model-api/#floatfield @@ -540,9 +540,9 @@ backend normally orders them. **New in Django development version** -If you want to reverse the order in which a queryset's elements are returned, -you can use the ``reverse()`` method. Calling ``reverse()`` a second time -restores the ordering back to the normal direction. +Use the ``reverse()`` method to reverse the order in which a queryset's +elements are returned. Calling ``reverse()`` a second time restores the +ordering back to the normal direction. To retrieve the ''last'' five items in a queryset, you could do this:: @@ -552,7 +552,7 @@ Note that this is not quite the same as slicing from the end of a sequence in Python. The above example will return the last item first, then the penultimate item and so on. If we had a Python sequence and looked at ``seq[:-5]``, we would see the fifth-last item first. Django doesn't support -that mode of access (slicing from the end), since it is not possible to do it +that mode of access (slicing from the end), because it's not possible to do it efficiently in SQL. ``distinct()`` @@ -1660,7 +1660,7 @@ entry. The entries select by the second filter may or may not be the same as the entries in the first filter. We are filtering the ``Blog`` items with each filter statement, not the ``Entry`` items. -All of this behaviour also applies to ``exclude()``: all the conditions in a +All of this behavior also applies to ``exclude()``: all the conditions in a single ``exclude()`` statement apply to a single instance (if those conditions are talking about the same multi-valued relation). Conditions in subsequent ``filter()`` or ``exclude()`` calls that refer to the same relation may end up @@ -2101,24 +2101,24 @@ Updating multiple objects at once **New in Django development version** Sometimes you want to set a field to a particular value for all the objects in -a queryset. You can do this with the ``update()`` method. For example:: +a ``QuerySet``. You can do this with the ``update()`` method. For example:: - # Update all the headlines to the same value. - Entry.objects.all().update(headline='Everything is the same') + # Update all the headlines with pub_date in 2007. + Entry.objects.filter(pub_date__year=2007).update(headline='Everything is the same') You can only set non-relation fields and ``ForeignKey`` fields using this -method and the value you set the field to must be a normal Python value (you -can't set a field to be equal to some other field at the moment). +method, and the value you set the field to must be a hard-coded Python value +(i.e., you can't set a field to be equal to some other field at the moment). To update ``ForeignKey`` fields, set the new value to be the new model instance you want to point to. Example:: b = Blog.objects.get(pk=1) - # Make all entries belong to this blog. + # Change every Entry so that it belongs to this Blog. Entry.objects.all().update(blog=b) The ``update()`` method is applied instantly and doesn't return anything -(similar to ``delete()``). The only restriction on the queryset that is +(similar to ``delete()``). The only restriction on the ``QuerySet`` that is updated is that it can only access one database table, the model's main table. So don't try to filter based on related fields or anything like that; it won't work. diff --git a/tests/modeltests/many_to_one/models.py b/tests/modeltests/many_to_one/models.py index 6616f8b55e..53ad4466bb 100644 --- a/tests/modeltests/many_to_one/models.py +++ b/tests/modeltests/many_to_one/models.py @@ -246,7 +246,7 @@ FieldError: Cannot resolve keyword 'reporter_id' into field. Choices are: headli >>> Reporter.objects.filter(article__reporter__exact=r).distinct() [] -# Check that implied __exact also works +# Check that implied __exact also works. >>> Reporter.objects.filter(article__reporter=r).distinct() [] @@ -266,11 +266,24 @@ True >>> Reporter.objects.order_by('first_name') [] -# Deletes using a join in the query +# You can delete using a JOIN in the query. >>> Reporter.objects.filter(article__headline__startswith='This').delete() >>> Reporter.objects.all() [] >>> Article.objects.all() [] +# Check that Article.objects.select_related().dates() works properly when +# there are multiple Articles with the same date but different foreign-key +# objects (Reporters). +>>> r1 = Reporter.objects.create(first_name='Mike', last_name='Royko', email='royko@suntimes.com') +>>> r2 = Reporter.objects.create(first_name='John', last_name='Kass', email='jkass@tribune.com') +>>> a1 = Article.objects.create(headline='First', pub_date=datetime(1980, 4, 23), reporter=r1) +>>> a2 = Article.objects.create(headline='Second', pub_date=datetime(1980, 4, 23), reporter=r2) +>>> Article.objects.select_related().dates('pub_date', 'day') +[datetime.datetime(1980, 4, 23, 0, 0)] +>>> Article.objects.select_related().dates('pub_date', 'month') +[datetime.datetime(1980, 4, 1, 0, 0)] +>>> Article.objects.select_related().dates('pub_date', 'year') +[datetime.datetime(1980, 1, 1, 0, 0)] """} diff --git a/tests/regressiontests/queries/models.py b/tests/regressiontests/queries/models.py index 483aa7218c..6fd361ec19 100644 --- a/tests/regressiontests/queries/models.py +++ b/tests/regressiontests/queries/models.py @@ -117,6 +117,24 @@ class LoopZ(models.Model): class Meta: ordering = ['z'] +# A model and custom default manager combination. +class CustomManager(models.Manager): + def get_query_set(self): + return super(CustomManager, self).get_query_set().filter(public=True, + tag__name='t1') + +class ManagedModel(models.Model): + data = models.CharField(max_length=10) + tag = models.ForeignKey(Tag) + public = models.BooleanField(default=True) + + objects = CustomManager() + normal_manager = models.Manager() + + def __unicode__(self): + return self.data + + __test__ = {'API_TESTS':""" >>> t1 = Tag(name='t1') >>> t1.save() @@ -654,5 +672,34 @@ Bug #7045 -- extra tables used to crash SQL construction on the second use. >>> s = qs.query.as_sql() >>> s = qs.query.as_sql() # test passes if this doesn't raise an exception. +Bug #7098 -- Make sure semi-deprecated ordering by related models syntax still +works. +>>> Item.objects.values('note__note').order_by('queries_note.note', 'id') +[{'note__note': u'n2'}, {'note__note': u'n3'}, {'note__note': u'n3'}, {'note__note': u'n3'}] + +Bug #7096 -- Make sure exclude() with multiple conditions continues to work. +>>> Tag.objects.filter(parent=t1, name='t3').order_by('name') +[] +>>> Tag.objects.exclude(parent=t1, name='t3').order_by('name') +[, , , ] +>>> Item.objects.exclude(tags__name='t1', name='one').order_by('name').distinct() +[, , ] +>>> Item.objects.filter(name__in=['three', 'four']).exclude(tags__name='t1').order_by('name') +[, ] + +More twisted cases, involving nested negations. +>>> Item.objects.exclude(~Q(tags__name='t1', name='one')) +[] +>>> Item.objects.filter(~Q(tags__name='t1', name='one'), name='two') +[] +>>> Item.objects.exclude(~Q(tags__name='t1', name='one'), name='two') +[, , ] + +Bug #7095 +Updates that are filtered on the model being updated are somewhat tricky to get +in MySQL. This exercises that case. +>>> mm = ManagedModel.objects.create(data='mm1', tag=t1, public=True) +>>> ManagedModel.objects.update(data='mm') + """}