Coverage for /Users/davegaeddert/Developer/dropseed/plain/plain-models/plain/models/migrations/autodetector.py: 9%
640 statements
« prev ^ index » next coverage.py v7.6.9, created at 2024-12-23 11:16 -0600
« prev ^ index » next coverage.py v7.6.9, created at 2024-12-23 11:16 -0600
1import functools
2import re
3from graphlib import TopologicalSorter
4from itertools import chain
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
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).
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 """
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}
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
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
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
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.
111 The resulting migrations aren't specially named, but the names
112 do matter for dependencies inside the set.
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)
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 = {}
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))
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))
148 self.from_state.resolve_fields_and_relations()
149 self.to_state.resolve_fields_and_relations()
151 # Renames have to come first
152 self.generate_renamed_models()
154 # Prepare lists of fields and generate through model map
155 self._prepare_field_lists()
156 self._generate_through_model_map()
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()
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()
190 self._sort_migrations()
191 self._build_migration_list(graph)
192 self._optimize_migrations()
194 return self.migrations
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 }
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 )
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
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 f"Cannot resolve operation dependencies: {self.generated_operations!r}"
365 )
366 num_ops = new_num_ops
368 def _sort_migrations(self):
369 """
370 Reorder to make things possible. Reordering may be needed so FKs work
371 nicely inside the same app.
372 """
373 for package_label, ops in sorted(self.generated_operations.items()):
374 ts = TopologicalSorter()
375 for op in ops:
376 ts.add(op)
377 for dep in op._auto_deps:
378 # Resolve intra-app dependencies to handle circular
379 # references involving a swappable model.
380 dep = self._resolve_dependency(dep)[0]
381 if dep[0] != package_label:
382 continue
383 ts.add(op, *(x for x in ops if self.check_dependency(x, dep)))
384 self.generated_operations[package_label] = list(ts.static_order())
386 def _optimize_migrations(self):
387 # Add in internal dependencies among the migrations
388 for package_label, migrations in self.migrations.items():
389 for m1, m2 in zip(migrations, migrations[1:]):
390 m2.dependencies.append((package_label, m1.name))
392 # De-dupe dependencies
393 for migrations in self.migrations.values():
394 for migration in migrations:
395 migration.dependencies = list(set(migration.dependencies))
397 # Optimize migrations
398 for package_label, migrations in self.migrations.items():
399 for migration in migrations:
400 migration.operations = MigrationOptimizer().optimize(
401 migration.operations, package_label
402 )
404 def check_dependency(self, operation, dependency):
405 """
406 Return True if the given operation depends on the given dependency,
407 False otherwise.
408 """
409 # Created model
410 if dependency[2] is None and dependency[3] is True:
411 return (
412 isinstance(operation, operations.CreateModel)
413 and operation.name_lower == dependency[1].lower()
414 )
415 # Created field
416 elif dependency[2] is not None and dependency[3] is True:
417 return (
418 isinstance(operation, operations.CreateModel)
419 and operation.name_lower == dependency[1].lower()
420 and any(dependency[2] == x for x, y in operation.fields)
421 ) or (
422 isinstance(operation, operations.AddField)
423 and operation.model_name_lower == dependency[1].lower()
424 and operation.name_lower == dependency[2].lower()
425 )
426 # Removed field
427 elif dependency[2] is not None and dependency[3] is False:
428 return (
429 isinstance(operation, operations.RemoveField)
430 and operation.model_name_lower == dependency[1].lower()
431 and operation.name_lower == dependency[2].lower()
432 )
433 # Removed model
434 elif dependency[2] is None and dependency[3] is False:
435 return (
436 isinstance(operation, operations.DeleteModel)
437 and operation.name_lower == dependency[1].lower()
438 )
439 # Field being altered
440 elif dependency[2] is not None and dependency[3] == "alter":
441 return (
442 isinstance(operation, operations.AlterField)
443 and operation.model_name_lower == dependency[1].lower()
444 and operation.name_lower == dependency[2].lower()
445 )
446 # order_with_respect_to being unset for a field
447 elif dependency[2] is not None and dependency[3] == "order_wrt_unset":
448 return (
449 isinstance(operation, operations.AlterOrderWithRespectTo)
450 and operation.name_lower == dependency[1].lower()
451 and (operation.order_with_respect_to or "").lower()
452 != dependency[2].lower()
453 )
454 # Unknown dependency. Raise an error.
455 else:
456 raise ValueError(f"Can't handle dependency {dependency!r}")
458 def add_operation(
459 self, package_label, operation, dependencies=None, beginning=False
460 ):
461 # Dependencies are
462 # (package_label, model_name, field_name, create/delete as True/False)
463 operation._auto_deps = dependencies or []
464 if beginning:
465 self.generated_operations.setdefault(package_label, []).insert(0, operation)
466 else:
467 self.generated_operations.setdefault(package_label, []).append(operation)
469 def swappable_first_key(self, item):
470 """
471 Place potential swappable models first in lists of created models (only
472 real way to solve #22783).
473 """
474 try:
475 model_state = self.to_state.models[item]
476 base_names = {
477 base if isinstance(base, str) else base.__name__
478 for base in model_state.bases
479 }
480 string_version = f"{item[0]}.{item[1]}"
481 if (
482 model_state.options.get("swappable")
483 or "BaseUser" in base_names
484 or "AbstractBaseUser" in base_names
485 or settings.AUTH_USER_MODEL.lower() == string_version.lower()
486 ):
487 return ("___" + item[0], "___" + item[1])
488 except LookupError:
489 pass
490 return item
492 def generate_renamed_models(self):
493 """
494 Find any renamed models, generate the operations for them, and remove
495 the old entry from the model lists. Must be run before other
496 model-level generation.
497 """
498 self.renamed_models = {}
499 self.renamed_models_rel = {}
500 added_models = self.new_model_keys - self.old_model_keys
501 for package_label, model_name in sorted(added_models):
502 model_state = self.to_state.models[package_label, model_name]
503 model_fields_def = self.only_relation_agnostic_fields(model_state.fields)
505 removed_models = self.old_model_keys - self.new_model_keys
506 for rem_package_label, rem_model_name in removed_models:
507 if rem_package_label == package_label:
508 rem_model_state = self.from_state.models[
509 rem_package_label, rem_model_name
510 ]
511 rem_model_fields_def = self.only_relation_agnostic_fields(
512 rem_model_state.fields
513 )
514 if model_fields_def == rem_model_fields_def:
515 if self.questioner.ask_rename_model(
516 rem_model_state, model_state
517 ):
518 dependencies = []
519 fields = list(model_state.fields.values()) + [
520 field.remote_field
521 for relations in self.to_state.relations[
522 package_label, model_name
523 ].values()
524 for field in relations.values()
525 ]
526 for field in fields:
527 if field.is_relation:
528 dependencies.extend(
529 self._get_dependencies_for_foreign_key(
530 package_label,
531 model_name,
532 field,
533 self.to_state,
534 )
535 )
536 self.add_operation(
537 package_label,
538 operations.RenameModel(
539 old_name=rem_model_state.name,
540 new_name=model_state.name,
541 ),
542 dependencies=dependencies,
543 )
544 self.renamed_models[package_label, model_name] = (
545 rem_model_name
546 )
547 renamed_models_rel_key = f"{rem_model_state.package_label}.{rem_model_state.name_lower}"
548 self.renamed_models_rel[renamed_models_rel_key] = (
549 f"{model_state.package_label}.{model_state.name_lower}"
550 )
551 self.old_model_keys.remove(
552 (rem_package_label, rem_model_name)
553 )
554 self.old_model_keys.add((package_label, model_name))
555 break
557 def generate_created_models(self):
558 """
559 Find all new models (both managed and unmanaged) and make create
560 operations for them as well as separate operations to create any
561 foreign key or M2M relationships (these are optimized later, if
562 possible).
564 Defer any model options that refer to collections of fields that might
565 be deferred.
566 """
567 old_keys = self.old_model_keys | self.old_unmanaged_keys
568 added_models = self.new_model_keys - old_keys
569 added_unmanaged_models = self.new_unmanaged_keys - old_keys
570 all_added_models = chain(
571 sorted(added_models, key=self.swappable_first_key, reverse=True),
572 sorted(added_unmanaged_models, key=self.swappable_first_key, reverse=True),
573 )
574 for package_label, model_name in all_added_models:
575 model_state = self.to_state.models[package_label, model_name]
576 # Gather related fields
577 related_fields = {}
578 primary_key_rel = None
579 for field_name, field in model_state.fields.items():
580 if field.remote_field:
581 if field.remote_field.model:
582 if field.primary_key:
583 primary_key_rel = field.remote_field.model
584 elif not field.remote_field.parent_link:
585 related_fields[field_name] = field
586 if getattr(field.remote_field, "through", None):
587 related_fields[field_name] = field
589 # Are there indexes to defer?
590 indexes = model_state.options.pop("indexes")
591 constraints = model_state.options.pop("constraints")
592 order_with_respect_to = model_state.options.pop(
593 "order_with_respect_to", None
594 )
595 # Depend on the deletion of any possible proxy version of us
596 dependencies = [
597 (package_label, model_name, None, False),
598 ]
599 # Depend on all bases
600 for base in model_state.bases:
601 if isinstance(base, str) and "." in base:
602 base_package_label, base_name = base.split(".", 1)
603 dependencies.append((base_package_label, base_name, None, True))
604 # Depend on the removal of base fields if the new model has
605 # a field with the same name.
606 old_base_model_state = self.from_state.models.get(
607 (base_package_label, base_name)
608 )
609 new_base_model_state = self.to_state.models.get(
610 (base_package_label, base_name)
611 )
612 if old_base_model_state and new_base_model_state:
613 removed_base_fields = (
614 set(old_base_model_state.fields)
615 .difference(
616 new_base_model_state.fields,
617 )
618 .intersection(model_state.fields)
619 )
620 for removed_base_field in removed_base_fields:
621 dependencies.append(
622 (
623 base_package_label,
624 base_name,
625 removed_base_field,
626 False,
627 )
628 )
629 # Depend on the other end of the primary key if it's a relation
630 if primary_key_rel:
631 dependencies.append(
632 resolve_relation(
633 primary_key_rel,
634 package_label,
635 model_name,
636 )
637 + (None, True)
638 )
639 # Generate creation operation
640 self.add_operation(
641 package_label,
642 operations.CreateModel(
643 name=model_state.name,
644 fields=[
645 d
646 for d in model_state.fields.items()
647 if d[0] not in related_fields
648 ],
649 options=model_state.options,
650 bases=model_state.bases,
651 managers=model_state.managers,
652 ),
653 dependencies=dependencies,
654 beginning=True,
655 )
657 # Don't add operations which modify the database for unmanaged models
658 if not model_state.options.get("managed", True):
659 continue
661 # Generate operations for each related field
662 for name, field in sorted(related_fields.items()):
663 dependencies = self._get_dependencies_for_foreign_key(
664 package_label,
665 model_name,
666 field,
667 self.to_state,
668 )
669 # Depend on our own model being created
670 dependencies.append((package_label, model_name, None, True))
671 # Make operation
672 self.add_operation(
673 package_label,
674 operations.AddField(
675 model_name=model_name,
676 name=name,
677 field=field,
678 ),
679 dependencies=list(set(dependencies)),
680 )
681 # Generate other opns
682 if order_with_respect_to:
683 self.add_operation(
684 package_label,
685 operations.AlterOrderWithRespectTo(
686 name=model_name,
687 order_with_respect_to=order_with_respect_to,
688 ),
689 dependencies=[
690 (package_label, model_name, order_with_respect_to, True),
691 (package_label, model_name, None, True),
692 ],
693 )
694 related_dependencies = [
695 (package_label, model_name, name, True)
696 for name in sorted(related_fields)
697 ]
698 related_dependencies.append((package_label, model_name, None, True))
699 for index in indexes:
700 self.add_operation(
701 package_label,
702 operations.AddIndex(
703 model_name=model_name,
704 index=index,
705 ),
706 dependencies=related_dependencies,
707 )
708 for constraint in constraints:
709 self.add_operation(
710 package_label,
711 operations.AddConstraint(
712 model_name=model_name,
713 constraint=constraint,
714 ),
715 dependencies=related_dependencies,
716 )
718 def generate_deleted_models(self):
719 """
720 Find all deleted models (managed and unmanaged) and make delete
721 operations for them as well as separate operations to delete any
722 foreign key or M2M relationships (these are optimized later, if
723 possible).
725 Also bring forward removal of any model options that refer to
726 collections of fields - the inverse of generate_created_models().
727 """
728 new_keys = self.new_model_keys | self.new_unmanaged_keys
729 deleted_models = self.old_model_keys - new_keys
730 deleted_unmanaged_models = self.old_unmanaged_keys - new_keys
731 all_deleted_models = chain(
732 sorted(deleted_models), sorted(deleted_unmanaged_models)
733 )
734 for package_label, model_name in all_deleted_models:
735 model_state = self.from_state.models[package_label, model_name]
736 # Gather related fields
737 related_fields = {}
738 for field_name, field in model_state.fields.items():
739 if field.remote_field:
740 if field.remote_field.model:
741 related_fields[field_name] = field
742 if getattr(field.remote_field, "through", None):
743 related_fields[field_name] = field
745 # Then remove each related field
746 for name in sorted(related_fields):
747 self.add_operation(
748 package_label,
749 operations.RemoveField(
750 model_name=model_name,
751 name=name,
752 ),
753 )
754 # Finally, remove the model.
755 # This depends on both the removal/alteration of all incoming fields
756 # and the removal of all its own related fields, and if it's
757 # a through model the field that references it.
758 dependencies = []
759 relations = self.from_state.relations
760 for (
761 related_object_package_label,
762 object_name,
763 ), relation_related_fields in relations[package_label, model_name].items():
764 for field_name, field in relation_related_fields.items():
765 dependencies.append(
766 (related_object_package_label, object_name, field_name, False),
767 )
768 if not field.many_to_many:
769 dependencies.append(
770 (
771 related_object_package_label,
772 object_name,
773 field_name,
774 "alter",
775 ),
776 )
778 for name in sorted(related_fields):
779 dependencies.append((package_label, model_name, name, False))
780 # We're referenced in another field's through=
781 through_user = self.through_users.get(
782 (package_label, model_state.name_lower)
783 )
784 if through_user:
785 dependencies.append(
786 (through_user[0], through_user[1], through_user[2], False)
787 )
788 # Finally, make the operation, deduping any dependencies
789 self.add_operation(
790 package_label,
791 operations.DeleteModel(
792 name=model_state.name,
793 ),
794 dependencies=list(set(dependencies)),
795 )
797 def create_renamed_fields(self):
798 """Work out renamed fields."""
799 self.renamed_operations = []
800 old_field_keys = self.old_field_keys.copy()
801 for package_label, model_name, field_name in sorted(
802 self.new_field_keys - old_field_keys
803 ):
804 old_model_name = self.renamed_models.get(
805 (package_label, model_name), model_name
806 )
807 old_model_state = self.from_state.models[package_label, old_model_name]
808 new_model_state = self.to_state.models[package_label, model_name]
809 field = new_model_state.get_field(field_name)
810 # Scan to see if this is actually a rename!
811 field_dec = self.deep_deconstruct(field)
812 for rem_package_label, rem_model_name, rem_field_name in sorted(
813 old_field_keys - self.new_field_keys
814 ):
815 if rem_package_label == package_label and rem_model_name == model_name:
816 old_field = old_model_state.get_field(rem_field_name)
817 old_field_dec = self.deep_deconstruct(old_field)
818 if (
819 field.remote_field
820 and field.remote_field.model
821 and "to" in old_field_dec[2]
822 ):
823 old_rel_to = old_field_dec[2]["to"]
824 if old_rel_to in self.renamed_models_rel:
825 old_field_dec[2]["to"] = self.renamed_models_rel[old_rel_to]
826 old_field.set_attributes_from_name(rem_field_name)
827 old_db_column = old_field.get_attname_column()[1]
828 if old_field_dec == field_dec or (
829 # Was the field renamed and db_column equal to the
830 # old field's column added?
831 old_field_dec[0:2] == field_dec[0:2]
832 and dict(old_field_dec[2], db_column=old_db_column)
833 == field_dec[2]
834 ):
835 if self.questioner.ask_rename(
836 model_name, rem_field_name, field_name, field
837 ):
838 self.renamed_operations.append(
839 (
840 rem_package_label,
841 rem_model_name,
842 old_field.db_column,
843 rem_field_name,
844 package_label,
845 model_name,
846 field,
847 field_name,
848 )
849 )
850 old_field_keys.remove(
851 (rem_package_label, rem_model_name, rem_field_name)
852 )
853 old_field_keys.add((package_label, model_name, field_name))
854 self.renamed_fields[
855 package_label, model_name, field_name
856 ] = rem_field_name
857 break
859 def generate_renamed_fields(self):
860 """Generate RenameField operations."""
861 for (
862 rem_package_label,
863 rem_model_name,
864 rem_db_column,
865 rem_field_name,
866 package_label,
867 model_name,
868 field,
869 field_name,
870 ) in self.renamed_operations:
871 # A db_column mismatch requires a prior noop AlterField for the
872 # subsequent RenameField to be a noop on attempts at preserving the
873 # old name.
874 if rem_db_column != field.db_column:
875 altered_field = field.clone()
876 altered_field.name = rem_field_name
877 self.add_operation(
878 package_label,
879 operations.AlterField(
880 model_name=model_name,
881 name=rem_field_name,
882 field=altered_field,
883 ),
884 )
885 self.add_operation(
886 package_label,
887 operations.RenameField(
888 model_name=model_name,
889 old_name=rem_field_name,
890 new_name=field_name,
891 ),
892 )
893 self.old_field_keys.remove(
894 (rem_package_label, rem_model_name, rem_field_name)
895 )
896 self.old_field_keys.add((package_label, model_name, field_name))
898 def generate_added_fields(self):
899 """Make AddField operations."""
900 for package_label, model_name, field_name in sorted(
901 self.new_field_keys - self.old_field_keys
902 ):
903 self._generate_added_field(package_label, model_name, field_name)
905 def _generate_added_field(self, package_label, model_name, field_name):
906 field = self.to_state.models[package_label, model_name].get_field(field_name)
907 # Adding a field always depends at least on its removal.
908 dependencies = [(package_label, model_name, field_name, False)]
909 # Fields that are foreignkeys/m2ms depend on stuff.
910 if field.remote_field and field.remote_field.model:
911 dependencies.extend(
912 self._get_dependencies_for_foreign_key(
913 package_label,
914 model_name,
915 field,
916 self.to_state,
917 )
918 )
919 # You can't just add NOT NULL fields with no default or fields
920 # which don't allow empty strings as default.
921 time_fields = (models.DateField, models.DateTimeField, models.TimeField)
922 preserve_default = (
923 field.null
924 or field.has_default()
925 or field.many_to_many
926 or (field.blank and field.empty_strings_allowed)
927 or (isinstance(field, time_fields) and field.auto_now)
928 )
929 if not preserve_default:
930 field = field.clone()
931 if isinstance(field, time_fields) and field.auto_now_add:
932 field.default = self.questioner.ask_auto_now_add_addition(
933 field_name, model_name
934 )
935 else:
936 field.default = self.questioner.ask_not_null_addition(
937 field_name, model_name
938 )
939 if (
940 field.unique
941 and field.default is not models.NOT_PROVIDED
942 and callable(field.default)
943 ):
944 self.questioner.ask_unique_callable_default_addition(field_name, model_name)
945 self.add_operation(
946 package_label,
947 operations.AddField(
948 model_name=model_name,
949 name=field_name,
950 field=field,
951 preserve_default=preserve_default,
952 ),
953 dependencies=dependencies,
954 )
956 def generate_removed_fields(self):
957 """Make RemoveField operations."""
958 for package_label, model_name, field_name in sorted(
959 self.old_field_keys - self.new_field_keys
960 ):
961 self._generate_removed_field(package_label, model_name, field_name)
963 def _generate_removed_field(self, package_label, model_name, field_name):
964 self.add_operation(
965 package_label,
966 operations.RemoveField(
967 model_name=model_name,
968 name=field_name,
969 ),
970 # We might need to depend on the removal of an
971 # order_with_respect_to or index operation;
972 # this is safely ignored if there isn't one
973 dependencies=[
974 (package_label, model_name, field_name, "order_wrt_unset"),
975 ],
976 )
978 def generate_altered_fields(self):
979 """
980 Make AlterField operations, or possibly RemovedField/AddField if alter
981 isn't possible.
982 """
983 for package_label, model_name, field_name in sorted(
984 self.old_field_keys & self.new_field_keys
985 ):
986 # Did the field change?
987 old_model_name = self.renamed_models.get(
988 (package_label, model_name), model_name
989 )
990 old_field_name = self.renamed_fields.get(
991 (package_label, model_name, field_name), field_name
992 )
993 old_field = self.from_state.models[package_label, old_model_name].get_field(
994 old_field_name
995 )
996 new_field = self.to_state.models[package_label, model_name].get_field(
997 field_name
998 )
999 dependencies = []
1000 # Implement any model renames on relations; these are handled by RenameModel
1001 # so we need to exclude them from the comparison
1002 if hasattr(new_field, "remote_field") and getattr(
1003 new_field.remote_field, "model", None
1004 ):
1005 rename_key = resolve_relation(
1006 new_field.remote_field.model, package_label, model_name
1007 )
1008 if rename_key in self.renamed_models:
1009 new_field.remote_field.model = old_field.remote_field.model
1010 # Handle ForeignKey which can only have a single to_field.
1011 remote_field_name = getattr(new_field.remote_field, "field_name", None)
1012 if remote_field_name:
1013 to_field_rename_key = rename_key + (remote_field_name,)
1014 if to_field_rename_key in self.renamed_fields:
1015 # Repoint both model and field name because to_field
1016 # inclusion in ForeignKey.deconstruct() is based on
1017 # both.
1018 new_field.remote_field.model = old_field.remote_field.model
1019 new_field.remote_field.field_name = (
1020 old_field.remote_field.field_name
1021 )
1022 # Handle ForeignObjects which can have multiple from_fields/to_fields.
1023 from_fields = getattr(new_field, "from_fields", None)
1024 if from_fields:
1025 from_rename_key = (package_label, model_name)
1026 new_field.from_fields = tuple(
1027 [
1028 self.renamed_fields.get(
1029 from_rename_key + (from_field,), from_field
1030 )
1031 for from_field in from_fields
1032 ]
1033 )
1034 new_field.to_fields = tuple(
1035 [
1036 self.renamed_fields.get(rename_key + (to_field,), to_field)
1037 for to_field in new_field.to_fields
1038 ]
1039 )
1040 dependencies.extend(
1041 self._get_dependencies_for_foreign_key(
1042 package_label,
1043 model_name,
1044 new_field,
1045 self.to_state,
1046 )
1047 )
1048 if hasattr(new_field, "remote_field") and getattr(
1049 new_field.remote_field, "through", None
1050 ):
1051 rename_key = resolve_relation(
1052 new_field.remote_field.through, package_label, model_name
1053 )
1054 if rename_key in self.renamed_models:
1055 new_field.remote_field.through = old_field.remote_field.through
1056 old_field_dec = self.deep_deconstruct(old_field)
1057 new_field_dec = self.deep_deconstruct(new_field)
1058 # If the field was confirmed to be renamed it means that only
1059 # db_column was allowed to change which generate_renamed_fields()
1060 # already accounts for by adding an AlterField operation.
1061 if old_field_dec != new_field_dec and old_field_name == field_name:
1062 both_m2m = old_field.many_to_many and new_field.many_to_many
1063 neither_m2m = not old_field.many_to_many and not new_field.many_to_many
1064 if both_m2m or neither_m2m:
1065 # Either both fields are m2m or neither is
1066 preserve_default = True
1067 if (
1068 old_field.null
1069 and not new_field.null
1070 and not new_field.has_default()
1071 and not new_field.many_to_many
1072 ):
1073 field = new_field.clone()
1074 new_default = self.questioner.ask_not_null_alteration(
1075 field_name, model_name
1076 )
1077 if new_default is not models.NOT_PROVIDED:
1078 field.default = new_default
1079 preserve_default = False
1080 else:
1081 field = new_field
1082 self.add_operation(
1083 package_label,
1084 operations.AlterField(
1085 model_name=model_name,
1086 name=field_name,
1087 field=field,
1088 preserve_default=preserve_default,
1089 ),
1090 dependencies=dependencies,
1091 )
1092 else:
1093 # We cannot alter between m2m and concrete fields
1094 self._generate_removed_field(package_label, model_name, field_name)
1095 self._generate_added_field(package_label, model_name, field_name)
1097 def create_altered_indexes(self):
1098 option_name = operations.AddIndex.option_name
1100 for package_label, model_name in sorted(self.kept_model_keys):
1101 old_model_name = self.renamed_models.get(
1102 (package_label, model_name), model_name
1103 )
1104 old_model_state = self.from_state.models[package_label, old_model_name]
1105 new_model_state = self.to_state.models[package_label, model_name]
1107 old_indexes = old_model_state.options[option_name]
1108 new_indexes = new_model_state.options[option_name]
1109 added_indexes = [idx for idx in new_indexes if idx not in old_indexes]
1110 removed_indexes = [idx for idx in old_indexes if idx not in new_indexes]
1111 renamed_indexes = []
1112 # Find renamed indexes.
1113 remove_from_added = []
1114 remove_from_removed = []
1115 for new_index in added_indexes:
1116 new_index_dec = new_index.deconstruct()
1117 new_index_name = new_index_dec[2].pop("name")
1118 for old_index in removed_indexes:
1119 old_index_dec = old_index.deconstruct()
1120 old_index_name = old_index_dec[2].pop("name")
1121 # Indexes are the same except for the names.
1122 if (
1123 new_index_dec == old_index_dec
1124 and new_index_name != old_index_name
1125 ):
1126 renamed_indexes.append((old_index_name, new_index_name, None))
1127 remove_from_added.append(new_index)
1128 remove_from_removed.append(old_index)
1130 # Remove renamed indexes from the lists of added and removed
1131 # indexes.
1132 added_indexes = [
1133 idx for idx in added_indexes if idx not in remove_from_added
1134 ]
1135 removed_indexes = [
1136 idx for idx in removed_indexes if idx not in remove_from_removed
1137 ]
1139 self.altered_indexes.update(
1140 {
1141 (package_label, model_name): {
1142 "added_indexes": added_indexes,
1143 "removed_indexes": removed_indexes,
1144 "renamed_indexes": renamed_indexes,
1145 }
1146 }
1147 )
1149 def generate_added_indexes(self):
1150 for (package_label, model_name), alt_indexes in self.altered_indexes.items():
1151 dependencies = self._get_dependencies_for_model(package_label, model_name)
1152 for index in alt_indexes["added_indexes"]:
1153 self.add_operation(
1154 package_label,
1155 operations.AddIndex(
1156 model_name=model_name,
1157 index=index,
1158 ),
1159 dependencies=dependencies,
1160 )
1162 def generate_removed_indexes(self):
1163 for (package_label, model_name), alt_indexes in self.altered_indexes.items():
1164 for index in alt_indexes["removed_indexes"]:
1165 self.add_operation(
1166 package_label,
1167 operations.RemoveIndex(
1168 model_name=model_name,
1169 name=index.name,
1170 ),
1171 )
1173 def generate_renamed_indexes(self):
1174 for (package_label, model_name), alt_indexes in self.altered_indexes.items():
1175 for old_index_name, new_index_name, old_fields in alt_indexes[
1176 "renamed_indexes"
1177 ]:
1178 self.add_operation(
1179 package_label,
1180 operations.RenameIndex(
1181 model_name=model_name,
1182 new_name=new_index_name,
1183 old_name=old_index_name,
1184 old_fields=old_fields,
1185 ),
1186 )
1188 def create_altered_constraints(self):
1189 option_name = operations.AddConstraint.option_name
1190 for package_label, model_name in sorted(self.kept_model_keys):
1191 old_model_name = self.renamed_models.get(
1192 (package_label, model_name), model_name
1193 )
1194 old_model_state = self.from_state.models[package_label, old_model_name]
1195 new_model_state = self.to_state.models[package_label, model_name]
1197 old_constraints = old_model_state.options[option_name]
1198 new_constraints = new_model_state.options[option_name]
1199 add_constraints = [c for c in new_constraints if c not in old_constraints]
1200 rem_constraints = [c for c in old_constraints if c not in new_constraints]
1202 self.altered_constraints.update(
1203 {
1204 (package_label, model_name): {
1205 "added_constraints": add_constraints,
1206 "removed_constraints": rem_constraints,
1207 }
1208 }
1209 )
1211 def generate_added_constraints(self):
1212 for (
1213 package_label,
1214 model_name,
1215 ), alt_constraints in self.altered_constraints.items():
1216 dependencies = self._get_dependencies_for_model(package_label, model_name)
1217 for constraint in alt_constraints["added_constraints"]:
1218 self.add_operation(
1219 package_label,
1220 operations.AddConstraint(
1221 model_name=model_name,
1222 constraint=constraint,
1223 ),
1224 dependencies=dependencies,
1225 )
1227 def generate_removed_constraints(self):
1228 for (
1229 package_label,
1230 model_name,
1231 ), alt_constraints in self.altered_constraints.items():
1232 for constraint in alt_constraints["removed_constraints"]:
1233 self.add_operation(
1234 package_label,
1235 operations.RemoveConstraint(
1236 model_name=model_name,
1237 name=constraint.name,
1238 ),
1239 )
1241 @staticmethod
1242 def _get_dependencies_for_foreign_key(
1243 package_label, model_name, field, project_state
1244 ):
1245 remote_field_model = None
1246 if hasattr(field.remote_field, "model"):
1247 remote_field_model = field.remote_field.model
1248 else:
1249 relations = project_state.relations[package_label, model_name]
1250 for (remote_package_label, remote_model_name), fields in relations.items():
1251 if any(
1252 field == related_field.remote_field
1253 for related_field in fields.values()
1254 ):
1255 remote_field_model = f"{remote_package_label}.{remote_model_name}"
1256 break
1257 # Account for FKs to swappable models
1258 swappable_setting = getattr(field, "swappable_setting", None)
1259 if swappable_setting is not None:
1260 dep_package_label = "__setting__"
1261 dep_object_name = swappable_setting
1262 else:
1263 dep_package_label, dep_object_name = resolve_relation(
1264 remote_field_model,
1265 package_label,
1266 model_name,
1267 )
1268 dependencies = [(dep_package_label, dep_object_name, None, True)]
1269 if getattr(field.remote_field, "through", None):
1270 through_package_label, through_object_name = resolve_relation(
1271 field.remote_field.through,
1272 package_label,
1273 model_name,
1274 )
1275 dependencies.append(
1276 (through_package_label, through_object_name, None, True)
1277 )
1278 return dependencies
1280 def _get_dependencies_for_model(self, package_label, model_name):
1281 """Return foreign key dependencies of the given model."""
1282 dependencies = []
1283 model_state = self.to_state.models[package_label, model_name]
1284 for field in model_state.fields.values():
1285 if field.is_relation:
1286 dependencies.extend(
1287 self._get_dependencies_for_foreign_key(
1288 package_label,
1289 model_name,
1290 field,
1291 self.to_state,
1292 )
1293 )
1294 return dependencies
1296 def _get_altered_foo_together_operations(self, option_name):
1297 for package_label, model_name in sorted(self.kept_model_keys):
1298 old_model_name = self.renamed_models.get(
1299 (package_label, model_name), model_name
1300 )
1301 old_model_state = self.from_state.models[package_label, old_model_name]
1302 new_model_state = self.to_state.models[package_label, model_name]
1304 # We run the old version through the field renames to account for those
1305 old_value = old_model_state.options.get(option_name)
1306 old_value = (
1307 {
1308 tuple(
1309 self.renamed_fields.get((package_label, model_name, n), n)
1310 for n in unique
1311 )
1312 for unique in old_value
1313 }
1314 if old_value
1315 else set()
1316 )
1318 new_value = new_model_state.options.get(option_name)
1319 new_value = set(new_value) if new_value else set()
1321 if old_value != new_value:
1322 dependencies = []
1323 for foo_togethers in new_value:
1324 for field_name in foo_togethers:
1325 field = new_model_state.get_field(field_name)
1326 if field.remote_field and field.remote_field.model:
1327 dependencies.extend(
1328 self._get_dependencies_for_foreign_key(
1329 package_label,
1330 model_name,
1331 field,
1332 self.to_state,
1333 )
1334 )
1335 yield (
1336 old_value,
1337 new_value,
1338 package_label,
1339 model_name,
1340 dependencies,
1341 )
1343 def _generate_removed_altered_foo_together(self, operation):
1344 for (
1345 old_value,
1346 new_value,
1347 package_label,
1348 model_name,
1349 dependencies,
1350 ) in self._get_altered_foo_together_operations(operation.option_name):
1351 removal_value = new_value.intersection(old_value)
1352 if removal_value or old_value:
1353 self.add_operation(
1354 package_label,
1355 operation(
1356 name=model_name, **{operation.option_name: removal_value}
1357 ),
1358 dependencies=dependencies,
1359 )
1361 def generate_altered_db_table(self):
1362 models_to_check = self.kept_model_keys.union(self.kept_unmanaged_keys)
1363 for package_label, model_name in sorted(models_to_check):
1364 old_model_name = self.renamed_models.get(
1365 (package_label, model_name), model_name
1366 )
1367 old_model_state = self.from_state.models[package_label, old_model_name]
1368 new_model_state = self.to_state.models[package_label, model_name]
1369 old_db_table_name = old_model_state.options.get("db_table")
1370 new_db_table_name = new_model_state.options.get("db_table")
1371 if old_db_table_name != new_db_table_name:
1372 self.add_operation(
1373 package_label,
1374 operations.AlterModelTable(
1375 name=model_name,
1376 table=new_db_table_name,
1377 ),
1378 )
1380 def generate_altered_db_table_comment(self):
1381 models_to_check = self.kept_model_keys.union(self.kept_unmanaged_keys)
1382 for package_label, model_name in sorted(models_to_check):
1383 old_model_name = self.renamed_models.get(
1384 (package_label, model_name), model_name
1385 )
1386 old_model_state = self.from_state.models[package_label, old_model_name]
1387 new_model_state = self.to_state.models[package_label, model_name]
1389 old_db_table_comment = old_model_state.options.get("db_table_comment")
1390 new_db_table_comment = new_model_state.options.get("db_table_comment")
1391 if old_db_table_comment != new_db_table_comment:
1392 self.add_operation(
1393 package_label,
1394 operations.AlterModelTableComment(
1395 name=model_name,
1396 table_comment=new_db_table_comment,
1397 ),
1398 )
1400 def generate_altered_options(self):
1401 """
1402 Work out if any non-schema-affecting options have changed and make an
1403 operation to represent them in state changes (in case Python code in
1404 migrations needs them).
1405 """
1406 models_to_check = self.kept_model_keys.union(
1407 self.kept_unmanaged_keys,
1408 # unmanaged converted to managed
1409 self.old_unmanaged_keys & self.new_model_keys,
1410 # managed converted to unmanaged
1411 self.old_model_keys & self.new_unmanaged_keys,
1412 )
1414 for package_label, model_name in sorted(models_to_check):
1415 old_model_name = self.renamed_models.get(
1416 (package_label, model_name), model_name
1417 )
1418 old_model_state = self.from_state.models[package_label, old_model_name]
1419 new_model_state = self.to_state.models[package_label, model_name]
1420 old_options = {
1421 key: value
1422 for key, value in old_model_state.options.items()
1423 if key in AlterModelOptions.ALTER_OPTION_KEYS
1424 }
1425 new_options = {
1426 key: value
1427 for key, value in new_model_state.options.items()
1428 if key in AlterModelOptions.ALTER_OPTION_KEYS
1429 }
1430 if old_options != new_options:
1431 self.add_operation(
1432 package_label,
1433 operations.AlterModelOptions(
1434 name=model_name,
1435 options=new_options,
1436 ),
1437 )
1439 def generate_altered_order_with_respect_to(self):
1440 for package_label, model_name in sorted(self.kept_model_keys):
1441 old_model_name = self.renamed_models.get(
1442 (package_label, model_name), model_name
1443 )
1444 old_model_state = self.from_state.models[package_label, old_model_name]
1445 new_model_state = self.to_state.models[package_label, model_name]
1446 if old_model_state.options.get(
1447 "order_with_respect_to"
1448 ) != new_model_state.options.get("order_with_respect_to"):
1449 # Make sure it comes second if we're adding
1450 # (removal dependency is part of RemoveField)
1451 dependencies = []
1452 if new_model_state.options.get("order_with_respect_to"):
1453 dependencies.append(
1454 (
1455 package_label,
1456 model_name,
1457 new_model_state.options["order_with_respect_to"],
1458 True,
1459 )
1460 )
1461 # Actually generate the operation
1462 self.add_operation(
1463 package_label,
1464 operations.AlterOrderWithRespectTo(
1465 name=model_name,
1466 order_with_respect_to=new_model_state.options.get(
1467 "order_with_respect_to"
1468 ),
1469 ),
1470 dependencies=dependencies,
1471 )
1473 def generate_altered_managers(self):
1474 for package_label, model_name in sorted(self.kept_model_keys):
1475 old_model_name = self.renamed_models.get(
1476 (package_label, model_name), model_name
1477 )
1478 old_model_state = self.from_state.models[package_label, old_model_name]
1479 new_model_state = self.to_state.models[package_label, model_name]
1480 if old_model_state.managers != new_model_state.managers:
1481 self.add_operation(
1482 package_label,
1483 operations.AlterModelManagers(
1484 name=model_name,
1485 managers=new_model_state.managers,
1486 ),
1487 )
1489 def arrange_for_graph(self, changes, graph, migration_name=None):
1490 """
1491 Take a result from changes() and a MigrationGraph, and fix the names
1492 and dependencies of the changes so they extend the graph from the leaf
1493 nodes for each app.
1494 """
1495 leaves = graph.leaf_nodes()
1496 name_map = {}
1497 for package_label, migrations in list(changes.items()):
1498 if not migrations:
1499 continue
1500 # Find the app label's current leaf node
1501 app_leaf = None
1502 for leaf in leaves:
1503 if leaf[0] == package_label:
1504 app_leaf = leaf
1505 break
1506 # Do they want an initial migration for this app?
1507 if app_leaf is None and not self.questioner.ask_initial(package_label):
1508 # They don't.
1509 for migration in migrations:
1510 name_map[(package_label, migration.name)] = (
1511 package_label,
1512 "__first__",
1513 )
1514 del changes[package_label]
1515 continue
1516 # Work out the next number in the sequence
1517 if app_leaf is None:
1518 next_number = 1
1519 else:
1520 next_number = (self.parse_number(app_leaf[1]) or 0) + 1
1521 # Name each migration
1522 for i, migration in enumerate(migrations):
1523 if i == 0 and app_leaf:
1524 migration.dependencies.append(app_leaf)
1525 new_name_parts = ["%04i" % next_number]
1526 if migration_name:
1527 new_name_parts.append(migration_name)
1528 elif i == 0 and not app_leaf:
1529 new_name_parts.append("initial")
1530 else:
1531 new_name_parts.append(migration.suggest_name()[:100])
1532 new_name = "_".join(new_name_parts)
1533 name_map[(package_label, migration.name)] = (package_label, new_name)
1534 next_number += 1
1535 migration.name = new_name
1536 # Now fix dependencies
1537 for migrations in changes.values():
1538 for migration in migrations:
1539 migration.dependencies = [
1540 name_map.get(d, d) for d in migration.dependencies
1541 ]
1542 return changes
1544 def _trim_to_packages(self, changes, package_labels):
1545 """
1546 Take changes from arrange_for_graph() and set of app labels, and return
1547 a modified set of changes which trims out as many migrations that are
1548 not in package_labels as possible. Note that some other migrations may
1549 still be present as they may be required dependencies.
1550 """
1551 # Gather other app dependencies in a first pass
1552 app_dependencies = {}
1553 for package_label, migrations in changes.items():
1554 for migration in migrations:
1555 for dep_package_label, name in migration.dependencies:
1556 app_dependencies.setdefault(package_label, set()).add(
1557 dep_package_label
1558 )
1559 required_packages = set(package_labels)
1560 # Keep resolving till there's no change
1561 old_required_packages = None
1562 while old_required_packages != required_packages:
1563 old_required_packages = set(required_packages)
1564 required_packages.update(
1565 *[
1566 app_dependencies.get(package_label, ())
1567 for package_label in required_packages
1568 ]
1569 )
1570 # Remove all migrations that aren't needed
1571 for package_label in list(changes):
1572 if package_label not in required_packages:
1573 del changes[package_label]
1574 return changes
1576 @classmethod
1577 def parse_number(cls, name):
1578 """
1579 Given a migration name, try to extract a number from the beginning of
1580 it. For a squashed migration such as '0001_squashed_0004…', return the
1581 second number. If no number is found, return None.
1582 """
1583 if squashed_match := re.search(r".*_squashed_(\d+)", name):
1584 return int(squashed_match[1])
1585 match = re.match(r"^\d+", name)
1586 if match:
1587 return int(match[0])
1588 return None