#36530 closed Bug (fixed)
System check for ManyToManyField & Composite Primary Key checks only one direction
Reported by: | Jacob Walls | Owned by: | Jason Hall |
---|---|---|---|
Component: | Database layer (models, ORM) | Version: | 5.2 |
Severity: | Release blocker | Keywords: | compositeprimarykey |
Cc: | Simon Charette | Triage Stage: | Ready for checkin |
Has patch: | yes | Needs documentation: | no |
Needs tests: | no | Patch needs improvement: | no |
Easy pickings: | no | UI/UX: | no |
Description
#36034 added the system check E347 for ManyToManyField
when the "to_model" has a composite pk, but the situation is just as unsupported when the composite primary key model is the "from_model", i.e. the one with *both* the ManyToManyField
and the CompositePrimaryKey
.
We should be able to just adjust this system check condition to check the "from" table also.
e.g. this (purely for demo, not suggesting we edit this test model):
-
tests/composite_pk/models/tenant.py
diff --git a/tests/composite_pk/models/tenant.py b/tests/composite_pk/models/tenant.py index 65eb0feae8..ef777edff9 100644
a b class TimeStamped(models.Model): 60 60 id = models.SmallIntegerField(unique=True) 61 61 created = models.DateTimeField(auto_now_add=True) 62 62 text = models.TextField(default="", blank=True) 63 tenants = models.ManyToManyField(Tenant)
gives:
ERROR: test_serialize_datetime (composite_pk.tests.CompositePKFixturesTests.test_serialize_datetime) ---------------------------------------------------------------------- Traceback (most recent call last): File "/Users/jwalls/django/tests/composite_pk/tests.py", line 368, in test_serialize_datetime result = serializers.serialize("json", TimeStamped.objects.all()) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/jwalls/django/django/core/serializers/__init__.py", line 134, in serialize s.serialize(queryset, **options) File "/Users/jwalls/django/django/core/serializers/base.py", line 144, in serialize self.handle_m2m_field(obj, field) File "/Users/jwalls/django/django/core/serializers/python.py", line 95, in handle_m2m_field queryset_iterator(obj, field), ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/jwalls/django/django/core/serializers/python.py", line 89, in queryset_iterator query_set = getattr(obj, field.name).select_related(None).only("pk") ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/jwalls/django/django/db/models/manager.py", line 87, in manager_method return getattr(self.get_queryset(), name)(*args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/jwalls/django/django/db/models/query.py", line 1632, in select_related self._not_support_combined_queries("select_related") File "/Users/jwalls/django/django/db/models/query.py", line 2052, in _not_support_combined_queries if self.query.combinator: ^^^^^^^^^^ File "/Users/jwalls/django/django/db/models/query.py", line 302, in query self._filter_or_exclude_inplace(negate, args, kwargs) File "/Users/jwalls/django/django/db/models/query.py", line 1549, in _filter_or_exclude_inplace self._query.add_q(Q(*args, **kwargs)) File "/Users/jwalls/django/django/db/models/sql/query.py", line 1667, in add_q clause, _ = self._add_q(q_object, can_reuse) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/jwalls/django/django/db/models/sql/query.py", line 1699, in _add_q child_clause, needed_inner = self.build_filter( ^^^^^^^^^^^^^^^^^^ File "/Users/jwalls/django/django/db/models/sql/query.py", line 1609, in build_filter condition = self.build_lookup(lookups, col, value) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/jwalls/django/django/db/models/sql/query.py", line 1436, in build_lookup lookup = lookup_class(lhs, rhs) ^^^^^^^^^^^^^^^^^^^^^^ File "/Users/jwalls/django/django/db/models/lookups.py", line 35, in __init__ self.rhs = self.get_prep_lookup() ^^^^^^^^^^^^^^^^^^^^^^ File "/Users/jwalls/django/django/db/models/fields/tuple_lookups.py", line 55, in get_prep_lookup self.check_rhs_length_equals_lhs_length() File "/Users/jwalls/django/django/db/models/fields/tuple_lookups.py", line 69, in check_rhs_length_equals_lhs_length len_lhs = len(self.lhs) ^^^^^^^^^^^^^ TypeError: object of type 'Col' has no len() ---------------------------------------------------------------------- Ran 170 tests in 0.183s FAILED (errors=1)
Change History (14)
comment:1 by , 5 weeks ago
Cc: | added |
---|---|
Easy pickings: | unset |
Triage Stage: | Unreviewed → Accepted |
comment:2 by , 5 weeks ago
Owner: | set to |
---|---|
Status: | new → assigned |
comment:3 by , 5 weeks ago
I was just trying to reproduce this issue with the following test:
@isolate_apps("model_fields") class M2MCompositePKCheckTests(SimpleTestCase): def test_m2m_from_model_composite_pk_should_raise_e347(self): class Target(models.Model): name = models.CharField(max_length=100) class Meta: app_label = "model_fields" class TimeStamped(models.Model): id = models.IntegerField() created = models.DateTimeField() tenants = models.ManyToManyField(Target) class Meta: app_label = "model_fields" constraints = [ models.UniqueConstraint(fields=["id", "created"], name="composite_pk") ] errors = TimeStamped.check() self.assertTrue( any(e.id == "models.E347" for e in errors), msg="Expected E347 for ManyToManyField declared on a model with composite primary key", )
I'm not sure if this approach (using UniqueConstraint to simulate a composite PK) is appropriate for the final test, or if we should prefer the version that uses CompositePrimaryKey and field.check(from_model=...).
Just wanted to share what I came up with to help confirm the issue before working on the fix. Happy to adjust based on feedback!
comment:4 by , 5 weeks ago
We should use CompositePrimaryKey
, as that ensures the check guards what is unsupported in fact.
Natalia's version riffs on the tests from #36034, which is what I had in mind. And incidentally why I thought this was an easy picking, but I suppose I should go to the forum before taking up a 1-person campaign to use that flag more ;)
comment:5 by , 5 weeks ago
Has patch: | set |
---|
comment:6 by , 5 weeks ago
Patch needs improvement: | set |
---|
comment:7 by , 5 weeks ago
Triage Stage: | Accepted → Ready for checkin |
---|
comment:8 by , 5 weeks ago
Triage Stage: | Ready for checkin → Accepted |
---|
Please refer to the contributing documentation, you can't mark your own patch RFC.
comment:9 by , 5 weeks ago
Patch needs improvement: | unset |
---|
comment:10 by , 5 weeks ago
Patch needs improvement: | set |
---|
comment:11 by , 5 weeks ago
Patch needs improvement: | unset |
---|
comment:12 by , 4 weeks ago
Triage Stage: | Accepted → Ready for checkin |
---|
Thank you Jacob! How about this for a regression test?