Loading django/db/migrations/autodetector.py +13 −3 Original line number Diff line number Diff line Loading @@ -237,9 +237,17 @@ class MigrationAutodetector(object): deps_satisfied = True operation_dependencies = set() for dep in operation._auto_deps: is_swappable_dep = False if dep[0] == "__setting__": operation_dependencies.add((dep[0], dep[1])) elif dep[0] != app_label: # We need to temporarily resolve the swappable dependency to prevent # circular references. While keeping the dependency checks on the # resolved model we still add the swappable dependencies. # See #23322 resolved_app_label, resolved_object_name = getattr(settings, dep[1]).split('.') original_dep = dep dep = (resolved_app_label, resolved_object_name.lower(), dep[2], dep[3]) is_swappable_dep = True if dep[0] != app_label and dep[0] != "__setting__": # External app dependency. See if it's not yet # satisfied. for other_operation in self.generated_operations.get(dep[0], []): Loading @@ -249,7 +257,9 @@ class MigrationAutodetector(object): if not deps_satisfied: break else: if self.migrations.get(dep[0], None): if is_swappable_dep: operation_dependencies.add((original_dep[0], original_dep[1])) elif dep[0] in self.migrations: operation_dependencies.add((dep[0], self.migrations[dep[0]][-1].name)) else: # If we can't find the other app, we add a first/last dependency, Loading tests/migrations/test_autodetector.py +82 −0 Original line number Diff line number Diff line # -*- coding: utf-8 -*- from django.conf import settings from django.test import TestCase, override_settings from django.db.migrations.autodetector import MigrationAutodetector from django.db.migrations.questioner import MigrationQuestioner Loading Loading @@ -1103,3 +1104,84 @@ class AutodetectorTests(TestCase): self.assertOperationTypes(changes, 'a', 0, ["CreateModel", "CreateModel"]) self.assertOperationTypes(changes, 'a', 1, ["AddField"]) self.assertOperationTypes(changes, 'b', 0, ["CreateModel", "CreateModel"]) @override_settings(AUTH_USER_MODEL="a.Tenant") def test_circular_dependency_swappable(self): """ Tests that the dependency resolver knows to explicitly resolve swappable models (#23322) """ tenant = ModelState("a", "Tenant", [ ("id", models.AutoField(primary_key=True)), ("primary_address", models.ForeignKey("b.Address"))], bases=(AbstractBaseUser, ) ) address = ModelState("b", "Address", [ ("id", models.AutoField(primary_key=True)), ("tenant", models.ForeignKey(settings.AUTH_USER_MODEL)), ]) # Make state before = self.make_project_state([]) after = self.make_project_state([address, tenant]) autodetector = MigrationAutodetector(before, after) changes = autodetector._detect_changes() # Right number of migrations? self.assertNumberMigrations(changes, 'a', 2) self.assertNumberMigrations(changes, 'b', 1) self.assertOperationTypes(changes, 'a', 0, ["CreateModel"]) self.assertOperationTypes(changes, 'a', 1, ["AddField"]) self.assertOperationTypes(changes, 'b', 0, ["CreateModel"]) self.assertEqual(changes['a'][0].dependencies, []) self.assertEqual(set(changes['a'][1].dependencies), set([('a', 'auto_1'), ('b', 'auto_1')])) self.assertEqual(changes['b'][0].dependencies, [('__setting__', 'AUTH_USER_MODEL')]) @override_settings(AUTH_USER_MODEL="b.Tenant") def test_circular_dependency_swappable2(self): """ Tests that the dependency resolver knows to explicitly resolve swappable models but with the swappable not being the first migrated model (#23322) """ address = ModelState("a", "Address", [ ("id", models.AutoField(primary_key=True)), ("tenant", models.ForeignKey(settings.AUTH_USER_MODEL)), ]) tenant = ModelState("b", "Tenant", [ ("id", models.AutoField(primary_key=True)), ("primary_address", models.ForeignKey("a.Address"))], bases=(AbstractBaseUser, ) ) # Make state before = self.make_project_state([]) after = self.make_project_state([address, tenant]) autodetector = MigrationAutodetector(before, after) changes = autodetector._detect_changes() # Right number of migrations? self.assertNumberMigrations(changes, 'a', 2) self.assertNumberMigrations(changes, 'b', 1) self.assertOperationTypes(changes, 'a', 0, ["CreateModel"]) self.assertOperationTypes(changes, 'a', 1, ["AddField"]) self.assertOperationTypes(changes, 'b', 0, ["CreateModel"]) self.assertEqual(changes['a'][0].dependencies, []) self.assertEqual(set(changes['a'][1].dependencies), set([('__setting__', 'AUTH_USER_MODEL'), ('a', 'auto_1')])) self.assertEqual(changes['b'][0].dependencies, [('a', 'auto_1')]) @override_settings(AUTH_USER_MODEL="a.Person") def test_circular_dependency_swappable_self(self): """ Tests that the dependency resolver knows to explicitly resolve swappable models (#23322) """ person = ModelState("a", "Person", [ ("id", models.AutoField(primary_key=True)), ("parent1", models.ForeignKey(settings.AUTH_USER_MODEL, related_name='children')) ]) # Make state before = self.make_project_state([]) after = self.make_project_state([person]) autodetector = MigrationAutodetector(before, after) changes = autodetector._detect_changes() # Right number of migrations? self.assertNumberMigrations(changes, 'a', 1) self.assertOperationTypes(changes, 'a', 0, ["CreateModel"]) self.assertEqual(changes['a'][0].dependencies, []) Loading
django/db/migrations/autodetector.py +13 −3 Original line number Diff line number Diff line Loading @@ -237,9 +237,17 @@ class MigrationAutodetector(object): deps_satisfied = True operation_dependencies = set() for dep in operation._auto_deps: is_swappable_dep = False if dep[0] == "__setting__": operation_dependencies.add((dep[0], dep[1])) elif dep[0] != app_label: # We need to temporarily resolve the swappable dependency to prevent # circular references. While keeping the dependency checks on the # resolved model we still add the swappable dependencies. # See #23322 resolved_app_label, resolved_object_name = getattr(settings, dep[1]).split('.') original_dep = dep dep = (resolved_app_label, resolved_object_name.lower(), dep[2], dep[3]) is_swappable_dep = True if dep[0] != app_label and dep[0] != "__setting__": # External app dependency. See if it's not yet # satisfied. for other_operation in self.generated_operations.get(dep[0], []): Loading @@ -249,7 +257,9 @@ class MigrationAutodetector(object): if not deps_satisfied: break else: if self.migrations.get(dep[0], None): if is_swappable_dep: operation_dependencies.add((original_dep[0], original_dep[1])) elif dep[0] in self.migrations: operation_dependencies.add((dep[0], self.migrations[dep[0]][-1].name)) else: # If we can't find the other app, we add a first/last dependency, Loading
tests/migrations/test_autodetector.py +82 −0 Original line number Diff line number Diff line # -*- coding: utf-8 -*- from django.conf import settings from django.test import TestCase, override_settings from django.db.migrations.autodetector import MigrationAutodetector from django.db.migrations.questioner import MigrationQuestioner Loading Loading @@ -1103,3 +1104,84 @@ class AutodetectorTests(TestCase): self.assertOperationTypes(changes, 'a', 0, ["CreateModel", "CreateModel"]) self.assertOperationTypes(changes, 'a', 1, ["AddField"]) self.assertOperationTypes(changes, 'b', 0, ["CreateModel", "CreateModel"]) @override_settings(AUTH_USER_MODEL="a.Tenant") def test_circular_dependency_swappable(self): """ Tests that the dependency resolver knows to explicitly resolve swappable models (#23322) """ tenant = ModelState("a", "Tenant", [ ("id", models.AutoField(primary_key=True)), ("primary_address", models.ForeignKey("b.Address"))], bases=(AbstractBaseUser, ) ) address = ModelState("b", "Address", [ ("id", models.AutoField(primary_key=True)), ("tenant", models.ForeignKey(settings.AUTH_USER_MODEL)), ]) # Make state before = self.make_project_state([]) after = self.make_project_state([address, tenant]) autodetector = MigrationAutodetector(before, after) changes = autodetector._detect_changes() # Right number of migrations? self.assertNumberMigrations(changes, 'a', 2) self.assertNumberMigrations(changes, 'b', 1) self.assertOperationTypes(changes, 'a', 0, ["CreateModel"]) self.assertOperationTypes(changes, 'a', 1, ["AddField"]) self.assertOperationTypes(changes, 'b', 0, ["CreateModel"]) self.assertEqual(changes['a'][0].dependencies, []) self.assertEqual(set(changes['a'][1].dependencies), set([('a', 'auto_1'), ('b', 'auto_1')])) self.assertEqual(changes['b'][0].dependencies, [('__setting__', 'AUTH_USER_MODEL')]) @override_settings(AUTH_USER_MODEL="b.Tenant") def test_circular_dependency_swappable2(self): """ Tests that the dependency resolver knows to explicitly resolve swappable models but with the swappable not being the first migrated model (#23322) """ address = ModelState("a", "Address", [ ("id", models.AutoField(primary_key=True)), ("tenant", models.ForeignKey(settings.AUTH_USER_MODEL)), ]) tenant = ModelState("b", "Tenant", [ ("id", models.AutoField(primary_key=True)), ("primary_address", models.ForeignKey("a.Address"))], bases=(AbstractBaseUser, ) ) # Make state before = self.make_project_state([]) after = self.make_project_state([address, tenant]) autodetector = MigrationAutodetector(before, after) changes = autodetector._detect_changes() # Right number of migrations? self.assertNumberMigrations(changes, 'a', 2) self.assertNumberMigrations(changes, 'b', 1) self.assertOperationTypes(changes, 'a', 0, ["CreateModel"]) self.assertOperationTypes(changes, 'a', 1, ["AddField"]) self.assertOperationTypes(changes, 'b', 0, ["CreateModel"]) self.assertEqual(changes['a'][0].dependencies, []) self.assertEqual(set(changes['a'][1].dependencies), set([('__setting__', 'AUTH_USER_MODEL'), ('a', 'auto_1')])) self.assertEqual(changes['b'][0].dependencies, [('a', 'auto_1')]) @override_settings(AUTH_USER_MODEL="a.Person") def test_circular_dependency_swappable_self(self): """ Tests that the dependency resolver knows to explicitly resolve swappable models (#23322) """ person = ModelState("a", "Person", [ ("id", models.AutoField(primary_key=True)), ("parent1", models.ForeignKey(settings.AUTH_USER_MODEL, related_name='children')) ]) # Make state before = self.make_project_state([]) after = self.make_project_state([person]) autodetector = MigrationAutodetector(before, after) changes = autodetector._detect_changes() # Right number of migrations? self.assertNumberMigrations(changes, 'a', 1) self.assertOperationTypes(changes, 'a', 0, ["CreateModel"]) self.assertEqual(changes['a'][0].dependencies, [])