Index: django/db/models/base.py
===================================================================
--- django/db/models/base.py    (revision 9845)
+++ django/db/models/base.py    (working copy)
@@ -12,10 +12,11 @@
 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, CollectedObjects
+from django.db.models.query import delete_objects, Q, CollectedFields, CollectedObjects
 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
@@ -412,7 +413,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.
@@ -424,44 +425,76 @@
         pk_val = self._get_pk_val()
         if seen_objs.add(self.__class__, pk_val, self, parent, nullable):
             return
+            
+        if not getattr(settings, 'ON_DELETE_HANDLED_BY_DB', False):
+            ON_DELETE_NONE_HANDLING = getattr(settings, 'ON_DELETE_NONE_HANDLING', CASCADE)
 
-        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
+            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:
-                for sub_obj in getattr(self, rel_opts_name).all():
-                    sub_obj._collect_sub_objects(seen_objs, self.__class__, related.field.null)
+                    raise AttributeError('Unexpected value for on_delete')
 
-        # 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)
+            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:
+                    for sub_obj in getattr(self, rel_opts_name).all():
+                        _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, 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: django/db/models/fields/related.py
===================================================================
--- django/db/models/fields/related.py  (revision 9845)
+++ django/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,
@@ -584,7 +596,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
@@ -597,6 +610,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):
         """
@@ -611,10 +626,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):
@@ -645,7 +662,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
@@ -690,6 +709,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: django/db/models/__init__.py
===================================================================
--- django/db/models/__init__.py        (revision 9845)
+++ 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, RESTRICT, SET_NULL
 from django.db.models import signals
 
 # Admin stages.
Index: django/db/models/query.py
===================================================================
--- django/db/models/query.py   (revision 9845)
+++ django/db/models/query.py   (working copy)
@@ -117,7 +117,44 @@
         """
         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 QuerySet(object):
     """
     Represents a lazy database lookup for a set of objects.
@@ -427,12 +464,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
@@ -951,8 +989,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.
@@ -992,6 +1029,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()
@@ -1003,6 +1049,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: django/db/backends/creation.py
===================================================================
--- django/db/backends/creation.py      (revision 9845)
+++ django/db/backends/creation.py      (working copy)
@@ -94,9 +94,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
@@ -117,6 +119,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
@@ -125,13 +128,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: tests/modeltests/on_delete_and_on_update_db/__init__.py
===================================================================

Property changes on: tests/modeltests/on_delete_and_on_update_db/__init__.py
___________________________________________________________________
Name: svn:keywords
   + Id

Index: tests/modeltests/on_delete_and_on_update_db/tests.py
===================================================================
--- tests/modeltests/on_delete_and_on_update_db/tests.py        (revision 0)
+++ tests/modeltests/on_delete_and_on_update_db/tests.py        (revision 0)
@@ -0,0 +1,115 @@
+"""
+Test ON DELETE and ON UPDATE behavior for foreign keys
+when it's handled by the database
+(ie. when settings.ON_DELETE_HANDLED_BY_DB=True
+and settings.ON_UPDATE_HANDLED_BY_DB=True).
+"""
+
+from django.conf import settings
+from django.db import IntegrityError
+from django.test import TestCase
+
+from models import *
+
+
+class ON_DELETE_Tests(TestCase):
+    def setUp(self):
+        self._old = getattr(settings, 'ON_DELETE_HANDLED_BY_DB', False)
+        settings.ON_DELETE_HANDLED_BY_DB = True
+
+    def tearDown(self):
+        settings.ON_DELETE_HANDLED_BY_DB = self._old
+
+    def test_on_delete_SET_NULL_requires_nullable_field(self):
+        try:
+            class Test(models.Model):
+                fk = models.ForeignKey(Data, to_field='data', on_delete=models.SET_NULL)
+        except ValueError, e:
+            self.assertEqual(str(e), "'on_delete=SET_NULL' specified for ForeignKey field 'Test.fk', but the field is not nullable.")
+        else:
+            self.assertTrue(False, 'Expected exception not raised.')
+        
+    def test_on_delete_None(self):
+        #SQL behavior is to default to RESTRICT if no ON DELETE clause is specified
+        FKModel = FK_on_delete_None
+        data = Data.objects.create(data=1)
+        FKModel.objects.create(fk=data)
+        self.assertRaises(IntegrityError, data.delete)
+
+    def test_on_delete_CASCADE(self):
+        FKModel = FK_on_delete_CASCADE
+        data = Data.objects.create(data=1)
+        fk_id = FKModel.objects.create(fk=data).id
+        self.assertEqual(data, FKModel.objects.get(id=fk_id).fk)
+        data.delete()
+        self.assertRaises(FKModel.DoesNotExist, FKModel.objects.get, id=fk_id)
+
+    def test_on_delete_RESTRICT(self):
+        FKModel = FK_on_delete_RESTRICT
+        data = Data.objects.create(data=1)
+        FKModel.objects.create(fk=data)
+        self.assertRaises(IntegrityError, data.delete)
+
+    def test_on_delete_SET_NULL(self):
+        FKModel = FK_on_delete_SET_NULL
+        data = Data.objects.create(data=1)
+        fk_id = FKModel.objects.create(fk=data).id
+        self.assertEqual(data, FKModel.objects.get(id=fk_id).fk)
+        self.assertEqual(1, FKModel.objects.get(id=fk_id).fk_id)
+        data.delete()
+        self.assertEqual(None, FKModel.objects.get(id=fk_id).fk)
+        self.assertEqual(None, FKModel.objects.get(id=fk_id).fk_id)
+
+
+class ON_UPDATE_Tests(TestCase):
+    def setUp(self):
+        self._old = getattr(settings, 'ON_UPDATE_HANDLED_BY_DB', False)
+        settings.ON_UPDATE_HANDLED_BY_DB = True
+
+    def tearDown(self):
+        settings.ON_UPDATE_HANDLED_BY_DB = self._old
+
+    def test_on_update_SET_NULL_requires_nullable_field(self):
+        try:
+            class Test(models.Model):
+                fk = models.ForeignKey(Data, to_field='data', on_update=models.SET_NULL)
+        except ValueError, e:
+            self.assertEqual(str(e), "'on_update=SET_NULL' specified for ForeignKey field 'Test.fk', but the field is not nullable.")
+        else:
+            self.assertTrue(False, 'Expected exception not raised.')
+        
+    def test_on_update_None(self):
+        #SQL behavior is to default to RESTRICT if no ON DELETE clause is specified
+        FKModel = FK_on_update_None
+        data = Data.objects.create(data=1)
+        FKModel.objects.create(fk=data)
+        data.data += 1
+        self.assertRaises(IntegrityError, data.save)
+
+    def test_on_update_CASCADE(self):
+        FKModel = FK_on_update_CASCADE
+        data = Data.objects.create(data=1)
+        fk_id = FKModel.objects.create(fk=data).id
+        self.assertEqual(1, FKModel.objects.get(id=fk_id).fk_id)
+        data.data += 1
+        data.save()
+        self.assertEqual(2, FKModel.objects.get(id=fk_id).fk_id)
+
+    def test_on_update_RESTRICT(self):
+        FKModel = FK_on_update_RESTRICT
+        data = Data.objects.create(data=1)
+        FKModel.objects.create(fk=data)
+        data.data += 1
+        self.assertRaises(IntegrityError, data.save)
+
+    def test_on_update_SET_NULL(self):
+        FKModel = FK_on_update_SET_NULL
+        data = Data.objects.create(data=1)
+        fk_id = FKModel.objects.create(fk=data).id
+        self.assertEqual(data, FKModel.objects.get(id=fk_id).fk)
+        self.assertEqual(1, FKModel.objects.get(id=fk_id).fk_id)
+        data.data += 1
+        data.save()
+        self.assertEqual(None, FKModel.objects.get(id=fk_id).fk)
+        self.assertEqual(None, FKModel.objects.get(id=fk_id).fk_id)
+

Property changes on: tests/modeltests/on_delete_and_on_update_db/tests.py
___________________________________________________________________
Name: svn:keywords
   + Id

Index: tests/modeltests/on_delete_and_on_update_db/models.py
===================================================================
--- tests/modeltests/on_delete_and_on_update_db/models.py       (revision 0)
+++ tests/modeltests/on_delete_and_on_update_db/models.py       (revision 0)
@@ -0,0 +1,57 @@
+"""
+Test ON DELETE and ON UPDATE behavior for foreign keys
+when it's handled by the database.
+"""
+
+from django.db import models
+
+
+#Note: all the lines marked "Used for unit testing only" mean:
+#Some tests (the ones modeltests/on_delete_and_update_db/test.py) 
+#need to force SQL generation to include ON UPDATE and ON DELETE clauses,
+#while others (the ones modeltests/on_delete_and_update_django/test.py) 
+#need to force the SQL not to include these clauses. 
+#Normally the SQL generation is controlled by the settings.py file; 
+#however, usually the database is created all at once when the unit 
+#tests are started, and it uses the settings at the time that happens;
+#there is no way (that I found) to have different settings for different 
+#unit tests. So, this method of controlling it field-by-field was
+#added instead; it is intended to be used only by the unit tests.
+
+
+class Data(models.Model):
+    data = models.IntegerField(unique=True)
+
+
+class FK_on_delete_None(models.Model):
+    fk = models.ForeignKey(Data, to_field='data', on_delete=None)
+    fk.ON_DELETE_HANDLED_BY_DB = True #Used for unit testing only (see comment above)
+
+class FK_on_delete_CASCADE(models.Model):
+    fk = models.ForeignKey(Data, to_field='data', on_delete=models.CASCADE)
+    fk.ON_DELETE_HANDLED_BY_DB = True #Used for unit testing only (see comment above)
+
+class FK_on_delete_RESTRICT(models.Model):
+    fk = models.ForeignKey(Data, to_field='data', on_delete=models.RESTRICT)
+    fk.ON_DELETE_HANDLED_BY_DB = True #Used for unit testing only (see comment above)
+
+class FK_on_delete_SET_NULL(models.Model):
+    fk = models.ForeignKey(Data, to_field='data', null=True, on_delete=models.SET_NULL)
+    fk.ON_DELETE_HANDLED_BY_DB = True #Used for unit testing only (see comment above)
+
+
+class FK_on_update_None(models.Model):
+    fk = models.ForeignKey(Data, to_field='data', on_update=None)
+    fk.ON_UPDATE_HANDLED_BY_DB = True #Used for unit testing only (see comment above)
+
+class FK_on_update_CASCADE(models.Model):
+    fk = models.ForeignKey(Data, to_field='data', on_update=models.CASCADE)
+    fk.ON_UPDATE_HANDLED_BY_DB = True #Used for unit testing only (see comment above)
+
+class FK_on_update_RESTRICT(models.Model):
+    fk = models.ForeignKey(Data, to_field='data', on_update=models.RESTRICT)
+    fk.ON_UPDATE_HANDLED_BY_DB = True #Used for unit testing only (see comment above)
+
+class FK_on_update_SET_NULL(models.Model):
+    fk = models.ForeignKey(Data, to_field='data', null=True, on_update=models.SET_NULL)
+    fk.ON_UPDATE_HANDLED_BY_DB = True #Used for unit testing only (see comment above)

Property changes on: tests/modeltests/on_delete_and_on_update_db/models.py
___________________________________________________________________
Name: svn:keywords
   + Id

Index: tests/modeltests/on_delete_and_on_update_django/__init__.py
===================================================================

Property changes on: tests/modeltests/on_delete_and_on_update_django/__init__.py
___________________________________________________________________
Name: svn:keywords
   + Id

Index: tests/modeltests/on_delete_and_on_update_django/tests.py
===================================================================
--- tests/modeltests/on_delete_and_on_update_django/tests.py    (revision 0)
+++ tests/modeltests/on_delete_and_on_update_django/tests.py    (revision 0)
@@ -0,0 +1,149 @@
+"""
+Test ON DELETE and ON UPDATE behavior for foreign keys
+when it's handled by the Django
+(ie. when settings.ON_DELETE_HANDLED_BY_DB=False
+and settings.ON_UPDATE_HANDLED_BY_DB=False).
+"""
+
+from django.conf import settings
+from django.db import IntegrityError
+from django.db.models.fields.related import CASCADE, RESTRICT, SET_NULL
+from django.test import TestCase
+
+from models import *
+
+
+class ON_DELETE_Tests(TestCase):
+    def setUp(self):
+        self._old_ON_DELETE_HANDLED_BY_DB = getattr(settings, 'ON_DELETE_HANDLED_BY_DB', False)
+        settings.ON_DELETE_HANDLED_BY_DB = False
+        self._old_ON_DELETE_NONE_HANDLING = getattr(settings, 'ON_DELETE_NONE_HANDLING', CASCADE)
+    
+    def tearDown(self):
+        settings.ON_DELETE_HANDLED_BY_DB = self._old_ON_DELETE_HANDLED_BY_DB
+        settings.ON_DELETE_NONE_HANDLING = self._old_ON_DELETE_NONE_HANDLING
+    
+    def test_on_delete_SET_NULL_requires_nullable_field(self):
+        try:
+            class Test(models.Model):
+                fk = models.ForeignKey(Data, to_field='data', on_delete=models.SET_NULL)
+        except ValueError, e:
+            self.assertEqual(str(e), "'on_delete=SET_NULL' specified for ForeignKey field 'Test.fk', but the field is not nullable.")
+        else:
+            self.assertTrue(False, 'Expected exception not raised.')
+    
+    def test_on_delete_None_CASCADE(self):
+        FKModel = FK_on_delete_None
+        settings.ON_DELETE_NONE_HANDLING = CASCADE
+        data = Data.objects.create(data=1)
+        fk_id = FKModel.objects.create(fk=data).id
+        FKModel.objects.get(id=fk_id)
+        data.delete()
+        self.assertRaises(FKModel.DoesNotExist, FKModel.objects.get, id=fk_id)
+    
+    def test_on_delete_None_SET_NULL(self):
+        FKModel = FK_on_delete_None
+        settings.ON_DELETE_NONE_HANDLING = SET_NULL
+        data = Data.objects.create(data=1)
+        fk_id = FKModel.objects.create(fk=data).id
+        self.assertEqual(data, FKModel.objects.get(id=fk_id).fk)
+        self.assertEqual(1, FKModel.objects.get(id=fk_id).fk_id)
+        data.delete()
+        self.assertEqual(None, FKModel.objects.get(id=fk_id).fk)
+        self.assertEqual(None, FKModel.objects.get(id=fk_id).fk_id)
+    
+    def test_on_delete_None_SET_NULL_NoNull(self):
+        FKModel = FK_on_delete_None_NoNull
+        settings.ON_DELETE_NONE_HANDLING = SET_NULL
+        data = Data.objects.create(data=1)
+        FKModel.objects.create(fk=data).id
+        self.assertRaises(IntegrityError, data.delete)
+    
+    def test_on_delete_None_RESTRICT(self):
+        FKModel = FK_on_delete_None
+        settings.ON_DELETE_NONE_HANDLING = RESTRICT
+        data = Data.objects.create(data=1)
+        FKModel.objects.create(fk=data)
+        self.assertRaises(IntegrityError, data.delete)
+    
+    def test_on_delete_CASCADE(self):
+        FKModel = FK_on_delete_CASCADE
+        data = Data.objects.create(data=1)
+        fk_id = FKModel.objects.create(fk=data).id
+        FKModel.objects.get(id=fk_id)
+        data.delete()
+        self.assertRaises(FKModel.DoesNotExist, FKModel.objects.get, id=fk_id)
+    
+    def test_on_delete_RESTRICT(self):
+        FKModel = FK_on_delete_RESTRICT
+        data = Data.objects.create(data=1)
+        FKModel.objects.create(fk=data)
+        self.assertRaises(IntegrityError, data.delete)
+    
+    def test_on_delete_SET_NULL(self):
+        FKModel = FK_on_delete_SET_NULL
+        data = Data.objects.create(data=1)
+        fk_id = FKModel.objects.create(fk=data).id
+        self.assertEqual(data, FKModel.objects.get(id=fk_id).fk)
+        self.assertEqual(1, FKModel.objects.get(id=fk_id).fk_id)
+        data.delete()
+        self.assertEqual(None, FKModel.objects.get(id=fk_id).fk)
+        self.assertEqual(None, FKModel.objects.get(id=fk_id).fk_id)
+
+
+class ON_UPDATE_Tests(TestCase):
+    def setUp(self):
+        self._old_ON_UPDATE_HANDLED_BY_DB = getattr(settings, 'ON_UPDATE_HANDLED_BY_DB', False)
+        settings.ON_UPDATE_HANDLED_BY_DB = False
+    
+    def tearDown(self):
+        settings.ON_UPDATE_HANDLED_BY_DB = self._old_ON_UPDATE_HANDLED_BY_DB
+    
+    def test_on_update_SET_NULL_requires_nullable_field(self):
+        try:
+            class Test(models.Model):
+                fk = models.ForeignKey(Data, to_field='data', on_update=models.SET_NULL)
+        except ValueError, e:
+            self.assertEqual(str(e), "'on_update=SET_NULL' specified for ForeignKey field 'Test.fk', but the field is not nullable.")
+        else:
+            self.assertTrue(False, 'Expected exception not raised.')
+    
+    def test_on_update_None(self):
+        #Currently Django has no ON UPDATE behavior at all,
+        #so the default SQL behavior when no ON UPDATE clause
+        #is specified takes over: it acts like ON UPDATE RESTRICT.
+        FKModel = FK_on_update_None
+        data = Data.objects.create(data=1)
+        FKModel.objects.create(fk=data)
+        data.data += 1
+        self.assertRaises(IntegrityError, data.save)
+    
+    def test_on_update_CASCADE(self):
+        #Currently Django has no ON UPDATE behavior at all,
+        #so the default SQL behavior when no ON UPDATE clause
+        #is specified takes over: it acts like ON UPDATE RESTRICT.
+        FKModel = FK_on_update_CASCADE
+        data = Data.objects.create(data=1)
+        FKModel.objects.create(fk=data)
+        data.data += 1
+        self.assertRaises(IntegrityError, data.save)
+    
+    def test_on_update_RESTRICT(self):
+        #Currently Django has no ON UPDATE behavior at all,
+        #so the default SQL behavior when no ON UPDATE clause
+        #is specified takes over: it acts like ON UPDATE RESTRICT.
+        FKModel = FK_on_update_RESTRICT
+        data = Data.objects.create(data=1)
+        FKModel.objects.create(fk=data)
+        data.data += 1
+        self.assertRaises(IntegrityError, data.save)
+    
+    def test_on_update_SET_NULL(self):
+        #Currently Django has no ON UPDATE behavior at all,
+        #so the default SQL behavior when no ON UPDATE clause
+        #is specified takes over: it acts like ON UPDATE RESTRICT.
+        FKModel = FK_on_update_SET_NULL
+        data = Data.objects.create(data=1)
+        FKModel.objects.create(fk=data)
+        data.data += 1
+        self.assertRaises(IntegrityError, data.save)
\ No newline at end of file

Property changes on: tests/modeltests/on_delete_and_on_update_django/tests.py
___________________________________________________________________
Name: svn:keywords
   + Id

Index: tests/modeltests/on_delete_and_on_update_django/models.py
===================================================================
--- tests/modeltests/on_delete_and_on_update_django/models.py   (revision 0)
+++ tests/modeltests/on_delete_and_on_update_django/models.py   (revision 0)
@@ -0,0 +1,53 @@
+"""
+Test ON DELETE and ON UPDATE behavior for foreign keys
+when it's handled by the Django.
+"""
+
+from django.db import models
+
+
+#Note: for the meaning of the lines marked "Used for unit testing only",
+#see comment in modeltests/on_delete_and_update_db/models.py
+
+
+class Data(models.Model):
+    data = models.IntegerField(unique=True)
+
+
+class FK_on_delete_None(models.Model):
+    fk = models.ForeignKey(Data, to_field='data', null=True, on_delete=None)
+    fk.ON_DELETE_HANDLED_BY_DB = False #Used for unit testing only (see comment above)
+
+class FK_on_delete_None_NoNull(models.Model):
+    fk = models.ForeignKey(Data, to_field='data', on_delete=None)
+    fk.ON_DELETE_HANDLED_BY_DB = False #Used for unit testing only (see comment above)
+
+class FK_on_delete_CASCADE(models.Model):
+    fk = models.ForeignKey(Data, to_field='data', on_delete=models.CASCADE)
+    fk.ON_DELETE_HANDLED_BY_DB = False #Used for unit testing only (see comment above)
+
+class FK_on_delete_RESTRICT(models.Model):
+    fk = models.ForeignKey(Data, to_field='data', on_delete=models.RESTRICT)
+    fk.ON_DELETE_HANDLED_BY_DB = False #Used for unit testing only (see comment above)
+
+class FK_on_delete_SET_NULL(models.Model):
+    fk = models.ForeignKey(Data, to_field='data', null=True, on_delete=models.SET_NULL)
+    fk.ON_DELETE_HANDLED_BY_DB = False #Used for unit testing only (see comment above)
+
+
+class FK_on_update_None(models.Model):
+    fk = models.ForeignKey(Data, to_field='data', on_update=None)
+    fk.ON_UPDATE_HANDLED_BY_DB = False #Used for unit testing only (see comment above)
+
+class FK_on_update_CASCADE(models.Model):
+    fk = models.ForeignKey(Data, to_field='data', on_update=models.CASCADE)
+    fk.ON_UPDATE_HANDLED_BY_DB = False #Used for unit testing only (see comment above)
+
+class FK_on_update_RESTRICT(models.Model):
+    fk = models.ForeignKey(Data, to_field='data', on_update=models.RESTRICT)
+    fk.ON_UPDATE_HANDLED_BY_DB = False #Used for unit testing only (see comment above)
+
+class FK_on_update_SET_NULL(models.Model):
+    fk = models.ForeignKey(Data, to_field='data', null=True, on_update=models.SET_NULL)
+    fk.ON_UPDATE_HANDLED_BY_DB = False #Used for unit testing only (see comment above)
+    
\ No newline at end of file

Property changes on: tests/modeltests/on_delete_and_on_update_django/models.py
___________________________________________________________________
Name: svn:keywords
   + Id

Index: tests/modeltests/delete/models.py
===================================================================
--- tests/modeltests/delete/models.py   (revision 9845)
+++ tests/modeltests/delete/models.py   (working copy)
@@ -46,7 +46,7 @@
 
 ## First, test the CollectedObjects data structure directly
 
->>> from django.db.models.query import CollectedObjects
+>>> from django.db.models.query import CollectedFields, CollectedObjects
 
 >>> g = CollectedObjects()
 >>> g.add("key1", 1, "item1", None)
@@ -112,8 +112,8 @@
 >>> d1 = D(c=c1, a=a1)
 >>> d1.save()
 
->>> o = CollectedObjects()
->>> a1._collect_sub_objects(o)
+>>> o, p = CollectedObjects(), CollectedFields()
+>>> a1._collect_sub_objects(o, p)
 >>> o.keys()
 [<class 'modeltests.delete.models.D'>, <class 'modeltests.delete.models.C'>, <class 'modeltests.delete.models.B'>, <class 'modeltests.delete.models.A'>]
 >>> a1.delete()
@@ -131,8 +131,8 @@
 >>> d2 = D(c=c2, a=a2)
 >>> d2.save()
 
->>> o = CollectedObjects()
->>> a2._collect_sub_objects(o)
+>>> o, p = CollectedObjects(), CollectedFields()
+>>> a2._collect_sub_objects(o, p)
 >>> o.keys()
 [<class 'modeltests.delete.models.D'>, <class 'modeltests.delete.models.C'>, <class 'modeltests.delete.models.B'>, <class 'modeltests.delete.models.A'>]
 >>> a2.delete()
@@ -163,8 +163,8 @@
 # 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, p = CollectedObjects(), CollectedFields()
+>>> e1._collect_sub_objects(o, p)
 >>> o.keys()
 [<class 'modeltests.delete.models.F'>, <class 'modeltests.delete.models.E'>]
 
@@ -179,8 +179,8 @@
 
 # Same deal as before, though we are starting from the other object.
 
->>> o = CollectedObjects()
->>> f2._collect_sub_objects(o)
+>>> o, p = CollectedObjects(), CollectedFields()
+>>> f2._collect_sub_objects(o, p)
 >>> o.keys()
 [<class 'modeltests.delete.models.F'>, <class 'modeltests.delete.models.E'>]