@@ -667,6 +667,33 @@ def prepare_duplicates_for_delete(obj):
667667 # so original_finding.all() now only contains outside-scope duplicates.
668668 reconfigure_duplicate_cluster (original , original .original_finding .all ())
669669
670+ # When DUPLICATE_CLUSTER_CASCADE_DELETE=True, reconfigure_duplicate_cluster is a no-op.
671+ # Match legacy finding_delete(): delete outside-scope cluster members that point at
672+ # in-scope originals (duplicate_cluster.order_by("-id").delete()). Transitive duplicate
673+ # chains do not need a separate expansion pass — fix_loop_duplicates above normalizes them.
674+ if settings .DUPLICATE_CLUSTER_CASCADE_DELETE :
675+ outside_cascade_qs = Finding .objects .filter (
676+ duplicate_finding_id__in = scope_ids_subquery ,
677+ ).exclude (id__in = scope_ids_subquery )
678+ outside_count = outside_cascade_qs .count ()
679+ if outside_count :
680+ logger .debug (
681+ "cascade-delete %d outside-scope duplicate findings (DUPLICATE_CLUSTER_CASCADE_DELETE)" ,
682+ outside_count ,
683+ )
684+ bulk_delete_findings (outside_cascade_qs , order_desc = True )
685+ else :
686+ outside_orphan_count = Finding .objects .filter (
687+ duplicate_finding_id__in = scope_ids_subquery ,
688+ ).exclude (
689+ id__in = scope_ids_subquery ,
690+ ).update (duplicate_finding = None , duplicate = False )
691+ if outside_orphan_count :
692+ logger .debug (
693+ "nulled %d outside-scope duplicate_finding references to prevent FK violation" ,
694+ outside_orphan_count ,
695+ )
696+
670697
671698@receiver (pre_delete , sender = Test )
672699def test_pre_delete (sender , instance , ** kwargs ):
@@ -763,14 +790,18 @@ def bulk_clear_finding_m2m(finding_qs):
763790 Notes .objects .filter (id__in = note_ids ).delete ()
764791
765792
766- def _bulk_delete_findings_internal (finding_qs , chunk_size = 1000 ):
793+ def _bulk_delete_findings_internal (finding_qs , chunk_size = 1000 , * , order_desc = False ):
767794 """
768795 Delete findings and all related objects efficiently. Including any related object in Dojo-Pro
769796
770797 Sends the pre_bulk_delete signal, clears M2M through tables (not
771798 discovered by _meta.related_objects), then uses cascade_delete for
772799 all FK relations via raw SQL.
773800 Chunked with per-chunk transaction.atomic() for crash safety.
801+
802+ When order_desc is True, findings are processed highest id first (matches
803+ finding_delete: duplicate_cluster.order_by("-id").delete()) so self-FK
804+ duplicate chains delete children before parents.
774805 """
775806 from dojo .signals import pre_bulk_delete_findings # noqa: PLC0415 circular import
776807 from dojo .utils_cascade_delete import ( # noqa: PLC0415 circular import
@@ -780,9 +811,10 @@ def _bulk_delete_findings_internal(finding_qs, chunk_size=1000):
780811
781812 pre_bulk_delete_findings .send (sender = Finding , finding_qs = finding_qs )
782813 bulk_clear_finding_m2m (finding_qs )
814+ ordered_qs = finding_qs .order_by ("-id" ) if order_desc else finding_qs .order_by ("id" )
783815 for chunk_num , chunk_ids in enumerate (
784816 batched (
785- finding_qs .values_list ("id" , flat = True ). order_by ( "id" ).iterator (chunk_size = chunk_size ),
817+ ordered_qs .values_list ("id" , flat = True ).iterator (chunk_size = chunk_size ),
786818 chunk_size ,
787819 strict = False ,
788820 ),
@@ -798,7 +830,7 @@ def _bulk_delete_findings_internal(finding_qs, chunk_size=1000):
798830 )
799831
800832
801- def bulk_delete_findings (finding_qs , chunk_size = 1000 , cascade_root = None ):
833+ def bulk_delete_findings (finding_qs , chunk_size = 1000 , cascade_root = None , * , order_desc = False ):
802834 """
803835 Entry point; may delegate to Pro via settings.BULK_DELETE_FINDINGS_METHOD.
804836
@@ -813,8 +845,9 @@ def bulk_delete_findings(finding_qs, chunk_size=1000, cascade_root=None):
813845 finding_qs ,
814846 chunk_size = chunk_size ,
815847 cascade_root = cascade_root ,
848+ order_desc = order_desc ,
816849 )
817- return _bulk_delete_findings_internal (finding_qs , chunk_size = chunk_size )
850+ return _bulk_delete_findings_internal (finding_qs , chunk_size = chunk_size , order_desc = order_desc )
818851
819852
820853def fix_loop_duplicates (scope_qs = None ):
0 commit comments