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