Ticket #7539: on_delete_on_update-r11620.diff.txt

File on_delete_on_update-r11620.diff.txt, 21.0 KB (added by glassfordm, 14 years ago)

Very preliminary patch for 1.2 release

Line 
1MJG-MBP:django_on_delete_patch mjg$ svn diff
2Index: django/db/models/base.py
3===================================================================
4--- django/db/models/base.py (revision 11620)
5+++ django/db/models/base.py (working copy)
6@@ -13,10 +13,11 @@
7 from django.db.models.fields import AutoField, FieldDoesNotExist
8 from django.db.models.fields.related import OneToOneRel, ManyToOneRel, OneToOneField
9 from django.db.models.query import delete_objects, Q
10-from django.db.models.query_utils import CollectedObjects, DeferredAttribute
11+from django.db.models.query_utils import CollectedFields, CollectedObjects, DeferredAttribute
12 from django.db.models.options import Options
13-from django.db import connection, transaction, DatabaseError
14+from django.db import connection, transaction, DatabaseError, IntegrityError
15 from django.db.models import signals
16+from django.db.models.fields.related import CASCADE, PROTECT, SET_NULL, SET_DEFAULT
17 from django.db.models.loading import register_models, get_model
18 from django.utils.functional import curry
19 from django.utils.encoding import smart_str, force_unicode, smart_unicode
20@@ -507,7 +508,7 @@
21
22 save_base.alters_data = True
23
24- def _collect_sub_objects(self, seen_objs, parent=None, nullable=False):
25+ def _collect_sub_objects(self, seen_objs, fields_to_set, parent=None, nullable=False):
26 """
27 Recursively populates seen_objs with all objects related to this
28 object.
29@@ -519,16 +520,65 @@
30 pk_val = self._get_pk_val()
31 if seen_objs.add(self.__class__, pk_val, self, parent, nullable):
32 return
33+
34+ def _handle_sub_obj(related, sub_obj):
35+ on_delete = related.field.rel.on_delete
36+ if on_delete is None:
37+ #If no explicit on_delete option is specified, use the old
38+ #django behavior as the default: SET_NULL if the foreign
39+ #key is nullable, otherwise CASCADE.
40+ if related.field.null:
41+ on_delete = SET_NULL
42+ else:
43+ on_delete = CASCADE
44+
45+ if on_delete == CASCADE:
46+ sub_obj._collect_sub_objects(seen_objs, fields_to_set, self.__class__)
47+ elif on_delete == PROTECT:
48+ msg = '[Django] Cannot delete a parent object: a foreign key constraint fails (ForeignKey `%s` (pk=`%s`) references `%s` (pk=`%s`))' % (
49+ sub_obj.__class__,
50+ sub_obj._get_pk_val(),
51+ self.__class__,
52+ pk_val,
53+ )
54+ raise IntegrityError(msg)
55+ elif on_delete == SET_NULL:
56+ if not related.field.null:
57+ 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`))' % (
58+ sub_obj.__class__,
59+ sub_obj._get_pk_val(),
60+ self.__class__,
61+ pk_val,
62+ )
63+ raise IntegrityError(msg)
64+ fields_to_set.add(sub_obj.__class__, sub_obj._get_pk_val(), sub_obj, related.field.name, None)
65+ elif on_delete == SET_DEFAULT:
66+ if not related.field.has_default():
67+ msg = '[Django] Cannot delete a parent object: foreign key constraint on_delete=SET_DEFAULT is specified for a foreign key with no default value (ForeignKey `%s` (pk=`%s`) references `%s` (pk=`%s`))' % (
68+ sub_obj.__class__,
69+ sub_obj._get_pk_val(),
70+ self.__class__,
71+ pk_val,
72+ )
73+ raise IntegrityError(msg)
74+ fields_to_set.add(sub_obj.__class__, sub_obj._get_pk_val(), sub_obj, related.field.name, related.field.get_default())
75+ else:
76+ raise AttributeError('Unexpected value for on_delete')
77
78 for related in self._meta.get_all_related_objects():
79 rel_opts_name = related.get_accessor_name()
80 if isinstance(related.field.rel, OneToOneRel):
81 try:
82+ # delattr(self, rel_opts_name) #Delete first to clear any stale cache
83+ #TODO: the above line is a bit of a hack
84+ #It's one way (not a very good one) to work around stale cache data causing
85+ #spurious RESTRICT errors, etc; it would be better to prevent the cache from
86+ #becoming stale in the first place.
87 sub_obj = getattr(self, rel_opts_name)
88 except ObjectDoesNotExist:
89 pass
90 else:
91- sub_obj._collect_sub_objects(seen_objs, self.__class__, related.field.null)
92+ _handle_sub_obj(related, sub_obj)
93 else:
94 # To make sure we can access all elements, we can't use the
95 # normal manager on the related object. So we work directly
96@@ -541,7 +591,7 @@
97 raise AssertionError("Should never get here.")
98 delete_qs = rel_descriptor.delete_manager(self).all()
99 for sub_obj in delete_qs:
100- sub_obj._collect_sub_objects(seen_objs, self.__class__, related.field.null)
101+ _handle_sub_obj(related, sub_obj)
102
103 # Handle any ancestors (for the model-inheritance case). We do this by
104 # traversing to the most remote parent classes -- those with no parents
105@@ -556,18 +606,18 @@
106 continue
107 # At this point, parent_obj is base class (no ancestor models). So
108 # delete it and all its descendents.
109- parent_obj._collect_sub_objects(seen_objs)
110+ parent_obj._collect_sub_objects(seen_objs, fields_to_set)
111
112 def delete(self):
113 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)
114
115 # Find all the objects than need to be deleted.
116 seen_objs = CollectedObjects()
117- self._collect_sub_objects(seen_objs)
118+ fields_to_set = CollectedFields()
119+ self._collect_sub_objects(seen_objs, fields_to_set)
120
121 # Actually delete the objects.
122- delete_objects(seen_objs)
123-
124+ delete_objects(seen_objs, fields_to_set)
125 delete.alters_data = True
126
127 def _get_FIELD_display(self, field):
128Index: django/db/models/fields/related.py
129===================================================================
130--- django/db/models/fields/related.py (revision 11620)
131+++ django/db/models/fields/related.py (working copy)
132@@ -20,6 +20,16 @@
133
134 pending_lookups = {}
135
136+class CASCADE(object):
137+ pass
138+class PROTECT(object):
139+ pass
140+class SET_NULL(object):
141+ pass
142+class SET_DEFAULT(object):
143+ pass
144+ALLOWED_ON_DELETE_ACTION_TYPES = set([None, CASCADE, PROTECT, SET_NULL, SET_DEFAULT])
145+
146 def add_lazy_relation(cls, field, relation, operation):
147 """
148 Adds a lookup on ``cls`` when a related field is defined using a string,
149@@ -218,6 +228,16 @@
150 # object you just set.
151 setattr(instance, self.cache_name, value)
152 setattr(value, self.related.field.get_cache_name(), instance)
153+
154+ #TODO: the following function is a bit of a hack
155+ #It's one way (not a very good one) to work around stale cache data causing
156+ #spurious RESTRICT errors, etc; it would be better to prevent the cache from
157+ #becoming stale in the first place.
158+ # def __delete__(self, instance):
159+ # try:
160+ # return delattr(instance, self.cache_name)
161+ # except AttributeError:
162+ # pass
163
164 class ReverseSingleRelatedObjectDescriptor(object):
165 # This class provides the functionality that makes the related-object
166@@ -628,7 +648,8 @@
167
168 class ManyToOneRel(object):
169 def __init__(self, to, field_name, related_name=None,
170- limit_choices_to=None, lookup_overrides=None, parent_link=False):
171+ limit_choices_to=None, lookup_overrides=None, parent_link=False,
172+ on_delete=None):
173 try:
174 to._meta
175 except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT
176@@ -641,6 +662,7 @@
177 self.lookup_overrides = lookup_overrides or {}
178 self.multiple = True
179 self.parent_link = parent_link
180+ self.on_delete = on_delete
181
182 def get_related_field(self):
183 """
184@@ -655,10 +677,12 @@
185
186 class OneToOneRel(ManyToOneRel):
187 def __init__(self, to, field_name, related_name=None,
188- limit_choices_to=None, lookup_overrides=None, parent_link=False):
189+ limit_choices_to=None, lookup_overrides=None, parent_link=False,
190+ on_delete=None):
191 super(OneToOneRel, self).__init__(to, field_name,
192 related_name=related_name, limit_choices_to=limit_choices_to,
193- lookup_overrides=lookup_overrides, parent_link=parent_link)
194+ lookup_overrides=lookup_overrides, parent_link=parent_link,
195+ on_delete=on_delete)
196 self.multiple = False
197
198 class ManyToManyRel(object):
199@@ -697,7 +721,8 @@
200 related_name=kwargs.pop('related_name', None),
201 limit_choices_to=kwargs.pop('limit_choices_to', None),
202 lookup_overrides=kwargs.pop('lookup_overrides', None),
203- parent_link=kwargs.pop('parent_link', False))
204+ parent_link=kwargs.pop('parent_link', False),
205+ on_delete=kwargs.pop('on_delete', None))
206 Field.__init__(self, **kwargs)
207
208 self.db_index = True
209@@ -742,6 +767,16 @@
210 target = self.rel.to._meta.db_table
211 cls._meta.duplicate_targets[self.column] = (target, "o2m")
212
213+ on_delete = self.rel.on_delete
214+ if on_delete not in ALLOWED_ON_DELETE_ACTION_TYPES:
215+ raise ValueError("Invalid value 'on_delete=%s' specified for %s %s.%s." % (on_delete, type(self).__name__, cls.__name__, name))
216+ if on_delete == SET_NULL and not self.null:
217+ specification = "'on_delete=SET_NULL'"
218+ raise ValueError("%s specified for %s '%s.%s', but the field is not nullable." % (specification, type(self).__name__, cls.__name__, name))
219+ if on_delete == SET_DEFAULT and not self.has_default():
220+ specification = "'on_delete=SET_DEFAULT'"
221+ raise ValueError("%s specified for %s '%s.%s', but the field has no default value." % (specification, type(self).__name__, cls.__name__, name))
222+
223 def contribute_to_related_class(self, cls, related):
224 setattr(cls, related.get_accessor_name(), ForeignRelatedObjectsDescriptor(related))
225
226Index: django/db/models/__init__.py
227===================================================================
228--- django/db/models/__init__.py (revision 11620)
229+++ django/db/models/__init__.py (working copy)
230@@ -11,6 +11,7 @@
231 from django.db.models.fields.subclassing import SubfieldBase
232 from django.db.models.fields.files import FileField, ImageField
233 from django.db.models.fields.related import ForeignKey, OneToOneField, ManyToManyField, ManyToOneRel, ManyToManyRel, OneToOneRel
234+from django.db.models.fields.related import CASCADE, PROTECT, SET_NULL, SET_DEFAULT
235 from django.db.models import signals
236
237 # Admin stages.
238Index: django/db/models/query.py
239===================================================================
240--- django/db/models/query.py (revision 11620)
241+++ django/db/models/query.py (working copy)
242@@ -11,8 +11,9 @@
243
244 from django.db import connection, transaction, IntegrityError
245 from django.db.models.aggregates import Aggregate
246+from django.db.models.sql.constants import GET_ITERATOR_CHUNK_SIZE
247 from django.db.models.fields import DateField
248-from django.db.models.query_utils import Q, select_related_descend, CollectedObjects, CyclicDependency, deferred_class_factory
249+from django.db.models.query_utils import Q, select_related_descend, CollectedFields, CollectedObjects, CyclicDependency, deferred_class_factory
250 from django.db.models import signals, sql
251
252
253@@ -391,12 +392,13 @@
254 # Collect all the objects to be deleted in this chunk, and all the
255 # objects that are related to the objects that are to be deleted.
256 seen_objs = CollectedObjects(seen_objs)
257+ fields_to_set = CollectedFields()
258 for object in del_query[:CHUNK_SIZE]:
259- object._collect_sub_objects(seen_objs)
260+ object._collect_sub_objects(seen_objs, fields_to_set)
261
262 if not seen_objs:
263 break
264- delete_objects(seen_objs)
265+ delete_objects(seen_objs, fields_to_set)
266
267 # Clear the result cache, in case this QuerySet gets reused.
268 self._result_cache = None
269@@ -1002,7 +1004,7 @@
270 setattr(obj, f.get_cache_name(), rel_obj)
271 return obj, index_end
272
273-def delete_objects(seen_objs):
274+def delete_objects(seen_objs, fields_to_set):
275 """
276 Iterate through a list of seen classes, and remove any instances that are
277 referred to.
278@@ -1023,6 +1025,19 @@
279
280 obj_pairs = {}
281 try:
282+ for cls, cls_dct in fields_to_set.iteritems():
283+ #TODO: batch these, similar to UpdateQuery.clear_related?
284+ #(Note that it may be harder to do here because the default value
285+ #for a given field may be different for each instance,
286+ #while UpdateQuery.clear_related always uses the value None).
287+ query = sql.UpdateQuery(cls, connection)
288+ for instance, field_names_and_values in cls_dct.itervalues():
289+ query.where = query.where_class()
290+ pk = query.model._meta.pk
291+ query.where.add((sql.where.Constraint(None, pk.column, pk), 'exact', instance.pk), sql.where.AND)
292+ query.add_update_values(field_names_and_values)
293+ query.execute_sql()
294+
295 for cls in ordered_classes:
296 items = seen_objs[cls].items()
297 items.sort()
298@@ -1032,33 +1047,29 @@
299 for pk_val, instance in items:
300 signals.pre_delete.send(sender=cls, instance=instance)
301
302+ # Handle related GenericRelation and ManyToManyField instances
303 pk_list = [pk for pk,instance in items]
304 del_query = sql.DeleteQuery(cls, connection)
305 del_query.delete_batch_related(pk_list)
306
307- update_query = sql.UpdateQuery(cls, connection)
308- for field, model in cls._meta.get_fields_with_model():
309- if (field.rel and field.null and field.rel.to in seen_objs and
310- filter(lambda f: f.column == field.rel.get_related_field().column,
311- field.rel.to._meta.fields)):
312- if model:
313- sql.UpdateQuery(model, connection).clear_related(field,
314- pk_list)
315- else:
316- update_query.clear_related(field, pk_list)
317-
318- # Now delete the actual data.
319 for cls in ordered_classes:
320 items = obj_pairs[cls]
321 items.reverse()
322-
323 pk_list = [pk for pk,instance in items]
324 del_query = sql.DeleteQuery(cls, connection)
325 del_query.delete_batch(pk_list)
326
327- # Last cleanup; set NULLs where there once was a reference to the
328- # object, NULL the primary key of the found objects, and perform
329- # post-notification.
330+ #Last cleanup; set NULLs and default values where there once was a
331+ #reference to the object, NULL the primary key of the found objects,
332+ #and perform post-notification.
333+ for cls, cls_dct in fields_to_set.iteritems():
334+ for instance, field_names_and_values in cls_dct.itervalues():
335+ for field_name, field_value in field_names_and_values.iteritems():
336+ field = cls._meta.get_field_by_name(field_name)[0]
337+ setattr(instance, field.attname, field_value)
338+ for cls in ordered_classes:
339+ items = obj_pairs[cls]
340+ items.reverse()
341 for pk_val, instance in items:
342 for field in cls._meta.fields:
343 if field.rel and field.null and field.rel.to in seen_objs:
344Index: django/db/models/query_utils.py
345===================================================================
346--- django/db/models/query_utils.py (revision 11620)
347+++ django/db/models/query_utils.py (working copy)
348@@ -124,6 +124,56 @@
349 """
350 return self.data.keys()
351
352+class CollectedFields(object):
353+ """
354+ A container that stores the model object and field name
355+ for fields that need to be set to enforce on_delete=SET_NULL
356+ and on_delete=SET_DEFAULT ForeigKey constraints.
357+ """
358+
359+ def __init__(self):
360+ self.data = {}
361+
362+ def add(self, model, pk, obj, field_name, field_value):
363+ """
364+ Adds an item.
365+ model is the class of the object being added,
366+ pk is the primary key, obj is the object itself,
367+ field_name is the name of the field to be set,
368+ field_value is the value it needs to be set to.
369+ """
370+ d = self.data.setdefault(model, SortedDict())
371+ obj, field_names_and_values = d.setdefault(pk, (obj, dict()))
372+ assert field_name not in field_names_and_values or field_names_and_values[field_name] == field_value
373+ field_names_and_values[field_name] = field_value
374+
375+ def __contains__(self, key):
376+ return self.data.__contains__(key)
377+
378+ def __getitem__(self, key):
379+ return self.data[key]
380+
381+ def __nonzero__(self):
382+ return bool(self.data)
383+
384+ def iteritems(self):
385+ return self.data.iteritems()
386+
387+ def iterkeys(self):
388+ return self.data.iterkeys()
389+
390+ def itervalues(self):
391+ return self.data.itervalues()
392+
393+ def items(self):
394+ return self.data.items()
395+
396+ def keys(self):
397+ return self.data.keys()
398+
399+ def values(self):
400+ return self.data.values()
401+
402 class QueryWrapper(object):
403 """
404 A type that indicates the contents are an SQL fragment and the associate
405Index: tests/modeltests/delete/models.py
406===================================================================
407--- tests/modeltests/delete/models.py (revision 11620)
408+++ tests/modeltests/delete/models.py (working copy)
409@@ -46,7 +46,7 @@
410
411 ## First, test the CollectedObjects data structure directly
412
413->>> from django.db.models.query import CollectedObjects
414+>>> from django.db.models.query_utils import CollectedFields, CollectedObjects
415
416 >>> g = CollectedObjects()
417 >>> g.add("key1", 1, "item1", None)
418@@ -112,10 +112,12 @@
419 >>> d1 = D(c=c1, a=a1)
420 >>> d1.save()
421
422->>> o = CollectedObjects()
423->>> a1._collect_sub_objects(o)
424+>>> o, f = CollectedObjects(), CollectedFields()
425+>>> a1._collect_sub_objects(o, f)
426 >>> o.keys()
427 [<class 'modeltests.delete.models.D'>, <class 'modeltests.delete.models.C'>, <class 'modeltests.delete.models.B'>, <class 'modeltests.delete.models.A'>]
428+>>> f.keys()
429+[]
430 >>> a1.delete()
431
432 # Same again with a known bad order
433@@ -131,10 +133,12 @@
434 >>> d2 = D(c=c2, a=a2)
435 >>> d2.save()
436
437->>> o = CollectedObjects()
438->>> a2._collect_sub_objects(o)
439+>>> o, f = CollectedObjects(), CollectedFields()
440+>>> a2._collect_sub_objects(o, f)
441 >>> o.keys()
442 [<class 'modeltests.delete.models.D'>, <class 'modeltests.delete.models.C'>, <class 'modeltests.delete.models.B'>, <class 'modeltests.delete.models.A'>]
443+>>> f.keys()
444+[]
445 >>> a2.delete()
446
447 ### Tests for models E,F - nullable related fields ###
448@@ -163,21 +167,14 @@
449 # Since E.f is nullable, we should delete F first (after nulling out
450 # the E.f field), then E.
451
452->>> o = CollectedObjects()
453->>> e1._collect_sub_objects(o)
454+>>> o, f = CollectedObjects(), CollectedFields()
455+>>> e1._collect_sub_objects(o, f)
456 >>> o.keys()
457 [<class 'modeltests.delete.models.F'>, <class 'modeltests.delete.models.E'>]
458+>>> f.keys()
459+[<class 'modeltests.delete.models.E'>]
460
461-# temporarily replace the UpdateQuery class to verify that E.f is actually nulled out first
462->>> import django.db.models.sql
463->>> class LoggingUpdateQuery(django.db.models.sql.UpdateQuery):
464-... def clear_related(self, related_field, pk_list):
465-... print "CLEARING FIELD",related_field.name
466-... return super(LoggingUpdateQuery, self).clear_related(related_field, pk_list)
467->>> original_class = django.db.models.sql.UpdateQuery
468->>> django.db.models.sql.UpdateQuery = LoggingUpdateQuery
469 >>> e1.delete()
470-CLEARING FIELD f
471
472 >>> e2 = E()
473 >>> e2.save()
474@@ -188,15 +185,13 @@
475
476 # Same deal as before, though we are starting from the other object.
477
478->>> o = CollectedObjects()
479->>> f2._collect_sub_objects(o)
480+>>> o, f = CollectedObjects(), CollectedFields()
481+>>> f2._collect_sub_objects(o, f)
482 >>> o.keys()
483-[<class 'modeltests.delete.models.F'>, <class 'modeltests.delete.models.E'>]
484+[<class 'modeltests.delete.models.F'>]
485+>>> f.keys()
486+[<class 'modeltests.delete.models.E'>]
487
488 >>> f2.delete()
489-CLEARING FIELD f
490-
491-# Put this back to normal
492->>> django.db.models.sql.UpdateQuery = original_class
493 """
494 }
495MJG-MBP:django_on_delete_patch mjg$
Back to Top