Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Autodetect ForeignKeys and add dependencies/split on circulars

  • Loading branch information...
commit e2d7e83256234251a81ad3388428f6579795a672 1 parent 48493cf
Andrew Godwin authored
101  django/db/migrations/autodetector.py
@@ -36,19 +36,87 @@ def changes(self):
36 36
         """
37 37
         # We'll store migrations as lists by app names for now
38 38
         self.migrations = {}
39  
-        # Adding models.
  39
+        old_app_cache = self.from_state.render()
  40
+        new_app_cache = self.to_state.render()
  41
+        # Adding models. Phase 1 is adding models with no outward relationships.
40 42
         added_models = set(self.to_state.models.keys()) - set(self.from_state.models.keys())
  43
+        pending_add = {}
41 44
         for app_label, model_name in added_models:
42 45
             model_state = self.to_state.models[app_label, model_name]
  46
+            # Are there any relationships out from this model? if so, punt it to the next phase.
  47
+            related_fields = []
  48
+            for field in new_app_cache.get_model(app_label, model_name)._meta.fields:
  49
+                if hasattr(field, "rel"):
  50
+                    if hasattr(field.rel, "to"):
  51
+                        related_fields.append((field.name, field.rel.to._meta.app_label.lower(), field.rel.to._meta.object_name.lower()))
  52
+                    if hasattr(field.rel, "through") and not field.rel.though._meta.auto_created:
  53
+                        related_fields.append((field.name, field.rel.through._meta.app_label.lower(), field.rel.through._meta.object_name.lower()))
  54
+            if related_fields:
  55
+                pending_add[app_label, model_name] = related_fields
  56
+            else:
  57
+                self.add_to_migration(
  58
+                    app_label,
  59
+                    operations.CreateModel(
  60
+                        name = model_state.name,
  61
+                        fields = model_state.fields,
  62
+                        options = model_state.options,
  63
+                        bases = model_state.bases,
  64
+                    )
  65
+                )
  66
+        # Phase 2 is progressively adding pending models, splitting up into two
  67
+        # migrations if required.
  68
+        pending_new_fks = []
  69
+        while pending_add:
  70
+            # Is there one we can add that has all dependencies satisfied?
  71
+            satisfied = [(m, rf) for m, rf in pending_add.items() if all((al, mn) not in pending_add for f, al, mn in rf)]
  72
+            if satisfied:
  73
+                (app_label, model_name), related_fields = sorted(satisfied)[0]
  74
+                model_state = self.to_state.models[app_label, model_name]
  75
+                self.add_to_migration(
  76
+                    app_label,
  77
+                    operations.CreateModel(
  78
+                        name = model_state.name,
  79
+                        fields = model_state.fields,
  80
+                        options = model_state.options,
  81
+                        bases = model_state.bases,
  82
+                    )
  83
+                )
  84
+                for field_name, other_app_label, other_model_name in related_fields:
  85
+                    self.add_dependency(app_label, other_app_label)
  86
+                del pending_add[app_label, model_name]
  87
+            # Ah well, we'll need to split one. Pick deterministically.
  88
+            else:
  89
+                (app_label, model_name), related_fields = sorted(pending_add.items())[0]
  90
+                model_state = self.to_state.models[app_label, model_name]
  91
+                # Work out the fields that need splitting out
  92
+                bad_fields = dict((f, (al, mn)) for f, al, mn in related_fields if (al, mn) in pending_add)
  93
+                # Create the model, without those
  94
+                self.add_to_migration(
  95
+                    app_label,
  96
+                    operations.CreateModel(
  97
+                        name = model_state.name,
  98
+                        fields = [(n, f) for n, f in model_state.fields if n not in bad_fields],
  99
+                        options = model_state.options,
  100
+                        bases = model_state.bases,
  101
+                    )
  102
+                )
  103
+                # Add the bad fields to be made in a phase 3
  104
+                for field_name, (other_app_label, other_model_name) in bad_fields.items():
  105
+                    pending_new_fks.append((app_label, model_name, field_name, other_app_label))
  106
+                del pending_add[app_label, model_name]
  107
+        # Phase 3 is adding the final set of FKs as separate new migrations
  108
+        for app_label, model_name, field_name, other_app_label in pending_new_fks:
  109
+            model_state = self.to_state.models[app_label, model_name]
43 110
             self.add_to_migration(
44 111
                 app_label,
45  
-                operations.CreateModel(
46  
-                    name = model_state.name,
47  
-                    fields = model_state.fields,
48  
-                    options = model_state.options,
49  
-                    bases = model_state.bases,
50  
-                )
  112
+                operations.AddField(
  113
+                    model_name = model_name,
  114
+                    name = field_name,
  115
+                    field = model_state.get_field_by_name(field_name),
  116
+                ),
  117
+                new = True,
51 118
             )
  119
+            self.add_dependency(app_label, other_app_label)
52 120
         # Removing models
53 121
         removed_models = set(self.from_state.models.keys()) - set(self.to_state.models.keys())
54 122
         for app_label, model_name in removed_models:
@@ -127,16 +195,31 @@ def changes(self):
127 195
         for app_label, migrations in self.migrations.items():
128 196
             for m1, m2 in zip(migrations, migrations[1:]):
129 197
                 m2.dependencies.append((app_label, m1.name))
  198
+        # Clean up dependencies
  199
+        for app_label, migrations in self.migrations.items():
  200
+            for migration in migrations:
  201
+                migration.dependencies = list(set(migration.dependencies))
130 202
         return self.migrations
131 203
 
132  
-    def add_to_migration(self, app_label, operation):
  204
+    def add_to_migration(self, app_label, operation, new=False):
133 205
         migrations = self.migrations.setdefault(app_label, [])
134  
-        if not migrations:
  206
+        if not migrations or new:
135 207
             subclass = type("Migration", (Migration,), {"operations": [], "dependencies": []})
136 208
             instance = subclass("auto_%i" % (len(migrations) + 1), app_label)
137 209
             migrations.append(instance)
138 210
         migrations[-1].operations.append(operation)
139 211
 
  212
+    def add_dependency(self, app_label, other_app_label):
  213
+        """
  214
+        Adds a dependency to app_label's newest migration on
  215
+        other_app_label's latest migration.
  216
+        """
  217
+        if self.migrations.get(other_app_label, []):
  218
+            dependency = (other_app_label, self.migrations[other_app_label][-1].name)
  219
+        else:
  220
+            dependency = (other_app_label, "__first__")
  221
+        self.migrations[app_label][-1].dependencies.append(dependency)
  222
+
140 223
     def arrange_for_graph(self, changes, graph):
141 224
         """
142 225
         Takes in a result from changes() and a MigrationGraph,
7  django/db/migrations/state.py
@@ -70,7 +70,7 @@ def from_model(cls, model):
70 70
         """
71 71
         # Deconstruct the fields
72 72
         fields = []
73  
-        for field in model._meta.local_fields:
  73
+        for field in model._meta.fields:
74 74
             name, path, args, kwargs = field.deconstruct()
75 75
             field_class = import_by_path(path)
76 76
             fields.append((name, field_class(*args, **kwargs)))
@@ -83,12 +83,15 @@ def from_model(cls, model):
83 83
             if name in model._meta.original_attrs:
84 84
                 options[name] = model._meta.original_attrs[name]
85 85
         # Make our record
  86
+        bases = tuple(model for model in model.__bases__ if (not hasattr(model, "_meta") or not model._meta.abstract))
  87
+        if not bases:
  88
+            bases = (models.Model, )
86 89
         return cls(
87 90
             model._meta.app_label,
88 91
             model._meta.object_name,
89 92
             fields,
90 93
             options,
91  
-            model.__bases__,
  94
+            bases,
92 95
         )
93 96
 
94 97
     def clone(self):
67  tests/migrations/test_autodetector.py
@@ -15,9 +15,12 @@ class AutodetectorTests(TestCase):
15 15
     author_name = ModelState("testapp", "Author", [("id", models.AutoField(primary_key=True)), ("name", models.CharField(max_length=200))])
16 16
     author_name_longer = ModelState("testapp", "Author", [("id", models.AutoField(primary_key=True)), ("name", models.CharField(max_length=400))])
17 17
     author_name_renamed = ModelState("testapp", "Author", [("id", models.AutoField(primary_key=True)), ("names", models.CharField(max_length=200))])
  18
+    author_with_book = ModelState("testapp", "Author", [("id", models.AutoField(primary_key=True)), ("name", models.CharField(max_length=200)), ("book", models.ForeignKey("otherapp.Book"))])
18 19
     other_pony = ModelState("otherapp", "Pony", [("id", models.AutoField(primary_key=True))])
19 20
     other_stable = ModelState("otherapp", "Stable", [("id", models.AutoField(primary_key=True))])
20 21
     third_thing = ModelState("thirdapp", "Thing", [("id", models.AutoField(primary_key=True))])
  22
+    book = ModelState("otherapp", "Book", [("id", models.AutoField(primary_key=True)), ("author", models.ForeignKey("testapp.Author")), ("title", models.CharField(max_length=200))])
  23
+    edition = ModelState("thirdapp", "Edition", [("id", models.AutoField(primary_key=True)), ("book", models.ForeignKey("otherapp.Book"))])
21 24
 
22 25
     def make_project_state(self, model_states):
23 26
         "Shortcut to make ProjectStates from lists of predefined models"
@@ -167,3 +170,67 @@ def test_rename_field(self):
167 170
         self.assertEqual(action.__class__.__name__, "RenameField")
168 171
         self.assertEqual(action.old_name, "name")
169 172
         self.assertEqual(action.new_name, "names")
  173
+
  174
+    def test_fk_dependency(self):
  175
+        "Tests that having a ForeignKey automatically adds a dependency"
  176
+        # Make state
  177
+        before = self.make_project_state([])
  178
+        after = self.make_project_state([self.author_name, self.book, self.edition])
  179
+        autodetector = MigrationAutodetector(before, after)
  180
+        changes = autodetector.changes()
  181
+        # Right number of migrations?
  182
+        self.assertEqual(len(changes['testapp']), 1)
  183
+        self.assertEqual(len(changes['otherapp']), 1)
  184
+        self.assertEqual(len(changes['thirdapp']), 1)
  185
+        # Right number of actions?
  186
+        migration1 = changes['testapp'][0]
  187
+        self.assertEqual(len(migration1.operations), 1)
  188
+        migration2 = changes['otherapp'][0]
  189
+        self.assertEqual(len(migration2.operations), 1)
  190
+        migration3 = changes['thirdapp'][0]
  191
+        self.assertEqual(len(migration3.operations), 1)
  192
+        # Right actions?
  193
+        action = migration1.operations[0]
  194
+        self.assertEqual(action.__class__.__name__, "CreateModel")
  195
+        action = migration2.operations[0]
  196
+        self.assertEqual(action.__class__.__name__, "CreateModel")
  197
+        action = migration3.operations[0]
  198
+        self.assertEqual(action.__class__.__name__, "CreateModel")
  199
+        # Right dependencies?
  200
+        self.assertEqual(migration1.dependencies, [])
  201
+        self.assertEqual(migration2.dependencies, [("testapp", "auto_1")])
  202
+        self.assertEqual(migration3.dependencies, [("otherapp", "auto_1")])
  203
+
  204
+    def test_circular_fk_dependency(self):
  205
+        """
  206
+        Tests that having a circular ForeignKey dependency automatically
  207
+        resolves the situation into 2 migrations on one side and 1 on the other.
  208
+        """
  209
+        # Make state
  210
+        before = self.make_project_state([])
  211
+        after = self.make_project_state([self.author_with_book, self.book])
  212
+        autodetector = MigrationAutodetector(before, after)
  213
+        changes = autodetector.changes()
  214
+        # Right number of migrations?
  215
+        self.assertEqual(len(changes['testapp']), 1)
  216
+        self.assertEqual(len(changes['otherapp']), 2)
  217
+        # Right number of actions?
  218
+        migration1 = changes['testapp'][0]
  219
+        self.assertEqual(len(migration1.operations), 1)
  220
+        migration2 = changes['otherapp'][0]
  221
+        self.assertEqual(len(migration2.operations), 1)
  222
+        migration3 = changes['otherapp'][1]
  223
+        self.assertEqual(len(migration2.operations), 1)
  224
+        # Right actions?
  225
+        action = migration1.operations[0]
  226
+        self.assertEqual(action.__class__.__name__, "CreateModel")
  227
+        action = migration2.operations[0]
  228
+        self.assertEqual(action.__class__.__name__, "CreateModel")
  229
+        self.assertEqual(len(action.fields), 2)
  230
+        action = migration3.operations[0]
  231
+        self.assertEqual(action.__class__.__name__, "AddField")
  232
+        self.assertEqual(action.name, "author")
  233
+        # Right dependencies?
  234
+        self.assertEqual(migration1.dependencies, [("otherapp", "auto_1")])
  235
+        self.assertEqual(migration2.dependencies, [])
  236
+        self.assertEqual(set(migration3.dependencies), set([("otherapp", "auto_1"), ("testapp", "auto_1")]))

0 notes on commit e2d7e83

Please sign in to comment.
Something went wrong with that request. Please try again.