Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -672,6 +672,7 @@ answer newbie questions, and generally made Django that much better:
Nick Presta <[email protected]>
Nick Sandford <[email protected]>
Nick Sarbicki <[email protected]>
Nick Stefan
Niclas Olofsson <[email protected]>
Nicola Larosa <[email protected]>
Nicolas Lara <[email protected]>
Expand Down
11 changes: 11 additions & 0 deletions django/db/backends/base/operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,17 @@ def deferrable_sql(self):
"""
return ''

def fk_on_delete_sql(self, operation):
"""
Return the SQL to make an ON DELETE statement during a CREATE TABLE
statement.
"""
if operation in ['CASCADE', 'SET NULL', 'RESTRICT']:
return ' ON DELETE %s ' % operation
if operation == '':
return ''
raise NotImplementedError('ON DELETE %s is not supported.' % operation)

def distinct_sql(self, fields, params):
"""
Return an SQL DISTINCT clause which removes duplicate rows from the
Expand Down
10 changes: 9 additions & 1 deletion django/db/backends/base/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ class BaseDatabaseSchemaEditor:

sql_create_fk = (
"ALTER TABLE %(table)s ADD CONSTRAINT %(name)s FOREIGN KEY (%(column)s) "
"REFERENCES %(to_table)s (%(to_column)s)%(deferrable)s"
"REFERENCES %(to_table)s (%(to_column)s)%(on_delete)s%(deferrable)s"
)
sql_create_inline_fk = None
sql_create_column_inline_fk = None
Expand Down Expand Up @@ -176,6 +176,7 @@ def table_sql(self, model):
definition += ' ' + self.sql_create_inline_fk % {
'to_table': self.quote_name(to_table),
'to_column': self.quote_name(to_column),
'on_delete': self._create_on_delete_sql(model, field),
}
elif self.connection.features.supports_foreign_keys:
self.deferred_sql.append(self._create_fk_sql(model, field, '_fk_%(to_table)s_%(to_column)s'))
Expand Down Expand Up @@ -1032,6 +1033,12 @@ def _rename_field_sql(self, table, old_field, new_field, new_type):
"type": new_type,
}

def _create_on_delete_sql(self, model, field):
on_delete = getattr(field.remote_field, 'on_delete', None)
if on_delete and on_delete.with_db:
return on_delete.as_sql(self.connection)
return ''

def _create_fk_sql(self, model, field, suffix):
table = Table(model._meta.db_table, self.quote_name)
name = self._fk_constraint_name(model, field, suffix)
Expand All @@ -1047,6 +1054,7 @@ def _create_fk_sql(self, model, field, suffix):
to_table=to_table,
to_column=to_column,
deferrable=deferrable,
on_delete=self._create_on_delete_sql(model, field),
)

def _fk_constraint_name(self, model, field, suffix):
Expand Down
5 changes: 4 additions & 1 deletion django/db/backends/sqlite3/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):

sql_delete_table = "DROP TABLE %(table)s"
sql_create_fk = None
sql_create_inline_fk = "REFERENCES %(to_table)s (%(to_column)s) DEFERRABLE INITIALLY DEFERRED"
sql_create_inline_fk = (
"REFERENCES %(to_table)s (%(to_column)s) "
"%(on_delete)sDEFERRABLE INITIALLY DEFERRED"
)
sql_create_unique = "CREATE UNIQUE INDEX %(name)s ON %(table)s (%(columns)s)"
sql_delete_unique = "DROP INDEX %(name)s"

Expand Down
10 changes: 10 additions & 0 deletions django/db/migrations/serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,15 @@ def serialize(self):
return string.rstrip(','), imports


class OnDeleteSerializer(BaseSerializer):
def serialize(self):
if self.value.value is None:
return 'models.%s' % (self.value.name), {}
if self.value:
return 'models.%s(%s)' % (self.value.name, self.value.value), {}
return None


class RegexSerializer(BaseSerializer):
def serialize(self):
regex_pattern, pattern_imports = serializer_factory(self.value.pattern).serialize()
Expand Down Expand Up @@ -298,6 +307,7 @@ class Serializer:
collections.abc.Iterable: IterableSerializer,
(COMPILED_REGEX_TYPE, RegexObject): RegexSerializer,
uuid.UUID: UUIDSerializer,
models.OnDelete: OnDeleteSerializer,
}

@classmethod
Expand Down
10 changes: 6 additions & 4 deletions django/db/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
from django.db.models.constraints import * # NOQA
from django.db.models.constraints import __all__ as constraints_all
from django.db.models.deletion import (
CASCADE, DO_NOTHING, PROTECT, RESTRICT, SET, SET_DEFAULT, SET_NULL,
ProtectedError, RestrictedError,
CASCADE, DB_CASCADE, DB_RESTRICT, DB_SET_NULL, DO_NOTHING, PROTECT,
RESTRICT, SET, SET_DEFAULT, SET_NULL, OnDelete, ProtectedError,
RestrictedError,
)
from django.db.models.enums import * # NOQA
from django.db.models.enums import __all__ as enums_all
Expand Down Expand Up @@ -37,8 +38,9 @@
__all__ = aggregates_all + constraints_all + enums_all + fields_all + indexes_all
__all__ += [
'ObjectDoesNotExist', 'signals',
'CASCADE', 'DO_NOTHING', 'PROTECT', 'RESTRICT', 'SET', 'SET_DEFAULT',
'SET_NULL', 'ProtectedError', 'RestrictedError',
'CASCADE', 'DB_CASCADE', 'DB_RESTRICT', 'DB_SET_NULL', 'DO_NOTHING',
'PROTECT', 'RESTRICT', 'SET', 'SET_DEFAULT', 'SET_NULL', 'OnDelete',
'ProtectedError', 'RestrictedError',
'Case', 'Exists', 'Expression', 'ExpressionList', 'ExpressionWrapper', 'F',
'Func', 'OrderBy', 'OuterRef', 'RowRange', 'Subquery', 'Value',
'ValueRange', 'When',
Expand Down
62 changes: 53 additions & 9 deletions django/db/models/deletion.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,25 @@ def __init__(self, msg, restricted_objects):
super().__init__(msg, restricted_objects)


def CASCADE(collector, field, sub_objs, using):
class OnDelete:
def __init__(self, name, operation, value=None):
self.name = name
self.operation = operation
self.value = value
if not self.with_db and not callable(self.operation):
raise TypeError('operation should be callable')
if self.with_db and callable(self.operation):
raise TypeError('operation should be string not callable')

def __call__(self, *args, **kwargs):
return self.operation(*args, **kwargs)

@property
def with_db(self):
return hasattr(self, 'as_sql')


def application_cascade(collector, field, sub_objs, using):
collector.collect(
sub_objs, source=field.remote_field.model, source_attr=field.name,
nullable=field.null, fail_on_restricted=False,
Expand All @@ -29,7 +47,7 @@ def CASCADE(collector, field, sub_objs, using):
collector.add_field_update(field, None, sub_objs)


def PROTECT(collector, field, sub_objs, using):
def application_protect(collector, field, sub_objs, using):
raise ProtectedError(
"Cannot delete some instances of model '%s' because they are "
"referenced through a protected foreign key: '%s.%s'" % (
Expand All @@ -39,34 +57,56 @@ def PROTECT(collector, field, sub_objs, using):
)


def RESTRICT(collector, field, sub_objs, using):
def application_restrict(collector, field, sub_objs, using):
collector.add_restricted_objects(field, sub_objs)
collector.add_dependency(field.remote_field.model, field.model)


def SET(value):
def application_set(value):
if callable(value):
def set_on_delete(collector, field, sub_objs, using):
collector.add_field_update(field, value(), sub_objs)
else:
def set_on_delete(collector, field, sub_objs, using):
collector.add_field_update(field, value, sub_objs)
set_on_delete.deconstruct = lambda: ('django.db.models.SET', (value,), {})
return set_on_delete
return OnDelete('SET', set_on_delete, value)


def SET_NULL(collector, field, sub_objs, using):
def application_set_null(collector, field, sub_objs, using):
collector.add_field_update(field, None, sub_objs)


def SET_DEFAULT(collector, field, sub_objs, using):
def application_set_default(collector, field, sub_objs, using):
collector.add_field_update(field, field.get_default(), sub_objs)


def DO_NOTHING(collector, field, sub_objs, using):
def application_do_nothing(collector, field, sub_objs, using):
pass


CASCADE = OnDelete('CASCADE', application_cascade)
DO_NOTHING = OnDelete('DO_NOTHING', application_do_nothing)
PROTECT = OnDelete('PROTECT', application_protect)
RESTRICT = OnDelete('RESTRICT', application_restrict)
SET = OnDelete('SET', application_set)
SET_NULL = OnDelete('SET_NULL', application_set_null)
SET_DEFAULT = OnDelete('SET_DEFAULT', application_set_default)


class DatabaseOnDelete(OnDelete):
def __call__(self, collector, field, sub_objs, using):
raise TypeError('operation should be string not callable')

def as_sql(self, connection):
return connection.ops.fk_on_delete_sql(self.operation)


DB_CASCADE = DatabaseOnDelete('DB_CASCADE', 'CASCADE')
DB_RESTRICT = DatabaseOnDelete('DB_RESTRICT', 'RESTRICT')
DB_SET_NULL = DatabaseOnDelete('DB_SET_NULL', 'SET NULL')


def get_candidate_relations_to_delete(opts):
# The candidate relations are the ones that come from N-1 and 1-1 relations.
# N-N (i.e., many-to-many) relations aren't candidates for deletion.
Expand Down Expand Up @@ -190,6 +230,7 @@ def can_fast_delete(self, objs, from_field=None):
all(link == from_field for link in opts.concrete_model._meta.parents.values()) and
# Foreign keys pointing to this model.
all(
related.field.remote_field.on_delete.with_db or
related.field.remote_field.on_delete is DO_NOTHING
for related in get_candidate_relations_to_delete(opts)
) and (
Expand Down Expand Up @@ -271,7 +312,10 @@ def collect(self, objs, source=None, nullable=False, collect_related=True,
if keep_parents and related.model in parents:
continue
field = related.field
if field.remote_field.on_delete == DO_NOTHING:
if (
field.remote_field.on_delete.with_db or
field.remote_field.on_delete == DO_NOTHING
):
continue
related_model = related.related_model
if self.can_fast_delete(related_model, from_field=field):
Expand Down
56 changes: 55 additions & 1 deletion django/db/models/fields/related.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@
from django.db.backends import utils
from django.db.models import Q
from django.db.models.constants import LOOKUP_SEP
from django.db.models.deletion import CASCADE, SET_DEFAULT, SET_NULL
from django.db.models.deletion import (
CASCADE, DB_CASCADE, DB_RESTRICT, DB_SET_NULL, RESTRICT, SET_DEFAULT,
SET_NULL,
)
from django.db.models.query_utils import PathInfo
from django.db.models.utils import make_model_tuple
from django.utils.functional import cached_property
Expand Down Expand Up @@ -854,6 +857,57 @@ def _check_on_delete(self):
id='fields.E321',
)
]
elif on_delete in [DB_CASCADE, DB_RESTRICT, DB_SET_NULL] and (
any( # generic relation
hasattr(field, 'bulk_related_objects') for field in
self.model._meta.private_fields
) or
any( # generic foreign key
hasattr(field, 'get_content_type') for field in
self.model._meta.private_fields
)
):
return [
checks.Error(
'Field specifies unsupported on_delete=DB_* on model '
'declaring a GenericForeignKey.',
hint='Change the on_delete rule.',
obj=self,
id='fields.E345',
)
]
elif on_delete in [DB_CASCADE, DB_RESTRICT, DB_SET_NULL] and len(
# multi table inheritance
self.model._meta.concrete_model._meta.parents.values()
):
return [
checks.Error(
'Field specifies unsupported on_delete=DB_* on multi-table'
' inherited model.',
hint='Change the on_delete rule.',
obj=self,
id='fields.E345',
)
]
elif on_delete in [CASCADE, RESTRICT, SET_DEFAULT, SET_NULL] and (
any( # field_A <- CASCADE <- field_B <- DB_CASCADE <- field_C
getattr(field_B.remote_field, 'on_delete', None) and
getattr(field_B.remote_field, 'on_delete') in [
DB_CASCADE, DB_RESTRICT, DB_SET_NULL
]
for field_B in self.remote_field.model._meta.fields
)
):
return [
checks.Error(
'Field specifies unsupported on_delete relation with a '
'model also using an on_delete=DB_* relation.',
hint='Change the on_delete rule so that DB_* relations '
'point to models using DB_* or DO_NOTHING relations.',
obj=self,
id='fields.E345',
)
]
else:
return []

Expand Down
2 changes: 2 additions & 0 deletions docs/ref/checks.txt
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,8 @@ Related fields
with a ``through`` model.
* **fields.W344**: The field's intermediary table ``<table name>`` clashes with
the table name of ``<model>``/``<model>.<field name>``.
* **fields.E345**: Field specifies unsupported ``on_delete=<action>`` model
relation. This would interfere with other expected model behaviors.

Models
------
Expand Down
25 changes: 25 additions & 0 deletions docs/ref/models/fields.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1550,6 +1550,31 @@ The possible values for :attr:`~ForeignKey.on_delete` are found in
integrity, this will cause an :exc:`~django.db.IntegrityError` unless
you manually add an SQL ``ON DELETE`` constraint to the database field.

* .. attribute:: DB_CASCADE

.. versionadded:: 3.1

Django takes no direct action, behaves exactly like ``DO_NOTHING``,
but also sets ``ON DELETE CASCADE`` as a SQL constraint. The cascade
deletion then happens at the database level.

* .. attribute:: DB_SET_NULL

.. versionadded:: 3.1

Django takes no direct action, behaves exactly like ``DO_NOTHING``, but
also adds ``ON DELETE SET NULL`` as a SQL constraint. The setting of
a ``NULL`` value then happens at the database level.

* .. attribute:: DB_RESTRICT

.. versionadded:: 3.1

Django takes no direct action, behaves exactly like ``DO_NOTHING``, but
also adds ``ON DELETE RESTRICT`` as a SQL constraint. The immediate
check of referential integrity (e.g. no longer deferred)
happens at the database level.

.. attribute:: ForeignKey.limit_choices_to

Sets a limit to the available choices for this field when this field is
Expand Down
5 changes: 5 additions & 0 deletions docs/releases/3.1.txt
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,11 @@ Models
:class:`~django.db.models.DateTimeField`, and the new :lookup:`iso_week_day`
lookup allows querying by an ISO-8601 day of week.

* The ``on_delete`` argument for ``ForeignKey`` and ``OneToOneField`` now accepts
``DB_CASCADE``, ``DB_SET_NULL``, and ``DB_RESTRICT``. These
will behave like ``DO_NOTHING`` in Django, but leverage the ``ON DELETE`` SQL
constraint equivalents.

* :meth:`.QuerySet.explain` now supports:

* ``TREE`` format on MySQL 8.0.16+,
Expand Down
12 changes: 12 additions & 0 deletions tests/delete/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,18 @@ class SecondReferrer(models.Model):
)


class BaseDbCascade(models.Model):
pass


class RelToBaseDbCascade(models.Model):
name = models.CharField(max_length=30)

db_cascade = models.ForeignKey(BaseDbCascade, models.DB_CASCADE, null=True, related_name='db_cascade_set')
db_set_null = models.ForeignKey(BaseDbCascade, models.DB_SET_NULL, null=True, related_name='db_set_null_set')
db_restrict = models.ForeignKey(BaseDbCascade, models.DB_RESTRICT, null=True, related_name='db_restrict_set')


class DeleteTop(models.Model):
b1 = GenericRelation('GenericB1')
b2 = GenericRelation('GenericB2')
Expand Down
Loading