Hello there.
Recently I needed to manually squash the migrations, after trying to use squashmigrations and failing hard several times, I’ve used the a approach:
Here’s what I’ve done, hopefully this can help someone in the future.
Step 1
Remove all migrations from all apps and run makemigrations
Since I had quite a few apps, I wrote a small script to:
- detect all apps with migrations and delete them;
- recreate migrations
Just remember to take note of all the apps that had migrations recreated, you’ll need it on Step 4.
Delete all migration files inside the apps migrations folder Script
import subprocess
from collections.abc import Generator
from pathlib import Path
def main():
app_dirs_with_migrations = get_app_dirs_with_migrations()
for migration_files in app_dirs_with_migrations.values():
for migration_file in migration_files:
migration_file.unlink()
# Recreate all migrations
subprocess.run(["uv", "run", "python", "manage.py", "makemigrations"], check=False) # noqa: S607
def get_app_dirs_with_migrations() -> dict[Path, list[Path]]:
project_root = Path.cwd().joinpath("your-root-project-dir")
apps_with_migrations: dict[Path, list[Path]] = {}
for app_dir in iter_path_dirs(project_root):
migrations = migration_files_from_app(app_dir)
if not migrations:
continue
apps_with_migrations[app_dir] = migrations
return apps_with_migrations
def iter_path_dirs(path: Path) -> Generator[Path, None, None]:
for sub_path in path.iterdir():
if sub_path.is_dir() and sub_path.stem not in ("__pycache__", "static", "templates"):
yield sub_path
def migration_files_from_app(app_dir: Path) -> list[Path]:
migrations = []
for app_sub_folder in iter_path_dirs(app_dir):
if app_sub_folder.stem != "migrations":
continue
for file in app_sub_folder.iterdir():
if file.stem.startswith("__"):
continue
migrations.append(file)
return sorted(migrations)
if __name__ == "__main__":
main()
Step 2
Running the new created migrations on a clean database
Since some of my models are using typing.Generic the migrations were using the typing.Generic class in the migrations.CreateModel operation, under the options bases, this was causing an error. So I needed to manually edit some of the files to remove the typing.Generic from the bases option inside some migrations.
To run the migrations from scratch on a clean database I’ve used docker for that.
test.yml
services:
django:
build:
context: .
dockerfile: ./compose/local/django/Dockerfile
image: project_local_django
container_name: project_test
depends_on:
- test_postgres
- redis
environment:
POSTGRES_HOST: test_postgres
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
POSTGRES_PORT: 5432
POSTGRES_DB: project
REDIS_URL: "redis://redis:6379/0"
command: pytest --cov=bid_capital_loan_service --cov-report=html:cov_report ${PYTEST_ARGS:-""}
volumes:
- .cov_report:/app/cov_report
test_postgres:
image: docker.io/postgres:16
container_name: bid_test_postgres
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
POSTGRES_DB: project
ports:
- 5445:5432
redis:
image: docker.io/redis:6
container_name: your_project_local_redis
ports:
- 6380:6379
bash script to run migrations
docker compose -f test.yml down
docker compose -f test.yml up test_postgres -d
docker compose -f test.yml build
docker compose -f test.yml run django python manage.py migrate
docker compose -f test.yml down
Step 3
Running migrate --fake-initial on the target database
Since I had more than one environment, I’ve targeted the UAT environment for safety first (You need to make sure that all target databases are on the same version, all migrations that were deleted are applied on them).
uv run python manage.py migrate --fake-initial
This marked all the new “migrations” as being applied on the target database.
Step 4
Running migrate app_name --prune on the target database
After all the migrations were faked, I ran the migrate --prune command to remove all migrations that no longer existed. You need to run this for each app though, that’s quite painful.
uv run python manage.py migrate app_name --prune
Step 5
Assert that no migrations needs to be applied
Since we did some massive changes, it’s a good double check that you don’t have any migrations to be applied after the changes.
uv run python manage.py migrate --plan
Finale
That’s it, all apps now have one (or just a few) migrations. Notice that any local (other) environment will need to do steps 3-5 after this goes to your main branch (or just drop the database).