Index: tests/modeltests/invalid_models/models.py
===================================================================
--- tests/modeltests/invalid_models/models.py	(revision 12288)
+++ tests/modeltests/invalid_models/models.py	(working copy)
@@ -181,6 +181,12 @@
 class UniqueM2M(models.Model):
     """ Model to test for unique ManyToManyFields, which are invalid. """
     unique_people = models.ManyToManyField( Person, unique=True )
+    
+class InvalidSetNull(models.Model):
+    fk = models.ForeignKey('self', on_delete=models.SET_NULL)
+    
+class InvalidSetDefault(models.Model):
+    fk = models.ForeignKey('self', on_delete=models.SET_DEFAULT)
 
 
 model_errors = """invalid_models.fielderrors: "charfield": CharFields require a "max_length" attribute.
@@ -279,4 +285,6 @@
 invalid_models.abstractrelationmodel: 'fk1' has a relation with model AbstractModel, which has either not been installed or is abstract.
 invalid_models.abstractrelationmodel: 'fk2' has an m2m relation with model AbstractModel, which has either not been installed or is abstract.
 invalid_models.uniquem2m: ManyToManyFields cannot be unique.  Remove the unique argument on 'unique_people'.
+invalid_models.invalidsetnull: 'fk' specifies on_delete=SET_NULL, but cannot be null.
+invalid_models.invalidsetdefault: 'fk' specifies on_delete=SET_DEFAULT, but has no default value.
 """
Index: tests/modeltests/delete/models.py
===================================================================
--- tests/modeltests/delete/models.py	(revision 12288)
+++ 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,29 +93,10 @@
 >>> 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)
@@ -163,17 +107,14 @@
 # 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, using):
-...         print "CLEARING FIELD",related_field.name
-...         return super(LoggingUpdateQuery, self).clear_related(related_field, pk_list, using)
+...     def update_batch(self, pk_list, values, using):
+...         if values == {'f': None}:
+...             print "CLEARING FIELD f"
+...         return super(LoggingUpdateQuery, self).update_batch(pk_list, values, using)
+
 >>> original_class = django.db.models.sql.UpdateQuery
 >>> django.db.models.sql.UpdateQuery = LoggingUpdateQuery
 >>> e1.delete()
@@ -187,12 +128,6 @@
 >>> 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
 
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,215 @@
+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 = models.ManyToManyField(R, related_name="m_set")    
+    m2m_through = models.ManyToManyField(R, through="MR", related_name="m_through_set")
+    m2m_through_null = models.ManyToManyField(R, through="MRNull", related_name="m_through_null_set")
+    
+class MR(models.Model):
+    m = models.ForeignKey(M)
+    r = models.ForeignKey(R)
+
+class MRNull(models.Model):
+    m = models.ForeignKey(M)
+    r = models.ForeignKey(R, null=True, on_delete=models.SET_NULL)
+
+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.failIf(A.objects.filter(name='auto_nullable').exists())
+        
+        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)        
+        
+        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())
+        
+        m = M.objects.create()
+        r = R.objects.create()
+        MRNull.objects.create(m=m, r=r)        
+        r.delete()
+        self.failIf(not MRNull.objects.exists())
+        self.failIf(m.m2m_through_null.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_instance_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('update_setnull')
+        a.setnull.delete()
+        
+        a = create_a('update_cascade')
+        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: django/db/models/sql/subqueries.py
===================================================================
--- django/db/models/sql/subqueries.py	(revision 12288)
+++ django/db/models/sql/subqueries.py	(working copy)
@@ -26,55 +26,39 @@
         self.where = where
         self.get_compiler(using).execute_sql(None)
 
-    def delete_batch_related(self, pk_list, using):
+    def delete_generic_relation_hack(self, pk_list, using):
         """
-        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, using=using)
-
         for f in cls._meta.many_to_many:
+            if not isinstance(f, generic.GenericRelation):
+                continue
             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)
+            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, using=using)
 
-    def delete_batch(self, pk_list, using):
+    def delete_batch(self, pk_list, using, 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, using=using)
@@ -106,20 +90,14 @@
                 related_updates=self.related_updates.copy(), **kwargs)
 
 
-    def clear_related(self, related_field, pk_list, using):
-        """
-        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, using):
+        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',
+            self.where.add((Constraint(None, pk_field.column, pk_field), 'in',
                     pk_list[offset : offset + GET_ITERATOR_CHUNK_SIZE]),
                     AND)
-            self.values = [(related_field, None, None)]
             self.get_compiler(using).execute_sql(None)
 
     def add_update_values(self, values):
Index: django/db/models/base.py
===================================================================
--- django/db/models/base.py	(revision 12288)
+++ django/db/models/base.py	(working copy)
@@ -7,8 +7,9 @@
 from django.core import validators
 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 Collector
 from django.db.models.options import Options
 from django.db import connections, router, transaction, DatabaseError, DEFAULT_DB_ALIAS
 from django.db.models import signals
@@ -535,74 +536,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, using=None):
         using = using or router.db_for_write(self.__class__, instance=self)
         connection = connections[using]
         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)
+        collector = Collector()
+        collector.collect([self])
+        collector.delete(using=using)
 
-        # Actually delete the objects.
-        delete_objects(seen_objs, using)
-
     delete.alters_data = True
 
     def _get_FIELD_display(self, field):
Index: django/db/models/options.py
===================================================================
--- django/db/models/options.py	(revision 12288)
+++ 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 12288)
+++ django/db/models/fields/related.py	(working copy)
@@ -7,6 +7,7 @@
 from django.db.models.related import RelatedObject
 from django.db.models.query import QuerySet
 from django.db.models.query_utils import QueryWrapper
+from django.db.models.deletion import CASCADE
 from django.utils.encoding import smart_unicode
 from django.utils.translation import ugettext_lazy as _, string_concat, ungettext, ugettext
 from django.utils.functional import curry
@@ -687,8 +688,7 @@
         manager.add(*value)
 
 class ManyToOneRel(object):
-    def __init__(self, to, field_name, related_name=None,
-            limit_choices_to=None, lookup_overrides=None, parent_link=False):
+    def __init__(self, to, field_name, related_name=None, limit_choices_to=None, parent_link=False, on_delete=None):
         try:
             to._meta
         except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT
@@ -698,9 +698,9 @@
         if limit_choices_to is None:
             limit_choices_to = {}
         self.limit_choices_to = limit_choices_to
-        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?"
@@ -718,11 +718,11 @@
         return data[0]
 
 class OneToOneRel(ManyToOneRel):
-    def __init__(self, to, field_name, related_name=None,
-            limit_choices_to=None, lookup_overrides=None, parent_link=False):
+    def __init__(self, to, field_name, related_name=None, limit_choices_to=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)
+                parent_link=parent_link, on_delete=on_delete
+        )
         self.multiple = False
 
 class ManyToManyRel(object):
@@ -771,8 +771,9 @@
         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=kwargs.pop('on_delete', CASCADE),
+        )
         Field.__init__(self, **kwargs)
 
         self.db_index = True
Index: django/db/models/__init__.py
===================================================================
--- django/db/models/__init__.py	(revision 12288)
+++ 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.deletion 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,213 @@
+from django.utils.datastructures import SortedDict
+from django.utils.functional import wraps
+from django.db import connections, transaction, IntegrityError, DEFAULT_DB_ALIAS
+from django.db.models import signals, sql
+from django.db.models.sql.constants import GET_ITERATOR_CHUNK_SIZE
+
+def CASCADE(collector, field, sub_objs):
+    collector.collect(sub_objs, field.rel.to, field.null)
+    if field.null:
+        # FIXME: there should be a connection feature indicating whether nullable related fields should be nulled out before deletion
+        collector.add_field_update(field, None, sub_objs)
+
+def PROTECT(collector, field, sub_objs):
+    msg = "Cannot delete some instances of model '%s' because they are referenced through a protected foreign key: '%s.%s'" % (
+        field.rel.to.__name__, sub_objs[0].__class__.__name__, field.name
+    )
+    raise IntegrityError(msg)
+
+def SET(value):
+    def set_on_delete(collector, field, sub_objs):
+        collector.add_field_update(field, value, sub_objs)
+    return set_on_delete
+
+def SET_NULL(collector, field, sub_objs):
+    collector.add_field_update(field, None, sub_objs)
+
+def SET_DEFAULT(collector, field, sub_objs):
+    collector.add_field_update(field, field.get_default(), sub_objs)
+
+def DO_NOTHING(collector, field, sub_objs):
+    pass
+
+def force_managed(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 Collector(object):
+    def __init__(self, previously_collected=None):
+        self.data = {} # {model: [instances]}
+        self.batches = {} # {model: {field: set([instances])}}
+        self.field_updates = {} # {model: {(field, value): set([instances])}}        
+        self.dependencies = {} # {model: set([models])}
+        if previously_collected:
+            self.blocked = set(previously_collected)
+            self.blocked.update(previously_collected.blocked)
+        else:
+            self.blocked = set()
+
+    def add(self, objs, source=None, nullable=False):
+        """
+        Adds '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.
+        
+        Returns a list of all objects that were not already collected.
+        """
+        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 source is not None and not nullable:
+            self.dependencies.setdefault(source, set()).add(model)
+        return new_objs
+        
+    def add_batch(self, model, field, objs):
+        """
+        Schedules a batch delete. Every instance of 'model' that related to an instance of 'obj' through 'field' will be deleted.
+        """
+        self.batches.setdefault(model, {}).setdefault(field, set()).update(objs)
+        
+    def add_field_update(self, field, value, objs):
+        """
+        Schedules a field update. 'objs' must be a homogenous iterable collection of model instances (e.g. a QuerySet).
+        """
+        objs = list(objs)
+        if not objs:
+            return
+        model = objs[0].__class__
+        self.field_updates.setdefault(model, {}).setdefault((field, value), set()).update(objs)
+        
+    def collect(self, objs, source=None, nullable=False, collect_related=True):
+        """
+        Adds 'objs' to the collection of objects to be deleted as well as all parent instances.
+        'objs' must be a homogenous 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.
+        """
+        new_objs = self.add(objs, source, nullable)
+        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.model, related.field, new_objs)
+                else:
+                    sub_objs = related.model._base_manager.filter(**{"%s__in" % related.field.name: new_objs})
+                    if not sub_objs:
+                        continue
+                    related.field.rel.on_delete(self, related.field, 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
+                dependencies = self.dependencies.get(model)
+                if not dependencies or not dependencies.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])
+    
+    @force_managed
+    def delete(self, using=None):
+        using = using or DEFAULT_DB_ALIAS
+        # sort instance collections 
+        for instances in self.data.itervalues():
+            instances.sort(key=lambda obj: obj.pk)
+
+        # if possible, bring the 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)
+            for (field, value), instances in instances_for_fieldvalues.iteritems():
+                query.update_batch([obj.pk for obj in instances], {field.name: value}, using)
+
+        # reverse instance collections
+        for instances in self.data.itervalues():
+            instances.reverse()
+
+        # delete batches
+        for model, batches in self.batches.iteritems():
+            query = sql.DeleteQuery(model)
+            for field, instances in batches.iteritems():
+                query.delete_batch([obj.pk for obj in instances], using, field)
+
+        # delete instances
+        for model, instances in self.data.iteritems():
+            query = sql.DeleteQuery(model)
+            pk_list = [obj.pk for obj in instances]
+            query.delete_generic_relation_hack(pk_list, using)
+            query.delete_batch(pk_list, using)
+        
+        # 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 12288)
+++ django/db/models/query.py	(working copy)
@@ -7,7 +7,8 @@
 from django.db import connections, router, 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, InvalidQuery
+from django.db.models.query_utils import Q, select_related_descend, deferred_class_factory, InvalidQuery
+from django.db.models.deletion import Collector
 from django.db.models import signals, sql
 from django.utils.copycompat import deepcopy
 
@@ -427,17 +428,15 @@
 
         # Delete objects in chunks to prevent the list of related objects from
         # becoming too long.
-        seen_objs = None
+        collector = None
         while 1:
             # 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)
-
-            if not seen_objs:
+            collector = Collector(collector)
+            collector.collect(del_query[:CHUNK_SIZE])
+            if not collector:
                 break
-            delete_objects(seen_objs, del_query.db)
+            collector.delete(using=del_query.db)
 
         # Clear the result cache, in case this QuerySet gets reused.
         self._result_cache = None
@@ -1166,81 +1165,7 @@
                 setattr(obj, f.get_cache_name(), rel_obj)
     return obj, index_end
 
-def delete_objects(seen_objs, using):
-    """
-    Iterate through a list of seen classes, and remove any instances that are
-    referred to.
-    """
-    connection = connections[using]
-    if not transaction.is_managed(using=using):
-        transaction.enter_transaction_management(using=using)
-        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)
-            del_query.delete_batch_related(pk_list, using=using)
-
-            update_query = sql.UpdateQuery(cls)
-            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).clear_related(field, pk_list, using=using)
-                    else:
-                        update_query.clear_related(field, pk_list, using=using)
-
-        # 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)
-            del_query.delete_batch(pk_list, using=using)
-
-            # 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(using=using)
-        else:
-            transaction.commit_unless_managed(using=using)
-    finally:
-        if forced_managed:
-            transaction.leave_transaction_management(using=using)
-
 class RawQuerySet(object):
     """
     Provides an iterator which converts the results of raw SQL queries into
Index: django/db/models/query_utils.py
===================================================================
--- django/db/models/query_utils.py	(revision 12288)
+++ django/db/models/query_utils.py	(working copy)
@@ -10,122 +10,14 @@
 from django.utils.copycompat import deepcopy
 
 from django.utils import tree
-from django.utils.datastructures import SortedDict
 
 
-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 InvalidQuery(Exception):
     """
     The query passed to raw isn't a safe query to use with raw.
     """
     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
Index: django/core/management/validation.py
===================================================================
--- django/core/management/validation.py	(revision 12288)
+++ django/core/management/validation.py	(working copy)
@@ -22,6 +22,7 @@
     from django.db import models, connection
     from django.db.models.loading import get_app_errors
     from django.db.models.fields.related import RelatedObject
+    from django.db.models.deletion import SET_NULL, SET_DEFAULT
 
     e = ModelErrorCollection(outfile)
 
@@ -66,6 +67,13 @@
             # Perform any backend-specific field validation.
             connection.validation.validate_field(e, opts, f)
 
+            # Check if the on_delete behavior is sane
+            if f.rel and hasattr(f.rel, 'on_delete'):
+                if f.rel.on_delete == SET_NULL and not f.null:
+                    e.add(opts, "'%s' specifies on_delete=SET_NULL, but cannot be null." % f.name)
+                elif f.rel.on_delete == SET_DEFAULT and not f.has_default():
+                    e.add(opts, "'%s' specifies on_delete=SET_DEFAULT, but has no default value." % f.name)
+
             # Check to see if the related field will clash with any existing
             # fields, m2m fields, m2m related objects or related objects
             if f.rel:
