#37281 new Bug

Converting a unique_together field to a ForeignKey generates a migration with AlterUniqueTogether before AddField (FieldDoesNotExist)

Reported by: Konstantinos Giannopoulos Owned by:
Component: Migrations Version: 6.1
Severity: Normal Keywords: autodetector unique_together migrations
Cc: Konstantinos Giannopoulos Triage Stage: Unreviewed
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

When an existing field that participates in unique_together is replaced by a ForeignKey, and unique_together is updated to reference the new field, makemigrations emits the operations in an unapplyable order: AlterUniqueTogether is placed before the AddField that creates the referenced column.

Steps to reproduce.

Initial models:

class A(models.Model):
    pass

class B(models.Model):
    type = models.CharField(max_length=20)
    version = models.IntegerField()

    class Meta:
        unique_together = [("type", "version")]

Change B to replace type with a FK:

class B(models.Model):
    a = models.ForeignKey(A, on_delete=models.CASCADE, null=True)
    version = models.IntegerField()

    class Meta:
        unique_together = [("a", "version")]

makemigrations produces:

operations = [
    migrations.AlterUniqueTogether(name="b", unique_together={("a", "version")}),
    migrations.AddField(model_name="b", name="a", field=models.ForeignKey(...)),
    migrations.RemoveField(model_name="b", name="type"),
]

migrate then raises:

django.core.exceptions.FieldDoesNotExist: B has no field named 'a'

Manually moving AddField before AlterUniqueTogether produces a working migration.

Affected versions.

Confirmed broken on 5.0.9, 5.1.4, 5.2.5, and 6.1. Correct on 3.2.25 and 4.2.16.

Regression.

Bisected between 4.2 (good) and 5.0 (bad). First bad commit:

1282b5e4207440af659ef0e0e0c486fdfba8e7b7 — "Fixed #32528 -- Replaced django.utils.topological_sort with graphlib.TopologicalSort()".

That change is a refactor and both sorts are correct; they differ in tie-breaking. graphlib.TopologicalSorter returns nodes that have no ordering constraint between them in insertion order, which differs from the previous implementation and exposes a pre-existing missing dependency (below). The commit is the trigger rather than the underlying defect.

Root cause.

In MigrationAutodetector._get_altered_foo_together_operations, the generated AlterUniqueTogether gains a dependency only when a referenced field is a ForeignKey, and that dependency targets the CREATE of the related model, not the AddField of the local column. Nothing records that AlterUniqueTogether must run after the AddField of the fields it references. Without that edge, _sort_migrations() may order the two operations either way; the previous sort happened to place AddField first, graphlib does not.

Suggested fix.

Add a dependency on the CREATE of every field named in the constraint, not only foreign keys (Type.CREATE matches AddField in check_dependency). This mirrors what generate_created_models() already does for the new-model case:

for foo_togethers in new_value:
    for field_name in foo_togethers:
        field = new_model_state.get_field(field_name)
        if field.remote_field and field.remote_field.model:
            dependencies.extend(self._get_dependencies_for_foreign_key(...))
        dependencies.append(
            OperationDependency(
                app_label, model_name, field_name,
                OperationDependency.Type.CREATE,
            )
        )

With this applied, the operations are ordered AddField -> AlterUniqueTogether -> RemoveField and the migration applies cleanly (verified against 5.2.5 and 6.1).

Related tickets.

A recurring family of ordering bugs affects this operation against field add/remove, all surfacing as FieldDoesNotExist: #23794, #26180, #27933, #28366, #29124. Those concern AlterUniqueTogether vs RemoveField; this ticket is the AddField variant (field-to-FK conversion). It is also distinct from #25551 (new model + FK + unique_together in one migration, fixed in 1.9).

I would like to work on the fix for this if the ticket is accepted. I have a reproduction and a candidate patch ready and am happy to submit a PR with a regression test.

Change History (0)

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