﻿id	summary	reporter	owner	description	type	status	component	version	severity	resolution	keywords	cc	stage	has_patch	needs_docs	needs_tests	needs_better_patch	easy	ui_ux
37337	MigrationAutodetector defers ForeignKey components of CompositePrimaryKey to AddField, causing FieldDoesNotExist on CreateModel	Vasiliy Tyukin		"== 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:
{{{
#!python
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.

 2. '''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.

 3. '''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}}}:
{{{
#!python
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.

 4. '''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) ==

{{{
#!python
# 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}}}:
{{{
#!python
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}}}."	Bug	new	Migrations	6.1	Normal		composite-primary-key, migrations, autodetector, optimizer		Unreviewed	0	0	0	0	0	0
