1import functools
2import re
3from graphlib import TopologicalSorter
4from itertools import chain
5
6from plain import models
7from plain.models.migrations import operations
8from plain.models.migrations.migration import Migration
9from plain.models.migrations.operations.models import AlterModelOptions
10from plain.models.migrations.optimizer import MigrationOptimizer
11from plain.models.migrations.questioner import MigrationQuestioner
12from plain.models.migrations.utils import (
13 COMPILED_REGEX_TYPE,
14 RegexObject,
15 resolve_relation,
16)
17from plain.runtime import settings
18
19
20class MigrationAutodetector:
21 """
22 Take a pair of ProjectStates and compare them to see what the first would
23 need doing to make it match the second (the second usually being the
24 project's current state).
25
26 Note that this naturally operates on entire projects at a time,
27 as it's likely that changes interact (for example, you can't
28 add a ForeignKey without having a migration to add the table it
29 depends on first). A user interface may offer single-app usage
30 if it wishes, with the caveat that it may not always be possible.
31 """
32
33 def __init__(self, from_state, to_state, questioner=None):
34 self.from_state = from_state
35 self.to_state = to_state
36 self.questioner = questioner or MigrationQuestioner()
37 self.existing_packages = {app for app, model in from_state.models}
38
39 def changes(
40 self, graph, trim_to_packages=None, convert_packages=None, migration_name=None
41 ):
42 """
43 Main entry point to produce a list of applicable changes.
44 Take a graph to base names on and an optional set of packages
45 to try and restrict to (restriction is not guaranteed)
46 """
47 changes = self._detect_changes(convert_packages, graph)
48 changes = self.arrange_for_graph(changes, graph, migration_name)
49 if trim_to_packages:
50 changes = self._trim_to_packages(changes, trim_to_packages)
51 return changes
52
53 def deep_deconstruct(self, obj):
54 """
55 Recursive deconstruction for a field and its arguments.
56 Used for full comparison for rename/alter; sometimes a single-level
57 deconstruction will not compare correctly.
58 """
59 if isinstance(obj, list):
60 return [self.deep_deconstruct(value) for value in obj]
61 elif isinstance(obj, tuple):
62 return tuple(self.deep_deconstruct(value) for value in obj)
63 elif isinstance(obj, dict):
64 return {key: self.deep_deconstruct(value) for key, value in obj.items()}
65 elif isinstance(obj, functools.partial):
66 return (
67 obj.func,
68 self.deep_deconstruct(obj.args),
69 self.deep_deconstruct(obj.keywords),
70 )
71 elif isinstance(obj, COMPILED_REGEX_TYPE):
72 return RegexObject(obj)
73 elif isinstance(obj, type):
74 # If this is a type that implements 'deconstruct' as an instance method,
75 # avoid treating this as being deconstructible itself - see #22951
76 return obj
77 elif hasattr(obj, "deconstruct"):
78 deconstructed = obj.deconstruct()
79 if isinstance(obj, models.Field):
80 # we have a field which also returns a name
81 deconstructed = deconstructed[1:]
82 path, args, kwargs = deconstructed
83 return (
84 path,
85 [self.deep_deconstruct(value) for value in args],
86 {key: self.deep_deconstruct(value) for key, value in kwargs.items()},
87 )
88 else:
89 return obj
90
91 def only_relation_agnostic_fields(self, fields):
92 """
93 Return a definition of the fields that ignores field names and
94 what related fields actually relate to. Used for detecting renames (as
95 the related fields change during renames).
96 """
97 fields_def = []
98 for name, field in sorted(fields.items()):
99 deconstruction = self.deep_deconstruct(field)
100 if field.remote_field and field.remote_field.model:
101 deconstruction[2].pop("to", None)
102 fields_def.append(deconstruction)
103 return fields_def
104
105 def _detect_changes(self, convert_packages=None, graph=None):
106 """
107 Return a dict of migration plans which will achieve the
108 change from from_state to to_state. The dict has app labels
109 as keys and a list of migrations as values.
110
111 The resulting migrations aren't specially named, but the names
112 do matter for dependencies inside the set.
113
114 convert_packages is the list of packages to convert to use migrations
115 (i.e. to make initial migrations for, in the usual case)
116
117 graph is an optional argument that, if provided, can help improve
118 dependency generation and avoid potential circular dependencies.
119 """
120 # The first phase is generating all the operations for each app
121 # and gathering them into a big per-app list.
122 # Then go through that list, order it, and split into migrations to
123 # resolve dependencies caused by M2Ms and FKs.
124 self.generated_operations = {}
125 self.altered_indexes = {}
126 self.altered_constraints = {}
127 self.renamed_fields = {}
128
129 # Prepare some old/new state and model lists, ignoring unmigrated packages.
130 self.old_model_keys = set()
131 self.old_unmanaged_keys = set()
132 self.new_model_keys = set()
133 self.new_unmanaged_keys = set()
134 for (package_label, model_name), model_state in self.from_state.models.items():
135 if not model_state.options.get("managed", True):
136 self.old_unmanaged_keys.add((package_label, model_name))
137 elif package_label not in self.from_state.real_packages:
138 self.old_model_keys.add((package_label, model_name))
139
140 for (package_label, model_name), model_state in self.to_state.models.items():
141 if not model_state.options.get("managed", True):
142 self.new_unmanaged_keys.add((package_label, model_name))
143 elif package_label not in self.from_state.real_packages or (
144 convert_packages and package_label in convert_packages
145 ):
146 self.new_model_keys.add((package_label, model_name))
147
148 self.from_state.resolve_fields_and_relations()
149 self.to_state.resolve_fields_and_relations()
150
151 # Renames have to come first
152 self.generate_renamed_models()
153
154 # Prepare lists of fields and generate through model map
155 self._prepare_field_lists()
156 self._generate_through_model_map()
157
158 # Generate non-rename model operations
159 self.generate_deleted_models()
160 self.generate_created_models()
161 self.generate_altered_options()
162 self.generate_altered_managers()
163 self.generate_altered_db_table_comment()
164
165 # Create the renamed fields and store them in self.renamed_fields.
166 # They are used by create_altered_indexes(), generate_altered_fields(),
167 # generate_removed_altered_index(), and
168 # generate_altered_index().
169 self.create_renamed_fields()
170 # Create the altered indexes and store them in self.altered_indexes.
171 # This avoids the same computation in generate_removed_indexes()
172 # and generate_added_indexes().
173 self.create_altered_indexes()
174 self.create_altered_constraints()
175 # Generate index removal operations before field is removed
176 self.generate_removed_constraints()
177 self.generate_removed_indexes()
178 # Generate field renaming operations.
179 self.generate_renamed_fields()
180 self.generate_renamed_indexes()
181 # Generate field operations.
182 self.generate_removed_fields()
183 self.generate_added_fields()
184 self.generate_altered_fields()
185 self.generate_altered_order_with_respect_to()
186 self.generate_added_indexes()
187 self.generate_added_constraints()
188 self.generate_altered_db_table()
189
190 self._sort_migrations()
191 self._build_migration_list(graph)
192 self._optimize_migrations()
193
194 return self.migrations
195
196 def _prepare_field_lists(self):
197 """
198 Prepare field lists and a list of the fields that used through models
199 in the old state so dependencies can be made from the through model
200 deletion to the field that uses it.
201 """
202 self.kept_model_keys = self.old_model_keys & self.new_model_keys
203 self.kept_unmanaged_keys = self.old_unmanaged_keys & self.new_unmanaged_keys
204 self.through_users = {}
205 self.old_field_keys = {
206 (package_label, model_name, field_name)
207 for package_label, model_name in self.kept_model_keys
208 for field_name in self.from_state.models[
209 package_label,
210 self.renamed_models.get((package_label, model_name), model_name),
211 ].fields
212 }
213 self.new_field_keys = {
214 (package_label, model_name, field_name)
215 for package_label, model_name in self.kept_model_keys
216 for field_name in self.to_state.models[package_label, model_name].fields
217 }
218
219 def _generate_through_model_map(self):
220 """Through model map generation."""
221 for package_label, model_name in sorted(self.old_model_keys):
222 old_model_name = self.renamed_models.get(
223 (package_label, model_name), model_name
224 )
225 old_model_state = self.from_state.models[package_label, old_model_name]
226 for field_name, field in old_model_state.fields.items():
227 if hasattr(field, "remote_field") and getattr(
228 field.remote_field, "through", None
229 ):
230 through_key = resolve_relation(
231 field.remote_field.through, package_label, model_name
232 )
233 self.through_users[through_key] = (
234 package_label,
235 old_model_name,
236 field_name,
237 )
238
239 @staticmethod
240 def _resolve_dependency(dependency):
241 """
242 Return the resolved dependency and a boolean denoting whether or not
243 it was swappable.
244 """
245 if dependency[0] != "__setting__":
246 return dependency, False
247 resolved_package_label, resolved_object_name = getattr(
248 settings, dependency[1]
249 ).split(".")
250 return (resolved_package_label, resolved_object_name.lower()) + dependency[
251 2:
252 ], True
253
254 def _build_migration_list(self, graph=None):
255 """
256 Chop the lists of operations up into migrations with dependencies on
257 each other. Do this by going through an app's list of operations until
258 one is found that has an outgoing dependency that isn't in another
259 app's migration yet (hasn't been chopped off its list). Then chop off
260 the operations before it into a migration and move onto the next app.
261 If the loops completes without doing anything, there's a circular
262 dependency (which _should_ be impossible as the operations are
263 all split at this point so they can't depend and be depended on).
264 """
265 self.migrations = {}
266 num_ops = sum(len(x) for x in self.generated_operations.values())
267 chop_mode = False
268 while num_ops:
269 # On every iteration, we step through all the packages and see if there
270 # is a completed set of operations.
271 # If we find that a subset of the operations are complete we can
272 # try to chop it off from the rest and continue, but we only
273 # do this if we've already been through the list once before
274 # without any chopping and nothing has changed.
275 for package_label in sorted(self.generated_operations):
276 chopped = []
277 dependencies = set()
278 for operation in list(self.generated_operations[package_label]):
279 deps_satisfied = True
280 operation_dependencies = set()
281 for dep in operation._auto_deps:
282 # Temporarily resolve the swappable dependency to
283 # prevent circular references. While keeping the
284 # dependency checks on the resolved model, add the
285 # swappable dependencies.
286 original_dep = dep
287 dep, is_swappable_dep = self._resolve_dependency(dep)
288 if dep[0] != package_label:
289 # External app dependency. See if it's not yet
290 # satisfied.
291 for other_operation in self.generated_operations.get(
292 dep[0], []
293 ):
294 if self.check_dependency(other_operation, dep):
295 deps_satisfied = False
296 break
297 if not deps_satisfied:
298 break
299 else:
300 if is_swappable_dep:
301 operation_dependencies.add(
302 (original_dep[0], original_dep[1])
303 )
304 elif dep[0] in self.migrations:
305 operation_dependencies.add(
306 (dep[0], self.migrations[dep[0]][-1].name)
307 )
308 else:
309 # If we can't find the other app, we add a
310 # first/last dependency, but only if we've
311 # already been through once and checked
312 # everything.
313 if chop_mode:
314 # If the app already exists, we add a
315 # dependency on the last migration, as
316 # we don't know which migration
317 # contains the target field. If it's
318 # not yet migrated or has no
319 # migrations, we use __first__.
320 if graph and graph.leaf_nodes(dep[0]):
321 operation_dependencies.add(
322 graph.leaf_nodes(dep[0])[0]
323 )
324 else:
325 operation_dependencies.add(
326 (dep[0], "__first__")
327 )
328 else:
329 deps_satisfied = False
330 if deps_satisfied:
331 chopped.append(operation)
332 dependencies.update(operation_dependencies)
333 del self.generated_operations[package_label][0]
334 else:
335 break
336 # Make a migration! Well, only if there's stuff to put in it
337 if dependencies or chopped:
338 if not self.generated_operations[package_label] or chop_mode:
339 subclass = type(
340 "Migration",
341 (Migration,),
342 {"operations": [], "dependencies": []},
343 )
344 instance = subclass(
345 "auto_%i"
346 % (len(self.migrations.get(package_label, [])) + 1),
347 package_label,
348 )
349 instance.dependencies = list(dependencies)
350 instance.operations = chopped
351 instance.initial = package_label not in self.existing_packages
352 self.migrations.setdefault(package_label, []).append(instance)
353 chop_mode = False
354 else:
355 self.generated_operations[package_label] = (
356 chopped + self.generated_operations[package_label]
357 )
358 new_num_ops = sum(len(x) for x in self.generated_operations.values())
359 if new_num_ops == num_ops:
360 if not chop_mode:
361 chop_mode = True
362 else:
363 raise ValueError(
364 "Cannot resolve operation dependencies: %r"
365 % self.generated_operations
366 )
367 num_ops = new_num_ops
368
369 def _sort_migrations(self):
370 """
371 Reorder to make things possible. Reordering may be needed so FKs work
372 nicely inside the same app.
373 """
374 for package_label, ops in sorted(self.generated_operations.items()):
375 ts = TopologicalSorter()
376 for op in ops:
377 ts.add(op)
378 for dep in op._auto_deps:
379 # Resolve intra-app dependencies to handle circular
380 # references involving a swappable model.
381 dep = self._resolve_dependency(dep)[0]
382 if dep[0] != package_label:
383 continue
384 ts.add(op, *(x for x in ops if self.check_dependency(x, dep)))
385 self.generated_operations[package_label] = list(ts.static_order())
386
387 def _optimize_migrations(self):
388 # Add in internal dependencies among the migrations
389 for package_label, migrations in self.migrations.items():
390 for m1, m2 in zip(migrations, migrations[1:]):
391 m2.dependencies.append((package_label, m1.name))
392
393 # De-dupe dependencies
394 for migrations in self.migrations.values():
395 for migration in migrations:
396 migration.dependencies = list(set(migration.dependencies))
397
398 # Optimize migrations
399 for package_label, migrations in self.migrations.items():
400 for migration in migrations:
401 migration.operations = MigrationOptimizer().optimize(
402 migration.operations, package_label
403 )
404
405 def check_dependency(self, operation, dependency):
406 """
407 Return True if the given operation depends on the given dependency,
408 False otherwise.
409 """
410 # Created model
411 if dependency[2] is None and dependency[3] is True:
412 return (
413 isinstance(operation, operations.CreateModel)
414 and operation.name_lower == dependency[1].lower()
415 )
416 # Created field
417 elif dependency[2] is not None and dependency[3] is True:
418 return (
419 isinstance(operation, operations.CreateModel)
420 and operation.name_lower == dependency[1].lower()
421 and any(dependency[2] == x for x, y in operation.fields)
422 ) or (
423 isinstance(operation, operations.AddField)
424 and operation.model_name_lower == dependency[1].lower()
425 and operation.name_lower == dependency[2].lower()
426 )
427 # Removed field
428 elif dependency[2] is not None and dependency[3] is False:
429 return (
430 isinstance(operation, operations.RemoveField)
431 and operation.model_name_lower == dependency[1].lower()
432 and operation.name_lower == dependency[2].lower()
433 )
434 # Removed model
435 elif dependency[2] is None and dependency[3] is False:
436 return (
437 isinstance(operation, operations.DeleteModel)
438 and operation.name_lower == dependency[1].lower()
439 )
440 # Field being altered
441 elif dependency[2] is not None and dependency[3] == "alter":
442 return (
443 isinstance(operation, operations.AlterField)
444 and operation.model_name_lower == dependency[1].lower()
445 and operation.name_lower == dependency[2].lower()
446 )
447 # order_with_respect_to being unset for a field
448 elif dependency[2] is not None and dependency[3] == "order_wrt_unset":
449 return (
450 isinstance(operation, operations.AlterOrderWithRespectTo)
451 and operation.name_lower == dependency[1].lower()
452 and (operation.order_with_respect_to or "").lower()
453 != dependency[2].lower()
454 )
455 # Unknown dependency. Raise an error.
456 else:
457 raise ValueError(f"Can't handle dependency {dependency!r}")
458
459 def add_operation(
460 self, package_label, operation, dependencies=None, beginning=False
461 ):
462 # Dependencies are
463 # (package_label, model_name, field_name, create/delete as True/False)
464 operation._auto_deps = dependencies or []
465 if beginning:
466 self.generated_operations.setdefault(package_label, []).insert(0, operation)
467 else:
468 self.generated_operations.setdefault(package_label, []).append(operation)
469
470 def swappable_first_key(self, item):
471 """
472 Place potential swappable models first in lists of created models (only
473 real way to solve #22783).
474 """
475 try:
476 model_state = self.to_state.models[item]
477 base_names = {
478 base if isinstance(base, str) else base.__name__
479 for base in model_state.bases
480 }
481 string_version = f"{item[0]}.{item[1]}"
482 if (
483 model_state.options.get("swappable")
484 or "BaseUser" in base_names
485 or "AbstractBaseUser" in base_names
486 or settings.AUTH_USER_MODEL.lower() == string_version.lower()
487 ):
488 return ("___" + item[0], "___" + item[1])
489 except LookupError:
490 pass
491 return item
492
493 def generate_renamed_models(self):
494 """
495 Find any renamed models, generate the operations for them, and remove
496 the old entry from the model lists. Must be run before other
497 model-level generation.
498 """
499 self.renamed_models = {}
500 self.renamed_models_rel = {}
501 added_models = self.new_model_keys - self.old_model_keys
502 for package_label, model_name in sorted(added_models):
503 model_state = self.to_state.models[package_label, model_name]
504 model_fields_def = self.only_relation_agnostic_fields(model_state.fields)
505
506 removed_models = self.old_model_keys - self.new_model_keys
507 for rem_package_label, rem_model_name in removed_models:
508 if rem_package_label == package_label:
509 rem_model_state = self.from_state.models[
510 rem_package_label, rem_model_name
511 ]
512 rem_model_fields_def = self.only_relation_agnostic_fields(
513 rem_model_state.fields
514 )
515 if model_fields_def == rem_model_fields_def:
516 if self.questioner.ask_rename_model(
517 rem_model_state, model_state
518 ):
519 dependencies = []
520 fields = list(model_state.fields.values()) + [
521 field.remote_field
522 for relations in self.to_state.relations[
523 package_label, model_name
524 ].values()
525 for field in relations.values()
526 ]
527 for field in fields:
528 if field.is_relation:
529 dependencies.extend(
530 self._get_dependencies_for_foreign_key(
531 package_label,
532 model_name,
533 field,
534 self.to_state,
535 )
536 )
537 self.add_operation(
538 package_label,
539 operations.RenameModel(
540 old_name=rem_model_state.name,
541 new_name=model_state.name,
542 ),
543 dependencies=dependencies,
544 )
545 self.renamed_models[
546 package_label, model_name
547 ] = rem_model_name
548 renamed_models_rel_key = "{}.{}".format(
549 rem_model_state.package_label,
550 rem_model_state.name_lower,
551 )
552 self.renamed_models_rel[
553 renamed_models_rel_key
554 ] = f"{model_state.package_label}.{model_state.name_lower}"
555 self.old_model_keys.remove(
556 (rem_package_label, rem_model_name)
557 )
558 self.old_model_keys.add((package_label, model_name))
559 break
560
561 def generate_created_models(self):
562 """
563 Find all new models (both managed and unmanaged) and make create
564 operations for them as well as separate operations to create any
565 foreign key or M2M relationships (these are optimized later, if
566 possible).
567
568 Defer any model options that refer to collections of fields that might
569 be deferred.
570 """
571 old_keys = self.old_model_keys | self.old_unmanaged_keys
572 added_models = self.new_model_keys - old_keys
573 added_unmanaged_models = self.new_unmanaged_keys - old_keys
574 all_added_models = chain(
575 sorted(added_models, key=self.swappable_first_key, reverse=True),
576 sorted(added_unmanaged_models, key=self.swappable_first_key, reverse=True),
577 )
578 for package_label, model_name in all_added_models:
579 model_state = self.to_state.models[package_label, model_name]
580 # Gather related fields
581 related_fields = {}
582 primary_key_rel = None
583 for field_name, field in model_state.fields.items():
584 if field.remote_field:
585 if field.remote_field.model:
586 if field.primary_key:
587 primary_key_rel = field.remote_field.model
588 elif not field.remote_field.parent_link:
589 related_fields[field_name] = field
590 if getattr(field.remote_field, "through", None):
591 related_fields[field_name] = field
592
593 # Are there indexes to defer?
594 indexes = model_state.options.pop("indexes")
595 constraints = model_state.options.pop("constraints")
596 order_with_respect_to = model_state.options.pop(
597 "order_with_respect_to", None
598 )
599 # Depend on the deletion of any possible proxy version of us
600 dependencies = [
601 (package_label, model_name, None, False),
602 ]
603 # Depend on all bases
604 for base in model_state.bases:
605 if isinstance(base, str) and "." in base:
606 base_package_label, base_name = base.split(".", 1)
607 dependencies.append((base_package_label, base_name, None, True))
608 # Depend on the removal of base fields if the new model has
609 # a field with the same name.
610 old_base_model_state = self.from_state.models.get(
611 (base_package_label, base_name)
612 )
613 new_base_model_state = self.to_state.models.get(
614 (base_package_label, base_name)
615 )
616 if old_base_model_state and new_base_model_state:
617 removed_base_fields = (
618 set(old_base_model_state.fields)
619 .difference(
620 new_base_model_state.fields,
621 )
622 .intersection(model_state.fields)
623 )
624 for removed_base_field in removed_base_fields:
625 dependencies.append(
626 (
627 base_package_label,
628 base_name,
629 removed_base_field,
630 False,
631 )
632 )
633 # Depend on the other end of the primary key if it's a relation
634 if primary_key_rel:
635 dependencies.append(
636 resolve_relation(
637 primary_key_rel,
638 package_label,
639 model_name,
640 )
641 + (None, True)
642 )
643 # Generate creation operation
644 self.add_operation(
645 package_label,
646 operations.CreateModel(
647 name=model_state.name,
648 fields=[
649 d
650 for d in model_state.fields.items()
651 if d[0] not in related_fields
652 ],
653 options=model_state.options,
654 bases=model_state.bases,
655 managers=model_state.managers,
656 ),
657 dependencies=dependencies,
658 beginning=True,
659 )
660
661 # Don't add operations which modify the database for unmanaged models
662 if not model_state.options.get("managed", True):
663 continue
664
665 # Generate operations for each related field
666 for name, field in sorted(related_fields.items()):
667 dependencies = self._get_dependencies_for_foreign_key(
668 package_label,
669 model_name,
670 field,
671 self.to_state,
672 )
673 # Depend on our own model being created
674 dependencies.append((package_label, model_name, None, True))
675 # Make operation
676 self.add_operation(
677 package_label,
678 operations.AddField(
679 model_name=model_name,
680 name=name,
681 field=field,
682 ),
683 dependencies=list(set(dependencies)),
684 )
685 # Generate other opns
686 if order_with_respect_to:
687 self.add_operation(
688 package_label,
689 operations.AlterOrderWithRespectTo(
690 name=model_name,
691 order_with_respect_to=order_with_respect_to,
692 ),
693 dependencies=[
694 (package_label, model_name, order_with_respect_to, True),
695 (package_label, model_name, None, True),
696 ],
697 )
698 related_dependencies = [
699 (package_label, model_name, name, True)
700 for name in sorted(related_fields)
701 ]
702 related_dependencies.append((package_label, model_name, None, True))
703 for index in indexes:
704 self.add_operation(
705 package_label,
706 operations.AddIndex(
707 model_name=model_name,
708 index=index,
709 ),
710 dependencies=related_dependencies,
711 )
712 for constraint in constraints:
713 self.add_operation(
714 package_label,
715 operations.AddConstraint(
716 model_name=model_name,
717 constraint=constraint,
718 ),
719 dependencies=related_dependencies,
720 )
721
722 def generate_deleted_models(self):
723 """
724 Find all deleted models (managed and unmanaged) and make delete
725 operations for them as well as separate operations to delete any
726 foreign key or M2M relationships (these are optimized later, if
727 possible).
728
729 Also bring forward removal of any model options that refer to
730 collections of fields - the inverse of generate_created_models().
731 """
732 new_keys = self.new_model_keys | self.new_unmanaged_keys
733 deleted_models = self.old_model_keys - new_keys
734 deleted_unmanaged_models = self.old_unmanaged_keys - new_keys
735 all_deleted_models = chain(
736 sorted(deleted_models), sorted(deleted_unmanaged_models)
737 )
738 for package_label, model_name in all_deleted_models:
739 model_state = self.from_state.models[package_label, model_name]
740 # Gather related fields
741 related_fields = {}
742 for field_name, field in model_state.fields.items():
743 if field.remote_field:
744 if field.remote_field.model:
745 related_fields[field_name] = field
746 if getattr(field.remote_field, "through", None):
747 related_fields[field_name] = field
748
749 # Then remove each related field
750 for name in sorted(related_fields):
751 self.add_operation(
752 package_label,
753 operations.RemoveField(
754 model_name=model_name,
755 name=name,
756 ),
757 )
758 # Finally, remove the model.
759 # This depends on both the removal/alteration of all incoming fields
760 # and the removal of all its own related fields, and if it's
761 # a through model the field that references it.
762 dependencies = []
763 relations = self.from_state.relations
764 for (
765 related_object_package_label,
766 object_name,
767 ), relation_related_fields in relations[package_label, model_name].items():
768 for field_name, field in relation_related_fields.items():
769 dependencies.append(
770 (related_object_package_label, object_name, field_name, False),
771 )
772 if not field.many_to_many:
773 dependencies.append(
774 (
775 related_object_package_label,
776 object_name,
777 field_name,
778 "alter",
779 ),
780 )
781
782 for name in sorted(related_fields):
783 dependencies.append((package_label, model_name, name, False))
784 # We're referenced in another field's through=
785 through_user = self.through_users.get(
786 (package_label, model_state.name_lower)
787 )
788 if through_user:
789 dependencies.append(
790 (through_user[0], through_user[1], through_user[2], False)
791 )
792 # Finally, make the operation, deduping any dependencies
793 self.add_operation(
794 package_label,
795 operations.DeleteModel(
796 name=model_state.name,
797 ),
798 dependencies=list(set(dependencies)),
799 )
800
801 def create_renamed_fields(self):
802 """Work out renamed fields."""
803 self.renamed_operations = []
804 old_field_keys = self.old_field_keys.copy()
805 for package_label, model_name, field_name in sorted(
806 self.new_field_keys - old_field_keys
807 ):
808 old_model_name = self.renamed_models.get(
809 (package_label, model_name), model_name
810 )
811 old_model_state = self.from_state.models[package_label, old_model_name]
812 new_model_state = self.to_state.models[package_label, model_name]
813 field = new_model_state.get_field(field_name)
814 # Scan to see if this is actually a rename!
815 field_dec = self.deep_deconstruct(field)
816 for rem_package_label, rem_model_name, rem_field_name in sorted(
817 old_field_keys - self.new_field_keys
818 ):
819 if rem_package_label == package_label and rem_model_name == model_name:
820 old_field = old_model_state.get_field(rem_field_name)
821 old_field_dec = self.deep_deconstruct(old_field)
822 if (
823 field.remote_field
824 and field.remote_field.model
825 and "to" in old_field_dec[2]
826 ):
827 old_rel_to = old_field_dec[2]["to"]
828 if old_rel_to in self.renamed_models_rel:
829 old_field_dec[2]["to"] = self.renamed_models_rel[old_rel_to]
830 old_field.set_attributes_from_name(rem_field_name)
831 old_db_column = old_field.get_attname_column()[1]
832 if old_field_dec == field_dec or (
833 # Was the field renamed and db_column equal to the
834 # old field's column added?
835 old_field_dec[0:2] == field_dec[0:2]
836 and dict(old_field_dec[2], db_column=old_db_column)
837 == field_dec[2]
838 ):
839 if self.questioner.ask_rename(
840 model_name, rem_field_name, field_name, field
841 ):
842 self.renamed_operations.append(
843 (
844 rem_package_label,
845 rem_model_name,
846 old_field.db_column,
847 rem_field_name,
848 package_label,
849 model_name,
850 field,
851 field_name,
852 )
853 )
854 old_field_keys.remove(
855 (rem_package_label, rem_model_name, rem_field_name)
856 )
857 old_field_keys.add((package_label, model_name, field_name))
858 self.renamed_fields[
859 package_label, model_name, field_name
860 ] = rem_field_name
861 break
862
863 def generate_renamed_fields(self):
864 """Generate RenameField operations."""
865 for (
866 rem_package_label,
867 rem_model_name,
868 rem_db_column,
869 rem_field_name,
870 package_label,
871 model_name,
872 field,
873 field_name,
874 ) in self.renamed_operations:
875 # A db_column mismatch requires a prior noop AlterField for the
876 # subsequent RenameField to be a noop on attempts at preserving the
877 # old name.
878 if rem_db_column != field.db_column:
879 altered_field = field.clone()
880 altered_field.name = rem_field_name
881 self.add_operation(
882 package_label,
883 operations.AlterField(
884 model_name=model_name,
885 name=rem_field_name,
886 field=altered_field,
887 ),
888 )
889 self.add_operation(
890 package_label,
891 operations.RenameField(
892 model_name=model_name,
893 old_name=rem_field_name,
894 new_name=field_name,
895 ),
896 )
897 self.old_field_keys.remove(
898 (rem_package_label, rem_model_name, rem_field_name)
899 )
900 self.old_field_keys.add((package_label, model_name, field_name))
901
902 def generate_added_fields(self):
903 """Make AddField operations."""
904 for package_label, model_name, field_name in sorted(
905 self.new_field_keys - self.old_field_keys
906 ):
907 self._generate_added_field(package_label, model_name, field_name)
908
909 def _generate_added_field(self, package_label, model_name, field_name):
910 field = self.to_state.models[package_label, model_name].get_field(field_name)
911 # Adding a field always depends at least on its removal.
912 dependencies = [(package_label, model_name, field_name, False)]
913 # Fields that are foreignkeys/m2ms depend on stuff.
914 if field.remote_field and field.remote_field.model:
915 dependencies.extend(
916 self._get_dependencies_for_foreign_key(
917 package_label,
918 model_name,
919 field,
920 self.to_state,
921 )
922 )
923 # You can't just add NOT NULL fields with no default or fields
924 # which don't allow empty strings as default.
925 time_fields = (models.DateField, models.DateTimeField, models.TimeField)
926 preserve_default = (
927 field.null
928 or field.has_default()
929 or field.many_to_many
930 or (field.blank and field.empty_strings_allowed)
931 or (isinstance(field, time_fields) and field.auto_now)
932 )
933 if not preserve_default:
934 field = field.clone()
935 if isinstance(field, time_fields) and field.auto_now_add:
936 field.default = self.questioner.ask_auto_now_add_addition(
937 field_name, model_name
938 )
939 else:
940 field.default = self.questioner.ask_not_null_addition(
941 field_name, model_name
942 )
943 if (
944 field.unique
945 and field.default is not models.NOT_PROVIDED
946 and callable(field.default)
947 ):
948 self.questioner.ask_unique_callable_default_addition(field_name, model_name)
949 self.add_operation(
950 package_label,
951 operations.AddField(
952 model_name=model_name,
953 name=field_name,
954 field=field,
955 preserve_default=preserve_default,
956 ),
957 dependencies=dependencies,
958 )
959
960 def generate_removed_fields(self):
961 """Make RemoveField operations."""
962 for package_label, model_name, field_name in sorted(
963 self.old_field_keys - self.new_field_keys
964 ):
965 self._generate_removed_field(package_label, model_name, field_name)
966
967 def _generate_removed_field(self, package_label, model_name, field_name):
968 self.add_operation(
969 package_label,
970 operations.RemoveField(
971 model_name=model_name,
972 name=field_name,
973 ),
974 # We might need to depend on the removal of an
975 # order_with_respect_to or index operation;
976 # this is safely ignored if there isn't one
977 dependencies=[
978 (package_label, model_name, field_name, "order_wrt_unset"),
979 ],
980 )
981
982 def generate_altered_fields(self):
983 """
984 Make AlterField operations, or possibly RemovedField/AddField if alter
985 isn't possible.
986 """
987 for package_label, model_name, field_name in sorted(
988 self.old_field_keys & self.new_field_keys
989 ):
990 # Did the field change?
991 old_model_name = self.renamed_models.get(
992 (package_label, model_name), model_name
993 )
994 old_field_name = self.renamed_fields.get(
995 (package_label, model_name, field_name), field_name
996 )
997 old_field = self.from_state.models[package_label, old_model_name].get_field(
998 old_field_name
999 )
1000 new_field = self.to_state.models[package_label, model_name].get_field(
1001 field_name
1002 )
1003 dependencies = []
1004 # Implement any model renames on relations; these are handled by RenameModel
1005 # so we need to exclude them from the comparison
1006 if hasattr(new_field, "remote_field") and getattr(
1007 new_field.remote_field, "model", None
1008 ):
1009 rename_key = resolve_relation(
1010 new_field.remote_field.model, package_label, model_name
1011 )
1012 if rename_key in self.renamed_models:
1013 new_field.remote_field.model = old_field.remote_field.model
1014 # Handle ForeignKey which can only have a single to_field.
1015 remote_field_name = getattr(new_field.remote_field, "field_name", None)
1016 if remote_field_name:
1017 to_field_rename_key = rename_key + (remote_field_name,)
1018 if to_field_rename_key in self.renamed_fields:
1019 # Repoint both model and field name because to_field
1020 # inclusion in ForeignKey.deconstruct() is based on
1021 # both.
1022 new_field.remote_field.model = old_field.remote_field.model
1023 new_field.remote_field.field_name = (
1024 old_field.remote_field.field_name
1025 )
1026 # Handle ForeignObjects which can have multiple from_fields/to_fields.
1027 from_fields = getattr(new_field, "from_fields", None)
1028 if from_fields:
1029 from_rename_key = (package_label, model_name)
1030 new_field.from_fields = tuple(
1031 [
1032 self.renamed_fields.get(
1033 from_rename_key + (from_field,), from_field
1034 )
1035 for from_field in from_fields
1036 ]
1037 )
1038 new_field.to_fields = tuple(
1039 [
1040 self.renamed_fields.get(rename_key + (to_field,), to_field)
1041 for to_field in new_field.to_fields
1042 ]
1043 )
1044 dependencies.extend(
1045 self._get_dependencies_for_foreign_key(
1046 package_label,
1047 model_name,
1048 new_field,
1049 self.to_state,
1050 )
1051 )
1052 if hasattr(new_field, "remote_field") and getattr(
1053 new_field.remote_field, "through", None
1054 ):
1055 rename_key = resolve_relation(
1056 new_field.remote_field.through, package_label, model_name
1057 )
1058 if rename_key in self.renamed_models:
1059 new_field.remote_field.through = old_field.remote_field.through
1060 old_field_dec = self.deep_deconstruct(old_field)
1061 new_field_dec = self.deep_deconstruct(new_field)
1062 # If the field was confirmed to be renamed it means that only
1063 # db_column was allowed to change which generate_renamed_fields()
1064 # already accounts for by adding an AlterField operation.
1065 if old_field_dec != new_field_dec and old_field_name == field_name:
1066 both_m2m = old_field.many_to_many and new_field.many_to_many
1067 neither_m2m = not old_field.many_to_many and not new_field.many_to_many
1068 if both_m2m or neither_m2m:
1069 # Either both fields are m2m or neither is
1070 preserve_default = True
1071 if (
1072 old_field.null
1073 and not new_field.null
1074 and not new_field.has_default()
1075 and not new_field.many_to_many
1076 ):
1077 field = new_field.clone()
1078 new_default = self.questioner.ask_not_null_alteration(
1079 field_name, model_name
1080 )
1081 if new_default is not models.NOT_PROVIDED:
1082 field.default = new_default
1083 preserve_default = False
1084 else:
1085 field = new_field
1086 self.add_operation(
1087 package_label,
1088 operations.AlterField(
1089 model_name=model_name,
1090 name=field_name,
1091 field=field,
1092 preserve_default=preserve_default,
1093 ),
1094 dependencies=dependencies,
1095 )
1096 else:
1097 # We cannot alter between m2m and concrete fields
1098 self._generate_removed_field(package_label, model_name, field_name)
1099 self._generate_added_field(package_label, model_name, field_name)
1100
1101 def create_altered_indexes(self):
1102 option_name = operations.AddIndex.option_name
1103
1104 for package_label, model_name in sorted(self.kept_model_keys):
1105 old_model_name = self.renamed_models.get(
1106 (package_label, model_name), model_name
1107 )
1108 old_model_state = self.from_state.models[package_label, old_model_name]
1109 new_model_state = self.to_state.models[package_label, model_name]
1110
1111 old_indexes = old_model_state.options[option_name]
1112 new_indexes = new_model_state.options[option_name]
1113 added_indexes = [idx for idx in new_indexes if idx not in old_indexes]
1114 removed_indexes = [idx for idx in old_indexes if idx not in new_indexes]
1115 renamed_indexes = []
1116 # Find renamed indexes.
1117 remove_from_added = []
1118 remove_from_removed = []
1119 for new_index in added_indexes:
1120 new_index_dec = new_index.deconstruct()
1121 new_index_name = new_index_dec[2].pop("name")
1122 for old_index in removed_indexes:
1123 old_index_dec = old_index.deconstruct()
1124 old_index_name = old_index_dec[2].pop("name")
1125 # Indexes are the same except for the names.
1126 if (
1127 new_index_dec == old_index_dec
1128 and new_index_name != old_index_name
1129 ):
1130 renamed_indexes.append((old_index_name, new_index_name, None))
1131 remove_from_added.append(new_index)
1132 remove_from_removed.append(old_index)
1133
1134 # Remove renamed indexes from the lists of added and removed
1135 # indexes.
1136 added_indexes = [
1137 idx for idx in added_indexes if idx not in remove_from_added
1138 ]
1139 removed_indexes = [
1140 idx for idx in removed_indexes if idx not in remove_from_removed
1141 ]
1142
1143 self.altered_indexes.update(
1144 {
1145 (package_label, model_name): {
1146 "added_indexes": added_indexes,
1147 "removed_indexes": removed_indexes,
1148 "renamed_indexes": renamed_indexes,
1149 }
1150 }
1151 )
1152
1153 def generate_added_indexes(self):
1154 for (package_label, model_name), alt_indexes in self.altered_indexes.items():
1155 dependencies = self._get_dependencies_for_model(package_label, model_name)
1156 for index in alt_indexes["added_indexes"]:
1157 self.add_operation(
1158 package_label,
1159 operations.AddIndex(
1160 model_name=model_name,
1161 index=index,
1162 ),
1163 dependencies=dependencies,
1164 )
1165
1166 def generate_removed_indexes(self):
1167 for (package_label, model_name), alt_indexes in self.altered_indexes.items():
1168 for index in alt_indexes["removed_indexes"]:
1169 self.add_operation(
1170 package_label,
1171 operations.RemoveIndex(
1172 model_name=model_name,
1173 name=index.name,
1174 ),
1175 )
1176
1177 def generate_renamed_indexes(self):
1178 for (package_label, model_name), alt_indexes in self.altered_indexes.items():
1179 for old_index_name, new_index_name, old_fields in alt_indexes[
1180 "renamed_indexes"
1181 ]:
1182 self.add_operation(
1183 package_label,
1184 operations.RenameIndex(
1185 model_name=model_name,
1186 new_name=new_index_name,
1187 old_name=old_index_name,
1188 old_fields=old_fields,
1189 ),
1190 )
1191
1192 def create_altered_constraints(self):
1193 option_name = operations.AddConstraint.option_name
1194 for package_label, model_name in sorted(self.kept_model_keys):
1195 old_model_name = self.renamed_models.get(
1196 (package_label, model_name), model_name
1197 )
1198 old_model_state = self.from_state.models[package_label, old_model_name]
1199 new_model_state = self.to_state.models[package_label, model_name]
1200
1201 old_constraints = old_model_state.options[option_name]
1202 new_constraints = new_model_state.options[option_name]
1203 add_constraints = [c for c in new_constraints if c not in old_constraints]
1204 rem_constraints = [c for c in old_constraints if c not in new_constraints]
1205
1206 self.altered_constraints.update(
1207 {
1208 (package_label, model_name): {
1209 "added_constraints": add_constraints,
1210 "removed_constraints": rem_constraints,
1211 }
1212 }
1213 )
1214
1215 def generate_added_constraints(self):
1216 for (
1217 package_label,
1218 model_name,
1219 ), alt_constraints in self.altered_constraints.items():
1220 dependencies = self._get_dependencies_for_model(package_label, model_name)
1221 for constraint in alt_constraints["added_constraints"]:
1222 self.add_operation(
1223 package_label,
1224 operations.AddConstraint(
1225 model_name=model_name,
1226 constraint=constraint,
1227 ),
1228 dependencies=dependencies,
1229 )
1230
1231 def generate_removed_constraints(self):
1232 for (
1233 package_label,
1234 model_name,
1235 ), alt_constraints in self.altered_constraints.items():
1236 for constraint in alt_constraints["removed_constraints"]:
1237 self.add_operation(
1238 package_label,
1239 operations.RemoveConstraint(
1240 model_name=model_name,
1241 name=constraint.name,
1242 ),
1243 )
1244
1245 @staticmethod
1246 def _get_dependencies_for_foreign_key(
1247 package_label, model_name, field, project_state
1248 ):
1249 remote_field_model = None
1250 if hasattr(field.remote_field, "model"):
1251 remote_field_model = field.remote_field.model
1252 else:
1253 relations = project_state.relations[package_label, model_name]
1254 for (remote_package_label, remote_model_name), fields in relations.items():
1255 if any(
1256 field == related_field.remote_field
1257 for related_field in fields.values()
1258 ):
1259 remote_field_model = f"{remote_package_label}.{remote_model_name}"
1260 break
1261 # Account for FKs to swappable models
1262 swappable_setting = getattr(field, "swappable_setting", None)
1263 if swappable_setting is not None:
1264 dep_package_label = "__setting__"
1265 dep_object_name = swappable_setting
1266 else:
1267 dep_package_label, dep_object_name = resolve_relation(
1268 remote_field_model,
1269 package_label,
1270 model_name,
1271 )
1272 dependencies = [(dep_package_label, dep_object_name, None, True)]
1273 if getattr(field.remote_field, "through", None):
1274 through_package_label, through_object_name = resolve_relation(
1275 field.remote_field.through,
1276 package_label,
1277 model_name,
1278 )
1279 dependencies.append(
1280 (through_package_label, through_object_name, None, True)
1281 )
1282 return dependencies
1283
1284 def _get_dependencies_for_model(self, package_label, model_name):
1285 """Return foreign key dependencies of the given model."""
1286 dependencies = []
1287 model_state = self.to_state.models[package_label, model_name]
1288 for field in model_state.fields.values():
1289 if field.is_relation:
1290 dependencies.extend(
1291 self._get_dependencies_for_foreign_key(
1292 package_label,
1293 model_name,
1294 field,
1295 self.to_state,
1296 )
1297 )
1298 return dependencies
1299
1300 def _get_altered_foo_together_operations(self, option_name):
1301 for package_label, model_name in sorted(self.kept_model_keys):
1302 old_model_name = self.renamed_models.get(
1303 (package_label, model_name), model_name
1304 )
1305 old_model_state = self.from_state.models[package_label, old_model_name]
1306 new_model_state = self.to_state.models[package_label, model_name]
1307
1308 # We run the old version through the field renames to account for those
1309 old_value = old_model_state.options.get(option_name)
1310 old_value = (
1311 {
1312 tuple(
1313 self.renamed_fields.get((package_label, model_name, n), n)
1314 for n in unique
1315 )
1316 for unique in old_value
1317 }
1318 if old_value
1319 else set()
1320 )
1321
1322 new_value = new_model_state.options.get(option_name)
1323 new_value = set(new_value) if new_value else set()
1324
1325 if old_value != new_value:
1326 dependencies = []
1327 for foo_togethers in new_value:
1328 for field_name in foo_togethers:
1329 field = new_model_state.get_field(field_name)
1330 if field.remote_field and field.remote_field.model:
1331 dependencies.extend(
1332 self._get_dependencies_for_foreign_key(
1333 package_label,
1334 model_name,
1335 field,
1336 self.to_state,
1337 )
1338 )
1339 yield (
1340 old_value,
1341 new_value,
1342 package_label,
1343 model_name,
1344 dependencies,
1345 )
1346
1347 def _generate_removed_altered_foo_together(self, operation):
1348 for (
1349 old_value,
1350 new_value,
1351 package_label,
1352 model_name,
1353 dependencies,
1354 ) in self._get_altered_foo_together_operations(operation.option_name):
1355 removal_value = new_value.intersection(old_value)
1356 if removal_value or old_value:
1357 self.add_operation(
1358 package_label,
1359 operation(
1360 name=model_name, **{operation.option_name: removal_value}
1361 ),
1362 dependencies=dependencies,
1363 )
1364
1365 def generate_altered_db_table(self):
1366 models_to_check = self.kept_model_keys.union(self.kept_unmanaged_keys)
1367 for package_label, model_name in sorted(models_to_check):
1368 old_model_name = self.renamed_models.get(
1369 (package_label, model_name), model_name
1370 )
1371 old_model_state = self.from_state.models[package_label, old_model_name]
1372 new_model_state = self.to_state.models[package_label, model_name]
1373 old_db_table_name = old_model_state.options.get("db_table")
1374 new_db_table_name = new_model_state.options.get("db_table")
1375 if old_db_table_name != new_db_table_name:
1376 self.add_operation(
1377 package_label,
1378 operations.AlterModelTable(
1379 name=model_name,
1380 table=new_db_table_name,
1381 ),
1382 )
1383
1384 def generate_altered_db_table_comment(self):
1385 models_to_check = self.kept_model_keys.union(self.kept_unmanaged_keys)
1386 for package_label, model_name in sorted(models_to_check):
1387 old_model_name = self.renamed_models.get(
1388 (package_label, model_name), model_name
1389 )
1390 old_model_state = self.from_state.models[package_label, old_model_name]
1391 new_model_state = self.to_state.models[package_label, model_name]
1392
1393 old_db_table_comment = old_model_state.options.get("db_table_comment")
1394 new_db_table_comment = new_model_state.options.get("db_table_comment")
1395 if old_db_table_comment != new_db_table_comment:
1396 self.add_operation(
1397 package_label,
1398 operations.AlterModelTableComment(
1399 name=model_name,
1400 table_comment=new_db_table_comment,
1401 ),
1402 )
1403
1404 def generate_altered_options(self):
1405 """
1406 Work out if any non-schema-affecting options have changed and make an
1407 operation to represent them in state changes (in case Python code in
1408 migrations needs them).
1409 """
1410 models_to_check = self.kept_model_keys.union(
1411 self.kept_unmanaged_keys,
1412 # unmanaged converted to managed
1413 self.old_unmanaged_keys & self.new_model_keys,
1414 # managed converted to unmanaged
1415 self.old_model_keys & self.new_unmanaged_keys,
1416 )
1417
1418 for package_label, model_name in sorted(models_to_check):
1419 old_model_name = self.renamed_models.get(
1420 (package_label, model_name), model_name
1421 )
1422 old_model_state = self.from_state.models[package_label, old_model_name]
1423 new_model_state = self.to_state.models[package_label, model_name]
1424 old_options = {
1425 key: value
1426 for key, value in old_model_state.options.items()
1427 if key in AlterModelOptions.ALTER_OPTION_KEYS
1428 }
1429 new_options = {
1430 key: value
1431 for key, value in new_model_state.options.items()
1432 if key in AlterModelOptions.ALTER_OPTION_KEYS
1433 }
1434 if old_options != new_options:
1435 self.add_operation(
1436 package_label,
1437 operations.AlterModelOptions(
1438 name=model_name,
1439 options=new_options,
1440 ),
1441 )
1442
1443 def generate_altered_order_with_respect_to(self):
1444 for package_label, model_name in sorted(self.kept_model_keys):
1445 old_model_name = self.renamed_models.get(
1446 (package_label, model_name), model_name
1447 )
1448 old_model_state = self.from_state.models[package_label, old_model_name]
1449 new_model_state = self.to_state.models[package_label, model_name]
1450 if old_model_state.options.get(
1451 "order_with_respect_to"
1452 ) != new_model_state.options.get("order_with_respect_to"):
1453 # Make sure it comes second if we're adding
1454 # (removal dependency is part of RemoveField)
1455 dependencies = []
1456 if new_model_state.options.get("order_with_respect_to"):
1457 dependencies.append(
1458 (
1459 package_label,
1460 model_name,
1461 new_model_state.options["order_with_respect_to"],
1462 True,
1463 )
1464 )
1465 # Actually generate the operation
1466 self.add_operation(
1467 package_label,
1468 operations.AlterOrderWithRespectTo(
1469 name=model_name,
1470 order_with_respect_to=new_model_state.options.get(
1471 "order_with_respect_to"
1472 ),
1473 ),
1474 dependencies=dependencies,
1475 )
1476
1477 def generate_altered_managers(self):
1478 for package_label, model_name in sorted(self.kept_model_keys):
1479 old_model_name = self.renamed_models.get(
1480 (package_label, model_name), model_name
1481 )
1482 old_model_state = self.from_state.models[package_label, old_model_name]
1483 new_model_state = self.to_state.models[package_label, model_name]
1484 if old_model_state.managers != new_model_state.managers:
1485 self.add_operation(
1486 package_label,
1487 operations.AlterModelManagers(
1488 name=model_name,
1489 managers=new_model_state.managers,
1490 ),
1491 )
1492
1493 def arrange_for_graph(self, changes, graph, migration_name=None):
1494 """
1495 Take a result from changes() and a MigrationGraph, and fix the names
1496 and dependencies of the changes so they extend the graph from the leaf
1497 nodes for each app.
1498 """
1499 leaves = graph.leaf_nodes()
1500 name_map = {}
1501 for package_label, migrations in list(changes.items()):
1502 if not migrations:
1503 continue
1504 # Find the app label's current leaf node
1505 app_leaf = None
1506 for leaf in leaves:
1507 if leaf[0] == package_label:
1508 app_leaf = leaf
1509 break
1510 # Do they want an initial migration for this app?
1511 if app_leaf is None and not self.questioner.ask_initial(package_label):
1512 # They don't.
1513 for migration in migrations:
1514 name_map[(package_label, migration.name)] = (
1515 package_label,
1516 "__first__",
1517 )
1518 del changes[package_label]
1519 continue
1520 # Work out the next number in the sequence
1521 if app_leaf is None:
1522 next_number = 1
1523 else:
1524 next_number = (self.parse_number(app_leaf[1]) or 0) + 1
1525 # Name each migration
1526 for i, migration in enumerate(migrations):
1527 if i == 0 and app_leaf:
1528 migration.dependencies.append(app_leaf)
1529 new_name_parts = ["%04i" % next_number]
1530 if migration_name:
1531 new_name_parts.append(migration_name)
1532 elif i == 0 and not app_leaf:
1533 new_name_parts.append("initial")
1534 else:
1535 new_name_parts.append(migration.suggest_name()[:100])
1536 new_name = "_".join(new_name_parts)
1537 name_map[(package_label, migration.name)] = (package_label, new_name)
1538 next_number += 1
1539 migration.name = new_name
1540 # Now fix dependencies
1541 for migrations in changes.values():
1542 for migration in migrations:
1543 migration.dependencies = [
1544 name_map.get(d, d) for d in migration.dependencies
1545 ]
1546 return changes
1547
1548 def _trim_to_packages(self, changes, package_labels):
1549 """
1550 Take changes from arrange_for_graph() and set of app labels, and return
1551 a modified set of changes which trims out as many migrations that are
1552 not in package_labels as possible. Note that some other migrations may
1553 still be present as they may be required dependencies.
1554 """
1555 # Gather other app dependencies in a first pass
1556 app_dependencies = {}
1557 for package_label, migrations in changes.items():
1558 for migration in migrations:
1559 for dep_package_label, name in migration.dependencies:
1560 app_dependencies.setdefault(package_label, set()).add(
1561 dep_package_label
1562 )
1563 required_packages = set(package_labels)
1564 # Keep resolving till there's no change
1565 old_required_packages = None
1566 while old_required_packages != required_packages:
1567 old_required_packages = set(required_packages)
1568 required_packages.update(
1569 *[
1570 app_dependencies.get(package_label, ())
1571 for package_label in required_packages
1572 ]
1573 )
1574 # Remove all migrations that aren't needed
1575 for package_label in list(changes):
1576 if package_label not in required_packages:
1577 del changes[package_label]
1578 return changes
1579
1580 @classmethod
1581 def parse_number(cls, name):
1582 """
1583 Given a migration name, try to extract a number from the beginning of
1584 it. For a squashed migration such as '0001_squashed_0004…', return the
1585 second number. If no number is found, return None.
1586 """
1587 if squashed_match := re.search(r".*_squashed_(\d+)", name):
1588 return int(squashed_match[1])
1589 match = re.match(r"^\d+", name)
1590 if match:
1591 return int(match[0])
1592 return None