Partial migration causing django_migrations table out of sync during zero-downtime deploy
0 reputation · 18 Dec 2022, 21:25 UTC
0 reputation · 18 Dec 2022, 21:25 UTC
When a Django application is upgraded, the migrate command applies each migration sequentially and records the migration name in the django_migrations table. The goal is to keep the application online while the schema evolves, which requires that the table entry and the actual database schema remain aligned after each step.
The uncertainty lies in how to handle migrations on backends that do not support transactional DDL. Enabling Migration.atomic=False can bypass automatic transaction wrapping, but it does not provide online schema change mechanisms, and RunPython operations still execute within the same transaction context, potentially leaving partial changes if a DDL statement commits early. The precise approach to guarantee consistency across PostgreSQL, MySQL, and SQLite while avoiding downtime has not been resolved.
How can a small Django app guarantee that django_migrations stays in sync with the live schema during incremental, zero‑downtime migrations? Is there a reliable pattern for using Migration.atomic=False together with RunPython to offset non‑transactional DDL on MySQL or SQLite? What deployment coordination strategy prevents table locks while ensuring the django_migrations entry reflects the final schema state?
35775 reputation · 18 Dec 2022, 23:23 UTC
No single Django setting makes DDL atomic on MySQL or SQLite. The reliable pattern is to split each logical change into backward‑compatible additive migrations, run migrations as a separate pre‑deploy step under a deployment lock, and write idempotent RunPython operations that check state before applying changes. Migration.atomic=False only removes Django’s transaction wrapper; it does not prevent implicit commits from individual DDL statements on non‑transactional backends.
django_migrations is written only after commit, guaranteeing sync.COMMIT. atomic=False disables the outer transaction but cannot roll back DDL. A crash mid‑migration leaves schema partially applied while the migration record may be missing or present depending on failure point.Migration.atomic=False Actually DoesIt tells Django not to wrap the migration in BEGIN/COMMIT. RunPython then executes without an outer transaction. However, any DDL issued via schema_editor inside RunPython still triggers implicit commits on MySQL/SQLite. You cannot group multiple DDL statements atomically on these backends.
migrate in CI/CD before deploying the new application code. Use a deployment lock so only one instance runs migrations.RunPython: For MySQL/SQLite, write RunPython functions that inspect the database (e.g., check if column exists) before executing DDL. This makes re‑running safe after a partial failure.NOT NULL constraints, or remove legacy tables.# 1. Acquire lock (PostgreSQL example)
SELECT pg_try_advisory_xact_lock(123456);
# 2. Run migrations
python manage.py migrate --plan # verify order
python manage.py migrate
# 3. Release lock (automatic at transaction end)
# 4. Deploy new application code
# 5. Run cleanup migration (same lock process)On MySQL use SELECT GET_LOCK('migrate', 10) / RELEASE_LOCK('migrate'). Ensure health checks tolerate intermediate schema states (e.g., nullable columns present but not yet required).
Which database backend(s) are you targeting in production? The locking mechanism and the need for atomic=False differ between PostgreSQL and MySQL/SQLite, so the exact deployment script changes slightly.
Use comments to ask for clarification. Post a solution as an answer.
35,775 reputation · 19 Dec 2022, 07:48 UTC
Assuming Django 3.2+, setting Migration.atomic = False removes Django’s transaction wrapper, but RunPython does not gain a separate rollback boundary. If an ALTER TABLE commits early on MySQL or SQLite, a later RunPython failure cannot reverse that DDL, and django_migrations will not record the migration until the entire migration succeeds.
Practical verification: query django_migrations for the expected app and name, then compare the live schema using information_schema (PostgreSQL/MySQL) or sqlite_master (SQLite). Treat any mismatch as a partial migration before promoting the release.