Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

magic-removal: Fixed #1133 -- Added ability to use Q objects as args

in DB lookup queries.


git-svn-id: http://code.djangoproject.com/svn/django/branches/magic-removal@1884 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit dde6963869ba4a72b100daedd66470e080f99b75 1 parent e9a1394
Russell Keith-Magee authored January 09, 2006
70  django/db/models/manager.py
@@ -5,6 +5,7 @@
5 5
 from django.db.models.query import handle_legacy_orderlist, orderlist2sql, orderfield2column
6 6
 from django.dispatch import dispatcher
7 7
 from django.db.models import signals
  8
+from django.utils.datastructures import SortedDict
8 9
 
9 10
 # Size of each "chunk" for get_iterator calls.
10 11
 # Larger values are slightly faster at the expense of more storage space.
@@ -47,7 +48,7 @@ def contribute_to_class(self, klass, name):
47 48
            self.creation_counter < klass._default_manager.creation_counter:
48 49
                 klass._default_manager = self
49 50
 
50  
-    def _get_sql_clause(self, **kwargs):
  51
+    def _get_sql_clause(self, *args, **kwargs):
51 52
         def quote_only_if_word(word):
52 53
             if ' ' in word:
53 54
                 return word
@@ -59,12 +60,28 @@ def quote_only_if_word(word):
59 60
         # Construct the fundamental parts of the query: SELECT X FROM Y WHERE Z.
60 61
         select = ["%s.%s" % (backend.quote_name(opts.db_table), backend.quote_name(f.column)) for f in opts.fields]
61 62
         tables = (kwargs.get('tables') and [quote_only_if_word(t) for t in kwargs['tables']] or [])
  63
+        joins = SortedDict()
62 64
         where = kwargs.get('where') and kwargs['where'][:] or []
63 65
         params = kwargs.get('params') and kwargs['params'][:] or []
64 66
 
  67
+        # Convert all the args into SQL.
  68
+        table_count = 0
  69
+        for arg in args:
  70
+            # check that the provided argument is a Query (i.e., it has a get_sql method)
  71
+            if not hasattr(arg, 'get_sql'):
  72
+                raise TypeError, "'%s' is not a valid query argument" % str(arg)
  73
+
  74
+            tables2, joins2, where2, params2 = arg.get_sql(opts)
  75
+            tables.extend(tables2)
  76
+            joins.update(joins2)
  77
+            where.extend(where2)
  78
+            params.extend(params2)
  79
+
  80
+
65 81
         # Convert the kwargs into SQL.
66  
-        tables2, joins, where2, params2 = parse_lookup(kwargs.items(), opts)
  82
+        tables2, joins2, where2, params2 = parse_lookup(kwargs.items(), opts)
67 83
         tables.extend(tables2)
  84
+        joins.update(joins2)
68 85
         where.extend(where2)
69 86
         params.extend(params2)
70 87
 
@@ -129,13 +146,13 @@ def quote_only_if_word(word):
129 146
 
130 147
         return select, " ".join(sql), params
131 148
 
132  
-    def get_iterator(self, **kwargs):
  149
+    def get_iterator(self, *args, **kwargs):
133 150
         # kwargs['select'] is a dictionary, and dictionaries' key order is
134 151
         # undefined, so we convert it to a list of tuples internally.
135 152
         kwargs['select'] = kwargs.get('select', {}).items()
136 153
 
137 154
         cursor = connection.cursor()
138  
-        select, sql, params = self._get_sql_clause(**kwargs)
  155
+        select, sql, params = self._get_sql_clause(*args, **kwargs)
139 156
         cursor.execute("SELECT " + (kwargs.get('distinct') and "DISTINCT " or "") + ",".join(select) + sql, params)
140 157
         fill_cache = kwargs.get('select_related')
141 158
         index_end = len(self.klass._meta.fields)
@@ -152,35 +169,41 @@ def get_iterator(self, **kwargs):
152 169
                     setattr(obj, k[0], row[index_end+i])
153 170
                 yield obj
154 171
 
155  
-    def get_list(self, **kwargs):
156  
-        return list(self.get_iterator(**kwargs))
  172
+    def get_list(self, *args, **kwargs):
  173
+        return list(self.get_iterator(*args, **kwargs))
157 174
 
158  
-    def get_count(self, **kwargs):
  175
+    def get_count(self, *args, **kwargs):
159 176
         kwargs['order_by'] = []
160 177
         kwargs['offset'] = None
161 178
         kwargs['limit'] = None
162 179
         kwargs['select_related'] = False
163  
-        _, sql, params = self._get_sql_clause(**kwargs)
  180
+        _, sql, params = self._get_sql_clause(*args, **kwargs)
164 181
         cursor = connection.cursor()
165 182
         cursor.execute("SELECT COUNT(*)" + sql, params)
166 183
         return cursor.fetchone()[0]
167 184
 
168  
-    def get_object(self, **kwargs):
169  
-        obj_list = self.get_list(**kwargs)
  185
+    def get_object(self, *args, **kwargs):
  186
+        obj_list = self.get_list(*args, **kwargs)
170 187
         if len(obj_list) < 1:
171 188
             raise self.klass.DoesNotExist, "%s does not exist for %s" % (self.klass._meta.object_name, kwargs)
172 189
         assert len(obj_list) == 1, "get_object() returned more than one %s -- it returned %s! Lookup parameters were %s" % (self.klass._meta.object_name, len(obj_list), kwargs)
173 190
         return obj_list[0]
174 191
 
175 192
     def get_in_bulk(self, *args, **kwargs):
176  
-        id_list = args and args[0] or kwargs.get('id_list', [])
177  
-        assert id_list != [], "get_in_bulk() cannot be passed an empty list."
  193
+        # Separate any list arguments: the first list will be used as the id list; subsequent
  194
+        # lists will be ignored.
  195
+        id_args = filter(lambda arg: isinstance(arg, list), args)
  196
+        # Separate any non-list arguments: these are assumed to be query arguments
  197
+        sql_args = filter(lambda arg: not isinstance(arg, list), args)
  198
+
  199
+        id_list = id_args and id_args[0] or kwargs.get('id_list', [])
  200
+        assert id_list != [], "get_in_bulk() cannot be passed an empty ID list."        
178 201
         kwargs['where'] = ["%s.%s IN (%s)" % (backend.quote_name(self.klass._meta.db_table), backend.quote_name(self.klass._meta.pk.column), ",".join(['%s'] * len(id_list)))]
179 202
         kwargs['params'] = id_list
180  
-        obj_list = self.get_list(**kwargs)
  203
+        obj_list = self.get_list(*sql_args, **kwargs)
181 204
         return dict([(getattr(o, self.klass._meta.pk.attname), o) for o in obj_list])
182 205
 
183  
-    def get_values_iterator(self, **kwargs):
  206
+    def get_values_iterator(self, *args, **kwargs):
184 207
         # select_related and select aren't supported in get_values().
185 208
         kwargs['select_related'] = False
186 209
         kwargs['select'] = {}
@@ -192,7 +215,7 @@ def get_values_iterator(self, **kwargs):
192 215
             fields = [f.column for f in self.klass._meta.fields]
193 216
 
194 217
         cursor = connection.cursor()
195  
-        _, sql, params = self._get_sql_clause(**kwargs)
  218
+        _, sql, params = self._get_sql_clause(*args, **kwargs)
196 219
         select = ['%s.%s' % (backend.quote_name(self.klass._meta.db_table), backend.quote_name(f)) for f in fields]
197 220
         cursor.execute("SELECT " + (kwargs.get('distinct') and "DISTINCT " or "") + ",".join(select) + sql, params)
198 221
         while 1:
@@ -202,17 +225,22 @@ def get_values_iterator(self, **kwargs):
202 225
             for row in rows:
203 226
                 yield dict(zip(fields, row))
204 227
 
205  
-    def get_values(self, **kwargs):
206  
-        return list(self.get_values_iterator(**kwargs))
  228
+    def get_values(self, *args, **kwargs):
  229
+        return list(self.get_values_iterator(*args, **kwargs))
207 230
 
208  
-    def __get_latest(self, **kwargs):
  231
+    def __get_latest(self, *args, **kwargs):
209 232
         kwargs['order_by'] = ('-' + self.klass._meta.get_latest_by,)
210 233
         kwargs['limit'] = 1
211  
-        return self.get_object(**kwargs)
  234
+        return self.get_object(*args, **kwargs)
212 235
 
213 236
     def __get_date_list(self, field, *args, **kwargs):
  237
+        # Separate any string arguments: the first will be used as the kind
  238
+        kind_args = filter(lambda arg: isinstance(arg, str), args)
  239
+        # Separate any non-list arguments: these are assumed to be query arguments
  240
+        sql_args = filter(lambda arg: not isinstance(arg, str), args)
  241
+        
214 242
         from django.db.backends.util import typecast_timestamp
215  
-        kind = args and args[0] or kwargs['kind']
  243
+        kind = kind_args and kind_args[0] or kwargs.get(['kind'],"")
216 244
         assert kind in ("month", "year", "day"), "'kind' must be one of 'year', 'month' or 'day'."
217 245
         order = 'ASC'
218 246
         if kwargs.has_key('order'):
@@ -223,7 +251,7 @@ def __get_date_list(self, field, *args, **kwargs):
223 251
         if field.null:
224 252
             kwargs.setdefault('where', []).append('%s.%s IS NOT NULL' % \
225 253
                 (backend.quote_name(self.klass._meta.db_table), backend.quote_name(field.column)))
226  
-        select, sql, params = self._get_sql_clause(**kwargs)
  254
+        select, sql, params = self._get_sql_clause(*sql_args, **kwargs)
227 255
         sql = 'SELECT %s %s GROUP BY 1 ORDER BY 1 %s' % \
228 256
             (backend.get_date_trunc_sql(kind, '%s.%s' % (backend.quote_name(self.klass._meta.db_table),
229 257
             backend.quote_name(field.column))), sql, order)
2  django/db/models/query.py
@@ -198,6 +198,8 @@ def parse_lookup(kwarg_items, opts):
198 198
         elif value is None:
199 199
             pass
200 200
         elif kwarg == 'complex':
  201
+            if not hasattr(value, 'get_sql'):
  202
+                raise TypeError, "'%s' is not a valid query argument" % str(arg)
201 203
             tables2, joins2, where2, params2 = value.get_sql(opts)
202 204
             tables.extend(tables2)
203 205
             joins.update(joins2)
71  docs/db-api.txt
@@ -224,32 +224,67 @@ OR lookups
224 224
 
225 225
 **New in Django development version.**
226 226
 
227  
-By default, multiple lookups are "AND"ed together. If you'd like to use ``OR``
228  
-statements in your queries, use the ``complex`` lookup type.
  227
+By default, keyword argument queries are "AND"ed together. If you have more complex query 
  228
+requirements (for example, you need to include an ``OR`` statement in your query), you need 
  229
+to use ``Q`` objects.
229 230
 
230  
-``complex`` takes an expression of clauses, each of which is an instance of
231  
-``django.core.meta.Q``. ``Q`` takes an arbitrary number of keyword arguments in
232  
-the standard Django lookup format. And you can use Python's "and" (``&``) and
233  
-"or" (``|``) operators to combine ``Q`` instances. For example::
  231
+A ``Q`` object is an instance of ``django.core.meta.Q``, used to encapsulate a collection of 
  232
+keyword arguments. These keyword arguments are specified in the same way as keyword arguments to
  233
+the basic lookup functions like get_object() and get_list(). For example::
234 234
 
235  
-    from django.core.meta import Q
236  
-    polls.get_object(complex=(Q(question__startswith='Who') | Q(question__startswith='What')))
  235
+    Q(question__startswith='What')
237 236
 
238  
-The ``|`` symbol signifies an "OR", so this (roughly) translates into::
  237
+``Q`` objects can be combined using the ``&`` and ``|`` operators. When an operator is used on two
  238
+``Q`` objects, it yields a new ``Q`` object. For example the statement::
239 239
 
240  
-    SELECT * FROM polls
241  
-    WHERE question LIKE 'Who%' OR question LIKE 'What%';
  240
+    Q(question__startswith='Who') | Q(question__startswith='What')
242 241
 
243  
-You can use ``&`` and ``|`` operators together, and use parenthetical grouping.
244  
-Example::
  242
+... yields a single ``Q`` object that represents the "OR" of two "question__startswith" queries, equivalent to the SQL WHERE clause::
245 243
 
246  
-    polls.get_object(complex=(Q(question__startswith='Who') & (Q(pub_date__exact=date(2005, 5, 2)) | Q(pub_date__exact=date(2005, 5, 6))))
  244
+    ... WHERE question LIKE 'Who%' OR question LIKE 'What%'
247 245
 
248  
-This roughly translates into::
  246
+You can compose statements of arbitrary complexity by combining ``Q`` objects with the ``&`` and ``|`` operators. Parenthetical grouping can also be used. 
249 247
 
250  
-    SELECT * FROM polls
251  
-    WHERE question LIKE 'Who%'
252  
-        AND (pub_date = '2005-05-02' OR pub_date = '2005-05-06');
  248
+One or more ``Q`` objects can then provided as arguments to the lookup functions. If multiple 
  249
+``Q`` object arguments are provided to a lookup function, they will be "AND"ed together. 
  250
+For example::
  251
+
  252
+    polls.get_object(
  253
+        Q(question__startswith='Who'), 
  254
+        Q(pub_date__exact=date(2005, 5, 2)) | Q(pub_date__exact=date(2005, 5, 6))
  255
+    )
  256
+
  257
+... roughly translates into the SQL::
  258
+
  259
+    SELECT * from polls WHERE question LIKE 'Who%'
  260
+        AND (pub_date = '2005-05-02' OR pub_date = '2005-05-06')
  261
+
  262
+If necessary, lookup functions can mix the use of ``Q`` objects and keyword arguments. All arguments
  263
+provided to a lookup function (be they keyword argument or ``Q`` object) are "AND"ed together. 
  264
+However, if a ``Q`` object is provided, it must precede the definition of any keyword arguments. 
  265
+For example::
  266
+
  267
+    polls.get_object(
  268
+        Q(pub_date__exact=date(2005, 5, 2)) | Q(pub_date__exact=date(2005, 5, 6)),
  269
+        question__startswith='Who')
  270
+
  271
+... would be a valid query, equivalent to the previous example; but::
  272
+
  273
+    # INVALID QUERY
  274
+    polls.get_object(
  275
+        question__startswith='Who', 
  276
+        Q(pub_date__exact=date(2005, 5, 2)) | Q(pub_date__exact=date(2005, 5, 6)))
  277
+
  278
+... would not be valid. 
  279
+
  280
+A ``Q`` objects can also be provided to the ``complex`` keyword argument. For example::
  281
+
  282
+    polls.get_object(
  283
+        complex=Q(question__startswith='Who') & 
  284
+            (Q(pub_date__exact=date(2005, 5, 2)) | 
  285
+             Q(pub_date__exact=date(2005, 5, 6))
  286
+        )
  287
+    )
253 288
 
254 289
 See the `OR lookups examples page`_ for more examples.
255 290
 
2  tests/modeltests/lookup/models.py
@@ -68,7 +68,7 @@ def __repr__(self):
68 68
 >>> Article.objects.get_in_bulk([])
69 69
 Traceback (most recent call last):
70 70
     ...
71  
-AssertionError: get_in_bulk() cannot be passed an empty list.
  71
+AssertionError: get_in_bulk() cannot be passed an empty ID list.
72 72
 
73 73
 # get_values() is just like get_list(), except it returns a list of
74 74
 # dictionaries instead of object instances -- and you can specify which fields
31  tests/modeltests/or_lookups/models.py
@@ -3,7 +3,7 @@
3 3
 
4 4
 To perform an OR lookup, or a lookup that combines ANDs and ORs, use the
5 5
 ``complex`` keyword argument, and pass it an expression of clauses using the
6  
-variable ``django.db.models.Q``.
  6
+variable ``django.db.models.Q`` (or any object with a get_sql method).
7 7
 """
8 8
 
9 9
 from django.db import models
@@ -54,4 +54,33 @@ def __repr__(self):
54 54
 >>> Article.objects.get_list(complex=(Q(pk=1) | Q(pk=2) | Q(pk=3)))
55 55
 [Hello, Goodbye, Hello and goodbye]
56 56
 
  57
+# Queries can use Q objects as args
  58
+>>> Article.objects.get_list(Q(headline__startswith='Hello'))
  59
+[Hello, Hello and goodbye]
  60
+
  61
+# Q arg objects are ANDed 
  62
+>>> Article.objects.get_list(Q(headline__startswith='Hello'), Q(headline__contains='bye'))
  63
+[Hello and goodbye]
  64
+
  65
+# Q arg AND order is irrelevant
  66
+>>> Article.objects.get_list(Q(headline__contains='bye'), headline__startswith='Hello')
  67
+[Hello and goodbye]
  68
+
  69
+# QOrs are ok, as they ultimately resolve to a Q 
  70
+>>> Article.objects.get_list(Q(headline__contains='Hello') | Q(headline__contains='bye'))
  71
+[Hello, Goodbye, Hello and goodbye]
  72
+
  73
+# Try some arg queries with operations other than get_list
  74
+>>> Article.objects.get_object(Q(headline__startswith='Hello'), Q(headline__contains='bye'))
  75
+Hello and goodbye
  76
+
  77
+>>> Article.objects.get_count(Q(headline__startswith='Hello') | Q(headline__contains='bye'))
  78
+3
  79
+
  80
+>>> Article.objects.get_values(Q(headline__startswith='Hello'), Q(headline__contains='bye'))
  81
+[{'headline': 'Hello and goodbye', 'pub_date': datetime.datetime(2005, 11, 29, 0, 0), 'id': 3}]
  82
+
  83
+>>> Article.objects.get_in_bulk([1,2], Q(headline__startswith='Hello'))
  84
+{1: Hello}
  85
+
57 86
 """

0 notes on commit dde6963

Please sign in to comment.
Something went wrong with that request. Please try again.