~ubuntu-branches/debian/sid/python-django/sid

« back to all changes in this revision

Viewing changes to django/db/migrations/optimizer.py

  • Committer: Package Import Robot
  • Author(s): Raphaël Hertzog
  • Date: 2014-09-17 14:15:11 UTC
  • mfrom: (1.3.17) (6.2.18 experimental)
  • Revision ID: package-import@ubuntu.com-20140917141511-icneokthe9ww5sk4
Tags: 1.7-2
* Release to unstable.
* Add a migrate-south sample script to help users apply their South
  migrations. Thanks to Brian May.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
from __future__ import unicode_literals
 
2
 
 
3
from django.db import migrations
 
4
from django.utils import six
 
5
 
 
6
 
 
7
class MigrationOptimizer(object):
 
8
    """
 
9
    Powers the optimization process, where you provide a list of Operations
 
10
    and you are returned a list of equal or shorter length - operations
 
11
    are merged into one if possible.
 
12
 
 
13
    For example, a CreateModel and an AddField can be optimized into a
 
14
    new CreateModel, and CreateModel and DeleteModel can be optimized into
 
15
    nothing.
 
16
    """
 
17
 
 
18
    def optimize(self, operations, app_label=None):
 
19
        """
 
20
        Main optimization entry point. Pass in a list of Operation instances,
 
21
        get out a new list of Operation instances.
 
22
 
 
23
        Unfortunately, due to the scope of the optimization (two combinable
 
24
        operations might be separated by several hundred others), this can't be
 
25
        done as a peephole optimization with checks/output implemented on
 
26
        the Operations themselves; instead, the optimizer looks at each
 
27
        individual operation and scans forwards in the list to see if there
 
28
        are any matches, stopping at boundaries - operations which can't
 
29
        be optimized over (RunSQL, operations on the same field/model, etc.)
 
30
 
 
31
        The inner loop is run until the starting list is the same as the result
 
32
        list, and then the result is returned. This means that operation
 
33
        optimization must be stable and always return an equal or shorter list.
 
34
 
 
35
        The app_label argument is optional, but if you pass it you'll get more
 
36
        efficient optimization.
 
37
        """
 
38
        # Internal tracking variable for test assertions about # of loops
 
39
        self._iterations = 0
 
40
        while True:
 
41
            result = self.optimize_inner(operations, app_label)
 
42
            self._iterations += 1
 
43
            if result == operations:
 
44
                return result
 
45
            operations = result
 
46
 
 
47
    def optimize_inner(self, operations, app_label=None):
 
48
        """
 
49
        Inner optimization loop.
 
50
        """
 
51
        new_operations = []
 
52
        for i, operation in enumerate(operations):
 
53
            # Compare it to each operation after it
 
54
            for j, other in enumerate(operations[i + 1:]):
 
55
                result = self.reduce(operation, other, operations[i + 1:i + j + 1])
 
56
                if result is not None:
 
57
                    # Optimize! Add result, then remaining others, then return
 
58
                    new_operations.extend(result)
 
59
                    new_operations.extend(operations[i + 1:i + 1 + j])
 
60
                    new_operations.extend(operations[i + j + 2:])
 
61
                    return new_operations
 
62
                if not self.can_optimize_through(operation, other, app_label):
 
63
                    new_operations.append(operation)
 
64
                    break
 
65
            else:
 
66
                new_operations.append(operation)
 
67
        return new_operations
 
68
 
 
69
    #### REDUCTION ####
 
70
 
 
71
    def reduce(self, operation, other, in_between=None):
 
72
        """
 
73
        Either returns a list of zero, one or two operations,
 
74
        or None, meaning this pair cannot be optimized.
 
75
        """
 
76
        submethods = [
 
77
            (
 
78
                migrations.CreateModel,
 
79
                migrations.DeleteModel,
 
80
                self.reduce_model_create_delete,
 
81
            ),
 
82
            (
 
83
                migrations.AlterModelTable,
 
84
                migrations.DeleteModel,
 
85
                self.reduce_model_alter_delete,
 
86
            ),
 
87
            (
 
88
                migrations.AlterUniqueTogether,
 
89
                migrations.DeleteModel,
 
90
                self.reduce_model_alter_delete,
 
91
            ),
 
92
            (
 
93
                migrations.AlterIndexTogether,
 
94
                migrations.DeleteModel,
 
95
                self.reduce_model_alter_delete,
 
96
            ),
 
97
            (
 
98
                migrations.CreateModel,
 
99
                migrations.RenameModel,
 
100
                self.reduce_model_create_rename,
 
101
            ),
 
102
            (
 
103
                migrations.RenameModel,
 
104
                migrations.RenameModel,
 
105
                self.reduce_model_rename_self,
 
106
            ),
 
107
            (
 
108
                migrations.CreateModel,
 
109
                migrations.AddField,
 
110
                self.reduce_create_model_add_field,
 
111
            ),
 
112
            (
 
113
                migrations.CreateModel,
 
114
                migrations.AlterField,
 
115
                self.reduce_create_model_alter_field,
 
116
            ),
 
117
            (
 
118
                migrations.CreateModel,
 
119
                migrations.RemoveField,
 
120
                self.reduce_create_model_remove_field,
 
121
            ),
 
122
            (
 
123
                migrations.AddField,
 
124
                migrations.AlterField,
 
125
                self.reduce_add_field_alter_field,
 
126
            ),
 
127
            (
 
128
                migrations.AddField,
 
129
                migrations.RemoveField,
 
130
                self.reduce_add_field_delete_field,
 
131
            ),
 
132
            (
 
133
                migrations.AlterField,
 
134
                migrations.RemoveField,
 
135
                self.reduce_alter_field_delete_field,
 
136
            ),
 
137
            (
 
138
                migrations.AddField,
 
139
                migrations.RenameField,
 
140
                self.reduce_add_field_rename_field,
 
141
            ),
 
142
            (
 
143
                migrations.AlterField,
 
144
                migrations.RenameField,
 
145
                self.reduce_alter_field_rename_field,
 
146
            ),
 
147
            (
 
148
                migrations.CreateModel,
 
149
                migrations.RenameField,
 
150
                self.reduce_create_model_rename_field,
 
151
            ),
 
152
            (
 
153
                migrations.RenameField,
 
154
                migrations.RenameField,
 
155
                self.reduce_rename_field_self,
 
156
            ),
 
157
        ]
 
158
        for ia, ib, om in submethods:
 
159
            if isinstance(operation, ia) and isinstance(other, ib):
 
160
                return om(operation, other, in_between or [])
 
161
        return None
 
162
 
 
163
    def model_to_key(self, model):
 
164
        """
 
165
        Takes either a model class or a "appname.ModelName" string
 
166
        and returns (appname, modelname)
 
167
        """
 
168
        if isinstance(model, six.string_types):
 
169
            return model.split(".", 1)
 
170
        else:
 
171
            return (
 
172
                model._meta.app_label,
 
173
                model._meta.object_name,
 
174
            )
 
175
 
 
176
    def reduce_model_create_delete(self, operation, other, in_between):
 
177
        """
 
178
        Folds a CreateModel and a DeleteModel into nothing.
 
179
        """
 
180
        if (operation.name.lower() == other.name.lower() and
 
181
                not operation.options.get("proxy", False)):
 
182
            return []
 
183
 
 
184
    def reduce_model_alter_delete(self, operation, other, in_between):
 
185
        """
 
186
        Folds an AlterModelSomething and a DeleteModel into just delete.
 
187
        """
 
188
        if operation.name.lower() == other.name.lower():
 
189
            return [other]
 
190
 
 
191
    def reduce_model_create_rename(self, operation, other, in_between):
 
192
        """
 
193
        Folds a model rename into its create
 
194
        """
 
195
        if operation.name.lower() == other.old_name.lower():
 
196
            return [
 
197
                migrations.CreateModel(
 
198
                    other.new_name,
 
199
                    fields=operation.fields,
 
200
                    options=operation.options,
 
201
                    bases=operation.bases,
 
202
                )
 
203
            ]
 
204
 
 
205
    def reduce_model_rename_self(self, operation, other, in_between):
 
206
        """
 
207
        Folds a model rename into another one
 
208
        """
 
209
        if operation.new_name.lower() == other.old_name.lower():
 
210
            return [
 
211
                migrations.RenameModel(
 
212
                    operation.old_name,
 
213
                    other.new_name,
 
214
                )
 
215
            ]
 
216
 
 
217
    def reduce_create_model_add_field(self, operation, other, in_between):
 
218
        if operation.name.lower() == other.model_name.lower():
 
219
            # Don't allow optimisations of FKs through models they reference
 
220
            if hasattr(other.field, "rel") and other.field.rel:
 
221
                for between in in_between:
 
222
                    # Check that it doesn't point to the model
 
223
                    app_label, object_name = self.model_to_key(other.field.rel.to)
 
224
                    if between.references_model(object_name, app_label):
 
225
                        return None
 
226
                    # Check that it's not through the model
 
227
                    if getattr(other.field.rel, "through", None):
 
228
                        app_label, object_name = self.model_to_key(other.field.rel.through)
 
229
                        if between.references_model(object_name, app_label):
 
230
                            return None
 
231
            # OK, that's fine
 
232
            return [
 
233
                migrations.CreateModel(
 
234
                    operation.name,
 
235
                    fields=operation.fields + [(other.name, other.field)],
 
236
                    options=operation.options,
 
237
                    bases=operation.bases,
 
238
                )
 
239
            ]
 
240
 
 
241
    def reduce_create_model_alter_field(self, operation, other, in_between):
 
242
        if operation.name.lower() == other.model_name.lower():
 
243
            return [
 
244
                migrations.CreateModel(
 
245
                    operation.name,
 
246
                    fields=[
 
247
                        (n, other.field if n == other.name else v)
 
248
                        for n, v in operation.fields
 
249
                    ],
 
250
                    options=operation.options,
 
251
                    bases=operation.bases,
 
252
                )
 
253
            ]
 
254
 
 
255
    def reduce_create_model_rename_field(self, operation, other, in_between):
 
256
        if operation.name.lower() == other.model_name.lower():
 
257
            return [
 
258
                migrations.CreateModel(
 
259
                    operation.name,
 
260
                    fields=[
 
261
                        (other.new_name if n == other.old_name else n, v)
 
262
                        for n, v in operation.fields
 
263
                    ],
 
264
                    options=operation.options,
 
265
                    bases=operation.bases,
 
266
                )
 
267
            ]
 
268
 
 
269
    def reduce_create_model_remove_field(self, operation, other, in_between):
 
270
        if operation.name.lower() == other.model_name.lower():
 
271
            return [
 
272
                migrations.CreateModel(
 
273
                    operation.name,
 
274
                    fields=[
 
275
                        (n, v)
 
276
                        for n, v in operation.fields
 
277
                        if n.lower() != other.name.lower()
 
278
                    ],
 
279
                    options=operation.options,
 
280
                    bases=operation.bases,
 
281
                )
 
282
            ]
 
283
 
 
284
    def reduce_add_field_alter_field(self, operation, other, in_between):
 
285
        if operation.model_name.lower() == other.model_name.lower() and operation.name.lower() == other.name.lower():
 
286
            return [
 
287
                migrations.AddField(
 
288
                    model_name=operation.model_name,
 
289
                    name=operation.name,
 
290
                    field=other.field,
 
291
                )
 
292
            ]
 
293
 
 
294
    def reduce_add_field_delete_field(self, operation, other, in_between):
 
295
        if operation.model_name.lower() == other.model_name.lower() and operation.name.lower() == other.name.lower():
 
296
            return []
 
297
 
 
298
    def reduce_alter_field_delete_field(self, operation, other, in_between):
 
299
        if operation.model_name.lower() == other.model_name.lower() and operation.name.lower() == other.name.lower():
 
300
            return [other]
 
301
 
 
302
    def reduce_add_field_rename_field(self, operation, other, in_between):
 
303
        if operation.model_name.lower() == other.model_name.lower() and operation.name.lower() == other.old_name.lower():
 
304
            return [
 
305
                migrations.AddField(
 
306
                    model_name=operation.model_name,
 
307
                    name=other.new_name,
 
308
                    field=operation.field,
 
309
                )
 
310
            ]
 
311
 
 
312
    def reduce_alter_field_rename_field(self, operation, other, in_between):
 
313
        if operation.model_name.lower() == other.model_name.lower() and operation.name.lower() == other.old_name.lower():
 
314
            return [
 
315
                other,
 
316
                migrations.AlterField(
 
317
                    model_name=operation.model_name,
 
318
                    name=other.new_name,
 
319
                    field=operation.field,
 
320
                ),
 
321
            ]
 
322
 
 
323
    def reduce_rename_field_self(self, operation, other, in_between):
 
324
        if operation.model_name.lower() == other.model_name.lower() and operation.new_name.lower() == other.old_name.lower():
 
325
            return [
 
326
                migrations.RenameField(
 
327
                    operation.model_name,
 
328
                    operation.old_name,
 
329
                    other.new_name,
 
330
                ),
 
331
            ]
 
332
 
 
333
    #### THROUGH CHECKS ####
 
334
 
 
335
    def can_optimize_through(self, operation, other, app_label=None):
 
336
        """
 
337
        Returns True if it's possible to optimize 'operation' with something
 
338
        the other side of 'other'. This is possible if, for example, they
 
339
        affect different models.
 
340
        """
 
341
        MODEL_LEVEL_OPERATIONS = (
 
342
            migrations.CreateModel,
 
343
            migrations.AlterModelTable,
 
344
            migrations.AlterUniqueTogether,
 
345
            migrations.AlterIndexTogether,
 
346
        )
 
347
        FIELD_LEVEL_OPERATIONS = (
 
348
            migrations.AddField,
 
349
            migrations.AlterField,
 
350
        )
 
351
        # If it's a model level operation, let it through if there's
 
352
        # nothing that looks like a reference to us in 'other'.
 
353
        if isinstance(operation, MODEL_LEVEL_OPERATIONS):
 
354
            if not other.references_model(operation.name, app_label):
 
355
                return True
 
356
        # If it's field level, only let it through things that don't reference
 
357
        # the field (which includes not referencing the model)
 
358
        if isinstance(operation, FIELD_LEVEL_OPERATIONS):
 
359
            if not other.references_field(operation.model_name, operation.name, app_label):
 
360
                return True
 
361
        return False