[Django] #36530: System check for ManyToManyField & Composite Primary Key checks only one direction

37 views
Skip to first unread message

Django

unread,
Jul 27, 2025, 9:58:32 PM7/27/25
to django-...@googlegroups.com
#36530: System check for ManyToManyField & Composite Primary Key checks only one
direction
-------------------------------------+-------------------------------------
Reporter: Jacob Walls | Type: Bug
Status: new | Component: Database
| layer (models, ORM)
Version: 5.2 | Severity: Release
| blocker
Keywords: compositeprimarykey | Triage Stage:
| Unreviewed
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 1 | UI/UX: 0
-------------------------------------+-------------------------------------
#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
[https://github.com/django/django/blob/45ba7683a6e399815731a13cceb3acb518a2f1f2/django/db/models/fields/related.py#L1544
this system check condition] to check the "from" table also.
----
e.g. this (purely for demo, not suggesting we edit this test model):
{{{#!diff
diff --git a/tests/composite_pk/models/tenant.py
b/tests/composite_pk/models/tenant.py
index 65eb0feae8..ef777edff9 100644
--- a/tests/composite_pk/models/tenant.py
+++ b/tests/composite_pk/models/tenant.py
@@ -60,3 +60,4 @@ class TimeStamped(models.Model):
id = models.SmallIntegerField(unique=True)
created = models.DateTimeField(auto_now_add=True)
text = models.TextField(default="", blank=True)
+ tenants = models.ManyToManyField(Tenant)
}}}

gives:
{{{#!py
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)
}}}
--
Ticket URL: <https://code.djangoproject.com/ticket/36530>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.

Django

unread,
Jul 28, 2025, 11:19:26 AM7/28/25
to django-...@googlegroups.com
#36530: System check for ManyToManyField & Composite Primary Key checks only one
direction
-------------------------------------+-------------------------------------
Reporter: Jacob Walls | Owner: (none)
Type: Bug | Status: new
Component: Database layer | Version: 5.2
(models, ORM) |
Severity: Release blocker | Resolution:
Keywords: compositeprimarykey | Triage Stage: Accepted
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Changes (by Natalia Bidart):

* cc: Simon Charette (added)
* easy: 1 => 0
* stage: Unreviewed => Accepted

Comment:

Thank you Jacob! How about this for a regression test?
{{{#!python
def test_many_to_many_from_model_with_composite_primary_key(self):
class Parent(models.Model):
name = models.CharField(max_length=20)

class Child(models.Model):
pk = models.CompositePrimaryKey("version", "name")
version = models.IntegerField()
name = models.CharField(max_length=20)
parents = models.ManyToManyField(Parent)

field = Child._meta.get_field("parents")
self.assertEqual(
field.check(from_model=Child),
[
Error(
"Field has a CompositePrimaryKey and defines a
relation with model "
"'Parent' which is not supported.",
obj=field,
id="fields.E348",
),
],
)
}}}
--
Ticket URL: <https://code.djangoproject.com/ticket/36530#comment:1>

Django

unread,
Jul 28, 2025, 11:20:57 AM7/28/25
to django-...@googlegroups.com
#36530: System check for ManyToManyField & Composite Primary Key checks only one
direction
-------------------------------------+-------------------------------------
Reporter: Jacob Walls | Owner: Jason
| Hall
Type: Bug | Status: assigned
Component: Database layer | Version: 5.2
(models, ORM) |
Severity: Release blocker | Resolution:
Keywords: compositeprimarykey | Triage Stage: Accepted
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Changes (by Jason Hall):

* owner: (none) => Jason Hall
* status: new => assigned

--
Ticket URL: <https://code.djangoproject.com/ticket/36530#comment:2>

Django

unread,
Jul 28, 2025, 11:26:38 AM7/28/25
to django-...@googlegroups.com
#36530: System check for ManyToManyField & Composite Primary Key checks only one
direction
-------------------------------------+-------------------------------------
Reporter: Jacob Walls | Owner: Jason
| Hall
Type: Bug | Status: assigned
Component: Database layer | Version: 5.2
(models, ORM) |
Severity: Release blocker | Resolution:
Keywords: compositeprimarykey | Triage Stage: Accepted
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Comment (by Jason Hall):

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!
--
Ticket URL: <https://code.djangoproject.com/ticket/36530#comment:3>

Django

unread,
Jul 28, 2025, 11:37:24 AM7/28/25
to django-...@googlegroups.com
#36530: System check for ManyToManyField & Composite Primary Key checks only one
direction
-------------------------------------+-------------------------------------
Reporter: Jacob Walls | Owner: Jason
| Hall
Type: Bug | Status: assigned
Component: Database layer | Version: 5.2
(models, ORM) |
Severity: Release blocker | Resolution:
Keywords: compositeprimarykey | Triage Stage: Accepted
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Comment (by Jacob Walls):

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 ;)
--
Ticket URL: <https://code.djangoproject.com/ticket/36530#comment:4>

Django

unread,
Jul 28, 2025, 12:11:03 PM7/28/25
to django-...@googlegroups.com
#36530: System check for ManyToManyField & Composite Primary Key checks only one
direction
-------------------------------------+-------------------------------------
Reporter: Jacob Walls | Owner: Jason
| Hall
Type: Bug | Status: assigned
Component: Database layer | Version: 5.2
(models, ORM) |
Severity: Release blocker | Resolution:
Keywords: compositeprimarykey | Triage Stage: Accepted
Has patch: 1 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Changes (by Jason Hall):

* has_patch: 0 => 1

--
Ticket URL: <https://code.djangoproject.com/ticket/36530#comment:5>

Django

unread,
Jul 29, 2025, 5:18:22 AM7/29/25
to django-...@googlegroups.com
#36530: System check for ManyToManyField & Composite Primary Key checks only one
direction
-------------------------------------+-------------------------------------
Reporter: Jacob Walls | Owner: Jason
| Hall
Type: Bug | Status: assigned
Component: Database layer | Version: 5.2
(models, ORM) |
Severity: Release blocker | Resolution:
Keywords: compositeprimarykey | Triage Stage: Accepted
Has patch: 1 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 1
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Changes (by Sarah Boyce):

* needs_better_patch: 0 => 1

--
Ticket URL: <https://code.djangoproject.com/ticket/36530#comment:6>

Django

unread,
Jul 31, 2025, 12:36:29 PM7/31/25
to django-...@googlegroups.com
#36530: System check for ManyToManyField & Composite Primary Key checks only one
direction
-------------------------------------+-------------------------------------
Reporter: Jacob Walls | Owner: Jason
| Hall
Type: Bug | Status: assigned
Component: Database layer | Version: 5.2
(models, ORM) |
Severity: Release blocker | Resolution:
Keywords: compositeprimarykey | Triage Stage: Ready for
| checkin
Has patch: 1 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 1
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Changes (by Jason Hall):

* stage: Accepted => Ready for checkin

--
Ticket URL: <https://code.djangoproject.com/ticket/36530#comment:7>

Django

unread,
Jul 31, 2025, 12:41:29 PM7/31/25
to django-...@googlegroups.com
#36530: System check for ManyToManyField & Composite Primary Key checks only one
direction
-------------------------------------+-------------------------------------
Reporter: Jacob Walls | Owner: Jason
| Hall
Type: Bug | Status: assigned
Component: Database layer | Version: 5.2
(models, ORM) |
Severity: Release blocker | Resolution:
Keywords: compositeprimarykey | Triage Stage: Accepted
Has patch: 1 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 1
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Changes (by Simon Charette):

* stage: Ready for checkin => Accepted

Comment:

Please refer to the contributing documentation,
[https://docs.djangoproject.com/en/5.2/faq/contributing/#i-m-sure-my-
ticket-is-absolutely-100-perfect-can-i-mark-it-as-ready-for-checkin-myself
you can't mark your own patch RFC].
--
Ticket URL: <https://code.djangoproject.com/ticket/36530#comment:8>

Django

unread,
Jul 31, 2025, 1:20:32 PM7/31/25
to django-...@googlegroups.com
#36530: System check for ManyToManyField & Composite Primary Key checks only one
direction
-------------------------------------+-------------------------------------
Reporter: Jacob Walls | Owner: Jason
| Hall
Type: Bug | Status: assigned
Component: Database layer | Version: 5.2
(models, ORM) |
Severity: Release blocker | Resolution:
Keywords: compositeprimarykey | Triage Stage: Accepted
Has patch: 1 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Changes (by Jason Hall):

* needs_better_patch: 1 => 0

--
Ticket URL: <https://code.djangoproject.com/ticket/36530#comment:9>

Django

unread,
Aug 1, 2025, 11:02:36 AM8/1/25
to django-...@googlegroups.com
#36530: System check for ManyToManyField & Composite Primary Key checks only one
direction
-------------------------------------+-------------------------------------
Reporter: Jacob Walls | Owner: Jason
| Hall
Type: Bug | Status: assigned
Component: Database layer | Version: 5.2
(models, ORM) |
Severity: Release blocker | Resolution:
Keywords: compositeprimarykey | Triage Stage: Accepted
Has patch: 1 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 1
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Changes (by Natalia Bidart):

* needs_better_patch: 0 => 1

--
Ticket URL: <https://code.djangoproject.com/ticket/36530#comment:10>

Django

unread,
Aug 1, 2025, 12:03:29 PM8/1/25
to django-...@googlegroups.com
#36530: System check for ManyToManyField & Composite Primary Key checks only one
direction
-------------------------------------+-------------------------------------
Reporter: Jacob Walls | Owner: Jason
| Hall
Type: Bug | Status: assigned
Component: Database layer | Version: 5.2
(models, ORM) |
Severity: Release blocker | Resolution:
Keywords: compositeprimarykey | Triage Stage: Accepted
Has patch: 1 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Changes (by Jason Hall):

* needs_better_patch: 1 => 0

--
Ticket URL: <https://code.djangoproject.com/ticket/36530#comment:11>

Django

unread,
Aug 4, 2025, 11:59:46 AM8/4/25
to django-...@googlegroups.com
#36530: System check for ManyToManyField & Composite Primary Key checks only one
direction
-------------------------------------+-------------------------------------
Reporter: Jacob Walls | Owner: Jason
| Hall
Type: Bug | Status: assigned
Component: Database layer | Version: 5.2
(models, ORM) |
Severity: Release blocker | Resolution:
Keywords: compositeprimarykey | Triage Stage: Ready for
| checkin
Has patch: 1 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Changes (by Natalia Bidart):

* stage: Accepted => Ready for checkin

--
Ticket URL: <https://code.djangoproject.com/ticket/36530#comment:12>

Django

unread,
Aug 5, 2025, 7:34:52 AM8/5/25
to django-...@googlegroups.com
#36530: System check for ManyToManyField & Composite Primary Key checks only one
direction
-------------------------------------+-------------------------------------
Reporter: Jacob Walls | Owner: Jason
| Hall
Type: Bug | Status: closed
Component: Database layer | Version: 5.2
(models, ORM) |
Severity: Release blocker | Resolution: fixed
Keywords: compositeprimarykey | Triage Stage: Ready for
| checkin
Has patch: 1 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Changes (by nessita <124304+nessita@…>):

* resolution: => fixed
* status: assigned => closed

Comment:

In [changeset:"2013092b693be0ebdf36f41dc61615a2de1bbe31" 2013092]:
{{{#!CommitTicketReference repository=""
revision="2013092b693be0ebdf36f41dc61615a2de1bbe31"
Fixed #36530 -- Extended fields.E347 to check for ManyToManyField
involving CompositePrimaryKey on either side.

Thanks to Jacob Walls for the report.
}}}
--
Ticket URL: <https://code.djangoproject.com/ticket/36530#comment:13>

Django

unread,
Aug 5, 2025, 7:48:50 AM8/5/25
to django-...@googlegroups.com
#36530: System check for ManyToManyField & Composite Primary Key checks only one
direction
-------------------------------------+-------------------------------------
Reporter: Jacob Walls | Owner: Jason
| Hall
Type: Bug | Status: closed
Component: Database layer | Version: 5.2
(models, ORM) |
Severity: Release blocker | Resolution: fixed
Keywords: compositeprimarykey | Triage Stage: Ready for
| checkin
Has patch: 1 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Comment (by Natalia <124304+nessita@…>):

In [changeset:"bdc3f9e3508fc144c5e9710f5b672cc41f6e742d" bdc3f9e]:
{{{#!CommitTicketReference repository=""
revision="bdc3f9e3508fc144c5e9710f5b672cc41f6e742d"
[5.2.x] Fixed #36530 -- Extended fields.E347 to check for ManyToManyField
involving CompositePrimaryKey on either side.

Thanks to Jacob Walls for the report.

Backport of 2013092b693be0ebdf36f41dc61615a2de1bbe31 from main.
}}}
--
Ticket URL: <https://code.djangoproject.com/ticket/36530#comment:14>
Reply all
Reply to author
Forward
0 new messages