mirror of
				https://github.com/django/django.git
				synced 2025-10-31 09:41:08 +00:00 
			
		
		
		
	[1.7.x] Rewrote migration autodetector to involve actual computer science.
Fixes #22605, #22735; also lays the ground for some other fixes. Conflicts: django/db/migrations/autodetector.py
This commit is contained in:
		| @@ -7,6 +7,7 @@ from django.db import models | |||||||
| from django.db.migrations import operations | from django.db.migrations import operations | ||||||
| from django.db.migrations.migration import Migration | from django.db.migrations.migration import Migration | ||||||
| from django.db.migrations.questioner import MigrationQuestioner | from django.db.migrations.questioner import MigrationQuestioner | ||||||
|  | from django.db.migrations.optimizer import MigrationOptimizer | ||||||
|  |  | ||||||
|  |  | ||||||
| class MigrationAutodetector(object): | class MigrationAutodetector(object): | ||||||
| @@ -39,6 +40,43 @@ class MigrationAutodetector(object): | |||||||
|             changes = self._trim_to_apps(changes, trim_to_apps) |             changes = self._trim_to_apps(changes, trim_to_apps) | ||||||
|         return changes |         return changes | ||||||
|  |  | ||||||
|  |     def deep_deconstruct(self, obj): | ||||||
|  |         """ | ||||||
|  |         Recursive deconstruction for a field and its arguments. | ||||||
|  |         Used for full comparison for rename/alter; sometimes a single-level | ||||||
|  |         deconstruction will not compare correctly. | ||||||
|  |         """ | ||||||
|  |         if not hasattr(obj, 'deconstruct'): | ||||||
|  |             return obj | ||||||
|  |         deconstructed = obj.deconstruct() | ||||||
|  |         if isinstance(obj, models.Field): | ||||||
|  |             # we have a field which also returns a name | ||||||
|  |             deconstructed = deconstructed[1:] | ||||||
|  |         path, args, kwargs = deconstructed | ||||||
|  |         return ( | ||||||
|  |             path, | ||||||
|  |             [self.deep_deconstruct(value) for value in args], | ||||||
|  |             dict( | ||||||
|  |                 (key, self.deep_deconstruct(value)) | ||||||
|  |                 for key, value in kwargs.items() | ||||||
|  |             ), | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     def only_relation_agnostic_fields(self, fields): | ||||||
|  |         """ | ||||||
|  |         Return a definition of the fields that ignores field names and | ||||||
|  |         what related fields actually relate to. | ||||||
|  |         Used for detecting renames (as, of course, the related fields | ||||||
|  |         change during renames) | ||||||
|  |         """ | ||||||
|  |         fields_def = [] | ||||||
|  |         for name, field in fields: | ||||||
|  |             deconstruction = self.deep_deconstruct(field) | ||||||
|  |             if field.rel and field.rel.to: | ||||||
|  |                 del deconstruction[2]['to'] | ||||||
|  |             fields_def.append(deconstruction) | ||||||
|  |         return fields_def | ||||||
|  |  | ||||||
|     def _detect_changes(self): |     def _detect_changes(self): | ||||||
|         """ |         """ | ||||||
|         Returns a dict of migration plans which will achieve the |         Returns a dict of migration plans which will achieve the | ||||||
| @@ -48,245 +86,443 @@ class MigrationAutodetector(object): | |||||||
|         The resulting migrations aren't specially named, but the names |         The resulting migrations aren't specially named, but the names | ||||||
|         do matter for dependencies inside the set. |         do matter for dependencies inside the set. | ||||||
|         """ |         """ | ||||||
|         # We'll store migrations as lists by app names for now |  | ||||||
|         self.migrations = {} |  | ||||||
|         old_apps = self.from_state.render(ignore_swappable=True) |  | ||||||
|         new_apps = self.to_state.render() |  | ||||||
|         # Prepare lists of old/new model keys that we care about |  | ||||||
|         # (i.e. ignoring proxy ones and unmigrated ones) |  | ||||||
|  |  | ||||||
|         old_model_keys = [] |         # The first phase is generating all the operations for each app | ||||||
|         for al, mn in self.from_state.models.keys(): |         # and gathering them into a big per-app list. | ||||||
|             model = old_apps.get_model(al, mn) |         # We'll then go through that list later and order it and split | ||||||
|  |         # into migrations to resolve dependencies caused by M2Ms and FKs. | ||||||
|  |         self.generated_operations = {} | ||||||
|  |  | ||||||
|  |         # Prepare some old/new state and model lists, ignoring | ||||||
|  |         # proxy models and unmigrated apps. | ||||||
|  |         self.old_apps = self.from_state.render(ignore_swappable=True) | ||||||
|  |         self.new_apps = self.to_state.render() | ||||||
|  |         self.old_model_keys = [] | ||||||
|  |         for al, mn in sorted(self.from_state.models.keys()): | ||||||
|  |             model = self.old_apps.get_model(al, mn) | ||||||
|             if not model._meta.proxy and model._meta.managed and al not in self.from_state.real_apps: |             if not model._meta.proxy and model._meta.managed and al not in self.from_state.real_apps: | ||||||
|                 old_model_keys.append((al, mn)) |                 self.old_model_keys.append((al, mn)) | ||||||
|  |         self.new_model_keys = [] | ||||||
|  |         for al, mn in sorted(self.to_state.models.keys()): | ||||||
|  |             model = self.new_apps.get_model(al, mn) | ||||||
|  |             if not model._meta.proxy and model._meta.managed and al not in self.from_state.real_apps: | ||||||
|  |                 self.new_model_keys.append((al, mn)) | ||||||
|  |  | ||||||
|         new_model_keys = [] |         # Renames have to come first | ||||||
|         for al, mn in self.to_state.models.keys(): |         self.generate_renamed_models() | ||||||
|             model = new_apps.get_model(al, mn) |  | ||||||
|             if not model._meta.proxy and model._meta.managed and al not in self.to_state.real_apps: |  | ||||||
|                 new_model_keys.append((al, mn)) |  | ||||||
|  |  | ||||||
|         def _deep_deconstruct(obj, field=True): |         # Prepare field lists, and prepare a list of the fields that used | ||||||
|             """ |         # through models in the old state so we can make dependencies | ||||||
|             Recursive deconstruction for a field and its arguments. |         # from the through model deletion to the field that uses it. | ||||||
|             """ |         self.kept_model_keys = set(self.old_model_keys).intersection(self.new_model_keys) | ||||||
|             if not hasattr(obj, 'deconstruct'): |         self.through_users = {} | ||||||
|                 return obj |         self.old_field_keys = set() | ||||||
|             deconstructed = obj.deconstruct() |         self.new_field_keys = set() | ||||||
|             if field: |         for app_label, model_name in sorted(self.kept_model_keys): | ||||||
|                 deconstructed = deconstructed[1:] |             old_model_name = self.renamed_models.get((app_label, model_name), model_name) | ||||||
|             name, args, kwargs = deconstructed |             old_model_state = self.from_state.models[app_label, old_model_name] | ||||||
|  |             new_model_state = self.to_state.models[app_label, model_name] | ||||||
|  |             self.old_field_keys.update((app_label, model_name, x) for x, y in old_model_state.fields) | ||||||
|  |             self.new_field_keys.update((app_label, model_name, x) for x, y in new_model_state.fields) | ||||||
|  |             # Through model stuff | ||||||
|  |             for field_name, field in old_model_state.fields: | ||||||
|  |                 old_field = self.old_apps.get_model(app_label, old_model_name)._meta.get_field_by_name(field_name)[0] | ||||||
|  |                 if hasattr(old_field, "rel") and hasattr(old_field.rel, "through") and not old_field.rel.through._meta.auto_created: | ||||||
|  |                     through_key = ( | ||||||
|  |                         old_field.rel.through._meta.app_label, | ||||||
|  |                         old_field.rel.through._meta.object_name.lower(), | ||||||
|  |                     ) | ||||||
|  |                     self.through_users[through_key] = (app_label, old_model_name, field_name) | ||||||
|  |  | ||||||
|  |         # Generate non-rename model operations | ||||||
|  |         self.generate_created_models() | ||||||
|  |         self.generate_deleted_models() | ||||||
|  |  | ||||||
|  |         # Generate field operations | ||||||
|  |         self.generate_added_fields() | ||||||
|  |         self.generate_removed_fields() | ||||||
|  |         self.generate_altered_fields() | ||||||
|  |         self.generate_altered_unique_together() | ||||||
|  |         self.generate_altered_index_together() | ||||||
|  |  | ||||||
|  |         # Now, reordering to make things possible. The order we have already | ||||||
|  |         # isn't bad, but we need to pull a few things around so FKs work nicely | ||||||
|  |         # inside the same app | ||||||
|  |         for app_label, ops in sorted(self.generated_operations.items()): | ||||||
|  |             for i in range(10000): | ||||||
|  |                 found = False | ||||||
|  |                 for i, op in enumerate(ops): | ||||||
|  |                     for dep in op._auto_deps: | ||||||
|  |                         if dep[0] == app_label: | ||||||
|  |                             # Alright, there's a dependency on the same app. | ||||||
|  |                             for j, op2 in enumerate(ops): | ||||||
|  |                                 if self.check_dependency(op2, dep) and j > i: | ||||||
|  |                                     ops = ops[:i] + ops[i+1:j+1] + [op] + ops[j+1:] | ||||||
|  |                                     found = True | ||||||
|  |                                     break | ||||||
|  |                         if found: | ||||||
|  |                             break | ||||||
|  |                     if found: | ||||||
|  |                         break | ||||||
|  |                 if not found: | ||||||
|  |                     break | ||||||
|  |             else: | ||||||
|  |                 raise ValueError("Infinite loop caught in operation dependency resolution") | ||||||
|  |             self.generated_operations[app_label] = ops | ||||||
|  |  | ||||||
|  |         # Now, we need to chop the lists of operations up into migrations with | ||||||
|  |         # dependencies on each other. | ||||||
|  |         # We do this by stepping up an app's list of operations until we | ||||||
|  |         # find one that has an outgoing dependency that isn't in another app's | ||||||
|  |         # migration yet (hasn't been chopped off its list). We then chop off the | ||||||
|  |         # operations before it into a migration and move onto the next app. | ||||||
|  |         # If we loop back around without doing anything, there's a circular | ||||||
|  |         # dependency (which _should_ be impossible as the operations are all | ||||||
|  |         # split at this point so they can't depend and be depended on) | ||||||
|  |  | ||||||
|  |         self.migrations = {} | ||||||
|  |         num_ops = sum(len(x) for x in self.generated_operations.values()) | ||||||
|  |         chop_mode = False | ||||||
|  |         while num_ops: | ||||||
|  |             # On every iteration, we step through all the apps and see if there | ||||||
|  |             # is a completed set of operations. | ||||||
|  |             # If we find that a subset of the operations are complete we can | ||||||
|  |             # try to chop it off from the rest and continue, but we only | ||||||
|  |             # do this if we've already been through the list once before | ||||||
|  |             # without any chopping and nothing has changed. | ||||||
|  |             for app_label in sorted(self.generated_operations.keys()): | ||||||
|  |                 chopped = [] | ||||||
|  |                 dependencies = set() | ||||||
|  |                 for operation in list(self.generated_operations[app_label]): | ||||||
|  |                     deps_satisfied = True | ||||||
|  |                     operation_dependencies = set() | ||||||
|  |                     for dep in operation._auto_deps: | ||||||
|  |                         if dep[0] == "__setting__": | ||||||
|  |                             operation_dependencies.add((dep[0], dep[1])) | ||||||
|  |                         elif dep[0] != app_label: | ||||||
|  |                             # External app dependency. See if it's not yet | ||||||
|  |                             # satisfied. | ||||||
|  |                             for other_operation in self.generated_operations[dep[0]]: | ||||||
|  |                                 if self.check_dependency(other_operation, dep): | ||||||
|  |                                     deps_satisfied = False | ||||||
|  |                                     break | ||||||
|  |                             if not deps_satisfied: | ||||||
|  |                                 break | ||||||
|  |                             else: | ||||||
|  |                                 if self.migrations.get(dep[0], None): | ||||||
|  |                                     operation_dependencies.add((dep[0], self.migrations[dep[0]][-1].name)) | ||||||
|  |                                 else: | ||||||
|  |                                     operation_dependencies.add((dep[0], "__latest__")) | ||||||
|  |                     if deps_satisfied: | ||||||
|  |                         chopped.append(operation) | ||||||
|  |                         dependencies.update(operation_dependencies) | ||||||
|  |                         self.generated_operations[app_label] = self.generated_operations[app_label][1:] | ||||||
|  |                     else: | ||||||
|  |                         break | ||||||
|  |                 # Make a migration! Well, only if there's stuff to put in it | ||||||
|  |                 if dependencies or chopped: | ||||||
|  |                     if not self.generated_operations[app_label] or chop_mode: | ||||||
|  |                         subclass = type(str("Migration"), (Migration,), {"operations": [], "dependencies": []}) | ||||||
|  |                         instance = subclass("auto_%i" % (len(self.migrations.get(app_label, [])) + 1), app_label) | ||||||
|  |                         instance.dependencies = list(dependencies) | ||||||
|  |                         instance.operations = chopped | ||||||
|  |                         self.migrations.setdefault(app_label, []).append(instance) | ||||||
|  |                         chop_mode = False | ||||||
|  |                     else: | ||||||
|  |                         self.generated_operations[app_label] = chopped + self.generated_operations[app_label] | ||||||
|  |             new_num_ops = sum(len(x) for x in self.generated_operations.values()) | ||||||
|  |             if new_num_ops == num_ops: | ||||||
|  |                 if not chop_mode: | ||||||
|  |                     chop_mode = True | ||||||
|  |                 else: | ||||||
|  |                     raise ValueError("Cannot resolve operation dependencies") | ||||||
|  |             num_ops = new_num_ops | ||||||
|  |  | ||||||
|  |         # OK, add in internal dependencies among the migrations | ||||||
|  |         for app_label, migrations in self.migrations.items(): | ||||||
|  |             for m1, m2 in zip(migrations, migrations[1:]): | ||||||
|  |                 m2.dependencies.append((app_label, m1.name)) | ||||||
|  |  | ||||||
|  |         # De-dupe dependencies | ||||||
|  |         for app_label, migrations in self.migrations.items(): | ||||||
|  |             for migration in migrations: | ||||||
|  |                 migration.dependencies = list(set(migration.dependencies)) | ||||||
|  |  | ||||||
|  |         # Optimize migrations | ||||||
|  |         for app_label, migrations in self.migrations.items(): | ||||||
|  |             for migration in migrations: | ||||||
|  |                 migration.operations = MigrationOptimizer().optimize(migration.operations, app_label=app_label) | ||||||
|  |  | ||||||
|  |         return self.migrations | ||||||
|  |  | ||||||
|  |     def check_dependency(self, operation, dependency): | ||||||
|  |         """ | ||||||
|  |         Checks if an operation dependency matches an operation. | ||||||
|  |         """ | ||||||
|  |         # Created model | ||||||
|  |         if dependency[2] is None and dependency[3] is True: | ||||||
|             return ( |             return ( | ||||||
|                 name, |                 isinstance(operation, operations.CreateModel) and | ||||||
|                 [_deep_deconstruct(value, field=False) for value in args], |                 operation.name.lower() == dependency[1].lower() | ||||||
|                 dict([(key, _deep_deconstruct(value, field=False)) |  | ||||||
|                       for key, value in kwargs.items()]) |  | ||||||
|             ) |             ) | ||||||
|  |         # Created field | ||||||
|  |         elif dependency[2] is not None and dependency[3] is True: | ||||||
|  |             return ( | ||||||
|  |                 ( | ||||||
|  |                     isinstance(operation, operations.CreateModel) and | ||||||
|  |                     operation.name.lower() == dependency[1].lower() and | ||||||
|  |                     any(dependency[2] == x for x, y in operation.fields) | ||||||
|  |                 ) or | ||||||
|  |                 ( | ||||||
|  |                     isinstance(operation, operations.AddField) and | ||||||
|  |                     operation.model_name.lower() == dependency[1].lower() and | ||||||
|  |                     operation.name.lower() == dependency[2].lower() | ||||||
|  |                 ) | ||||||
|  |             ) | ||||||
|  |         # Removed field | ||||||
|  |         elif dependency[2] is not None and dependency[3] is False: | ||||||
|  |             return ( | ||||||
|  |                 isinstance(operation, operations.RemoveField) and | ||||||
|  |                 operation.model_name.lower() == dependency[1].lower() and | ||||||
|  |                 operation.name.lower() == dependency[2].lower() | ||||||
|  |             ) | ||||||
|  |         # Unknown dependency. Raise an error. | ||||||
|  |         else: | ||||||
|  |             raise ValueError("Can't handle dependency %r" % dependency) | ||||||
|  |  | ||||||
|         def _rel_agnostic_fields_def(fields): |     def add_operation(self, app_label, operation, dependencies=None): | ||||||
|             """ |         # Dependencies are (app_label, model_name, field_name, create/delete as True/False) | ||||||
|             Return a definition of the fields that ignores field names and |         operation._auto_deps = dependencies or [] | ||||||
|             what related fields actually relate to. |         self.generated_operations.setdefault(app_label, []).append(operation) | ||||||
|             """ |  | ||||||
|             fields_def = [] |  | ||||||
|             for name, field in fields: |  | ||||||
|                 deconstruction = _deep_deconstruct(field) |  | ||||||
|                 if field.rel and field.rel.to: |  | ||||||
|                     del deconstruction[2]['to'] |  | ||||||
|                 fields_def.append(deconstruction) |  | ||||||
|             return fields_def |  | ||||||
|  |  | ||||||
|         # Find any renamed models. |     def generate_renamed_models(self): | ||||||
|         renamed_models = {} |         """ | ||||||
|         renamed_models_rel = {} |         Finds any renamed models, and generates the operations for them, | ||||||
|         added_models = set(new_model_keys) - set(old_model_keys) |         and removes the old entry from the model lists. | ||||||
|         for app_label, model_name in added_models: |         Must be run before other model-level generation. | ||||||
|  |         """ | ||||||
|  |         self.renamed_models = {} | ||||||
|  |         self.renamed_models_rel = {} | ||||||
|  |         added_models = set(self.new_model_keys) - set(self.old_model_keys) | ||||||
|  |         for app_label, model_name in sorted(added_models): | ||||||
|             model_state = self.to_state.models[app_label, model_name] |             model_state = self.to_state.models[app_label, model_name] | ||||||
|             model_fields_def = _rel_agnostic_fields_def(model_state.fields) |             model_fields_def = self.only_relation_agnostic_fields(model_state.fields) | ||||||
|  |  | ||||||
|             removed_models = set(old_model_keys) - set(new_model_keys) |             removed_models = set(self.old_model_keys) - set(self.new_model_keys) | ||||||
|             for rem_app_label, rem_model_name in removed_models: |             for rem_app_label, rem_model_name in removed_models: | ||||||
|                 if rem_app_label == app_label: |                 if rem_app_label == app_label: | ||||||
|                     rem_model_state = self.from_state.models[rem_app_label, rem_model_name] |                     rem_model_state = self.from_state.models[rem_app_label, rem_model_name] | ||||||
|                     rem_model_fields_def = _rel_agnostic_fields_def(rem_model_state.fields) |                     rem_model_fields_def = self.only_relation_agnostic_fields(rem_model_state.fields) | ||||||
|                     if model_fields_def == rem_model_fields_def: |                     if model_fields_def == rem_model_fields_def: | ||||||
|                         if self.questioner.ask_rename_model(rem_model_state, model_state): |                         if self.questioner.ask_rename_model(rem_model_state, model_state): | ||||||
|                             self.add_to_migration( |                             self.add_operation( | ||||||
|                                 app_label, |                                 app_label, | ||||||
|                                 operations.RenameModel( |                                 operations.RenameModel( | ||||||
|                                     old_name=rem_model_state.name, |                                     old_name=rem_model_state.name, | ||||||
|                                     new_name=model_state.name, |                                     new_name=model_state.name, | ||||||
|                                 ) |                                 ) | ||||||
|                             ) |                             ) | ||||||
|                             renamed_models[app_label, model_name] = rem_model_name |                             self.renamed_models[app_label, model_name] = rem_model_name | ||||||
|                             renamed_models_rel['%s.%s' % (rem_model_state.app_label, rem_model_state.name)] = '%s.%s' % (model_state.app_label, model_state.name) |                             self.renamed_models_rel['%s.%s' % (rem_model_state.app_label, rem_model_state.name)] = '%s.%s' % (model_state.app_label, model_state.name) | ||||||
|                             old_model_keys.remove((rem_app_label, rem_model_name)) |                             self.old_model_keys.remove((rem_app_label, rem_model_name)) | ||||||
|                             old_model_keys.append((app_label, model_name)) |                             self.old_model_keys.append((app_label, model_name)) | ||||||
|                             break |                             break | ||||||
|  |  | ||||||
|         # Adding models. Phase 1 is adding models with no outward relationships. |     def generate_created_models(self): | ||||||
|         added_models = set(new_model_keys) - set(old_model_keys) |         """ | ||||||
|         pending_add = {} |         Find all new models and make creation operations for them, | ||||||
|         for app_label, model_name in added_models: |         and separate operations to create any foreign key or M2M relationships | ||||||
|  |         (we'll optimise these back in later if we can) | ||||||
|  |  | ||||||
|  |         We also defer any model options that refer to collections of fields | ||||||
|  |         that might be deferred (e.g. unique_together, index_together) | ||||||
|  |         """ | ||||||
|  |         added_models = set(self.new_model_keys) - set(self.old_model_keys) | ||||||
|  |         for app_label, model_name in sorted(added_models): | ||||||
|             model_state = self.to_state.models[app_label, model_name] |             model_state = self.to_state.models[app_label, model_name] | ||||||
|             # Are there any relationships out from this model? if so, punt it to the next phase. |             # Gather related fields | ||||||
|             related_fields = [] |             related_fields = {} | ||||||
|             for field in new_apps.get_model(app_label, model_name)._meta.local_fields: |             for field in self.new_apps.get_model(app_label, model_name)._meta.local_fields: | ||||||
|                 if field.rel: |                 if field.rel: | ||||||
|                     if field.rel.to: |                     if field.rel.to: | ||||||
|                         related_fields.append((field.name, field.rel.to._meta.app_label, field.rel.to._meta.model_name)) |                         related_fields[field.name] = field | ||||||
|                     if hasattr(field.rel, "through") and not field.rel.through._meta.auto_created: |                     if hasattr(field.rel, "through") and not field.rel.through._meta.auto_created: | ||||||
|                         related_fields.append((field.name, field.rel.through._meta.app_label, field.rel.through._meta.model_name)) |                         related_fields[field.name] = field | ||||||
|             for field in new_apps.get_model(app_label, model_name)._meta.local_many_to_many: |             for field in self.new_apps.get_model(app_label, model_name)._meta.local_many_to_many: | ||||||
|                 if field.rel.to: |                 if field.rel.to: | ||||||
|                     related_fields.append((field.name, field.rel.to._meta.app_label, field.rel.to._meta.model_name)) |                     related_fields[field.name] = field | ||||||
|                 if hasattr(field.rel, "through") and not field.rel.through._meta.auto_created: |                 if hasattr(field.rel, "through") and not field.rel.through._meta.auto_created: | ||||||
|                     related_fields.append((field.name, field.rel.through._meta.app_label, field.rel.through._meta.model_name)) |                     related_fields[field.name] = field | ||||||
|             if related_fields: |             # Are there unique/index_together to defer? | ||||||
|                 pending_add[app_label, model_name] = related_fields |             unique_together = model_state.options.pop('unique_together', None) | ||||||
|             else: |             index_together = model_state.options.pop('index_together', None) | ||||||
|                 self.add_to_migration( |             # Generate creation operatoin | ||||||
|                     app_label, |             self.add_operation( | ||||||
|                     operations.CreateModel( |                 app_label, | ||||||
|                         name=model_state.name, |                 operations.CreateModel( | ||||||
|                         fields=model_state.fields, |                     name=model_state.name, | ||||||
|                         options=model_state.options, |                     fields=[d for d in model_state.fields if d[0] not in related_fields], | ||||||
|                         bases=model_state.bases, |                     options=model_state.options, | ||||||
|                     ) |                     bases=model_state.bases, | ||||||
|                 ) |                 ) | ||||||
|  |             ) | ||||||
|         # Phase 2 is progressively adding pending models, splitting up into two |             # Generate operations for each related field | ||||||
|         # migrations if required. |             for name, field in sorted(related_fields.items()): | ||||||
|         pending_new_fks = [] |                 # Account for FKs to swappable models | ||||||
|         pending_unique_together = [] |                 swappable_setting = getattr(field, 'swappable_setting', None) | ||||||
|         added_phase_2 = set() |  | ||||||
|         while pending_add: |  | ||||||
|             # Is there one we can add that has all dependencies satisfied? |  | ||||||
|             satisfied = [ |  | ||||||
|                 (m, rf) |  | ||||||
|                 for m, rf in pending_add.items() |  | ||||||
|                 if all((al, mn) not in pending_add for f, al, mn in rf) |  | ||||||
|             ] |  | ||||||
|             if satisfied: |  | ||||||
|                 (app_label, model_name), related_fields = sorted(satisfied)[0] |  | ||||||
|                 model_state = self.to_state.models[app_label, model_name] |  | ||||||
|                 self.add_to_migration( |  | ||||||
|                     app_label, |  | ||||||
|                     operations.CreateModel( |  | ||||||
|                         name=model_state.name, |  | ||||||
|                         fields=model_state.fields, |  | ||||||
|                         options=model_state.options, |  | ||||||
|                         bases=model_state.bases, |  | ||||||
|                     ), |  | ||||||
|                     # If it's already been added in phase 2 put it in a new |  | ||||||
|                     # migration for safety. |  | ||||||
|                     new=any((al, mn) in added_phase_2 for f, al, mn in related_fields), |  | ||||||
|                 ) |  | ||||||
|                 added_phase_2.add((app_label, model_name)) |  | ||||||
|             # Ah well, we'll need to split one. Pick deterministically. |  | ||||||
|             else: |  | ||||||
|                 (app_label, model_name), related_fields = sorted(pending_add.items())[0] |  | ||||||
|                 model_state = self.to_state.models[app_label, model_name] |  | ||||||
|                 # Defer unique together constraints creation, see ticket #22275 |  | ||||||
|                 unique_together_constraints = model_state.options.pop('unique_together', None) |  | ||||||
|                 if unique_together_constraints: |  | ||||||
|                     pending_unique_together.append((app_label, model_name, |  | ||||||
|                                                    unique_together_constraints)) |  | ||||||
|                 # Work out the fields that need splitting out |  | ||||||
|                 bad_fields = dict((f, (al, mn)) for f, al, mn in related_fields if (al, mn) in pending_add) |  | ||||||
|                 # Create the model, without those |  | ||||||
|                 self.add_to_migration( |  | ||||||
|                     app_label, |  | ||||||
|                     operations.CreateModel( |  | ||||||
|                         name=model_state.name, |  | ||||||
|                         fields=[(n, f) for n, f in model_state.fields if n not in bad_fields], |  | ||||||
|                         options=model_state.options, |  | ||||||
|                         bases=model_state.bases, |  | ||||||
|                     ) |  | ||||||
|                 ) |  | ||||||
|                 # Add the bad fields to be made in a phase 3 |  | ||||||
|                 for field_name, (other_app_label, other_model_name) in bad_fields.items(): |  | ||||||
|                     pending_new_fks.append((app_label, model_name, field_name, other_app_label)) |  | ||||||
|             for field_name, other_app_label, other_model_name in related_fields: |  | ||||||
|                 # If it depends on a swappable something, add a dynamic depend'cy |  | ||||||
|                 swappable_setting = new_apps.get_model(app_label, model_name)._meta.get_field_by_name(field_name)[0].swappable_setting |  | ||||||
|                 if swappable_setting is not None: |                 if swappable_setting is not None: | ||||||
|                     self.add_swappable_dependency(app_label, swappable_setting) |                     dep_app_label = "__setting__" | ||||||
|                 elif app_label != other_app_label: |                     dep_object_name = swappable_setting | ||||||
|                     self.add_dependency(app_label, other_app_label) |                 else: | ||||||
|             del pending_add[app_label, model_name] |                     dep_app_label = field.rel.to._meta.app_label | ||||||
|  |                     dep_object_name = field.rel.to._meta.object_name | ||||||
|         # Phase 3 is adding the final set of FKs as separate new migrations. |                 # Make operation | ||||||
|         for app_label, model_name, field_name, other_app_label in pending_new_fks: |                 self.add_operation( | ||||||
|             model_state = self.to_state.models[app_label, model_name] |                     app_label, | ||||||
|             self.add_to_migration( |                     operations.AddField( | ||||||
|                 app_label, |                         model_name=model_name, | ||||||
|                 operations.AddField( |                         name=name, | ||||||
|                     model_name=model_name, |                         field=field, | ||||||
|                     name=field_name, |                     ), | ||||||
|                     field=model_state.get_field_by_name(field_name), |                     dependencies = [ | ||||||
|                 ), |                         (dep_app_label, dep_object_name, None, True), | ||||||
|                 new=True, |                     ] | ||||||
|             ) |  | ||||||
|             # If it depends on a swappable something, add a dynamic depend'cy |  | ||||||
|             swappable_setting = new_apps.get_model(app_label, model_name)._meta.get_field_by_name(field_name)[0].swappable_setting |  | ||||||
|             if swappable_setting is not None: |  | ||||||
|                 self.add_swappable_dependency(app_label, swappable_setting) |  | ||||||
|             elif app_label != other_app_label: |  | ||||||
|                 self.add_dependency(app_label, other_app_label) |  | ||||||
|         # Phase 3.1 - unique together constraints |  | ||||||
|         for app_label, model_name, unique_together in pending_unique_together: |  | ||||||
|             self.add_to_migration( |  | ||||||
|                 app_label, |  | ||||||
|                 operations.AlterUniqueTogether( |  | ||||||
|                     name=model_name, |  | ||||||
|                     unique_together=unique_together |  | ||||||
|                 ) |                 ) | ||||||
|             ) |             # Generate other opns | ||||||
|         # Changes within models |             if unique_together: | ||||||
|         kept_models = set(old_model_keys).intersection(new_model_keys) |                 self.add_operation( | ||||||
|         old_fields = set() |  | ||||||
|         new_fields = set() |  | ||||||
|         unique_together_operations = [] |  | ||||||
|         for app_label, model_name in kept_models: |  | ||||||
|             old_model_name = renamed_models.get((app_label, model_name), model_name) |  | ||||||
|             old_model_state = self.from_state.models[app_label, old_model_name] |  | ||||||
|             new_model_state = self.to_state.models[app_label, model_name] |  | ||||||
|             # Collect field changes for later global dealing with (so AddFields |  | ||||||
|             # always come before AlterFields even on separate models) |  | ||||||
|             old_fields.update((app_label, model_name, x) for x, y in old_model_state.fields) |  | ||||||
|             new_fields.update((app_label, model_name, x) for x, y in new_model_state.fields) |  | ||||||
|             # Unique_together changes. Operations will be added to migration a |  | ||||||
|             # bit later, after fields creation. See ticket #22035. |  | ||||||
|             if old_model_state.options.get("unique_together", set()) != new_model_state.options.get("unique_together", set()): |  | ||||||
|                 unique_together_operations.append(( |  | ||||||
|                     app_label, |                     app_label, | ||||||
|                     operations.AlterUniqueTogether( |                     operations.AlterUniqueTogether( | ||||||
|                         name=model_name, |                         name=model_name, | ||||||
|                         unique_together=new_model_state.options.get("unique_together", set()), |                         unique_together=unique_together, | ||||||
|  |                     ), | ||||||
|  |                     dependencies = [ | ||||||
|  |                         (app_label, model_name, name, True) | ||||||
|  |                         for name, field in sorted(related_fields.items()) | ||||||
|  |                     ] | ||||||
|  |                 ) | ||||||
|  |             if index_together: | ||||||
|  |                 self.add_operation( | ||||||
|  |                     app_label, | ||||||
|  |                     operations.AlterIndexTogether( | ||||||
|  |                         name=model_name, | ||||||
|  |                         index_together=index_together, | ||||||
|  |                     ), | ||||||
|  |                     dependencies = [ | ||||||
|  |                         (app_label, model_name, name, True) | ||||||
|  |                         for name, field in sorted(related_fields.items()) | ||||||
|  |                     ] | ||||||
|  |                 ) | ||||||
|  |  | ||||||
|  |     def generate_deleted_models(self): | ||||||
|  |         """ | ||||||
|  |         Find all deleted models and make creation operations for them, | ||||||
|  |         and separate operations to delete any foreign key or M2M relationships | ||||||
|  |         (we'll optimise these back in later if we can) | ||||||
|  |  | ||||||
|  |         We also bring forward removal of any model options that refer to | ||||||
|  |         collections of fields - the inverse of generate_created_models. | ||||||
|  |         """ | ||||||
|  |         deleted_models = set(self.old_model_keys) - set(self.new_model_keys) | ||||||
|  |         for app_label, model_name in sorted(deleted_models): | ||||||
|  |             model_state = self.from_state.models[app_label, model_name] | ||||||
|  |             model = self.old_apps.get_model(app_label, model_name) | ||||||
|  |             # Gather related fields | ||||||
|  |             related_fields = {} | ||||||
|  |             for field in model._meta.local_fields: | ||||||
|  |                 if field.rel: | ||||||
|  |                     if field.rel.to: | ||||||
|  |                         related_fields[field.name] = field | ||||||
|  |                     if hasattr(field.rel, "through") and not field.rel.through._meta.auto_created: | ||||||
|  |                         related_fields[field.name] = field | ||||||
|  |             for field in model._meta.local_many_to_many: | ||||||
|  |                 if field.rel.to: | ||||||
|  |                     related_fields[field.name] = field | ||||||
|  |                 if hasattr(field.rel, "through") and not field.rel.through._meta.auto_created: | ||||||
|  |                     related_fields[field.name] = field | ||||||
|  |             # Generate option removal first | ||||||
|  |             unique_together = model_state.options.pop('unique_together', None) | ||||||
|  |             index_together = model_state.options.pop('index_together', None) | ||||||
|  |             if unique_together: | ||||||
|  |                 self.add_operation( | ||||||
|  |                     app_label, | ||||||
|  |                     operations.AlterUniqueTogether( | ||||||
|  |                         name=model_name, | ||||||
|  |                         unique_together=None, | ||||||
|                     ) |                     ) | ||||||
|  |                 ) | ||||||
|  |             if index_together: | ||||||
|  |                 self.add_operation( | ||||||
|  |                     app_label, | ||||||
|  |                     operations.AlterIndexTogether( | ||||||
|  |                         name=model_name, | ||||||
|  |                         index_together=None, | ||||||
|  |                     ) | ||||||
|  |                 ) | ||||||
|  |             # Then remove each related field | ||||||
|  |             for name, field in sorted(related_fields.items()): | ||||||
|  |                 self.add_operation( | ||||||
|  |                     app_label, | ||||||
|  |                     operations.RemoveField( | ||||||
|  |                         model_name=model_name, | ||||||
|  |                         name=name, | ||||||
|  |                     ) | ||||||
|  |                 ) | ||||||
|  |             # Finally, remove the model. | ||||||
|  |             # This depends on both the removal of all incoming fields | ||||||
|  |             # and the removal of all its own related fields, and if it's | ||||||
|  |             # a through model the field that references it. | ||||||
|  |             dependencies = [] | ||||||
|  |             for related_object in model._meta.get_all_related_objects(): | ||||||
|  |                 dependencies.append(( | ||||||
|  |                     related_object.model._meta.app_label, | ||||||
|  |                     related_object.model._meta.object_name, | ||||||
|  |                     related_object.field.name, | ||||||
|  |                     False, | ||||||
|                 )) |                 )) | ||||||
|  |             for related_object in model._meta.get_all_related_many_to_many_objects(): | ||||||
|  |                 dependencies.append(( | ||||||
|  |                     related_object.model._meta.app_label, | ||||||
|  |                     related_object.model._meta.object_name, | ||||||
|  |                     related_object.field.name, | ||||||
|  |                     False, | ||||||
|  |                 )) | ||||||
|  |             for name, field in sorted(related_fields.items()): | ||||||
|  |                 dependencies.append((app_label, model_name, name, False)) | ||||||
|  |             # We're referenced in another field's through= | ||||||
|  |             through_user = self.through_users.get((app_label, model_state.name.lower()), None) | ||||||
|  |             if through_user: | ||||||
|  |                 dependencies.append((through_user[0], through_user[1], through_user[2], False)) | ||||||
|  |             # Finally, make the operation, deduping any dependencies | ||||||
|  |             self.add_operation( | ||||||
|  |                 app_label, | ||||||
|  |                 operations.DeleteModel( | ||||||
|  |                     name=model_state.name, | ||||||
|  |                 ), | ||||||
|  |                 dependencies = list(set(dependencies)), | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |     def generate_added_fields(self): | ||||||
|         # New fields |         # New fields | ||||||
|         renamed_fields = {} |         self.renamed_fields = {} | ||||||
|         for app_label, model_name, field_name in new_fields - old_fields: |         for app_label, model_name, field_name in sorted(self.new_field_keys - self.old_field_keys): | ||||||
|             old_model_name = renamed_models.get((app_label, model_name), model_name) |             old_model_name = self.renamed_models.get((app_label, model_name), model_name) | ||||||
|             old_model_state = self.from_state.models[app_label, old_model_name] |             old_model_state = self.from_state.models[app_label, old_model_name] | ||||||
|             new_model_state = self.to_state.models[app_label, model_name] |             new_model_state = self.to_state.models[app_label, model_name] | ||||||
|             field = new_model_state.get_field_by_name(field_name) |             field = new_model_state.get_field_by_name(field_name) | ||||||
|             # Scan to see if this is actually a rename! |             # Scan to see if this is actually a rename! | ||||||
|             field_dec = _deep_deconstruct(field) |             field_dec = self.deep_deconstruct(field) | ||||||
|             found_rename = False |             found_rename = False | ||||||
|             for rem_app_label, rem_model_name, rem_field_name in (old_fields - new_fields): |             for rem_app_label, rem_model_name, rem_field_name in sorted(self.old_field_keys - self.new_field_keys): | ||||||
|                 if rem_app_label == app_label and rem_model_name == model_name: |                 if rem_app_label == app_label and rem_model_name == model_name: | ||||||
|                     old_field_dec = _deep_deconstruct(old_model_state.get_field_by_name(rem_field_name)) |                     old_field_dec = self.deep_deconstruct(old_model_state.get_field_by_name(rem_field_name)) | ||||||
|                     if field.rel and field.rel.to and 'to' in old_field_dec[2]: |                     if field.rel and field.rel.to and 'to' in old_field_dec[2]: | ||||||
|                         old_rel_to = old_field_dec[2]['to'] |                         old_rel_to = old_field_dec[2]['to'] | ||||||
|                         if old_rel_to in renamed_models_rel: |                         if old_rel_to in self.renamed_models_rel: | ||||||
|                             old_field_dec[2]['to'] = renamed_models_rel[old_rel_to] |                             old_field_dec[2]['to'] = self.renamed_models_rel[old_rel_to] | ||||||
|                     if old_field_dec == field_dec: |                     if old_field_dec == field_dec: | ||||||
|                         if self.questioner.ask_rename(model_name, rem_field_name, field_name, field): |                         if self.questioner.ask_rename(model_name, rem_field_name, field_name, field): | ||||||
|                             self.add_to_migration( |                             self.add_operation( | ||||||
|                                 app_label, |                                 app_label, | ||||||
|                                 operations.RenameField( |                                 operations.RenameField( | ||||||
|                                     model_name=model_name, |                                     model_name=model_name, | ||||||
| @@ -294,9 +530,9 @@ class MigrationAutodetector(object): | |||||||
|                                     new_name=field_name, |                                     new_name=field_name, | ||||||
|                                 ) |                                 ) | ||||||
|                             ) |                             ) | ||||||
|                             old_fields.remove((rem_app_label, rem_model_name, rem_field_name)) |                             self.old_field_keys.remove((rem_app_label, rem_model_name, rem_field_name)) | ||||||
|                             old_fields.add((app_label, model_name, field_name)) |                             self.old_field_keys.add((app_label, model_name, field_name)) | ||||||
|                             renamed_fields[app_label, model_name, field_name] = rem_field_name |                             self.renamed_fields[app_label, model_name, field_name] = rem_field_name | ||||||
|                             found_rename = True |                             found_rename = True | ||||||
|                             break |                             break | ||||||
|             if found_rename: |             if found_rename: | ||||||
| @@ -305,7 +541,7 @@ class MigrationAutodetector(object): | |||||||
|             if not field.null and not field.has_default() and not isinstance(field, models.ManyToManyField): |             if not field.null and not field.has_default() and not isinstance(field, models.ManyToManyField): | ||||||
|                 field = field.clone() |                 field = field.clone() | ||||||
|                 field.default = self.questioner.ask_not_null_addition(field_name, model_name) |                 field.default = self.questioner.ask_not_null_addition(field_name, model_name) | ||||||
|                 self.add_to_migration( |                 self.add_operation( | ||||||
|                     app_label, |                     app_label, | ||||||
|                     operations.AddField( |                     operations.AddField( | ||||||
|                         model_name=model_name, |                         model_name=model_name, | ||||||
| @@ -315,7 +551,7 @@ class MigrationAutodetector(object): | |||||||
|                     ) |                     ) | ||||||
|                 ) |                 ) | ||||||
|             else: |             else: | ||||||
|                 self.add_to_migration( |                 self.add_operation( | ||||||
|                     app_label, |                     app_label, | ||||||
|                     operations.AddField( |                     operations.AddField( | ||||||
|                         model_name=model_name, |                         model_name=model_name, | ||||||
| @@ -323,33 +559,34 @@ class MigrationAutodetector(object): | |||||||
|                         field=field, |                         field=field, | ||||||
|                     ) |                     ) | ||||||
|                 ) |                 ) | ||||||
|                 new_field = new_apps.get_model(app_label, model_name)._meta.get_field_by_name(field_name)[0] |  | ||||||
|                 swappable_setting = getattr(new_field, 'swappable_setting', None) |     def generate_removed_fields(self): | ||||||
|                 if swappable_setting is not None: |         """ | ||||||
|                     self.add_swappable_dependency(app_label, swappable_setting) |         Fields that have been removed. | ||||||
|         # Old fields |         """ | ||||||
|         for app_label, model_name, field_name in old_fields - new_fields: |         for app_label, model_name, field_name in sorted(self.old_field_keys - self.new_field_keys): | ||||||
|             old_model_name = renamed_models.get((app_label, model_name), model_name) |             self.add_operation( | ||||||
|             old_model_state = self.from_state.models[app_label, old_model_name] |  | ||||||
|             new_model_state = self.to_state.models[app_label, model_name] |  | ||||||
|             self.add_to_migration( |  | ||||||
|                 app_label, |                 app_label, | ||||||
|                 operations.RemoveField( |                 operations.RemoveField( | ||||||
|                     model_name=model_name, |                     model_name=model_name, | ||||||
|                     name=field_name, |                     name=field_name, | ||||||
|                 ) |                 ) | ||||||
|             ) |             ) | ||||||
|         # The same fields |  | ||||||
|         for app_label, model_name, field_name in old_fields.intersection(new_fields): |     def generate_altered_fields(self): | ||||||
|  |         """ | ||||||
|  |         Fields that have been altered. | ||||||
|  |         """ | ||||||
|  |         for app_label, model_name, field_name in sorted(self.old_field_keys.intersection(self.new_field_keys)): | ||||||
|             # Did the field change? |             # Did the field change? | ||||||
|             old_model_name = renamed_models.get((app_label, model_name), model_name) |             old_model_name = self.renamed_models.get((app_label, model_name), model_name) | ||||||
|             old_model_state = self.from_state.models[app_label, old_model_name] |             old_model_state = self.from_state.models[app_label, old_model_name] | ||||||
|             new_model_state = self.to_state.models[app_label, model_name] |             new_model_state = self.to_state.models[app_label, model_name] | ||||||
|             old_field_name = renamed_fields.get((app_label, model_name, field_name), field_name) |             old_field_name = self.renamed_fields.get((app_label, model_name, field_name), field_name) | ||||||
|             old_field_dec = _deep_deconstruct(old_model_state.get_field_by_name(old_field_name)) |             old_field_dec = self.deep_deconstruct(old_model_state.get_field_by_name(old_field_name)) | ||||||
|             new_field_dec = _deep_deconstruct(new_model_state.get_field_by_name(field_name)) |             new_field_dec = self.deep_deconstruct(new_model_state.get_field_by_name(field_name)) | ||||||
|             if old_field_dec != new_field_dec: |             if old_field_dec != new_field_dec: | ||||||
|                 self.add_to_migration( |                 self.add_operation( | ||||||
|                     app_label, |                     app_label, | ||||||
|                     operations.AlterField( |                     operations.AlterField( | ||||||
|                         model_name=model_name, |                         model_name=model_name, | ||||||
| @@ -357,53 +594,34 @@ class MigrationAutodetector(object): | |||||||
|                         field=new_model_state.get_field_by_name(field_name), |                         field=new_model_state.get_field_by_name(field_name), | ||||||
|                     ) |                     ) | ||||||
|                 ) |                 ) | ||||||
|         for app_label, operation in unique_together_operations: |  | ||||||
|             self.add_to_migration(app_label, operation) |     def generate_altered_unique_together(self): | ||||||
|         # Removing models |         for app_label, model_name in sorted(self.kept_model_keys): | ||||||
|         removed_models = set(old_model_keys) - set(new_model_keys) |             old_model_name = self.renamed_models.get((app_label, model_name), model_name) | ||||||
|         for app_label, model_name in removed_models: |             old_model_state = self.from_state.models[app_label, old_model_name] | ||||||
|             model_state = self.from_state.models[app_label, model_name] |             new_model_state = self.to_state.models[app_label, model_name] | ||||||
|             self.add_to_migration( |             if old_model_state.options.get("unique_together", None) != new_model_state.options.get("unique_together", None): | ||||||
|                 app_label, |                 self.add_operation( | ||||||
|                 operations.DeleteModel( |                     app_label, | ||||||
|                     model_state.name, |                     operations.AlterUniqueTogether( | ||||||
|  |                         name=model_name, | ||||||
|  |                         unique_together=new_model_state.options['unique_together'], | ||||||
|  |                     ) | ||||||
|                 ) |                 ) | ||||||
|             ) |  | ||||||
|         # Alright, now add internal dependencies |  | ||||||
|         for app_label, migrations in self.migrations.items(): |  | ||||||
|             for m1, m2 in zip(migrations, migrations[1:]): |  | ||||||
|                 m2.dependencies.append((app_label, m1.name)) |  | ||||||
|         # Clean up dependencies |  | ||||||
|         for app_label, migrations in self.migrations.items(): |  | ||||||
|             for migration in migrations: |  | ||||||
|                 migration.dependencies = list(set(migration.dependencies)) |  | ||||||
|         return self.migrations |  | ||||||
|  |  | ||||||
|     def add_to_migration(self, app_label, operation, new=False): |     def generate_altered_index_together(self): | ||||||
|         migrations = self.migrations.setdefault(app_label, []) |         for app_label, model_name in sorted(self.kept_model_keys): | ||||||
|         if not migrations or new: |             old_model_name = self.renamed_models.get((app_label, model_name), model_name) | ||||||
|             subclass = type(str("Migration"), (Migration,), {"operations": [], "dependencies": []}) |             old_model_state = self.from_state.models[app_label, old_model_name] | ||||||
|             instance = subclass("auto_%i" % (len(migrations) + 1), app_label) |             new_model_state = self.to_state.models[app_label, model_name] | ||||||
|             migrations.append(instance) |             if old_model_state.options.get("index_together", None) != new_model_state.options.get("index_together", None): | ||||||
|         migrations[-1].operations.append(operation) |                 self.add_operation( | ||||||
|  |                     app_label, | ||||||
|     def add_dependency(self, app_label, other_app_label): |                     operations.AlterIndexTogether( | ||||||
|         """ |                         name=model_name, | ||||||
|         Adds a dependency to app_label's newest migration on |                         index_together=new_model_state.options['index_together'], | ||||||
|         other_app_label's latest migration. |                     ) | ||||||
|         """ |                 ) | ||||||
|         if self.migrations.get(other_app_label): |  | ||||||
|             dependency = (other_app_label, self.migrations[other_app_label][-1].name) |  | ||||||
|         else: |  | ||||||
|             dependency = (other_app_label, "__first__") |  | ||||||
|         self.migrations[app_label][-1].dependencies.append(dependency) |  | ||||||
|  |  | ||||||
|     def add_swappable_dependency(self, app_label, setting_name): |  | ||||||
|         """ |  | ||||||
|         Adds a dependency to the value of a swappable model setting. |  | ||||||
|         """ |  | ||||||
|         dependency = ("__setting__", setting_name) |  | ||||||
|         self.migrations[app_label][-1].dependencies.append(dependency) |  | ||||||
|  |  | ||||||
|     def arrange_for_graph(self, changes, graph): |     def arrange_for_graph(self, changes, graph): | ||||||
|         """ |         """ | ||||||
|   | |||||||
| @@ -51,7 +51,7 @@ class MigrationOptimizer(object): | |||||||
|         for i, operation in enumerate(operations): |         for i, operation in enumerate(operations): | ||||||
|             # Compare it to each operation after it |             # Compare it to each operation after it | ||||||
|             for j, other in enumerate(operations[i + 1:]): |             for j, other in enumerate(operations[i + 1:]): | ||||||
|                 result = self.reduce(operation, other) |                 result = self.reduce(operation, other, operations[i+1:i+j+1]) | ||||||
|                 if result is not None: |                 if result is not None: | ||||||
|                     # Optimize! Add result, then remaining others, then return |                     # Optimize! Add result, then remaining others, then return | ||||||
|                     new_operations.extend(result) |                     new_operations.extend(result) | ||||||
| @@ -67,7 +67,7 @@ class MigrationOptimizer(object): | |||||||
|  |  | ||||||
|     #### REDUCTION #### |     #### REDUCTION #### | ||||||
|  |  | ||||||
|     def reduce(self, operation, other): |     def reduce(self, operation, other, in_between=None): | ||||||
|         """ |         """ | ||||||
|         Either returns a list of zero, one or two operations, |         Either returns a list of zero, one or two operations, | ||||||
|         or None, meaning this pair cannot be optimized. |         or None, meaning this pair cannot be optimized. | ||||||
| @@ -156,24 +156,24 @@ class MigrationOptimizer(object): | |||||||
|         ] |         ] | ||||||
|         for ia, ib, om in submethods: |         for ia, ib, om in submethods: | ||||||
|             if isinstance(operation, ia) and isinstance(other, ib): |             if isinstance(operation, ia) and isinstance(other, ib): | ||||||
|                 return om(operation, other) |                 return om(operation, other, in_between or []) | ||||||
|         return None |         return None | ||||||
|  |  | ||||||
|     def reduce_model_create_delete(self, operation, other): |     def reduce_model_create_delete(self, operation, other, in_between): | ||||||
|         """ |         """ | ||||||
|         Folds a CreateModel and a DeleteModel into nothing. |         Folds a CreateModel and a DeleteModel into nothing. | ||||||
|         """ |         """ | ||||||
|         if operation.name.lower() == other.name.lower(): |         if operation.name.lower() == other.name.lower(): | ||||||
|             return [] |             return [] | ||||||
|  |  | ||||||
|     def reduce_model_alter_delete(self, operation, other): |     def reduce_model_alter_delete(self, operation, other, in_between): | ||||||
|         """ |         """ | ||||||
|         Folds an AlterModelSomething and a DeleteModel into just delete. |         Folds an AlterModelSomething and a DeleteModel into just delete. | ||||||
|         """ |         """ | ||||||
|         if operation.name.lower() == other.name.lower(): |         if operation.name.lower() == other.name.lower(): | ||||||
|             return [other] |             return [other] | ||||||
|  |  | ||||||
|     def reduce_model_create_rename(self, operation, other): |     def reduce_model_create_rename(self, operation, other, in_between): | ||||||
|         """ |         """ | ||||||
|         Folds a model rename into its create |         Folds a model rename into its create | ||||||
|         """ |         """ | ||||||
| @@ -187,7 +187,7 @@ class MigrationOptimizer(object): | |||||||
|                 ) |                 ) | ||||||
|             ] |             ] | ||||||
|  |  | ||||||
|     def reduce_model_rename_self(self, operation, other): |     def reduce_model_rename_self(self, operation, other, in_between): | ||||||
|         """ |         """ | ||||||
|         Folds a model rename into another one |         Folds a model rename into another one | ||||||
|         """ |         """ | ||||||
| @@ -199,8 +199,17 @@ class MigrationOptimizer(object): | |||||||
|                 ) |                 ) | ||||||
|             ] |             ] | ||||||
|  |  | ||||||
|     def reduce_create_model_add_field(self, operation, other): |     def reduce_create_model_add_field(self, operation, other, in_between): | ||||||
|         if operation.name.lower() == other.model_name.lower(): |         if operation.name.lower() == other.model_name.lower(): | ||||||
|  |             # Don't allow optimisations of FKs through models they reference | ||||||
|  |             if hasattr(other.field, "rel") and other.field.rel: | ||||||
|  |                 for between in in_between: | ||||||
|  |                     if between.references_model( | ||||||
|  |                         other.field.rel.to._meta.object_name, | ||||||
|  |                         other.field.rel.to._meta.app_label, | ||||||
|  |                     ): | ||||||
|  |                         return None | ||||||
|  |             # OK, that's fine | ||||||
|             return [ |             return [ | ||||||
|                 migrations.CreateModel( |                 migrations.CreateModel( | ||||||
|                     operation.name, |                     operation.name, | ||||||
| @@ -210,7 +219,7 @@ class MigrationOptimizer(object): | |||||||
|                 ) |                 ) | ||||||
|             ] |             ] | ||||||
|  |  | ||||||
|     def reduce_create_model_alter_field(self, operation, other): |     def reduce_create_model_alter_field(self, operation, other, in_between): | ||||||
|         if operation.name.lower() == other.model_name.lower(): |         if operation.name.lower() == other.model_name.lower(): | ||||||
|             return [ |             return [ | ||||||
|                 migrations.CreateModel( |                 migrations.CreateModel( | ||||||
| @@ -224,7 +233,7 @@ class MigrationOptimizer(object): | |||||||
|                 ) |                 ) | ||||||
|             ] |             ] | ||||||
|  |  | ||||||
|     def reduce_create_model_rename_field(self, operation, other): |     def reduce_create_model_rename_field(self, operation, other, in_between): | ||||||
|         if operation.name.lower() == other.model_name.lower(): |         if operation.name.lower() == other.model_name.lower(): | ||||||
|             return [ |             return [ | ||||||
|                 migrations.CreateModel( |                 migrations.CreateModel( | ||||||
| @@ -238,7 +247,7 @@ class MigrationOptimizer(object): | |||||||
|                 ) |                 ) | ||||||
|             ] |             ] | ||||||
|  |  | ||||||
|     def reduce_create_model_remove_field(self, operation, other): |     def reduce_create_model_remove_field(self, operation, other, in_between): | ||||||
|         if operation.name.lower() == other.model_name.lower(): |         if operation.name.lower() == other.model_name.lower(): | ||||||
|             return [ |             return [ | ||||||
|                 migrations.CreateModel( |                 migrations.CreateModel( | ||||||
| @@ -253,7 +262,7 @@ class MigrationOptimizer(object): | |||||||
|                 ) |                 ) | ||||||
|             ] |             ] | ||||||
|  |  | ||||||
|     def reduce_add_field_alter_field(self, operation, other): |     def reduce_add_field_alter_field(self, operation, other, in_between): | ||||||
|         if operation.model_name.lower() == other.model_name.lower() and operation.name.lower() == other.name.lower(): |         if operation.model_name.lower() == other.model_name.lower() and operation.name.lower() == other.name.lower(): | ||||||
|             return [ |             return [ | ||||||
|                 migrations.AddField( |                 migrations.AddField( | ||||||
| @@ -263,15 +272,15 @@ class MigrationOptimizer(object): | |||||||
|                 ) |                 ) | ||||||
|             ] |             ] | ||||||
|  |  | ||||||
|     def reduce_add_field_delete_field(self, operation, other): |     def reduce_add_field_delete_field(self, operation, other, in_between): | ||||||
|         if operation.model_name.lower() == other.model_name.lower() and operation.name.lower() == other.name.lower(): |         if operation.model_name.lower() == other.model_name.lower() and operation.name.lower() == other.name.lower(): | ||||||
|             return [] |             return [] | ||||||
|  |  | ||||||
|     def reduce_alter_field_delete_field(self, operation, other): |     def reduce_alter_field_delete_field(self, operation, other, in_between): | ||||||
|         if operation.model_name.lower() == other.model_name.lower() and operation.name.lower() == other.name.lower(): |         if operation.model_name.lower() == other.model_name.lower() and operation.name.lower() == other.name.lower(): | ||||||
|             return [other] |             return [other] | ||||||
|  |  | ||||||
|     def reduce_add_field_rename_field(self, operation, other): |     def reduce_add_field_rename_field(self, operation, other, in_between): | ||||||
|         if operation.model_name.lower() == other.model_name.lower() and operation.name.lower() == other.old_name.lower(): |         if operation.model_name.lower() == other.model_name.lower() and operation.name.lower() == other.old_name.lower(): | ||||||
|             return [ |             return [ | ||||||
|                 migrations.AddField( |                 migrations.AddField( | ||||||
| @@ -281,7 +290,7 @@ class MigrationOptimizer(object): | |||||||
|                 ) |                 ) | ||||||
|             ] |             ] | ||||||
|  |  | ||||||
|     def reduce_alter_field_rename_field(self, operation, other): |     def reduce_alter_field_rename_field(self, operation, other, in_between): | ||||||
|         if operation.model_name.lower() == other.model_name.lower() and operation.name.lower() == other.old_name.lower(): |         if operation.model_name.lower() == other.model_name.lower() and operation.name.lower() == other.old_name.lower(): | ||||||
|             return [ |             return [ | ||||||
|                 other, |                 other, | ||||||
| @@ -292,7 +301,7 @@ class MigrationOptimizer(object): | |||||||
|                 ), |                 ), | ||||||
|             ] |             ] | ||||||
|  |  | ||||||
|     def reduce_rename_field_self(self, operation, other): |     def reduce_rename_field_self(self, operation, other, in_between): | ||||||
|         if operation.model_name.lower() == other.model_name.lower() and operation.new_name.lower() == other.old_name.lower(): |         if operation.model_name.lower() == other.model_name.lower() and operation.new_name.lower() == other.old_name.lower(): | ||||||
|             return [ |             return [ | ||||||
|                 migrations.RenameField( |                 migrations.RenameField( | ||||||
|   | |||||||
| @@ -41,6 +41,8 @@ class AutodetectorTests(TestCase): | |||||||
|         ("id", models.AutoField(primary_key=True)), |         ("id", models.AutoField(primary_key=True)), | ||||||
|         ("publishers", models.ManyToManyField("testapp.Publisher")), |         ("publishers", models.ManyToManyField("testapp.Publisher")), | ||||||
|     ]) |     ]) | ||||||
|  |     author_with_m2m_through = ModelState("testapp", "Author", [("id", models.AutoField(primary_key=True)), ("publishers", models.ManyToManyField("testapp.Publisher", through="testapp.Contract"))]) | ||||||
|  |     contract = ModelState("testapp", "Contract", [("id", models.AutoField(primary_key=True)), ("author", models.ForeignKey("testapp.Author")), ("publisher", models.ForeignKey("testapp.Publisher"))]) | ||||||
|     publisher = ModelState("testapp", "Publisher", [("id", models.AutoField(primary_key=True)), ("name", models.CharField(max_length=100))]) |     publisher = ModelState("testapp", "Publisher", [("id", models.AutoField(primary_key=True)), ("name", models.CharField(max_length=100))]) | ||||||
|     publisher_with_author = ModelState("testapp", "Publisher", [("id", models.AutoField(primary_key=True)), ("author", models.ForeignKey("testapp.Author")), ("name", models.CharField(max_length=100))]) |     publisher_with_author = ModelState("testapp", "Publisher", [("id", models.AutoField(primary_key=True)), ("author", models.ForeignKey("testapp.Author")), ("name", models.CharField(max_length=100))]) | ||||||
|     publisher_with_book = ModelState("testapp", "Publisher", [("id", models.AutoField(primary_key=True)), ("author", models.ForeignKey("otherapp.Book")), ("name", models.CharField(max_length=100))]) |     publisher_with_book = ModelState("testapp", "Publisher", [("id", models.AutoField(primary_key=True)), ("author", models.ForeignKey("otherapp.Book")), ("name", models.CharField(max_length=100))]) | ||||||
| @@ -62,6 +64,67 @@ class AutodetectorTests(TestCase): | |||||||
|     knight = ModelState("eggs", "Knight", [("id", models.AutoField(primary_key=True))]) |     knight = ModelState("eggs", "Knight", [("id", models.AutoField(primary_key=True))]) | ||||||
|     rabbit = ModelState("eggs", "Rabbit", [("id", models.AutoField(primary_key=True)), ("knight", models.ForeignKey("eggs.Knight")), ("parent", models.ForeignKey("eggs.Rabbit"))], {"unique_together": [("parent", "knight")]}) |     rabbit = ModelState("eggs", "Rabbit", [("id", models.AutoField(primary_key=True)), ("knight", models.ForeignKey("eggs.Knight")), ("parent", models.ForeignKey("eggs.Rabbit"))], {"unique_together": [("parent", "knight")]}) | ||||||
|  |  | ||||||
|  |     def repr_changes(self, changes): | ||||||
|  |         output = "" | ||||||
|  |         for app_label, migrations in sorted(changes.items()): | ||||||
|  |             output += "  %s:\n" % app_label | ||||||
|  |             for migration in migrations: | ||||||
|  |                 output += "    %s\n" % migration.name | ||||||
|  |                 for operation in migration.operations: | ||||||
|  |                     output += "      %s\n" % operation | ||||||
|  |         return output | ||||||
|  |  | ||||||
|  |     def assertNumberMigrations(self, changes, app_label, number): | ||||||
|  |         if not changes.get(app_label, None): | ||||||
|  |             self.fail("No migrations found for %s\n%s" % (app_label, self.repr_changes(changes))) | ||||||
|  |         if len(changes[app_label]) != number: | ||||||
|  |             self.fail("Incorrect number of migrations (%s) for %s (expected %s)\n%s" % ( | ||||||
|  |                 len(changes[app_label]), | ||||||
|  |                 app_label, | ||||||
|  |                 number, | ||||||
|  |                 self.repr_changes(changes), | ||||||
|  |             )) | ||||||
|  |  | ||||||
|  |     def assertOperationTypes(self, changes, app_label, index, types): | ||||||
|  |         if not changes.get(app_label, None): | ||||||
|  |             self.fail("No migrations found for %s\n%s" % (app_label, self.repr_changes(changes))) | ||||||
|  |         if len(changes[app_label]) < index + 1: | ||||||
|  |             self.fail("No migration at index %s for %s\n%s" % (index, app_label, self.repr_changes(changes))) | ||||||
|  |         migration = changes[app_label][index] | ||||||
|  |         real_types = [operation.__class__.__name__ for operation in migration.operations] | ||||||
|  |         if types != real_types: | ||||||
|  |             self.fail("Operation type mismatch for %s.%s (expected %s):\n%s" % ( | ||||||
|  |                 app_label, | ||||||
|  |                 migration.name, | ||||||
|  |                 types, | ||||||
|  |                 self.repr_changes(changes), | ||||||
|  |             )) | ||||||
|  |  | ||||||
|  |     def assertOperationAttributes(self, changes, app_label, index, operation_index, **attrs): | ||||||
|  |         if not changes.get(app_label, None): | ||||||
|  |             self.fail("No migrations found for %s\n%s" % (app_label, self.repr_changes(changes))) | ||||||
|  |         if len(changes[app_label]) < index + 1: | ||||||
|  |             self.fail("No migration at index %s for %s\n%s" % (index, app_label, self.repr_changes(changes))) | ||||||
|  |         migration = changes[app_label][index] | ||||||
|  |         if len(changes[app_label]) < index + 1: | ||||||
|  |             self.fail("No operation at index %s for %s.%s\n%s" % ( | ||||||
|  |                 operation_index, | ||||||
|  |                 app_label, | ||||||
|  |                 migration.name, | ||||||
|  |                 self.repr_changes(changes), | ||||||
|  |             )) | ||||||
|  |         operation = migration.operations[operation_index] | ||||||
|  |         for attr, value in attrs.items(): | ||||||
|  |             if getattr(operation, attr, None) != value: | ||||||
|  |                 self.fail("Attribute mismatch for %s.%s op #%s, %s (expected %r):\n%s" % ( | ||||||
|  |                     app_label, | ||||||
|  |                     migration.name, | ||||||
|  |                     operation_index + 1, | ||||||
|  |                     attr, | ||||||
|  |                     value, | ||||||
|  |                     self.repr_changes(changes), | ||||||
|  |                 )) | ||||||
|  |  | ||||||
|     def make_project_state(self, model_states): |     def make_project_state(self, model_states): | ||||||
|         "Shortcut to make ProjectStates from lists of predefined models" |         "Shortcut to make ProjectStates from lists of predefined models" | ||||||
|         project_state = ProjectState() |         project_state = ProjectState() | ||||||
| @@ -281,6 +344,9 @@ class AutodetectorTests(TestCase): | |||||||
|     def test_fk_dependency(self): |     def test_fk_dependency(self): | ||||||
|         "Tests that having a ForeignKey automatically adds a dependency" |         "Tests that having a ForeignKey automatically adds a dependency" | ||||||
|         # Make state |         # Make state | ||||||
|  |         # Note that testapp (author) has no dependencies, | ||||||
|  |         # otherapp (book) depends on testapp (author), | ||||||
|  |         # thirdapp (edition) depends on otherapp (book) | ||||||
|         before = self.make_project_state([]) |         before = self.make_project_state([]) | ||||||
|         after = self.make_project_state([self.author_name, self.book, self.edition]) |         after = self.make_project_state([self.author_name, self.book, self.edition]) | ||||||
|         autodetector = MigrationAutodetector(before, after) |         autodetector = MigrationAutodetector(before, after) | ||||||
| @@ -322,12 +388,15 @@ class AutodetectorTests(TestCase): | |||||||
|         self.assertEqual(len(changes['testapp']), 1) |         self.assertEqual(len(changes['testapp']), 1) | ||||||
|         # Right number of actions? |         # Right number of actions? | ||||||
|         migration = changes['testapp'][0] |         migration = changes['testapp'][0] | ||||||
|         self.assertEqual(len(migration.operations), 2) |         self.assertEqual(len(migration.operations), 3) | ||||||
|         # Right actions? |         # Right actions? | ||||||
|         action = migration.operations[0] |         action = migration.operations[0] | ||||||
|         self.assertEqual(action.__class__.__name__, "CreateModel") |         self.assertEqual(action.__class__.__name__, "CreateModel") | ||||||
|         action = migration.operations[1] |         action = migration.operations[1] | ||||||
|         self.assertEqual(action.__class__.__name__, "CreateModel") |         self.assertEqual(action.__class__.__name__, "CreateModel") | ||||||
|  |         # Third action might vanish one day if the optimizer improves. | ||||||
|  |         action = migration.operations[2] | ||||||
|  |         self.assertEqual(action.__class__.__name__, "AddField") | ||||||
|         # Right dependencies? |         # Right dependencies? | ||||||
|         self.assertEqual(migration.dependencies, []) |         self.assertEqual(migration.dependencies, []) | ||||||
|  |  | ||||||
| @@ -350,10 +419,12 @@ class AutodetectorTests(TestCase): | |||||||
|         migration2 = changes['otherapp'][0] |         migration2 = changes['otherapp'][0] | ||||||
|         self.assertEqual(len(migration2.operations), 1) |         self.assertEqual(len(migration2.operations), 1) | ||||||
|         migration3 = changes['otherapp'][1] |         migration3 = changes['otherapp'][1] | ||||||
|         self.assertEqual(len(migration2.operations), 1) |         self.assertEqual(len(migration3.operations), 1) | ||||||
|         # Right actions? |         # Right actions? | ||||||
|         action = migration1.operations[0] |         action = migration1.operations[0] | ||||||
|         self.assertEqual(action.__class__.__name__, "CreateModel") |         self.assertEqual(action.__class__.__name__, "CreateModel") | ||||||
|  |         self.assertEqual(action.name, "Author") | ||||||
|  |         self.assertEqual(len(action.fields), 3) | ||||||
|         action = migration2.operations[0] |         action = migration2.operations[0] | ||||||
|         self.assertEqual(action.__class__.__name__, "CreateModel") |         self.assertEqual(action.__class__.__name__, "CreateModel") | ||||||
|         self.assertEqual(len(action.fields), 2) |         self.assertEqual(len(action.fields), 2) | ||||||
| @@ -362,8 +433,8 @@ class AutodetectorTests(TestCase): | |||||||
|         self.assertEqual(action.name, "author") |         self.assertEqual(action.name, "author") | ||||||
|         # Right dependencies? |         # Right dependencies? | ||||||
|         self.assertEqual(migration1.dependencies, [("otherapp", "auto_1")]) |         self.assertEqual(migration1.dependencies, [("otherapp", "auto_1")]) | ||||||
|         self.assertEqual(migration2.dependencies, [('testapp', '__first__')]) |         self.assertEqual(migration2.dependencies, []) | ||||||
|         self.assertEqual(set(migration3.dependencies), set([("otherapp", "auto_1"), ("testapp", "auto_1")])) |         self.assertEqual(set(migration3.dependencies), set([("testapp", "auto_1"), ("otherapp", "auto_1")])) | ||||||
|  |  | ||||||
|     def test_same_app_circular_fk_dependency(self): |     def test_same_app_circular_fk_dependency(self): | ||||||
|         """ |         """ | ||||||
| @@ -376,23 +447,23 @@ class AutodetectorTests(TestCase): | |||||||
|         autodetector = MigrationAutodetector(before, after) |         autodetector = MigrationAutodetector(before, after) | ||||||
|         changes = autodetector._detect_changes() |         changes = autodetector._detect_changes() | ||||||
|         # Right number of migrations? |         # Right number of migrations? | ||||||
|         self.assertEqual(len(changes['testapp']), 2) |         self.assertEqual(len(changes['testapp']), 1) | ||||||
|         # Right number of actions? |         # Right number of actions? | ||||||
|         migration1 = changes['testapp'][0] |         migration1 = changes['testapp'][0] | ||||||
|         self.assertEqual(len(migration1.operations), 2) |         self.assertEqual(len(migration1.operations), 4) | ||||||
|         migration2 = changes['testapp'][1] |  | ||||||
|         self.assertEqual(len(migration2.operations), 1) |  | ||||||
|         # Right actions? |         # Right actions? | ||||||
|         action = migration1.operations[0] |         action = migration1.operations[0] | ||||||
|         self.assertEqual(action.__class__.__name__, "CreateModel") |         self.assertEqual(action.__class__.__name__, "CreateModel") | ||||||
|         action = migration1.operations[1] |         action = migration1.operations[1] | ||||||
|         self.assertEqual(action.__class__.__name__, "CreateModel") |         self.assertEqual(action.__class__.__name__, "CreateModel") | ||||||
|         action = migration2.operations[0] |         action = migration1.operations[2] | ||||||
|         self.assertEqual(action.__class__.__name__, "AddField") |         self.assertEqual(action.__class__.__name__, "AddField") | ||||||
|         self.assertEqual(action.name, "publisher") |         self.assertEqual(action.name, "publisher") | ||||||
|  |         action = migration1.operations[3] | ||||||
|  |         self.assertEqual(action.__class__.__name__, "AddField") | ||||||
|  |         self.assertEqual(action.name, "author") | ||||||
|         # Right dependencies? |         # Right dependencies? | ||||||
|         self.assertEqual(migration1.dependencies, []) |         self.assertEqual(migration1.dependencies, []) | ||||||
|         self.assertEqual(migration2.dependencies, [("testapp", "auto_1")]) |  | ||||||
|  |  | ||||||
|     def test_same_app_circular_fk_dependency_and_unique_together(self): |     def test_same_app_circular_fk_dependency_and_unique_together(self): | ||||||
|         """ |         """ | ||||||
| @@ -406,29 +477,22 @@ class AutodetectorTests(TestCase): | |||||||
|         autodetector = MigrationAutodetector(before, after) |         autodetector = MigrationAutodetector(before, after) | ||||||
|         changes = autodetector._detect_changes() |         changes = autodetector._detect_changes() | ||||||
|         # Right number of migrations? |         # Right number of migrations? | ||||||
|         self.assertEqual(len(changes['eggs']), 2) |         self.assertEqual(len(changes['eggs']), 1) | ||||||
|         # Right number of actions? |         # Right number of actions? | ||||||
|         migration1 = changes['eggs'][0] |         migration1 = changes['eggs'][0] | ||||||
|         self.assertEqual(len(migration1.operations), 2) |         self.assertEqual(len(migration1.operations), 3) | ||||||
|         migration2 = changes['eggs'][1] |  | ||||||
|         self.assertEqual(len(migration2.operations), 2) |  | ||||||
|         # Right actions? |         # Right actions? | ||||||
|         action = migration1.operations[0] |         action = migration1.operations[0] | ||||||
|         self.assertEqual(action.__class__.__name__, "CreateModel") |         self.assertEqual(action.__class__.__name__, "CreateModel") | ||||||
|         action = migration1.operations[1] |         action = migration1.operations[1] | ||||||
|         self.assertEqual(action.__class__.__name__, "CreateModel") |         self.assertEqual(action.__class__.__name__, "CreateModel") | ||||||
|         # CreateModel action for Rabbit should not have unique_together now |  | ||||||
|         self.assertEqual(action.name, "Rabbit") |         self.assertEqual(action.name, "Rabbit") | ||||||
|         self.assertFalse("unique_together" in action.options) |         self.assertFalse("unique_together" in action.options) | ||||||
|         action = migration2.operations[0] |         action = migration1.operations[2] | ||||||
|         self.assertEqual(action.__class__.__name__, "AddField") |  | ||||||
|         self.assertEqual(action.name, "parent") |  | ||||||
|         action = migration2.operations[1] |  | ||||||
|         self.assertEqual(action.__class__.__name__, "AlterUniqueTogether") |         self.assertEqual(action.__class__.__name__, "AlterUniqueTogether") | ||||||
|         self.assertEqual(action.name, "rabbit") |         self.assertEqual(action.name, "rabbit") | ||||||
|         # Right dependencies? |         # Right dependencies? | ||||||
|         self.assertEqual(migration1.dependencies, []) |         self.assertEqual(migration1.dependencies, []) | ||||||
|         self.assertEqual(migration2.dependencies, [("eggs", "auto_1")]) |  | ||||||
|  |  | ||||||
|     def test_unique_together(self): |     def test_unique_together(self): | ||||||
|         "Tests unique_together detection" |         "Tests unique_together detection" | ||||||
| @@ -658,11 +722,54 @@ class AutodetectorTests(TestCase): | |||||||
|         self.assertEqual(len(changes['otherapp']), 1) |         self.assertEqual(len(changes['otherapp']), 1) | ||||||
|         # Right number of actions? |         # Right number of actions? | ||||||
|         migration = changes['otherapp'][0] |         migration = changes['otherapp'][0] | ||||||
|         self.assertEqual(len(migration.operations), 2) |         self.assertEqual(len(migration.operations), 4) | ||||||
|         # Right actions in right order? |         # Right actions in right order? | ||||||
|  |         # The first two are because we can't optimise RemoveField | ||||||
|  |         # into DeleteModel reliably. | ||||||
|         action = migration.operations[0] |         action = migration.operations[0] | ||||||
|         self.assertEqual(action.__class__.__name__, "RemoveField") |         self.assertEqual(action.__class__.__name__, "RemoveField") | ||||||
|         self.assertEqual(action.name, "authors") |         self.assertEqual(action.name, "author") | ||||||
|         action = migration.operations[1] |         action = migration.operations[1] | ||||||
|  |         self.assertEqual(action.__class__.__name__, "RemoveField") | ||||||
|  |         self.assertEqual(action.name, "book") | ||||||
|  |         action = migration.operations[2] | ||||||
|  |         self.assertEqual(action.__class__.__name__, "RemoveField") | ||||||
|  |         self.assertEqual(action.name, "authors") | ||||||
|  |         action = migration.operations[3] | ||||||
|         self.assertEqual(action.__class__.__name__, "DeleteModel") |         self.assertEqual(action.__class__.__name__, "DeleteModel") | ||||||
|         self.assertEqual(action.name, "Attribution") |         self.assertEqual(action.name, "Attribution") | ||||||
|  |  | ||||||
|  |     def test_m2m_w_through_multistep_remove(self): | ||||||
|  |         """ | ||||||
|  |         A model with a m2m field that specifies a "through" model cannot be removed in the same | ||||||
|  |         migration as that through model as the schema will pass through an inconsistent state. | ||||||
|  |         The autodetector should produce two migrations to avoid this issue. | ||||||
|  |         """ | ||||||
|  |         before = self.make_project_state([self.author_with_m2m_through, self.publisher, self.contract]) | ||||||
|  |         after = self.make_project_state([self.publisher]) | ||||||
|  |         autodetector = MigrationAutodetector(before, after) | ||||||
|  |         changes = autodetector._detect_changes() | ||||||
|  |         # Right number of migrations? | ||||||
|  |         self.assertNumberMigrations(changes, "testapp", 1) | ||||||
|  |         # Right actions in right order? | ||||||
|  |         self.assertOperationTypes(changes, "testapp", 0, ["RemoveField", "RemoveField", "DeleteModel", "RemoveField", "DeleteModel"]) | ||||||
|  |         # Actions touching the right stuff? | ||||||
|  |         self.assertOperationAttributes(changes, "testapp", 0, 0, name="publishers") | ||||||
|  |         self.assertOperationAttributes(changes, "testapp", 0, 1, name="author") | ||||||
|  |         self.assertOperationAttributes(changes, "testapp", 0, 2, name="Author") | ||||||
|  |         self.assertOperationAttributes(changes, "testapp", 0, 3, name="publisher") | ||||||
|  |         self.assertOperationAttributes(changes, "testapp", 0, 4, name="Contract") | ||||||
|  |  | ||||||
|  |     def test_non_circular_foreignkey_dependency_removal(self): | ||||||
|  |         """ | ||||||
|  |         If two models with a ForeignKey from one to the other are removed at the same time, | ||||||
|  |         the autodetector should remove them in the correct order. | ||||||
|  |         """ | ||||||
|  |         before = self.make_project_state([self.author_with_publisher, self.publisher_with_author]) | ||||||
|  |         after = self.make_project_state([]) | ||||||
|  |         autodetector = MigrationAutodetector(before, after) | ||||||
|  |         changes = autodetector._detect_changes() | ||||||
|  |         # Right number of migrations? | ||||||
|  |         self.assertNumberMigrations(changes, "testapp", 1) | ||||||
|  |         # Right actions in right order? | ||||||
|  |         self.assertOperationTypes(changes, "testapp", 0, ["RemoveField", "RemoveField", "DeleteModel", "DeleteModel"]) | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user