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