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

  1. Autodetector field classification (django/db/migrations/autodetector.py): In MigrationAutodetector.generate_created_models(), fields are inspected:
    for 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
    
    For a CompositePrimaryKey, the constituent ForeignKey fields have field.primary_key == False (the primary key is the virtual pk field). Consequently, the autodetector classifies them as related_fields and excludes them from CreateModel.fields, deferring them into separate AddField operations.
  1. Topological sorting level displacement (_sort_migrations):
    • The target model (CrewMember) has employee = OneToOneField('Employee', primary_key=True). Because of primary_key_rel, CreateModel('CrewMember') depends on CreateModel('Employee') (Level 1 in TopologicalSorter).
    • Therefore, AddField('crewmemberboat', 'crewmember') depends on CreateModel('CrewMember') and is assigned to Level 2.
  1. Optimization barrier in IndexOperation (django/db/migrations/operations/models.py):
    • At Level 2, many AddIndex and AddConstraint operations from Level 0/1 models are emitted before AddField.
    • Neither IndexOperation nor AddConstraint overrides references_model(), inheriting the default from Operation:
      def references_model(self, name, app_label):
          return True  # If in doubt, return True
      
    • As a result, ModelOperation.can_reduce_through(operation) returns False for ANY index or constraint belonging to other models.
    • MigrationOptimizer.optimize_inner() cannot reduce AddField into CreateModel across these unrelated index operations.
  1. Failure during CreateModel execution (django/db/models/fields/composite.py): When the migration runs, CreateModel invokes schema_editor.create_model(), which inspects model._meta.pk.columns -> CompositePrimaryKey.fields. Since the constituent field was deferred to AddField, 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

  1. In IndexOperation (django/db/migrations/operations/models.py), implement references_model:
    def references_model(self, name, app_label):
        return self.model_name_lower == name.lower()
    
  2. In MigrationAutodetector.generate_created_models(), check whether a related field belongs to a CompositePrimaryKey before deferring it to related_fields.

Change History (1)

comment:1 by Vasiliy Tyukin, 38 minutes ago

Component: UncategorizedMigrations
Note: See TracTickets for help on using tickets.
Back to Top