194 lines
7.0 KiB
Python
194 lines
7.0 KiB
Python
|
"""
|
||
|
Query subclasses which provide extra functionality beyond simple data retrieval.
|
||
|
"""
|
||
|
|
||
|
from django.core.exceptions import FieldError
|
||
|
from django.db import connections
|
||
|
from django.db.models.query_utils import Q
|
||
|
from django.db.models.sql.constants import (
|
||
|
CURSOR, GET_ITERATOR_CHUNK_SIZE, NO_RESULTS,
|
||
|
)
|
||
|
from django.db.models.sql.query import Query
|
||
|
|
||
|
__all__ = ['DeleteQuery', 'UpdateQuery', 'InsertQuery', 'AggregateQuery']
|
||
|
|
||
|
|
||
|
class DeleteQuery(Query):
|
||
|
"""A DELETE SQL query."""
|
||
|
|
||
|
compiler = 'SQLDeleteCompiler'
|
||
|
|
||
|
def do_query(self, table, where, using):
|
||
|
self.alias_map = {table: self.alias_map[table]}
|
||
|
self.where = where
|
||
|
cursor = self.get_compiler(using).execute_sql(CURSOR)
|
||
|
return cursor.rowcount if cursor else 0
|
||
|
|
||
|
def delete_batch(self, pk_list, using):
|
||
|
"""
|
||
|
Set up and execute delete queries for all the objects in pk_list.
|
||
|
|
||
|
More than one physical query may be executed if there are a
|
||
|
lot of values in pk_list.
|
||
|
"""
|
||
|
# number of objects deleted
|
||
|
num_deleted = 0
|
||
|
field = self.get_meta().pk
|
||
|
for offset in range(0, len(pk_list), GET_ITERATOR_CHUNK_SIZE):
|
||
|
self.where = self.where_class()
|
||
|
self.add_q(Q(
|
||
|
**{field.attname + '__in': pk_list[offset:offset + GET_ITERATOR_CHUNK_SIZE]}))
|
||
|
num_deleted += self.do_query(self.get_meta().db_table, self.where, using=using)
|
||
|
return num_deleted
|
||
|
|
||
|
def delete_qs(self, query, using):
|
||
|
"""
|
||
|
Delete the queryset in one SQL query (if possible). For simple queries
|
||
|
this is done by copying the query.query.where to self.query, for
|
||
|
complex queries by using subquery.
|
||
|
"""
|
||
|
innerq = query.query
|
||
|
# Make sure the inner query has at least one table in use.
|
||
|
innerq.get_initial_alias()
|
||
|
# The same for our new query.
|
||
|
self.get_initial_alias()
|
||
|
innerq_used_tables = tuple([t for t in innerq.alias_map if innerq.alias_refcount[t]])
|
||
|
if not innerq_used_tables or innerq_used_tables == tuple(self.alias_map):
|
||
|
# There is only the base table in use in the query.
|
||
|
self.where = innerq.where
|
||
|
else:
|
||
|
pk = query.model._meta.pk
|
||
|
if not connections[using].features.update_can_self_select:
|
||
|
# We can't do the delete using subquery.
|
||
|
values = list(query.values_list('pk', flat=True))
|
||
|
if not values:
|
||
|
return 0
|
||
|
return self.delete_batch(values, using)
|
||
|
else:
|
||
|
innerq.clear_select_clause()
|
||
|
innerq.select = [
|
||
|
pk.get_col(self.get_initial_alias())
|
||
|
]
|
||
|
values = innerq
|
||
|
self.where = self.where_class()
|
||
|
self.add_q(Q(pk__in=values))
|
||
|
cursor = self.get_compiler(using).execute_sql(CURSOR)
|
||
|
return cursor.rowcount if cursor else 0
|
||
|
|
||
|
|
||
|
class UpdateQuery(Query):
|
||
|
"""An UPDATE SQL query."""
|
||
|
|
||
|
compiler = 'SQLUpdateCompiler'
|
||
|
|
||
|
def __init__(self, *args, **kwargs):
|
||
|
super().__init__(*args, **kwargs)
|
||
|
self._setup_query()
|
||
|
|
||
|
def _setup_query(self):
|
||
|
"""
|
||
|
Run on initialization and at the end of chaining. Any attributes that
|
||
|
would normally be set in __init__() should go here instead.
|
||
|
"""
|
||
|
self.values = []
|
||
|
self.related_ids = None
|
||
|
self.related_updates = {}
|
||
|
|
||
|
def clone(self):
|
||
|
obj = super().clone()
|
||
|
obj.related_updates = self.related_updates.copy()
|
||
|
return obj
|
||
|
|
||
|
def update_batch(self, pk_list, values, using):
|
||
|
self.add_update_values(values)
|
||
|
for offset in range(0, len(pk_list), GET_ITERATOR_CHUNK_SIZE):
|
||
|
self.where = self.where_class()
|
||
|
self.add_q(Q(pk__in=pk_list[offset: offset + GET_ITERATOR_CHUNK_SIZE]))
|
||
|
self.get_compiler(using).execute_sql(NO_RESULTS)
|
||
|
|
||
|
def add_update_values(self, values):
|
||
|
"""
|
||
|
Convert a dictionary of field name to value mappings into an update
|
||
|
query. This is the entry point for the public update() method on
|
||
|
querysets.
|
||
|
"""
|
||
|
values_seq = []
|
||
|
for name, val in values.items():
|
||
|
field = self.get_meta().get_field(name)
|
||
|
direct = not (field.auto_created and not field.concrete) or not field.concrete
|
||
|
model = field.model._meta.concrete_model
|
||
|
if not direct or (field.is_relation and field.many_to_many):
|
||
|
raise FieldError(
|
||
|
'Cannot update model field %r (only non-relations and '
|
||
|
'foreign keys permitted).' % field
|
||
|
)
|
||
|
if model is not self.get_meta().concrete_model:
|
||
|
self.add_related_update(model, field, val)
|
||
|
continue
|
||
|
values_seq.append((field, model, val))
|
||
|
return self.add_update_fields(values_seq)
|
||
|
|
||
|
def add_update_fields(self, values_seq):
|
||
|
"""
|
||
|
Append a sequence of (field, model, value) triples to the internal list
|
||
|
that will be used to generate the UPDATE query. Might be more usefully
|
||
|
called add_update_targets() to hint at the extra information here.
|
||
|
"""
|
||
|
for field, model, val in values_seq:
|
||
|
if hasattr(val, 'resolve_expression'):
|
||
|
# Resolve expressions here so that annotations are no longer needed
|
||
|
val = val.resolve_expression(self, allow_joins=False, for_save=True)
|
||
|
self.values.append((field, model, val))
|
||
|
|
||
|
def add_related_update(self, model, field, value):
|
||
|
"""
|
||
|
Add (name, value) to an update query for an ancestor model.
|
||
|
|
||
|
Update are coalesced so that only one update query per ancestor is run.
|
||
|
"""
|
||
|
self.related_updates.setdefault(model, []).append((field, None, value))
|
||
|
|
||
|
def get_related_updates(self):
|
||
|
"""
|
||
|
Return a list of query objects: one for each update required to an
|
||
|
ancestor model. Each query will have the same filtering conditions as
|
||
|
the current query but will only update a single table.
|
||
|
"""
|
||
|
if not self.related_updates:
|
||
|
return []
|
||
|
result = []
|
||
|
for model, values in self.related_updates.items():
|
||
|
query = UpdateQuery(model)
|
||
|
query.values = values
|
||
|
if self.related_ids is not None:
|
||
|
query.add_filter(('pk__in', self.related_ids))
|
||
|
result.append(query)
|
||
|
return result
|
||
|
|
||
|
|
||
|
class InsertQuery(Query):
|
||
|
compiler = 'SQLInsertCompiler'
|
||
|
|
||
|
def __init__(self, *args, **kwargs):
|
||
|
super().__init__(*args, **kwargs)
|
||
|
self.fields = []
|
||
|
self.objs = []
|
||
|
|
||
|
def insert_values(self, fields, objs, raw=False):
|
||
|
self.fields = fields
|
||
|
self.objs = objs
|
||
|
self.raw = raw
|
||
|
|
||
|
|
||
|
class AggregateQuery(Query):
|
||
|
"""
|
||
|
Take another query as a parameter to the FROM clause and only select the
|
||
|
elements in the provided list.
|
||
|
"""
|
||
|
|
||
|
compiler = 'SQLAggregateCompiler'
|
||
|
|
||
|
def add_subquery(self, query, using):
|
||
|
query.subquery = True
|
||
|
self.subquery, self.sub_params = query.get_compiler(using).as_sql(with_col_aliases=True)
|