mirror of
				https://github.com/django/django.git
				synced 2025-10-31 01:25:32 +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.migration import Migration | ||||
| from django.db.migrations.questioner import MigrationQuestioner | ||||
| from django.db.migrations.optimizer import MigrationOptimizer | ||||
|  | ||||
|  | ||||
| class MigrationAutodetector(object): | ||||
| @@ -39,6 +40,43 @@ class MigrationAutodetector(object): | ||||
|             changes = self._trim_to_apps(changes, trim_to_apps) | ||||
|         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): | ||||
|         """ | ||||
|         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 | ||||
|         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 = [] | ||||
|         for al, mn in self.from_state.models.keys(): | ||||
|             model = old_apps.get_model(al, mn) | ||||
|         # The first phase is generating all the operations for each app | ||||
|         # and gathering them into a big per-app list. | ||||
|         # 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: | ||||
|                 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 = [] | ||||
|         for al, mn in self.to_state.models.keys(): | ||||
|             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)) | ||||
|         # Renames have to come first | ||||
|         self.generate_renamed_models() | ||||
|  | ||||
|         def _deep_deconstruct(obj, field=True): | ||||
|             """ | ||||
|             Recursive deconstruction for a field and its arguments. | ||||
|             """ | ||||
|             if not hasattr(obj, 'deconstruct'): | ||||
|                 return obj | ||||
|             deconstructed = obj.deconstruct() | ||||
|             if field: | ||||
|                 deconstructed = deconstructed[1:] | ||||
|             name, args, kwargs = deconstructed | ||||
|         # Prepare field lists, and prepare a list of the fields that used | ||||
|         # through models in the old state so we can make dependencies | ||||
|         # 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) | ||||
|         self.through_users = {} | ||||
|         self.old_field_keys = set() | ||||
|         self.new_field_keys = set() | ||||
|         for app_label, model_name in sorted(self.kept_model_keys): | ||||
|             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] | ||||
|             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 ( | ||||
|                 name, | ||||
|                 [_deep_deconstruct(value, field=False) for value in args], | ||||
|                 dict([(key, _deep_deconstruct(value, field=False)) | ||||
|                       for key, value in kwargs.items()]) | ||||
|                 isinstance(operation, operations.CreateModel) and | ||||
|                 operation.name.lower() == dependency[1].lower() | ||||
|             ) | ||||
|         # 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): | ||||
|             """ | ||||
|             Return a definition of the fields that ignores field names and | ||||
|             what related fields actually relate to. | ||||
|             """ | ||||
|             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 | ||||
|     def add_operation(self, app_label, operation, dependencies=None): | ||||
|         # Dependencies are (app_label, model_name, field_name, create/delete as True/False) | ||||
|         operation._auto_deps = dependencies or [] | ||||
|         self.generated_operations.setdefault(app_label, []).append(operation) | ||||
|  | ||||
|         # Find any renamed models. | ||||
|         renamed_models = {} | ||||
|         renamed_models_rel = {} | ||||
|         added_models = set(new_model_keys) - set(old_model_keys) | ||||
|         for app_label, model_name in added_models: | ||||
|     def generate_renamed_models(self): | ||||
|         """ | ||||
|         Finds any renamed models, and generates the operations for them, | ||||
|         and removes the old entry from the model lists. | ||||
|         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_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: | ||||
|                 if rem_app_label == app_label: | ||||
|                     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 self.questioner.ask_rename_model(rem_model_state, model_state): | ||||
|                             self.add_to_migration( | ||||
|                             self.add_operation( | ||||
|                                 app_label, | ||||
|                                 operations.RenameModel( | ||||
|                                     old_name=rem_model_state.name, | ||||
|                                     new_name=model_state.name, | ||||
|                                 ) | ||||
|                             ) | ||||
|                             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) | ||||
|                             old_model_keys.remove((rem_app_label, rem_model_name)) | ||||
|                             old_model_keys.append((app_label, model_name)) | ||||
|                             self.renamed_models[app_label, model_name] = rem_model_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) | ||||
|                             self.old_model_keys.remove((rem_app_label, rem_model_name)) | ||||
|                             self.old_model_keys.append((app_label, model_name)) | ||||
|                             break | ||||
|  | ||||
|         # Adding models. Phase 1 is adding models with no outward relationships. | ||||
|         added_models = set(new_model_keys) - set(old_model_keys) | ||||
|         pending_add = {} | ||||
|         for app_label, model_name in added_models: | ||||
|     def generate_created_models(self): | ||||
|         """ | ||||
|         Find all new models and make creation operations for them, | ||||
|         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] | ||||
|             # Are there any relationships out from this model? if so, punt it to the next phase. | ||||
|             related_fields = [] | ||||
|             for field in new_apps.get_model(app_label, model_name)._meta.local_fields: | ||||
|             # Gather related fields | ||||
|             related_fields = {} | ||||
|             for field in self.new_apps.get_model(app_label, model_name)._meta.local_fields: | ||||
|                 if field.rel: | ||||
|                     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: | ||||
|                         related_fields.append((field.name, field.rel.through._meta.app_label, field.rel.through._meta.model_name)) | ||||
|             for field in new_apps.get_model(app_label, model_name)._meta.local_many_to_many: | ||||
|                         related_fields[field.name] = field | ||||
|             for field in self.new_apps.get_model(app_label, model_name)._meta.local_many_to_many: | ||||
|                 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: | ||||
|                     related_fields.append((field.name, field.rel.through._meta.app_label, field.rel.through._meta.model_name)) | ||||
|             if related_fields: | ||||
|                 pending_add[app_label, model_name] = related_fields | ||||
|             else: | ||||
|                 self.add_to_migration( | ||||
|                     app_label, | ||||
|                     operations.CreateModel( | ||||
|                         name=model_state.name, | ||||
|                         fields=model_state.fields, | ||||
|                         options=model_state.options, | ||||
|                         bases=model_state.bases, | ||||
|                     ) | ||||
|                     related_fields[field.name] = field | ||||
|             # Are there unique/index_together to defer? | ||||
|             unique_together = model_state.options.pop('unique_together', None) | ||||
|             index_together = model_state.options.pop('index_together', None) | ||||
|             # Generate creation operatoin | ||||
|             self.add_operation( | ||||
|                 app_label, | ||||
|                 operations.CreateModel( | ||||
|                     name=model_state.name, | ||||
|                     fields=[d for d in model_state.fields if d[0] not in related_fields], | ||||
|                     options=model_state.options, | ||||
|                     bases=model_state.bases, | ||||
|                 ) | ||||
|  | ||||
|         # Phase 2 is progressively adding pending models, splitting up into two | ||||
|         # migrations if required. | ||||
|         pending_new_fks = [] | ||||
|         pending_unique_together = [] | ||||
|         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 | ||||
|             ) | ||||
|             # Generate operations for each related field | ||||
|             for name, field in sorted(related_fields.items()): | ||||
|                 # Account for FKs to swappable models | ||||
|                 swappable_setting = getattr(field, 'swappable_setting', None) | ||||
|                 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) | ||||
|             del pending_add[app_label, model_name] | ||||
|  | ||||
|         # Phase 3 is adding the final set of FKs as separate new migrations. | ||||
|         for app_label, model_name, field_name, other_app_label in pending_new_fks: | ||||
|             model_state = self.to_state.models[app_label, model_name] | ||||
|             self.add_to_migration( | ||||
|                 app_label, | ||||
|                 operations.AddField( | ||||
|                     model_name=model_name, | ||||
|                     name=field_name, | ||||
|                     field=model_state.get_field_by_name(field_name), | ||||
|                 ), | ||||
|                 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 | ||||
|                     dep_app_label = "__setting__" | ||||
|                     dep_object_name = swappable_setting | ||||
|                 else: | ||||
|                     dep_app_label = field.rel.to._meta.app_label | ||||
|                     dep_object_name = field.rel.to._meta.object_name | ||||
|                 # Make operation | ||||
|                 self.add_operation( | ||||
|                     app_label, | ||||
|                     operations.AddField( | ||||
|                         model_name=model_name, | ||||
|                         name=name, | ||||
|                         field=field, | ||||
|                     ), | ||||
|                     dependencies = [ | ||||
|                         (dep_app_label, dep_object_name, None, True), | ||||
|                     ] | ||||
|                 ) | ||||
|             ) | ||||
|         # Changes within models | ||||
|         kept_models = set(old_model_keys).intersection(new_model_keys) | ||||
|         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(( | ||||
|             # Generate other opns | ||||
|             if unique_together: | ||||
|                 self.add_operation( | ||||
|                     app_label, | ||||
|                     operations.AlterUniqueTogether( | ||||
|                         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 | ||||
|         renamed_fields = {} | ||||
|         for app_label, model_name, field_name in new_fields - old_fields: | ||||
|             old_model_name = renamed_models.get((app_label, model_name), model_name) | ||||
|         self.renamed_fields = {} | ||||
|         for app_label, model_name, field_name in sorted(self.new_field_keys - self.old_field_keys): | ||||
|             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] | ||||
|             new_model_state = self.to_state.models[app_label, model_name] | ||||
|             field = new_model_state.get_field_by_name(field_name) | ||||
|             # Scan to see if this is actually a rename! | ||||
|             field_dec = _deep_deconstruct(field) | ||||
|             field_dec = self.deep_deconstruct(field) | ||||
|             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: | ||||
|                     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]: | ||||
|                         old_rel_to = old_field_dec[2]['to'] | ||||
|                         if old_rel_to in renamed_models_rel: | ||||
|                             old_field_dec[2]['to'] = renamed_models_rel[old_rel_to] | ||||
|                         if old_rel_to in self.renamed_models_rel: | ||||
|                             old_field_dec[2]['to'] = self.renamed_models_rel[old_rel_to] | ||||
|                     if old_field_dec == field_dec: | ||||
|                         if self.questioner.ask_rename(model_name, rem_field_name, field_name, field): | ||||
|                             self.add_to_migration( | ||||
|                             self.add_operation( | ||||
|                                 app_label, | ||||
|                                 operations.RenameField( | ||||
|                                     model_name=model_name, | ||||
| @@ -294,9 +530,9 @@ class MigrationAutodetector(object): | ||||
|                                     new_name=field_name, | ||||
|                                 ) | ||||
|                             ) | ||||
|                             old_fields.remove((rem_app_label, rem_model_name, rem_field_name)) | ||||
|                             old_fields.add((app_label, model_name, field_name)) | ||||
|                             renamed_fields[app_label, model_name, field_name] = rem_field_name | ||||
|                             self.old_field_keys.remove((rem_app_label, rem_model_name, rem_field_name)) | ||||
|                             self.old_field_keys.add((app_label, model_name, field_name)) | ||||
|                             self.renamed_fields[app_label, model_name, field_name] = rem_field_name | ||||
|                             found_rename = True | ||||
|                             break | ||||
|             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): | ||||
|                 field = field.clone() | ||||
|                 field.default = self.questioner.ask_not_null_addition(field_name, model_name) | ||||
|                 self.add_to_migration( | ||||
|                 self.add_operation( | ||||
|                     app_label, | ||||
|                     operations.AddField( | ||||
|                         model_name=model_name, | ||||
| @@ -315,7 +551,7 @@ class MigrationAutodetector(object): | ||||
|                     ) | ||||
|                 ) | ||||
|             else: | ||||
|                 self.add_to_migration( | ||||
|                 self.add_operation( | ||||
|                     app_label, | ||||
|                     operations.AddField( | ||||
|                         model_name=model_name, | ||||
| @@ -323,33 +559,34 @@ class MigrationAutodetector(object): | ||||
|                         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) | ||||
|                 if swappable_setting is not None: | ||||
|                     self.add_swappable_dependency(app_label, swappable_setting) | ||||
|         # Old fields | ||||
|         for app_label, model_name, field_name in old_fields - new_fields: | ||||
|             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] | ||||
|             self.add_to_migration( | ||||
|  | ||||
|     def generate_removed_fields(self): | ||||
|         """ | ||||
|         Fields that have been removed. | ||||
|         """ | ||||
|         for app_label, model_name, field_name in sorted(self.old_field_keys - self.new_field_keys): | ||||
|             self.add_operation( | ||||
|                 app_label, | ||||
|                 operations.RemoveField( | ||||
|                     model_name=model_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? | ||||
|             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] | ||||
|             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_dec = _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)) | ||||
|             old_field_name = self.renamed_fields.get((app_label, model_name, field_name), field_name) | ||||
|             old_field_dec = self.deep_deconstruct(old_model_state.get_field_by_name(old_field_name)) | ||||
|             new_field_dec = self.deep_deconstruct(new_model_state.get_field_by_name(field_name)) | ||||
|             if old_field_dec != new_field_dec: | ||||
|                 self.add_to_migration( | ||||
|                 self.add_operation( | ||||
|                     app_label, | ||||
|                     operations.AlterField( | ||||
|                         model_name=model_name, | ||||
| @@ -357,53 +594,34 @@ class MigrationAutodetector(object): | ||||
|                         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) | ||||
|         # Removing models | ||||
|         removed_models = set(old_model_keys) - set(new_model_keys) | ||||
|         for app_label, model_name in removed_models: | ||||
|             model_state = self.from_state.models[app_label, model_name] | ||||
|             self.add_to_migration( | ||||
|                 app_label, | ||||
|                 operations.DeleteModel( | ||||
|                     model_state.name, | ||||
|  | ||||
|     def generate_altered_unique_together(self): | ||||
|         for app_label, model_name in sorted(self.kept_model_keys): | ||||
|             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] | ||||
|             new_model_state = self.to_state.models[app_label, model_name] | ||||
|             if old_model_state.options.get("unique_together", None) != new_model_state.options.get("unique_together", None): | ||||
|                 self.add_operation( | ||||
|                     app_label, | ||||
|                     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): | ||||
|         migrations = self.migrations.setdefault(app_label, []) | ||||
|         if not migrations or new: | ||||
|             subclass = type(str("Migration"), (Migration,), {"operations": [], "dependencies": []}) | ||||
|             instance = subclass("auto_%i" % (len(migrations) + 1), app_label) | ||||
|             migrations.append(instance) | ||||
|         migrations[-1].operations.append(operation) | ||||
|  | ||||
|     def add_dependency(self, app_label, other_app_label): | ||||
|         """ | ||||
|         Adds a dependency to app_label's newest migration on | ||||
|         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 generate_altered_index_together(self): | ||||
|         for app_label, model_name in sorted(self.kept_model_keys): | ||||
|             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] | ||||
|             new_model_state = self.to_state.models[app_label, model_name] | ||||
|             if old_model_state.options.get("index_together", None) != new_model_state.options.get("index_together", None): | ||||
|                 self.add_operation( | ||||
|                     app_label, | ||||
|                     operations.AlterIndexTogether( | ||||
|                         name=model_name, | ||||
|                         index_together=new_model_state.options['index_together'], | ||||
|                     ) | ||||
|                 ) | ||||
|  | ||||
|     def arrange_for_graph(self, changes, graph): | ||||
|         """ | ||||
|   | ||||
| @@ -51,7 +51,7 @@ class MigrationOptimizer(object): | ||||
|         for i, operation in enumerate(operations): | ||||
|             # Compare it to each operation after it | ||||
|             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: | ||||
|                     # Optimize! Add result, then remaining others, then return | ||||
|                     new_operations.extend(result) | ||||
| @@ -67,7 +67,7 @@ class MigrationOptimizer(object): | ||||
|  | ||||
|     #### REDUCTION #### | ||||
|  | ||||
|     def reduce(self, operation, other): | ||||
|     def reduce(self, operation, other, in_between=None): | ||||
|         """ | ||||
|         Either returns a list of zero, one or two operations, | ||||
|         or None, meaning this pair cannot be optimized. | ||||
| @@ -156,24 +156,24 @@ class MigrationOptimizer(object): | ||||
|         ] | ||||
|         for ia, ib, om in submethods: | ||||
|             if isinstance(operation, ia) and isinstance(other, ib): | ||||
|                 return om(operation, other) | ||||
|                 return om(operation, other, in_between or []) | ||||
|         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. | ||||
|         """ | ||||
|         if operation.name.lower() == other.name.lower(): | ||||
|             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. | ||||
|         """ | ||||
|         if operation.name.lower() == other.name.lower(): | ||||
|             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 | ||||
|         """ | ||||
| @@ -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 | ||||
|         """ | ||||
| @@ -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(): | ||||
|             # 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 [ | ||||
|                 migrations.CreateModel( | ||||
|                     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(): | ||||
|             return [ | ||||
|                 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(): | ||||
|             return [ | ||||
|                 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(): | ||||
|             return [ | ||||
|                 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(): | ||||
|             return [ | ||||
|                 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(): | ||||
|             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(): | ||||
|             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(): | ||||
|             return [ | ||||
|                 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(): | ||||
|             return [ | ||||
|                 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(): | ||||
|             return [ | ||||
|                 migrations.RenameField( | ||||
|   | ||||
| @@ -41,6 +41,8 @@ class AutodetectorTests(TestCase): | ||||
|         ("id", models.AutoField(primary_key=True)), | ||||
|         ("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_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))]) | ||||
| @@ -62,6 +64,67 @@ class AutodetectorTests(TestCase): | ||||
|     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")]}) | ||||
|  | ||||
|     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): | ||||
|         "Shortcut to make ProjectStates from lists of predefined models" | ||||
|         project_state = ProjectState() | ||||
| @@ -281,6 +344,9 @@ class AutodetectorTests(TestCase): | ||||
|     def test_fk_dependency(self): | ||||
|         "Tests that having a ForeignKey automatically adds a dependency" | ||||
|         # 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([]) | ||||
|         after = self.make_project_state([self.author_name, self.book, self.edition]) | ||||
|         autodetector = MigrationAutodetector(before, after) | ||||
| @@ -322,12 +388,15 @@ class AutodetectorTests(TestCase): | ||||
|         self.assertEqual(len(changes['testapp']), 1) | ||||
|         # Right number of actions? | ||||
|         migration = changes['testapp'][0] | ||||
|         self.assertEqual(len(migration.operations), 2) | ||||
|         self.assertEqual(len(migration.operations), 3) | ||||
|         # Right actions? | ||||
|         action = migration.operations[0] | ||||
|         self.assertEqual(action.__class__.__name__, "CreateModel") | ||||
|         action = migration.operations[1] | ||||
|         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? | ||||
|         self.assertEqual(migration.dependencies, []) | ||||
|  | ||||
| @@ -350,10 +419,12 @@ class AutodetectorTests(TestCase): | ||||
|         migration2 = changes['otherapp'][0] | ||||
|         self.assertEqual(len(migration2.operations), 1) | ||||
|         migration3 = changes['otherapp'][1] | ||||
|         self.assertEqual(len(migration2.operations), 1) | ||||
|         self.assertEqual(len(migration3.operations), 1) | ||||
|         # Right actions? | ||||
|         action = migration1.operations[0] | ||||
|         self.assertEqual(action.__class__.__name__, "CreateModel") | ||||
|         self.assertEqual(action.name, "Author") | ||||
|         self.assertEqual(len(action.fields), 3) | ||||
|         action = migration2.operations[0] | ||||
|         self.assertEqual(action.__class__.__name__, "CreateModel") | ||||
|         self.assertEqual(len(action.fields), 2) | ||||
| @@ -362,8 +433,8 @@ class AutodetectorTests(TestCase): | ||||
|         self.assertEqual(action.name, "author") | ||||
|         # Right dependencies? | ||||
|         self.assertEqual(migration1.dependencies, [("otherapp", "auto_1")]) | ||||
|         self.assertEqual(migration2.dependencies, [('testapp', '__first__')]) | ||||
|         self.assertEqual(set(migration3.dependencies), set([("otherapp", "auto_1"), ("testapp", "auto_1")])) | ||||
|         self.assertEqual(migration2.dependencies, []) | ||||
|         self.assertEqual(set(migration3.dependencies), set([("testapp", "auto_1"), ("otherapp", "auto_1")])) | ||||
|  | ||||
|     def test_same_app_circular_fk_dependency(self): | ||||
|         """ | ||||
| @@ -376,23 +447,23 @@ class AutodetectorTests(TestCase): | ||||
|         autodetector = MigrationAutodetector(before, after) | ||||
|         changes = autodetector._detect_changes() | ||||
|         # Right number of migrations? | ||||
|         self.assertEqual(len(changes['testapp']), 2) | ||||
|         self.assertEqual(len(changes['testapp']), 1) | ||||
|         # Right number of actions? | ||||
|         migration1 = changes['testapp'][0] | ||||
|         self.assertEqual(len(migration1.operations), 2) | ||||
|         migration2 = changes['testapp'][1] | ||||
|         self.assertEqual(len(migration2.operations), 1) | ||||
|         self.assertEqual(len(migration1.operations), 4) | ||||
|         # Right actions? | ||||
|         action = migration1.operations[0] | ||||
|         self.assertEqual(action.__class__.__name__, "CreateModel") | ||||
|         action = migration1.operations[1] | ||||
|         self.assertEqual(action.__class__.__name__, "CreateModel") | ||||
|         action = migration2.operations[0] | ||||
|         action = migration1.operations[2] | ||||
|         self.assertEqual(action.__class__.__name__, "AddField") | ||||
|         self.assertEqual(action.name, "publisher") | ||||
|         action = migration1.operations[3] | ||||
|         self.assertEqual(action.__class__.__name__, "AddField") | ||||
|         self.assertEqual(action.name, "author") | ||||
|         # Right dependencies? | ||||
|         self.assertEqual(migration1.dependencies, []) | ||||
|         self.assertEqual(migration2.dependencies, [("testapp", "auto_1")]) | ||||
|  | ||||
|     def test_same_app_circular_fk_dependency_and_unique_together(self): | ||||
|         """ | ||||
| @@ -406,29 +477,22 @@ class AutodetectorTests(TestCase): | ||||
|         autodetector = MigrationAutodetector(before, after) | ||||
|         changes = autodetector._detect_changes() | ||||
|         # Right number of migrations? | ||||
|         self.assertEqual(len(changes['eggs']), 2) | ||||
|         self.assertEqual(len(changes['eggs']), 1) | ||||
|         # Right number of actions? | ||||
|         migration1 = changes['eggs'][0] | ||||
|         self.assertEqual(len(migration1.operations), 2) | ||||
|         migration2 = changes['eggs'][1] | ||||
|         self.assertEqual(len(migration2.operations), 2) | ||||
|         self.assertEqual(len(migration1.operations), 3) | ||||
|         # Right actions? | ||||
|         action = migration1.operations[0] | ||||
|         self.assertEqual(action.__class__.__name__, "CreateModel") | ||||
|         action = migration1.operations[1] | ||||
|         self.assertEqual(action.__class__.__name__, "CreateModel") | ||||
|         # CreateModel action for Rabbit should not have unique_together now | ||||
|         self.assertEqual(action.name, "Rabbit") | ||||
|         self.assertFalse("unique_together" in action.options) | ||||
|         action = migration2.operations[0] | ||||
|         self.assertEqual(action.__class__.__name__, "AddField") | ||||
|         self.assertEqual(action.name, "parent") | ||||
|         action = migration2.operations[1] | ||||
|         action = migration1.operations[2] | ||||
|         self.assertEqual(action.__class__.__name__, "AlterUniqueTogether") | ||||
|         self.assertEqual(action.name, "rabbit") | ||||
|         # Right dependencies? | ||||
|         self.assertEqual(migration1.dependencies, []) | ||||
|         self.assertEqual(migration2.dependencies, [("eggs", "auto_1")]) | ||||
|  | ||||
|     def test_unique_together(self): | ||||
|         "Tests unique_together detection" | ||||
| @@ -658,11 +722,54 @@ class AutodetectorTests(TestCase): | ||||
|         self.assertEqual(len(changes['otherapp']), 1) | ||||
|         # Right number of actions? | ||||
|         migration = changes['otherapp'][0] | ||||
|         self.assertEqual(len(migration.operations), 2) | ||||
|         self.assertEqual(len(migration.operations), 4) | ||||
|         # Right actions in right order? | ||||
|         # The first two are because we can't optimise RemoveField | ||||
|         # into DeleteModel reliably. | ||||
|         action = migration.operations[0] | ||||
|         self.assertEqual(action.__class__.__name__, "RemoveField") | ||||
|         self.assertEqual(action.name, "authors") | ||||
|         self.assertEqual(action.name, "author") | ||||
|         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.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