On Mon, 29 May 2017, Brian May wrote:
> Otherwise, I think we have three options. I recommend reading the Django
> ticket in full before deciding. 
[…]
> 1. Apply work around from
> https://code.djangoproject.com/ticket/28250#comment:1 by manually
[…]
> 2. Remove migration from postinst, and give instructions for manually
> updating the database. Modify
[…]
> 3. Drop lava-server from testing. 
[…]

Option 4. Fix Django 1.10 with the attached patches.

I don't have time right now to test them, but I would love if someone else
could try them... the idea is to not barf on the inconsistent history if
we detect that the missing migration can be fake-applied.

Cheers,
-- 
Raphaël Hertzog ◈ Debian Developer

Support Debian LTS: https://www.freexian.com/services/debian-lts.html
Learn to master Debian: https://debian-handbook.info/get/
>From ee93aeecc298f801b85cd49366e5a431d1867f0b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Rapha=C3=ABl=20Hertzog?= <hert...@debian.org>
Date: Mon, 29 May 2017 15:44:39 +0200
Subject: [PATCH 1/2] Move detect_soft_applied() from
 django.db.migrations.executor to .loader
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

We want to be able to use that method in
loader.check_consistent_history() to accept an history where the initial
migration is going to be fake-applied. Since the executor has the
knowledge of the loader (but not the opposite), it makes sens to move
the code around.

Signed-off-by: Raphaël Hertzog <hert...@debian.org>
---
 django/db/migrations/executor.py | 81 +---------------------------------------
 django/db/migrations/loader.py   | 80 +++++++++++++++++++++++++++++++++++++++
 2 files changed, 81 insertions(+), 80 deletions(-)

diff --git a/django/db/migrations/executor.py b/django/db/migrations/executor.py
index 1a0b6f6322..ed5b64db60 100644
--- a/django/db/migrations/executor.py
+++ b/django/db/migrations/executor.py
@@ -1,7 +1,6 @@
 from __future__ import unicode_literals
 
 from django.apps.registry import apps as global_apps
-from django.db import migrations, router
 
 from .exceptions import InvalidMigrationPlan
 from .loader import MigrationLoader
@@ -235,7 +234,7 @@ class MigrationExecutor(object):
         if not fake:
             if fake_initial:
                 # Test to see if this is an already-applied initial migration
-                applied, state = self.detect_soft_applied(state, migration)
+                applied, state = self.loader.detect_soft_applied(state, migration)
                 if applied:
                     fake = True
             if not fake:
@@ -290,81 +289,3 @@ class MigrationExecutor(object):
             if all_applied and key not in applied:
                 self.recorder.record_applied(*key)
 
-    def detect_soft_applied(self, project_state, migration):
-        """
-        Tests whether a migration has been implicitly applied - that the
-        tables or columns it would create exist. This is intended only for use
-        on initial migrations (as it only looks for CreateModel and AddField).
-        """
-        def should_skip_detecting_model(migration, model):
-            """
-            No need to detect tables for proxy models, unmanaged models, or
-            models that can't be migrated on the current database.
-            """
-            return (
-                model._meta.proxy or not model._meta.managed or not
-                router.allow_migrate(
-                    self.connection.alias, migration.app_label,
-                    model_name=model._meta.model_name,
-                )
-            )
-
-        if migration.initial is None:
-            # Bail if the migration isn't the first one in its app
-            if any(app == migration.app_label for app, name in migration.dependencies):
-                return False, project_state
-        elif migration.initial is False:
-            # Bail if it's NOT an initial migration
-            return False, project_state
-
-        if project_state is None:
-            after_state = self.loader.project_state((migration.app_label, migration.name), at_end=True)
-        else:
-            after_state = migration.mutate_state(project_state)
-        apps = after_state.apps
-        found_create_model_migration = False
-        found_add_field_migration = False
-        existing_table_names = self.connection.introspection.table_names(self.connection.cursor())
-        # Make sure all create model and add field operations are done
-        for operation in migration.operations:
-            if isinstance(operation, migrations.CreateModel):
-                model = apps.get_model(migration.app_label, operation.name)
-                if model._meta.swapped:
-                    # We have to fetch the model to test with from the
-                    # main app cache, as it's not a direct dependency.
-                    model = global_apps.get_model(model._meta.swapped)
-                if should_skip_detecting_model(migration, model):
-                    continue
-                if model._meta.db_table not in existing_table_names:
-                    return False, project_state
-                found_create_model_migration = True
-            elif isinstance(operation, migrations.AddField):
-                model = apps.get_model(migration.app_label, operation.model_name)
-                if model._meta.swapped:
-                    # We have to fetch the model to test with from the
-                    # main app cache, as it's not a direct dependency.
-                    model = global_apps.get_model(model._meta.swapped)
-                if should_skip_detecting_model(migration, model):
-                    continue
-
-                table = model._meta.db_table
-                field = model._meta.get_field(operation.name)
-
-                # Handle implicit many-to-many tables created by AddField.
-                if field.many_to_many:
-                    if field.remote_field.through._meta.db_table not in existing_table_names:
-                        return False, project_state
-                    else:
-                        found_add_field_migration = True
-                        continue
-
-                column_names = [
-                    column.name for column in
-                    self.connection.introspection.get_table_description(self.connection.cursor(), table)
-                ]
-                if field.column not in column_names:
-                    return False, project_state
-                found_add_field_migration = True
-        # If we get this far and we found at least one CreateModel or AddField migration,
-        # the migration is considered implicitly applied.
-        return (found_create_model_migration or found_add_field_migration), after_state
diff --git a/django/db/migrations/loader.py b/django/db/migrations/loader.py
index 3a34e6da33..98053d029d 100644
--- a/django/db/migrations/loader.py
+++ b/django/db/migrations/loader.py
@@ -6,6 +6,7 @@ from importlib import import_module
 
 from django.apps import apps
 from django.conf import settings
+from django.db import migrations, router
 from django.db.migrations.graph import MigrationGraph
 from django.db.migrations.recorder import MigrationRecorder
 from django.utils import six
@@ -315,3 +316,82 @@ class MigrationLoader(object):
         See graph.make_state for the meaning of "nodes" and "at_end"
         """
         return self.graph.make_state(nodes=nodes, at_end=at_end, real_apps=list(self.unmigrated_apps))
+
+    def detect_soft_applied(self, project_state, migration):
+        """
+        Tests whether a migration has been implicitly applied - that the
+        tables or columns it would create exist. This is intended only for use
+        on initial migrations (as it only looks for CreateModel and AddField).
+        """
+        def should_skip_detecting_model(migration, model):
+            """
+            No need to detect tables for proxy models, unmanaged models, or
+            models that can't be migrated on the current database.
+            """
+            return (
+                model._meta.proxy or not model._meta.managed or not
+                router.allow_migrate(
+                    self.connection.alias, migration.app_label,
+                    model_name=model._meta.model_name,
+                )
+            )
+
+        if migration.initial is None:
+            # Bail if the migration isn't the first one in its app
+            if any(app == migration.app_label for app, name in migration.dependencies):
+                return False, project_state
+        elif migration.initial is False:
+            # Bail if it's NOT an initial migration
+            return False, project_state
+
+        if project_state is None:
+            after_state = self.project_state((migration.app_label, migration.name), at_end=True)
+        else:
+            after_state = migration.mutate_state(project_state)
+        apps = after_state.apps
+        found_create_model_migration = False
+        found_add_field_migration = False
+        existing_table_names = self.connection.introspection.table_names(self.connection.cursor())
+        # Make sure all create model and add field operations are done
+        for operation in migration.operations:
+            if isinstance(operation, migrations.CreateModel):
+                model = apps.get_model(migration.app_label, operation.name)
+                if model._meta.swapped:
+                    # We have to fetch the model to test with from the
+                    # main app cache, as it's not a direct dependency.
+                    model = global_apps.get_model(model._meta.swapped)
+                if should_skip_detecting_model(migration, model):
+                    continue
+                if model._meta.db_table not in existing_table_names:
+                    return False, project_state
+                found_create_model_migration = True
+            elif isinstance(operation, migrations.AddField):
+                model = apps.get_model(migration.app_label, operation.model_name)
+                if model._meta.swapped:
+                    # We have to fetch the model to test with from the
+                    # main app cache, as it's not a direct dependency.
+                    model = global_apps.get_model(model._meta.swapped)
+                if should_skip_detecting_model(migration, model):
+                    continue
+
+                table = model._meta.db_table
+                field = model._meta.get_field(operation.name)
+
+                # Handle implicit many-to-many tables created by AddField.
+                if field.many_to_many:
+                    if field.remote_field.through._meta.db_table not in existing_table_names:
+                        return False, project_state
+                    else:
+                        found_add_field_migration = True
+                        continue
+
+                column_names = [
+                    column.name for column in
+                    self.connection.introspection.get_table_description(self.connection.cursor(), table)
+                ]
+                if field.column not in column_names:
+                    return False, project_state
+                found_add_field_migration = True
+        # If we get this far and we found at least one CreateModel or AddField migration,
+        # the migration is considered implicitly applied.
+        return (found_create_model_migration or found_add_field_migration), after_state
-- 
2.11.0

>From c08ac1dfa045b08ee76decec6f2f70540915aafb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Rapha=C3=ABl=20Hertzog?= <hert...@debian.org>
Date: Mon, 29 May 2017 16:20:49 +0200
Subject: [PATCH 2/2] Fix #25850 -- improve migration history consistency check

With this change, we are now accepting unsatisfied dependencies on initial
migrations that are going to be fake-applied in the next migrate run.
This is a regression compared to 1.8 when we have to deal with
migration dependencies on applications who started without migrations.
---
 django/core/management/commands/migrate.py | 3 ++-
 django/db/migrations/loader.py             | 5 ++++-
 2 files changed, 6 insertions(+), 2 deletions(-)

diff --git a/django/core/management/commands/migrate.py b/django/core/management/commands/migrate.py
index 6846b33d2a..e589b0b0ae 100644
--- a/django/core/management/commands/migrate.py
+++ b/django/core/management/commands/migrate.py
@@ -83,7 +83,8 @@ class Command(BaseCommand):
         executor = MigrationExecutor(connection, self.migration_progress_callback)
 
         # Raise an error if any migrations are applied before their dependencies.
-        executor.loader.check_consistent_history(connection)
+        executor.loader.check_consistent_history(
+            connection, fake_initial=options['fake_initial'])
 
         # Before anything else, see if there's conflicting apps and drop out
         # hard if there are any
diff --git a/django/db/migrations/loader.py b/django/db/migrations/loader.py
index 98053d029d..ea5b1262b8 100644
--- a/django/db/migrations/loader.py
+++ b/django/db/migrations/loader.py
@@ -268,7 +268,7 @@ class MigrationLoader(object):
                     six.reraise(NodeNotFoundError, exc_value, sys.exc_info()[2])
             raise exc
 
-    def check_consistent_history(self, connection):
+    def check_consistent_history(self, connection, fake_initial=False):
         """
         Raise InconsistentMigrationHistory if any applied migrations have
         unapplied dependencies.
@@ -286,6 +286,9 @@ class MigrationLoader(object):
                     if parent in self.replacements:
                         if all(m in applied for m in self.replacements[parent].replaces):
                             continue
+                    # Skip initial migration that is going to be fake-applied
+                    if fake_initial and self.detect_soft_applied(None, parent):
+                        continue
                     raise InconsistentMigrationHistory(
                         "Migration {}.{} is applied before its dependency "
                         "{}.{} on database '{}'.".format(
-- 
2.11.0

Reply via email to