diff --git a/django/db/models/query.py b/django/db/models/query.py index 9b9388f2f5..1984ba6006 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -335,7 +335,7 @@ class QuerySet: raise TypeError("Complex aggregates require an alias") kwargs[arg.default_alias] = arg - query = self.query.clone() + query = self.query.chain() for (alias, aggregate_expr) in kwargs.items(): query.add_annotation(aggregate_expr, alias, is_summary=True) if not query.annotations[alias].contains_aggregate: @@ -632,7 +632,7 @@ class QuerySet: assert self.query.can_filter(), \ "Cannot update a query once a slice has been taken." self._for_write = True - query = self.query.clone(sql.UpdateQuery) + query = self.query.chain(sql.UpdateQuery) query.add_update_values(kwargs) # Clear any annotations so that they won't be present in subqueries. query._annotations = None @@ -651,7 +651,7 @@ class QuerySet: """ assert self.query.can_filter(), \ "Cannot update a query once a slice has been taken." - query = self.query.clone(sql.UpdateQuery) + query = self.query.chain(sql.UpdateQuery) query.add_update_fields(values) self._result_cache = None return query.get_compiler(self.db).execute_sql(CURSOR) @@ -1087,7 +1087,7 @@ class QuerySet: Return a copy of the current QuerySet. A lightweight alternative to deepcopy(). """ - c = self.__class__(model=self.model, query=self.query.clone(), using=self._db, hints=self._hints) + c = self.__class__(model=self.model, query=self.query.chain(), using=self._db, hints=self._hints) c._sticky_filter = self._sticky_filter c._for_write = self._for_write c._prefetch_related_lookups = self._prefetch_related_lookups[:] @@ -1267,7 +1267,7 @@ class RawQuerySet: """Select the database this RawQuerySet should execute against.""" return RawQuerySet( self.raw_query, model=self.model, - query=self.query.clone(using=alias), + query=self.query.chain(using=alias), params=self.params, translations=self.translations, using=alias, ) diff --git a/django/db/models/sql/compiler.py b/django/db/models/sql/compiler.py index 6e5ed79a41..980f6ee79f 100644 --- a/django/db/models/sql/compiler.py +++ b/django/db/models/sql/compiler.py @@ -1334,7 +1334,7 @@ class SQLUpdateCompiler(SQLCompiler): count = self.query.count_active_tables() if not self.query.related_updates and count == 1: return - query = self.query.clone(klass=Query) + query = self.query.chain(klass=Query) query.select_related = False query.clear_ordering(True) query._extra = {} diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py index 5e262d1765..bdec204351 100644 --- a/django/db/models/sql/query.py +++ b/django/db/models/sql/query.py @@ -7,6 +7,7 @@ databases). The abstraction barrier only works one way: this module has to know all about the internals of models in order to get the information it needs. """ from collections import Counter, Iterator, Mapping, OrderedDict +from contextlib import suppress from itertools import chain, count, product from string import ascii_uppercase @@ -58,6 +59,9 @@ class RawQuery: self.extra_select = {} self.annotation_select = {} + def chain(self, using): + return self.clone(using) + def clone(self, using): return RawQuery(self.sql, using, params=self.params) @@ -262,35 +266,21 @@ class Query: """ return self.model._meta - def clone(self, klass=None, **kwargs): + def clone(self): """ - Create a copy of the current instance. The 'kwargs' parameter can be - used by clients to update attributes after copying has taken place. + Return a copy of the current Query. A lightweight alternative to + to deepcopy(). """ obj = Empty() - obj.__class__ = klass or self.__class__ - obj.model = self.model + obj.__class__ = self.__class__ + # Copy references to everything. + obj.__dict__ = self.__dict__.copy() + # Clone attributes that can't use shallow copy. obj.alias_refcount = self.alias_refcount.copy() obj.alias_map = self.alias_map.copy() obj.external_aliases = self.external_aliases.copy() obj.table_map = self.table_map.copy() - obj.default_cols = self.default_cols - obj.default_ordering = self.default_ordering - obj.standard_ordering = self.standard_ordering - obj.select = self.select obj.where = self.where.clone() - obj.where_class = self.where_class - obj.group_by = self.group_by - obj.order_by = self.order_by - obj.low_mark, obj.high_mark = self.low_mark, self.high_mark - obj.distinct = self.distinct - obj.distinct_fields = self.distinct_fields - obj.select_for_update = self.select_for_update - obj.select_for_update_nowait = self.select_for_update_nowait - obj.select_for_update_skip_locked = self.select_for_update_skip_locked - obj.select_for_update_of = self.select_for_update_of - obj.select_related = self.select_related - obj.values_select = self.values_select obj._annotations = self._annotations.copy() if self._annotations is not None else None if self.annotation_select_mask is None: obj.annotation_select_mask = None @@ -302,10 +292,6 @@ class Query: # It will get re-populated in the cloned queryset the next time it's # used. obj._annotation_select_cache = None - obj.max_depth = self.max_depth - obj.combinator = self.combinator - obj.combinator_all = self.combinator_all - obj.combined_queries = self.combined_queries obj._extra = self._extra.copy() if self._extra is not None else None if self.extra_select_mask is None: obj.extra_select_mask = None @@ -315,21 +301,25 @@ class Query: obj._extra_select_cache = None else: obj._extra_select_cache = self._extra_select_cache.copy() - obj.extra_tables = self.extra_tables - obj.extra_order_by = self.extra_order_by - obj.deferred_loading = self.deferred_loading - if self.filter_is_sticky and self.used_aliases: - obj.used_aliases = self.used_aliases.copy() - else: - obj.used_aliases = set() - obj.filter_is_sticky = False - obj.subquery = self.subquery - if 'alias_prefix' in self.__dict__: - obj.alias_prefix = self.alias_prefix if 'subq_aliases' in self.__dict__: obj.subq_aliases = self.subq_aliases.copy() + obj.used_aliases = self.used_aliases.copy() + # Clear the cached_property + with suppress(AttributeError): + del obj.base_table + return obj - obj.__dict__.update(kwargs) + def chain(self, klass=None): + """ + Return a copy of the current Query that's ready for another operation. + The klass argument changes the type of the Query, e.g. UpdateQuery. + """ + obj = self.clone() + if klass and obj.__class__ != klass: + obj.__class__ = klass + if not obj.filter_is_sticky: + obj.used_aliases = set() + obj.filter_is_sticky = False if hasattr(obj, '_setup_query'): obj._setup_query() return obj diff --git a/django/db/models/sql/subqueries.py b/django/db/models/sql/subqueries.py index 1cb9aa9357..edff51a56a 100644 --- a/django/db/models/sql/subqueries.py +++ b/django/db/models/sql/subqueries.py @@ -87,16 +87,17 @@ class UpdateQuery(Query): def _setup_query(self): """ - Run on initialization and after cloning. Any attributes that would - normally be set in __init__ should go in here, instead, so that they - are also set up after a clone() call. + 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, klass=None, **kwargs): - return super().clone(klass, related_updates=self.related_updates.copy(), **kwargs) + 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)