mirror of
https://github.com/django/django.git
synced 2025-11-07 07:15:35 +00:00
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.
306 lines
11 KiB
Python
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), [])
|