diff --git a/django/db/models/manager.py b/django/db/models/manager.py index 6005874516..b60eed262a 100644 --- a/django/db/models/manager.py +++ b/django/db/models/manager.py @@ -1,4 +1,4 @@ -from django.db.models.query import QuerySet +from django.db.models.query import QuerySet, EmptyQuerySet from django.dispatch import dispatcher from django.db.models import signals from django.db.models.fields import FieldDoesNotExist @@ -41,12 +41,18 @@ class Manager(object): ####################### # PROXIES TO QUERYSET # ####################### + + def get_empty_query_set(self): + return EmptyQuerySet(self.model) def get_query_set(self): """Returns a new QuerySet object. Subclasses can override this method to easily customise the behaviour of the Manager. """ return QuerySet(self.model) + + def none(self): + return self.get_empty_query_set() def all(self): return self.get_query_set() diff --git a/django/db/models/query.py b/django/db/models/query.py index f42e8cfd42..cbfd09e8d7 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -25,6 +25,9 @@ QUERY_TERMS = ( # Larger values are slightly faster at the expense of more storage space. GET_ITERATOR_CHUNK_SIZE = 100 +class EmptyResultSet(Exception): + pass + #################### # HELPER FUNCTIONS # #################### @@ -168,7 +171,12 @@ class QuerySet(object): extra_select = self._select.items() cursor = connection.cursor() - select, sql, params = self._get_sql_clause() + + try: + select, sql, params = self._get_sql_clause() + except EmptyResultSet: + raise StopIteration + cursor.execute("SELECT " + (self._distinct and "DISTINCT " or "") + ",".join(select) + sql, params) fill_cache = self._select_related index_end = len(self.model._meta.fields) @@ -192,7 +200,12 @@ class QuerySet(object): counter._offset = None counter._limit = None counter._select_related = False - select, sql, params = counter._get_sql_clause() + + try: + select, sql, params = counter._get_sql_clause() + except EmptyResultSet: + return 0 + cursor = connection.cursor() if self._distinct: id_col = "%s.%s" % (backend.quote_name(self.model._meta.db_table), @@ -523,7 +536,12 @@ class ValuesQuerySet(QuerySet): field_names = [f.attname for f in self.model._meta.fields] cursor = connection.cursor() - select, sql, params = self._get_sql_clause() + + try: + select, sql, params = self._get_sql_clause() + except EmptyResultSet: + raise StopIteration + select = ['%s.%s' % (backend.quote_name(self.model._meta.db_table), backend.quote_name(c)) for c in columns] cursor.execute("SELECT " + (self._distinct and "DISTINCT " or "") + ",".join(select) + sql, params) while 1: @@ -545,7 +563,12 @@ class DateQuerySet(QuerySet): if self._field.null: self._where.append('%s.%s IS NOT NULL' % \ (backend.quote_name(self.model._meta.db_table), backend.quote_name(self._field.column))) - select, sql, params = self._get_sql_clause() + + try: + select, sql, params = self._get_sql_clause() + except EmptyResultSet: + raise StopIteration + sql = 'SELECT %s %s GROUP BY 1 ORDER BY 1 %s' % \ (backend.get_date_trunc_sql(self._kind, '%s.%s' % (backend.quote_name(self.model._meta.db_table), backend.quote_name(self._field.column))), sql, self._order) @@ -562,6 +585,25 @@ class DateQuerySet(QuerySet): c._kind = self._kind c._order = self._order return c + +class EmptyQuerySet(QuerySet): + def __init__(self, model=None): + super(EmptyQuerySet, self).__init__(model) + self._result_cache = [] + + def iterator(self): + raise StopIteration + + def count(self): + return 0 + + def delete(self): + pass + + def _clone(self, klass=None, **kwargs): + c = super(EmptyQuerySet, self)._clone(klass, **kwargs) + c._result_cache = [] + return c class QOperator(object): "Base class for QAnd and QOr" @@ -571,10 +613,14 @@ class QOperator(object): def get_sql(self, opts): joins, where, params = SortedDict(), [], [] for val in self.args: - joins2, where2, params2 = val.get_sql(opts) - joins.update(joins2) - where.extend(where2) - params.extend(params2) + try: + joins2, where2, params2 = val.get_sql(opts) + joins.update(joins2) + where.extend(where2) + params.extend(params2) + except EmptyResultSet: + if not isinstance(self, QOr): + raise EmptyResultSet if where: return joins, ['(%s)' % self.operator.join(where)], params return joins, [], params @@ -628,8 +674,11 @@ class QNot(Q): self.q = q def get_sql(self, opts): - joins, where, params = self.q.get_sql(opts) - where2 = ['(NOT (%s))' % " AND ".join(where)] + try: + joins, where, params = self.q.get_sql(opts) + where2 = ['(NOT (%s))' % " AND ".join(where)] + except EmptyResultSet: + return SortedDict(), [], [] return joins, where2, params def get_where_clause(lookup_type, table_prefix, field_name, value): @@ -645,11 +694,7 @@ def get_where_clause(lookup_type, table_prefix, field_name, value): if in_string: return '%s%s IN (%s)' % (table_prefix, field_name, in_string) else: - # Most backends do not accept an empty string inside the IN - # expression, i.e. cannot do "WHERE ... IN ()". Since there are - # also some backends that do not accept "WHERE false", we instead - # use an expression that always evaluates to False. - return '0=1' + raise EmptyResultSet elif lookup_type == 'range': return '%s%s BETWEEN %%s AND %%s' % (table_prefix, field_name) elif lookup_type in ('year', 'month', 'day'): diff --git a/docs/db-api.txt b/docs/db-api.txt index 0af2c773fb..99bb30054b 100644 --- a/docs/db-api.txt +++ b/docs/db-api.txt @@ -525,6 +525,21 @@ Examples:: [datetime.datetime(2005, 3, 20), datetime.datetime(2005, 2, 20)] >>> Entry.objects.filter(headline__contains='Lennon').dates('pub_date', 'day') [datetime.datetime(2005, 3, 20)] + +``none()`` +~~~~~~~~~~ + +**New in Django development version** + +Returns an ``EmptyQuerySet`` -- a ``QuerySet`` that always evaluates to +an empty list. This can be used in cases where you know that you should +return an empty result set and your caller is expecting a ``QuerySet`` +object (instead of returning an empty list, for example.) + +Examples:: + + >>> Entry.objects.none() + [] ``select_related()`` ~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/modeltests/lookup/models.py b/tests/modeltests/lookup/models.py index 09c3aa7aa8..aa903d1a64 100644 --- a/tests/modeltests/lookup/models.py +++ b/tests/modeltests/lookup/models.py @@ -191,4 +191,19 @@ DoesNotExist: Article matching query does not exist. >>> Article.objects.filter(headline__contains='\\') [] +# none() returns an EmptyQuerySet that behaves like any other QuerySet object +>>> Article.objects.none() +[] +>>> Article.objects.none().filter(headline__startswith='Article') +[] +>>> Article.objects.none().count() +0 + +# using __in with an empty list should return an empty query set +>>> Article.objects.filter(id__in=[]) +[] + +>>> Article.objects.exclude(id__in=[]) +[, , , , , , , , , ] + """}