def test_loading_squashed_complex_multi_apps(self): loader = MigrationLoader(connection) loader.build_graph() plan = set(loader.graph.forwards_plan(("app1", "4_auto"))) expected_plan = set( [("app1", "4_auto"), ("app1", "2_squashed_3"), ("app2", "1_squashed_2"), ("app1", "1_auto")] ) self.assertEqual(plan, expected_plan)
def test_loading_squashed(self): "Tests loading a squashed migration" migration_loader = MigrationLoader(connection) recorder = MigrationRecorder(connection) # Loading with nothing applied should just give us the one node self.assertEqual(len([x for x in migration_loader.graph.nodes if x[0] == "migrations"]), 1) # However, fake-apply one migration and it should now use the old two recorder.record_applied("migrations", "0001_initial") migration_loader.build_graph() self.assertEqual(len([x for x in migration_loader.graph.nodes if x[0] == "migrations"]), 2) recorder.flush()
def test_loading_squashed_complex_multi_apps(self): loader = MigrationLoader(connection) loader.build_graph() plan = set(loader.graph.forwards_plan(('app1', '4_auto'))) expected_plan = { ('app1', '1_auto'), ('app2', '1_squashed_2'), ('app1', '2_squashed_3'), ('app1', '4_auto'), } self.assertEqual(plan, expected_plan)
def test_loading_squashed_complex_multi_apps_partially_applied(self): loader = MigrationLoader(connection) recorder = MigrationRecorder(connection) 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)
def handle(self, *args, **options): ops = [] for model in apps.get_models(include_auto_created=True): if not hasattr(model, 'VersionModel') or model._meta.proxy: continue ops.extend(self.build_operations(model)) if options['initial']: m = Migration('0003_triggers', 'share') m.dependencies = [('share', '0002_create_share_user')] else: ml = MigrationLoader(connection=connection) ml.build_graph() last_share_migration = [x[1] for x in ml.graph.leaf_nodes() if x[0] == 'share'][0] next_number = '{0:04d}'.format(int(last_share_migration[0:4]) + 1) m = Migration('{}_update_trigger_migrations_{}'.format(next_number, datetime.datetime.now().strftime("%Y%m%d_%H%M")), 'share') m.dependencies = [('share', '0002_create_share_user'), ('share', last_share_migration)] m.operations = ops self.write_migration(m)
def test_loading_squashed_ref_squashed(self): "Tests loading a squashed migration with a new migration referencing it" r""" The sample migrations are structured 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)
def handle(self, *app_labels, **options): self.verbosity = int(options.get('verbosity')) self.interactive = options.get('interactive') self.dry_run = options.get('dry_run', False) self.merge = options.get('merge', False) # Make sure the app they asked for exists app_labels = set(app_labels) bad_app_labels = set() for app_label in app_labels: try: apps.get_app_config(app_label) except LookupError: bad_app_labels.add(app_label) if bad_app_labels: for app_label in bad_app_labels: self.stderr.write("App '%s' could not be found. Is it in INSTALLED_APPS?" % app_label) sys.exit(2) # Load the current graph state. Takes a connection, but it's not used # (makemigrations doesn't look at the database state). # Also make sure the graph is built without unmigrated apps shoehorned in. loader = MigrationLoader(connections[DEFAULT_DB_ALIAS]) loader.build_graph(ignore_unmigrated=True) # Before anything else, see if there's conflicting apps and drop out # hard if there are any and they don't want to merge conflicts = loader.detect_conflicts() if conflicts and not self.merge: name_str = "; ".join( "%s in %s" % (", ".join(names), app) for app, names in conflicts.items() ) raise CommandError("Conflicting migrations detected (%s).\nTo fix them run 'python manage.py makemigrations --merge'" % name_str) # If they want to merge and there's nothing to merge, then politely exit if self.merge and not conflicts: self.stdout.write("No conflicts detected to merge.") return # If they want to merge and there is something to merge, then # divert into the merge code if self.merge and conflicts: return self.handle_merge(loader, conflicts) # Detect changes autodetector = MigrationAutodetector( loader.graph.project_state(), ProjectState.from_apps(apps), InteractiveMigrationQuestioner(specified_apps=app_labels), ) changes = autodetector.changes(graph=loader.graph, trim_to_apps=app_labels or None) # No changes? Tell them. if not changes and self.verbosity >= 1: if len(app_labels) == 1: self.stdout.write("No changes detected in app '%s'" % app_labels.pop()) elif len(app_labels) > 1: self.stdout.write("No changes detected in apps '%s'" % ("', '".join(app_labels))) else: self.stdout.write("No changes detected") return directory_created = {} for app_label, app_migrations in changes.items(): if self.verbosity >= 1: self.stdout.write(self.style.MIGRATE_HEADING("Migrations for '%s':" % app_label) + "\n") for migration in app_migrations: # Describe the migration writer = MigrationWriter(migration) if self.verbosity >= 1: self.stdout.write(" %s:\n" % (self.style.MIGRATE_LABEL(writer.filename),)) for operation in migration.operations: self.stdout.write(" - %s\n" % operation.describe()) # Write it if not self.dry_run: migrations_directory = os.path.dirname(writer.path) if not directory_created.get(app_label, False): if not os.path.isdir(migrations_directory): os.mkdir(migrations_directory) init_path = os.path.join(migrations_directory, "__init__.py") if not os.path.isfile(init_path): open(init_path, "w").close() # We just do this once per app directory_created[app_label] = True migration_string = writer.as_string() with open(writer.path, "wb") as fh: fh.write(migration_string)
def test_loading_squashed_erroneous(self): "Tests loading a complex but erroneous set of squashed migrations" loader = MigrationLoader(connection) recorder = MigrationRecorder(connection) self.addCleanup(recorder.flush) def num_nodes(): plan = set(loader.graph.forwards_plan(('migrations', '7_auto'))) return len(plan - loader.applied_migrations) # Empty database: use squashed migration loader.build_graph() self.assertEqual(num_nodes(), 5) # Starting at 1 or 2 should use the squashed migration too recorder.record_applied("migrations", "1_auto") loader.build_graph() self.assertEqual(num_nodes(), 4) recorder.record_applied("migrations", "2_auto") loader.build_graph() self.assertEqual(num_nodes(), 3) # However, starting at 3 or 4, nonexistent migrations would be needed. msg = ("Migration migrations.6_auto depends on nonexistent node ('migrations', '5_auto'). " "Django tried to replace migration migrations.5_auto with any of " "[migrations.3_squashed_5] but wasn't able to because some of the replaced " "migrations are already applied.") recorder.record_applied("migrations", "3_auto") with self.assertRaisesMessage(NodeNotFoundError, msg): loader.build_graph() recorder.record_applied("migrations", "4_auto") with self.assertRaisesMessage(NodeNotFoundError, msg): loader.build_graph() # Starting at 5 to 7 we are passed the squashed migrations recorder.record_applied("migrations", "5_auto") loader.build_graph() self.assertEqual(num_nodes(), 2) recorder.record_applied("migrations", "6_auto") loader.build_graph() self.assertEqual(num_nodes(), 1) recorder.record_applied("migrations", "7_auto") loader.build_graph() self.assertEqual(num_nodes(), 0)
def test_loading_squashed_complex(self): "Tests loading a complex set of squashed migrations" loader = MigrationLoader(connection) recorder = MigrationRecorder(connection) self.addCleanup(recorder.flush) def num_nodes(): plan = set(loader.graph.forwards_plan(('migrations', '7_auto'))) return len(plan - loader.applied_migrations) # Empty database: use squashed migration loader.build_graph() self.assertEqual(num_nodes(), 5) # Starting at 1 or 2 should use the squashed migration too recorder.record_applied("migrations", "1_auto") loader.build_graph() self.assertEqual(num_nodes(), 4) recorder.record_applied("migrations", "2_auto") loader.build_graph() self.assertEqual(num_nodes(), 3) # However, starting at 3 to 5 cannot use the squashed migration recorder.record_applied("migrations", "3_auto") loader.build_graph() self.assertEqual(num_nodes(), 4) recorder.record_applied("migrations", "4_auto") loader.build_graph() self.assertEqual(num_nodes(), 3) # Starting at 5 to 7 we are passed the squashed migrations recorder.record_applied("migrations", "5_auto") loader.build_graph() self.assertEqual(num_nodes(), 2) recorder.record_applied("migrations", "6_auto") loader.build_graph() self.assertEqual(num_nodes(), 1) recorder.record_applied("migrations", "7_auto") loader.build_graph() self.assertEqual(num_nodes(), 0)
def handle(self, *app_labels, **options): self.verbosity = int(options.get('verbosity')) self.interactive = options.get('interactive') self.dry_run = options.get('dry_run', False) self.merge = options.get('merge', False) self.empty = options.get('empty', False) # Make sure the app they asked for exists app_labels = set(app_labels) bad_app_labels = set() for app_label in app_labels: try: apps.get_app_config(app_label) except LookupError: bad_app_labels.add(app_label) if bad_app_labels: for app_label in bad_app_labels: self.stderr.write("App '%s' could not be found. Is it in INSTALLED_APPS?" % app_label) sys.exit(2) # Load the current graph state. Takes a connection, but it's not used # (makemigrations doesn't look at the database state). # Also make sure the graph is built without unmigrated apps shoehorned in. loader = MigrationLoader(connections[DEFAULT_DB_ALIAS], load=False) loader.build_graph(ignore_unmigrated=True) # Before anything else, see if there's conflicting apps and drop out # hard if there are any and they don't want to merge conflicts = loader.detect_conflicts() if conflicts and not self.merge: name_str = "; ".join( "%s in %s" % (", ".join(names), app) for app, names in conflicts.items() ) raise CommandError("Conflicting migrations detected (%s).\nTo fix them run 'python manage.py makemigrations --merge'" % name_str) # If they want to merge and there's nothing to merge, then politely exit if self.merge and not conflicts: self.stdout.write("No conflicts detected to merge.") return # If they want to merge and there is something to merge, then # divert into the merge code if self.merge and conflicts: return self.handle_merge(loader, conflicts) # Set up autodetector autodetector = MigrationAutodetector( loader.graph.project_state(), ProjectState.from_apps(apps), InteractiveMigrationQuestioner(specified_apps=app_labels), ) # If they want to make an empty migration, make one for each app if self.empty: if not app_labels: raise CommandError("You must supply at least one app label when using --empty.") # Make a fake changes() result we can pass to arrange_for_graph changes = dict( (app, [Migration("custom", app)]) for app in app_labels ) changes = autodetector.arrange_for_graph(changes, loader.graph) self.write_migration_files(changes) return # Detect changes changes = autodetector.changes(graph=loader.graph, trim_to_apps=app_labels or None) # No changes? Tell them. if not changes and self.verbosity >= 1: if len(app_labels) == 1: self.stdout.write("No changes detected in app '%s'" % app_labels.pop()) elif len(app_labels) > 1: self.stdout.write("No changes detected in apps '%s'" % ("', '".join(app_labels))) else: self.stdout.write("No changes detected") return self.write_migration_files(changes)
def test_loading_squashed_ref_squashed(self): "Tests loading a squashed migration with a new migration referencing it" r""" The sample migrations are structured 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.keys() expected_plan = { ('app1', '1_auto'), ('app2', '1_squashed_2'), ('app1', '2_squashed_3'), ('app1', '4_auto'), } self.assertEqual(plan, expected_plan) # Load with nothing applied and migrate to a replaced migration. # Not possible if loader.replace_migrations is True (default). loader.build_graph() msg = "Node ('app1', '3_auto') not a valid node" with self.assertRaisesMessage(NodeNotFoundError, msg): loader.graph.forwards_plan(('app1', '3_auto')) # Possible if loader.replace_migrations is False. loader.replace_migrations = False loader.build_graph() plan = set(loader.graph.forwards_plan(('app1', '3_auto'))) plan = plan - loader.applied_migrations.keys() expected_plan = { ('app1', '1_auto'), ('app2', '1_auto'), ('app2', '2_auto'), ('app1', '2_auto'), ('app1', '3_auto'), } self.assertEqual(plan, expected_plan) loader.replace_migrations = True # Fake-apply a few from app1: unsquashes migration in app1. self.record_applied(recorder, 'app1', '1_auto') self.record_applied(recorder, 'app1', '2_auto') loader.build_graph() plan = set(loader.graph.forwards_plan(('app1', '4_auto'))) plan = plan - loader.applied_migrations.keys() 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. self.record_applied(recorder, 'app2', '1_auto') loader.build_graph() plan = set(loader.graph.forwards_plan(('app1', '4_auto'))) plan = plan - loader.applied_migrations.keys() expected_plan = { ('app2', '2_auto'), ('app1', '3_auto'), ('app1', '4_auto'), } self.assertEqual(plan, expected_plan)
def test_loading_squashed_erroneous(self): "Tests loading a complex but erroneous set of squashed migrations" loader = MigrationLoader(connection) recorder = MigrationRecorder(connection) self.addCleanup(recorder.flush) def num_nodes(): plan = set(loader.graph.forwards_plan(('migrations', '7_auto'))) return len(plan - loader.applied_migrations.keys()) # Empty database: use squashed migration loader.build_graph() self.assertEqual(num_nodes(), 5) # Starting at 1 or 2 should use the squashed migration too self.record_applied(recorder, 'migrations', '1_auto') loader.build_graph() self.assertEqual(num_nodes(), 4) self.record_applied(recorder, 'migrations', '2_auto') loader.build_graph() self.assertEqual(num_nodes(), 3) # However, starting at 3 or 4, nonexistent migrations would be needed. msg = ( "Migration migrations.6_auto depends on nonexistent node ('migrations', '5_auto'). " "Django tried to replace migration migrations.5_auto with any of " "[migrations.3_squashed_5] but wasn't able to because some of the replaced " "migrations are already applied.") self.record_applied(recorder, 'migrations', '3_auto') with self.assertRaisesMessage(NodeNotFoundError, msg): loader.build_graph() self.record_applied(recorder, 'migrations', '4_auto') with self.assertRaisesMessage(NodeNotFoundError, msg): loader.build_graph() # Starting at 5 to 7 we are passed the squashed migrations self.record_applied(recorder, 'migrations', '5_auto') loader.build_graph() self.assertEqual(num_nodes(), 2) self.record_applied(recorder, 'migrations', '6_auto') loader.build_graph() self.assertEqual(num_nodes(), 1) self.record_applied(recorder, 'migrations', '7_auto') loader.build_graph() self.assertEqual(num_nodes(), 0)
def test_loading_squashed_complex(self): "Tests loading a complex set of squashed migrations" loader = MigrationLoader(connection) recorder = MigrationRecorder(connection) self.addCleanup(recorder.flush) def num_nodes(): plan = set(loader.graph.forwards_plan(('migrations', '7_auto'))) return len(plan - loader.applied_migrations.keys()) # Empty database: use squashed migration loader.build_graph() self.assertEqual(num_nodes(), 5) # Starting at 1 or 2 should use the squashed migration too self.record_applied(recorder, 'migrations', '1_auto') loader.build_graph() self.assertEqual(num_nodes(), 4) self.record_applied(recorder, 'migrations', '2_auto') loader.build_graph() self.assertEqual(num_nodes(), 3) # However, starting at 3 to 5 cannot use the squashed migration self.record_applied(recorder, 'migrations', '3_auto') loader.build_graph() self.assertEqual(num_nodes(), 4) self.record_applied(recorder, 'migrations', '4_auto') loader.build_graph() self.assertEqual(num_nodes(), 3) # Starting at 5 to 7 we are past the squashed migrations. self.record_applied(recorder, 'migrations', '5_auto') loader.build_graph() self.assertEqual(num_nodes(), 2) self.record_applied(recorder, 'migrations', '6_auto') loader.build_graph() self.assertEqual(num_nodes(), 1) self.record_applied(recorder, 'migrations', '7_auto') loader.build_graph() self.assertEqual(num_nodes(), 0)
def test_loading_squashed_complex(self): "Tests loading a complex set of squashed migrations" loader = MigrationLoader(connection) recorder = MigrationRecorder(connection) def num_nodes(): plan = set(loader.graph.forwards_plan(('migrations', '7_auto'))) return len(plan - loader.applied_migrations) # Empty database: use squashed migration loader.build_graph() self.assertEqual(num_nodes(), 5) # Starting at 1 or 2 should use the squashed migration too recorder.record_applied("migrations", "1_auto") loader.build_graph() self.assertEqual(num_nodes(), 4) recorder.record_applied("migrations", "2_auto") loader.build_graph() self.assertEqual(num_nodes(), 3) # However, starting at 3 to 5 cannot use the squashed migration recorder.record_applied("migrations", "3_auto") loader.build_graph() self.assertEqual(num_nodes(), 4) recorder.record_applied("migrations", "4_auto") loader.build_graph() self.assertEqual(num_nodes(), 3) # Starting at 5 to 7 we are passed the squashed migrations recorder.record_applied("migrations", "5_auto") loader.build_graph() self.assertEqual(num_nodes(), 2) recorder.record_applied("migrations", "6_auto") loader.build_graph() self.assertEqual(num_nodes(), 1) recorder.record_applied("migrations", "7_auto") loader.build_graph() self.assertEqual(num_nodes(), 0) recorder.flush()
class Command(MakeMigrationsCommand): option_list = BaseCommand.option_list def handle(self, *app_labels, **options): self.verbosity = int(options.get('verbosity')) self.interactive = options.get('interactive') self.dry_run = False self.loader = MigrationLoader(None, ignore_no_migrations=True) for app_label in app_labels: self.handle_app(app_label) def handle_app(self, app_label): app = apps.get_app_config(app_label) self.convert(app) def convert(self, app): print('Processing %s' % app.label) if not self.has_south_migrations(app) and not self.has_initial_data_outside_of_migrations(app): print('App %s is already migrated to new style migrations' % app.label) return True if self.has_south_migrations(app): self.remove_migrations(app) self.create_new_migrations(app) if self.has_initial_data_outside_of_migrations(app): print('Found initial_data outside of migrations') self.create_data_migration_from_initial_data(app) def has_south_migrations(self, app): """ Apps with South migrations are in both sets""" return (app.label in self.loader.unmigrated_apps and app.label in self.loader.migrated_apps) def has_initial_data_outside_of_migrations(self, app): # Are there any initial_data fixtures if not self.get_initial_data_fixtures(app): return False # Check if initial data is already inside migration leaf_nodes = self.loader.graph.leaf_nodes(app.label) if not leaf_nodes: return True _, migration_name = leaf_nodes[0] migration_string = open(os.path.join(self.get_migrations_dir(app), migration_name + '.py')).read() if "call_command('loaddata'" in migration_string: return False return True def get_migrations_dir(self, app): module_name = self.loader.migrations_module(app.label) module = import_module(module_name) return os.path.dirname(module.__file__) def get_initial_data_fixtures(self, app): fixture_dir = os.path.join(app.path, 'fixtures') return list(glob.iglob(os.path.join(fixture_dir, 'initial_data.*'))) def remove_migrations(self, app): print(' Removing old South migrations') directory = self.get_migrations_dir(app) for name in os.listdir(directory): if (name.endswith('.py') or name.endswith('.pyc'))and name != '__init__.py': print(' Deleting %s %s' % (name, '(fake)' if self.dry_run else '')) if not self.dry_run: os.remove(os.path.join(directory, name)) def create_new_migrations(self, app): call_command('makemigrations', app.label, dry_run=self.dry_run, verbosity=self.verbosity) def create_data_migration_from_initial_data(self, app): # Create empty migration call_command('makemigrations', app.label, empty=True, dry_run=self.dry_run, verbosity=self.verbosity) # Get latest migration self.loader.build_graph() _, migration_name = self.loader.graph.leaf_nodes(app.label)[0] # Find the file directory = self.get_migrations_dir(app) empty_migration_file = os.path.join(directory, migration_name + '.py') # Inject code migration_string = open(empty_migration_file).read() callable_code = RUN_CODE % ( ', '.join(map(lambda fixture_name: '"%s"' % os.path.basename(fixture_name), self.get_initial_data_fixtures(app))), app.label ) migration_string = (migration_string.replace('class Migration', callable_code + 'class Migration') .replace('operations = [', 'operations = [' + OPERATIONS)) with open(empty_migration_file, "wb") as fh: fh.write(migration_string.encode('utf-8')) # wipe *.pyc try: os.remove(os.path.join(directory, os.path.join(directory, migration_name + '.pyc'))) except OSError: pass