Opened 39 minutes ago
Last modified 38 minutes ago
#37337 new Bug
MigrationAutodetector defers ForeignKey components of CompositePrimaryKey to AddField, causing FieldDoesNotExist on CreateModel
| Reported by: | Vasiliy Tyukin | Owned by: | |
|---|---|---|---|
| Component: | Migrations | Version: | 6.1 |
| Severity: | Normal | Keywords: | composite-primary-key, migrations, autodetector, optimizer |
| Cc: | Triage Stage: | Unreviewed | |
| Has patch: | no | Needs documentation: | no |
| Needs tests: | no | Patch needs improvement: | no |
| Easy pickings: | no | UI/UX: | no |
Description
Problem Description
When a model uses models.CompositePrimaryKey where one of the constituent foreign keys points to a model whose own primary key is a relation (e.g. OneToOneField(primary_key=True)), running makemigrations generates an invalid migration that fails on migrate with FieldDoesNotExist.
Root Cause Analysis
- Autodetector field classification (
django/db/migrations/autodetector.py): InMigrationAutodetector.generate_created_models(), fields are inspected:For afor field_name, field in model_state.fields.items(): if field.remote_field: if field.remote_field.model: if field.primary_key: primary_key_rel = field.remote_field.model elif not field.remote_field.parent_link: related_fields[field_name] = field
CompositePrimaryKey, the constituent ForeignKey fields havefield.primary_key == False(the primary key is the virtualpkfield). Consequently, the autodetector classifies them asrelated_fieldsand excludes them fromCreateModel.fields, deferring them into separateAddFieldoperations.
- Topological sorting level displacement (
_sort_migrations):- The target model (
CrewMember) hasemployee = OneToOneField('Employee', primary_key=True). Because ofprimary_key_rel,CreateModel('CrewMember')depends onCreateModel('Employee')(Level 1 inTopologicalSorter). - Therefore,
AddField('crewmemberboat', 'crewmember')depends onCreateModel('CrewMember')and is assigned to Level 2.
- The target model (
- Optimization barrier in
IndexOperation(django/db/migrations/operations/models.py):- At Level 2, many
AddIndexandAddConstraintoperations from Level 0/1 models are emitted beforeAddField. - Neither
IndexOperationnorAddConstraintoverridesreferences_model(), inheriting the default fromOperation:def references_model(self, name, app_label): return True # If in doubt, return True
- As a result,
ModelOperation.can_reduce_through(operation)returnsFalsefor ANY index or constraint belonging to other models. MigrationOptimizer.optimize_inner()cannot reduceAddFieldintoCreateModelacross these unrelated index operations.
- At Level 2, many
- Failure during
CreateModelexecution (django/db/models/fields/composite.py): When the migration runs,CreateModelinvokesschema_editor.create_model(), which inspectsmodel._meta.pk.columns->CompositePrimaryKey.fields. Since the constituent field was deferred toAddField,meta.get_field(...)raises:django.core.exceptions.FieldDoesNotExist: CrewMemberBoat has no field named 'crewmember'.
Minimal Reproducible Example (MRE)
# models.py from django.db import models class Employee(models.Model): name = models.CharField(max_length=64) class CrewMember(models.Model): employee = models.OneToOneField(Employee, on_delete=models.CASCADE, primary_key=True) class Boat(models.Model): name = models.CharField(max_length=64) class Meta: indexes = [models.Index(fields=["name"], name="idx_boat_name")] class CrewMemberBoat(models.Model): pk = models.CompositePrimaryKey("crewmember", "boat") crewmember = models.ForeignKey(CrewMember, on_delete=models.CASCADE) boat = models.ForeignKey(Boat, on_delete=models.CASCADE)
Run python manage.py makemigrations followed by python manage.py migrate.
Traceback
django.core.exceptions.FieldDoesNotExist: CrewMemberBoat has no field named 'crewmember'
File "django/db/models/options.py", line 686, in get_field
raise FieldDoesNotExist("%s has no field named '%s'" % (self.object_name, field_name))
File "django/db/models/fields/composite.py", line 86, in <genexpr>
return tuple(meta.get_field(field_name) for field_name in self.field_names)
File "django/db/backends/base/schema.py", line 472, in create_model
Suggested Fixes
- In
IndexOperation(django/db/migrations/operations/models.py), implementreferences_model:def references_model(self, name, app_label): return self.model_name_lower == name.lower()
- In
MigrationAutodetector.generate_created_models(), check whether a related field belongs to aCompositePrimaryKeybefore deferring it torelated_fields.
Note:
See TracTickets
for help on using tickets.