parent
6b5926978b
commit
509379a161
1
AUTHORS
1
AUTHORS
|
@ -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/>
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
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
|
||||
|
||||
|
@ -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):
|
||||
"""
|
||||
|
@ -108,27 +133,133 @@ class MigrationGraph(object):
|
|||
self.nodes = {}
|
||||
self.cached = False
|
||||
|
||||
def add_node(self, key, implementation):
|
||||
node = Node(key)
|
||||
self.node_map[key] = node
|
||||
self.nodes[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] = 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:
|
||||
|
|
|
@ -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.
|
||||
|
@ -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
|
||||
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
|
||||
reverse_replacements = {}
|
||||
for key, migration in replacing.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.
|
||||
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]
|
||||
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
|
||||
# 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)
|
||||
# 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())
|
||||
else:
|
||||
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 self.replacements.items():
|
||||
for replaced in migration.replaces:
|
||||
reverse_replacements.setdefault(replaced, set()).add(key)
|
||||
# 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)
|
||||
|
@ -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
|
||||
|
|
|
@ -250,6 +250,122 @@ class GraphTests(SimpleTestCase):
|
|||
with self.assertRaisesMessage(NodeNotFoundError, msg):
|
||||
graph.add_dependency("app_a.0002", ("app_a", "0002"), ("app_a", "0001"))
|
||||
|
||||
def test_validate_consistency(self):
|
||||
"""
|
||||
Tests for missing nodes, using `validate_consistency()` to raise the error.
|
||||
"""
|
||||
# Build graph
|
||||
graph = MigrationGraph()
|
||||
graph.add_node(("app_a", "0001"), None)
|
||||
# Add dependency with missing parent node (skipping validation).
|
||||
graph.add_dependency("app_a.0001", ("app_a", "0001"), ("app_b", "0002"), skip_validation=True)
|
||||
msg = "Migration app_a.0001 dependencies reference nonexistent parent node ('app_b', '0002')"
|
||||
with self.assertRaisesMessage(NodeNotFoundError, msg):
|
||||
graph.validate_consistency()
|
||||
# Add missing parent node and ensure `validate_consistency()` no longer raises error.
|
||||
graph.add_node(("app_b", "0002"), None)
|
||||
graph.validate_consistency()
|
||||
# Add dependency with missing child node (skipping validation).
|
||||
graph.add_dependency("app_a.0002", ("app_a", "0002"), ("app_a", "0001"), skip_validation=True)
|
||||
msg = "Migration app_a.0002 dependencies reference nonexistent child node ('app_a', '0002')"
|
||||
with self.assertRaisesMessage(NodeNotFoundError, msg):
|
||||
graph.validate_consistency()
|
||||
# Add missing child node and ensure `validate_consistency()` no longer raises error.
|
||||
graph.add_node(("app_a", "0002"), None)
|
||||
graph.validate_consistency()
|
||||
# Rawly add dummy node.
|
||||
msg = "app_a.0001 (req'd by app_a.0002) is missing!"
|
||||
graph.add_dummy_node(
|
||||
key=("app_a", "0001"),
|
||||
origin="app_a.0002",
|
||||
error_message=msg
|
||||
)
|
||||
with self.assertRaisesMessage(NodeNotFoundError, msg):
|
||||
graph.validate_consistency()
|
||||
|
||||
def test_remove_replaced_nodes(self):
|
||||
"""
|
||||
Tests that replaced nodes are properly removed and dependencies remapped.
|
||||
"""
|
||||
# Add some dummy nodes to be replaced.
|
||||
graph = MigrationGraph()
|
||||
graph.add_dummy_node(key=("app_a", "0001"), origin="app_a.0002", error_message="BAD!")
|
||||
graph.add_dummy_node(key=("app_a", "0002"), origin="app_b.0001", error_message="BAD!")
|
||||
graph.add_dependency("app_a.0002", ("app_a", "0002"), ("app_a", "0001"), skip_validation=True)
|
||||
# Add some normal parent and child nodes to test dependency remapping.
|
||||
graph.add_node(("app_c", "0001"), None)
|
||||
graph.add_node(("app_b", "0001"), None)
|
||||
graph.add_dependency("app_a.0001", ("app_a", "0001"), ("app_c", "0001"), skip_validation=True)
|
||||
graph.add_dependency("app_b.0001", ("app_b", "0001"), ("app_a", "0002"), skip_validation=True)
|
||||
# Try replacing before replacement node exists.
|
||||
msg = (
|
||||
"Unable to find replacement node ('app_a', '0001_squashed_0002'). It was either"
|
||||
" never added to the migration graph, or has been removed."
|
||||
)
|
||||
with self.assertRaisesMessage(NodeNotFoundError, msg):
|
||||
graph.remove_replaced_nodes(
|
||||
replacement=("app_a", "0001_squashed_0002"),
|
||||
replaced=[("app_a", "0001"), ("app_a", "0002")]
|
||||
)
|
||||
graph.add_node(("app_a", "0001_squashed_0002"), None)
|
||||
# Ensure `validate_consistency()` still raises an error at this stage.
|
||||
with self.assertRaisesMessage(NodeNotFoundError, "BAD!"):
|
||||
graph.validate_consistency()
|
||||
# Remove the dummy nodes.
|
||||
graph.remove_replaced_nodes(
|
||||
replacement=("app_a", "0001_squashed_0002"),
|
||||
replaced=[("app_a", "0001"), ("app_a", "0002")]
|
||||
)
|
||||
# Ensure graph is now consistent and dependencies have been remapped
|
||||
graph.validate_consistency()
|
||||
parent_node = graph.node_map[("app_c", "0001")]
|
||||
replacement_node = graph.node_map[("app_a", "0001_squashed_0002")]
|
||||
child_node = graph.node_map[("app_b", "0001")]
|
||||
self.assertIn(parent_node, replacement_node.parents)
|
||||
self.assertIn(replacement_node, parent_node.children)
|
||||
self.assertIn(child_node, replacement_node.children)
|
||||
self.assertIn(replacement_node, child_node.parents)
|
||||
|
||||
def test_remove_replacement_node(self):
|
||||
"""
|
||||
Tests that a replacement node is properly removed and child dependencies remapped.
|
||||
We assume parent dependencies are already correct.
|
||||
"""
|
||||
# Add some dummy nodes to be replaced.
|
||||
graph = MigrationGraph()
|
||||
graph.add_node(("app_a", "0001"), None)
|
||||
graph.add_node(("app_a", "0002"), None)
|
||||
graph.add_dependency("app_a.0002", ("app_a", "0002"), ("app_a", "0001"))
|
||||
# Try removing replacement node before replacement node exists.
|
||||
msg = (
|
||||
"Unable to remove replacement node ('app_a', '0001_squashed_0002'). It was"
|
||||
" either never added to the migration graph, or has been removed already."
|
||||
)
|
||||
with self.assertRaisesMessage(NodeNotFoundError, msg):
|
||||
graph.remove_replacement_node(
|
||||
replacement=("app_a", "0001_squashed_0002"),
|
||||
replaced=[("app_a", "0001"), ("app_a", "0002")]
|
||||
)
|
||||
graph.add_node(("app_a", "0001_squashed_0002"), None)
|
||||
# Add a child node to test dependency remapping.
|
||||
graph.add_node(("app_b", "0001"), None)
|
||||
graph.add_dependency("app_b.0001", ("app_b", "0001"), ("app_a", "0001_squashed_0002"))
|
||||
# Remove the replacement node.
|
||||
graph.remove_replacement_node(
|
||||
replacement=("app_a", "0001_squashed_0002"),
|
||||
replaced=[("app_a", "0001"), ("app_a", "0002")]
|
||||
)
|
||||
# Ensure graph is consistent and child dependency has been remapped
|
||||
graph.validate_consistency()
|
||||
replaced_node = graph.node_map[("app_a", "0002")]
|
||||
child_node = graph.node_map[("app_b", "0001")]
|
||||
self.assertIn(child_node, replaced_node.children)
|
||||
self.assertIn(replaced_node, child_node.parents)
|
||||
# Ensure child dependency hasn't also gotten remapped to the other replaced node.
|
||||
other_replaced_node = graph.node_map[("app_a", "0001")]
|
||||
self.assertNotIn(child_node, other_replaced_node.children)
|
||||
self.assertNotIn(other_replaced_node, child_node.parents)
|
||||
|
||||
def test_infinite_loop(self):
|
||||
"""
|
||||
Tests a complex dependency graph:
|
||||
|
|
|
@ -397,3 +397,70 @@ class LoaderTests(TestCase):
|
|||
msg = "Migration migrations.0002_second is applied before its dependency migrations.0001_initial"
|
||||
with self.assertRaisesMessage(InconsistentMigrationHistory, msg):
|
||||
loader.check_consistent_history(connection)
|
||||
|
||||
@override_settings(MIGRATION_MODULES={
|
||||
"app1": "migrations.test_migrations_squashed_ref_squashed.app1",
|
||||
"app2": "migrations.test_migrations_squashed_ref_squashed.app2",
|
||||
})
|
||||
@modify_settings(INSTALLED_APPS={'append': [
|
||||
"migrations.test_migrations_squashed_ref_squashed.app1",
|
||||
"migrations.test_migrations_squashed_ref_squashed.app2",
|
||||
]})
|
||||
def test_loading_squashed_ref_squashed(self):
|
||||
"Tests loading a squashed migration with a new migration referencing it"
|
||||
"""
|
||||
The sample migrations are structred like this:
|
||||
|
||||
app_1 1 --> 2 ---------------------*--> 3 *--> 4
|
||||
\ / /
|
||||
*-------------------*----/--> 2_sq_3 --*
|
||||
\ / /
|
||||
=============== \ ============= / == / ======================
|
||||
app_2 *--> 1_sq_2 --* /
|
||||
\ /
|
||||
*--> 1 --> 2 --*
|
||||
|
||||
Where 2_sq_3 is a replacing migration for 2 and 3 in app_1,
|
||||
as 1_sq_2 is a replacing migration for 1 and 2 in app_2.
|
||||
"""
|
||||
|
||||
loader = MigrationLoader(connection)
|
||||
recorder = MigrationRecorder(connection)
|
||||
self.addCleanup(recorder.flush)
|
||||
|
||||
# Load with nothing applied: both migrations squashed.
|
||||
loader.build_graph()
|
||||
plan = set(loader.graph.forwards_plan(('app1', '4_auto')))
|
||||
plan = plan - loader.applied_migrations
|
||||
expected_plan = {
|
||||
('app1', '1_auto'),
|
||||
('app2', '1_squashed_2'),
|
||||
('app1', '2_squashed_3'),
|
||||
('app1', '4_auto'),
|
||||
}
|
||||
self.assertEqual(plan, expected_plan)
|
||||
|
||||
# Fake-apply a few from app1: unsquashes migration in app1.
|
||||
recorder.record_applied('app1', '1_auto')
|
||||
recorder.record_applied('app1', '2_auto')
|
||||
loader.build_graph()
|
||||
plan = set(loader.graph.forwards_plan(('app1', '4_auto')))
|
||||
plan = plan - loader.applied_migrations
|
||||
expected_plan = {
|
||||
('app2', '1_squashed_2'),
|
||||
('app1', '3_auto'),
|
||||
('app1', '4_auto'),
|
||||
}
|
||||
self.assertEqual(plan, expected_plan)
|
||||
|
||||
# Fake-apply one from app2: unsquashes migration in app2 too.
|
||||
recorder.record_applied('app2', '1_auto')
|
||||
loader.build_graph()
|
||||
plan = set(loader.graph.forwards_plan(('app1', '4_auto')))
|
||||
plan = plan - loader.applied_migrations
|
||||
expected_plan = {
|
||||
('app2', '2_auto'),
|
||||
('app1', '3_auto'),
|
||||
('app1', '4_auto'),
|
||||
}
|
||||
self.assertEqual(plan, expected_plan)
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
pass
|
|
@ -0,0 +1,9 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [("app1", "1_auto")]
|
|
@ -0,0 +1,14 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
replaces = [
|
||||
("app1", "2_auto"),
|
||||
("app1", "3_auto"),
|
||||
]
|
||||
|
||||
dependencies = [("app1", "1_auto"), ("app2", "1_squashed_2")]
|
|
@ -0,0 +1,9 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [("app1", "2_auto"), ("app2", "2_auto")]
|
|
@ -0,0 +1,9 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [("app1", "2_squashed_3")]
|
|
@ -0,0 +1,9 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [("app1", "1_auto")]
|
|
@ -0,0 +1,14 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
replaces = [
|
||||
("app2", "1_auto"),
|
||||
("app2", "2_auto"),
|
||||
]
|
||||
|
||||
dependencies = [("app1", "1_auto")]
|
|
@ -0,0 +1,9 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [("app2", "1_auto")]
|
Loading…
Reference in New Issue