Index: tests/modeltests/on_delete/__init__.py
===================================================================
--- tests/modeltests/on_delete/__init__.py	(revision 0)
+++ tests/modeltests/on_delete/__init__.py	(revision 0)
@@ -0,0 +1 @@
+#
\ No newline at end of file
Index: tests/modeltests/on_delete/models.py
===================================================================
--- tests/modeltests/on_delete/models.py	(revision 0)
+++ tests/modeltests/on_delete/models.py	(revision 0)
@@ -0,0 +1,204 @@
+from django.test import TestCase
+from django.db import models, IntegrityError
+
+class R(models.Model):
+    is_default = models.BooleanField(default=False)
+
+    def __str__(self):
+        return "%s" % self.pk
+
+get_default_r = lambda: R.objects.get_or_create(is_default=True)[0]
+    
+class S(models.Model):
+    r = models.ForeignKey(R)
+    
+class T(models.Model):
+    s = models.ForeignKey(S)
+
+class U(models.Model):
+    t = models.ForeignKey(T)
+
+
+class A(models.Model):
+    name = models.CharField(max_length=10)    
+
+    auto = models.ForeignKey(R, related_name="auto_set")
+    auto_nullable = models.ForeignKey(R, null=True, related_name='auto_nullable_set')
+    setnull = models.ForeignKey(R, on_delete=models.SET_NULL, null=True, related_name='setnull_set')
+    setdefault = models.ForeignKey(R, on_delete=models.SET_DEFAULT, default=get_default_r, related_name='setdefault_set')
+    setdefault_none = models.ForeignKey(R, on_delete=models.SET_DEFAULT, default=None, null=True, related_name='setnull_nullable_set')
+    cascade = models.ForeignKey(R, on_delete=models.CASCADE, related_name='cascade_set')
+    cascade_nullable = models.ForeignKey(R, on_delete=models.CASCADE, null=True, related_name='cascade_nullable_set')
+    protect = models.ForeignKey(R, on_delete=models.PROTECT, null=True)
+    donothing = models.ForeignKey(R, on_delete=models.DO_NOTHING, null=True, related_name='donothing_set')
+    
+def create_a(name):
+    a = A(name=name)
+    for name in ('auto', 'auto_nullable', 'setnull', 'setdefault', 'setdefault_none', 'cascade', 'cascade_nullable', 'protect', 'donothing'):
+        r = R.objects.create()
+        setattr(a, name, r)
+    a.save()
+    return a
+    
+class M(models.Model):
+    m2m_through = models.ManyToManyField(R, through="MR", related_name="m_through_set")
+    m2m = models.ManyToManyField(R, related_name="m_set")
+    
+class MR(models.Model):
+    m = models.ForeignKey(M)
+    r = models.ForeignKey(R)
+    
+
+class OnDeleteTests(TestCase):
+    def test_basics(self):
+        DEFAULT = get_default_r()
+        
+        a = create_a('auto')
+        a.auto.delete()
+        self.failIf(A.objects.filter(name='auto').exists())
+        
+        a = create_a('auto_nullable')
+        a.auto_nullable.delete()
+        self.failUnlessEqual([a], list(A.objects.all()))
+        
+        a = create_a('setnull')
+        a.setnull.delete()
+        a = A.objects.get(pk=a.pk)
+        self.failUnlessEqual(None, a.setnull)
+        
+        a = create_a('setdefault')
+        a.setdefault.delete()
+        a = A.objects.get(pk=a.pk)
+        self.failUnlessEqual(DEFAULT, a.setdefault)
+        
+        a = create_a('setdefault_none')
+        a.setdefault_none.delete()
+        a = A.objects.get(pk=a.pk)
+        self.failUnlessEqual(None, a.setdefault_none)
+        
+        a = create_a('cascade')
+        a.cascade.delete()
+        self.failIf(A.objects.filter(name='cascade').exists())
+        
+        a = create_a('cascade_nullable')
+        a.cascade_nullable.delete()
+        self.failIf(A.objects.filter(name='cascade_nullable').exists())
+        
+        a = create_a('protect')
+        self.assertRaises(IntegrityError, a.protect.delete)
+        
+        # Testing DO_NOTHING is a bit harder: It would raise IntegrityError for a normal model, 
+        # so we connect to pre_delete and set the fk to a known value.
+        replacement_r = R.objects.create()
+        def check_do_nothing(sender, **kwargs):
+            obj = kwargs['instance']
+            obj.donothing_set.update(donothing=replacement_r)
+        models.signals.pre_delete.connect(check_do_nothing)
+        a = create_a('do_nothing')
+        a.donothing.delete()
+        a = A.objects.get(pk=a.pk)
+        self.failUnlessEqual(replacement_r, a.donothing)
+        models.signals.pre_delete.disconnect(check_do_nothing)        
+        
+        # cleanup
+        A.objects.all().update(protect=None, donothing=None)
+        R.objects.all().delete()
+        self.failIf(A.objects.exists())
+        
+    def test_m2m(self):
+        m = M.objects.create()
+        r = R.objects.create()
+        MR.objects.create(m=m, r=r)
+        r.delete()
+        self.failIf(MR.objects.exists())
+        
+        r = R.objects.create()
+        MR.objects.create(m=m, r=r)
+        m.delete()
+        self.failIf(MR.objects.exists())
+        
+        m = M.objects.create()
+        r = R.objects.create()
+        m.m2m.add(r)
+        r.delete()
+        through = M._meta.get_field('m2m').rel.through
+        self.failIf(through.objects.exists())
+        
+        r = R.objects.create()
+        m.m2m.add(r)
+        m.delete()
+        self.failIf(through.objects.exists())
+    
+    def assert_num_queries(self, num, func, *args, **kwargs):
+        from django.conf import settings
+        from django.db import connection
+        old_debug = settings.DEBUG
+        settings.DEBUG = True
+        query_count = len(connection.queries)
+        func(*args, **kwargs)
+        self.failUnlessEqual(num, len(connection.queries) - query_count)
+        connection.queries = connection.queries[:query_count]
+        settings.DEBUG = old_debug
+    
+    def test_bulk(self):
+        from django.db.models.sql.constants import GET_ITERATOR_CHUNK_SIZE
+        s = S.objects.create(r=R.objects.create())
+        for i in xrange(2*GET_ITERATOR_CHUNK_SIZE):
+            T.objects.create(s=s)
+        #   1 (select related `T` instances)
+        # + 1 (select related `U` instances)
+        # + 2 (delete `T` instances in batches)
+        # + 1 (delete `s`)
+        self.assert_num_queries(5, s.delete)
+        self.failIf(S.objects.exists())
+        
+    def test_cache_update(self):
+        deleted = []
+        related_setnull_sets = []
+        def pre_delete(sender, **kwargs):
+            obj = kwargs['instance']
+            deleted.append(obj)
+            if isinstance(obj, R):
+                related_setnull_sets.append(list(a.pk for a in obj.setnull_set.all()))
+
+        models.signals.pre_delete.connect(pre_delete)
+        a = create_a('cache_update_setnull')
+        a.setnull.delete()
+        
+        a = create_a('cache_update_setnull')
+        a.cascade.delete()
+        
+        for obj in deleted:
+            self.failUnlessEqual(None, obj.pk)
+            
+        for pk_list in related_setnull_sets:
+            for a in A.objects.filter(id__in=pk_list):
+                self.failUnlessEqual(None, a.setnull)
+        
+        models.signals.pre_delete.disconnect(pre_delete)
+
+    def test_deletion_order(self):
+        pre_delete_order = []
+        post_delete_order = []
+
+        def log_post_delete(sender, **kwargs):
+            pre_delete_order.append((sender, kwargs['instance'].pk))
+
+        def log_pre_delete(sender, **kwargs):
+            post_delete_order.append((sender, kwargs['instance'].pk))
+        
+        models.signals.post_delete.connect(log_post_delete)
+        models.signals.pre_delete.connect(log_pre_delete)
+        
+        r = R.objects.create(pk=1)
+        s1 = S.objects.create(pk=1, r=r)
+        s2 = S.objects.create(pk=2, r=r)
+        t1 = T.objects.create(pk=1, s=s1)
+        t2 = T.objects.create(pk=2, s=s2)
+        r.delete()
+        self.failUnlessEqual(pre_delete_order, [(T, 2), (T, 1), (S, 2), (S, 1), (R, 1)])
+        self.failUnlessEqual(post_delete_order, [(T, 1), (T, 2), (S, 1), (S, 2), (R, 1)])
+        
+        models.signals.post_delete.disconnect(log_post_delete)
+        models.signals.post_delete.disconnect(log_pre_delete)
+        
Index: tests/modeltests/delete/models.py
===================================================================
--- tests/modeltests/delete/models.py	(revision 11724)
+++ tests/modeltests/delete/models.py	(working copy)
@@ -44,38 +44,6 @@
 __test__ = {'API_TESTS': """
 ### Tests for models A,B,C,D ###
 
-## First, test the CollectedObjects data structure directly
-
->>> from django.db.models.query import CollectedObjects
-
->>> g = CollectedObjects()
->>> g.add("key1", 1, "item1", None)
-False
->>> g["key1"]
-{1: 'item1'}
->>> g.add("key2", 1, "item1", "key1")
-False
->>> g.add("key2", 2, "item2", "key1")
-False
->>> g["key2"]
-{1: 'item1', 2: 'item2'}
->>> g.add("key3", 1, "item1", "key1")
-False
->>> g.add("key3", 1, "item1", "key2")
-True
->>> g.ordered_keys()
-['key3', 'key2', 'key1']
-
->>> g.add("key2", 1, "item1", "key3")
-True
->>> g.ordered_keys()
-Traceback (most recent call last):
-    ...
-CyclicDependency: There is a cyclic dependency of items to be processed.
-
-
-## Second, test the usage of CollectedObjects by Model.delete()
-
 # Due to the way that transactions work in the test harness,
 # doing m.delete() here can work but fail in a real situation,
 # since it may delete all objects, but not in the right order.
@@ -111,11 +79,6 @@
 >>> c1.save()
 >>> d1 = D(c=c1, a=a1)
 >>> d1.save()
-
->>> o = CollectedObjects()
->>> a1._collect_sub_objects(o)
->>> o.keys()
-[<class 'modeltests.delete.models.D'>, <class 'modeltests.delete.models.C'>, <class 'modeltests.delete.models.B'>, <class 'modeltests.delete.models.A'>]
 >>> a1.delete()
 
 # Same again with a known bad order
@@ -130,54 +93,17 @@
 >>> c2.save()
 >>> d2 = D(c=c2, a=a2)
 >>> d2.save()
-
->>> o = CollectedObjects()
->>> a2._collect_sub_objects(o)
->>> o.keys()
-[<class 'modeltests.delete.models.D'>, <class 'modeltests.delete.models.C'>, <class 'modeltests.delete.models.B'>, <class 'modeltests.delete.models.A'>]
 >>> a2.delete()
 
 ### Tests for models E,F - nullable related fields ###
 
-## First, test the CollectedObjects data structure directly
-
->>> g = CollectedObjects()
->>> g.add("key1", 1, "item1", None)
-False
->>> g.add("key2", 1, "item1", "key1", nullable=True)
-False
->>> g.add("key1", 1, "item1", "key2")
-True
->>> g.ordered_keys()
-['key1', 'key2']
-
-## Second, test the usage of CollectedObjects by Model.delete()
-
 >>> e1 = E()
 >>> e1.save()
 >>> f1 = F(e=e1)
 >>> f1.save()
 >>> e1.f = f1
 >>> e1.save()
-
-# Since E.f is nullable, we should delete F first (after nulling out
-# the E.f field), then E.
-
->>> o = CollectedObjects()
->>> e1._collect_sub_objects(o)
->>> o.keys()
-[<class 'modeltests.delete.models.F'>, <class 'modeltests.delete.models.E'>]
-
-# temporarily replace the UpdateQuery class to verify that E.f is actually nulled out first
->>> import django.db.models.sql
->>> class LoggingUpdateQuery(django.db.models.sql.UpdateQuery):
-...     def clear_related(self, related_field, pk_list):
-...         print "CLEARING FIELD",related_field.name
-...         return super(LoggingUpdateQuery, self).clear_related(related_field, pk_list)
->>> original_class = django.db.models.sql.UpdateQuery
->>> django.db.models.sql.UpdateQuery = LoggingUpdateQuery
 >>> e1.delete()
-CLEARING FIELD f
 
 >>> e2 = E()
 >>> e2.save()
@@ -185,18 +111,6 @@
 >>> f2.save()
 >>> e2.f = f2
 >>> e2.save()
-
-# Same deal as before, though we are starting from the other object.
-
->>> o = CollectedObjects()
->>> f2._collect_sub_objects(o)
->>> o.keys()
-[<class 'modeltests.delete.models.F'>, <class 'modeltests.delete.models.E'>]
-
 >>> f2.delete()
-CLEARING FIELD f
-
-# Put this back to normal
->>> django.db.models.sql.UpdateQuery = original_class
 """
 }
Index: django/db/models/sql/subqueries.py
===================================================================
--- django/db/models/sql/subqueries.py	(revision 11724)
+++ django/db/models/sql/subqueries.py	(working copy)
@@ -34,55 +34,40 @@
         self.where = where
         self.execute_sql(None)
 
-    def delete_batch_related(self, pk_list):
+    def delete_generic_relation_hack(self, pk_list):
         """
-        Set up and execute delete queries for all the objects related to the
-        primary key values in pk_list. To delete the objects themselves, use
-        the delete_batch() method.
-
-        More than one physical query may be executed if there are a
-        lot of values in pk_list.
+        Delete objects related to self.model through a GenericRelation.
+        This should be handled by a custom `on_delete` handler in django.contrib.contentttypes.
         """
         from django.contrib.contenttypes import generic
         cls = self.model
-        for related in cls._meta.get_all_related_many_to_many_objects():
-            if not isinstance(related.field, generic.GenericRelation):
-                for offset in range(0, len(pk_list), GET_ITERATOR_CHUNK_SIZE):
-                    where = self.where_class()
-                    where.add((Constraint(None,
-                            related.field.m2m_reverse_name(), related.field),
-                            'in',
-                            pk_list[offset : offset+GET_ITERATOR_CHUNK_SIZE]),
-                            AND)
-                    self.do_query(related.field.m2m_db_table(), where)
-
         for f in cls._meta.many_to_many:
-            w1 = self.where_class()
-            if isinstance(f, generic.GenericRelation):
-                from django.contrib.contenttypes.models import ContentType
-                field = f.rel.to._meta.get_field(f.content_type_field_name)
-                w1.add((Constraint(None, field.column, field), 'exact',
-                        ContentType.objects.get_for_model(cls).id), AND)
+            if not isinstance(f, generic.GenericRelation):
+                continue
+            w1 = self.where_class()            
+            from django.contrib.contenttypes.models import ContentType
+            field = f.rel.to._meta.get_field(f.content_type_field_name)
+            w1.add((Constraint(None, field.column, field), 'exact',
+                    ContentType.objects.get_for_model(cls).id), AND)
             for offset in range(0, len(pk_list), GET_ITERATOR_CHUNK_SIZE):
                 where = self.where_class()
                 where.add((Constraint(None, f.m2m_column_name(), f), 'in',
                         pk_list[offset : offset + GET_ITERATOR_CHUNK_SIZE]),
                         AND)
-                if w1:
-                    where.add(w1, AND)
+                where.add(w1, AND)
                 self.do_query(f.m2m_db_table(), where)
 
-    def delete_batch(self, pk_list):
+    def delete_batch(self, pk_list, field=None):
         """
-        Set up and execute delete queries for all the objects in pk_list. This
-        should be called after delete_batch_related(), if necessary.
+        Set up and execute delete queries for all the objects in pk_list. 
 
         More than one physical query may be executed if there are a
         lot of values in pk_list.
         """
+        if not field:
+            field = self.model._meta.pk
         for offset in range(0, len(pk_list), GET_ITERATOR_CHUNK_SIZE):
             where = self.where_class()
-            field = self.model._meta.pk
             where.add((Constraint(None, field.column, field), 'in',
                     pk_list[offset : offset + GET_ITERATOR_CHUNK_SIZE]), AND)
             self.do_query(self.model._meta.db_table, where)
@@ -201,20 +186,12 @@
         for alias in self.tables[1:]:
             self.alias_refcount[alias] = 0
 
-    def clear_related(self, related_field, pk_list):
-        """
-        Set up and execute an update query that clears related entries for the
-        keys in pk_list.
-
-        This is used by the QuerySet.delete_objects() method.
-        """
+    def update_batch(self, pk_list, values):
+        pk_field = self.model._meta.pk
+        self.add_update_values(values)
         for offset in range(0, len(pk_list), GET_ITERATOR_CHUNK_SIZE):
-            self.where = self.where_class()
-            f = self.model._meta.pk
-            self.where.add((Constraint(None, f.column, f), 'in',
-                    pk_list[offset : offset + GET_ITERATOR_CHUNK_SIZE]),
-                    AND)
-            self.values = [(related_field.column, None, '%s')]
+            self.where = self.where_class()            
+            self.where.add((Constraint(None, pk_field.column, pk_field), 'in', pk_list[offset : offset + GET_ITERATOR_CHUNK_SIZE]), AND)
             self.execute_sql(None)
 
     def add_update_values(self, values):
Index: django/db/models/base.py
===================================================================
--- django/db/models/base.py	(revision 11724)
+++ django/db/models/base.py	(working copy)
@@ -7,8 +7,9 @@
 from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned, FieldError
 from django.db.models.fields import AutoField, FieldDoesNotExist
 from django.db.models.fields.related import OneToOneRel, ManyToOneRel, OneToOneField
-from django.db.models.query import delete_objects, Q
-from django.db.models.query_utils import CollectedObjects, DeferredAttribute
+from django.db.models.query import Q
+from django.db.models.query_utils import DeferredAttribute
+from django.db.models.deletion import CollectedObjects
 from django.db.models.options import Options
 from django.db import connection, transaction, DatabaseError
 from django.db.models import signals
@@ -513,71 +514,15 @@
 
     save_base.alters_data = True
 
-    def _collect_sub_objects(self, seen_objs, parent=None, nullable=False):
-        """
-        Recursively populates seen_objs with all objects related to this
-        object.
-
-        When done, seen_objs.items() will be in the format:
-            [(model_class, {pk_val: obj, pk_val: obj, ...}),
-             (model_class, {pk_val: obj, pk_val: obj, ...}), ...]
-        """
-        pk_val = self._get_pk_val()
-        if seen_objs.add(self.__class__, pk_val, self, parent, nullable):
-            return
-
-        for related in self._meta.get_all_related_objects():
-            rel_opts_name = related.get_accessor_name()
-            if isinstance(related.field.rel, OneToOneRel):
-                try:
-                    sub_obj = getattr(self, rel_opts_name)
-                except ObjectDoesNotExist:
-                    pass
-                else:
-                    sub_obj._collect_sub_objects(seen_objs, self.__class__, related.field.null)
-            else:
-                # To make sure we can access all elements, we can't use the
-                # normal manager on the related object. So we work directly
-                # with the descriptor object.
-                for cls in self.__class__.mro():
-                    if rel_opts_name in cls.__dict__:
-                        rel_descriptor = cls.__dict__[rel_opts_name]
-                        break
-                else:
-                    # in the case of a hidden fkey just skip it, it'll get
-                    # processed as an m2m
-                    if not related.field.rel.is_hidden():
-                        raise AssertionError("Should never get here.")
-                    else:
-                        continue
-                delete_qs = rel_descriptor.delete_manager(self).all()
-                for sub_obj in delete_qs:
-                    sub_obj._collect_sub_objects(seen_objs, self.__class__, related.field.null)
-
-        # Handle any ancestors (for the model-inheritance case). We do this by
-        # traversing to the most remote parent classes -- those with no parents
-        # themselves -- and then adding those instances to the collection. That
-        # will include all the child instances down to "self".
-        parent_stack = [p for p in self._meta.parents.values() if p is not None]
-        while parent_stack:
-            link = parent_stack.pop()
-            parent_obj = getattr(self, link.name)
-            if parent_obj._meta.parents:
-                parent_stack.extend(parent_obj._meta.parents.values())
-                continue
-            # At this point, parent_obj is base class (no ancestor models). So
-            # delete it and all its descendents.
-            parent_obj._collect_sub_objects(seen_objs)
-
     def delete(self):
         assert self._get_pk_val() is not None, "%s object can't be deleted because its %s attribute is set to None." % (self._meta.object_name, self._meta.pk.attname)
 
         # Find all the objects than need to be deleted.
         seen_objs = CollectedObjects()
-        self._collect_sub_objects(seen_objs)
+        seen_objs.collect([self])
 
         # Actually delete the objects.
-        delete_objects(seen_objs)
+        seen_objs.delete()
 
     delete.alters_data = True
 
Index: django/db/models/options.py
===================================================================
--- django/db/models/options.py	(revision 11724)
+++ django/db/models/options.py	(working copy)
@@ -376,7 +376,7 @@
                     cache[obj] = parent
                 else:
                     cache[obj] = model
-        for klass in get_models():
+        for klass in get_models(include_auto_created=True):
             for f in klass._meta.local_fields:
                 if f.rel and not isinstance(f.rel.to, str) and self == f.rel.to._meta:
                     cache[RelatedObject(f.rel.to, klass, f)] = None
Index: django/db/models/fields/related.py
===================================================================
--- django/db/models/fields/related.py	(revision 11724)
+++ django/db/models/fields/related.py	(working copy)
@@ -1,4 +1,4 @@
-from django.db import connection, transaction
+from django.db import connection, transaction, IntegrityError
 from django.db.backends import util
 from django.db.models import signals, get_model
 from django.db.models.fields import AutoField, Field, IntegerField, PositiveIntegerField, PositiveSmallIntegerField, FieldDoesNotExist
@@ -18,6 +18,30 @@
 
 RECURSIVE_RELATIONSHIP_CONSTANT = 'self'
 
+def CASCADE(seen_objs, parent_model, related, sub_objs):
+    seen_objs.collect(sub_objs, parent_model)
+
+def PROTECT(seen_objs, parent_model, related, sub_objs):
+    msg = "Cannot delete some instances of model '%s' because they are referenced through a protected foreign key: '%s.%s'" % (
+        parent_model.__name__, related.model.__name__, related.field.name
+    )
+    raise IntegrityError(msg)
+
+def SET(value):
+    def set_on_delete(seen_objs, parent_model, related, sub_objs):
+        seen_objs.add_field_update(related.field, value, sub_objs)
+    return set_on_delete
+
+def SET_NULL(seen_objs, parent_model, related, sub_objs):
+    seen_objs.add_field_update(related.field, None, sub_objs)
+
+def SET_DEFAULT(seen_objs, parent_model, related, sub_objs):
+    seen_objs.add_field_update(related.field, related.field.get_default(), sub_objs)
+
+def DO_NOTHING(seen_objs, parent_model, related, sub_objs):
+    pass
+
+
 pending_lookups = {}
 
 def add_lazy_relation(cls, field, relation, operation):
@@ -625,7 +649,8 @@
 
 class ManyToOneRel(object):
     def __init__(self, to, field_name, related_name=None,
-            limit_choices_to=None, lookup_overrides=None, parent_link=False):
+            limit_choices_to=None, lookup_overrides=None, parent_link=False,
+            on_delete=None):
         try:
             to._meta
         except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT
@@ -638,6 +663,7 @@
         self.lookup_overrides = lookup_overrides or {}
         self.multiple = True
         self.parent_link = parent_link
+        self.on_delete = on_delete
 
     def is_hidden(self):
         "Should the related object be hidden?"
@@ -656,10 +682,12 @@
 
 class OneToOneRel(ManyToOneRel):
     def __init__(self, to, field_name, related_name=None,
-            limit_choices_to=None, lookup_overrides=None, parent_link=False):
+            limit_choices_to=None, lookup_overrides=None, parent_link=False, 
+            on_delete=None):
         super(OneToOneRel, self).__init__(to, field_name,
                 related_name=related_name, limit_choices_to=limit_choices_to,
-                lookup_overrides=lookup_overrides, parent_link=parent_link)
+                lookup_overrides=lookup_overrides, parent_link=parent_link,
+                on_delete=on_delete)
         self.multiple = False
 
 class ManyToManyRel(object):
@@ -696,12 +724,20 @@
         else:
             assert not to._meta.abstract, "%s cannot define a relation with abstract class %s" % (self.__class__.__name__, to._meta.object_name)
         kwargs['verbose_name'] = kwargs.get('verbose_name', None)
+        
+        on_delete = kwargs.pop('on_delete', None)
+        if on_delete is None:
+            if kwargs.get('null', False):
+                on_delete = SET_NULL
+            else:
+                on_delete = CASCADE
 
         kwargs['rel'] = rel_class(to, to_field,
             related_name=kwargs.pop('related_name', None),
             limit_choices_to=kwargs.pop('limit_choices_to', None),
             lookup_overrides=kwargs.pop('lookup_overrides', None),
-            parent_link=kwargs.pop('parent_link', False))
+            parent_link=kwargs.pop('parent_link', False),
+            on_delete=on_delete)
         Field.__init__(self, **kwargs)
 
         self.db_index = True
@@ -746,6 +782,14 @@
             target = self.rel.to._meta.db_table
         cls._meta.duplicate_targets[self.column] = (target, "o2m")
 
+        on_delete = self.rel.on_delete
+        if on_delete == SET_NULL and not self.null:
+            specification = "'on_delete=SET_NULL'"
+            raise ValueError("%s specified for %s '%s.%s', but the field is not nullable." % (specification, type(self).__name__, cls.__name__, name))
+        if on_delete == SET_DEFAULT and not self.has_default():
+            specification = "'on_delete=SET_DEFAULT'"
+            raise ValueError("%s specified for %s '%s.%s', but the field has no default value." % (specification, type(self).__name__, cls.__name__, name))
+
     def contribute_to_related_class(self, cls, related):
         # Internal FK's - i.e., those with a related name ending with '+' -
         # don't get a related descriptor.
Index: django/db/models/__init__.py
===================================================================
--- django/db/models/__init__.py	(revision 11724)
+++ django/db/models/__init__.py	(working copy)
@@ -11,6 +11,7 @@
 from django.db.models.fields.subclassing import SubfieldBase
 from django.db.models.fields.files import FileField, ImageField
 from django.db.models.fields.related import ForeignKey, OneToOneField, ManyToManyField, ManyToOneRel, ManyToManyRel, OneToOneRel
+from django.db.models.fields.related import CASCADE, PROTECT, SET, SET_NULL, SET_DEFAULT, DO_NOTHING
 from django.db.models import signals
 
 # Admin stages.
Index: django/db/models/deletion.py
===================================================================
--- django/db/models/deletion.py	(revision 0)
+++ django/db/models/deletion.py	(revision 0)
@@ -0,0 +1,166 @@
+from django.utils.datastructures import SortedDict
+from django.utils.functional import wraps
+from django.db import connection, transaction
+from django.db.models import signals, sql
+from django.db.models.sql.constants import GET_ITERATOR_CHUNK_SIZE
+
+def ensure_transaction(func):
+    @wraps(func)
+    def decorated(*args, **kwargs):
+        if not transaction.is_managed():
+            transaction.enter_transaction_management()
+            forced_managed = True
+        else:
+            forced_managed = False
+        try:                    
+            func(*args, **kwargs)
+            if forced_managed:
+                transaction.commit()
+            else:
+                transaction.commit_unless_managed()
+        finally:
+            if forced_managed:
+                transaction.leave_transaction_management()
+    return decorated
+
+class CollectedObjects(object):
+    def __init__(self, previously_seen=None):
+        # {model: {(field, value): set([instances])}}
+        self.field_updates = {}
+        self.data = {}
+        self.batches = {}
+        self.children = {}
+        if previously_seen:
+            self.blocked = set(previously_seen)
+            self.blocked.update(previously_seen.blocked)
+        else:
+            self.blocked = set()
+
+    def add(self, objs, parent_model=None):
+        if not objs:
+            return []
+        new_objs = []
+        model = objs[0].__class__
+        instances = self.data.setdefault(model, [])
+        for obj in objs:
+            if obj not in instances and obj not in self.blocked:
+                new_objs.append(obj)
+        instances.extend(new_objs)
+        # 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.
+        if new_objs and parent_model is not None:
+            self.children.setdefault(parent_model, set()).add(model)
+        return new_objs
+        
+    def add_batch(self, related, objs):
+        batches = self.batches.setdefault(related.model, dict())
+        batches[related.field] = objs
+        
+    def add_field_update(self, field, value, objs):
+        if not objs:
+            return
+        model = objs[0].__class__
+        updates = self.field_updates.setdefault(model, dict())
+        updates.setdefault((field, value), set()).update(objs)
+        
+    def collect(self, instances, parent=None, collect_related=True):
+        new_objs = self.add(instances, parent)
+        if not new_objs:
+            return
+        model = new_objs[0].__class__
+        
+        # Recusively collect parent models, but not their related objects.
+        for parent, ptr in model._meta.parents.items():
+            if ptr:
+                parent_objs = [getattr(obj, ptr.name) for obj in new_objs]
+                self.collect(parent_objs, model, collect_related=False)
+
+        if collect_related:
+            for related in model._meta.get_all_related_objects():
+                if related.model._meta.auto_created:
+                    self.add_batch(related, new_objs)
+                else:
+                    sub_objs = related.model._base_manager.filter(**{"%s__in" % related.field.name: new_objs})
+                    if sub_objs:
+                        related.field.rel.on_delete(self, model, related, sub_objs)
+
+    def instances_with_model(self):
+        for model, instances in self.data.iteritems():
+            for obj in instances:
+                yield model, obj
+                
+    def __iter__(self):
+        for model, obj in self.instances_with_model():
+            yield obj
+
+    def __nonzero__(self):
+        return bool(self.data)
+
+    def sort(self):
+        sorted_models = []
+        models = self.data.keys()
+        while len(sorted_models) < len(models):
+            found = False
+            for model in models:
+                if model in sorted_models:
+                    continue
+                children = self.children.get(model)
+                if not children or not children.difference(sorted_models):
+                    sorted_models.append(model)
+                    found = True
+            if not found:
+                return
+        self.data = SortedDict([(model, self.data[model]) for model in sorted_models])
+    
+    @ensure_transaction
+    def delete(self):
+        # sort instance collections 
+        for instances in self.data.itervalues():
+            instances.sort(key=lambda obj: obj.pk)
+
+        # if possible, bring models in an order suitable for databases that don't support transactions 
+        # or cannot defer contraint checks until the end of a transaction.
+        self.sort()
+        
+        # send pre_delete signals
+        for model, obj in self.instances_with_model():
+            if not model._meta.auto_created:
+                signals.pre_delete.send(sender=model, instance=obj)
+
+        # update fields
+        for model, instances_for_fieldvalues in self.field_updates.iteritems():
+            query = sql.UpdateQuery(model, connection)
+            for (field, value), instances in instances_for_fieldvalues.iteritems():
+                query.update_batch([obj.pk for obj in instances], {field.name: value})
+
+        # reverse instance collections
+        for instances in self.data.itervalues():
+            instances.reverse()
+
+        # delete batches
+        for model, batches in self.batches.iteritems():
+            query = sql.DeleteQuery(model, connection)
+            for field, instances in batches.iteritems():
+                query.delete_batch([obj.pk for obj in instances], field)
+
+        # delete instances
+        for model, instances in self.data.iteritems():
+            query = sql.DeleteQuery(model, connection)
+            pk_list = [obj.pk for obj in instances]
+            query.delete_generic_relation_hack(pk_list)            
+            query.delete_batch(pk_list)
+        
+        # send post_delete signals
+        for model, obj in self.instances_with_model():
+            if not model._meta.auto_created:
+                signals.post_delete.send(sender=model, instance=obj)
+        
+        # update collected instances
+        for model, instances_for_fieldvalues in self.field_updates.iteritems():
+            for (field, value), instances in instances_for_fieldvalues.iteritems():
+                for obj in instances:
+                    setattr(obj, field.attname, value)
+        for model, instances in self.data.iteritems():
+            for instance in instances:
+                setattr(instance, model._meta.pk.attname, None)
Index: django/db/models/query.py
===================================================================
--- django/db/models/query.py	(revision 11724)
+++ django/db/models/query.py	(working copy)
@@ -6,7 +6,8 @@
 from django.db import connection, transaction, IntegrityError
 from django.db.models.aggregates import Aggregate
 from django.db.models.fields import DateField
-from django.db.models.query_utils import Q, select_related_descend, CollectedObjects, CyclicDependency, deferred_class_factory
+from django.db.models.query_utils import Q, select_related_descend, deferred_class_factory
+from django.db.models.deletion import CollectedObjects
 from django.db.models import signals, sql
 
 # Used to control how many objects are worked with at once in some cases (e.g.
@@ -384,12 +385,11 @@
             # Collect all the objects to be deleted in this chunk, and all the
             # objects that are related to the objects that are to be deleted.
             seen_objs = CollectedObjects(seen_objs)
-            for object in del_query[:CHUNK_SIZE]:
-                object._collect_sub_objects(seen_objs)
+            seen_objs.collect(del_query[:CHUNK_SIZE])
 
             if not seen_objs:
                 break
-            delete_objects(seen_objs)
+            seen_objs.delete()
 
         # Clear the result cache, in case this QuerySet gets reused.
         self._result_cache = None
@@ -1000,82 +1000,7 @@
                 setattr(obj, f.get_cache_name(), rel_obj)
     return obj, index_end
 
-def delete_objects(seen_objs):
-    """
-    Iterate through a list of seen classes, and remove any instances that are
-    referred to.
-    """
-    if not transaction.is_managed():
-        transaction.enter_transaction_management()
-        forced_managed = True
-    else:
-        forced_managed = False
-    try:
-        ordered_classes = seen_objs.keys()
-    except CyclicDependency:
-        # If there is a cyclic dependency, we cannot in general delete the
-        # objects.  However, if an appropriate transaction is set up, or if the
-        # database is lax enough, it will succeed. So for now, we go ahead and
-        # try anyway.
-        ordered_classes = seen_objs.unordered_keys()
 
-    obj_pairs = {}
-    try:
-        for cls in ordered_classes:
-            items = seen_objs[cls].items()
-            items.sort()
-            obj_pairs[cls] = items
-
-            # Pre-notify all instances to be deleted.
-            for pk_val, instance in items:
-                if not cls._meta.auto_created:
-                    signals.pre_delete.send(sender=cls, instance=instance)
-
-            pk_list = [pk for pk,instance in items]
-            del_query = sql.DeleteQuery(cls, connection)
-            del_query.delete_batch_related(pk_list)
-
-            update_query = sql.UpdateQuery(cls, connection)
-            for field, model in cls._meta.get_fields_with_model():
-                if (field.rel and field.null and field.rel.to in seen_objs and
-                        filter(lambda f: f.column == field.rel.get_related_field().column,
-                        field.rel.to._meta.fields)):
-                    if model:
-                        sql.UpdateQuery(model, connection).clear_related(field,
-                                pk_list)
-                    else:
-                        update_query.clear_related(field, pk_list)
-
-        # Now delete the actual data.
-        for cls in ordered_classes:
-            items = obj_pairs[cls]
-            items.reverse()
-
-            pk_list = [pk for pk,instance in items]
-            del_query = sql.DeleteQuery(cls, connection)
-            del_query.delete_batch(pk_list)
-
-            # Last cleanup; set NULLs where there once was a reference to the
-            # object, NULL the primary key of the found objects, and perform
-            # post-notification.
-            for pk_val, instance in items:
-                for field in cls._meta.fields:
-                    if field.rel and field.null and field.rel.to in seen_objs:
-                        setattr(instance, field.attname, None)
-
-                if not cls._meta.auto_created:
-                    signals.post_delete.send(sender=cls, instance=instance)
-                setattr(instance, cls._meta.pk.attname, None)
-
-        if forced_managed:
-            transaction.commit()
-        else:
-            transaction.commit_unless_managed()
-    finally:
-        if forced_managed:
-            transaction.leave_transaction_management()
-
-
 def insert_query(model, values, return_id=False, raw_values=False):
     """
     Inserts a new record for the given model. This provides an interface to
Index: django/db/models/query_utils.py
===================================================================
--- django/db/models/query_utils.py	(revision 11724)
+++ django/db/models/query_utils.py	(working copy)
@@ -10,120 +10,12 @@
 from copy import deepcopy
 
 from django.utils import tree
-from django.utils.datastructures import SortedDict
 
 try:
     sorted
 except NameError:
     from django.utils.itercompat import sorted  # For Python 2.3.
 
-
-class CyclicDependency(Exception):
-    """
-    An error when dealing with a collection of objects that have a cyclic
-    dependency, i.e. when deleting multiple objects.
-    """
-    pass
-
-class CollectedObjects(object):
-    """
-    A container that stores keys and lists of values along with remembering the
-    parent objects for all the keys.
-
-    This is used for the database object deletion routines so that we can
-    calculate the 'leaf' objects which should be deleted first.
-
-    previously_seen is an optional argument. It must be a CollectedObjects
-    instance itself; any previously_seen collected object will be blocked from
-    being added to this instance.
-    """
-
-    def __init__(self, previously_seen=None):
-        self.data = {}
-        self.children = {}
-        if previously_seen:
-            self.blocked = previously_seen.blocked
-            for cls, seen in previously_seen.data.items():
-                self.blocked.setdefault(cls, SortedDict()).update(seen)
-        else:
-            self.blocked = {}
-
-    def add(self, model, pk, obj, parent_model, nullable=False):
-        """
-        Adds an item to the container.
-
-        Arguments:
-        * model - the class of the object being added.
-        * pk - the primary key.
-        * obj - the object itself.
-        * parent_model - the model of the parent object that this object was
-          reached through.
-        * nullable - should be True if this relation is nullable.
-
-        Returns True if the item already existed in the structure and
-        False otherwise.
-        """
-        if pk in self.blocked.get(model, {}):
-            return True
-
-        d = self.data.setdefault(model, SortedDict())
-        retval = pk in d
-        d[pk] = obj
-        # 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.
-        if parent_model is not None and not nullable:
-            self.children.setdefault(parent_model, []).append(model)
-        return retval
-
-    def __contains__(self, key):
-        return self.data.__contains__(key)
-
-    def __getitem__(self, key):
-        return self.data[key]
-
-    def __nonzero__(self):
-        return bool(self.data)
-
-    def iteritems(self):
-        for k in self.ordered_keys():
-            yield k, self[k]
-
-    def items(self):
-        return list(self.iteritems())
-
-    def keys(self):
-        return self.ordered_keys()
-
-    def ordered_keys(self):
-        """
-        Returns the models in the order that they should be dealt with (i.e.
-        models with no dependencies first).
-        """
-        dealt_with = SortedDict()
-        # Start with items that have no children
-        models = self.data.keys()
-        while len(dealt_with) < len(models):
-            found = False
-            for model in models:
-                if model in dealt_with:
-                    continue
-                children = self.children.setdefault(model, [])
-                if len([c for c in children if c not in dealt_with]) == 0:
-                    dealt_with[model] = None
-                    found = True
-            if not found:
-                raise CyclicDependency(
-                    "There is a cyclic dependency of items to be processed.")
-
-        return dealt_with.keys()
-
-    def unordered_keys(self):
-        """
-        Fallback for the case where is a cyclic dependency but we don't  care.
-        """
-        return self.data.keys()
-
 class QueryWrapper(object):
     """
     A type that indicates the contents are an SQL fragment and the associate
