Index: db/models/base.py
===================================================================
--- db/models/base.py	(revision 10558)
+++ db/models/base.py	(working copy)
@@ -13,10 +13,11 @@
 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_utils import CollectedObjects, CollectedFields, DeferredAttribute
 from django.db.models.options import Options
-from django.db import connection, transaction, DatabaseError
+from django.db import connection, transaction, DatabaseError, IntegrityError
 from django.db.models import signals
+from django.db.models.fields.related import CASCADE, RESTRICT, SET_NULL
 from django.db.models.loading import register_models, get_model
 from django.utils.functional import curry
 from django.utils.encoding import smart_str, force_unicode, smart_unicode
@@ -495,7 +496,7 @@
 
     save_base.alters_data = True
 
-    def _collect_sub_objects(self, seen_objs, parent=None, nullable=False):
+    def _collect_sub_objects(self, seen_objs, fields_to_null, parent=None, nullable=False):
         """
         Recursively populates seen_objs with all objects related to this
         object.
@@ -508,53 +509,85 @@
         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
+        if not getattr(settings, 'ON_DELETE_HANDLED_BY_DB', False):
+            ON_DELETE_NONE_HANDLING = getattr(settings, 'ON_DELETE_NONE_HANDLING', CASCADE)
+
+            def _handle_sub_obj(related, sub_obj):
+                on_delete = related.field.rel.on_delete
+                if on_delete is None:
+                    on_delete = ON_DELETE_NONE_HANDLING
+
+                if on_delete == CASCADE:
+                    sub_obj._collect_sub_objects(seen_objs, fields_to_null, self.__class__)
+                elif on_delete == RESTRICT:
+                    msg = '[Django] Cannot delete a parent object: a foreign key constraint fails (ForeignKey `%s` (pk=`%s`) references `%s` (pk=`%s`))' % (
+                        sub_obj.__class__,
+                        sub_obj._get_pk_val(),
+                        self.__class__,
+                        pk_val,
+                        )
+                    raise IntegrityError(msg) #TODO: include error number also? E.g., "raise IntegrityError(1451, msg)" (but 1451 is MySQL-specific, I believe)
+                elif on_delete == SET_NULL:
+                    if not related.field.null:
+                        msg = '[Django] Cannot delete a parent object: foreign key constraint on_delete=SET_NULL is specified for a non-nullable foreign key (ForeignKey `%s` (pk=`%s`) references `%s` (pk=`%s`))' % (
+                            sub_obj.__class__,
+                            sub_obj._get_pk_val(),
+                            self.__class__,
+                            pk_val,
+                            )
+                        raise IntegrityError(msg) #TODO: include error number also? E.g., "raise IntegrityError(<errno>, msg)" (but the error numbers are db-specific, I believe)
+                    fields_to_null.add(sub_obj.__class__, sub_obj._get_pk_val(), sub_obj, related.field.name)
                 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
+                    raise AttributeError('Unexpected value for on_delete')
+
+            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:
+                        _handle_sub_obj(related, sub_obj)
                 else:
-                    raise AssertionError("Should never get here.")
-                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)
+                    # 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:
+                        raise AssertionError("Should never get here.")
+                    delete_qs = rel_descriptor.delete_manager(self).all()
+                    for sub_obj in delete_qs:
+                        _handle_sub_obj(related, sub_obj)
 
-        # 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 = self._meta.parents.values()
-        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)
+            # 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 = self._meta.parents.values()
+            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, fields_to_null)
 
     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)
+        fields_to_null = CollectedFields()
+        self._collect_sub_objects(seen_objs, fields_to_null)
 
         # Actually delete the objects.
-        delete_objects(seen_objs)
+        delete_objects(seen_objs, fields_to_null)
 
     delete.alters_data = True
 
Index: db/models/fields/related.py
===================================================================
--- db/models/fields/related.py	(revision 10558)
+++ db/models/fields/related.py	(working copy)
@@ -10,6 +10,7 @@
 from django.utils.functional import curry
 from django.core import exceptions
 from django import forms
+from django.conf import settings
 
 try:
     set
@@ -20,6 +21,17 @@
 
 pending_lookups = {}
 
+class _OnDeleteOrUpdateAction(object):
+    pass
+class RESTRICT(_OnDeleteOrUpdateAction):
+    sql = 'RESTRICT'
+class CASCADE(_OnDeleteOrUpdateAction):
+    sql = 'CASCADE'
+class SET_NULL(_OnDeleteOrUpdateAction):
+    sql = 'SET NULL'
+ALLOWED_ON_DELETE_ACTION_TYPES = set([None, CASCADE, RESTRICT, SET_NULL])
+ALLOWED_ON_UPDATE_ACTION_TYPES = ALLOWED_ON_DELETE_ACTION_TYPES.copy()
+
 def add_lazy_relation(cls, field, relation, operation):
     """
     Adds a lookup on ``cls`` when a related field is defined using a string,
@@ -604,7 +616,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, on_update=None):
         try:
             to._meta
         except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT
@@ -617,6 +630,8 @@
         self.lookup_overrides = lookup_overrides or {}
         self.multiple = True
         self.parent_link = parent_link
+        self.on_delete = on_delete
+        self.on_update = on_update
 
     def get_related_field(self):
         """
@@ -631,10 +646,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, on_update=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, on_update=on_update)
         self.multiple = False
 
 class ManyToManyRel(object):
@@ -673,7 +690,9 @@
             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', None),
+            on_update=kwargs.pop('on_update', None))
         Field.__init__(self, **kwargs)
 
         self.db_index = True
@@ -718,6 +737,22 @@
             target = self.rel.to._meta.db_table
         cls._meta.duplicate_targets[self.column] = (target, "o2m")
 
+        on_delete, on_update = self.rel.on_delete, self.rel.on_update
+        if on_delete not in ALLOWED_ON_DELETE_ACTION_TYPES:
+            raise ValueError("Invalid value 'on_delete=%s' specified for ForeignKey field %s.%s." % (on_delete, cls.__name__, name))
+        if on_update not in ALLOWED_ON_UPDATE_ACTION_TYPES:
+            raise ValueError("Invalid value 'on_update=%s' specified for ForeignKey field %s.%s." % (on_update, cls.__name__, name))
+        if (on_delete == SET_NULL or on_update == SET_NULL) and not self.null:
+            if on_delete == SET_NULL and on_update == SET_NULL:
+                specification = "'on_delete=SET_NULL' and 'on_update=SET_NULL'"
+            elif on_delete == SET_NULL:
+                specification = "'on_delete=SET_NULL'"
+            else:
+                specification = "'on_update=SET_NULL'"
+            raise ValueError("%s specified for ForeignKey field '%s.%s', but the field is not nullable." % (specification, cls.__name__, name))
+        if on_delete is None and getattr(settings, 'ON_DELETE_NONE_HANDLING', CASCADE) == SET_NULL and not self.null:
+            raise ValueError("on_delete=SET_NULL is being used by default (based on the ON_DELETE_NONE_HANDLING setting) for ForeignKey field '%s.%s', but the field is not nullable." % (specification, cls.__name__, name))
+
     def contribute_to_related_class(self, cls, related):
         setattr(cls, related.get_accessor_name(), ForeignRelatedObjectsDescriptor(related))
 
Index: db/models/__init__.py
===================================================================
--- db/models/__init__.py	(revision 10558)
+++ 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, RESTRICT, SET_NULL
 from django.db.models import signals
 
 # Admin stages.
Index: db/models/query.py
===================================================================
--- db/models/query.py	(revision 10558)
+++ db/models/query.py	(working copy)
@@ -359,12 +359,13 @@
             # 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()
+            fields_to_null = CollectedFields()
             for object in del_query[:CHUNK_SIZE]:
-                object._collect_sub_objects(seen_objs)
+                object._collect_sub_objects(seen_objs, fields_to_null)
 
             if not seen_objs:
                 break
-            delete_objects(seen_objs)
+            delete_objects(seen_objs, fields_to_null)
 
         # Clear the result cache, in case this QuerySet gets reused.
         self._result_cache = None
@@ -952,7 +953,7 @@
                 setattr(obj, f.get_cache_name(), rel_obj)
     return obj, index_end
 
-def delete_objects(seen_objs):
+def delete_objects(seen_objs, fields_to_null):
     """
     Iterate through a list of seen classes, and remove any instances that are
     referred to.
@@ -998,6 +999,15 @@
                         update_query.clear_related(field, pk_list)
 
         # Now delete the actual data.
+        for cls, cls_dct in fields_to_null.iteritems():
+            update_query = sql.UpdateQuery(cls, connection)
+            field_dict = {}
+            for pk, (_, field_names) in cls_dct.iteritems():
+                for field_name in field_names:
+                    pk_set = field_dict.setdefault(field_name, set())
+                    pk_set.add(pk)
+            for field_name, pk_set in field_dict.iteritems():
+                update_query.clear_related(cls._meta.get_field_by_name(field_name)[0], list(pk_set))
         for cls in ordered_classes:
             items = obj_pairs[cls]
             items.reverse()
@@ -1009,6 +1019,11 @@
             # 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 cls, cls_dct in fields_to_null.iteritems():
+                for instance, field_names in cls_dct.itervalues():
+                    for field_name in field_names:
+                        field = cls._meta.get_field_by_name(field_name)[0]
+                        setattr(instance, field.attname, None)
             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:
Index: db/models/query_utils.py
===================================================================
--- db/models/query_utils.py	(revision 10558)
+++ db/models/query_utils.py	(working copy)
@@ -111,6 +111,43 @@
         """
         return self.data.keys()
 
+class CollectedFields(object):
+    """
+    A container that stores model objects and fields that need to
+    be nulled to enforce the on_delete=SET_NULL ForeignKey constraint.
+    """
+    def __init__(self):
+        self.data = {}
+
+    def add(self, model, pk, obj, field_name):
+        """
+        Adds an item.
+        model is the class of the object being added,
+        pk is the primary key, obj is the object itself, 
+        field_name is the name of the field to be nulled.
+        """
+        d = self.data.setdefault(model, SortedDict())
+        obj, field_names = d.setdefault(pk, (obj, set()))
+        field_names.add(field_name)
+
+    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):
+        return self.data.iteritems()
+
+    def iterkeys(self):
+        return self.data.iterkeys()
+
+    def itervalues(self):
+        return self.data.itervalues()
+
 class QueryWrapper(object):
     """
     A type that indicates the contents are an SQL fragment and the associate
Index: db/backends/creation.py
===================================================================
--- db/backends/creation.py	(revision 10558)
+++ db/backends/creation.py	(working copy)
@@ -96,9 +96,11 @@
         "Return the SQL snippet defining the foreign key reference for a field"
         qn = self.connection.ops.quote_name
         if field.rel.to in known_models:
+            on_delete_clause, on_update_clause = self.on_delete_and_update_clauses(style, field)
             output = [style.SQL_KEYWORD('REFERENCES') + ' ' + \
                 style.SQL_TABLE(qn(field.rel.to._meta.db_table)) + ' (' + \
                 style.SQL_FIELD(qn(field.rel.to._meta.get_field(field.rel.field_name).column)) + ')' +
+                on_delete_clause + on_update_clause +
                 self.connection.ops.deferrable_sql()
             ]
             pending = False
@@ -121,6 +123,7 @@
         opts = model._meta
         if model in pending_references:
             for rel_class, f in pending_references[model]:
+                on_delete_clause, on_update_clause = self.on_delete_and_update_clauses(style, f)
                 rel_opts = rel_class._meta
                 r_table = rel_opts.db_table
                 r_col = f.column
@@ -129,13 +132,37 @@
                 # For MySQL, r_name must be unique in the first 64 characters.
                 # So we are careful with character usage here.
                 r_name = '%s_refs_%s_%x' % (r_col, col, abs(hash((r_table, table))))
-                final_output.append(style.SQL_KEYWORD('ALTER TABLE') + ' %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s (%s)%s;' % \
+                final_output.append(style.SQL_KEYWORD('ALTER TABLE') + ' %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s (%s)%s%s%s;' % \
                     (qn(r_table), qn(truncate_name(r_name, self.connection.ops.max_name_length())),
                     qn(r_col), qn(table), qn(col),
+                    on_delete_clause, on_update_clause,
                     self.connection.ops.deferrable_sql()))
             del pending_references[model]
         return final_output
 
+    def on_delete_and_update_clauses(self, style, foreign_key_field):
+        on_delete_clause = ''
+        try:
+            on_delete = getattr(foreign_key_field, 'ON_DELETE_HANDLED_BY_DB') #USED FOR UNIT TESTING ONLY (see comment in modeltests/on_delete_and_update_db/models.py)
+        except AttributeError:
+            on_delete = getattr(settings, 'ON_DELETE_HANDLED_BY_DB', False)
+        if on_delete:
+            on_delete = getattr(foreign_key_field.rel, 'on_delete', None)
+            if on_delete:
+                on_delete_clause = style.SQL_KEYWORD(' ON DELETE %s' % foreign_key_field.rel.on_delete.sql)
+            
+        on_update_clause = ''
+        try:
+            on_update = getattr(foreign_key_field, 'ON_UPDATE_HANDLED_BY_DB') #USED FOR UNIT TESTING ONLY; (see comment in modeltests/on_delete_and_update_db/models.py)
+        except AttributeError:
+            on_update = getattr(settings, 'ON_UPDATE_HANDLED_BY_DB', False)
+        if on_update:
+            on_update = getattr(foreign_key_field.rel, 'on_update', None)
+            if on_update:
+                on_update_clause = style.SQL_KEYWORD(' ON UPDATE %s' % foreign_key_field.rel.on_update.sql)
+
+        return on_delete_clause, on_update_clause
+        
     def sql_for_many_to_many(self, model, style):
         "Return the CREATE TABLE statments for all the many-to-many tables defined on a model"
         output = []
Index: contrib/admin/options.py
===================================================================
--- contrib/admin/options.py	(revision 10558)
+++ contrib/admin/options.py	(working copy)
@@ -1006,6 +1006,7 @@
         perms_needed = set()
         get_deleted_objects(deleted_objects, perms_needed, request.user, obj, opts, 1, self.admin_site)
 
+        import logging; logging.debug('deleting %s' % deleted_objects)
         if request.POST: # The user has already confirmed the deletion.
             if perms_needed:
                 raise PermissionDenied
