diff --git a/django/db/migrations/executor.py b/django/db/migrations/executor.py index 83d624e08a..57042a8690 100644 --- a/django/db/migrations/executor.py +++ b/django/db/migrations/executor.py @@ -225,8 +225,9 @@ class MigrationExecutor: # Alright, do it normally with self.connection.schema_editor(atomic=migration.atomic) as schema_editor: state = migration.apply(state, schema_editor) - self.record_migration(migration) - migration_recorded = True + if not schema_editor.deferred_sql: + self.record_migration(migration) + migration_recorded = True if not migration_recorded: self.record_migration(migration) # Report progress diff --git a/tests/migrations/test_executor.py b/tests/migrations/test_executor.py index 8cab8732c4..e61d8f1276 100644 --- a/tests/migrations/test_executor.py +++ b/tests/migrations/test_executor.py @@ -684,6 +684,33 @@ class ExecutorTests(MigrationTestBase): ) self.assertTableNotExists('record_migration_model') + def test_migrations_not_applied_on_deferred_sql_failure(self): + """Migrations are not recorded if deferred SQL application fails.""" + class DeferredSQL: + def __str__(self): + raise DatabaseError('Failed to apply deferred SQL') + + class Migration(migrations.Migration): + atomic = False + + def apply(self, project_state, schema_editor, collect_sql=False): + schema_editor.deferred_sql.append(DeferredSQL()) + + executor = MigrationExecutor(connection) + with self.assertRaisesMessage(DatabaseError, 'Failed to apply deferred SQL'): + executor.apply_migration( + ProjectState(), + Migration('0001_initial', 'deferred_sql'), + ) + # The migration isn't recorded as applied since it failed. + migration_recorder = MigrationRecorder(connection) + self.assertIs( + migration_recorder.migration_qs.filter( + app='deferred_sql', name='0001_initial', + ).exists(), + False, + ) + class FakeLoader: def __init__(self, graph, applied):