#36611 new Bug

Model validation of constraint involving ForeignObject considers only first column

Reported by: Jacob Walls Owned by:
Component: Database layer (models, ORM) Version: 5.2
Severity: Normal Keywords:
Cc: Triage Stage: Unreviewed
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

Discovered during #36580 in review.

Similar to #36431, where only the first column of a ForeignObject was considered in values(), only the first column is considered during model validation of constraints.

Composite PK's are not affected, because they raise system checks if you try to use them in a constraint. But ForeignObject has been broken since its introduction in this regard. Reproduced on 5.2, but thus, not a release blocker.

Rough test (needs adjusting to avoid hijacking this model and unnecessarily skipping tests on backends not supporting constraints):

  • tests/composite_pk/models/tenant.py

    diff --git a/tests/composite_pk/models/tenant.py b/tests/composite_pk/models/tenant.py
    index 65eb0feae8..c818ec4de7 100644
    a b class Comment(models.Model):  
    4848    text = models.TextField(default="", blank=True)
    4949    integer = models.IntegerField(default=0)
    5050
     51    class Meta:
     52        # TODO: use new model instead
     53        required_db_features = {"supports_table_check_constraints"}
     54        constraints = [
     55            models.CheckConstraint(
     56                condition=models.Q(user__lt=(1000, 1000)),
     57                name="user_limit",
     58            ),
     59        ]
     60
    5161
    5262class Post(models.Model):
    5363    pk = models.CompositePrimaryKey("tenant_id", "id")
  • tests/composite_pk/test_models.py

    diff --git a/tests/composite_pk/test_models.py b/tests/composite_pk/test_models.py
    index 27157a52ad..05aafd5306 100644
    a b  
    11from django.contrib.contenttypes.models import ContentType
    22from django.core.exceptions import ValidationError
     3from django.db import connection
    34from django.test import TestCase
     5from django.test.utils import CaptureQueriesContext
    46
    57from .models import Comment, Tenant, Token, User
    68
    class CompositePKModelsTests(TestCase):  
    119121                self.assertSequenceEqual(ctx.exception.messages, messages)
    120122
    121123    def test_full_clean_update(self):
    122         with self.assertNumQueries(1):
     124        with CaptureQueriesContext(connection) as ctx:
     125            self.comment_1.full_clean()
     126        select_queries = [
     127            query["sql"]
     128            for query in ctx.captured_queries
     129            if "select" in query["sql"].lower()
     130        ]
     131        self.assertEqual(len(select_queries), 2, select_queries)  # 1 on 5.2.x
     132
     133    def test_full_clean_update_invalid(self):
     134        self.comment_1.tenant_id = 1001
     135        with self.assertRaises(ValidationError):
     136            self.comment_1.full_clean()
     137
     138        self.comment_1.tenant_id = 1
     139        self.comment_1.user_id = 1001
     140        with self.assertRaises(ValidationError):
    123141            self.comment_1.full_clean()
    124142
    125143    def test_field_conflicts(self):
  • tests/composite_pk/tests.py

    diff --git a/tests/composite_pk/tests.py b/tests/composite_pk/tests.py
    index 2245a472e4..5b7e34a0bc 100644
    a b class CompositePKTests(TestCase):  
    187187            self.assertEqual(user.email, self.user.email)
    188188
    189189    def test_select_related(self):
    190         Comment.objects.create(tenant=self.tenant, id=2)
     190        user2 = User.objects.create(
     191            tenant=self.tenant,
     192            id=2,
     193            email="user0002@example.com",
     194        )
     195        Comment.objects.create(tenant=self.tenant, id=2, user=user2)
    191196        with self.assertNumQueries(1):
    192197            comments = list(Comment.objects.select_related("user").order_by("pk"))
    193198            self.assertEqual(len(comments), 2)
    194199            self.assertEqual(comments[0].user, self.user)
    195             self.assertIsNone(comments[1].user)
     200            self.assertEqual(comments[1].user, user2)
    196201
    197202    def test_model_forms(self):
    198203        fields = ["tenant", "id", "user_id", "text", "integer"]

Notice 1001 only appears in the first query of the test_full_clean_update_invalid.

FAIL: test_full_clean_update_invalid (composite_pk.test_models.CompositePKModelsTests.test_full_clean_update_invalid)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/jwalls/django/tests/composite_pk/test_models.py", line 140, in test_full_clean_update_invalid
    with self.assertRaises(ValidationError):
         ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^
AssertionError: ValidationError not raised

----------------------------------------------------------------------
(0.000)
SELECT 1 AS "a"
FROM "composite_pk_tenant"
WHERE "composite_pk_tenant"."id" = 1001
LIMIT 1;

args=(1,
      1001);

ALIAS=DEFAULT (0.000)
SELECT 1 AS "a"
FROM "composite_pk_tenant"
WHERE "composite_pk_tenant"."id" = 1
LIMIT 1;

args=(1,
      1);

ALIAS=DEFAULT
----------------------------------------------------------------------
Ran 2 tests in 0.003s

FAILED (failures=1)

Change History (0)

Note: See TracTickets for help on using tickets.
Back to Top