Coverage for /Users/davegaeddert/Developer/dropseed/plain/plain-models/plain/models/migrations/autodetector.py: 9%

1 import functools

2 import re

3 from graphlib import TopologicalSorter

4 from itertools import chain

5

6 from plain import models

7 from plain.models.migrations import operations

8 from plain.models.migrations.migration import Migration

9 from plain.models.migrations.operations.models import AlterModelOptions

10 from plain.models.migrations.optimizer import MigrationOptimizer

11 from plain.models.migrations.questioner import MigrationQuestioner

12 from plain.models.migrations.utils import (

13 COMPILED_REGEX_TYPE,

14 RegexObject,

15 resolve_relation,

16)

17 from plain.runtime import settings

18

19

20 class 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 f"Cannot resolve operation dependencies: {self.generated_operations! r}"

365 )

366 num_ops = new_num_ops

367

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())

385

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))

391

392 # De-dupe dependencies

393 for migrations in self.migrations.values():

394 for migration in migrations:

395 migration.dependencies = list(set(migration.dependencies))

396

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 )

403

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}")

457

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)

468

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

491

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)

504

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

556

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).

563

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

588

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 )

656

657 # Don't add operations which modify the database for unmanaged models

658 if not model_state.options.get("managed", True):

659 continue

660

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 )

717

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).

724

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

744

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 )

777

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 )

796

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

858

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))

897

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)

904

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 )

955

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)

962

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 )

977

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)

1096

1097 def create_altered_indexes(self):

1098 option_name = operations.AddIndex.option_name

1099

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]

1106

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)

1129

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 ]

1138

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 )

1148

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 )

1161

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 )

1172

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 )

1187

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]

1196

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]

1201

1202 self.altered_constraints.update(

1203 {

1204 (package_label, model_name): {

1205 "added_constraints": add_constraints,

1206 "removed_constraints": rem_constraints,

1207 }

1208 }

1209 )

1210

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 )

1226

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 )

1240

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

1279

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

1295

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]

1303

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 )

1317

1318 new_value = new_model_state.options.get(option_name)

1319 new_value = set(new_value) if new_value else set()

1320

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 )

1342

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 )

1360

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 )

1379

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]

1388

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 )

1399

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 )

1413

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 )

1438

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 )

1472

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 )

1488

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

1543

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

1575

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