Django

Code

Ticket #7539: on_delete_on_update-r10558.diff

File on_delete_on_update-r10558.diff, 20.9 kB (added by dokterbob, 1 year ago)

Patch updated for r10558. Very fresh and barely tested, see more info in ticket.

  • db/models/base.py

    old new  
    1313from django.db.models.fields import AutoField, FieldDoesNotExist 
    1414from django.db.models.fields.related import OneToOneRel, ManyToOneRel, OneToOneField 
    1515from django.db.models.query import delete_objects, Q 
    16 from django.db.models.query_utils import CollectedObjects, DeferredAttribute 
     16from django.db.models.query_utils import CollectedObjects, CollectedFields, DeferredAttribute 
    1717from django.db.models.options import Options 
    18 from django.db import connection, transaction, DatabaseError 
     18from django.db import connection, transaction, DatabaseError, IntegrityError 
    1919from django.db.models import signals 
     20from django.db.models.fields.related import CASCADE, RESTRICT, SET_NULL 
    2021from django.db.models.loading import register_models, get_model 
    2122from django.utils.functional import curry 
    2223from django.utils.encoding import smart_str, force_unicode, smart_unicode 
     
    495496 
    496497    save_base.alters_data = True 
    497498 
    498     def _collect_sub_objects(self, seen_objs, parent=None, nullable=False): 
     499    def _collect_sub_objects(self, seen_objs, fields_to_null, parent=None, nullable=False): 
    499500        """ 
    500501        Recursively populates seen_objs with all objects related to this 
    501502        object. 
     
    508509        if seen_objs.add(self.__class__, pk_val, self, parent, nullable): 
    509510            return 
    510511 
    511         for related in self._meta.get_all_related_objects(): 
    512             rel_opts_name = related.get_accessor_name() 
    513             if isinstance(related.field.rel, OneToOneRel): 
    514                 try: 
    515                     sub_obj = getattr(self, rel_opts_name) 
    516                 except ObjectDoesNotExist: 
    517                     pass 
     512        if not getattr(settings, 'ON_DELETE_HANDLED_BY_DB', False): 
     513            ON_DELETE_NONE_HANDLING = getattr(settings, 'ON_DELETE_NONE_HANDLING', CASCADE) 
     514 
     515            def _handle_sub_obj(related, sub_obj): 
     516                on_delete = related.field.rel.on_delete 
     517                if on_delete is None: 
     518                    on_delete = ON_DELETE_NONE_HANDLING 
     519 
     520                if on_delete == CASCADE: 
     521                    sub_obj._collect_sub_objects(seen_objs, fields_to_null, self.__class__) 
     522                elif on_delete == RESTRICT: 
     523                    msg = '[Django] Cannot delete a parent object: a foreign key constraint fails (ForeignKey `%s` (pk=`%s`) references `%s` (pk=`%s`))' % ( 
     524                        sub_obj.__class__, 
     525                        sub_obj._get_pk_val(), 
     526                        self.__class__, 
     527                        pk_val, 
     528                        ) 
     529                    raise IntegrityError(msg) #TODO: include error number also? E.g., "raise IntegrityError(1451, msg)" (but 1451 is MySQL-specific, I believe) 
     530                elif on_delete == SET_NULL: 
     531                    if not related.field.null: 
     532                        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`))' % ( 
     533                            sub_obj.__class__, 
     534                            sub_obj._get_pk_val(), 
     535                            self.__class__, 
     536                            pk_val, 
     537                            ) 
     538                        raise IntegrityError(msg) #TODO: include error number also? E.g., "raise IntegrityError(<errno>, msg)" (but the error numbers are db-specific, I believe) 
     539                    fields_to_null.add(sub_obj.__class__, sub_obj._get_pk_val(), sub_obj, related.field.name) 
    518540                else: 
    519                     sub_obj._collect_sub_objects(seen_objs, self.__class__, related.field.null) 
    520             else: 
    521                 # To make sure we can access all elements, we can't use the 
    522                 # normal manager on the related object. So we work directly 
    523                 # with the descriptor object. 
    524                 for cls in self.__class__.mro(): 
    525                     if rel_opts_name in cls.__dict__: 
    526                         rel_descriptor = cls.__dict__[rel_opts_name] 
    527                         break 
     541                    raise AttributeError('Unexpected value for on_delete') 
     542 
     543            for related in self._meta.get_all_related_objects(): 
     544                rel_opts_name = related.get_accessor_name() 
     545                if isinstance(related.field.rel, OneToOneRel): 
     546                    try: 
     547                        sub_obj = getattr(self, rel_opts_name) 
     548                    except ObjectDoesNotExist: 
     549                        pass 
     550                    else: 
     551                        _handle_sub_obj(related, sub_obj) 
    528552                else: 
    529                     raise AssertionError("Should never get here.") 
    530                 delete_qs = rel_descriptor.delete_manager(self).all() 
    531                 for sub_obj in delete_qs: 
    532                     sub_obj._collect_sub_objects(seen_objs, self.__class__, related.field.null) 
     553                    # To make sure we can access all elements, we can't use the 
     554                    # normal manager on the related object. So we work directly 
     555                    # with the descriptor object. 
     556                    for cls in self.__class__.mro(): 
     557                        if rel_opts_name in cls.__dict__: 
     558                            rel_descriptor = cls.__dict__[rel_opts_name] 
     559                            break 
     560                    else: 
     561                        raise AssertionError("Should never get here.") 
     562                    delete_qs = rel_descriptor.delete_manager(self).all() 
     563                    for sub_obj in delete_qs: 
     564                        _handle_sub_obj(related, sub_obj) 
    533565 
    534         # Handle any ancestors (for the model-inheritance case). We do this by 
    535         # traversing to the most remote parent classes -- those with no parents 
    536         # themselves -- and then adding those instances to the collection. That 
    537         # will include all the child instances down to "self". 
    538         parent_stack = self._meta.parents.values() 
    539         while parent_stack: 
    540             link = parent_stack.pop() 
    541             parent_obj = getattr(self, link.name) 
    542             if parent_obj._meta.parents: 
    543                 parent_stack.extend(parent_obj._meta.parents.values()) 
    544                 continue 
    545             # At this point, parent_obj is base class (no ancestor models). So 
    546             # delete it and all its descendents. 
    547             parent_obj._collect_sub_objects(seen_objs
     566            # Handle any ancestors (for the model-inheritance case). We do this by 
     567            # traversing to the most remote parent classes -- those with no parents 
     568            # themselves -- and then adding those instances to the collection. That 
     569            # will include all the child instances down to "self". 
     570            parent_stack = self._meta.parents.values() 
     571            while parent_stack: 
     572                link = parent_stack.pop() 
     573                parent_obj = getattr(self, link.name) 
     574                if parent_obj._meta.parents: 
     575                    parent_stack.extend(parent_obj._meta.parents.values()) 
     576                    continue 
     577                # At this point, parent_obj is base class (no ancestor models). So 
     578                # delete it and all its descendents. 
     579                parent_obj._collect_sub_objects(seen_objs, fields_to_null
    548580 
    549581    def delete(self): 
    550582        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) 
    551583 
    552584        # Find all the objects than need to be deleted. 
    553585        seen_objs = CollectedObjects() 
    554         self._collect_sub_objects(seen_objs) 
     586        fields_to_null = CollectedFields() 
     587        self._collect_sub_objects(seen_objs, fields_to_null) 
    555588 
    556589        # Actually delete the objects. 
    557         delete_objects(seen_objs
     590        delete_objects(seen_objs, fields_to_null
    558591 
    559592    delete.alters_data = True 
    560593 
  • db/models/fields/related.py

    old new  
    1010from django.utils.functional import curry 
    1111from django.core import exceptions 
    1212from django import forms 
     13from django.conf import settings 
    1314 
    1415try: 
    1516    set 
     
    2021 
    2122pending_lookups = {} 
    2223 
     24class _OnDeleteOrUpdateAction(object): 
     25    pass 
     26class RESTRICT(_OnDeleteOrUpdateAction): 
     27    sql = 'RESTRICT' 
     28class CASCADE(_OnDeleteOrUpdateAction): 
     29    sql = 'CASCADE' 
     30class SET_NULL(_OnDeleteOrUpdateAction): 
     31    sql = 'SET NULL' 
     32ALLOWED_ON_DELETE_ACTION_TYPES = set([None, CASCADE, RESTRICT, SET_NULL]) 
     33ALLOWED_ON_UPDATE_ACTION_TYPES = ALLOWED_ON_DELETE_ACTION_TYPES.copy() 
     34 
    2335def add_lazy_relation(cls, field, relation, operation): 
    2436    """ 
    2537    Adds a lookup on ``cls`` when a related field is defined using a string, 
     
    604616 
    605617class ManyToOneRel(object): 
    606618    def __init__(self, to, field_name, related_name=None, 
    607             limit_choices_to=None, lookup_overrides=None, parent_link=False): 
     619            limit_choices_to=None, lookup_overrides=None, parent_link=False, 
     620            on_delete=None, on_update=None): 
    608621        try: 
    609622            to._meta 
    610623        except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT 
     
    617630        self.lookup_overrides = lookup_overrides or {} 
    618631        self.multiple = True 
    619632        self.parent_link = parent_link 
     633        self.on_delete = on_delete 
     634        self.on_update = on_update 
    620635 
    621636    def get_related_field(self): 
    622637        """ 
     
    631646 
    632647class OneToOneRel(ManyToOneRel): 
    633648    def __init__(self, to, field_name, related_name=None, 
    634             limit_choices_to=None, lookup_overrides=None, parent_link=False): 
     649            limit_choices_to=None, lookup_overrides=None, parent_link=False, 
     650            on_delete=None, on_update=None): 
    635651        super(OneToOneRel, self).__init__(to, field_name, 
    636652                related_name=related_name, limit_choices_to=limit_choices_to, 
    637                 lookup_overrides=lookup_overrides, parent_link=parent_link) 
     653                lookup_overrides=lookup_overrides, parent_link=parent_link, 
     654                on_delete=on_delete, on_update=on_update) 
    638655        self.multiple = False 
    639656 
    640657class ManyToManyRel(object): 
     
    673690            related_name=kwargs.pop('related_name', None), 
    674691            limit_choices_to=kwargs.pop('limit_choices_to', None), 
    675692            lookup_overrides=kwargs.pop('lookup_overrides', None), 
    676             parent_link=kwargs.pop('parent_link', False)) 
     693            parent_link=kwargs.pop('parent_link', False), 
     694            on_delete=kwargs.pop('on_delete', None), 
     695            on_update=kwargs.pop('on_update', None)) 
    677696        Field.__init__(self, **kwargs) 
    678697 
    679698        self.db_index = True 
     
    718737            target = self.rel.to._meta.db_table 
    719738        cls._meta.duplicate_targets[self.column] = (target, "o2m") 
    720739 
     740        on_delete, on_update = self.rel.on_delete, self.rel.on_update 
     741        if on_delete not in ALLOWED_ON_DELETE_ACTION_TYPES: 
     742            raise ValueError("Invalid value 'on_delete=%s' specified for ForeignKey field %s.%s." % (on_delete, cls.__name__, name)) 
     743        if on_update not in ALLOWED_ON_UPDATE_ACTION_TYPES: 
     744            raise ValueError("Invalid value 'on_update=%s' specified for ForeignKey field %s.%s." % (on_update, cls.__name__, name)) 
     745        if (on_delete == SET_NULL or on_update == SET_NULL) and not self.null: 
     746            if on_delete == SET_NULL and on_update == SET_NULL: 
     747                specification = "'on_delete=SET_NULL' and 'on_update=SET_NULL'" 
     748            elif on_delete == SET_NULL: 
     749                specification = "'on_delete=SET_NULL'" 
     750            else: 
     751                specification = "'on_update=SET_NULL'" 
     752            raise ValueError("%s specified for ForeignKey field '%s.%s', but the field is not nullable." % (specification, cls.__name__, name)) 
     753        if on_delete is None and getattr(settings, 'ON_DELETE_NONE_HANDLING', CASCADE) == SET_NULL and not self.null: 
     754            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)) 
     755 
    721756    def contribute_to_related_class(self, cls, related): 
    722757        setattr(cls, related.get_accessor_name(), ForeignRelatedObjectsDescriptor(related)) 
    723758 
  • db/models/__init__.py

    old new  
    1111from django.db.models.fields.subclassing import SubfieldBase 
    1212from django.db.models.fields.files import FileField, ImageField 
    1313from django.db.models.fields.related import ForeignKey, OneToOneField, ManyToManyField, ManyToOneRel, ManyToManyRel, OneToOneRel 
     14from django.db.models.fields.related import CASCADE, RESTRICT, SET_NULL 
    1415from django.db.models import signals 
    1516 
    1617# Admin stages. 
  • db/models/query.py

    old new  
    359359            # Collect all the objects to be deleted in this chunk, and all the 
    360360            # objects that are related to the objects that are to be deleted. 
    361361            seen_objs = CollectedObjects() 
     362            fields_to_null = CollectedFields() 
    362363            for object in del_query[:CHUNK_SIZE]: 
    363                 object._collect_sub_objects(seen_objs
     364                object._collect_sub_objects(seen_objs, fields_to_null
    364365 
    365366            if not seen_objs: 
    366367                break 
    367             delete_objects(seen_objs
     368            delete_objects(seen_objs, fields_to_null
    368369 
    369370        # Clear the result cache, in case this QuerySet gets reused. 
    370371        self._result_cache = None 
     
    952953                setattr(obj, f.get_cache_name(), rel_obj) 
    953954    return obj, index_end 
    954955 
    955 def delete_objects(seen_objs): 
     956def delete_objects(seen_objs, fields_to_null): 
    956957    """ 
    957958    Iterate through a list of seen classes, and remove any instances that are 
    958959    referred to. 
     
    998999                        update_query.clear_related(field, pk_list) 
    9991000 
    10001001        # Now delete the actual data. 
     1002        for cls, cls_dct in fields_to_null.iteritems(): 
     1003            update_query = sql.UpdateQuery(cls, connection) 
     1004            field_dict = {} 
     1005            for pk, (_, field_names) in cls_dct.iteritems(): 
     1006                for field_name in field_names: 
     1007                    pk_set = field_dict.setdefault(field_name, set()) 
     1008                    pk_set.add(pk) 
     1009            for field_name, pk_set in field_dict.iteritems(): 
     1010                update_query.clear_related(cls._meta.get_field_by_name(field_name)[0], list(pk_set)) 
    10011011        for cls in ordered_classes: 
    10021012            items = obj_pairs[cls] 
    10031013            items.reverse() 
     
    10091019            # Last cleanup; set NULLs where there once was a reference to the 
    10101020            # object, NULL the primary key of the found objects, and perform 
    10111021            # post-notification. 
     1022            for cls, cls_dct in fields_to_null.iteritems(): 
     1023                for instance, field_names in cls_dct.itervalues(): 
     1024                    for field_name in field_names: 
     1025                        field = cls._meta.get_field_by_name(field_name)[0] 
     1026                        setattr(instance, field.attname, None) 
    10121027            for pk_val, instance in items: 
    10131028                for field in cls._meta.fields: 
    10141029                    if field.rel and field.null and field.rel.to in seen_objs: 
  • db/models/query_utils.py

    old new  
    111111        """ 
    112112        return self.data.keys() 
    113113 
     114class CollectedFields(object): 
     115    """ 
     116    A container that stores model objects and fields that need to 
     117    be nulled to enforce the on_delete=SET_NULL ForeignKey constraint. 
     118    """ 
     119    def __init__(self): 
     120        self.data = {} 
     121 
     122    def add(self, model, pk, obj, field_name): 
     123        """ 
     124        Adds an item. 
     125        model is the class of the object being added, 
     126        pk is the primary key, obj is the object itself,  
     127        field_name is the name of the field to be nulled. 
     128        """ 
     129        d = self.data.setdefault(model, SortedDict()) 
     130        obj, field_names = d.setdefault(pk, (obj, set())) 
     131        field_names.add(field_name) 
     132 
     133    def __contains__(self, key): 
     134        return self.data.__contains__(key) 
     135 
     136    def __getitem__(self, key): 
     137        return self.data[key] 
     138 
     139    def __nonzero__(self): 
     140        return bool(self.data) 
     141 
     142    def iteritems(self): 
     143        return self.data.iteritems() 
     144 
     145    def iterkeys(self): 
     146        return self.data.iterkeys() 
     147 
     148    def itervalues(self): 
     149        return self.data.itervalues() 
     150 
    114151class QueryWrapper(object): 
    115152    """ 
    116153    A type that indicates the contents are an SQL fragment and the associate 
  • db/backends/creation.py

    old new  
    9696        "Return the SQL snippet defining the foreign key reference for a field" 
    9797        qn = self.connection.ops.quote_name 
    9898        if field.rel.to in known_models: 
     99            on_delete_clause, on_update_clause = self.on_delete_and_update_clauses(style, field) 
    99100            output = [style.SQL_KEYWORD('REFERENCES') + ' ' + \ 
    100101                style.SQL_TABLE(qn(field.rel.to._meta.db_table)) + ' (' + \ 
    101102                style.SQL_FIELD(qn(field.rel.to._meta.get_field(field.rel.field_name).column)) + ')' + 
     103                on_delete_clause + on_update_clause + 
    102104                self.connection.ops.deferrable_sql() 
    103105            ] 
    104106            pending = False 
     
    121123        opts = model._meta 
    122124        if model in pending_references: 
    123125            for rel_class, f in pending_references[model]: 
     126                on_delete_clause, on_update_clause = self.on_delete_and_update_clauses(style, f) 
    124127                rel_opts = rel_class._meta 
    125128                r_table = rel_opts.db_table 
    126129                r_col = f.column 
     
    129132                # For MySQL, r_name must be unique in the first 64 characters. 
    130133                # So we are careful with character usage here. 
    131134                r_name = '%s_refs_%s_%x' % (r_col, col, abs(hash((r_table, table)))) 
    132                 final_output.append(style.SQL_KEYWORD('ALTER TABLE') + ' %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s (%s)%s;' % \ 
     135                final_output.append(style.SQL_KEYWORD('ALTER TABLE') + ' %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s (%s)%s%s%s;' % \ 
    133136                    (qn(r_table), qn(truncate_name(r_name, self.connection.ops.max_name_length())), 
    134137                    qn(r_col), qn(table), qn(col), 
     138                    on_delete_clause, on_update_clause, 
    135139                    self.connection.ops.deferrable_sql())) 
    136140            del pending_references[model] 
    137141        return final_output 
    138142 
     143    def on_delete_and_update_clauses(self, style, foreign_key_field): 
     144        on_delete_clause = '' 
     145        try: 
     146            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) 
     147        except AttributeError: 
     148            on_delete = getattr(settings, 'ON_DELETE_HANDLED_BY_DB', False) 
     149        if on_delete: 
     150            on_delete = getattr(foreign_key_field.rel, 'on_delete', None) 
     151            if on_delete: 
     152                on_delete_clause = style.SQL_KEYWORD(' ON DELETE %s' % foreign_key_field.rel.on_delete.sql) 
     153             
     154        on_update_clause = '' 
     155        try: 
     156            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) 
     157        except AttributeError: 
     158            on_update = getattr(settings, 'ON_UPDATE_HANDLED_BY_DB', False) 
     159        if on_update: 
     160            on_update = getattr(foreign_key_field.rel, 'on_update', None) 
     161            if on_update: 
     162                on_update_clause = style.SQL_KEYWORD(' ON UPDATE %s' % foreign_key_field.rel.on_update.sql) 
     163 
     164        return on_delete_clause, on_update_clause 
     165         
    139166    def sql_for_many_to_many(self, model, style): 
    140167        "Return the CREATE TABLE statments for all the many-to-many tables defined on a model" 
    141168        output = [] 
  • contrib/admin/options.py

    old new  
    10061006        perms_needed = set() 
    10071007        get_deleted_objects(deleted_objects, perms_needed, request.user, obj, opts, 1, self.admin_site) 
    10081008 
     1009        import logging; logging.debug('deleting %s' % deleted_objects) 
    10091010        if request.POST: # The user has already confirmed the deletion. 
    10101011            if perms_needed: 
    10111012                raise PermissionDenied