Author: adrian
Date: 2007-01-22 20:11:08 -0600 (Mon, 22 Jan 2007)
New Revision: 4394

Modified:
   django/trunk/django/db/models/manager.py
   django/trunk/django/db/models/query.py
   django/trunk/docs/db-api.txt
   django/trunk/tests/modeltests/lookup/models.py
Log:
Fixed #3283 -- Added support for empty QuerySets via none() method. Thanks for 
the patch, medhat

Modified: django/trunk/django/db/models/manager.py
===================================================================
--- django/trunk/django/db/models/manager.py    2007-01-23 02:02:35 UTC (rev 
4393)
+++ django/trunk/django/db/models/manager.py    2007-01-23 02:11:08 UTC (rev 
4394)
@@ -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 @@
     #######################
     # 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()

Modified: django/trunk/django/db/models/query.py
===================================================================
--- django/trunk/django/db/models/query.py      2007-01-23 02:02:35 UTC (rev 
4393)
+++ django/trunk/django/db/models/query.py      2007-01-23 02:11:08 UTC (rev 
4394)
@@ -25,6 +25,9 @@
 # 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 @@
         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 @@
         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 @@
             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 @@
         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,7 +585,26 @@
         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"
     def __init__(self, *args):
@@ -571,10 +613,14 @@
     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 @@
         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 @@
         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'):

Modified: django/trunk/docs/db-api.txt
===================================================================
--- django/trunk/docs/db-api.txt        2007-01-23 02:02:35 UTC (rev 4393)
+++ django/trunk/docs/db-api.txt        2007-01-23 02:11:08 UTC (rev 4394)
@@ -525,7 +525,22 @@
     [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()``
 ~~~~~~~~~~~~~~~~~~~~
 

Modified: django/trunk/tests/modeltests/lookup/models.py
===================================================================
--- django/trunk/tests/modeltests/lookup/models.py      2007-01-23 02:02:35 UTC 
(rev 4393)
+++ django/trunk/tests/modeltests/lookup/models.py      2007-01-23 02:11:08 UTC 
(rev 4394)
@@ -191,4 +191,19 @@
 >>> Article.objects.filter(headline__contains='\\')
 [<Article: Article with \ backslash>]
 
+# 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=[])
+[<Article: Article with \ backslash>, <Article: Article% with percent sign>, 
<Article: Article_ with underscore>, <Article: Article 5>, <Article: Article 
6>, <Article: Article 4>, <Article: Article 2>, <Article: Article 3>, <Article: 
Article 7>, <Article: Article 1>]
+
 """}


--~--~---------~--~----~------------~-------~--~----~
You received this message because you are subscribed to the Google Groups 
"Django updates" group.
To post to this group, send email to [email protected]
To unsubscribe from this group, send email to [EMAIL PROTECTED]
For more options, visit this group at 
http://groups.google.com/group/django-updates?hl=en
-~----------~----~----~----~------~----~------~--~---

Reply via email to