1
0
mirror of https://github.com/django/django.git synced 2025-11-07 07:15:35 +00:00
Files
django/tests/composite_pk/test_checks.py
Hal Blackburn 74564946c3 Fixed #36704 -- Fixed system check error for proxy model with a composite pk.
Proxy models subclassing a model with a CompositePrimaryKey were
incorrectly reporting check errors because the check that requires only
local fields to be used in a composite pk was evaluated against the proxy
subclass, which has no fields.

To fix this, composite pk field checks are not evaluated against
proxy subclasses, as none of the checks are applicable to proxy
subclasses. This also has the benefit of not double-reporting real check
errors from an invalid superclass pk.

Thanks Clifford Gama for the review.
2025-11-04 11:59:21 -05:00

306 lines
11 KiB
Python

from django.core import checks
from django.db import connection, models
from django.db.models import F
from django.test import TestCase, skipUnlessAnyDBFeature
from django.test.utils import isolate_apps
@isolate_apps("composite_pk")
class CompositePKChecksTests(TestCase):
maxDiff = None
def test_composite_pk_must_be_unique_strings(self):
test_cases = (
(),
(0,),
(1,),
("id", False),
("id", "id"),
(("id",),),
)
for i, args in enumerate(test_cases):
with (
self.subTest(args=args),
self.assertRaisesMessage(
ValueError, "CompositePrimaryKey args must be unique strings."
),
):
models.CompositePrimaryKey(*args)
def test_composite_pk_must_include_at_least_2_fields(self):
expected_message = "CompositePrimaryKey must include at least two fields."
with self.assertRaisesMessage(ValueError, expected_message):
models.CompositePrimaryKey("id")
def test_composite_pk_cannot_have_a_default(self):
expected_message = "CompositePrimaryKey cannot have a default."
with self.assertRaisesMessage(ValueError, expected_message):
models.CompositePrimaryKey("tenant_id", "id", default=(1, 1))
def test_composite_pk_cannot_have_a_database_default(self):
expected_message = "CompositePrimaryKey cannot have a database default."
with self.assertRaisesMessage(ValueError, expected_message):
models.CompositePrimaryKey("tenant_id", "id", db_default=models.F("id"))
def test_composite_pk_cannot_have_a_db_column(self):
expected_message = "CompositePrimaryKey cannot have a db_column."
with self.assertRaisesMessage(ValueError, expected_message):
models.CompositePrimaryKey("tenant_id", "id", db_column="tenant_pk")
def test_composite_pk_cannot_be_editable(self):
expected_message = "CompositePrimaryKey cannot be editable."
with self.assertRaisesMessage(ValueError, expected_message):
models.CompositePrimaryKey("tenant_id", "id", editable=True)
def test_composite_pk_must_be_a_primary_key(self):
expected_message = "CompositePrimaryKey must be a primary key."
with self.assertRaisesMessage(ValueError, expected_message):
models.CompositePrimaryKey("tenant_id", "id", primary_key=False)
def test_composite_pk_must_be_blank(self):
expected_message = "CompositePrimaryKey must be blank."
with self.assertRaisesMessage(ValueError, expected_message):
models.CompositePrimaryKey("tenant_id", "id", blank=False)
def test_composite_pk_must_not_have_other_pk_field(self):
class Foo(models.Model):
pk = models.CompositePrimaryKey("foo_id", "id")
foo_id = models.IntegerField()
id = models.IntegerField(primary_key=True)
self.assertEqual(
Foo.check(databases=self.databases),
[
checks.Error(
"The model cannot have more than one field with "
"'primary_key=True'.",
obj=Foo,
id="models.E026",
),
],
)
def test_composite_pk_cannot_include_nullable_field(self):
class Foo(models.Model):
pk = models.CompositePrimaryKey("foo_id", "id")
foo_id = models.IntegerField()
id = models.IntegerField(null=True)
self.assertEqual(
Foo.check(databases=self.databases),
[
checks.Error(
"'id' cannot be included in the composite primary key.",
hint="'id' field may not set 'null=True'.",
obj=Foo,
id="models.E042",
),
],
)
def test_composite_pk_can_include_fk_name(self):
class Foo(models.Model):
pass
class Bar(models.Model):
pk = models.CompositePrimaryKey("foo", "id")
foo = models.ForeignKey(Foo, on_delete=models.CASCADE)
id = models.SmallIntegerField()
self.assertEqual(Foo.check(databases=self.databases), [])
self.assertEqual(Bar.check(databases=self.databases), [])
def test_composite_pk_cannot_include_same_field(self):
class Foo(models.Model):
pass
class Bar(models.Model):
pk = models.CompositePrimaryKey("foo", "foo_id")
foo = models.ForeignKey(Foo, on_delete=models.CASCADE)
id = models.SmallIntegerField()
self.assertEqual(Foo.check(databases=self.databases), [])
self.assertEqual(
Bar.check(databases=self.databases),
[
checks.Error(
"'foo_id' cannot be included in the composite primary key.",
hint="'foo_id' and 'foo' are the same fields.",
obj=Bar,
id="models.E042",
),
],
)
def test_composite_pk_cannot_include_composite_pk_field(self):
class Foo(models.Model):
pk = models.CompositePrimaryKey("id", "pk")
id = models.SmallIntegerField()
self.assertEqual(
Foo.check(databases=self.databases),
[
checks.Error(
"'pk' cannot be included in the composite primary key.",
hint="'pk' field has no column.",
obj=Foo,
id="models.E042",
),
],
)
def test_composite_pk_cannot_include_db_column(self):
class Foo(models.Model):
pk = models.CompositePrimaryKey("foo", "bar")
foo = models.SmallIntegerField(db_column="foo_id")
bar = models.SmallIntegerField(db_column="bar_id")
class Bar(models.Model):
pk = models.CompositePrimaryKey("foo_id", "bar_id")
foo = models.SmallIntegerField(db_column="foo_id")
bar = models.SmallIntegerField(db_column="bar_id")
self.assertEqual(Foo.check(databases=self.databases), [])
self.assertEqual(
Bar.check(databases=self.databases),
[
checks.Error(
"'foo_id' cannot be included in the composite primary key.",
hint="'foo_id' is not a valid field.",
obj=Bar,
id="models.E042",
),
checks.Error(
"'bar_id' cannot be included in the composite primary key.",
hint="'bar_id' is not a valid field.",
obj=Bar,
id="models.E042",
),
],
)
def test_foreign_object_can_refer_composite_pk(self):
class Foo(models.Model):
pass
class Bar(models.Model):
pk = models.CompositePrimaryKey("foo_id", "id")
foo = models.ForeignKey(Foo, on_delete=models.CASCADE)
id = models.IntegerField()
class Baz(models.Model):
pk = models.CompositePrimaryKey("foo_id", "id")
foo = models.ForeignKey(Foo, on_delete=models.CASCADE)
id = models.IntegerField()
bar_id = models.IntegerField()
bar = models.ForeignObject(
Bar,
on_delete=models.CASCADE,
from_fields=("foo_id", "bar_id"),
to_fields=("foo_id", "id"),
)
self.assertEqual(Foo.check(databases=self.databases), [])
self.assertEqual(Bar.check(databases=self.databases), [])
self.assertEqual(Baz.check(databases=self.databases), [])
def test_composite_pk_must_be_named_pk(self):
class Foo(models.Model):
primary_key = models.CompositePrimaryKey("foo_id", "id")
foo_id = models.IntegerField()
id = models.IntegerField()
self.assertEqual(
Foo.check(databases=self.databases),
[
checks.Error(
"'CompositePrimaryKey' must be named 'pk'.",
obj=Foo._meta.get_field("primary_key"),
id="fields.E013",
),
],
)
@skipUnlessAnyDBFeature(
"supports_virtual_generated_columns",
"supports_stored_generated_columns",
)
def test_composite_pk_cannot_include_generated_field(self):
class Foo(models.Model):
pk = models.CompositePrimaryKey("id", "foo")
id = models.IntegerField()
foo = models.GeneratedField(
expression=F("id"),
output_field=models.IntegerField(),
db_persist=connection.features.supports_stored_generated_columns,
)
self.assertEqual(
Foo.check(databases=self.databases),
[
checks.Error(
"'foo' cannot be included in the composite primary key.",
hint="'foo' field is a generated field.",
obj=Foo,
id="models.E042",
),
],
)
def test_composite_pk_cannot_include_non_local_field(self):
class Foo(models.Model):
a = models.SmallIntegerField()
class Bar(Foo):
pk = models.CompositePrimaryKey("a", "b")
b = models.SmallIntegerField()
self.assertEqual(Foo.check(databases=self.databases), [])
self.assertEqual(
Bar.check(databases=self.databases),
[
checks.Error(
"'a' cannot be included in the composite primary key.",
hint="'a' field is not a local field.",
obj=Bar,
id="models.E042",
),
],
)
def test_proxy_model_can_subclass_model_with_composite_pk(self):
class Foo(models.Model):
pk = models.CompositePrimaryKey("a", "b")
a = models.SmallIntegerField()
b = models.SmallIntegerField()
class Bar(Foo):
class Meta:
proxy = True
self.assertEqual(Foo.check(databases=self.databases), [])
self.assertEqual(Bar.check(databases=self.databases), [])
def test_proxy_model_does_not_check_superclass_composite_pk_errors(self):
class Foo(models.Model):
pk = models.CompositePrimaryKey("a", "b")
a = models.SmallIntegerField()
class Bar(Foo):
class Meta:
proxy = True
self.assertEqual(
Foo.check(databases=self.databases),
[
checks.Error(
"'b' cannot be included in the composite primary key.",
hint="'b' is not a valid field.",
obj=Foo,
id="models.E042",
),
],
)
self.assertEqual(Bar.check(databases=self.databases), [])