diff --git a/django/db/migrations/autodetector.py b/django/db/migrations/autodetector.py index a823b03738..6b01403d18 100644 --- a/django/db/migrations/autodetector.py +++ b/django/db/migrations/autodetector.py @@ -1,6 +1,7 @@ import functools import re -from collections import defaultdict +from collections import defaultdict, namedtuple +from enum import Enum from graphlib import TopologicalSorter from itertools import chain @@ -16,6 +17,26 @@ from django.db.migrations.utils import ( RegexObject, resolve_relation, ) +from django.utils.functional import cached_property + + +class OperationDependency( + namedtuple("OperationDependency", "app_label model_name field_name type") +): + class Type(Enum): + CREATE = 0 + REMOVE = 1 + ALTER = 2 + REMOVE_ORDER_WRT = 3 + ALTER_FOO_TOGETHER = 4 + + @cached_property + def model_name_lower(self): + return self.model_name.lower() + + @cached_property + def field_name_lower(self): + return self.field_name.lower() class MigrationAutodetector: @@ -255,12 +276,20 @@ class MigrationAutodetector: Return the resolved dependency and a boolean denoting whether or not it was swappable. """ - if dependency[0] != "__setting__": + if dependency.app_label != "__setting__": return dependency, False resolved_app_label, resolved_object_name = getattr( - settings, dependency[1] + settings, dependency.model_name ).split(".") - return (resolved_app_label, resolved_object_name.lower()) + dependency[2:], True + return ( + OperationDependency( + resolved_app_label, + resolved_object_name.lower(), + dependency.field_name, + dependency.type, + ), + True, + ) def _build_migration_list(self, graph=None): """ @@ -296,11 +325,11 @@ class MigrationAutodetector: # swappable dependencies. original_dep = dep dep, is_swappable_dep = self._resolve_dependency(dep) - if dep[0] != app_label: + if dep.app_label != app_label: # External app dependency. See if it's not yet # satisfied. for other_operation in self.generated_operations.get( - dep[0], [] + dep.app_label, [] ): if self.check_dependency(other_operation, dep): deps_satisfied = False @@ -310,11 +339,17 @@ class MigrationAutodetector: else: if is_swappable_dep: operation_dependencies.add( - (original_dep[0], original_dep[1]) + ( + original_dep.app_label, + original_dep.model_name, + ) ) - elif dep[0] in self.migrations: + elif dep.app_label in self.migrations: operation_dependencies.add( - (dep[0], self.migrations[dep[0]][-1].name) + ( + dep.app_label, + self.migrations[dep.app_label][-1].name, + ) ) else: # If we can't find the other app, we add a @@ -328,13 +363,13 @@ class MigrationAutodetector: # contains the target field. If it's # not yet migrated or has no # migrations, we use __first__. - if graph and graph.leaf_nodes(dep[0]): + if graph and graph.leaf_nodes(dep.app_label): operation_dependencies.add( - graph.leaf_nodes(dep[0])[0] + graph.leaf_nodes(dep.app_label)[0] ) else: operation_dependencies.add( - (dep[0], "__first__") + (dep.app_label, "__first__") ) else: deps_satisfied = False @@ -389,7 +424,7 @@ class MigrationAutodetector: # Resolve intra-app dependencies to handle circular # references involving a swappable model. dep = self._resolve_dependency(dep)[0] - if dep[0] != app_label: + if dep.app_label != app_label: continue ts.add(op, *(x for x in ops if self.check_dependency(x, dep))) self.generated_operations[app_label] = list(ts.static_order()) @@ -418,58 +453,79 @@ class MigrationAutodetector: False otherwise. """ # Created model - if dependency[2] is None and dependency[3] is True: + if ( + dependency.field_name is None + and dependency.type == OperationDependency.Type.CREATE + ): return ( isinstance(operation, operations.CreateModel) - and operation.name_lower == dependency[1].lower() + and operation.name_lower == dependency.model_name_lower ) # Created field - elif dependency[2] is not None and dependency[3] is True: + elif ( + dependency.field_name is not None + and dependency.type == OperationDependency.Type.CREATE + ): return ( isinstance(operation, operations.CreateModel) - and operation.name_lower == dependency[1].lower() - and any(dependency[2] == x for x, y in operation.fields) + and operation.name_lower == dependency.model_name_lower + and any(dependency.field_name == 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() + and operation.model_name_lower == dependency.model_name_lower + and operation.name_lower == dependency.field_name_lower ) # Removed field - elif dependency[2] is not None and dependency[3] is False: + elif ( + dependency.field_name is not None + and dependency.type == OperationDependency.Type.REMOVE + ): return ( isinstance(operation, operations.RemoveField) - and operation.model_name_lower == dependency[1].lower() - and operation.name_lower == dependency[2].lower() + and operation.model_name_lower == dependency.model_name_lower + and operation.name_lower == dependency.field_name_lower ) # Removed model - elif dependency[2] is None and dependency[3] is False: + elif ( + dependency.field_name is None + and dependency.type == OperationDependency.Type.REMOVE + ): return ( isinstance(operation, operations.DeleteModel) - and operation.name_lower == dependency[1].lower() + and operation.name_lower == dependency.model_name_lower ) # Field being altered - elif dependency[2] is not None and dependency[3] == "alter": + elif ( + dependency.field_name is not None + and dependency.type == OperationDependency.Type.ALTER + ): return ( isinstance(operation, operations.AlterField) - and operation.model_name_lower == dependency[1].lower() - and operation.name_lower == dependency[2].lower() + and operation.model_name_lower == dependency.model_name_lower + and operation.name_lower == dependency.field_name_lower ) # order_with_respect_to being unset for a field - elif dependency[2] is not None and dependency[3] == "order_wrt_unset": + elif ( + dependency.field_name is not None + and dependency.type == OperationDependency.Type.REMOVE_ORDER_WRT + ): return ( isinstance(operation, operations.AlterOrderWithRespectTo) - and operation.name_lower == dependency[1].lower() + and operation.name_lower == dependency.model_name_lower and (operation.order_with_respect_to or "").lower() - != dependency[2].lower() + != dependency.field_name_lower ) # Field is removed and part of an index/unique_together - elif dependency[2] is not None and dependency[3] == "foo_together_change": + elif ( + dependency.field_name is not None + and dependency.type == OperationDependency.Type.ALTER_FOO_TOGETHER + ): return ( isinstance( operation, (operations.AlterUniqueTogether, operations.AlterIndexTogether), ) - and operation.name_lower == dependency[1].lower() + and operation.name_lower == dependency.model_name_lower ) # Unknown dependency. Raise an error. else: @@ -615,13 +671,22 @@ class MigrationAutodetector: ) # Depend on the deletion of any possible proxy version of us dependencies = [ - (app_label, model_name, None, False), + OperationDependency( + app_label, model_name, None, OperationDependency.Type.REMOVE + ), ] # Depend on all bases for base in model_state.bases: if isinstance(base, str) and "." in base: base_app_label, base_name = base.split(".", 1) - dependencies.append((base_app_label, base_name, None, True)) + dependencies.append( + OperationDependency( + base_app_label, + base_name, + None, + OperationDependency.Type.CREATE, + ) + ) # Depend on the removal of base fields if the new model has # a field with the same name. old_base_model_state = self.from_state.models.get( @@ -640,17 +705,21 @@ class MigrationAutodetector: ) for removed_base_field in removed_base_fields: dependencies.append( - (base_app_label, base_name, removed_base_field, False) + OperationDependency( + base_app_label, + base_name, + removed_base_field, + OperationDependency.Type.REMOVE, + ) ) # Depend on the other end of the primary key if it's a relation if primary_key_rel: dependencies.append( - resolve_relation( - primary_key_rel, - app_label, - model_name, - ) - + (None, True) + OperationDependency( + *resolve_relation(primary_key_rel, app_label, model_name), + None, + OperationDependency.Type.CREATE, + ), ) # Generate creation operation self.add_operation( @@ -683,7 +752,11 @@ class MigrationAutodetector: self.to_state, ) # Depend on our own model being created - dependencies.append((app_label, model_name, None, True)) + dependencies.append( + OperationDependency( + app_label, model_name, None, OperationDependency.Type.CREATE + ) + ) # Make operation self.add_operation( app_label, @@ -703,14 +776,28 @@ class MigrationAutodetector: order_with_respect_to=order_with_respect_to, ), dependencies=[ - (app_label, model_name, order_with_respect_to, True), - (app_label, model_name, None, True), + OperationDependency( + app_label, + model_name, + order_with_respect_to, + OperationDependency.Type.CREATE, + ), + OperationDependency( + app_label, model_name, None, OperationDependency.Type.CREATE + ), ], ) related_dependencies = [ - (app_label, model_name, name, True) for name in sorted(related_fields) + OperationDependency( + app_label, model_name, name, OperationDependency.Type.CREATE + ) + for name in sorted(related_fields) ] - related_dependencies.append((app_label, model_name, None, True)) + related_dependencies.append( + OperationDependency( + app_label, model_name, None, OperationDependency.Type.CREATE + ) + ) for index in indexes: self.add_operation( app_label, @@ -754,7 +841,14 @@ class MigrationAutodetector: name=related_field_name, field=related_field, ), - dependencies=[(app_label, model_name, None, True)], + dependencies=[ + OperationDependency( + app_label, + model_name, + None, + OperationDependency.Type.CREATE, + ) + ], ) def generate_created_proxies(self): @@ -769,13 +863,22 @@ class MigrationAutodetector: assert model_state.options.get("proxy") # Depend on the deletion of any possible non-proxy version of us dependencies = [ - (app_label, model_name, None, False), + OperationDependency( + app_label, model_name, None, OperationDependency.Type.REMOVE + ), ] # Depend on all bases for base in model_state.bases: if isinstance(base, str) and "." in base: base_app_label, base_name = base.split(".", 1) - dependencies.append((base_app_label, base_name, None, True)) + dependencies.append( + OperationDependency( + base_app_label, + base_name, + None, + OperationDependency.Type.CREATE, + ) + ) # Generate creation operation self.add_operation( app_label, @@ -847,25 +950,34 @@ class MigrationAutodetector: ), relation_related_fields in relations[app_label, model_name].items(): for field_name, field in relation_related_fields.items(): dependencies.append( - (related_object_app_label, object_name, field_name, False), + OperationDependency( + related_object_app_label, + object_name, + field_name, + OperationDependency.Type.REMOVE, + ), ) if not field.many_to_many: dependencies.append( - ( + OperationDependency( related_object_app_label, object_name, field_name, - "alter", + OperationDependency.Type.ALTER, ), ) for name in sorted(related_fields): - dependencies.append((app_label, model_name, name, False)) + dependencies.append( + OperationDependency( + app_label, model_name, name, OperationDependency.Type.REMOVE + ) + ) # We're referenced in another field's through= through_user = self.through_users.get((app_label, model_state.name_lower)) if through_user: dependencies.append( - (through_user[0], through_user[1], through_user[2], False) + OperationDependency(*through_user, OperationDependency.Type.REMOVE), ) # Finally, make the operation, deduping any dependencies self.add_operation( @@ -998,7 +1110,11 @@ class MigrationAutodetector: def _generate_added_field(self, app_label, model_name, field_name): field = self.to_state.models[app_label, model_name].get_field(field_name) # Adding a field always depends at least on its removal. - dependencies = [(app_label, model_name, field_name, False)] + dependencies = [ + OperationDependency( + app_label, model_name, field_name, OperationDependency.Type.REMOVE + ) + ] # Fields that are foreignkeys/m2ms depend on stuff. if field.remote_field and field.remote_field.model: dependencies.extend( @@ -1065,8 +1181,18 @@ class MigrationAutodetector: # order_with_respect_to or index/unique_together operation; # this is safely ignored if there isn't one dependencies=[ - (app_label, model_name, field_name, "order_wrt_unset"), - (app_label, model_name, field_name, "foo_together_change"), + OperationDependency( + app_label, + model_name, + field_name, + OperationDependency.Type.REMOVE_ORDER_WRT, + ), + OperationDependency( + app_label, + model_name, + field_name, + OperationDependency.Type.ALTER_FOO_TOGETHER, + ), ], ) @@ -1399,14 +1525,25 @@ class MigrationAutodetector: app_label, model_name, ) - dependencies = [(dep_app_label, dep_object_name, None, True)] + dependencies = [ + OperationDependency( + dep_app_label, dep_object_name, None, OperationDependency.Type.CREATE + ) + ] if getattr(field.remote_field, "through", None): through_app_label, through_object_name = resolve_relation( field.remote_field.through, app_label, model_name, ) - dependencies.append((through_app_label, through_object_name, None, True)) + dependencies.append( + OperationDependency( + through_app_label, + through_object_name, + None, + OperationDependency.Type.CREATE, + ) + ) return dependencies def _get_dependencies_for_model(self, app_label, model_name): @@ -1617,11 +1754,11 @@ class MigrationAutodetector: dependencies = [] if new_model_state.options.get("order_with_respect_to"): dependencies.append( - ( + OperationDependency( app_label, model_name, new_model_state.options["order_with_respect_to"], - True, + OperationDependency.Type.CREATE, ) ) # Actually generate the operation