1
0
mirror of https://github.com/django/django.git synced 2025-10-27 15:46:10 +00:00

Fixed #21961 -- Added support for database-level delete options for ForeignKey.

Thanks Simon Charette for pair programming.

Co-authored-by: Nick Stefan <NickStefan12@gmail.com>
Co-authored-by: Akash Kumar Sen <71623442+Akash-Kumar-Sen@users.noreply.github.com>
Co-authored-by: Simon Charette <charette.s@gmail.com>
This commit is contained in:
Mariusz Felisiak
2025-10-18 15:03:50 +02:00
committed by GitHub
parent b1e0262c9f
commit 0c487aa3a7
33 changed files with 838 additions and 67 deletions

View File

@@ -3,7 +3,8 @@ from unittest import mock
from django.core.checks import Error
from django.core.checks import Warning as DjangoWarning
from django.db import connection, models
from django.test.testcases import SimpleTestCase
from django.test import skipUnlessDBFeature
from django.test.testcases import SimpleTestCase, TestCase
from django.test.utils import isolate_apps, modify_settings, override_settings
@@ -751,6 +752,29 @@ class RelativeFieldTests(SimpleTestCase):
],
)
def test_on_delete_db_set_null_on_non_nullable_field(self):
class Person(models.Model):
pass
class Model(models.Model):
foreign_key = models.ForeignKey("Person", models.DB_SET_NULL)
field = Model._meta.get_field("foreign_key")
self.assertEqual(
field.check(),
[
Error(
"Field specifies on_delete=DB_SET_NULL, but cannot be null.",
hint=(
"Set null=True argument on the field, or change the on_delete "
"rule."
),
obj=field,
id="fields.E320",
),
],
)
def test_on_delete_set_default_without_default_value(self):
class Person(models.Model):
pass
@@ -2259,3 +2283,175 @@ class M2mThroughFieldsTests(SimpleTestCase):
),
],
)
@isolate_apps("invalid_models_tests")
class DatabaseLevelOnDeleteTests(TestCase):
def test_db_set_default_support(self):
class Parent(models.Model):
pass
class Child(models.Model):
parent = models.ForeignKey(
Parent, models.DB_SET_DEFAULT, db_default=models.Value(1)
)
field = Child._meta.get_field("parent")
expected = (
[]
if connection.features.supports_on_delete_db_default
else [
Error(
f"{connection.display_name} does not support a DB_SET_DEFAULT.",
hint="Change the on_delete rule to SET_DEFAULT.",
obj=field,
id="fields.E324",
)
]
)
self.assertEqual(field.check(databases=self.databases), expected)
def test_db_set_default_required_db_features(self):
class Parent(models.Model):
pass
class Child(models.Model):
parent = models.ForeignKey(
Parent, models.DB_SET_DEFAULT, db_default=models.Value(1)
)
class Meta:
required_db_features = {"supports_on_delete_db_default"}
field = Child._meta.get_field("parent")
self.assertEqual(field.check(databases=self.databases), [])
@skipUnlessDBFeature("supports_on_delete_db_default")
def test_db_set_default_no_db_default(self):
class Parent(models.Model):
pass
class Child(models.Model):
parent = models.ForeignKey(Parent, models.DB_SET_DEFAULT)
field = Child._meta.get_field("parent")
self.assertEqual(
field.check(databases=self.databases),
[
Error(
"Field specifies on_delete=DB_SET_DEFAULT, but has no db_default "
"value.",
hint="Set a db_default value, or change the on_delete rule.",
obj=field,
id="fields.E322",
)
],
)
def test_python_db_chain(self):
class GrandParent(models.Model):
pass
class Parent(models.Model):
grand_parent = models.ForeignKey(GrandParent, models.DB_CASCADE)
class Child(models.Model):
parent = models.ForeignKey(Parent, models.RESTRICT)
field = Child._meta.get_field("parent")
self.assertEqual(
field.check(databases=self.databases),
[
Error(
"Field specifies Python-level on_delete variant, but referenced "
"model uses database-level variant.",
hint=(
"Use either database or Python on_delete variants uniformly in "
"the references chain."
),
obj=field,
id="fields.E323",
)
],
)
def test_db_python_chain(self):
class GrandParent(models.Model):
pass
class Parent(models.Model):
grand_parent = models.ForeignKey(GrandParent, models.CASCADE)
class Child(models.Model):
parent = models.ForeignKey(Parent, models.DB_SET_NULL, null=True)
field = Child._meta.get_field("parent")
self.assertEqual(
field.check(databases=self.databases),
[
Error(
"Field specifies database-level on_delete variant, but referenced "
"model uses Python-level variant.",
hint=(
"Use either database or Python on_delete variants uniformly in "
"the references chain."
),
obj=field,
id="fields.E323",
)
],
)
def test_db_python_chain_auto_created(self):
class GrandParent(models.Model):
pass
class Parent(GrandParent):
pass
class Child(models.Model):
parent = models.ForeignKey(Parent, on_delete=models.DB_CASCADE)
field = Child._meta.get_field("parent")
self.assertEqual(
field.check(databases=self.databases),
[
Error(
"Field specifies database-level on_delete variant, but referenced "
"model uses Python-level variant.",
hint=(
"Use either database or Python on_delete variants uniformly in "
"the references chain."
),
obj=field,
id="fields.E323",
)
],
)
def test_db_do_nothing_chain(self):
class GrandParent(models.Model):
pass
class Parent(models.Model):
grand_parent = models.ForeignKey(GrandParent, models.DO_NOTHING)
class Child(models.Model):
parent = models.ForeignKey(Parent, models.DB_SET_NULL, null=True)
field = Child._meta.get_field("parent")
self.assertEqual(field.check(databases=self.databases), [])
def test_do_nothing_db_chain(self):
class GrandParent(models.Model):
pass
class Parent(models.Model):
grand_parent = models.ForeignKey(GrandParent, models.DB_SET_NULL, null=True)
class Child(models.Model):
parent = models.ForeignKey(Parent, models.DO_NOTHING)
field = Child._meta.get_field("parent")
self.assertEqual(field.check(databases=self.databases), [])