Make MoveToDjangoMigrations work when the app isn't yet in the database.

Review Request #14530 — Created July 30, 2025 and submitted

Information

Django Evolution
release-2.x

Reviewers

MoveToDjangoMigrations had a dependency on the migrations being
applied. The idea was that:

  1. If this was a new app in the database, we'd want migrations to handle
    any initial work first.

  2. If the app was already installed, but the evolution was new, those
    migrations would have already been applied.

The problem was that the first case didn't necessarily work right.

We could reach a state where the migrations weren't in the graph, and
the dependency would break. This could happen if installing an app that
has both migrations and evolutions defined as custom for the app.
There's no signature for the app yet, and we have evolutions, so we
prioritize those and skip the migrations (so we can deal with any
potentially-broken state in the migratinos).

In this case, there was no way of really communicating that the
migrations should have been in the graph but also should not have been
applied since the evolution replaces them.

This change adds that capability. Along with the before/after
migrations/evolutions dependencies, there's now a new
replace_migrations dependency type that works like after_migrations
but makes the dependencies optional. This allows them to depend on a
recorded migration if it's in the graph as before, but to proceed
without failure if that migration wasn't found in the graph.

With that, we can keep a consistent graph in the case where the
migration would have been processed or picked up, but without requiring
it to be applied or recorded first in this case.

The new test app (move_to_migrations_app) simulates a module that
(in cooperation with the test setup) builds upon a faulty base (a
0001_initial that contains a superset of the fields actually in the
base model), adds missing fields in the evolution, hands it off to
Django, and then adds a field via a migration. This simulates the kind
of problems we've seen in some third-party apps that squash history back
down into a 0001_initial over time.

There are also fixes for the upgrade tests, which had previously been a
no-op due to a leftover return, masking some bad test harness setup.

Tested on all versions of Django, on all supported databases.

Summary ID
Make MoveToDjangoMigrations work when the app isn't yet in the database.
`MoveToDjangoMigrations` had a dependency on the migrations being applied. The idea was that: 1. If this was a new app in the database, we'd want migrations to handle any initial work first. 2. If the app was already installed, but the evolution was new, those migrations would have already been applied. The problem was that the first case didn't necessarily work right. We could reach a state where the migrations weren't in the graph, and the dependency would break. This could happen if installing an app that has both migrations and evolutions defined as custom for the app. There's no signature for the app yet, and we have evolutions, so we prioritize those and skip the migrations (so we can deal with any potentially-broken state in the migratinos). In this case, there was no way of really communicating that the migrations should have been in the graph but also should not have been applied since the evolution replaces them. This change adds that capability. Along with the before/after migrations/evolutions dependencies, there's now a new `replace_migrations` dependency type that works like `after_migrations` but makes the dependencies optional. This allows them to depend on a recorded migration if it's in the graph as before, but to proceed without failure if that migration wasn't found in the graph. With that, we can keep a consistent graph in the case where the migration would have been processed or picked up, but without requiring it to be applied or recorded first in this case. The new test app (`move_to_migrations_app`) simulates a module that (in cooperation with the test setup) builds upon a faulty base (a `0001_initial` that contains a superset of the fields actually in the base model), adds missing fields in the evolution, hands it off to Django, and then adds a field via a migration. This simulates the kind of problems we've seen in some third-party apps that squash history back down into a `0001_initial` over time. There are also fixes for the upgrade tests, which had previously been a no-op due to a leftover `return`, masking some bad test harness setup.
939ebccfd589efd2a9c8ba3028d390b984ff7a7f
Description From Last Updated

line too long (86 > 79 characters) Column: 80 Error code: E501

reviewbotreviewbot
Checks run (1 failed, 1 succeeded)
flake8 failed.
JSHint passed.

flake8

david
  1. Ship It!
  2. 
      
chipx86
Review request changed
Status:
Completed
Change Summary:
Pushed to release-2.x (eb70663)