1
0
mirror of https://github.com/django/django.git synced 2025-07-05 18:29:11 +00:00

queryset-refactor: Fixed up extra(select=...) calls with parameters so that the

parameters are substituted in correctly in all cases. This introduces an extra
argument to extra() for this purpose; no alternative there.

Also fixed values() to work if you don't specify *all* the extra select aliases
in the values() call.

Refs #3141.


git-svn-id: http://code.djangoproject.com/svn/django/branches/queryset-refactor@7340 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Malcolm Tredinnick 2008-03-20 19:16:04 +00:00
parent e2dfad15f1
commit 04da22633f
4 changed files with 76 additions and 58 deletions

View File

@ -142,16 +142,16 @@ class _QuerySet(object):
else: else:
requested = None requested = None
max_depth = self.query.max_depth max_depth = self.query.max_depth
index_end = len(self.model._meta.fields)
extra_select = self.query.extra_select.keys() extra_select = self.query.extra_select.keys()
index_start = len(extra_select)
for row in self.query.results_iter(): for row in self.query.results_iter():
if fill_cache: if fill_cache:
obj, index_end = get_cached_row(self.model, row, 0, max_depth, obj, _ = get_cached_row(self.model, row, index_start,
requested=requested) max_depth, requested=requested)
else: else:
obj = self.model(*row[:index_end]) obj = self.model(*row[index_start:])
for i, k in enumerate(extra_select): for i, k in enumerate(extra_select):
setattr(obj, k, row[index_end + i]) setattr(obj, k, row[i])
yield obj yield obj
def count(self): def count(self):
@ -413,14 +413,14 @@ class _QuerySet(object):
return obj return obj
def extra(self, select=None, where=None, params=None, tables=None, def extra(self, select=None, where=None, params=None, tables=None,
order_by=None): order_by=None, select_params=None):
""" """
Add extra SQL fragments to the query. Add extra SQL fragments to the query.
""" """
assert self.query.can_filter(), \ assert self.query.can_filter(), \
"Cannot change a query once a slice has been taken" "Cannot change a query once a slice has been taken"
clone = self._clone() clone = self._clone()
clone.query.add_extra(select, where, params, tables, order_by) clone.query.add_extra(select, select_params, where, params, tables, order_by)
return clone return clone
def reverse(self): def reverse(self):
@ -475,9 +475,10 @@ class ValuesQuerySet(QuerySet):
return self.iterator() return self.iterator()
def iterator(self): def iterator(self):
self.field_names.extend([f for f in self.query.extra_select.keys()]) self.query.trim_extra_select(self.extra_names)
names = self.query.extra_select.keys() + self.field_names
for row in self.query.results_iter(): for row in self.query.results_iter():
yield dict(zip(self.field_names, row)) yield dict(zip(names, row))
def _setup_query(self): def _setup_query(self):
""" """
@ -487,6 +488,7 @@ class ValuesQuerySet(QuerySet):
Called by the _clone() method after initialising the rest of the Called by the _clone() method after initialising the rest of the
instance. instance.
""" """
self.extra_names = []
if self._fields: if self._fields:
if not self.query.extra_select: if not self.query.extra_select:
field_names = list(self._fields) field_names = list(self._fields)
@ -496,7 +498,9 @@ class ValuesQuerySet(QuerySet):
for f in self._fields: for f in self._fields:
if f in names: if f in names:
field_names.append(f) field_names.append(f)
elif not self.query.extra_select.has_key(f): elif self.query.extra_select.has_key(f):
self.extra_names.append(f)
else:
raise FieldDoesNotExist('%s has no field named %r' raise FieldDoesNotExist('%s has no field named %r'
% (self.model._meta.object_name, f)) % (self.model._meta.object_name, f))
else: else:
@ -513,7 +517,8 @@ class ValuesQuerySet(QuerySet):
""" """
c = super(ValuesQuerySet, self)._clone(klass, **kwargs) c = super(ValuesQuerySet, self)._clone(klass, **kwargs)
c._fields = self._fields[:] c._fields = self._fields[:]
c.field_names = self.field_names[:] c.field_names = self.field_names
c.extra_names = self.extra_names
if setup and hasattr(c, '_setup_query'): if setup and hasattr(c, '_setup_query'):
c._setup_query() c._setup_query()
return c return c

View File

@ -73,6 +73,7 @@ class Query(object):
# These are for extensions. The contents are more or less appended # These are for extensions. The contents are more or less appended
# verbatim to the appropriate clause. # verbatim to the appropriate clause.
self.extra_select = {} # Maps col_alias -> col_sql. self.extra_select = {} # Maps col_alias -> col_sql.
self.extra_select_params = ()
self.extra_tables = () self.extra_tables = ()
self.extra_where = () self.extra_where = ()
self.extra_params = () self.extra_params = ()
@ -150,6 +151,7 @@ class Query(object):
obj.select_related = self.select_related obj.select_related = self.select_related
obj.max_depth = self.max_depth obj.max_depth = self.max_depth
obj.extra_select = self.extra_select.copy() obj.extra_select = self.extra_select.copy()
obj.extra_select_params = self.extra_select_params
obj.extra_tables = self.extra_tables obj.extra_tables = self.extra_tables
obj.extra_where = self.extra_where obj.extra_where = self.extra_where
obj.extra_params = self.extra_params obj.extra_params = self.extra_params
@ -214,6 +216,7 @@ class Query(object):
# get_from_clause() for details. # get_from_clause() for details.
from_, f_params = self.get_from_clause() from_, f_params = self.get_from_clause()
where, w_params = self.where.as_sql(qn=self.quote_name_unless_alias) where, w_params = self.where.as_sql(qn=self.quote_name_unless_alias)
params = list(self.extra_select_params)
result = ['SELECT'] result = ['SELECT']
if self.distinct: if self.distinct:
@ -222,7 +225,7 @@ class Query(object):
result.append('FROM') result.append('FROM')
result.extend(from_) result.extend(from_)
params = list(f_params) params.extend(f_params)
if where: if where:
result.append('WHERE %s' % where) result.append('WHERE %s' % where)
@ -351,8 +354,8 @@ class Query(object):
the model. the model.
""" """
qn = self.quote_name_unless_alias qn = self.quote_name_unless_alias
result = [] result = ['(%s) AS %s' % (col, alias) for alias, col in self.extra_select.items()]
aliases = [] aliases = self.extra_select.keys()
if self.select: if self.select:
for col in self.select: for col in self.select:
if isinstance(col, (list, tuple)): if isinstance(col, (list, tuple)):
@ -364,12 +367,9 @@ class Query(object):
if hasattr(col, 'alias'): if hasattr(col, 'alias'):
aliases.append(col.alias) aliases.append(col.alias)
elif self.default_cols: elif self.default_cols:
result = self.get_default_columns(True) cols = self.get_default_columns(True)
aliases = result[:] result.extend(cols)
aliases.extend(cols)
result.extend(['(%s) AS %s' % (col, alias)
for alias, col in self.extra_select.items()])
aliases.extend(self.extra_select.keys())
self._select_aliases = set(aliases) self._select_aliases = set(aliases)
return result return result
@ -403,9 +403,9 @@ class Query(object):
def get_from_clause(self): def get_from_clause(self):
""" """
Returns a list of strings that are joined together to go after the Returns a list of strings that are joined together to go after the
"FROM" part of the query, as well as any extra parameters that need to "FROM" part of the query, as well as a list any extra parameters that
be included. Sub-classes, can override this to create a from-clause via need to be included. Sub-classes, can override this to create a
a "select", for example (e.g. CountQuery). from-clause via a "select", for example (e.g. CountQuery).
This should only be called after any SQL construction methods that This should only be called after any SQL construction methods that
might change the tables we need. This means the select columns and might change the tables we need. This means the select columns and
@ -1253,6 +1253,7 @@ class Query(object):
self.distinct = False self.distinct = False
self.select = [select] self.select = [select]
self.extra_select = {} self.extra_select = {}
self.extra_select_params = ()
def add_select_related(self, fields): def add_select_related(self, fields):
""" """
@ -1267,7 +1268,7 @@ class Query(object):
d = d.setdefault(part, {}) d = d.setdefault(part, {})
self.select_related = field_dict self.select_related = field_dict
def add_extra(self, select, where, params, tables, order_by): def add_extra(self, select, select_params, where, params, tables, order_by):
""" """
Adds data to the various extra_* attributes for user-created additions Adds data to the various extra_* attributes for user-created additions
to the query. to the query.
@ -1279,6 +1280,8 @@ class Query(object):
not isinstance(self.extra_select, SortedDict)): not isinstance(self.extra_select, SortedDict)):
self.extra_select = SortedDict(self.extra_select) self.extra_select = SortedDict(self.extra_select)
self.extra_select.update(select) self.extra_select.update(select)
if select_params:
self.extra_select_params += tuple(select_params)
if where: if where:
self.extra_where += tuple(where) self.extra_where += tuple(where)
if params: if params:
@ -1288,6 +1291,17 @@ class Query(object):
if order_by: if order_by:
self.extra_order_by = order_by self.extra_order_by = order_by
def trim_extra_select(self, names):
"""
Removes any aliases in the extra_select dictionary that aren't in
'names'.
This is needed if we are selecting certain values that don't incldue
all of the extra_select names.
"""
for key in set(self.extra_select).difference(set(names)):
del self.extra_select[key]
def set_start(self, start): def set_start(self, start):
""" """
Sets the table from which to start joining. The start position is Sets the table from which to start joining. The start position is

View File

@ -841,8 +841,9 @@ You can only refer to ``ForeignKey`` relations in the list of fields passed to
list of fields and the ``depth`` parameter in the same ``select_related()`` list of fields and the ``depth`` parameter in the same ``select_related()``
call, since they are conflicting options. call, since they are conflicting options.
``extra(select=None, where=None, params=None, tables=None, order_by=None)`` ``extra(select=None, where=None, params=None, tables=None, order_by=None,
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ select_params=None)``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Sometimes, the Django query syntax by itself can't easily express a complex Sometimes, the Django query syntax by itself can't easily express a complex
``WHERE`` clause. For these edge cases, Django provides the ``extra()`` ``WHERE`` clause. For these edge cases, Django provides the ``extra()``
@ -901,31 +902,18 @@ of the arguments is required, but you should use at least one of them.
**New in Django development version** **New in Django development version**
In some rare cases, you might wish to pass parameters to the SQL fragments In some rare cases, you might wish to pass parameters to the SQL fragments
in ``extra(select=...)```. Since the ``params`` attribute is a sequence in ``extra(select=...)```. For this purpose, use the ``select_params``
and the ``select`` attribute is a dictionary, some care is required so parameter. Since ``select_params`` is a sequence and the ``select``
that the parameters are matched up correctly with the extra select pieces. attribute is a dictionary, some care is required so that the parameters
Firstly, in this situation, you should use a are matched up correctly with the extra select pieces. In this situation,
``django.utils.datastructures.SortedDict`` for the ``select`` value, not you should use a ``django.utils.datastructures.SortedDict`` for the
just a normal Python dictionary. Secondly, make sure that your parameters ``select`` value, not just a normal Python dictionary.
for the ``select`` come first in the list and that you have not passed any
parameters to an earlier ``extra()`` call for this queryset.
This will work:: This will work, for example::
Blog.objects.extra( Blog.objects.extra(
select=SortedDict(('a', '%s'), ('b', '%s')), select=SortedDict(('a', '%s'), ('b', '%s')),
params=('one', 'two')) select_params=('one', 'two'))
... while this won't::
# Will not work!
Blog.objects.extra(where=['foo=%s'], params=('bar',)).extra(
select=SortedDict(('a', '%s'), ('b', '%s')),
params=('one', 'two'))
In the second example, the earlier ``params`` usage will mess up the later
one. So always put your extra select pieces in the first ``extra()`` call
if you need to use parameters in them.
``where`` / ``tables`` ``where`` / ``tables``
You can define explicit SQL ``WHERE`` clauses -- perhaps to perform You can define explicit SQL ``WHERE`` clauses -- perhaps to perform
@ -965,19 +953,18 @@ of the arguments is required, but you should use at least one of them.
time). time).
``params`` ``params``
The ``select`` and ``where`` parameters described above may use standard The ``where`` parameter described above may use standard Python database
Python database string placeholders -- ``'%s'`` to indicate parameters the string placeholders -- ``'%s'`` to indicate parameters the database engine
database engine should automatically quote. The ``params`` argument is a should automatically quote. The ``params`` argument is a list of any extra
list of any extra parameters to be substituted. parameters to be substituted.
Example:: Example::
Entry.objects.extra(where=['headline=%s'], params=['Lennon']) Entry.objects.extra(where=['headline=%s'], params=['Lennon'])
Always use ``params`` instead of embedding values directly into ``select`` Always use ``params`` instead of embedding values directly into ``where``
or ``where`` because ``params`` will ensure values are quoted correctly because ``params`` will ensure values are quoted correctly according to
according to your particular backend. (For example, quotes will be escaped your particular backend. (For example, quotes will be escaped correctly.)
correctly.)
Bad:: Bad::
@ -987,8 +974,9 @@ of the arguments is required, but you should use at least one of them.
Entry.objects.extra(where=['headline=%s'], params=['Lennon']) Entry.objects.extra(where=['headline=%s'], params=['Lennon'])
The combined number of placeholders in the list of strings for ``select`` **New in Django development version** The ``select_params`` argument to
or ``where`` should equal the number of values in the ``params`` list. ``extra()`` is new. Previously, you could attempt to pass parameters for
``select`` in the ``params`` argument, but it worked very unreliably.
QuerySet methods that do not return QuerySets QuerySet methods that do not return QuerySets
--------------------------------------------- ---------------------------------------------

View File

@ -282,6 +282,10 @@ Bug #1878, #2939
>>> xx.save() >>> xx.save()
>>> Item.objects.exclude(name='two').values('creator', 'name').distinct().count() >>> Item.objects.exclude(name='two').values('creator', 'name').distinct().count()
4 4
>>> Item.objects.exclude(name='two').extra(select={'foo': '%s'}, select_params=(1,)).values('creator', 'name', 'foo').distinct().count()
4
>>> Item.objects.exclude(name='two').extra(select={'foo': '%s'}, select_params=(1,)).values('creator', 'name').distinct().count()
4
>>> xx.delete() >>> xx.delete()
Bug #2253 Bug #2253
@ -386,6 +390,8 @@ AssertionError: Cannot combine queries on two different base models.
Bug #3141 Bug #3141
>>> Author.objects.extra(select={'foo': '1'}).count() >>> Author.objects.extra(select={'foo': '1'}).count()
4 4
>>> Author.objects.extra(select={'foo': '%s'}, select_params=(1,)).count()
4
Bug #2400 Bug #2400
>>> Author.objects.filter(item__isnull=True) >>> Author.objects.filter(item__isnull=True)
@ -462,6 +468,11 @@ True
>>> qs.extra(order_by=('-good', 'id')) >>> qs.extra(order_by=('-good', 'id'))
[<Ranking: 3: a1>, <Ranking: 2: a2>, <Ranking: 1: a3>] [<Ranking: 3: a1>, <Ranking: 2: a2>, <Ranking: 1: a3>]
# Despite having some extra aliases in the query, we can still omit them in a
# values() query.
>>> qs.values('id', 'rank').order_by('id')
[{'id': 1, 'rank': 2}, {'id': 2, 'rank': 1}, {'id': 3, 'rank': 3}]
Bugs #2874, #3002 Bugs #2874, #3002
>>> qs = Item.objects.select_related().order_by('note__note', 'name') >>> qs = Item.objects.select_related().order_by('note__note', 'name')
>>> list(qs) >>> list(qs)
@ -533,7 +544,7 @@ thus fail.)
# This slightly odd comparison works aorund the fact that PostgreSQL will # This slightly odd comparison works aorund the fact that PostgreSQL will
# return 'one' and 'two' as strings, not Unicode objects. It's a side-effect of # return 'one' and 'two' as strings, not Unicode objects. It's a side-effect of
# using constants here and not a real concern. # using constants here and not a real concern.
>>> d = Item.objects.extra(select=SortedDict(s), params=params).values('a', 'b')[0] >>> d = Item.objects.extra(select=SortedDict(s), select_params=params).values('a', 'b')[0]
>>> d == {'a': u'one', 'b': u'two'} >>> d == {'a': u'one', 'b': u'two'}
True True