#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>