the candidate relations are the ones that come from n1 and 11 relations nn i e manytomany relations aren t candidates for deletion a model or queryset object initially model instances later values become lists field value instances model field instances fastdeletes is a list of querysetlikes that can be deleted without fetching the objects into memory tracks deletionorder dependency for databases without transactions or ability to defer constraint checks only concrete model classes should be included as the dependencies exist only between actual database tables proxy models are represented here by their concrete parent add objs to the collection of objects to be deleted if the call is the result of a cascade source should be the model that caused it and nullable should be set to true if the relation can be null return a list of all objects that were not already collected nullable relationships can be ignored they are nulled out before deleting and therefore do not affect the order in which objects have to be deleted schedule a field update objs must be a homogeneous iterable collection of model instances e g a queryset determine if the objects in the given querysetlike or single object can be fastdeleted this can be done if there are no cascades no parents and no signal listeners for the object class the fromfield tells where we are coming from we need this to determine if the objects are in fact to be deleted allow also skipping parent child parent chain preventing fast delete of the child the use of fromfield comes from the need to avoid cascade back to parent when parent delete is cascading to child foreign keys pointing to this model something like generic foreign key return the objs in suitably sized batches for the used connection add objs to the collection of objects to be deleted as well as all parent instances objs must be a homogeneous iterable collection of model instances e g a queryset if collectrelated is true related objects will be handled by their respective ondelete handler if the call is the result of a cascade source should be the model that caused it and nullable should be set to true if the relation can be null if reversedependency is true source will be deleted before the current model rather than after needed for cascading to parent models the one case in which the cascade follows the forwards direction of an fk rather than the reverse direction if keepparents is true data of parent model s will be not deleted if failonrestricted is false error won t be raised even if it s prohibited to delete such objects due to restrict that defers restricted object checking in recursive calls where the toplevel call may need to collect more objects to determine whether restricted ones can be deleted recursively collect concrete model s parent models but not their related objects these will be found by meta getfields preserve parent reverse relationships if keepparentstrue nonreferenced fields can be deferred if no signal receivers are connected for the related model as they ll never be exposed to the user skip field deferring when some relationships are selectrelated as interactions between both features are hard to get right this should only happen in the rare cases where relatedobjects is overridden anyway it s something like generic foreign key raise an error if collected restricted objects restrict aren t candidates for deletion also collected via cascade get a queryset of the related model to objs via related fields sort instance collections if possible bring the models in an order suitable for databases that don t support transactions or cannot defer constraint checks until the end of a transaction number of objects deleted for each model label optimize for the case with a single obj and no dependencies send predelete signals fast deletes update fields reverse instance collections delete instances the candidate relations are the ones that come from n 1 and 1 1 relations n n i e many to many relations aren t candidates for deletion a model or queryset object initially model instances later values become lists field value instances model field instances fast_deletes is a list of queryset likes that can be deleted without fetching the objects into memory tracks deletion order dependency for databases without transactions or ability to defer constraint checks only concrete model classes should be included as the dependencies exist only between actual database tables proxy models are represented here by their concrete parent model models add objs to the collection of objects to be deleted if the call is the result of a cascade source should be the model that caused it and nullable should be set to true if the relation can be null return a list of all objects that were not already collected nullable relationships can be ignored they are nulled out before deleting and therefore do not affect the order in which objects have to be deleted schedule a field update objs must be a homogeneous iterable collection of model instances e g a queryset determine if the objects in the given queryset like or single object can be fast deleted this can be done if there are no cascades no parents and no signal listeners for the object class the from_field tells where we are coming from we need this to determine if the objects are in fact to be deleted allow also skipping parent child parent chain preventing fast delete of the child the use of from_field comes from the need to avoid cascade back to parent when parent delete is cascading to child foreign keys pointing to this model something like generic foreign key return the objs in suitably sized batches for the used connection add objs to the collection of objects to be deleted as well as all parent instances objs must be a homogeneous iterable collection of model instances e g a queryset if collect_related is true related objects will be handled by their respective on_delete handler if the call is the result of a cascade source should be the model that caused it and nullable should be set to true if the relation can be null if reverse_dependency is true source will be deleted before the current model rather than after needed for cascading to parent models the one case in which the cascade follows the forwards direction of an fk rather than the reverse direction if keep_parents is true data of parent model s will be not deleted if fail_on_restricted is false error won t be raised even if it s prohibited to delete such objects due to restrict that defers restricted object checking in recursive calls where the top level call may need to collect more objects to determine whether restricted ones can be deleted recursively collect concrete model s parent models but not their related objects these will be found by meta get_fields preserve parent reverse relationships if keep_parents true non referenced fields can be deferred if no signal receivers are connected for the related model as they ll never be exposed to the user skip field deferring when some relationships are select_related as interactions between both features are hard to get right this should only happen in the rare cases where related_objects is overridden anyway it s something like generic foreign key raise an error if collected restricted objects restrict aren t candidates for deletion also collected via cascade get a queryset of the related model to objs via related fields sort instance collections if possible bring the models in an order suitable for databases that don t support transactions or cannot defer constraint checks until the end of a transaction number of objects deleted for each model label optimize for the case with a single obj and no dependencies send pre_delete signals fast deletes update fields reverse instance collections delete instances
from collections import Counter, defaultdict
from functools import partial, reduce
from itertools import chain
from operator import attrgetter, or_

from django.db import IntegrityError, connections, models, transaction
from django.db.models import query_utils, signals, sql


class ProtectedError(IntegrityError):
    def __init__(self, msg, protected_objects):
        self.protected_objects = protected_objects
        super().__init__(msg, protected_objects)


class RestrictedError(IntegrityError):
    def __init__(self, msg, restricted_objects):
        self.restricted_objects = restricted_objects
        super().__init__(msg, restricted_objects)


def CASCADE(collector, field, sub_objs, using):
    collector.collect(
        sub_objs,
        source=field.remote_field.model,
        source_attr=field.name,
        nullable=field.null,
        fail_on_restricted=False,
    )
    if field.null and not connections[using].features.can_defer_constraint_checks:
        collector.add_field_update(field, None, sub_objs)


def PROTECT(collector, field, sub_objs, using):
    raise ProtectedError(
        "Cannot delete some instances of model '%s' because they are "
        "referenced through a protected foreign key: '%s.%s'"
        % (
            field.remote_field.model.__name__,
            sub_objs[0].__class__.__name__,
            field.name,
        ),
        sub_objs,
    )


def RESTRICT(collector, field, sub_objs, using):
    collector.add_restricted_objects(field, sub_objs)
    collector.add_dependency(field.remote_field.model, field.model)


def SET(value):
    if callable(value):

        def set_on_delete(collector, field, sub_objs, using):
            collector.add_field_update(field, value(), sub_objs)

    else:

        def set_on_delete(collector, field, sub_objs, using):
            collector.add_field_update(field, value, sub_objs)

    set_on_delete.deconstruct = lambda: ("django.db.models.SET", (value,), {})
    set_on_delete.lazy_sub_objs = True
    return set_on_delete


def SET_NULL(collector, field, sub_objs, using):
    collector.add_field_update(field, None, sub_objs)


SET_NULL.lazy_sub_objs = True


def SET_DEFAULT(collector, field, sub_objs, using):
    collector.add_field_update(field, field.get_default(), sub_objs)


SET_DEFAULT.lazy_sub_objs = True


def DO_NOTHING(collector, field, sub_objs, using):
    pass


def get_candidate_relations_to_delete(opts):
    
    
    return (
        f
        for f in opts.get_fields(include_hidden=True)
        if f.auto_created and not f.concrete and (f.one_to_one or f.one_to_many)
    )


class Collector:
    def __init__(self, using, origin=None):
        self.using = using
        
        self.origin = origin
        
        self.data = defaultdict(set)
        
        self.field_updates = defaultdict(list)
        
        self.restricted_objects = defaultdict(partial(defaultdict, set))
        
        
        self.fast_deletes = []

        
        
        
        
        
        self.dependencies = defaultdict(set)  

    def add(self, objs, source=None, nullable=False, reverse_dependency=False):
        
        if not objs:
            return []
        new_objs = []
        model = objs[0].__class__
        instances = self.data[model]
        for obj in objs:
            if obj not in instances:
                new_objs.append(obj)
        instances.update(new_objs)
        
        
        
        if source is not None and not nullable:
            self.add_dependency(source, model, reverse_dependency=reverse_dependency)
        return new_objs

    def add_dependency(self, model, dependency, reverse_dependency=False):
        if reverse_dependency:
            model, dependency = dependency, model
        self.dependencies[model._meta.concrete_model].add(
            dependency._meta.concrete_model
        )
        self.data.setdefault(dependency, self.data.default_factory())

    def add_field_update(self, field, value, objs):
        
        self.field_updates[field, value].append(objs)

    def add_restricted_objects(self, field, objs):
        if objs:
            model = objs[0].__class__
            self.restricted_objects[model][field].update(objs)

    def clear_restricted_objects_from_set(self, model, objs):
        if model in self.restricted_objects:
            self.restricted_objects[model] = {
                field: items - objs
                for field, items in self.restricted_objects[model].items()
            }

    def clear_restricted_objects_from_queryset(self, model, qs):
        if model in self.restricted_objects:
            objs = set(
                qs.filter(
                    pk__in=[
                        obj.pk
                        for objs in self.restricted_objects[model].values()
                        for obj in objs
                    ]
                )
            )
            self.clear_restricted_objects_from_set(model, objs)

    def _has_signal_listeners(self, model):
        return signals.pre_delete.has_listeners(
            model
        ) or signals.post_delete.has_listeners(model)

    def can_fast_delete(self, objs, from_field=None):
        
        if from_field and from_field.remote_field.on_delete is not CASCADE:
            return False
        if hasattr(objs, "_meta"):
            model = objs._meta.model
        elif hasattr(objs, "model") and hasattr(objs, "_raw_delete"):
            model = objs.model
        else:
            return False
        if self._has_signal_listeners(model):
            return False
        
        
        opts = model._meta
        return (
            all(
                link == from_field
                for link in opts.concrete_model._meta.parents.values()
            )
            and
            
            all(
                related.field.remote_field.on_delete is DO_NOTHING
                for related in get_candidate_relations_to_delete(opts)
            )
            and (
                
                not any(
                    hasattr(field, "bulk_related_objects")
                    for field in opts.private_fields
                )
            )
        )

    def get_del_batches(self, objs, fields):
        
        field_names = [field.name for field in fields]
        conn_batch_size = max(
            connections[self.using].ops.bulk_batch_size(field_names, objs), 1
        )
        if len(objs) > conn_batch_size:
            return [
                objs[i : i + conn_batch_size]
                for i in range(0, len(objs), conn_batch_size)
            ]
        else:
            return [objs]

    def collect(
        self,
        objs,
        source=None,
        nullable=False,
        collect_related=True,
        source_attr=None,
        reverse_dependency=False,
        keep_parents=False,
        fail_on_restricted=True,
    ):
        
        if self.can_fast_delete(objs):
            self.fast_deletes.append(objs)
            return
        new_objs = self.add(
            objs, source, nullable, reverse_dependency=reverse_dependency
        )
        if not new_objs:
            return

        model = new_objs[0].__class__

        if not keep_parents:
            
            
            concrete_model = model._meta.concrete_model
            for ptr in concrete_model._meta.parents.values():
                if ptr:
                    parent_objs = [getattr(obj, ptr.name) for obj in new_objs]
                    self.collect(
                        parent_objs,
                        source=model,
                        source_attr=ptr.remote_field.related_name,
                        collect_related=False,
                        reverse_dependency=True,
                        fail_on_restricted=False,
                    )
        if not collect_related:
            return

        if keep_parents:
            parents = set(model._meta.get_parent_list())
        model_fast_deletes = defaultdict(list)
        protected_objects = defaultdict(list)
        for related in get_candidate_relations_to_delete(model._meta):
            
            if keep_parents and related.model in parents:
                continue
            field = related.field
            on_delete = field.remote_field.on_delete
            if on_delete == DO_NOTHING:
                continue
            related_model = related.related_model
            if self.can_fast_delete(related_model, from_field=field):
                model_fast_deletes[related_model].append(field)
                continue
            batches = self.get_del_batches(new_objs, [field])
            for batch in batches:
                sub_objs = self.related_objects(related_model, [field], batch)
                
                
                
                
                
                
                if not (
                    sub_objs.query.select_related
                    or self._has_signal_listeners(related_model)
                ):
                    referenced_fields = set(
                        chain.from_iterable(
                            (rf.attname for rf in rel.field.foreign_related_fields)
                            for rel in get_candidate_relations_to_delete(
                                related_model._meta
                            )
                        )
                    )
                    sub_objs = sub_objs.only(*tuple(referenced_fields))
                if getattr(on_delete, "lazy_sub_objs", False) or sub_objs:
                    try:
                        on_delete(self, field, sub_objs, self.using)
                    except ProtectedError as error:
                        key = "'%s.%s'" % (field.model.__name__, field.name)
                        protected_objects[key] += error.protected_objects
        if protected_objects:
            raise ProtectedError(
                "Cannot delete some instances of model %r because they are "
                "referenced through protected foreign keys: %s."
                % (
                    model.__name__,
                    ", ".join(protected_objects),
                ),
                set(chain.from_iterable(protected_objects.values())),
            )
        for related_model, related_fields in model_fast_deletes.items():
            batches = self.get_del_batches(new_objs, related_fields)
            for batch in batches:
                sub_objs = self.related_objects(related_model, related_fields, batch)
                self.fast_deletes.append(sub_objs)
        for field in model._meta.private_fields:
            if hasattr(field, "bulk_related_objects"):
                
                sub_objs = field.bulk_related_objects(new_objs, self.using)
                self.collect(
                    sub_objs, source=model, nullable=True, fail_on_restricted=False
                )

        if fail_on_restricted:
            
            
            for related_model, instances in self.data.items():
                self.clear_restricted_objects_from_set(related_model, instances)
            for qs in self.fast_deletes:
                self.clear_restricted_objects_from_queryset(qs.model, qs)
            if self.restricted_objects.values():
                restricted_objects = defaultdict(list)
                for related_model, fields in self.restricted_objects.items():
                    for field, objs in fields.items():
                        if objs:
                            key = "'%s.%s'" % (related_model.__name__, field.name)
                            restricted_objects[key] += objs
                if restricted_objects:
                    raise RestrictedError(
                        "Cannot delete some instances of model %r because "
                        "they are referenced through restricted foreign keys: "
                        "%s."
                        % (
                            model.__name__,
                            ", ".join(restricted_objects),
                        ),
                        set(chain.from_iterable(restricted_objects.values())),
                    )

    def related_objects(self, related_model, related_fields, objs):
        
        predicate = query_utils.Q.create(
            [(f"{related_field.name}__in", objs) for related_field in related_fields],
            connector=query_utils.Q.OR,
        )
        return related_model._base_manager.using(self.using).filter(predicate)

    def instances_with_model(self):
        for model, instances in self.data.items():
            for obj in instances:
                yield model, obj

    def sort(self):
        sorted_models = []
        concrete_models = set()
        models = list(self.data)
        while len(sorted_models) < len(models):
            found = False
            for model in models:
                if model in sorted_models:
                    continue
                dependencies = self.dependencies.get(model._meta.concrete_model)
                if not (dependencies and dependencies.difference(concrete_models)):
                    sorted_models.append(model)
                    concrete_models.add(model._meta.concrete_model)
                    found = True
            if not found:
                return
        self.data = {model: self.data[model] for model in sorted_models}

    def delete(self):
        
        for model, instances in self.data.items():
            self.data[model] = sorted(instances, key=attrgetter("pk"))

        
        
        
        self.sort()
        
        deleted_counter = Counter()

        
        if len(self.data) == 1 and len(instances) == 1:
            instance = list(instances)[0]
            if self.can_fast_delete(instance):
                with transaction.mark_for_rollback_on_error(self.using):
                    count = sql.DeleteQuery(model).delete_batch(
                        [instance.pk], self.using
                    )
                setattr(instance, model._meta.pk.attname, None)
                return count, {model._meta.label: count}

        with transaction.atomic(using=self.using, savepoint=False):
            
            for model, obj in self.instances_with_model():
                if not model._meta.auto_created:
                    signals.pre_delete.send(
                        sender=model,
                        instance=obj,
                        using=self.using,
                        origin=self.origin,
                    )

            
            for qs in self.fast_deletes:
                count = qs._raw_delete(using=self.using)
                if count:
                    deleted_counter[qs.model._meta.label] += count

            
            for (field, value), instances_list in self.field_updates.items():
                updates = []
                objs = []
                for instances in instances_list:
                    if (
                        isinstance(instances, models.QuerySet)
                        and instances._result_cache is None
                    ):
                        updates.append(instances)
                    else:
                        objs.extend(instances)
                if updates:
                    combined_updates = reduce(or_, updates)
                    combined_updates.update(**{field.name: value})
                if objs:
                    model = objs[0].__class__
                    query = sql.UpdateQuery(model)
                    query.update_batch(
                        list({obj.pk for obj in objs}), {field.name: value}, self.using
                    )

            
            for instances in self.data.values():
                instances.reverse()

            
            for model, instances in self.data.items():
                query = sql.DeleteQuery(model)
                pk_list = [obj.pk for obj in instances]
                count = query.delete_batch(pk_list, self.using)
                if count:
                    deleted_counter[model._meta.label] += count

                if not model._meta.auto_created:
                    for obj in instances:
                        signals.post_delete.send(
                            sender=model,
                            instance=obj,
                            using=self.using,
                            origin=self.origin,
                        )

        for model, instances in self.data.items():
            for instance in instances:
                setattr(instance, model._meta.pk.attname, None)
        return sum(deleted_counter.values()), dict(deleted_counter)