Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/django/db/models/deletion.py: 15%
243 statements
« prev ^ index » next coverage.py v7.0.5, created at 2023-01-17 06:13 +0000
« prev ^ index » next coverage.py v7.0.5, created at 2023-01-17 06:13 +0000
1from collections import Counter, defaultdict
2from functools import partial, reduce
3from itertools import chain
4from operator import attrgetter, or_
6from django.db import IntegrityError, connections, models, transaction
7from django.db.models import query_utils, signals, sql
10class ProtectedError(IntegrityError):
11 def __init__(self, msg, protected_objects):
12 self.protected_objects = protected_objects
13 super().__init__(msg, protected_objects)
16class RestrictedError(IntegrityError):
17 def __init__(self, msg, restricted_objects):
18 self.restricted_objects = restricted_objects
19 super().__init__(msg, restricted_objects)
22def CASCADE(collector, field, sub_objs, using):
23 collector.collect(
24 sub_objs,
25 source=field.remote_field.model,
26 source_attr=field.name,
27 nullable=field.null,
28 fail_on_restricted=False,
29 )
30 if field.null and not connections[using].features.can_defer_constraint_checks:
31 collector.add_field_update(field, None, sub_objs)
34def PROTECT(collector, field, sub_objs, using):
35 raise ProtectedError(
36 "Cannot delete some instances of model '%s' because they are "
37 "referenced through a protected foreign key: '%s.%s'"
38 % (
39 field.remote_field.model.__name__,
40 sub_objs[0].__class__.__name__,
41 field.name,
42 ),
43 sub_objs,
44 )
47def RESTRICT(collector, field, sub_objs, using):
48 collector.add_restricted_objects(field, sub_objs)
49 collector.add_dependency(field.remote_field.model, field.model)
52def SET(value):
53 if callable(value):
55 def set_on_delete(collector, field, sub_objs, using):
56 collector.add_field_update(field, value(), sub_objs)
58 else:
60 def set_on_delete(collector, field, sub_objs, using):
61 collector.add_field_update(field, value, sub_objs)
63 set_on_delete.deconstruct = lambda: ("django.db.models.SET", (value,), {})
64 set_on_delete.lazy_sub_objs = True
65 return set_on_delete
68def SET_NULL(collector, field, sub_objs, using):
69 collector.add_field_update(field, None, sub_objs)
72SET_NULL.lazy_sub_objs = True
75def SET_DEFAULT(collector, field, sub_objs, using):
76 collector.add_field_update(field, field.get_default(), sub_objs)
79SET_DEFAULT.lazy_sub_objs = True
82def DO_NOTHING(collector, field, sub_objs, using):
83 pass
86def get_candidate_relations_to_delete(opts):
87 # The candidate relations are the ones that come from N-1 and 1-1 relations.
88 # N-N (i.e., many-to-many) relations aren't candidates for deletion.
89 return (
90 f
91 for f in opts.get_fields(include_hidden=True)
92 if f.auto_created and not f.concrete and (f.one_to_one or f.one_to_many)
93 )
96class Collector:
97 def __init__(self, using, origin=None):
98 self.using = using
99 # A Model or QuerySet object.
100 self.origin = origin
101 # Initially, {model: {instances}}, later values become lists.
102 self.data = defaultdict(set)
103 # {(field, value): [instances, …]}
104 self.field_updates = defaultdict(list)
105 # {model: {field: {instances}}}
106 self.restricted_objects = defaultdict(partial(defaultdict, set))
107 # fast_deletes is a list of queryset-likes that can be deleted without
108 # fetching the objects into memory.
109 self.fast_deletes = []
111 # Tracks deletion-order dependency for databases without transactions
112 # or ability to defer constraint checks. Only concrete model classes
113 # should be included, as the dependencies exist only between actual
114 # database tables; proxy models are represented here by their concrete
115 # parent.
116 self.dependencies = defaultdict(set) # {model: {models}}
118 def add(self, objs, source=None, nullable=False, reverse_dependency=False):
119 """
120 Add 'objs' to the collection of objects to be deleted. If the call is
121 the result of a cascade, 'source' should be the model that caused it,
122 and 'nullable' should be set to True if the relation can be null.
124 Return a list of all objects that were not already collected.
125 """
126 if not objs:
127 return []
128 new_objs = []
129 model = objs[0].__class__
130 instances = self.data[model]
131 for obj in objs:
132 if obj not in instances:
133 new_objs.append(obj)
134 instances.update(new_objs)
135 # Nullable relationships can be ignored -- they are nulled out before
136 # deleting, and therefore do not affect the order in which objects have
137 # to be deleted.
138 if source is not None and not nullable:
139 self.add_dependency(source, model, reverse_dependency=reverse_dependency)
140 return new_objs
142 def add_dependency(self, model, dependency, reverse_dependency=False):
143 if reverse_dependency:
144 model, dependency = dependency, model
145 self.dependencies[model._meta.concrete_model].add(
146 dependency._meta.concrete_model
147 )
148 self.data.setdefault(dependency, self.data.default_factory())
150 def add_field_update(self, field, value, objs):
151 """
152 Schedule a field update. 'objs' must be a homogeneous iterable
153 collection of model instances (e.g. a QuerySet).
154 """
155 self.field_updates[field, value].append(objs)
157 def add_restricted_objects(self, field, objs):
158 if objs:
159 model = objs[0].__class__
160 self.restricted_objects[model][field].update(objs)
162 def clear_restricted_objects_from_set(self, model, objs):
163 if model in self.restricted_objects:
164 self.restricted_objects[model] = {
165 field: items - objs
166 for field, items in self.restricted_objects[model].items()
167 }
169 def clear_restricted_objects_from_queryset(self, model, qs):
170 if model in self.restricted_objects:
171 objs = set(
172 qs.filter(
173 pk__in=[
174 obj.pk
175 for objs in self.restricted_objects[model].values()
176 for obj in objs
177 ]
178 )
179 )
180 self.clear_restricted_objects_from_set(model, objs)
182 def _has_signal_listeners(self, model):
183 return signals.pre_delete.has_listeners(
184 model
185 ) or signals.post_delete.has_listeners(model)
187 def can_fast_delete(self, objs, from_field=None):
188 """
189 Determine if the objects in the given queryset-like or single object
190 can be fast-deleted. This can be done if there are no cascades, no
191 parents and no signal listeners for the object class.
193 The 'from_field' tells where we are coming from - we need this to
194 determine if the objects are in fact to be deleted. Allow also
195 skipping parent -> child -> parent chain preventing fast delete of
196 the child.
197 """
198 if from_field and from_field.remote_field.on_delete is not CASCADE:
199 return False
200 if hasattr(objs, "_meta"):
201 model = objs._meta.model
202 elif hasattr(objs, "model") and hasattr(objs, "_raw_delete"):
203 model = objs.model
204 else:
205 return False
206 if self._has_signal_listeners(model):
207 return False
208 # The use of from_field comes from the need to avoid cascade back to
209 # parent when parent delete is cascading to child.
210 opts = model._meta
211 return (
212 all(
213 link == from_field
214 for link in opts.concrete_model._meta.parents.values()
215 )
216 and
217 # Foreign keys pointing to this model.
218 all(
219 related.field.remote_field.on_delete is DO_NOTHING
220 for related in get_candidate_relations_to_delete(opts)
221 )
222 and (
223 # Something like generic foreign key.
224 not any(
225 hasattr(field, "bulk_related_objects")
226 for field in opts.private_fields
227 )
228 )
229 )
231 def get_del_batches(self, objs, fields):
232 """
233 Return the objs in suitably sized batches for the used connection.
234 """
235 field_names = [field.name for field in fields]
236 conn_batch_size = max(
237 connections[self.using].ops.bulk_batch_size(field_names, objs), 1
238 )
239 if len(objs) > conn_batch_size:
240 return [
241 objs[i : i + conn_batch_size]
242 for i in range(0, len(objs), conn_batch_size)
243 ]
244 else:
245 return [objs]
247 def collect(
248 self,
249 objs,
250 source=None,
251 nullable=False,
252 collect_related=True,
253 source_attr=None,
254 reverse_dependency=False,
255 keep_parents=False,
256 fail_on_restricted=True,
257 ):
258 """
259 Add 'objs' to the collection of objects to be deleted as well as all
260 parent instances. 'objs' must be a homogeneous iterable collection of
261 model instances (e.g. a QuerySet). If 'collect_related' is True,
262 related objects will be handled by their respective on_delete handler.
264 If the call is the result of a cascade, 'source' should be the model
265 that caused it and 'nullable' should be set to True, if the relation
266 can be null.
268 If 'reverse_dependency' is True, 'source' will be deleted before the
269 current model, rather than after. (Needed for cascading to parent
270 models, the one case in which the cascade follows the forwards
271 direction of an FK rather than the reverse direction.)
273 If 'keep_parents' is True, data of parent model's will be not deleted.
275 If 'fail_on_restricted' is False, error won't be raised even if it's
276 prohibited to delete such objects due to RESTRICT, that defers
277 restricted object checking in recursive calls where the top-level call
278 may need to collect more objects to determine whether restricted ones
279 can be deleted.
280 """
281 if self.can_fast_delete(objs):
282 self.fast_deletes.append(objs)
283 return
284 new_objs = self.add(
285 objs, source, nullable, reverse_dependency=reverse_dependency
286 )
287 if not new_objs:
288 return
290 model = new_objs[0].__class__
292 if not keep_parents:
293 # Recursively collect concrete model's parent models, but not their
294 # related objects. These will be found by meta.get_fields()
295 concrete_model = model._meta.concrete_model
296 for ptr in concrete_model._meta.parents.values():
297 if ptr:
298 parent_objs = [getattr(obj, ptr.name) for obj in new_objs]
299 self.collect(
300 parent_objs,
301 source=model,
302 source_attr=ptr.remote_field.related_name,
303 collect_related=False,
304 reverse_dependency=True,
305 fail_on_restricted=False,
306 )
307 if not collect_related:
308 return
310 if keep_parents:
311 parents = set(model._meta.get_parent_list())
312 model_fast_deletes = defaultdict(list)
313 protected_objects = defaultdict(list)
314 for related in get_candidate_relations_to_delete(model._meta):
315 # Preserve parent reverse relationships if keep_parents=True.
316 if keep_parents and related.model in parents:
317 continue
318 field = related.field
319 on_delete = field.remote_field.on_delete
320 if on_delete == DO_NOTHING:
321 continue
322 related_model = related.related_model
323 if self.can_fast_delete(related_model, from_field=field):
324 model_fast_deletes[related_model].append(field)
325 continue
326 batches = self.get_del_batches(new_objs, [field])
327 for batch in batches:
328 sub_objs = self.related_objects(related_model, [field], batch)
329 # Non-referenced fields can be deferred if no signal receivers
330 # are connected for the related model as they'll never be
331 # exposed to the user. Skip field deferring when some
332 # relationships are select_related as interactions between both
333 # features are hard to get right. This should only happen in
334 # the rare cases where .related_objects is overridden anyway.
335 if not (
336 sub_objs.query.select_related
337 or self._has_signal_listeners(related_model)
338 ):
339 referenced_fields = set(
340 chain.from_iterable(
341 (rf.attname for rf in rel.field.foreign_related_fields)
342 for rel in get_candidate_relations_to_delete(
343 related_model._meta
344 )
345 )
346 )
347 sub_objs = sub_objs.only(*tuple(referenced_fields))
348 if getattr(on_delete, "lazy_sub_objs", False) or sub_objs:
349 try:
350 on_delete(self, field, sub_objs, self.using)
351 except ProtectedError as error:
352 key = "'%s.%s'" % (field.model.__name__, field.name)
353 protected_objects[key] += error.protected_objects
354 if protected_objects:
355 raise ProtectedError(
356 "Cannot delete some instances of model %r because they are "
357 "referenced through protected foreign keys: %s."
358 % (
359 model.__name__,
360 ", ".join(protected_objects),
361 ),
362 set(chain.from_iterable(protected_objects.values())),
363 )
364 for related_model, related_fields in model_fast_deletes.items():
365 batches = self.get_del_batches(new_objs, related_fields)
366 for batch in batches:
367 sub_objs = self.related_objects(related_model, related_fields, batch)
368 self.fast_deletes.append(sub_objs)
369 for field in model._meta.private_fields:
370 if hasattr(field, "bulk_related_objects"):
371 # It's something like generic foreign key.
372 sub_objs = field.bulk_related_objects(new_objs, self.using)
373 self.collect(
374 sub_objs, source=model, nullable=True, fail_on_restricted=False
375 )
377 if fail_on_restricted:
378 # Raise an error if collected restricted objects (RESTRICT) aren't
379 # candidates for deletion also collected via CASCADE.
380 for related_model, instances in self.data.items():
381 self.clear_restricted_objects_from_set(related_model, instances)
382 for qs in self.fast_deletes:
383 self.clear_restricted_objects_from_queryset(qs.model, qs)
384 if self.restricted_objects.values():
385 restricted_objects = defaultdict(list)
386 for related_model, fields in self.restricted_objects.items():
387 for field, objs in fields.items():
388 if objs:
389 key = "'%s.%s'" % (related_model.__name__, field.name)
390 restricted_objects[key] += objs
391 if restricted_objects:
392 raise RestrictedError(
393 "Cannot delete some instances of model %r because "
394 "they are referenced through restricted foreign keys: "
395 "%s."
396 % (
397 model.__name__,
398 ", ".join(restricted_objects),
399 ),
400 set(chain.from_iterable(restricted_objects.values())),
401 )
403 def related_objects(self, related_model, related_fields, objs):
404 """
405 Get a QuerySet of the related model to objs via related fields.
406 """
407 predicate = query_utils.Q.create(
408 [(f"{related_field.name}__in", objs) for related_field in related_fields],
409 connector=query_utils.Q.OR,
410 )
411 return related_model._base_manager.using(self.using).filter(predicate)
413 def instances_with_model(self):
414 for model, instances in self.data.items():
415 for obj in instances:
416 yield model, obj
418 def sort(self):
419 sorted_models = []
420 concrete_models = set()
421 models = list(self.data)
422 while len(sorted_models) < len(models):
423 found = False
424 for model in models:
425 if model in sorted_models:
426 continue
427 dependencies = self.dependencies.get(model._meta.concrete_model)
428 if not (dependencies and dependencies.difference(concrete_models)):
429 sorted_models.append(model)
430 concrete_models.add(model._meta.concrete_model)
431 found = True
432 if not found:
433 return
434 self.data = {model: self.data[model] for model in sorted_models}
436 def delete(self):
437 # sort instance collections
438 for model, instances in self.data.items():
439 self.data[model] = sorted(instances, key=attrgetter("pk"))
441 # if possible, bring the models in an order suitable for databases that
442 # don't support transactions or cannot defer constraint checks until the
443 # end of a transaction.
444 self.sort()
445 # number of objects deleted for each model label
446 deleted_counter = Counter()
448 # Optimize for the case with a single obj and no dependencies
449 if len(self.data) == 1 and len(instances) == 1:
450 instance = list(instances)[0]
451 if self.can_fast_delete(instance):
452 with transaction.mark_for_rollback_on_error(self.using):
453 count = sql.DeleteQuery(model).delete_batch(
454 [instance.pk], self.using
455 )
456 setattr(instance, model._meta.pk.attname, None)
457 return count, {model._meta.label: count}
459 with transaction.atomic(using=self.using, savepoint=False):
460 # send pre_delete signals
461 for model, obj in self.instances_with_model():
462 if not model._meta.auto_created:
463 signals.pre_delete.send(
464 sender=model,
465 instance=obj,
466 using=self.using,
467 origin=self.origin,
468 )
470 # fast deletes
471 for qs in self.fast_deletes:
472 count = qs._raw_delete(using=self.using)
473 if count:
474 deleted_counter[qs.model._meta.label] += count
476 # update fields
477 for (field, value), instances_list in self.field_updates.items():
478 updates = []
479 objs = []
480 for instances in instances_list:
481 if (
482 isinstance(instances, models.QuerySet)
483 and instances._result_cache is None
484 ):
485 updates.append(instances)
486 else:
487 objs.extend(instances)
488 if updates:
489 combined_updates = reduce(or_, updates)
490 combined_updates.update(**{field.name: value})
491 if objs:
492 model = objs[0].__class__
493 query = sql.UpdateQuery(model)
494 query.update_batch(
495 list({obj.pk for obj in objs}), {field.name: value}, self.using
496 )
498 # reverse instance collections
499 for instances in self.data.values():
500 instances.reverse()
502 # delete instances
503 for model, instances in self.data.items():
504 query = sql.DeleteQuery(model)
505 pk_list = [obj.pk for obj in instances]
506 count = query.delete_batch(pk_list, self.using)
507 if count:
508 deleted_counter[model._meta.label] += count
510 if not model._meta.auto_created:
511 for obj in instances:
512 signals.post_delete.send(
513 sender=model,
514 instance=obj,
515 using=self.using,
516 origin=self.origin,
517 )
519 for model, instances in self.data.items():
520 for instance in instances:
521 setattr(instance, model._meta.pk.attname, None)
522 return sum(deleted_counter.values()), dict(deleted_counter)