#36611: Model validation of constraint involving ForeignObject considers only first column -------------------------------------+------------------------------------- Reporter: Jacob Walls | Owner: (none) Type: Bug | Status: new Component: Database layer | Version: 5.2 (models, ORM) | Severity: Release blocker | Resolution: Keywords: | Triage Stage: Accepted Has patch: 0 | Needs documentation: 0 Needs tests: 0 | Patch needs improvement: 0 Easy pickings: 0 | UI/UX: 0 -------------------------------------+------------------------------------- Comment (by Sarah Boyce):
Note that I have written a test {{{#!diff --- a/tests/foreign_object/models/__init__.py +++ b/tests/foreign_object/models/__init__.py @@ -1,5 +1,5 @@ from .article import Article, ArticleIdea, ArticleTag, ArticleTranslation, NewsArticle -from .customers import Address, Contact, Customer, CustomerTab +from .customers import Address, Contact, ContactCheck, Customer, CustomerTab from .empty_join import SlugPage from .person import Country, Friendship, Group, Membership, Person @@ -10,6 +10,7 @@ __all__ = [ "ArticleTag", "ArticleTranslation", "Contact", + "ContactCheck", "Country", "Customer", "CustomerTab", diff --git a/tests/foreign_object/models/customers.py b/tests/foreign_object/models/customers.py index 085b7272e9..f9a2e932c5 100644 --- a/tests/foreign_object/models/customers.py +++ b/tests/foreign_object/models/customers.py @@ -41,6 +41,27 @@ class Contact(models.Model): ) +class ContactCheck(models.Model): + company_code = models.CharField(max_length=1) + customer_code = models.IntegerField() + customer = models.ForeignObject( + Customer, + on_delete=models.CASCADE, + related_name="contact_checks", + to_fields=["customer_id", "company"], + from_fields=["customer_code", "company_code"], + ) + + class Meta: + required_db_features = {"supports_table_check_constraints"} + constraints = [ + models.CheckConstraint( + condition=models.Q(customer__lt=(1000, "c")), + name="customer_company_limit", + ), + ] + + class CustomerTab(models.Model): customer_id = models.IntegerField() customer = models.ForeignObject( diff --git a/tests/foreign_object/tests.py b/tests/foreign_object/tests.py index 09fb47e771..539c1a4fec 100644 --- a/tests/foreign_object/tests.py +++ b/tests/foreign_object/tests.py @@ -15,6 +15,7 @@ from .models import ( ArticleTag, ArticleTranslation, Country, + ContactCheck, CustomerTab, Friendship, Group, @@ -798,3 +799,9 @@ class ForeignObjectModelValidationTests(TestCase): def test_validate_constraints_excluding_foreign_object_member(self): customer_tab = CustomerTab(customer_id=150) customer_tab.validate_constraints(exclude={"customer_id"}) + + @skipUnlessDBFeature("supports_table_check_constraints") + def test_validate_constraints_with_foreign_object_multiple_fields(self): + contact = ContactCheck(company_code="d", customer_code=1500) + with self.assertRaisesMessage(ValidationError, "customer_company_limit"): + contact.validate_constraints() }}} When applied to Django 5.2, this fails with `AssertionError: ValidationError not raised` When applied to Django main, this fails with: {{{ ====================================================================== ERROR: test_validate_constraints_with_foreign_object_multiple_fields (foreign_object.tests.ForeignObjectModelValidationTests.test_validate_constraints_with_foreign_object_multiple_fields) ---------------------------------------------------------------------- Traceback (most recent call last): File "/path_to_django/tests/foreign_object/tests.py", line 807, in test_validate_constraints_with_foreign_object_multiple_fields contact.validate_constraints() File "/path_to_django/db/models/base.py", line 1653, in validate_constraints constraint.validate(model_class, self, exclude=exclude, using=using) File "/path_to_django/django/db/models/constraints.py", line 212, in validate if not Q(self.condition).check(against, using=using): ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/path_to_django/django/db/models/query_utils.py", line 187, in check return compiler.execute_sql(SINGLE) is not None ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/path_to_django/django/db/models/sql/compiler.py", line 1611, in execute_sql sql, params = self.as_sql() ^^^^^^^^^^^^^ File "/path_to_django/django/db/models/sql/compiler.py", line 795, in as_sql self.compile(self.where) if self.where is not None else ("", []) ^^^^^^^^^^^^^^^^^^^^^^^^ File "/path_to_django/django/db/models/sql/compiler.py", line 578, in compile sql, params = node.as_sql(self, self.connection) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/path_to_django/django/db/models/sql/where.py", line 151, in as_sql sql, params = compiler.compile(child) ^^^^^^^^^^^^^^^^^^^^^^^ File "/path_to_django/django/db/models/sql/compiler.py", line 578, in compile sql, params = node.as_sql(self, self.connection) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/path_to_django/django/db/models/lookups.py", line 404, in as_sql lhs_sql, params = self.process_lhs(compiler, connection) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/path_to_django/django/db/models/lookups.py", line 230, in process_lhs lhs_sql, params = super().process_lhs(compiler, connection, lhs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/path_to_django/django/db/models/lookups.py", line 110, in process_lhs sql, params = compiler.compile(lhs) ^^^^^^^^^^^^^^^^^^^^^ File "/path_to_django/django/db/models/sql/compiler.py", line 576, in compile sql, params = vendor_impl(self, self.connection) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/path_to_django/django/db/models/expressions.py", line 29, in as_sqlite sql, params = self.as_sql(compiler, connection, **extra_context) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/path_to_django/django/db/models/expressions.py", line 1107, in as_sql arg_sql, arg_params = compiler.compile(arg) ^^^^^^^^^^^^^^^^^^^^^ File "/path_to_django/db/models/sql/compiler.py", line 578, in compile sql, params = node.as_sql(self, self.connection) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/path_to_django/django/db/models/sql/where.py", line 151, in as_sql sql, params = compiler.compile(child) ^^^^^^^^^^^^^^^^^^^^^^^ File "/path_to_django/django/db/models/sql/compiler.py", line 578, in compile sql, params = node.as_sql(self, self.connection) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/path_to_django/django/db/models/fields/related_lookups.py", line 128, in as_sql return super().as_sql(compiler, connection) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/path_to_django/django/db/models/lookups.py", line 239, in as_sql rhs_sql, rhs_params = self.process_rhs(compiler, connection) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/path_to_django/django/db/models/lookups.py", line 138, in process_rhs return self.get_db_prep_lookup(value, connection) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/path_to_django/db/models/lookups.py", line 259, in get_db_prep_lookup field = getattr(self.lhs.output_field, "target_field", None) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/path_to_django/db/models/fields/related.py", line 506, in target_field raise exceptions.FieldError( django.core.exceptions.FieldError: The relation has multiple target fields, but only single target field was asked for }}} So note to self that any fix, we should also make sure we have applied to 5.2 and tested (in case it is relying on a commit currently applied to main, not to 5.2) -- Ticket URL: <https://code.djangoproject.com/ticket/36611#comment:5> Django <https://code.djangoproject.com/> The Web framework for perfectionists with deadlines. -- You received this message because you are subscribed to the Google Groups "Django updates" group. To unsubscribe from this group and stop receiving emails from it, send an email to django-updates+unsubscr...@googlegroups.com. To view this discussion visit https://groups.google.com/d/msgid/django-updates/010701995dd7484d-8736062e-2eeb-4dac-9a7d-5021145725dc-000000%40eu-central-1.amazonses.com.