def test_backwards_nothing_to_do(self): r""" If the current state satisfies the given target, do nothing. a: 1 <--- 2 b: \- 1 c: \- 1 If a1 is applied already and a2 is not, and we're asked to migrate to a1, don't apply or unapply b1 or c1, regardless of their current state. """ a1_impl = FakeMigration('a1') a1 = ('a', '1') a2_impl = FakeMigration('a2') a2 = ('a', '2') b1_impl = FakeMigration('b1') b1 = ('b', '1') c1_impl = FakeMigration('c1') c1 = ('c', '1') graph = MigrationGraph() graph.add_node(a1, a1_impl) graph.add_node(a2, a2_impl) graph.add_node(b1, b1_impl) graph.add_node(c1, c1_impl) graph.add_dependency(None, a2, a1) graph.add_dependency(None, b1, a1) graph.add_dependency(None, c1, a1) executor = MigrationExecutor(None) executor.loader = FakeLoader(graph, {a1, b1}) plan = executor.migration_plan({a1}) self.assertEqual(plan, [])
def test_minimize_rollbacks(self): """ Minimize unnecessary rollbacks in connected apps. When you say "./manage.py migrate appA 0001", rather than migrating to just after appA-0001 in the linearized migration plan (which could roll back migrations in other apps that depend on appA 0001, but don't need to be rolled back since we're not rolling back appA 0001), we migrate to just before appA-0002. """ a1_impl = FakeMigration('a1') a1 = ('a', '1') a2_impl = FakeMigration('a2') a2 = ('a', '2') b1_impl = FakeMigration('b1') b1 = ('b', '1') graph = MigrationGraph() graph.add_node(a1, a1_impl) graph.add_node(a2, a2_impl) graph.add_node(b1, b1_impl) graph.add_dependency(None, b1, a1) graph.add_dependency(None, a2, a1) executor = MigrationExecutor(None) executor.loader = FakeLoader(graph, {a1, b1, a2}) plan = executor.migration_plan({a1}) self.assertEqual(plan, [(a2_impl, True)])
def test_missing_parent_nodes(self): """ Tests for missing parent nodes. """ # Build graph graph = MigrationGraph() graph.add_node(("app_a", "0001"), None) graph.add_node(("app_a", "0002"), None) graph.add_node(("app_a", "0003"), None) graph.add_node(("app_b", "0001"), None) graph.add_dependency("app_a.0003", ("app_a", "0003"), ("app_a", "0002")) graph.add_dependency("app_a.0002", ("app_a", "0002"), ("app_a", "0001")) msg = "Migration app_a.0001 dependencies reference nonexistent parent node ('app_b', '0002')" with self.assertRaisesMessage(NodeNotFoundError, msg): graph.add_dependency("app_a.0001", ("app_a", "0001"), ("app_b", "0002"))
def test_graph_recursive(self): graph = MigrationGraph() root = ("app_a", "1") graph.add_node(root, None) expected = [root] for i in range(2, 750): parent = ("app_a", str(i - 1)) child = ("app_a", str(i)) graph.add_node(child, None) graph.add_dependency(str(i), child, parent) expected.append(child) leaf = expected[-1] forwards_plan = graph.forwards_plan(leaf) self.assertEqual(expected, forwards_plan) backwards_plan = graph.backwards_plan(root) self.assertEqual(expected[::-1], backwards_plan)
def test_graph_iterative(self): graph = MigrationGraph() root = ("app_a", "1") graph.add_node(root, None) expected = [root] for i in range(2, 1000): parent = ("app_a", str(i - 1)) child = ("app_a", str(i)) graph.add_node(child, None) graph.add_dependency(str(i), child, parent) expected.append(child) leaf = expected[-1] with self.assertWarnsMessage(RuntimeWarning, RECURSION_DEPTH_WARNING): forwards_plan = graph.forwards_plan(leaf) self.assertEqual(expected, forwards_plan) with self.assertWarnsMessage(RuntimeWarning, RECURSION_DEPTH_WARNING): backwards_plan = graph.backwards_plan(root) self.assertEqual(expected[::-1], backwards_plan)
def test_plan_invalid_node(self): """ Tests for forwards/backwards_plan of nonexistent node. """ graph = MigrationGraph() message = "Node ('app_b', '0001') not a valid node" with self.assertRaisesMessage(NodeNotFoundError, message): graph.forwards_plan(("app_b", "0001")) with self.assertRaisesMessage(NodeNotFoundError, message): graph.backwards_plan(("app_b", "0001"))
def test_complex_graph(self): r""" Tests a complex dependency graph: app_a: 0001 <-- 0002 <--- 0003 <-- 0004 \ \ / / app_b: 0001 <-\ 0002 <-X / \ \ / app_c: \ 0001 <-- 0002 <- """ # Build graph graph = MigrationGraph() graph.add_node(("app_a", "0001"), None) graph.add_node(("app_a", "0002"), None) graph.add_node(("app_a", "0003"), None) graph.add_node(("app_a", "0004"), None) graph.add_node(("app_b", "0001"), None) graph.add_node(("app_b", "0002"), None) graph.add_node(("app_c", "0001"), None) graph.add_node(("app_c", "0002"), None) graph.add_dependency("app_a.0004", ("app_a", "0004"), ("app_a", "0003")) graph.add_dependency("app_a.0003", ("app_a", "0003"), ("app_a", "0002")) graph.add_dependency("app_a.0002", ("app_a", "0002"), ("app_a", "0001")) graph.add_dependency("app_a.0003", ("app_a", "0003"), ("app_b", "0002")) graph.add_dependency("app_b.0002", ("app_b", "0002"), ("app_b", "0001")) graph.add_dependency("app_a.0004", ("app_a", "0004"), ("app_c", "0002")) graph.add_dependency("app_c.0002", ("app_c", "0002"), ("app_c", "0001")) graph.add_dependency("app_c.0001", ("app_c", "0001"), ("app_b", "0001")) graph.add_dependency("app_c.0002", ("app_c", "0002"), ("app_a", "0002")) # Test branch C only self.assertEqual( graph.forwards_plan(("app_c", "0002")), [('app_b', '0001'), ('app_c', '0001'), ('app_a', '0001'), ('app_a', '0002'), ('app_c', '0002')], ) # Test whole graph self.assertEqual( graph.forwards_plan(("app_a", "0004")), [ ('app_b', '0001'), ('app_c', '0001'), ('app_a', '0001'), ('app_a', '0002'), ('app_c', '0002'), ('app_b', '0002'), ('app_a', '0003'), ('app_a', '0004'), ], ) # Test reverse to b:0001 self.assertEqual( graph.backwards_plan(("app_b", "0001")), [ ('app_a', '0004'), ('app_c', '0002'), ('app_c', '0001'), ('app_a', '0003'), ('app_b', '0002'), ('app_b', '0001'), ], ) # Test roots and leaves self.assertEqual( graph.root_nodes(), [('app_a', '0001'), ('app_b', '0001'), ('app_c', '0001')], ) self.assertEqual( graph.leaf_nodes(), [('app_a', '0004'), ('app_b', '0002'), ('app_c', '0002')], )
def test_stringify(self): graph = MigrationGraph() self.assertEqual(str(graph), "Graph: 0 nodes, 0 edges") graph.add_node(("app_a", "0001"), None) graph.add_node(("app_a", "0002"), None) graph.add_node(("app_a", "0003"), None) graph.add_node(("app_b", "0001"), None) graph.add_node(("app_b", "0002"), None) graph.add_dependency("app_a.0002", ("app_a", "0002"), ("app_a", "0001")) graph.add_dependency("app_a.0003", ("app_a", "0003"), ("app_a", "0002")) graph.add_dependency("app_a.0003", ("app_a", "0003"), ("app_b", "0002")) self.assertEqual(str(graph), "Graph: 5 nodes, 3 edges") self.assertEqual(repr(graph), "<MigrationGraph: nodes=5, edges=3>")
def test_infinite_loop(self): """ Tests a complex dependency graph: app_a: 0001 <- \ app_b: 0001 <- x 0002 <- / \ app_c: 0001<- <------------- x 0002 And apply squashing on app_c. """ graph = MigrationGraph() graph.add_node(("app_a", "0001"), None) graph.add_node(("app_b", "0001"), None) graph.add_node(("app_b", "0002"), None) graph.add_node(("app_c", "0001_squashed_0002"), None) graph.add_dependency("app_b.0001", ("app_b", "0001"), ("app_c", "0001_squashed_0002")) graph.add_dependency("app_b.0002", ("app_b", "0002"), ("app_a", "0001")) graph.add_dependency("app_b.0002", ("app_b", "0002"), ("app_b", "0001")) graph.add_dependency("app_c.0001_squashed_0002", ("app_c", "0001_squashed_0002"), ("app_b", "0002")) with self.assertRaises(CircularDependencyError): graph.forwards_plan(("app_c", "0001_squashed_0002"))
def test_remove_replacement_node(self): """ 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_remove_replaced_nodes(self): """ 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_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()
class MigrationLoader: """ Load migration files from disk and their status from the database. Migration files are expected to live in the "migrations" directory of an app. Their names are entirely unimportant from a code perspective, but will probably follow the 1234_name.py convention. On initialization, this class will scan those directories, and open and read the python files, looking for a class called Migration, which should inherit from djmodels.db.migrations.Migration. See djmodels.db.migrations.migration for what that looks like. Some migrations will be marked as "replacing" another set of migrations. These are loaded into a separate set of migrations away from the main ones. If all the migrations they replace are either unapplied or missing from disk, then they are injected into the main set, replacing the named migrations. Any dependency pointers to the replaced migrations are re-pointed to the new migration. This does mean that this class MUST also talk to the database as well as to disk, but this is probably fine. We're already not just operating in memory. """ def __init__(self, connection, load=True, ignore_no_migrations=False): self.connection = connection self.disk_migrations = None self.applied_migrations = None self.ignore_no_migrations = ignore_no_migrations if load: self.build_graph() @classmethod def migrations_module(cls, app_label): """ Return the path to the migrations module for the specified app_label and a boolean indicating if the module is specified in settings.MIGRATION_MODULE. """ if app_label in settings.MIGRATION_MODULES: return settings.MIGRATION_MODULES[app_label], True else: app_package_name = apps.get_app_config(app_label).name return '%s.%s' % (app_package_name, MIGRATIONS_MODULE_NAME), False def load_disk(self): """Load the migrations from all INSTALLED_APPS from disk.""" self.disk_migrations = {} self.unmigrated_apps = set() self.migrated_apps = set() for app_config in apps.get_app_configs(): # Get the migrations module directory module_name, explicit = self.migrations_module(app_config.label) if module_name is None: self.unmigrated_apps.add(app_config.label) continue was_loaded = module_name in sys.modules try: module = import_module(module_name) except ImportError as e: # I hate doing this, but I don't want to squash other import errors. # Might be better to try a directory check directly. if ((explicit and self.ignore_no_migrations) or (not explicit and "No module named" in str(e) and MIGRATIONS_MODULE_NAME in str(e))): self.unmigrated_apps.add(app_config.label) continue raise else: # Empty directories are namespaces. # getattr() needed on PY36 and older (replace w/attribute access). if getattr(module, '__file__', None) is None: self.unmigrated_apps.add(app_config.label) continue # Module is not a package (e.g. migrations.py). if not hasattr(module, '__path__'): self.unmigrated_apps.add(app_config.label) continue # Force a reload if it's already loaded (tests need this) if was_loaded: reload(module) self.migrated_apps.add(app_config.label) migration_names = { name for _, name, is_pkg in pkgutil.iter_modules(module.__path__) if not is_pkg } # Load migrations for migration_name in migration_names: migration_path = '%s.%s' % (module_name, migration_name) try: migration_module = import_module(migration_path) except ImportError as e: if 'bad magic number' in str(e): raise ImportError( "Couldn't import %r as it appears to be a stale " ".pyc file." % migration_path) from e else: raise if not hasattr(migration_module, "Migration"): raise BadMigrationError( "Migration %s in app %s has no Migration class" % (migration_name, app_config.label)) self.disk_migrations[ app_config.label, migration_name] = migration_module.Migration( migration_name, app_config.label, ) def get_migration(self, app_label, name_prefix): """Return the named migration or raise NodeNotFoundError.""" return self.graph.nodes[app_label, name_prefix] def get_migration_by_prefix(self, app_label, name_prefix): """ Return the migration(s) which match the given app label and name_prefix. """ # Do the search results = [] for migration_app_label, migration_name in self.disk_migrations: if migration_app_label == app_label and migration_name.startswith( name_prefix): results.append((migration_app_label, migration_name)) if len(results) > 1: raise AmbiguityError( "There is more than one migration for '%s' with the prefix '%s'" % (app_label, name_prefix)) elif not results: raise KeyError( "There no migrations for '%s' with the prefix '%s'" % (app_label, name_prefix)) else: return self.disk_migrations[results[0]] def check_key(self, key, current_app): if (key[1] != "__first__" and key[1] != "__latest__") or key in self.graph: return key # Special-case __first__, which means "the first migration" for # migrated apps, and is ignored for unmigrated apps. It allows # makemigrations to declare dependencies on apps before they even have # migrations. if key[0] == current_app: # Ignore __first__ references to the same app (#22325) return if key[0] in self.unmigrated_apps: # This app isn't migrated, but something depends on it. # The models will get auto-added into the state, though # so we're fine. return if key[0] in self.migrated_apps: try: if key[1] == "__first__": return self.graph.root_nodes(key[0])[0] else: # "__latest__" return self.graph.leaf_nodes(key[0])[0] except IndexError: if self.ignore_no_migrations: return None else: 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: # Ignore __first__ references to the same app. if parent[0] == key[0] and parent[1] != '__first__': 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): """ Build a migration dependency graph using both the disk and database. You'll need to rebuild the graph if you apply migrations. This isn't usually a problem as generally migration stuff runs in a one-shot process. """ # Load disk data self.load_disk() # Load database data if self.connection is None: self.applied_migrations = set() else: recorder = MigrationRecorder(self.connection) self.applied_migrations = recorder.applied_migrations() # 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: 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: 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) raise NodeNotFoundError( "Migration {0} depends on nonexistent node ('{1}', '{2}'). " "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(exc.origin, exc.node[0], exc.node[1], tries), exc.node) from exc raise exc def check_consistent_history(self, connection): """ Raise InconsistentMigrationHistory if any applied migrations have unapplied dependencies. """ recorder = MigrationRecorder(connection) applied = recorder.applied_migrations() for migration in applied: # If the migration is unknown, skip it. if migration not in self.graph.nodes: continue for parent in self.graph.node_map[migration].parents: if parent not in applied: # Skip unapplied squashed migrations that have all of their # `replaces` applied. if parent in self.replacements: if all(m in applied for m in self.replacements[parent].replaces): continue raise InconsistentMigrationHistory( "Migration {}.{} is applied before its dependency " "{}.{} on database '{}'.".format( migration[0], migration[1], parent[0], parent[1], connection.alias, )) def detect_conflicts(self): """ Look through the loaded graph and detect any conflicts - apps with more than one leaf migration. Return a dict of the app labels that conflict with the migration names that conflict. """ seen_apps = {} conflicting_apps = set() for app_label, migration_name in self.graph.leaf_nodes(): if app_label in seen_apps: conflicting_apps.add(app_label) seen_apps.setdefault(app_label, set()).add(migration_name) return { app_label: seen_apps[app_label] for app_label in conflicting_apps } def project_state(self, nodes=None, at_end=True): """ Return a ProjectState object representing the most recent state that the loaded migrations represent. See graph.make_state() for the meaning of "nodes" and "at_end". """ return self.graph.make_state(nodes=nodes, at_end=at_end, real_apps=list(self.unmigrated_apps))
def test_minimize_rollbacks_branchy(self): r""" Minimize rollbacks when target has multiple in-app children. a: 1 <---- 3 <--\ \ \- 2 <--- 4 \ \ b: \- 1 <--- 2 """ a1_impl = FakeMigration('a1') a1 = ('a', '1') a2_impl = FakeMigration('a2') a2 = ('a', '2') a3_impl = FakeMigration('a3') a3 = ('a', '3') a4_impl = FakeMigration('a4') a4 = ('a', '4') b1_impl = FakeMigration('b1') b1 = ('b', '1') b2_impl = FakeMigration('b2') b2 = ('b', '2') graph = MigrationGraph() graph.add_node(a1, a1_impl) graph.add_node(a2, a2_impl) graph.add_node(a3, a3_impl) graph.add_node(a4, a4_impl) graph.add_node(b1, b1_impl) graph.add_node(b2, b2_impl) graph.add_dependency(None, a2, a1) graph.add_dependency(None, a3, a1) graph.add_dependency(None, a4, a2) graph.add_dependency(None, a4, a3) graph.add_dependency(None, b2, b1) graph.add_dependency(None, b1, a1) graph.add_dependency(None, b2, a2) executor = MigrationExecutor(None) executor.loader = FakeLoader(graph, {a1, b1, a2, b2, a3, a4}) plan = executor.migration_plan({a1}) should_be_rolled_back = [b2_impl, a4_impl, a2_impl, a3_impl] exp = [(m, True) for m in should_be_rolled_back] self.assertEqual(plan, exp)
def build_graph(self): """ Build a migration dependency graph using both the disk and database. You'll need to rebuild the graph if you apply migrations. This isn't usually a problem as generally migration stuff runs in a one-shot process. """ # Load disk data self.load_disk() # Load database data if self.connection is None: self.applied_migrations = set() else: recorder = MigrationRecorder(self.connection) self.applied_migrations = recorder.applied_migrations() # 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: 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: 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) raise NodeNotFoundError( "Migration {0} depends on nonexistent node ('{1}', '{2}'). " "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(exc.origin, exc.node[0], exc.node[1], tries), exc.node) from exc raise exc
def test_circular_graph_2(self): graph = MigrationGraph() graph.add_node(('A', '0001'), None) graph.add_node(('C', '0001'), None) graph.add_node(('B', '0001'), None) graph.add_dependency('A.0001', ('A', '0001'), ('B', '0001')) graph.add_dependency('B.0001', ('B', '0001'), ('A', '0001')) graph.add_dependency('C.0001', ('C', '0001'), ('B', '0001')) with self.assertRaises(CircularDependencyError): graph.forwards_plan(('C', '0001'))
def test_simple_graph(self): """ Tests a basic dependency graph: app_a: 0001 <-- 0002 <--- 0003 <-- 0004 / app_b: 0001 <-- 0002 <-/ """ # Build graph graph = MigrationGraph() graph.add_node(("app_a", "0001"), None) graph.add_node(("app_a", "0002"), None) graph.add_node(("app_a", "0003"), None) graph.add_node(("app_a", "0004"), None) graph.add_node(("app_b", "0001"), None) graph.add_node(("app_b", "0002"), None) graph.add_dependency("app_a.0004", ("app_a", "0004"), ("app_a", "0003")) graph.add_dependency("app_a.0003", ("app_a", "0003"), ("app_a", "0002")) graph.add_dependency("app_a.0002", ("app_a", "0002"), ("app_a", "0001")) graph.add_dependency("app_a.0003", ("app_a", "0003"), ("app_b", "0002")) graph.add_dependency("app_b.0002", ("app_b", "0002"), ("app_b", "0001")) # Test root migration case self.assertEqual( graph.forwards_plan(("app_a", "0001")), [('app_a', '0001')], ) # Test branch B only self.assertEqual( graph.forwards_plan(("app_b", "0002")), [("app_b", "0001"), ("app_b", "0002")], ) # Test whole graph self.assertEqual( graph.forwards_plan(("app_a", "0004")), [ ('app_b', '0001'), ('app_b', '0002'), ('app_a', '0001'), ('app_a', '0002'), ('app_a', '0003'), ('app_a', '0004'), ], ) # Test reverse to b:0002 self.assertEqual( graph.backwards_plan(("app_b", "0002")), [('app_a', '0004'), ('app_a', '0003'), ('app_b', '0002')], ) # Test roots and leaves self.assertEqual( graph.root_nodes(), [('app_a', '0001'), ('app_b', '0001')], ) self.assertEqual( graph.leaf_nodes(), [('app_a', '0004'), ('app_b', '0002')], )
def test_circular_graph(self): """ Tests a circular dependency graph. """ # Build graph graph = MigrationGraph() graph.add_node(("app_a", "0001"), None) graph.add_node(("app_a", "0002"), None) graph.add_node(("app_a", "0003"), None) graph.add_node(("app_b", "0001"), None) graph.add_node(("app_b", "0002"), None) graph.add_dependency("app_a.0003", ("app_a", "0003"), ("app_a", "0002")) graph.add_dependency("app_a.0002", ("app_a", "0002"), ("app_a", "0001")) graph.add_dependency("app_a.0001", ("app_a", "0001"), ("app_b", "0002")) graph.add_dependency("app_b.0002", ("app_b", "0002"), ("app_b", "0001")) graph.add_dependency("app_b.0001", ("app_b", "0001"), ("app_a", "0003")) # Test whole graph with self.assertRaises(CircularDependencyError): graph.forwards_plan(("app_a", "0003"))