Skip to content

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also .

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also .
...
Checking mergeability… Don’t worry, you can still create the pull request.
  • 9 commits
  • 4 files changed
  • 0 commit comments
  • 1 contributor
Showing with 159 additions and 6 deletions.
  1. +25 −1 django/db/models/query.py
  2. +73 −3 django/db/models/sql/query.py
  3. +9 −2 django/db/models/sql/where.py
  4. +52 −0 tests/raw_query/tests.py
View
26 django/db/models/query.py
@@ -1499,7 +1499,9 @@ def __init__(self, raw_query, model=None, query=None, params=None,
self.model = model
self._db = using
self._hints = hints or {}
- self.query = query or sql.RawQuery(sql=raw_query, using=self.db, params=params)
+ self.query = query or sql.RawQuery(
+ sql=raw_query, using=self.db,
+ params=params, pk_column=model._meta.pk.column)
self.params = params or ()
self.translations = translations or {}
@@ -1626,6 +1628,28 @@ def model_fields(self):
self._model_fields[converter(column)] = field
return self._model_fields
+ def _clone(self):
+ return RawQuerySet(self.raw_query, model=self.model,
+ query=self.query.clone(self.query.using),
+ params=self.params, translations=self.translations,
+ using=self._db)
+
+ def only(self, *fields):
+ only_qs = self._clone()
+ only_qs.query.set_immediate_columns(fields)
+ return only_qs
+
+ def _prepare(self):
+ return self
+
+ def _as_sql(self, connection):
+ """
+ Returns the internal query's SQL and parameters (as a tuple).
+ """
+ if self._db is None or connection == connections[self._db]:
+ return self.query.as_nested_sql(connection=connection)
+ raise ValueError("Can't do subqueries with queries on different DBs.")
+
class Prefetch(object):
def __init__(self, lookup, queryset=None, to_attr=None):
View
76 django/db/models/sql/query.py
@@ -37,20 +37,40 @@ class RawQuery(object):
A single raw SQL query
"""
- def __init__(self, sql, using, params=None):
+ SUBQUERY_ALIAS = 'sq'
+
+ def __init__(self, sql, using, params=None, pk_column=None):
self.params = params or ()
- self.sql = sql
+ self._sql = sql
self.using = using
self.cursor = None
+ self.pk_column = pk_column
+ self.immediate_columns = set()
# Mirror some properties of a normal query so that
# the compiler can be used to process results.
self.low_mark, self.high_mark = 0, None # Used for offset/limit
self.extra_select = {}
self.aggregate_select = {}
+ @property
+ def sql(self):
+ """
+ Wraps the raw SQL in an outer SELECT statement to select only
+ certain columns. Column resf requested by calling RawQuerySet.only().
+ """
+ if not self.immediate_columns:
+ return self._sql
+ # Never defer the primary key - without it, it's impossible
+ # to construct model instances.
+ columns = list(self.immediate_columns | {self.pk_column})
+ return self._select_columns_sql(columns)
+
def clone(self, using):
- return RawQuery(self.sql, using, params=self.params)
+ cloned_query = RawQuery(self._sql, using, params=self.params,
+ pk_column=self.pk_column)
+ cloned_query.set_immediate_columns(self.immediate_columns)
+ return cloned_query
def convert_values(self, value, field, connection):
"""Convert the database-returned value into a type that is consistent
@@ -87,6 +107,56 @@ def _execute_query(self):
self.cursor = connections[self.using].cursor()
self.cursor.execute(self.sql, self.params)
+ def set_immediate_columns(self, columns):
+ self.immediate_columns = set(columns)
+
+ def _select_columns_sql(self, columns, connection=None):
+ """
+ Returns this query's raw SQL wrapped in another SELECT query which
+ selects only the columns specified in the parameter passed.
+ """
+ connection = connection or connections[self.using]
+ select_format_str = ', '.join('%s' for _ in columns)
+ sql_query = 'SELECT %s FROM (%%s) %s' % (select_format_str,
+ self.SUBQUERY_ALIAS)
+ quoted_columns = [connection.ops.quote_name(col) for col in columns]
+ return sql_query % tuple(quoted_columns + [self._sql])
+
+ def as_nested_sql(self, connection):
+ """
+ Return this query as suitable for inclusion as a subquery. This
+ consists of the output of _select_columns_sql with an added
+ IS NOT NULL clause for each column selected.
+ """
+ columns = []
+ if self.immediate_columns:
+ columns = list(self.immediate_columns)
+ elif self.pk_column:
+ columns = [self.pk_column]
+
+ if columns:
+ qn = connection.ops.quote_name
+
+ def not_null_clause(column):
+ return '%s.%s IS NOT NULL' % (self.SUBQUERY_ALIAS, qn(column))
+ not_nulls = ' AND '.join(not_null_clause(c) for c in columns)
+ sql = '%s WHERE %s' % (self._select_columns_sql(columns), not_nulls)
+ else:
+ sql = self._sql
+
+ return sql, self.params
+
+ def as_subquery_condition(self, alias, columns, qn):
+ connection = connections[self.using]
+ qn2 = connection.ops.quote_name
+ if len(columns) == 1:
+ nested_sql, params = self.as_nested_sql(connection)
+ query_params = (qn(alias), qn2(columns[0]), nested_sql)
+ return '%s.%s IN (%s)' % query_params, params
+ # TODO: What about multi-column? When does this happen?
+ raise NotImplementedError(
+ "Can't use a raw query in a multi-column join")
+
class Query(object):
"""
View
11 django/db/models/sql/where.py
@@ -410,8 +410,15 @@ def as_sql(self, qn, connection):
query = query.query
query.clear_ordering(True)
- query_compiler = query.get_compiler(connection=connection)
- return query_compiler.as_subquery_condition(self.alias, self.columns, qn)
+ # RawQuerySet was sent
+ if hasattr(query, 'raw_query'):
+ if query._db and connection.alias != query._db:
+ raise ValueError("Can't do subqueries with queries on different DBs.")
+ query = query.query
+
+ if not hasattr(query, 'as_subquery_condition'):
+ query = query.get_compiler(connection=connection)
+ return query.as_subquery_condition(self.alias, self.columns, qn)
def relabel_aliases(self, change_map):
self.alias = change_map.get(self.alias, self.alias)
View
52 tests/raw_query/tests.py
@@ -239,3 +239,55 @@ def test_inheritance(self):
def test_query_count(self):
self.assertNumQueries(1, list, Author.objects.raw("SELECT * FROM raw_query_author"))
+
+ def test_subquery_count(self):
+ # RawQuerySets passed as parameter to __in filters should be embedded
+ # as a subquery, not evaluated
+ raw_qs = Author.objects.raw("SELECT * FROM raw_query_author")
+ self.assertNumQueries(1, list, Book.objects.filter(author__in=raw_qs))
+ self.assertNumQueries(1, list, Author.objects.filter(pk__in=raw_qs))
+ with self.assertNumQueries(2):
+ list(Book.objects.filter(author__in=list(raw_qs)))
+
+ def test_subquery_only(self):
+ raw_qs = Book.objects.raw('SELECT * FROM raw_query_book WHERE paperback')
+ self.assertQuerysetEqual(raw_qs.only('author_id'), raw_qs,
+ transform=lambda i: i, ordered=False)
+
+ with self.assertNumQueries(2):
+ raw_qs.only('author_id')[0].paperback
+
+ def test_subquery_only_in(self):
+ raw_qs = Book.objects.raw('SELECT * FROM raw_query_book WHERE paperback')
+ self.assertQuerysetEqual(Author.objects.filter(pk__in=raw_qs.only('author_id')),
+ Author.objects.filter(book__paperback=True).distinct(),
+ transform=lambda i: i, ordered=False)
+
+ def test_subquery_pk(self):
+ raw_qs = Author.objects.raw("SELECT * FROM raw_query_author WHERE id < 3")
+ qs = Author.objects.filter(id__lt=3)
+ self.assertQuerysetEqual(Author.objects.filter(pk__in=qs),
+ Author.objects.filter(pk__in=raw_qs),
+ transform=lambda i: i, ordered=False)
+
+ def test_subquery_foreignkey(self):
+ raw_qs = Author.objects.raw("SELECT * FROM raw_query_author WHERE id < 3")
+ self.assertQuerysetEqual(Book.objects.filter(author__in=raw_qs),
+ Book.objects.filter(author__in=list(raw_qs)),
+ transform=lambda i: i, ordered=False)
+
+ qs = Author.objects.filter(id__lt=3)
+ self.assertQuerysetEqual(Book.objects.filter(author__in=raw_qs),
+ Book.objects.filter(author__in=qs),
+ transform=lambda i: i, ordered=False)
+
+ def test_subquery_exclude(self):
+ raw_qs = Author.objects.raw("SELECT * FROM raw_query_author WHERE id < 3")
+ self.assertQuerysetEqual(Book.objects.exclude(author__in=raw_qs),
+ Book.objects.exclude(author__in=list(raw_qs)),
+ transform=lambda i: i, ordered=False)
+
+ qs = Author.objects.filter(id__lt=3)
+ self.assertQuerysetEqual(Book.objects.exclude(author__in=raw_qs),
+ Book.objects.exclude(author__in=qs),
+ transform=lambda i: i, ordered=False)

No commit comments for this range

Something went wrong with that request. Please try again.