Django

Code

root/django/branches/magic-removal/django/db/models/query.py

Revision 2803, 36.5 kB (checked in by adrian, 3 years ago)

magic-removal: Fixed #1685 -- order_by no longer breaks when using a custom 'select'. Thanks, feiyu.xie

Line 
1 from django.db import backend, connection, transaction
2 from django.db.models.fields import DateField, FieldDoesNotExist
3 from django.db.models import signals
4 from django.dispatch import dispatcher
5 from django.utils.datastructures import SortedDict
6
7 import operator
8
9 # For Python 2.3
10 if not hasattr(__builtins__, 'set'):
11     from sets import Set as set
12
13 LOOKUP_SEPARATOR = '__'
14
15 # Size of each "chunk" for get_iterator calls.
16 # Larger values are slightly faster at the expense of more storage space.
17 GET_ITERATOR_CHUNK_SIZE = 100
18
19 ####################
20 # HELPER FUNCTIONS #
21 ####################
22
23 # Django currently supports two forms of ordering.
24 # Form 1 (deprecated) example:
25 #     order_by=(('pub_date', 'DESC'), ('headline', 'ASC'), (None, 'RANDOM'))
26 # Form 2 (new-style) example:
27 #     order_by=('-pub_date', 'headline', '?')
28 # Form 1 is deprecated and will no longer be supported for Django's first
29 # official release. The following code converts from Form 1 to Form 2.
30
31 LEGACY_ORDERING_MAPPING = {'ASC': '_', 'DESC': '-_', 'RANDOM': '?'}
32
33 def handle_legacy_orderlist(order_list):
34     if not order_list or isinstance(order_list[0], basestring):
35         return order_list
36     else:
37         import warnings
38         new_order_list = [LEGACY_ORDERING_MAPPING[j.upper()].replace('_', str(i)) for i, j in order_list]
39         warnings.warn("%r ordering syntax is deprecated. Use %r instead." % (order_list, new_order_list), DeprecationWarning)
40         return new_order_list
41
42 def orderfield2column(f, opts):
43     try:
44         return opts.get_field(f, False).column
45     except FieldDoesNotExist:
46         return f
47
48 def orderlist2sql(order_list, opts, prefix=''):
49     if prefix.endswith('.'):
50         prefix = backend.quote_name(prefix[:-1]) + '.'
51     output = []
52     for f in handle_legacy_orderlist(order_list):
53         if f.startswith('-'):
54             output.append('%s%s DESC' % (prefix, backend.quote_name(orderfield2column(f[1:], opts))))
55         elif f == '?':
56             output.append(backend.get_random_function_sql())
57         else:
58             output.append('%s%s ASC' % (prefix, backend.quote_name(orderfield2column(f, opts))))
59     return ', '.join(output)
60
61 def quote_only_if_word(word):
62     if ' ' in word:
63         return word
64     else:
65         return backend.quote_name(word)
66
67 class QuerySet(object):
68     "Represents a lazy database lookup for a set of objects"
69     def __init__(self, model=None):
70         self.model = model
71         self._filters = Q()
72         self._order_by = None        # Ordering, e.g. ('date', '-name'). If None, use model's ordering.
73         self._select_related = False # Whether to fill cache for related objects.
74         self._distinct = False       # Whether the query should use SELECT DISTINCT.
75         self._select = {}            # Dictionary of attname -> SQL.
76         self._where = []             # List of extra WHERE clauses to use.
77         self._params = []            # List of params to use for extra WHERE clauses.
78         self._tables = []            # List of extra tables to use.
79         self._offset = None          # OFFSET clause
80         self._limit = None           # LIMIT clause
81         self._result_cache = None
82
83     ########################
84     # PYTHON MAGIC METHODS #
85     ########################
86
87     def __repr__(self):
88         return repr(self._get_data())
89
90     def __len__(self):
91         return len(self._get_data())
92
93     def __iter__(self):
94         return iter(self._get_data())
95
96     def __getitem__(self, k):
97         "Retrieve an item or slice from the set of results."
98         if self._result_cache is None:
99             if isinstance(k, slice):
100                 # Offset:
101                 if self._offset is None:
102                     offset = k.start
103                 elif k.start is None:
104                     offset = self._offset
105                 else:
106                     offset = self._offset + k.start
107                 # Now adjust offset to the bounds of any existing limit:
108                 if self._limit is not None and k.start is not None:
109                     limit = self._limit - k.start
110                 else:
111                     limit = self._limit
112
113                 # Limit:
114                 if k.stop is not None and k.start is not None:
115                     if limit is None:
116                         limit = k.stop - k.start
117                     else:
118                         limit = min((k.stop - k.start), limit)
119                 else:
120                     if limit is None:
121                         limit = k.stop
122                     else:
123                         if k.stop is not None:
124                             limit = min(k.stop, limit)
125
126                 if k.step is None:
127                     return self._clone(_offset=offset, _limit=limit)
128                 else:
129                     return list(self._clone(_offset=offset, _limit=limit))[::k.step]
130             else:
131                 return self._clone(_offset=k, _limit=1).get()
132         else:
133             return self._result_cache[k]
134
135     def __and__(self, other):
136         combined = self._combine(other)
137         combined._filters = self._filters & other._filters
138         return combined
139
140     def __or__(self, other):
141         combined = self._combine(other)
142         combined._filters = self._filters | other._filters
143         return combined
144
145     ####################################
146     # METHODS THAT DO DATABASE QUERIES #
147     ####################################
148
149     def iterator(self):
150         "Performs the SELECT database lookup of this QuerySet."
151         # self._select is a dictionary, and dictionaries' key order is
152         # undefined, so we convert it to a list of tuples.
153         extra_select = self._select.items()
154
155         cursor = connection.cursor()
156         select, sql, params = self._get_sql_clause()
157         cursor.execute("SELECT " + (self._distinct and "DISTINCT " or "") + ",".join(select) + sql, params)
158         fill_cache = self._select_related
159         index_end = len(self.model._meta.fields)
160         while 1:
161             rows = cursor.fetchmany(GET_ITERATOR_CHUNK_SIZE)
162             if not rows:
163                 raise StopIteration
164             for row in rows:
165                 if fill_cache:
166                     obj, index_end = get_cached_row(self.model, row, 0)
167                 else:
168                     obj = self.model(*row[:index_end])
169                 for i, k in enumerate(extra_select):
170                     setattr(obj, k[0], row[index_end+i])
171                 yield obj
172
173     def count(self):
174         "Performs a SELECT COUNT() and returns the number of records as an integer."
175         counter = self._clone()
176         counter._order_by = ()
177         counter._offset = None
178         counter._limit = None
179         counter._select_related = False
180         select, sql, params = counter._get_sql_clause()
181         cursor = connection.cursor()
182         cursor.execute("SELECT COUNT(*)" + sql, params)
183         return cursor.fetchone()[0]
184
185     def get(self, *args, **kwargs):
186         "Performs the SELECT and returns a single object matching the given keyword arguments."
187         clone = self.filter(*args, **kwargs)
188         if not clone._order_by:
189             clone._order_by = ()
190         obj_list = list(clone)
191         if len(obj_list) < 1:
192             raise self.model.DoesNotExist, "%s does not exist for %s" % (self.model._meta.object_name, kwargs)
193         assert len(obj_list) == 1, "get() returned more than one %s -- it returned %s! Lookup parameters were %s" % (self.model._meta.object_name, len(obj_list), kwargs)
194         return obj_list[0]
195
196     def latest(self, field_name=None):
197         """
198         Returns the latest object, according to the model's 'get_latest_by'
199         option or optional given field_name.
200         """
201         latest_by = field_name or self.model._meta.get_latest_by
202         assert bool(latest_by), "latest() requires either a field_name parameter or 'get_latest_by' in the model"
203         assert self._limit is None and self._offset is None, \
204                 "Cannot change a query once a slice has been taken."
205         return self._clone(_limit=1, _order_by=('-'+latest_by,)).get()
206
207     def in_bulk(self, id_list):
208         """
209         Returns a dictionary mapping each of the given IDs to the object with
210         that ID.
211         """
212         assert self._limit is None and self._offset is None, \
213                 "Cannot use 'limit' or 'offset' with in_bulk"
214         assert isinstance(id_list, (tuple,  list)), "in_bulk() must be provided with a list of IDs."
215         id_list = list(id_list)
216         if id_list == []:
217             return {}
218         qs = self._clone()
219         qs._where.append("%s.%s IN (%s)" % (backend.quote_name(self.model._meta.db_table), backend.quote_name(self.model._meta.pk.column), ",".join(['%s'] * len(id_list))))
220         qs._params.extend(id_list)
221         return dict([(obj._get_pk_val(), obj) for obj in qs.iterator()])
222
223     def delete(self):
224         """
225         Deletes the records in the current QuerySet.
226         """
227         assert self._limit is None and self._offset is None, \
228             "Cannot use 'limit' or 'offset' with delete."
229
230         del_query = self._clone()
231
232         # disable non-supported fields
233         del_query._select_related = False
234         del_query._order_by = []
235
236         # Delete objects in chunks to prevent an the list of
237         # related objects from becoming too long
238         more_objects = True
239         while more_objects:
240             # Collect all the objects to be deleted in this chunk, and all the objects
241             # that are related to the objects that are to be deleted
242             seen_objs = SortedDict()
243             more_objects = False
244             for object in del_query[0:GET_ITERATOR_CHUNK_SIZE]:
245                 more_objects = True
246                 object._collect_sub_objects(seen_objs)
247
248             # If one or more objects were found, delete them.
249             # Otherwise, stop looping.
250             if more_objects:
251                 delete_objects(seen_objs)
252
253         # Clear the result cache, in case this QuerySet gets reused.
254         self._result_cache = None
255     delete.alters_data = True
256
257     ##################################################
258     # PUBLIC METHODS THAT RETURN A QUERYSET SUBCLASS #
259     ##################################################
260
261     def values(self, *fields):
262         return self._clone(klass=ValuesQuerySet, _fields=fields)
263
264     def dates(self, field_name, kind, order='ASC'):
265         """
266         Returns a list of datetime objects representing all available dates
267         for the given field_name, scoped to 'kind'.
268         """
269         assert kind in ("month", "year", "day"), "'kind' must be one of 'year', 'month' or 'day'."
270         assert order in ('ASC', 'DESC'), "'order' must be either 'ASC' or 'DESC'."
271         # Let the FieldDoesNotExist exception propagate.
272         field = self.model._meta.get_field(field_name, many_to_many=False)
273         assert isinstance(field, DateField), "%r isn't a DateField." % field_name
274         return self._clone(klass=DateQuerySet, _field=field, _kind=kind, _order=order)
275
276     ##################################################################
277     # PUBLIC METHODS THAT ALTER ATTRIBUTES AND RETURN A NEW QUERYSET #
278     ##################################################################
279
280     def filter(self, *args, **kwargs):
281         "Returns a new QuerySet instance with the args ANDed to the existing set."
282         return self._filter_or_exclude(Q, *args, **kwargs)
283
284     def exclude(self, *args, **kwargs):
285         "Returns a new QuerySet instance with NOT (args) ANDed to the existing set."
286         return self._filter_or_exclude(QNot, *args, **kwargs)
287
288     def _filter_or_exclude(self, qtype, *args, **kwargs):
289         if len(args) > 0 or len(kwargs) > 0:
290             assert self._limit is None and self._offset is None, \
291                 "Cannot filter a query once a slice has been taken."
292
293         clone = self._clone()
294         if len(kwargs) > 0:
295             clone._filters = clone._filters & qtype(**kwargs)
296         if len(args) > 0:
297             clone._filters = clone._filters & reduce(operator.and_, args)
298         return clone
299
300     def select_related(self, true_or_false=True):
301         "Returns a new QuerySet instance with '_select_related' modified."
302         return self._clone(_select_related=true_or_false)
303
304     def order_by(self, *field_names):
305         "Returns a new QuerySet instance with the ordering changed."
306         assert self._limit is None and self._offset is None, \
307                 "Cannot reorder a query once a slice has been taken."
308         return self._clone(_order_by=field_names)
309
310     def distinct(self, true_or_false=True):
311         "Returns a new QuerySet instance with '_distinct' modified."
312         return self._clone(_distinct=true_or_false)
313
314     def extra(self, select=None, where=None, params=None, tables=None):
315         assert self._limit is None and self._offset is None, \
316                 "Cannot change a query once a slice has been taken"
317         clone = self._clone()
318         if select: clone._select.update(select)
319         if where: clone._where.extend(where)
320         if params: clone._params.extend(params)
321         if tables: clone._tables.extend(tables)
322         return clone
323
324     ###################
325     # PRIVATE METHODS #
326     ###################
327
328     def _clone(self, klass=None, **kwargs):
329         if klass is None:
330             klass = self.__class__
331         c = klass()
332         c.model = self.model
333         c._filters = self._filters
334         c._order_by = self._order_by
335         c._select_related = self._select_related
336         c._distinct = self._distinct
337         c._select = self._select.copy()
338         c._where = self._where[:]
339         c._params = self._params[:]
340         c._tables = self._tables[:]
341         c._offset = self._offset
342         c._limit = self._limit
343         c.__dict__.update(kwargs)
344         return c
345
346     def _combine(self, other):
347         assert self._limit is None and self._offset is None \
348             and other._limit is None and other._offset is None, \
349             "Cannot combine queries once a slice has been taken."
350         assert self._distinct == other._distinct, \
351             "Cannot combine a unique query with a non-unique query"
352         #  use 'other's order by
353         #  (so that A.filter(args1) & A.filter(args2) does the same as
354         #   A.filter(args1).filter(args2)
355         combined = other._clone()
356         # If 'self' is ordered and 'other' isn't, propagate 'self's ordering
357         if (self._order_by is not None and len(self._order_by) > 0) and \
358            (combined._order_by is None or len(combined._order_by) == 0):
359             combined._order_by = self._order_by
360         return combined
361
362     def _get_data(self):
363         if self._result_cache is None:
364             self._result_cache = list(self.iterator())
365         return self._result_cache
366
367     def _get_sql_clause(self):
368         opts = self.model._meta
369
370         # Construct the fundamental parts of the query: SELECT X FROM Y WHERE Z.
371         select = ["%s.%s" % (backend.quote_name(opts.db_table), backend.quote_name(f.column)) for f in opts.fields]
372         tables = [quote_only_if_word(t) for t in self._tables]
373         joins = SortedDict()
374         where = self._where[:]
375         params = self._params[:]
376
377         # Convert self._filters into SQL.
378         tables2, joins2, where2, params2 = self._filters.get_sql(opts)
379         tables.extend(tables2)
380         joins.update(joins2)
381         where.extend(where2)
382         params.extend(params2)
383
384         # Add additional tables and WHERE clauses based on select_related.
385         if self._select_related:
386             fill_table_cache(opts, select, tables, where, opts.db_table, [opts.db_table])
387
388         # Add any additional SELECTs.
389         if self._select:
390             select.extend(['(%s) AS %s' % (quote_only_if_word(s[1]), backend.quote_name(s[0])) for s in self._select.items()])
391
392         # Start composing the body of the SQL statement.
393         sql = [" FROM", backend.quote_name(opts.db_table)]
394
395         # Compose the join dictionary into SQL describing the joins.
396         if joins:
397             sql.append(" ".join(["%s %s AS %s ON %s" % (join_type, table, alias, condition)
398                             for (alias, (table, join_type, condition)) in joins.items()]))
399
400         # Compose the tables clause into SQL.
401         if tables:
402             sql.append(", " + ", ".join(tables))
403
404         # Compose the where clause into SQL.
405         if where:
406             sql.append(where and "WHERE " + " AND ".join(where))
407
408         # ORDER BY clause
409         order_by = []
410         if self._order_by is not None:
411             ordering_to_use = self._order_by
412         else:
413             ordering_to_use = opts.ordering
414         for f in handle_legacy_orderlist(ordering_to_use):
415             if f == '?': # Special case.
416                 order_by.append(backend.get_random_function_sql())
417             else:
418                 if f.startswith('-'):
419                     col_name = f[1:]
420                     order = "DESC"
421                 else:
422                     col_name = f
423                     order = "ASC"
424                 if "." in col_name:
425                     table_prefix, col_name = col_name.split('.', 1)
426                     table_prefix = backend.quote_name(table_prefix) + '.'
427                 else:
428                     # Use the database table as a column prefix if it wasn't given,
429                     # and if the requested column isn't a custom SELECT.
430                     if "." not in col_name and col_name not in (self._select or ()):
431                         table_prefix = backend.quote_name(opts.db_table) + '.'
432                     else:
433                         table_prefix = ''
434                 order_by.append('%s%s %s' % (table_prefix, backend.quote_name(orderfield2column(col_name, opts)), order))
435         if order_by:
436             sql.append("ORDER BY " + ", ".join(order_by))
437
438         # LIMIT and OFFSET clauses
439         if self._limit is not None:
440             sql.append("%s " % backend.get_limit_offset_sql(self._limit, self._offset))
441         else:
442             assert self._offset is None, "'offset' is not allowed without 'limit'"
443
444         return select, " ".join(sql), params
445
446 class ValuesQuerySet(QuerySet):
447     def iterator(self):
448         # select_related and select aren't supported in values().
449         self._select_related = False
450         self._select = {}
451
452         # self._fields is a list of field names to fetch.
453         if self._fields:
454             columns = [self.model._meta.get_field(f, many_to_many=False).column for f in self._fields]
455             field_names = self._fields
456         else: # Default to all fields.
457             columns = [f.column for f in self.model._meta.fields]
458             field_names = [f.attname for f in self.model._meta.fields]
459
460         cursor = connection.cursor()
461         select, sql, params = self._get_sql_clause()
462         select = ['%s.%s' % (backend.quote_name(self.model._meta.db_table), backend.quote_name(c)) for c in columns]
463         cursor.execute("SELECT " + (self._distinct and "DISTINCT " or "") + ",".join(select) + sql, params)
464         while 1:
465             rows = cursor.fetchmany(GET_ITERATOR_CHUNK_SIZE)
466             if not rows:
467                 raise StopIteration
468             for row in rows:
469                 yield dict(zip(field_names, row))
470
471     def _clone(self, klass=None, **kwargs):
472         c = super(ValuesQuerySet, self)._clone(klass, **kwargs)
473         c._fields = self._fields[:]
474         return c
475
476 class DateQuerySet(QuerySet):
477     def iterator(self):
478         from django.db.backends.util import typecast_timestamp
479         self._order_by = () # Clear this because it'll mess things up otherwise.
480         if self._field.null:
481             date_query._where.append('%s.%s IS NOT NULL' % \
482                 (backend.quote_name(self.model._meta.db_table), backend.quote_name(self._field.column)))
483         select, sql, params = self._get_sql_clause()
484         sql = 'SELECT %s %s GROUP BY 1 ORDER BY 1 %s' % \
485             (backend.get_date_trunc_sql(self._kind, '%s.%s' % (backend.quote_name(self.model._meta.db_table),
486             backend.quote_name(self._field.column))), sql, self._order)
487         cursor = connection.cursor()
488         cursor.execute(sql, params)
489         # We have to manually run typecast_timestamp(str()) on the results, because
490         # MySQL doesn't automatically cast the result of date functions as datetime
491         # objects -- MySQL returns the values as strings, instead.
492         return [typecast_timestamp(str(row[0])) for row in cursor.fetchall()]
493
494     def _clone(self, klass=None, **kwargs):
495         c = super(DateQuerySet, self)._clone(klass, **kwargs)
496         c._field = self._field
497         c._kind = self._kind
498         c._order = self._order
499         return c
500
501 class QOperator:
502     "Base class for QAnd and QOr"
503     def __init__(self, *args):
504         self.args = args
505
506     def get_sql(self, opts):
507         tables, joins, where, params = [], SortedDict(), [], []
508         for val in self.args:
509             tables2, joins2, where2, params2 = val.get_sql(opts)
510             tables.extend(tables2)
511             joins.update(joins2)
512             where.extend(where2)
513             params.extend(params2)
514         if where:
515             return tables, joins, ['(%s)' % self.operator.join(where)], params
516         return tables, joins, [], params
517
518 class QAnd(QOperator):
519     "Encapsulates a combined query that uses 'AND'."
520     operator = ' AND '
521     def __or__(self, other):
522         return QOr(self, other)
523
524     def __and__(self, other):
525         if isinstance(other, QAnd):
526             return QAnd(*(self.args+other.args))
527         elif isinstance(other, (Q, QOr)):
528             return QAnd(*(self.args+(other,)))
529         else:
530             raise TypeError, other
531
532 class QOr(QOperator):
533     "Encapsulates a combined query that uses 'OR'."
534     operator = ' OR '
535     def __and__(self, other):
536         return QAnd(self, other)
537
538     def __or__(self, other):
539         if isinstance(other, QOr):
540             return QOr(*(self.args+other.args))
541         elif isinstance(other, (Q, QAnd)):
542             return QOr(*(self.args+(other,)))
543         else:
544             raise TypeError, other
545
546 class Q(object):
547     "Encapsulates queries as objects that can be combined logically."
548     def __init__(self, **kwargs):
549         self.kwargs = kwargs
550
551     def __and__(self, other):
552         return QAnd(self, other)
553
554     def __or__(self, other):
555         return QOr(self, other)
556
557     def get_sql(self, opts):
558         return parse_lookup(self.kwargs.items(), opts)
559
560 class QNot(Q):
561     "Encapsulates NOT (...) queries as objects"
562
563     def get_sql(self, opts):
564         tables, joins, where, params = super(QNot, self).get_sql(opts)
565         where2 = ['(NOT (%s))' % " AND ".join(where)]
566         return tables, joins, where2, params
567
568 def get_where_clause(lookup_type, table_prefix, field_name, value):
569     if table_prefix.endswith('.'):
570         table_prefix = backend.quote_name(table_prefix[:-1])+'.'
571     field_name = backend.quote_name(field_name)
572     try:
573         return '%s%s %s' % (table_prefix, field_name, (backend.OPERATOR_MAPPING[lookup_type] % '%s'))
574     except KeyError:
575         pass
576     if lookup_type == 'in':
577         return '%s%s IN (%s)' % (table_prefix, field_name, ','.join(['%s' for v in value]))
578     elif lookup_type == 'range':
579         return '%s%s BETWEEN %%s AND %%s' % (table_prefix, field_name)
580     elif lookup_type in ('year', 'month', 'day'):
581         return "%s = %%s" % backend.get_date_extract_sql(lookup_type, table_prefix + field_name)
582     elif lookup_type == 'isnull':
583         return "%s%s IS %sNULL" % (table_prefix, field_name, (not value and 'NOT ' or ''))
584     raise TypeError, "Got invalid lookup_type: %s" % repr(lookup_type)
585
586 def get_cached_row(klass, row, index_start):
587     "Helper function that recursively returns an object with cache filled"
588     index_end = index_start + len(klass._meta.fields)
589     obj = klass(*row[index_start:index_end])
590     for f in klass._meta.fields:
591         if f.rel and not f.null:
592             rel_obj, index_end = get_cached_row(f.rel.to, row, index_end)
593             setattr(obj, f.get_cache_name(), rel_obj)
594     return obj, index_end
595
596 def fill_table_cache(opts, select, tables, where, old_prefix, cache_tables_seen):
597     """
598     Helper function that recursively populates the select, tables and where (in
599     place) for fill-cache queries.
600     """
601     for f in opts.fields:
602         if f.rel and not f.null:
603             db_table = f.rel.to._meta.db_table
604             if db_table not in cache_tables_seen:
605                 tables.append(backend.quote_name(db_table))
606             else: # The table was already seen, so give it a table alias.
607                 new_prefix = '%s%s' % (db_table, len(cache_tables_seen))
608                 tables.append('%s %s' % (backend.quote_name(db_table), backend.quote_name(new_prefix)))
609                 db_table = new_prefix
610             cache_tables_seen.append(db_table)
611             where.append('%s.%s = %s.%s' % \
612                 (backend.quote_name(old_prefix), backend.quote_name(f.column),
613                 backend.quote_name(db_table), backend.quote_name(f.rel.get_related_field().column)))
614             select.extend(['%s.%s' % (backend.quote_name(db_table), backend.quote_name(f2.column)) for f2 in f.rel.to._meta.fields])
615             fill_table_cache(f.rel.to._meta, select, tables, where, db_table, cache_tables_seen)
616
617 def parse_lookup(kwarg_items, opts):
618     # Helper function that handles converting API kwargs
619     # (e.g. "name__exact": "tom") to SQL.
620
621     # 'joins' is a sorted dictionary describing the tables that must be joined
622     # to complete the query. The dictionary is sorted because creation order
623     # is significant; it is a dictionary to ensure uniqueness of alias names.
624     #
625     # Each key-value pair follows the form
626     #   alias: (table, join_type, condition)
627     # where
628     #   alias is the AS alias for the joined table
629     #   table is the actual table name to be joined
630     #   join_type is the type of join (INNER JOIN, LEFT OUTER JOIN, etc)
631     #   condition is the where-like statement over which narrows the join.
632     #   alias will be derived from the lookup list name.
633     #
634     # At present, this method only every returns INNER JOINs; the option is
635     # there for others to implement custom Q()s, etc that return other join
636     # types.
637     tables, joins, where, params = [], SortedDict(), [], []
638
639     for kwarg, value in kwarg_items:
640         if value is not None:
641             path = kwarg.split(LOOKUP_SEPARATOR)
642             # Extract the last elements of the kwarg.
643             # The very-last is the clause (equals, like, etc).
644             # The second-last is the table column on which the clause is
645             # to be performed.
646             # The exceptions to this are:
647             # 1)  "pk", which is an implicit id__exact;
648             #     if we find "pk", make the clause "exact', and insert
649             #     a dummy name of None, which we will replace when
650             #     we know which table column to grab as the primary key.
651             # 2)  If there is only one part, assume it to be an __exact
652             clause = path.pop()
653             if clause == 'pk':
654                 clause = 'exact'
655                 path.append(None)
656             elif len(path) == 0:
657                 path.append(clause)
658                 clause = 'exact'
659
660             if len(path) < 1:
661                 raise TypeError, "Cannot parse keyword query %r" % kwarg
662
663             tables2, joins2, where2, params2 = lookup_inner(path, clause, value, opts, opts.db_table, None)
664             tables.extend(tables2)
665             joins.update(joins2)
666             where.extend(where2)
667             params.extend(params2)
668     return tables, joins, where, params
669
670 class FieldFound(Exception):
671     "Exception used to short circuit field-finding operations."
672     pass
673
674 def find_field(name, field_list, related_query):
675     """
676     Finds a field with a specific name in a list of field instances.
677     Returns None if there are no matches, or several matches.
678     """
679     if related_query:
680         matches = [f for f in field_list if f.field.related_query_name() == name]
681     else:
682         matches = [f for f in field_list if f.name == name]
683     if len(matches) != 1:
684         return None
685     return matches[0]
686
687 def lookup_inner(path, clause, value, opts, table, column):
688     tables, joins, where, params = [], SortedDict(), [], []
689     current_opts = opts
690     current_table = table
691     current_column = column
692     intermediate_table = None
693     join_required = False
694
695     name = path.pop(0)
696     # Has the primary key been requested? If so, expand it out
697     # to be the name of the current class' primary key
698     if name is None:
699         name = current_opts.pk.name
700
701     # Try to find the name in the fields associated with the current class
702     try:
703         # Does the name belong to a defined many-to-many field?
704         field = find_field(name, current_opts.many_to_many, False)
705         if field:
706             new_table = current_table + LOOKUP_SEPARATOR + name
707             new_opts = field.rel.to._meta
708             new_column = new_opts.pk.column
709
710             # Need to create an intermediate table join over the m2m table
711             # This process hijacks current_table/column to point to the
712             # intermediate table.
713             current_table = "m2m_" + new_table
714             intermediate_table = field.m2m_db_table()
715             join_column = field.m2m_reverse_name()
716             intermediate_column = field.m2m_column_name()
717
718             raise FieldFound
719
720         # Does the name belong to a reverse defined many-to-many field?
721         field = find_field(name, current_opts.get_all_related_many_to_many_objects(), True)
722         if field:
723             new_table = current_table + LOOKUP_SEPARATOR + name
724             new_opts = field.opts
725             new_column = new_opts.pk.column
726
727             # Need to create an intermediate table join over the m2m table.
728             # This process hijacks current_table/column to point to the
729             # intermediate table.
730             current_table = "m2m_" + new_table
731             intermediate_table = field.field.m2m_db_table()
732             join_column = field.field.m2m_column_name()
733             intermediate_column = field.field.m2m_reverse_name()
734
735             raise FieldFound
736
737         # Does the name belong to a one-to-many field?
738         field = find_field(name, current_opts.get_all_related_objects(), True)
739         if field:
740             new_table = table + LOOKUP_SEPARATOR + name
741             new_opts = field.opts
742             new_column = field.field.column
743             join_column = opts.pk.column
744
745             # 1-N fields MUST be joined, regardless of any other conditions.
746             join_required = True
747
748             raise FieldFound
749
750         # Does the name belong to a one-to-one, many-to-one, or regular field?
751         field = find_field(name, current_opts.fields, False)
752         if field:
753             if field.rel: # One-to-One/Many-to-one field
754                 new_table = current_table + LOOKUP_SEPARATOR + name
755                 new_opts = field.rel.to._meta
756                 new_column = new_opts.pk.column
757                 join_column = field.column
758
759             raise FieldFound
760
761     except FieldFound: # Match found, loop has been shortcut.
762         pass
763     except: # Any other exception; rethrow
764         raise
765     else: # No match found.
766         raise TypeError, "Cannot resolve keyword '%s' into field" % name
767
768     # Check to see if an intermediate join is required between current_table
769     # and new_table.
770     if intermediate_table:
771         joins[backend.quote_name(current_table)] = (
772             backend.quote_name(intermediate_table),
773             "LEFT OUTER JOIN",
774             "%s.%s = %s.%s" % \
775                 (backend.quote_name(table),
776                 backend.quote_name(current_opts.pk.column),
777                 backend.quote_name(current_table),
778                 backend.quote_name(intermediate_column))
779         )
780
781     if path:
782         if len(path) == 1 and path[0] in (new_opts.pk.name, None) \
783             and clause in ('exact', 'isnull') and not join_required:
784             # If the last name query is for a key, and the search is for
785             # isnull/exact, then the current (for N-1) or intermediate
786             # (for N-N) table can be used for the search - no need to join an
787             # extra table just to check the primary key.
788             new_table = current_table
789         else:
790             # There are 1 or more name queries pending, and we have ruled out
791             # any shortcuts; therefore, a join is required.
792             joins[backend.quote_name(new_table)] = (
793                 backend.quote_name(new_opts.db_table),
794                 "INNER JOIN",
795                 "%s.%s = %s.%s" %
796                     (backend.quote_name(current_table),
797                     backend.quote_name(join_column),
798                     backend.quote_name(new_table),
799                     backend.quote_name(new_column))
800             )
801             # If we have made the join, we don't need to tell subsequent
802             # recursive calls about the column name we joined on.
803             join_column = None
804
805         # There are name queries remaining. Recurse deeper.
806         tables2, joins2, where2, params2 = lookup_inner(path, clause, value, new_opts, new_table, join_column)
807
808         tables.extend(tables2)
809         joins.update(joins2)
810         where.extend(where2)
811         params.extend(params2)
812     else:
813         # Evaluate clause on current table.
814         if name in (current_opts.pk.name, None) and clause in ('exact', 'isnull') and current_column:
815             # If this is an exact/isnull key search, and the last pass
816             # found/introduced a current/intermediate table that we can use to
817             # optimize the query, then use that column name.
818             column = current_column
819         else:
820             column = field.column
821
822         where.append(get_where_clause(clause, current_table + '.', column, value))
823         params.extend(field.get_db_prep_lookup(clause, value))
824
825     return tables, joins, where, params
826
827 def delete_objects(seen_objs):
828     "Iterate through a list of seen classes, and remove any instances that are referred to"
829     ordered_classes = seen_objs.keys()
830     ordered_classes.reverse()
831
832     cursor = connection.cursor()
833
834     for cls in ordered_classes:
835         seen_objs[cls] = seen_objs[cls].items()
836         seen_objs[cls].sort()
837
838         # Pre notify all instances to be deleted
839         for pk_val, instance in seen_objs[cls]:
840             dispatcher.send(signal=signals.pre_delete, sender=cls, instance=instance)
841
842         pk_list = [pk for pk,instance in seen_objs[cls]]
843         for related in cls._meta.get_all_related_many_to_many_objects():
844             for offset in range(0, len(pk_list), GET_ITERATOR_CHUNK_SIZE):
845                 cursor.execute("DELETE FROM %s WHERE %s IN (%s)" % \
846                     (backend.quote_name(related.field.m2m_db_table()),
847                         backend.quote_name(related.field.m2m_reverse_name()),
848                         ','.join(['%s' for pk in pk_list[offset:offset+GET_ITERATOR_CHUNK_SIZE]])),
849                     pk_list[offset:offset+GET_ITERATOR_CHUNK_SIZE])
850         for f in cls._meta.many_to_many:
851             for offset in range(0, len(pk_list), GET_ITERATOR_CHUNK_SIZE):
852                 cursor.execute("DELETE FROM %s WHERE %s IN (%s)" % \
853                     (backend.quote_name(f.m2m_db_table()),
854                         backend.quote_name(f.m2m_column_name()),
855                         ','.join(['%s' for pk in pk_list[offset:offset+GET_ITERATOR_CHUNK_SIZE]])),
856                     pk_list[offset:offset+GET_ITERATOR_CHUNK_SIZE])
857         for field in cls._meta.fields:
858             if field.rel and field.null and field.rel.to in seen_objs:
859                 for offset in range(0, len(pk_list), GET_ITERATOR_CHUNK_SIZE):
860                     cursor.execute("UPDATE %s SET %s=NULL WHERE %s IN (%s)" % \
861                         (backend.quote_name(cls._meta.db_table),
862                             backend.quote_name(field.column),
863                             backend.quote_name(cls._meta.pk.column),
864                             ','.join(['%s' for pk in pk_list[offset:offset+GET_ITERATOR_CHUNK_SIZE]])),
865                         pk_list[offset:offset+GET_ITERATOR_CHUNK_SIZE])
866
867     # Now delete the actual data
868     for cls in ordered_classes:
869         seen_objs[cls].reverse()
870         pk_list = [pk for pk,instance in seen_objs[cls]]
871         for offset in range(0, len(pk_list), GET_ITERATOR_CHUNK_SIZE):
872             cursor.execute("DELETE FROM %s WHERE %s IN (%s)" % \
873                 (backend.quote_name(cls._meta.db_table),
874                     backend.quote_name(cls._meta.pk.column),
875                     ','.join(['%s' for pk in pk_list[offset:offset+GET_ITERATOR_CHUNK_SIZE]])),
876                 pk_list[offset:offset+GET_ITERATOR_CHUNK_SIZE])
877
878         # Last cleanup; set NULLs where there once was a reference to the object,
879         # NULL the primary key of the found objects, and perform post-notification.
880         for pk_val, instance in seen_objs[cls]:
881             for field in cls._meta.fields:
882                 if field.rel and field.null and field.rel.to in seen_objs:
883                     setattr(instance, field.attname, None)
884
885             setattr(instance, cls._meta.pk.attname, None)
886             dispatcher.send(signal=signals.post_delete, sender=cls, instance=instance)
887
888     transaction.commit_unless_managed()
Note: See TracBrowser for help on using the browser.