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

1import functools 

2import re 

3from graphlib import TopologicalSorter 

4from itertools import chain 

5 

6from plain import models 

7from plain.models.migrations import operations 

8from plain.models.migrations.migration import Migration 

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

10from plain.models.migrations.optimizer import MigrationOptimizer 

11from plain.models.migrations.questioner import MigrationQuestioner 

12from plain.models.migrations.utils import ( 

13 COMPILED_REGEX_TYPE, 

14 RegexObject, 

15 resolve_relation, 

16) 

17from plain.runtime import settings 

18 

19 

20class MigrationAutodetector: 

21 """ 

22 Take a pair of ProjectStates and compare them to see what the first would 

23 need doing to make it match the second (the second usually being the 

24 project's current state). 

25 

26 Note that this naturally operates on entire projects at a time, 

27 as it's likely that changes interact (for example, you can't 

28 add a ForeignKey without having a migration to add the table it 

29 depends on first). A user interface may offer single-app usage 

30 if it wishes, with the caveat that it may not always be possible. 

31 """ 

32 

33 def __init__(self, from_state, to_state, questioner=None): 

34 self.from_state = from_state 

35 self.to_state = to_state 

36 self.questioner = questioner or MigrationQuestioner() 

37 self.existing_packages = {app for app, model in from_state.models} 

38 

39 def changes( 

40 self, graph, trim_to_packages=None, convert_packages=None, migration_name=None 

41 ): 

42 """ 

43 Main entry point to produce a list of applicable changes. 

44 Take a graph to base names on and an optional set of packages 

45 to try and restrict to (restriction is not guaranteed) 

46 """ 

47 changes = self._detect_changes(convert_packages, graph) 

48 changes = self.arrange_for_graph(changes, graph, migration_name) 

49 if trim_to_packages: 

50 changes = self._trim_to_packages(changes, trim_to_packages) 

51 return changes 

52 

53 def deep_deconstruct(self, obj): 

54 """ 

55 Recursive deconstruction for a field and its arguments. 

56 Used for full comparison for rename/alter; sometimes a single-level 

57 deconstruction will not compare correctly. 

58 """ 

59 if isinstance(obj, list): 

60 return [self.deep_deconstruct(value) for value in obj] 

61 elif isinstance(obj, tuple): 

62 return tuple(self.deep_deconstruct(value) for value in obj) 

63 elif isinstance(obj, dict): 

64 return {key: self.deep_deconstruct(value) for key, value in obj.items()} 

65 elif isinstance(obj, functools.partial): 

66 return ( 

67 obj.func, 

68 self.deep_deconstruct(obj.args), 

69 self.deep_deconstruct(obj.keywords), 

70 ) 

71 elif isinstance(obj, COMPILED_REGEX_TYPE): 

72 return RegexObject(obj) 

73 elif isinstance(obj, type): 

74 # If this is a type that implements 'deconstruct' as an instance method, 

75 # avoid treating this as being deconstructible itself - see #22951 

76 return obj 

77 elif hasattr(obj, "deconstruct"): 

78 deconstructed = obj.deconstruct() 

79 if isinstance(obj, models.Field): 

80 # we have a field which also returns a name 

81 deconstructed = deconstructed[1:] 

82 path, args, kwargs = deconstructed 

83 return ( 

84 path, 

85 [self.deep_deconstruct(value) for value in args], 

86 {key: self.deep_deconstruct(value) for key, value in kwargs.items()}, 

87 ) 

88 else: 

89 return obj 

90 

91 def only_relation_agnostic_fields(self, fields): 

92 """ 

93 Return a definition of the fields that ignores field names and 

94 what related fields actually relate to. Used for detecting renames (as 

95 the related fields change during renames). 

96 """ 

97 fields_def = [] 

98 for name, field in sorted(fields.items()): 

99 deconstruction = self.deep_deconstruct(field) 

100 if field.remote_field and field.remote_field.model: 

101 deconstruction[2].pop("to", None) 

102 fields_def.append(deconstruction) 

103 return fields_def 

104 

105 def _detect_changes(self, convert_packages=None, graph=None): 

106 """ 

107 Return a dict of migration plans which will achieve the 

108 change from from_state to to_state. The dict has app labels 

109 as keys and a list of migrations as values. 

110 

111 The resulting migrations aren't specially named, but the names 

112 do matter for dependencies inside the set. 

113 

114 convert_packages is the list of packages to convert to use migrations 

115 (i.e. to make initial migrations for, in the usual case) 

116 

117 graph is an optional argument that, if provided, can help improve 

118 dependency generation and avoid potential circular dependencies. 

119 """ 

120 # The first phase is generating all the operations for each app 

121 # and gathering them into a big per-app list. 

122 # Then go through that list, order it, and split into migrations to 

123 # resolve dependencies caused by M2Ms and FKs. 

124 self.generated_operations = {} 

125 self.altered_indexes = {} 

126 self.altered_constraints = {} 

127 self.renamed_fields = {} 

128 

129 # Prepare some old/new state and model lists, ignoring unmigrated packages. 

130 self.old_model_keys = set() 

131 self.old_unmanaged_keys = set() 

132 self.new_model_keys = set() 

133 self.new_unmanaged_keys = set() 

134 for (package_label, model_name), model_state in self.from_state.models.items(): 

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

136 self.old_unmanaged_keys.add((package_label, model_name)) 

137 elif package_label not in self.from_state.real_packages: 

138 self.old_model_keys.add((package_label, model_name)) 

139 

140 for (package_label, model_name), model_state in self.to_state.models.items(): 

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

142 self.new_unmanaged_keys.add((package_label, model_name)) 

143 elif package_label not in self.from_state.real_packages or ( 

144 convert_packages and package_label in convert_packages 

145 ): 

146 self.new_model_keys.add((package_label, model_name)) 

147 

148 self.from_state.resolve_fields_and_relations() 

149 self.to_state.resolve_fields_and_relations() 

150 

151 # Renames have to come first 

152 self.generate_renamed_models() 

153 

154 # Prepare lists of fields and generate through model map 

155 self._prepare_field_lists() 

156 self._generate_through_model_map() 

157 

158 # Generate non-rename model operations 

159 self.generate_deleted_models() 

160 self.generate_created_models() 

161 self.generate_altered_options() 

162 self.generate_altered_managers() 

163 self.generate_altered_db_table_comment() 

164 

165 # Create the renamed fields and store them in self.renamed_fields. 

166 # They are used by create_altered_indexes(), generate_altered_fields(), 

167 # generate_removed_altered_index(), and 

168 # generate_altered_index(). 

169 self.create_renamed_fields() 

170 # Create the altered indexes and store them in self.altered_indexes. 

171 # This avoids the same computation in generate_removed_indexes() 

172 # and generate_added_indexes(). 

173 self.create_altered_indexes() 

174 self.create_altered_constraints() 

175 # Generate index removal operations before field is removed 

176 self.generate_removed_constraints() 

177 self.generate_removed_indexes() 

178 # Generate field renaming operations. 

179 self.generate_renamed_fields() 

180 self.generate_renamed_indexes() 

181 # Generate field operations. 

182 self.generate_removed_fields() 

183 self.generate_added_fields() 

184 self.generate_altered_fields() 

185 self.generate_altered_order_with_respect_to() 

186 self.generate_added_indexes() 

187 self.generate_added_constraints() 

188 self.generate_altered_db_table() 

189 

190 self._sort_migrations() 

191 self._build_migration_list(graph) 

192 self._optimize_migrations() 

193 

194 return self.migrations 

195 

196 def _prepare_field_lists(self): 

197 """ 

198 Prepare field lists and a list of the fields that used through models 

199 in the old state so dependencies can be made from the through model 

200 deletion to the field that uses it. 

201 """ 

202 self.kept_model_keys = self.old_model_keys & self.new_model_keys 

203 self.kept_unmanaged_keys = self.old_unmanaged_keys & self.new_unmanaged_keys 

204 self.through_users = {} 

205 self.old_field_keys = { 

206 (package_label, model_name, field_name) 

207 for package_label, model_name in self.kept_model_keys 

208 for field_name in self.from_state.models[ 

209 package_label, 

210 self.renamed_models.get((package_label, model_name), model_name), 

211 ].fields 

212 } 

213 self.new_field_keys = { 

214 (package_label, model_name, field_name) 

215 for package_label, model_name in self.kept_model_keys 

216 for field_name in self.to_state.models[package_label, model_name].fields 

217 } 

218 

219 def _generate_through_model_map(self): 

220 """Through model map generation.""" 

221 for package_label, model_name in sorted(self.old_model_keys): 

222 old_model_name = self.renamed_models.get( 

223 (package_label, model_name), model_name 

224 ) 

225 old_model_state = self.from_state.models[package_label, old_model_name] 

226 for field_name, field in old_model_state.fields.items(): 

227 if hasattr(field, "remote_field") and getattr( 

228 field.remote_field, "through", None 

229 ): 

230 through_key = resolve_relation( 

231 field.remote_field.through, package_label, model_name 

232 ) 

233 self.through_users[through_key] = ( 

234 package_label, 

235 old_model_name, 

236 field_name, 

237 ) 

238 

239 @staticmethod 

240 def _resolve_dependency(dependency): 

241 """ 

242 Return the resolved dependency and a boolean denoting whether or not 

243 it was swappable. 

244 """ 

245 if dependency[0] != "__setting__": 

246 return dependency, False 

247 resolved_package_label, resolved_object_name = getattr( 

248 settings, dependency[1] 

249 ).split(".") 

250 return (resolved_package_label, resolved_object_name.lower()) + dependency[ 

251 2: 

252 ], True 

253 

254 def _build_migration_list(self, graph=None): 

255 """ 

256 Chop the lists of operations up into migrations with dependencies on 

257 each other. Do this by going through an app's list of operations until 

258 one is found that has an outgoing dependency that isn't in another 

259 app's migration yet (hasn't been chopped off its list). Then chop off 

260 the operations before it into a migration and move onto the next app. 

261 If the loops completes without doing anything, there's a circular 

262 dependency (which _should_ be impossible as the operations are 

263 all split at this point so they can't depend and be depended on). 

264 """ 

265 self.migrations = {} 

266 num_ops = sum(len(x) for x in self.generated_operations.values()) 

267 chop_mode = False 

268 while num_ops: 

269 # On every iteration, we step through all the packages and see if there 

270 # is a completed set of operations. 

271 # If we find that a subset of the operations are complete we can 

272 # try to chop it off from the rest and continue, but we only 

273 # do this if we've already been through the list once before 

274 # without any chopping and nothing has changed. 

275 for package_label in sorted(self.generated_operations): 

276 chopped = [] 

277 dependencies = set() 

278 for operation in list(self.generated_operations[package_label]): 

279 deps_satisfied = True 

280 operation_dependencies = set() 

281 for dep in operation._auto_deps: 

282 # Temporarily resolve the swappable dependency to 

283 # prevent circular references. While keeping the 

284 # dependency checks on the resolved model, add the 

285 # swappable dependencies. 

286 original_dep = dep 

287 dep, is_swappable_dep = self._resolve_dependency(dep) 

288 if dep[0] != package_label: 

289 # External app dependency. See if it's not yet 

290 # satisfied. 

291 for other_operation in self.generated_operations.get( 

292 dep[0], [] 

293 ): 

294 if self.check_dependency(other_operation, dep): 

295 deps_satisfied = False 

296 break 

297 if not deps_satisfied: 

298 break 

299 else: 

300 if is_swappable_dep: 

301 operation_dependencies.add( 

302 (original_dep[0], original_dep[1]) 

303 ) 

304 elif dep[0] in self.migrations: 

305 operation_dependencies.add( 

306 (dep[0], self.migrations[dep[0]][-1].name) 

307 ) 

308 else: 

309 # If we can't find the other app, we add a 

310 # first/last dependency, but only if we've 

311 # already been through once and checked 

312 # everything. 

313 if chop_mode: 

314 # If the app already exists, we add a 

315 # dependency on the last migration, as 

316 # we don't know which migration 

317 # contains the target field. If it's 

318 # not yet migrated or has no 

319 # migrations, we use __first__. 

320 if graph and graph.leaf_nodes(dep[0]): 

321 operation_dependencies.add( 

322 graph.leaf_nodes(dep[0])[0] 

323 ) 

324 else: 

325 operation_dependencies.add( 

326 (dep[0], "__first__") 

327 ) 

328 else: 

329 deps_satisfied = False 

330 if deps_satisfied: 

331 chopped.append(operation) 

332 dependencies.update(operation_dependencies) 

333 del self.generated_operations[package_label][0] 

334 else: 

335 break 

336 # Make a migration! Well, only if there's stuff to put in it 

337 if dependencies or chopped: 

338 if not self.generated_operations[package_label] or chop_mode: 

339 subclass = type( 

340 "Migration", 

341 (Migration,), 

342 {"operations": [], "dependencies": []}, 

343 ) 

344 instance = subclass( 

345 "auto_%i" 

346 % (len(self.migrations.get(package_label, [])) + 1), 

347 package_label, 

348 ) 

349 instance.dependencies = list(dependencies) 

350 instance.operations = chopped 

351 instance.initial = package_label not in self.existing_packages 

352 self.migrations.setdefault(package_label, []).append(instance) 

353 chop_mode = False 

354 else: 

355 self.generated_operations[package_label] = ( 

356 chopped + self.generated_operations[package_label] 

357 ) 

358 new_num_ops = sum(len(x) for x in self.generated_operations.values()) 

359 if new_num_ops == num_ops: 

360 if not chop_mode: 

361 chop_mode = True 

362 else: 

363 raise ValueError( 

364 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