parent
6b5926978b
commit
509379a161
1
AUTHORS
1
AUTHORS
|
@ -323,6 +323,7 @@ answer newbie questions, and generally made Django that much better:
|
||||||
Janos Guljas
|
Janos Guljas
|
||||||
Jan Pazdziora
|
Jan Pazdziora
|
||||||
Jan Rademaker
|
Jan Rademaker
|
||||||
|
Jarek Głowacki <jarekwg@gmail.com>
|
||||||
Jarek Zgoda <jarek.zgoda@gmail.com>
|
Jarek Zgoda <jarek.zgoda@gmail.com>
|
||||||
Jason Davies (Esaj) <http://www.jasondavies.com/>
|
Jason Davies (Esaj) <http://www.jasondavies.com/>
|
||||||
Jason Huggins <http://www.jrandolph.com/blog/>
|
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.
|
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.message = message
|
||||||
|
self.origin = origin
|
||||||
self.node = node
|
self.node = node
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import sys
|
||||||
import warnings
|
import warnings
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from functools import total_ordering
|
from functools import total_ordering
|
||||||
|
|
||||||
from django.db.migrations.state import ProjectState
|
from django.db.migrations.state import ProjectState
|
||||||
|
from django.utils import six
|
||||||
from django.utils.datastructures import OrderedSet
|
from django.utils.datastructures import OrderedSet
|
||||||
from django.utils.encoding import python_2_unicode_compatible
|
from django.utils.encoding import python_2_unicode_compatible
|
||||||
|
|
||||||
|
@ -79,6 +81,29 @@ class Node(object):
|
||||||
return self.__dict__['_descendants']
|
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
|
@python_2_unicode_compatible
|
||||||
class MigrationGraph(object):
|
class MigrationGraph(object):
|
||||||
"""
|
"""
|
||||||
|
@ -108,27 +133,133 @@ class MigrationGraph(object):
|
||||||
self.nodes = {}
|
self.nodes = {}
|
||||||
self.cached = False
|
self.cached = False
|
||||||
|
|
||||||
def add_node(self, key, implementation):
|
def add_node(self, key, migration):
|
||||||
node = Node(key)
|
# If the key already exists, then it must be a dummy node.
|
||||||
self.node_map[key] = node
|
dummy_node = self.node_map.get(key)
|
||||||
self.nodes[key] = implementation
|
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()
|
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:
|
if child not in self.nodes:
|
||||||
raise NodeNotFoundError(
|
error_message = (
|
||||||
"Migration %s dependencies reference nonexistent child node %r" % (migration, child),
|
"Migration %s dependencies reference nonexistent"
|
||||||
child
|
" child node %r" % (migration, child)
|
||||||
)
|
)
|
||||||
|
self.add_dummy_node(child, migration, error_message)
|
||||||
if parent not in self.nodes:
|
if parent not in self.nodes:
|
||||||
raise NodeNotFoundError(
|
error_message = (
|
||||||
"Migration %s dependencies reference nonexistent parent node %r" % (migration, parent),
|
"Migration %s dependencies reference nonexistent"
|
||||||
parent
|
" 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[child].add_parent(self.node_map[parent])
|
||||||
self.node_map[parent].add_child(self.node_map[child])
|
self.node_map[parent].add_child(self.node_map[child])
|
||||||
|
if not skip_validation:
|
||||||
|
self.validate_consistency()
|
||||||
self.clear_cache()
|
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):
|
def clear_cache(self):
|
||||||
if self.cached:
|
if self.cached:
|
||||||
for node in self.nodes:
|
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 app with no migrations: %s" % key[0])
|
||||||
raise ValueError("Dependency on unknown app: %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):
|
def build_graph(self):
|
||||||
"""
|
"""
|
||||||
Builds a migration dependency graph using both the disk and database.
|
Builds a migration dependency graph using both the disk and database.
|
||||||
|
@ -179,92 +203,54 @@ class MigrationLoader(object):
|
||||||
else:
|
else:
|
||||||
recorder = MigrationRecorder(self.connection)
|
recorder = MigrationRecorder(self.connection)
|
||||||
self.applied_migrations = recorder.applied_migrations()
|
self.applied_migrations = recorder.applied_migrations()
|
||||||
# Do a first pass to separate out replacing and non-replacing migrations
|
# To start, populate the migration graph with nodes for ALL migrations
|
||||||
normal = {}
|
# and their dependencies. Also make note of replacing migrations at this step.
|
||||||
replacing = {}
|
self.graph = MigrationGraph()
|
||||||
|
self.replacements = {}
|
||||||
for key, migration in self.disk_migrations.items():
|
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:
|
if migration.replaces:
|
||||||
replacing[key] = migration
|
self.replacements[key] = migration
|
||||||
else:
|
# Add external dependencies now that the internal ones have been resolved.
|
||||||
normal[key] = migration
|
for key, migration in self.disk_migrations.items():
|
||||||
# Calculate reverse dependencies - i.e., for each migration, what depends on it?
|
self.add_external_dependencies(key, migration)
|
||||||
# This is just for dependency re-pointing when applying replacements,
|
# Carry out replacements where possible.
|
||||||
# so we ignore run_before here.
|
for key, migration in self.replacements.items():
|
||||||
reverse_dependencies = {}
|
# Get applied status of each of this migration's replacement targets.
|
||||||
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.
|
|
||||||
applied_statuses = [(target in self.applied_migrations) for target in migration.replaces]
|
applied_statuses = [(target in self.applied_migrations) for target in migration.replaces]
|
||||||
can_replace = all(applied_statuses) or (not any(applied_statuses))
|
# Ensure the replacing migration is only marked as applied if all of
|
||||||
if not can_replace:
|
# its replacement targets are.
|
||||||
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):
|
if all(applied_statuses):
|
||||||
self.applied_migrations.add(key)
|
self.applied_migrations.add(key)
|
||||||
# Store the replacement migrations for later checks
|
else:
|
||||||
self.replacements = replacing
|
self.applied_migrations.discard(key)
|
||||||
# Finally, make a graph and load everything into it
|
# A replacing migration can be used if either all or none of its
|
||||||
self.graph = MigrationGraph()
|
# replacement targets have been applied.
|
||||||
for key, migration in normal.items():
|
if all(applied_statuses) or (not any(applied_statuses)):
|
||||||
self.graph.add_node(key, migration)
|
self.graph.remove_replaced_nodes(key, migration.replaces)
|
||||||
|
else:
|
||||||
def _reraise_missing_dependency(migration, missing, exc):
|
# This replacing migration cannot be used because it is partially applied.
|
||||||
"""
|
# Remove it from the graph and remap dependencies to it (#25945).
|
||||||
Checks if ``missing`` could have been replaced by any squash
|
self.graph.remove_replacement_node(key, migration.replaces)
|
||||||
migration but wasn't because the the squash migration was partially
|
# Ensure the graph is consistent.
|
||||||
applied before. In that case raise a more understandable exception.
|
try:
|
||||||
|
self.graph.validate_consistency()
|
||||||
#23556
|
except NodeNotFoundError as exc:
|
||||||
"""
|
# Check if the missing node could have been replaced by any squash
|
||||||
if missing in reverse_replacements:
|
# migration but wasn't because the squash migration was partially
|
||||||
candidates = reverse_replacements.get(missing, set())
|
# 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)
|
is_replaced = any(candidate in self.graph.nodes for candidate in candidates)
|
||||||
if not is_replaced:
|
if not is_replaced:
|
||||||
tries = ', '.join('%s.%s' % c for c in candidates)
|
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}] "
|
"Django tried to replace migration {1}.{2} with any of [{3}] "
|
||||||
"but wasn't able to because some of the replaced migrations "
|
"but wasn't able to because some of the replaced migrations "
|
||||||
"are already applied.".format(
|
"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
|
exc_value.__cause__ = exc
|
||||||
if not hasattr(exc, '__traceback__'):
|
if not hasattr(exc, '__traceback__'):
|
||||||
exc.__traceback__ = sys.exc_info()[2]
|
exc.__traceback__ = sys.exc_info()[2]
|
||||||
six.reraise(NodeNotFoundError, exc_value, sys.exc_info()[2])
|
six.reraise(NodeNotFoundError, exc_value, sys.exc_info()[2])
|
||||||
raise exc
|
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):
|
def check_consistent_history(self, connection):
|
||||||
"""
|
"""
|
||||||
Raise InconsistentMigrationHistory if any applied migrations have
|
Raise InconsistentMigrationHistory if any applied migrations have
|
||||||
|
|
|
@ -250,6 +250,122 @@ class GraphTests(SimpleTestCase):
|
||||||
with self.assertRaisesMessage(NodeNotFoundError, msg):
|
with self.assertRaisesMessage(NodeNotFoundError, msg):
|
||||||
graph.add_dependency("app_a.0002", ("app_a", "0002"), ("app_a", "0001"))
|
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):
|
def test_infinite_loop(self):
|
||||||
"""
|
"""
|
||||||
Tests a complex dependency graph:
|
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"
|
msg = "Migration migrations.0002_second is applied before its dependency migrations.0001_initial"
|
||||||
with self.assertRaisesMessage(InconsistentMigrationHistory, msg):
|
with self.assertRaisesMessage(InconsistentMigrationHistory, msg):
|
||||||
loader.check_consistent_history(connection)
|
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