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
This commit is contained in:
Russell Keith-Magee 2006-01-09 11:01:38 +00:00
parent e9a13940d3
commit dde6963869
5 changed files with 135 additions and 41 deletions

View File

@ -5,6 +5,7 @@ from django.db.models.query import Q, parse_lookup, fill_table_cache, get_cached
from django.db.models.query import handle_legacy_orderlist, orderlist2sql, orderfield2column from django.db.models.query import handle_legacy_orderlist, orderlist2sql, orderfield2column
from django.dispatch import dispatcher from django.dispatch import dispatcher
from django.db.models import signals from django.db.models import signals
from django.utils.datastructures import SortedDict
# Size of each "chunk" for get_iterator calls. # Size of each "chunk" for get_iterator calls.
# Larger values are slightly faster at the expense of more storage space. # Larger values are slightly faster at the expense of more storage space.
@ -47,7 +48,7 @@ class Manager(object):
self.creation_counter < klass._default_manager.creation_counter: self.creation_counter < klass._default_manager.creation_counter:
klass._default_manager = self klass._default_manager = self
def _get_sql_clause(self, **kwargs): def _get_sql_clause(self, *args, **kwargs):
def quote_only_if_word(word): def quote_only_if_word(word):
if ' ' in word: if ' ' in word:
return word return word
@ -59,12 +60,28 @@ class Manager(object):
# Construct the fundamental parts of the query: SELECT X FROM Y WHERE Z. # Construct the fundamental parts of the query: SELECT X FROM Y WHERE Z.
select = ["%s.%s" % (backend.quote_name(opts.db_table), backend.quote_name(f.column)) for f in opts.fields] select = ["%s.%s" % (backend.quote_name(opts.db_table), backend.quote_name(f.column)) for f in opts.fields]
tables = (kwargs.get('tables') and [quote_only_if_word(t) for t in kwargs['tables']] or []) tables = (kwargs.get('tables') and [quote_only_if_word(t) for t in kwargs['tables']] or [])
joins = SortedDict()
where = kwargs.get('where') and kwargs['where'][:] or [] where = kwargs.get('where') and kwargs['where'][:] or []
params = kwargs.get('params') and kwargs['params'][:] or [] params = kwargs.get('params') and kwargs['params'][:] or []
# Convert the kwargs into SQL. # Convert all the args into SQL.
tables2, joins, where2, params2 = parse_lookup(kwargs.items(), opts) table_count = 0
for arg in args:
# check that the provided argument is a Query (i.e., it has a get_sql method)
if not hasattr(arg, 'get_sql'):
raise TypeError, "'%s' is not a valid query argument" % str(arg)
tables2, joins2, where2, params2 = arg.get_sql(opts)
tables.extend(tables2) tables.extend(tables2)
joins.update(joins2)
where.extend(where2)
params.extend(params2)
# Convert the kwargs into SQL.
tables2, joins2, where2, params2 = parse_lookup(kwargs.items(), opts)
tables.extend(tables2)
joins.update(joins2)
where.extend(where2) where.extend(where2)
params.extend(params2) params.extend(params2)
@ -129,13 +146,13 @@ class Manager(object):
return select, " ".join(sql), params return select, " ".join(sql), params
def get_iterator(self, **kwargs): def get_iterator(self, *args, **kwargs):
# kwargs['select'] is a dictionary, and dictionaries' key order is # kwargs['select'] is a dictionary, and dictionaries' key order is
# undefined, so we convert it to a list of tuples internally. # undefined, so we convert it to a list of tuples internally.
kwargs['select'] = kwargs.get('select', {}).items() kwargs['select'] = kwargs.get('select', {}).items()
cursor = connection.cursor() cursor = connection.cursor()
select, sql, params = self._get_sql_clause(**kwargs) select, sql, params = self._get_sql_clause(*args, **kwargs)
cursor.execute("SELECT " + (kwargs.get('distinct') and "DISTINCT " or "") + ",".join(select) + sql, params) cursor.execute("SELECT " + (kwargs.get('distinct') and "DISTINCT " or "") + ",".join(select) + sql, params)
fill_cache = kwargs.get('select_related') fill_cache = kwargs.get('select_related')
index_end = len(self.klass._meta.fields) index_end = len(self.klass._meta.fields)
@ -152,35 +169,41 @@ class Manager(object):
setattr(obj, k[0], row[index_end+i]) setattr(obj, k[0], row[index_end+i])
yield obj yield obj
def get_list(self, **kwargs): def get_list(self, *args, **kwargs):
return list(self.get_iterator(**kwargs)) return list(self.get_iterator(*args, **kwargs))
def get_count(self, **kwargs): def get_count(self, *args, **kwargs):
kwargs['order_by'] = [] kwargs['order_by'] = []
kwargs['offset'] = None kwargs['offset'] = None
kwargs['limit'] = None kwargs['limit'] = None
kwargs['select_related'] = False kwargs['select_related'] = False
_, sql, params = self._get_sql_clause(**kwargs) _, sql, params = self._get_sql_clause(*args, **kwargs)
cursor = connection.cursor() cursor = connection.cursor()
cursor.execute("SELECT COUNT(*)" + sql, params) cursor.execute("SELECT COUNT(*)" + sql, params)
return cursor.fetchone()[0] return cursor.fetchone()[0]
def get_object(self, **kwargs): def get_object(self, *args, **kwargs):
obj_list = self.get_list(**kwargs) obj_list = self.get_list(*args, **kwargs)
if len(obj_list) < 1: if len(obj_list) < 1:
raise self.klass.DoesNotExist, "%s does not exist for %s" % (self.klass._meta.object_name, kwargs) raise self.klass.DoesNotExist, "%s does not exist for %s" % (self.klass._meta.object_name, kwargs)
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) 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)
return obj_list[0] return obj_list[0]
def get_in_bulk(self, *args, **kwargs): def get_in_bulk(self, *args, **kwargs):
id_list = args and args[0] or kwargs.get('id_list', []) # Separate any list arguments: the first list will be used as the id list; subsequent
assert id_list != [], "get_in_bulk() cannot be passed an empty list." # lists will be ignored.
id_args = filter(lambda arg: isinstance(arg, list), args)
# Separate any non-list arguments: these are assumed to be query arguments
sql_args = filter(lambda arg: not isinstance(arg, list), args)
id_list = id_args and id_args[0] or kwargs.get('id_list', [])
assert id_list != [], "get_in_bulk() cannot be passed an empty ID list."
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)))] 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)))]
kwargs['params'] = id_list kwargs['params'] = id_list
obj_list = self.get_list(**kwargs) obj_list = self.get_list(*sql_args, **kwargs)
return dict([(getattr(o, self.klass._meta.pk.attname), o) for o in obj_list]) return dict([(getattr(o, self.klass._meta.pk.attname), o) for o in obj_list])
def get_values_iterator(self, **kwargs): def get_values_iterator(self, *args, **kwargs):
# select_related and select aren't supported in get_values(). # select_related and select aren't supported in get_values().
kwargs['select_related'] = False kwargs['select_related'] = False
kwargs['select'] = {} kwargs['select'] = {}
@ -192,7 +215,7 @@ class Manager(object):
fields = [f.column for f in self.klass._meta.fields] fields = [f.column for f in self.klass._meta.fields]
cursor = connection.cursor() cursor = connection.cursor()
_, sql, params = self._get_sql_clause(**kwargs) _, sql, params = self._get_sql_clause(*args, **kwargs)
select = ['%s.%s' % (backend.quote_name(self.klass._meta.db_table), backend.quote_name(f)) for f in fields] select = ['%s.%s' % (backend.quote_name(self.klass._meta.db_table), backend.quote_name(f)) for f in fields]
cursor.execute("SELECT " + (kwargs.get('distinct') and "DISTINCT " or "") + ",".join(select) + sql, params) cursor.execute("SELECT " + (kwargs.get('distinct') and "DISTINCT " or "") + ",".join(select) + sql, params)
while 1: while 1:
@ -202,17 +225,22 @@ class Manager(object):
for row in rows: for row in rows:
yield dict(zip(fields, row)) yield dict(zip(fields, row))
def get_values(self, **kwargs): def get_values(self, *args, **kwargs):
return list(self.get_values_iterator(**kwargs)) return list(self.get_values_iterator(*args, **kwargs))
def __get_latest(self, **kwargs): def __get_latest(self, *args, **kwargs):
kwargs['order_by'] = ('-' + self.klass._meta.get_latest_by,) kwargs['order_by'] = ('-' + self.klass._meta.get_latest_by,)
kwargs['limit'] = 1 kwargs['limit'] = 1
return self.get_object(**kwargs) return self.get_object(*args, **kwargs)
def __get_date_list(self, field, *args, **kwargs): def __get_date_list(self, field, *args, **kwargs):
# Separate any string arguments: the first will be used as the kind
kind_args = filter(lambda arg: isinstance(arg, str), args)
# Separate any non-list arguments: these are assumed to be query arguments
sql_args = filter(lambda arg: not isinstance(arg, str), args)
from django.db.backends.util import typecast_timestamp from django.db.backends.util import typecast_timestamp
kind = args and args[0] or kwargs['kind'] kind = kind_args and kind_args[0] or kwargs.get(['kind'],"")
assert kind in ("month", "year", "day"), "'kind' must be one of 'year', 'month' or 'day'." assert kind in ("month", "year", "day"), "'kind' must be one of 'year', 'month' or 'day'."
order = 'ASC' order = 'ASC'
if kwargs.has_key('order'): if kwargs.has_key('order'):
@ -223,7 +251,7 @@ class Manager(object):
if field.null: if field.null:
kwargs.setdefault('where', []).append('%s.%s IS NOT NULL' % \ kwargs.setdefault('where', []).append('%s.%s IS NOT NULL' % \
(backend.quote_name(self.klass._meta.db_table), backend.quote_name(field.column))) (backend.quote_name(self.klass._meta.db_table), backend.quote_name(field.column)))
select, sql, params = self._get_sql_clause(**kwargs) select, sql, params = self._get_sql_clause(*sql_args, **kwargs)
sql = 'SELECT %s %s GROUP BY 1 ORDER BY 1 %s' % \ sql = 'SELECT %s %s GROUP BY 1 ORDER BY 1 %s' % \
(backend.get_date_trunc_sql(kind, '%s.%s' % (backend.quote_name(self.klass._meta.db_table), (backend.get_date_trunc_sql(kind, '%s.%s' % (backend.quote_name(self.klass._meta.db_table),
backend.quote_name(field.column))), sql, order) backend.quote_name(field.column))), sql, order)

View File

@ -198,6 +198,8 @@ def parse_lookup(kwarg_items, opts):
elif value is None: elif value is None:
pass pass
elif kwarg == 'complex': elif kwarg == 'complex':
if not hasattr(value, 'get_sql'):
raise TypeError, "'%s' is not a valid query argument" % str(arg)
tables2, joins2, where2, params2 = value.get_sql(opts) tables2, joins2, where2, params2 = value.get_sql(opts)
tables.extend(tables2) tables.extend(tables2)
joins.update(joins2) joins.update(joins2)

View File

@ -224,32 +224,67 @@ OR lookups
**New in Django development version.** **New in Django development version.**
By default, multiple lookups are "AND"ed together. If you'd like to use ``OR`` By default, keyword argument queries are "AND"ed together. If you have more complex query
statements in your queries, use the ``complex`` lookup type. requirements (for example, you need to include an ``OR`` statement in your query), you need
to use ``Q`` objects.
``complex`` takes an expression of clauses, each of which is an instance of A ``Q`` object is an instance of ``django.core.meta.Q``, used to encapsulate a collection of
``django.core.meta.Q``. ``Q`` takes an arbitrary number of keyword arguments in keyword arguments. These keyword arguments are specified in the same way as keyword arguments to
the standard Django lookup format. And you can use Python's "and" (``&``) and the basic lookup functions like get_object() and get_list(). For example::
"or" (``|``) operators to combine ``Q`` instances. For example::
from django.core.meta import Q Q(question__startswith='What')
polls.get_object(complex=(Q(question__startswith='Who') | Q(question__startswith='What')))
The ``|`` symbol signifies an "OR", so this (roughly) translates into:: ``Q`` objects can be combined using the ``&`` and ``|`` operators. When an operator is used on two
``Q`` objects, it yields a new ``Q`` object. For example the statement::
SELECT * FROM polls Q(question__startswith='Who') | Q(question__startswith='What')
WHERE question LIKE 'Who%' OR question LIKE 'What%';
You can use ``&`` and ``|`` operators together, and use parenthetical grouping. ... yields a single ``Q`` object that represents the "OR" of two "question__startswith" queries, equivalent to the SQL WHERE clause::
Example::
polls.get_object(complex=(Q(question__startswith='Who') & (Q(pub_date__exact=date(2005, 5, 2)) | Q(pub_date__exact=date(2005, 5, 6)))) ... WHERE question LIKE 'Who%' OR question LIKE 'What%'
This roughly translates into:: You can compose statements of arbitrary complexity by combining ``Q`` objects with the ``&`` and ``|`` operators. Parenthetical grouping can also be used.
SELECT * FROM polls One or more ``Q`` objects can then provided as arguments to the lookup functions. If multiple
WHERE question LIKE 'Who%' ``Q`` object arguments are provided to a lookup function, they will be "AND"ed together.
AND (pub_date = '2005-05-02' OR pub_date = '2005-05-06'); For example::
polls.get_object(
Q(question__startswith='Who'),
Q(pub_date__exact=date(2005, 5, 2)) | Q(pub_date__exact=date(2005, 5, 6))
)
... roughly translates into the SQL::
SELECT * from polls WHERE question LIKE 'Who%'
AND (pub_date = '2005-05-02' OR pub_date = '2005-05-06')
If necessary, lookup functions can mix the use of ``Q`` objects and keyword arguments. All arguments
provided to a lookup function (be they keyword argument or ``Q`` object) are "AND"ed together.
However, if a ``Q`` object is provided, it must precede the definition of any keyword arguments.
For example::
polls.get_object(
Q(pub_date__exact=date(2005, 5, 2)) | Q(pub_date__exact=date(2005, 5, 6)),
question__startswith='Who')
... would be a valid query, equivalent to the previous example; but::
# INVALID QUERY
polls.get_object(
question__startswith='Who',
Q(pub_date__exact=date(2005, 5, 2)) | Q(pub_date__exact=date(2005, 5, 6)))
... would not be valid.
A ``Q`` objects can also be provided to the ``complex`` keyword argument. For example::
polls.get_object(
complex=Q(question__startswith='Who') &
(Q(pub_date__exact=date(2005, 5, 2)) |
Q(pub_date__exact=date(2005, 5, 6))
)
)
See the `OR lookups examples page`_ for more examples. See the `OR lookups examples page`_ for more examples.

View File

@ -68,7 +68,7 @@ Article 4
>>> Article.objects.get_in_bulk([]) >>> Article.objects.get_in_bulk([])
Traceback (most recent call last): Traceback (most recent call last):
... ...
AssertionError: get_in_bulk() cannot be passed an empty list. AssertionError: get_in_bulk() cannot be passed an empty ID list.
# get_values() is just like get_list(), except it returns a list of # get_values() is just like get_list(), except it returns a list of
# dictionaries instead of object instances -- and you can specify which fields # dictionaries instead of object instances -- and you can specify which fields

View File

@ -3,7 +3,7 @@
To perform an OR lookup, or a lookup that combines ANDs and ORs, use the To perform an OR lookup, or a lookup that combines ANDs and ORs, use the
``complex`` keyword argument, and pass it an expression of clauses using the ``complex`` keyword argument, and pass it an expression of clauses using the
variable ``django.db.models.Q``. variable ``django.db.models.Q`` (or any object with a get_sql method).
""" """
from django.db import models from django.db import models
@ -54,4 +54,33 @@ API_TESTS = """
>>> Article.objects.get_list(complex=(Q(pk=1) | Q(pk=2) | Q(pk=3))) >>> Article.objects.get_list(complex=(Q(pk=1) | Q(pk=2) | Q(pk=3)))
[Hello, Goodbye, Hello and goodbye] [Hello, Goodbye, Hello and goodbye]
# Queries can use Q objects as args
>>> Article.objects.get_list(Q(headline__startswith='Hello'))
[Hello, Hello and goodbye]
# Q arg objects are ANDed
>>> Article.objects.get_list(Q(headline__startswith='Hello'), Q(headline__contains='bye'))
[Hello and goodbye]
# Q arg AND order is irrelevant
>>> Article.objects.get_list(Q(headline__contains='bye'), headline__startswith='Hello')
[Hello and goodbye]
# QOrs are ok, as they ultimately resolve to a Q
>>> Article.objects.get_list(Q(headline__contains='Hello') | Q(headline__contains='bye'))
[Hello, Goodbye, Hello and goodbye]
# Try some arg queries with operations other than get_list
>>> Article.objects.get_object(Q(headline__startswith='Hello'), Q(headline__contains='bye'))
Hello and goodbye
>>> Article.objects.get_count(Q(headline__startswith='Hello') | Q(headline__contains='bye'))
3
>>> Article.objects.get_values(Q(headline__startswith='Hello'), Q(headline__contains='bye'))
[{'headline': 'Hello and goodbye', 'pub_date': datetime.datetime(2005, 11, 29, 0, 0), 'id': 3}]
>>> Article.objects.get_in_bulk([1,2], Q(headline__startswith='Hello'))
{1: Hello}
""" """