Loading AUTHORS +1 −0 Original line number Diff line number Diff line Loading @@ -323,6 +323,7 @@ answer newbie questions, and generally made Django that much better: Janos Guljas Jan Pazdziora Jan Rademaker Jarek Głowacki <jarekwg@gmail.com> Jarek Zgoda <jarek.zgoda@gmail.com> Jason Davies (Esaj) <http://www.jasondavies.com/> Jason Huggins <http://www.jrandolph.com/blog/> Loading django/db/migrations/exceptions.py +2 −1 Original line number Diff line number Diff line Loading @@ -52,8 +52,9 @@ class NodeNotFoundError(LookupError): Raised when an attempt on a node is made that is not available in the graph. """ def __init__(self, message, node): def __init__(self, message, node, origin=None): self.message = message self.origin = origin self.node = node def __str__(self): Loading django/db/migrations/graph.py +142 −11 Original line number Diff line number Diff line from __future__ import unicode_literals import sys import warnings from collections import deque from functools import total_ordering from django.db.migrations.state import ProjectState from django.utils import six from django.utils.datastructures import OrderedSet from django.utils.encoding import python_2_unicode_compatible Loading Loading @@ -79,6 +81,29 @@ class Node(object): return self.__dict__['_descendants'] class DummyNode(Node): def __init__(self, key, origin, error_message): super(DummyNode, self).__init__(key) self.origin = origin self.error_message = error_message def __repr__(self): return '<DummyNode: (%r, %r)>' % self.key def promote(self): """ Transition dummy to a normal node and clean off excess attribs. Creating a Node object from scratch would be too much of a hassle as many dependendies would need to be remapped. """ del self.origin del self.error_message self.__class__ = Node def raise_error(self): raise NodeNotFoundError(self.error_message, self.key, origin=self.origin) @python_2_unicode_compatible class MigrationGraph(object): """ Loading Loading @@ -108,27 +133,133 @@ class MigrationGraph(object): self.nodes = {} self.cached = False def add_node(self, key, implementation): def add_node(self, key, migration): # If the key already exists, then it must be a dummy node. dummy_node = self.node_map.get(key) if dummy_node: # Promote DummyNode to Node. dummy_node.promote() else: node = Node(key) self.node_map[key] = node self.nodes[key] = implementation self.nodes[key] = migration self.clear_cache() def add_dependency(self, migration, child, parent): def add_dummy_node(self, key, origin, error_message): node = DummyNode(key, origin, error_message) self.node_map[key] = node self.nodes[key] = None def add_dependency(self, migration, child, parent, skip_validation=False): """ This may create dummy nodes if they don't yet exist. If `skip_validation` is set, validate_consistency should be called afterwards. """ if child not in self.nodes: raise NodeNotFoundError( "Migration %s dependencies reference nonexistent child node %r" % (migration, child), child error_message = ( "Migration %s dependencies reference nonexistent" " child node %r" % (migration, child) ) self.add_dummy_node(child, migration, error_message) if parent not in self.nodes: raise NodeNotFoundError( "Migration %s dependencies reference nonexistent parent node %r" % (migration, parent), parent error_message = ( "Migration %s dependencies reference nonexistent" " parent node %r" % (migration, parent) ) self.add_dummy_node(parent, migration, error_message) self.node_map[child].add_parent(self.node_map[parent]) self.node_map[parent].add_child(self.node_map[child]) if not skip_validation: self.validate_consistency() self.clear_cache() def remove_replaced_nodes(self, replacement, replaced): """ Removes each of the `replaced` nodes (when they exist). Any dependencies that were referencing them are changed to reference the `replacement` node instead. """ # Cast list of replaced keys to set to speed up lookup later. replaced = set(replaced) try: replacement_node = self.node_map[replacement] except KeyError as exc: exc_value = NodeNotFoundError( "Unable to find replacement node %r. It was either never added" " to the migration graph, or has been removed." % (replacement, ), replacement ) exc_value.__cause__ = exc if not hasattr(exc, '__traceback__'): exc.__traceback__ = sys.exc_info()[2] six.reraise(NodeNotFoundError, exc_value, sys.exc_info()[2]) for replaced_key in replaced: self.nodes.pop(replaced_key, None) replaced_node = self.node_map.pop(replaced_key, None) if replaced_node: for child in replaced_node.children: child.parents.remove(replaced_node) # We don't want to create dependencies between the replaced # node and the replacement node as this would lead to # self-referencing on the replacement node at a later iteration. if child.key not in replaced: replacement_node.add_child(child) child.add_parent(replacement_node) for parent in replaced_node.parents: parent.children.remove(replaced_node) # Again, to avoid self-referencing. if parent.key not in replaced: replacement_node.add_parent(parent) parent.add_child(replacement_node) self.clear_cache() def remove_replacement_node(self, replacement, replaced): """ The inverse operation to `remove_replaced_nodes`. Almost. Removes the replacement node `replacement` and remaps its child nodes to `replaced` - the list of nodes it would have replaced. Its parent nodes are not remapped as they are expected to be correct already. """ self.nodes.pop(replacement, None) try: replacement_node = self.node_map.pop(replacement) except KeyError as exc: exc_value = NodeNotFoundError( "Unable to remove replacement node %r. It was either never added" " to the migration graph, or has been removed already." % (replacement, ), replacement ) exc_value.__cause__ = exc if not hasattr(exc, '__traceback__'): exc.__traceback__ = sys.exc_info()[2] six.reraise(NodeNotFoundError, exc_value, sys.exc_info()[2]) replaced_nodes = set() replaced_nodes_parents = set() for key in replaced: replaced_node = self.node_map.get(key) if replaced_node: replaced_nodes.add(replaced_node) replaced_nodes_parents |= replaced_node.parents # We're only interested in the latest replaced node, so filter out # replaced nodes that are parents of other replaced nodes. replaced_nodes -= replaced_nodes_parents for child in replacement_node.children: child.parents.remove(replacement_node) for replaced_node in replaced_nodes: replaced_node.add_child(child) child.add_parent(replaced_node) for parent in replacement_node.parents: parent.children.remove(replacement_node) # NOTE: There is no need to remap parent dependencies as we can # assume the replaced nodes already have the correct ancestry. self.clear_cache() def validate_consistency(self): """ Ensure there are no dummy nodes remaining in the graph. """ [n.raise_error() for n in self.node_map.values() if isinstance(n, DummyNode)] def clear_cache(self): if self.cached: for node in self.nodes: Loading django/db/migrations/loader.py +70 −122 Original line number Diff line number Diff line Loading @@ -165,6 +165,30 @@ class MigrationLoader(object): raise ValueError("Dependency on app with no migrations: %s" % key[0]) raise ValueError("Dependency on unknown app: %s" % key[0]) def add_internal_dependencies(self, key, migration): """ Internal dependencies need to be added first to ensure `__first__` dependencies find the correct root node. """ for parent in migration.dependencies: if parent[0] != key[0] or parent[1] == '__first__': # Ignore __first__ references to the same app (#22325). continue self.graph.add_dependency(migration, key, parent, skip_validation=True) def add_external_dependencies(self, key, migration): for parent in migration.dependencies: # Skip internal dependencies if key[0] == parent[0]: continue parent = self.check_key(parent, key[0]) if parent is not None: self.graph.add_dependency(migration, key, parent, skip_validation=True) for child in migration.run_before: child = self.check_key(child, key[0]) if child is not None: self.graph.add_dependency(migration, child, key, skip_validation=True) def build_graph(self): """ Builds a migration dependency graph using both the disk and database. Loading @@ -179,92 +203,54 @@ class MigrationLoader(object): else: recorder = MigrationRecorder(self.connection) self.applied_migrations = recorder.applied_migrations() # Do a first pass to separate out replacing and non-replacing migrations normal = {} replacing = {} # To start, populate the migration graph with nodes for ALL migrations # and their dependencies. Also make note of replacing migrations at this step. self.graph = MigrationGraph() self.replacements = {} for key, migration in self.disk_migrations.items(): self.graph.add_node(key, migration) # Internal (aka same-app) dependencies. self.add_internal_dependencies(key, migration) # Replacing migrations. if migration.replaces: replacing[key] = migration self.replacements[key] = migration # Add external dependencies now that the internal ones have been resolved. for key, migration in self.disk_migrations.items(): self.add_external_dependencies(key, migration) # Carry out replacements where possible. for key, migration in self.replacements.items(): # Get applied status of each of this migration's replacement targets. applied_statuses = [(target in self.applied_migrations) for target in migration.replaces] # Ensure the replacing migration is only marked as applied if all of # its replacement targets are. if all(applied_statuses): self.applied_migrations.add(key) else: normal[key] = migration # Calculate reverse dependencies - i.e., for each migration, what depends on it? # This is just for dependency re-pointing when applying replacements, # so we ignore run_before here. reverse_dependencies = {} for key, migration in normal.items(): for parent in migration.dependencies: reverse_dependencies.setdefault(parent, set()).add(key) # Remember the possible replacements to generate more meaningful error # messages self.applied_migrations.discard(key) # A replacing migration can be used if either all or none of its # replacement targets have been applied. if all(applied_statuses) or (not any(applied_statuses)): self.graph.remove_replaced_nodes(key, migration.replaces) else: # This replacing migration cannot be used because it is partially applied. # Remove it from the graph and remap dependencies to it (#25945). self.graph.remove_replacement_node(key, migration.replaces) # Ensure the graph is consistent. try: self.graph.validate_consistency() except NodeNotFoundError as exc: # Check if the missing node could have been replaced by any squash # migration but wasn't because the squash migration was partially # applied before. In that case raise a more understandable exception # (#23556). # Get reverse replacements. reverse_replacements = {} for key, migration in replacing.items(): for key, migration in self.replacements.items(): for replaced in migration.replaces: reverse_replacements.setdefault(replaced, set()).add(key) # Carry out replacements if we can - that is, if all replaced migrations # are either unapplied or missing. for key, migration in replacing.items(): # Ensure this replacement migration is not in applied_migrations self.applied_migrations.discard(key) # Do the check. We can replace if all our replace targets are # applied, or if all of them are unapplied. applied_statuses = [(target in self.applied_migrations) for target in migration.replaces] can_replace = all(applied_statuses) or (not any(applied_statuses)) if not can_replace: continue # Alright, time to replace. Step through the replaced migrations # and remove, repointing dependencies if needs be. for replaced in migration.replaces: if replaced in normal: # We don't care if the replaced migration doesn't exist; # the usage pattern here is to delete things after a while. del normal[replaced] for child_key in reverse_dependencies.get(replaced, set()): if child_key in migration.replaces: continue # List of migrations whose dependency on `replaced` needs # to be updated to a dependency on `key`. to_update = [] # Child key may itself be replaced, in which case it might # not be in `normal` anymore (depending on whether we've # processed its replacement yet). If it's present, we go # ahead and update it; it may be deleted later on if it is # replaced, but there's no harm in updating it regardless. if child_key in normal: to_update.append(normal[child_key]) # If the child key is replaced, we update its replacement's # dependencies too, if necessary. (We don't know if this # replacement will actually take effect or not, but either # way it's OK to update the replacing migration). if child_key in reverse_replacements: for replaces_child_key in reverse_replacements[child_key]: if replaced in replacing[replaces_child_key].dependencies: to_update.append(replacing[replaces_child_key]) # Actually perform the dependency update on all migrations # that require it. for migration_needing_update in to_update: migration_needing_update.dependencies.remove(replaced) migration_needing_update.dependencies.append(key) normal[key] = migration # Mark the replacement as applied if all its replaced ones are if all(applied_statuses): self.applied_migrations.add(key) # Store the replacement migrations for later checks self.replacements = replacing # Finally, make a graph and load everything into it self.graph = MigrationGraph() for key, migration in normal.items(): self.graph.add_node(key, migration) def _reraise_missing_dependency(migration, missing, exc): """ Checks if ``missing`` could have been replaced by any squash migration but wasn't because the the squash migration was partially applied before. In that case raise a more understandable exception. #23556 """ if missing in reverse_replacements: candidates = reverse_replacements.get(missing, set()) # Try to reraise exception with more detail. if exc.node in reverse_replacements: candidates = reverse_replacements.get(exc.node, set()) is_replaced = any(candidate in self.graph.nodes for candidate in candidates) if not is_replaced: tries = ', '.join('%s.%s' % c for c in candidates) Loading @@ -273,54 +259,16 @@ class MigrationLoader(object): "Django tried to replace migration {1}.{2} with any of [{3}] " "but wasn't able to because some of the replaced migrations " "are already applied.".format( migration, missing[0], missing[1], tries exc.origin, exc.node[0], exc.node[1], tries ), missing) exc.node ) exc_value.__cause__ = exc if not hasattr(exc, '__traceback__'): exc.__traceback__ = sys.exc_info()[2] six.reraise(NodeNotFoundError, exc_value, sys.exc_info()[2]) raise exc # Add all internal dependencies first to ensure __first__ dependencies # find the correct root node. for key, migration in normal.items(): for parent in migration.dependencies: if parent[0] != key[0] or parent[1] == '__first__': # Ignore __first__ references to the same app (#22325) continue try: self.graph.add_dependency(migration, key, parent) except NodeNotFoundError as e: # Since we added "key" to the nodes before this implies # "parent" is not in there. To make the raised exception # more understandable we check if parent could have been # replaced but hasn't (eg partially applied squashed # migration) _reraise_missing_dependency(migration, parent, e) for key, migration in normal.items(): for parent in migration.dependencies: if parent[0] == key[0]: # Internal dependencies already added. continue parent = self.check_key(parent, key[0]) if parent is not None: try: self.graph.add_dependency(migration, key, parent) except NodeNotFoundError as e: # Since we added "key" to the nodes before this implies # "parent" is not in there. _reraise_missing_dependency(migration, parent, e) for child in migration.run_before: child = self.check_key(child, key[0]) if child is not None: try: self.graph.add_dependency(migration, child, key) except NodeNotFoundError as e: # Since we added "key" to the nodes before this implies # "child" is not in there. _reraise_missing_dependency(migration, child, e) def check_consistent_history(self, connection): """ Raise InconsistentMigrationHistory if any applied migrations have Loading tests/migrations/test_graph.py +116 −0 File changed.Preview size limit exceeded, changes collapsed. Show changes Loading
AUTHORS +1 −0 Original line number Diff line number Diff line Loading @@ -323,6 +323,7 @@ answer newbie questions, and generally made Django that much better: Janos Guljas Jan Pazdziora Jan Rademaker Jarek Głowacki <jarekwg@gmail.com> Jarek Zgoda <jarek.zgoda@gmail.com> Jason Davies (Esaj) <http://www.jasondavies.com/> Jason Huggins <http://www.jrandolph.com/blog/> Loading
django/db/migrations/exceptions.py +2 −1 Original line number Diff line number Diff line Loading @@ -52,8 +52,9 @@ class NodeNotFoundError(LookupError): Raised when an attempt on a node is made that is not available in the graph. """ def __init__(self, message, node): def __init__(self, message, node, origin=None): self.message = message self.origin = origin self.node = node def __str__(self): Loading
django/db/migrations/graph.py +142 −11 Original line number Diff line number Diff line from __future__ import unicode_literals import sys import warnings from collections import deque from functools import total_ordering from django.db.migrations.state import ProjectState from django.utils import six from django.utils.datastructures import OrderedSet from django.utils.encoding import python_2_unicode_compatible Loading Loading @@ -79,6 +81,29 @@ class Node(object): return self.__dict__['_descendants'] class DummyNode(Node): def __init__(self, key, origin, error_message): super(DummyNode, self).__init__(key) self.origin = origin self.error_message = error_message def __repr__(self): return '<DummyNode: (%r, %r)>' % self.key def promote(self): """ Transition dummy to a normal node and clean off excess attribs. Creating a Node object from scratch would be too much of a hassle as many dependendies would need to be remapped. """ del self.origin del self.error_message self.__class__ = Node def raise_error(self): raise NodeNotFoundError(self.error_message, self.key, origin=self.origin) @python_2_unicode_compatible class MigrationGraph(object): """ Loading Loading @@ -108,27 +133,133 @@ class MigrationGraph(object): self.nodes = {} self.cached = False def add_node(self, key, implementation): def add_node(self, key, migration): # If the key already exists, then it must be a dummy node. dummy_node = self.node_map.get(key) if dummy_node: # Promote DummyNode to Node. dummy_node.promote() else: node = Node(key) self.node_map[key] = node self.nodes[key] = implementation self.nodes[key] = migration self.clear_cache() def add_dependency(self, migration, child, parent): def add_dummy_node(self, key, origin, error_message): node = DummyNode(key, origin, error_message) self.node_map[key] = node self.nodes[key] = None def add_dependency(self, migration, child, parent, skip_validation=False): """ This may create dummy nodes if they don't yet exist. If `skip_validation` is set, validate_consistency should be called afterwards. """ if child not in self.nodes: raise NodeNotFoundError( "Migration %s dependencies reference nonexistent child node %r" % (migration, child), child error_message = ( "Migration %s dependencies reference nonexistent" " child node %r" % (migration, child) ) self.add_dummy_node(child, migration, error_message) if parent not in self.nodes: raise NodeNotFoundError( "Migration %s dependencies reference nonexistent parent node %r" % (migration, parent), parent error_message = ( "Migration %s dependencies reference nonexistent" " parent node %r" % (migration, parent) ) self.add_dummy_node(parent, migration, error_message) self.node_map[child].add_parent(self.node_map[parent]) self.node_map[parent].add_child(self.node_map[child]) if not skip_validation: self.validate_consistency() self.clear_cache() def remove_replaced_nodes(self, replacement, replaced): """ Removes each of the `replaced` nodes (when they exist). Any dependencies that were referencing them are changed to reference the `replacement` node instead. """ # Cast list of replaced keys to set to speed up lookup later. replaced = set(replaced) try: replacement_node = self.node_map[replacement] except KeyError as exc: exc_value = NodeNotFoundError( "Unable to find replacement node %r. It was either never added" " to the migration graph, or has been removed." % (replacement, ), replacement ) exc_value.__cause__ = exc if not hasattr(exc, '__traceback__'): exc.__traceback__ = sys.exc_info()[2] six.reraise(NodeNotFoundError, exc_value, sys.exc_info()[2]) for replaced_key in replaced: self.nodes.pop(replaced_key, None) replaced_node = self.node_map.pop(replaced_key, None) if replaced_node: for child in replaced_node.children: child.parents.remove(replaced_node) # We don't want to create dependencies between the replaced # node and the replacement node as this would lead to # self-referencing on the replacement node at a later iteration. if child.key not in replaced: replacement_node.add_child(child) child.add_parent(replacement_node) for parent in replaced_node.parents: parent.children.remove(replaced_node) # Again, to avoid self-referencing. if parent.key not in replaced: replacement_node.add_parent(parent) parent.add_child(replacement_node) self.clear_cache() def remove_replacement_node(self, replacement, replaced): """ The inverse operation to `remove_replaced_nodes`. Almost. Removes the replacement node `replacement` and remaps its child nodes to `replaced` - the list of nodes it would have replaced. Its parent nodes are not remapped as they are expected to be correct already. """ self.nodes.pop(replacement, None) try: replacement_node = self.node_map.pop(replacement) except KeyError as exc: exc_value = NodeNotFoundError( "Unable to remove replacement node %r. It was either never added" " to the migration graph, or has been removed already." % (replacement, ), replacement ) exc_value.__cause__ = exc if not hasattr(exc, '__traceback__'): exc.__traceback__ = sys.exc_info()[2] six.reraise(NodeNotFoundError, exc_value, sys.exc_info()[2]) replaced_nodes = set() replaced_nodes_parents = set() for key in replaced: replaced_node = self.node_map.get(key) if replaced_node: replaced_nodes.add(replaced_node) replaced_nodes_parents |= replaced_node.parents # We're only interested in the latest replaced node, so filter out # replaced nodes that are parents of other replaced nodes. replaced_nodes -= replaced_nodes_parents for child in replacement_node.children: child.parents.remove(replacement_node) for replaced_node in replaced_nodes: replaced_node.add_child(child) child.add_parent(replaced_node) for parent in replacement_node.parents: parent.children.remove(replacement_node) # NOTE: There is no need to remap parent dependencies as we can # assume the replaced nodes already have the correct ancestry. self.clear_cache() def validate_consistency(self): """ Ensure there are no dummy nodes remaining in the graph. """ [n.raise_error() for n in self.node_map.values() if isinstance(n, DummyNode)] def clear_cache(self): if self.cached: for node in self.nodes: Loading
django/db/migrations/loader.py +70 −122 Original line number Diff line number Diff line Loading @@ -165,6 +165,30 @@ class MigrationLoader(object): raise ValueError("Dependency on app with no migrations: %s" % key[0]) raise ValueError("Dependency on unknown app: %s" % key[0]) def add_internal_dependencies(self, key, migration): """ Internal dependencies need to be added first to ensure `__first__` dependencies find the correct root node. """ for parent in migration.dependencies: if parent[0] != key[0] or parent[1] == '__first__': # Ignore __first__ references to the same app (#22325). continue self.graph.add_dependency(migration, key, parent, skip_validation=True) def add_external_dependencies(self, key, migration): for parent in migration.dependencies: # Skip internal dependencies if key[0] == parent[0]: continue parent = self.check_key(parent, key[0]) if parent is not None: self.graph.add_dependency(migration, key, parent, skip_validation=True) for child in migration.run_before: child = self.check_key(child, key[0]) if child is not None: self.graph.add_dependency(migration, child, key, skip_validation=True) def build_graph(self): """ Builds a migration dependency graph using both the disk and database. Loading @@ -179,92 +203,54 @@ class MigrationLoader(object): else: recorder = MigrationRecorder(self.connection) self.applied_migrations = recorder.applied_migrations() # Do a first pass to separate out replacing and non-replacing migrations normal = {} replacing = {} # To start, populate the migration graph with nodes for ALL migrations # and their dependencies. Also make note of replacing migrations at this step. self.graph = MigrationGraph() self.replacements = {} for key, migration in self.disk_migrations.items(): self.graph.add_node(key, migration) # Internal (aka same-app) dependencies. self.add_internal_dependencies(key, migration) # Replacing migrations. if migration.replaces: replacing[key] = migration self.replacements[key] = migration # Add external dependencies now that the internal ones have been resolved. for key, migration in self.disk_migrations.items(): self.add_external_dependencies(key, migration) # Carry out replacements where possible. for key, migration in self.replacements.items(): # Get applied status of each of this migration's replacement targets. applied_statuses = [(target in self.applied_migrations) for target in migration.replaces] # Ensure the replacing migration is only marked as applied if all of # its replacement targets are. if all(applied_statuses): self.applied_migrations.add(key) else: normal[key] = migration # Calculate reverse dependencies - i.e., for each migration, what depends on it? # This is just for dependency re-pointing when applying replacements, # so we ignore run_before here. reverse_dependencies = {} for key, migration in normal.items(): for parent in migration.dependencies: reverse_dependencies.setdefault(parent, set()).add(key) # Remember the possible replacements to generate more meaningful error # messages self.applied_migrations.discard(key) # A replacing migration can be used if either all or none of its # replacement targets have been applied. if all(applied_statuses) or (not any(applied_statuses)): self.graph.remove_replaced_nodes(key, migration.replaces) else: # This replacing migration cannot be used because it is partially applied. # Remove it from the graph and remap dependencies to it (#25945). self.graph.remove_replacement_node(key, migration.replaces) # Ensure the graph is consistent. try: self.graph.validate_consistency() except NodeNotFoundError as exc: # Check if the missing node could have been replaced by any squash # migration but wasn't because the squash migration was partially # applied before. In that case raise a more understandable exception # (#23556). # Get reverse replacements. reverse_replacements = {} for key, migration in replacing.items(): for key, migration in self.replacements.items(): for replaced in migration.replaces: reverse_replacements.setdefault(replaced, set()).add(key) # Carry out replacements if we can - that is, if all replaced migrations # are either unapplied or missing. for key, migration in replacing.items(): # Ensure this replacement migration is not in applied_migrations self.applied_migrations.discard(key) # Do the check. We can replace if all our replace targets are # applied, or if all of them are unapplied. applied_statuses = [(target in self.applied_migrations) for target in migration.replaces] can_replace = all(applied_statuses) or (not any(applied_statuses)) if not can_replace: continue # Alright, time to replace. Step through the replaced migrations # and remove, repointing dependencies if needs be. for replaced in migration.replaces: if replaced in normal: # We don't care if the replaced migration doesn't exist; # the usage pattern here is to delete things after a while. del normal[replaced] for child_key in reverse_dependencies.get(replaced, set()): if child_key in migration.replaces: continue # List of migrations whose dependency on `replaced` needs # to be updated to a dependency on `key`. to_update = [] # Child key may itself be replaced, in which case it might # not be in `normal` anymore (depending on whether we've # processed its replacement yet). If it's present, we go # ahead and update it; it may be deleted later on if it is # replaced, but there's no harm in updating it regardless. if child_key in normal: to_update.append(normal[child_key]) # If the child key is replaced, we update its replacement's # dependencies too, if necessary. (We don't know if this # replacement will actually take effect or not, but either # way it's OK to update the replacing migration). if child_key in reverse_replacements: for replaces_child_key in reverse_replacements[child_key]: if replaced in replacing[replaces_child_key].dependencies: to_update.append(replacing[replaces_child_key]) # Actually perform the dependency update on all migrations # that require it. for migration_needing_update in to_update: migration_needing_update.dependencies.remove(replaced) migration_needing_update.dependencies.append(key) normal[key] = migration # Mark the replacement as applied if all its replaced ones are if all(applied_statuses): self.applied_migrations.add(key) # Store the replacement migrations for later checks self.replacements = replacing # Finally, make a graph and load everything into it self.graph = MigrationGraph() for key, migration in normal.items(): self.graph.add_node(key, migration) def _reraise_missing_dependency(migration, missing, exc): """ Checks if ``missing`` could have been replaced by any squash migration but wasn't because the the squash migration was partially applied before. In that case raise a more understandable exception. #23556 """ if missing in reverse_replacements: candidates = reverse_replacements.get(missing, set()) # Try to reraise exception with more detail. if exc.node in reverse_replacements: candidates = reverse_replacements.get(exc.node, set()) is_replaced = any(candidate in self.graph.nodes for candidate in candidates) if not is_replaced: tries = ', '.join('%s.%s' % c for c in candidates) Loading @@ -273,54 +259,16 @@ class MigrationLoader(object): "Django tried to replace migration {1}.{2} with any of [{3}] " "but wasn't able to because some of the replaced migrations " "are already applied.".format( migration, missing[0], missing[1], tries exc.origin, exc.node[0], exc.node[1], tries ), missing) exc.node ) exc_value.__cause__ = exc if not hasattr(exc, '__traceback__'): exc.__traceback__ = sys.exc_info()[2] six.reraise(NodeNotFoundError, exc_value, sys.exc_info()[2]) raise exc # Add all internal dependencies first to ensure __first__ dependencies # find the correct root node. for key, migration in normal.items(): for parent in migration.dependencies: if parent[0] != key[0] or parent[1] == '__first__': # Ignore __first__ references to the same app (#22325) continue try: self.graph.add_dependency(migration, key, parent) except NodeNotFoundError as e: # Since we added "key" to the nodes before this implies # "parent" is not in there. To make the raised exception # more understandable we check if parent could have been # replaced but hasn't (eg partially applied squashed # migration) _reraise_missing_dependency(migration, parent, e) for key, migration in normal.items(): for parent in migration.dependencies: if parent[0] == key[0]: # Internal dependencies already added. continue parent = self.check_key(parent, key[0]) if parent is not None: try: self.graph.add_dependency(migration, key, parent) except NodeNotFoundError as e: # Since we added "key" to the nodes before this implies # "parent" is not in there. _reraise_missing_dependency(migration, parent, e) for child in migration.run_before: child = self.check_key(child, key[0]) if child is not None: try: self.graph.add_dependency(migration, child, key) except NodeNotFoundError as e: # Since we added "key" to the nodes before this implies # "child" is not in there. _reraise_missing_dependency(migration, child, e) def check_consistent_history(self, connection): """ Raise InconsistentMigrationHistory if any applied migrations have Loading
tests/migrations/test_graph.py +116 −0 File changed.Preview size limit exceeded, changes collapsed. Show changes