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

640 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2024-10-16 22:04 -0500

1import functools 

2import re 

3from graphlib import TopologicalSorter 

4from itertools import chain 

5 

6from plain import models 

7from plain.models.migrations import operations 

8from plain.models.migrations.migration import Migration 

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

10from plain.models.migrations.optimizer import MigrationOptimizer 

11from plain.models.migrations.questioner import MigrationQuestioner 

12from plain.models.migrations.utils import ( 

13 COMPILED_REGEX_TYPE, 

14 RegexObject, 

15 resolve_relation, 

16) 

17from plain.runtime import settings 

18 

19 

20class MigrationAutodetector: 

21 """ 

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

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

24 project's current state). 

25 

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

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

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

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

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

31 """ 

32 

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

34 self.from_state = from_state 

35 self.to_state = to_state 

36 self.questioner = questioner or MigrationQuestioner() 

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

38 

39 def changes( 

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

41 ): 

42 """ 

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

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

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

46 """ 

47 changes = self._detect_changes(convert_packages, graph) 

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

49 if trim_to_packages: 

50 changes = self._trim_to_packages(changes, trim_to_packages) 

51 return changes 

52 

53 def deep_deconstruct(self, obj): 

54 """ 

55 Recursive deconstruction for a field and its arguments. 

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

57 deconstruction will not compare correctly. 

58 """ 

59 if isinstance(obj, list): 

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

61 elif isinstance(obj, tuple): 

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

63 elif isinstance(obj, dict): 

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

65 elif isinstance(obj, functools.partial): 

66 return ( 

67 obj.func, 

68 self.deep_deconstruct(obj.args), 

69 self.deep_deconstruct(obj.keywords), 

70 ) 

71 elif isinstance(obj, COMPILED_REGEX_TYPE): 

72 return RegexObject(obj) 

73 elif isinstance(obj, type): 

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

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

76 return obj 

77 elif hasattr(obj, "deconstruct"): 

78 deconstructed = obj.deconstruct() 

79 if isinstance(obj, models.Field): 

80 # we have a field which also returns a name 

81 deconstructed = deconstructed[1:] 

82 path, args, kwargs = deconstructed 

83 return ( 

84 path, 

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

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

87 ) 

88 else: 

89 return obj 

90 

91 def only_relation_agnostic_fields(self, fields): 

92 """ 

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

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

95 the related fields change during renames). 

96 """ 

97 fields_def = [] 

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

99 deconstruction = self.deep_deconstruct(field) 

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

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

102 fields_def.append(deconstruction) 

103 return fields_def 

104 

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

106 """ 

107 Return a dict of migration plans which will achieve the 

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

109 as keys and a list of migrations as values. 

110 

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

112 do matter for dependencies inside the set. 

113 

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

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

116 

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

118 dependency generation and avoid potential circular dependencies. 

119 """ 

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

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

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

123 # resolve dependencies caused by M2Ms and FKs. 

124 self.generated_operations = {} 

125 self.altered_indexes = {} 

126 self.altered_constraints = {} 

127 self.renamed_fields = {} 

128 

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

130 self.old_model_keys = set() 

131 self.old_unmanaged_keys = set() 

132 self.new_model_keys = set() 

133 self.new_unmanaged_keys = set() 

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

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

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

137 elif package_label not in self.from_state.real_packages: 

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

139 

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

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

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

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

144 convert_packages and package_label in convert_packages 

145 ): 

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

147 

148 self.from_state.resolve_fields_and_relations() 

149 self.to_state.resolve_fields_and_relations() 

150 

151 # Renames have to come first 

152 self.generate_renamed_models() 

153 

154 # Prepare lists of fields and generate through model map 

155 self._prepare_field_lists() 

156 self._generate_through_model_map() 

157 

158 # Generate non-rename model operations 

159 self.generate_deleted_models() 

160 self.generate_created_models() 

161 self.generate_altered_options() 

162 self.generate_altered_managers() 

163 self.generate_altered_db_table_comment() 

164 

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

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

167 # generate_removed_altered_index(), and 

168 # generate_altered_index(). 

169 self.create_renamed_fields() 

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

171 # This avoids the same computation in generate_removed_indexes() 

172 # and generate_added_indexes(). 

173 self.create_altered_indexes() 

174 self.create_altered_constraints() 

175 # Generate index removal operations before field is removed 

176 self.generate_removed_constraints() 

177 self.generate_removed_indexes() 

178 # Generate field renaming operations. 

179 self.generate_renamed_fields() 

180 self.generate_renamed_indexes() 

181 # Generate field operations. 

182 self.generate_removed_fields() 

183 self.generate_added_fields() 

184 self.generate_altered_fields() 

185 self.generate_altered_order_with_respect_to() 

186 self.generate_added_indexes() 

187 self.generate_added_constraints() 

188 self.generate_altered_db_table() 

189 

190 self._sort_migrations() 

191 self._build_migration_list(graph) 

192 self._optimize_migrations() 

193 

194 return self.migrations 

195 

196 def _prepare_field_lists(self): 

197 """ 

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

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

200 deletion to the field that uses it. 

201 """ 

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

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

204 self.through_users = {} 

205 self.old_field_keys = { 

206 (package_label, model_name, field_name) 

207 for package_label, model_name in self.kept_model_keys 

208 for field_name in self.from_state.models[ 

209 package_label, 

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

211 ].fields 

212 } 

213 self.new_field_keys = { 

214 (package_label, model_name, field_name) 

215 for package_label, model_name in self.kept_model_keys 

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

217 } 

218 

219 def _generate_through_model_map(self): 

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

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

222 old_model_name = self.renamed_models.get( 

223 (package_label, model_name), model_name 

224 ) 

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

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

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

228 field.remote_field, "through", None 

229 ): 

230 through_key = resolve_relation( 

231 field.remote_field.through, package_label, model_name 

232 ) 

233 self.through_users[through_key] = ( 

234 package_label, 

235 old_model_name, 

236 field_name, 

237 ) 

238 

239 @staticmethod 

240 def _resolve_dependency(dependency): 

241 """ 

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

243 it was swappable. 

244 """ 

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

246 return dependency, False 

247 resolved_package_label, resolved_object_name = getattr( 

248 settings, dependency[1] 

249 ).split(".") 

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

251 2: 

252 ], True 

253 

254 def _build_migration_list(self, graph=None): 

255 """ 

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

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

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

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

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

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

262 dependency (which _should_ be impossible as the operations are 

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

264 """ 

265 self.migrations = {} 

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

267 chop_mode = False 

268 while num_ops: 

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

270 # is a completed set of operations. 

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

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

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

274 # without any chopping and nothing has changed. 

275 for package_label in sorted(self.generated_operations): 

276 chopped = [] 

277 dependencies = set() 

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

279 deps_satisfied = True 

280 operation_dependencies = set() 

281 for dep in operation._auto_deps: 

282 # Temporarily resolve the swappable dependency to 

283 # prevent circular references. While keeping the 

284 # dependency checks on the resolved model, add the 

285 # swappable dependencies. 

286 original_dep = dep 

287 dep, is_swappable_dep = self._resolve_dependency(dep) 

288 if dep[0] != package_label: 

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

290 # satisfied. 

291 for other_operation in self.generated_operations.get( 

292 dep[0], [] 

293 ): 

294 if self.check_dependency(other_operation, dep): 

295 deps_satisfied = False 

296 break 

297 if not deps_satisfied: 

298 break 

299 else: 

300 if is_swappable_dep: 

301 operation_dependencies.add( 

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

303 ) 

304 elif dep[0] in self.migrations: 

305 operation_dependencies.add( 

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

307 ) 

308 else: 

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

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

311 # already been through once and checked 

312 # everything. 

313 if chop_mode: 

314 # If the app already exists, we add a 

315 # dependency on the last migration, as 

316 # we don't know which migration 

317 # contains the target field. If it's 

318 # not yet migrated or has no 

319 # migrations, we use __first__. 

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

321 operation_dependencies.add( 

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

323 ) 

324 else: 

325 operation_dependencies.add( 

326 (dep[0], "__first__") 

327 ) 

328 else: 

329 deps_satisfied = False 

330 if deps_satisfied: 

331 chopped.append(operation) 

332 dependencies.update(operation_dependencies) 

333 del self.generated_operations[package_label][0] 

334 else: 

335 break 

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

337 if dependencies or chopped: 

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

339 subclass = type( 

340 "Migration", 

341 (Migration,), 

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

343 ) 

344 instance = subclass( 

345 "auto_%i" 

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

347 package_label, 

348 ) 

349 instance.dependencies = list(dependencies) 

350 instance.operations = chopped 

351 instance.initial = package_label not in self.existing_packages 

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

353 chop_mode = False 

354 else: 

355 self.generated_operations[package_label] = ( 

356 chopped + self.generated_operations[package_label] 

357 ) 

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

359 if new_num_ops == num_ops: 

360 if not chop_mode: 

361 chop_mode = True 

362 else: 

363 raise ValueError( 

364 "Cannot resolve operation dependencies: %r" 

365 % self.generated_operations 

366 ) 

367 num_ops = new_num_ops 

368 

369 def _sort_migrations(self): 

370 """ 

371 Reorder to make things possible. Reordering may be needed so FKs work 

372 nicely inside the same app. 

373 """ 

374 for package_label, ops in sorted(self.generated_operations.items()): 

375 ts = TopologicalSorter() 

376 for op in ops: 

377 ts.add(op) 

378 for dep in op._auto_deps: 

379 # Resolve intra-app dependencies to handle circular 

380 # references involving a swappable model. 

381 dep = self._resolve_dependency(dep)[0] 

382 if dep[0] != package_label: 

383 continue 

384 ts.add(op, *(x for x in ops if self.check_dependency(x, dep))) 

385 self.generated_operations[package_label] = list(ts.static_order()) 

386 

387 def _optimize_migrations(self): 

388 # Add in internal dependencies among the migrations 

389 for package_label, migrations in self.migrations.items(): 

390 for m1, m2 in zip(migrations, migrations[1:]): 

391 m2.dependencies.append((package_label, m1.name)) 

392 

393 # De-dupe dependencies 

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

395 for migration in migrations: 

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

397 

398 # Optimize migrations 

399 for package_label, migrations in self.migrations.items(): 

400 for migration in migrations: 

401 migration.operations = MigrationOptimizer().optimize( 

402 migration.operations, package_label 

403 ) 

404 

405 def check_dependency(self, operation, dependency): 

406 """ 

407 Return True if the given operation depends on the given dependency, 

408 False otherwise. 

409 """ 

410 # Created model 

411 if dependency[2] is None and dependency[3] is True: 

412 return ( 

413 isinstance(operation, operations.CreateModel) 

414 and operation.name_lower == dependency[1].lower() 

415 ) 

416 # Created field 

417 elif dependency[2] is not None and dependency[3] is True: 

418 return ( 

419 isinstance(operation, operations.CreateModel) 

420 and operation.name_lower == dependency[1].lower() 

421 and any(dependency[2] == x for x, y in operation.fields) 

422 ) or ( 

423 isinstance(operation, operations.AddField) 

424 and operation.model_name_lower == dependency[1].lower() 

425 and operation.name_lower == dependency[2].lower() 

426 ) 

427 # Removed field 

428 elif dependency[2] is not None and dependency[3] is False: 

429 return ( 

430 isinstance(operation, operations.RemoveField) 

431 and operation.model_name_lower == dependency[1].lower() 

432 and operation.name_lower == dependency[2].lower() 

433 ) 

434 # Removed model 

435 elif dependency[2] is None and dependency[3] is False: 

436 return ( 

437 isinstance(operation, operations.DeleteModel) 

438 and operation.name_lower == dependency[1].lower() 

439 ) 

440 # Field being altered 

441 elif dependency[2] is not None and dependency[3] == "alter": 

442 return ( 

443 isinstance(operation, operations.AlterField) 

444 and operation.model_name_lower == dependency[1].lower() 

445 and operation.name_lower == dependency[2].lower() 

446 ) 

447 # order_with_respect_to being unset for a field 

448 elif dependency[2] is not None and dependency[3] == "order_wrt_unset": 

449 return ( 

450 isinstance(operation, operations.AlterOrderWithRespectTo) 

451 and operation.name_lower == dependency[1].lower() 

452 and (operation.order_with_respect_to or "").lower() 

453 != dependency[2].lower() 

454 ) 

455 # Unknown dependency. Raise an error. 

456 else: 

457 raise ValueError(f"Can't handle dependency {dependency!r}") 

458 

459 def add_operation( 

460 self, package_label, operation, dependencies=None, beginning=False 

461 ): 

462 # Dependencies are 

463 # (package_label, model_name, field_name, create/delete as True/False) 

464 operation._auto_deps = dependencies or [] 

465 if beginning: 

466 self.generated_operations.setdefault(package_label, []).insert(0, operation) 

467 else: 

468 self.generated_operations.setdefault(package_label, []).append(operation) 

469 

470 def swappable_first_key(self, item): 

471 """ 

472 Place potential swappable models first in lists of created models (only 

473 real way to solve #22783). 

474 """ 

475 try: 

476 model_state = self.to_state.models[item] 

477 base_names = { 

478 base if isinstance(base, str) else base.__name__ 

479 for base in model_state.bases 

480 } 

481 string_version = f"{item[0]}.{item[1]}" 

482 if ( 

483 model_state.options.get("swappable") 

484 or "BaseUser" in base_names 

485 or "AbstractBaseUser" in base_names 

486 or settings.AUTH_USER_MODEL.lower() == string_version.lower() 

487 ): 

488 return ("___" + item[0], "___" + item[1]) 

489 except LookupError: 

490 pass 

491 return item 

492 

493 def generate_renamed_models(self): 

494 """ 

495 Find any renamed models, generate the operations for them, and remove 

496 the old entry from the model lists. Must be run before other 

497 model-level generation. 

498 """ 

499 self.renamed_models = {} 

500 self.renamed_models_rel = {} 

501 added_models = self.new_model_keys - self.old_model_keys 

502 for package_label, model_name in sorted(added_models): 

503 model_state = self.to_state.models[package_label, model_name] 

504 model_fields_def = self.only_relation_agnostic_fields(model_state.fields) 

505 

506 removed_models = self.old_model_keys - self.new_model_keys 

507 for rem_package_label, rem_model_name in removed_models: 

508 if rem_package_label == package_label: 

509 rem_model_state = self.from_state.models[ 

510 rem_package_label, rem_model_name 

511 ] 

512 rem_model_fields_def = self.only_relation_agnostic_fields( 

513 rem_model_state.fields 

514 ) 

515 if model_fields_def == rem_model_fields_def: 

516 if self.questioner.ask_rename_model( 

517 rem_model_state, model_state 

518 ): 

519 dependencies = [] 

520 fields = list(model_state.fields.values()) + [ 

521 field.remote_field 

522 for relations in self.to_state.relations[ 

523 package_label, model_name 

524 ].values() 

525 for field in relations.values() 

526 ] 

527 for field in fields: 

528 if field.is_relation: 

529 dependencies.extend( 

530 self._get_dependencies_for_foreign_key( 

531 package_label, 

532 model_name, 

533 field, 

534 self.to_state, 

535 ) 

536 ) 

537 self.add_operation( 

538 package_label, 

539 operations.RenameModel( 

540 old_name=rem_model_state.name, 

541 new_name=model_state.name, 

542 ), 

543 dependencies=dependencies, 

544 ) 

545 self.renamed_models[ 

546 package_label, model_name 

547 ] = rem_model_name 

548 renamed_models_rel_key = "{}.{}".format( 

549 rem_model_state.package_label, 

550 rem_model_state.name_lower, 

551 ) 

552 self.renamed_models_rel[ 

553 renamed_models_rel_key 

554 ] = f"{model_state.package_label}.{model_state.name_lower}" 

555 self.old_model_keys.remove( 

556 (rem_package_label, rem_model_name) 

557 ) 

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

559 break 

560 

561 def generate_created_models(self): 

562 """ 

563 Find all new models (both managed and unmanaged) and make create 

564 operations for them as well as separate operations to create any 

565 foreign key or M2M relationships (these are optimized later, if 

566 possible). 

567 

568 Defer any model options that refer to collections of fields that might 

569 be deferred. 

570 """ 

571 old_keys = self.old_model_keys | self.old_unmanaged_keys 

572 added_models = self.new_model_keys - old_keys 

573 added_unmanaged_models = self.new_unmanaged_keys - old_keys 

574 all_added_models = chain( 

575 sorted(added_models, key=self.swappable_first_key, reverse=True), 

576 sorted(added_unmanaged_models, key=self.swappable_first_key, reverse=True), 

577 ) 

578 for package_label, model_name in all_added_models: 

579 model_state = self.to_state.models[package_label, model_name] 

580 # Gather related fields 

581 related_fields = {} 

582 primary_key_rel = None 

583 for field_name, field in model_state.fields.items(): 

584 if field.remote_field: 

585 if field.remote_field.model: 

586 if field.primary_key: 

587 primary_key_rel = field.remote_field.model 

588 elif not field.remote_field.parent_link: 

589 related_fields[field_name] = field 

590 if getattr(field.remote_field, "through", None): 

591 related_fields[field_name] = field 

592 

593 # Are there indexes to defer? 

594 indexes = model_state.options.pop("indexes") 

595 constraints = model_state.options.pop("constraints") 

596 order_with_respect_to = model_state.options.pop( 

597 "order_with_respect_to", None 

598 ) 

599 # Depend on the deletion of any possible proxy version of us 

600 dependencies = [ 

601 (package_label, model_name, None, False), 

602 ] 

603 # Depend on all bases 

604 for base in model_state.bases: 

605 if isinstance(base, str) and "." in base: 

606 base_package_label, base_name = base.split(".", 1) 

607 dependencies.append((base_package_label, base_name, None, True)) 

608 # Depend on the removal of base fields if the new model has 

609 # a field with the same name. 

610 old_base_model_state = self.from_state.models.get( 

611 (base_package_label, base_name) 

612 ) 

613 new_base_model_state = self.to_state.models.get( 

614 (base_package_label, base_name) 

615 ) 

616 if old_base_model_state and new_base_model_state: 

617 removed_base_fields = ( 

618 set(old_base_model_state.fields) 

619 .difference( 

620 new_base_model_state.fields, 

621 ) 

622 .intersection(model_state.fields) 

623 ) 

624 for removed_base_field in removed_base_fields: 

625 dependencies.append( 

626 ( 

627 base_package_label, 

628 base_name, 

629 removed_base_field, 

630 False, 

631 ) 

632 ) 

633 # Depend on the other end of the primary key if it's a relation 

634 if primary_key_rel: 

635 dependencies.append( 

636 resolve_relation( 

637 primary_key_rel, 

638 package_label, 

639 model_name, 

640 ) 

641 + (None, True) 

642 ) 

643 # Generate creation operation 

644 self.add_operation( 

645 package_label, 

646 operations.CreateModel( 

647 name=model_state.name, 

648 fields=[ 

649 d 

650 for d in model_state.fields.items() 

651 if d[0] not in related_fields 

652 ], 

653 options=model_state.options, 

654 bases=model_state.bases, 

655 managers=model_state.managers, 

656 ), 

657 dependencies=dependencies, 

658 beginning=True, 

659 ) 

660 

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

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

663 continue 

664 

665 # Generate operations for each related field 

666 for name, field in sorted(related_fields.items()): 

667 dependencies = self._get_dependencies_for_foreign_key( 

668 package_label, 

669 model_name, 

670 field, 

671 self.to_state, 

672 ) 

673 # Depend on our own model being created 

674 dependencies.append((package_label, model_name, None, True)) 

675 # Make operation 

676 self.add_operation( 

677 package_label, 

678 operations.AddField( 

679 model_name=model_name, 

680 name=name, 

681 field=field, 

682 ), 

683 dependencies=list(set(dependencies)), 

684 ) 

685 # Generate other opns 

686 if order_with_respect_to: 

687 self.add_operation( 

688 package_label, 

689 operations.AlterOrderWithRespectTo( 

690 name=model_name, 

691 order_with_respect_to=order_with_respect_to, 

692 ), 

693 dependencies=[ 

694 (package_label, model_name, order_with_respect_to, True), 

695 (package_label, model_name, None, True), 

696 ], 

697 ) 

698 related_dependencies = [ 

699 (package_label, model_name, name, True) 

700 for name in sorted(related_fields) 

701 ] 

702 related_dependencies.append((package_label, model_name, None, True)) 

703 for index in indexes: 

704 self.add_operation( 

705 package_label, 

706 operations.AddIndex( 

707 model_name=model_name, 

708 index=index, 

709 ), 

710 dependencies=related_dependencies, 

711 ) 

712 for constraint in constraints: 

713 self.add_operation( 

714 package_label, 

715 operations.AddConstraint( 

716 model_name=model_name, 

717 constraint=constraint, 

718 ), 

719 dependencies=related_dependencies, 

720 ) 

721 

722 def generate_deleted_models(self): 

723 """ 

724 Find all deleted models (managed and unmanaged) and make delete 

725 operations for them as well as separate operations to delete any 

726 foreign key or M2M relationships (these are optimized later, if 

727 possible). 

728 

729 Also bring forward removal of any model options that refer to 

730 collections of fields - the inverse of generate_created_models(). 

731 """ 

732 new_keys = self.new_model_keys | self.new_unmanaged_keys 

733 deleted_models = self.old_model_keys - new_keys 

734 deleted_unmanaged_models = self.old_unmanaged_keys - new_keys 

735 all_deleted_models = chain( 

736 sorted(deleted_models), sorted(deleted_unmanaged_models) 

737 ) 

738 for package_label, model_name in all_deleted_models: 

739 model_state = self.from_state.models[package_label, model_name] 

740 # Gather related fields 

741 related_fields = {} 

742 for field_name, field in model_state.fields.items(): 

743 if field.remote_field: 

744 if field.remote_field.model: 

745 related_fields[field_name] = field 

746 if getattr(field.remote_field, "through", None): 

747 related_fields[field_name] = field 

748 

749 # Then remove each related field 

750 for name in sorted(related_fields): 

751 self.add_operation( 

752 package_label, 

753 operations.RemoveField( 

754 model_name=model_name, 

755 name=name, 

756 ), 

757 ) 

758 # Finally, remove the model. 

759 # This depends on both the removal/alteration of all incoming fields 

760 # and the removal of all its own related fields, and if it's 

761 # a through model the field that references it. 

762 dependencies = [] 

763 relations = self.from_state.relations 

764 for ( 

765 related_object_package_label, 

766 object_name, 

767 ), relation_related_fields in relations[package_label, model_name].items(): 

768 for field_name, field in relation_related_fields.items(): 

769 dependencies.append( 

770 (related_object_package_label, object_name, field_name, False), 

771 ) 

772 if not field.many_to_many: 

773 dependencies.append( 

774 ( 

775 related_object_package_label, 

776 object_name, 

777 field_name, 

778 "alter", 

779 ), 

780 ) 

781 

782 for name in sorted(related_fields): 

783 dependencies.append((package_label, model_name, name, False)) 

784 # We're referenced in another field's through= 

785 through_user = self.through_users.get( 

786 (package_label, model_state.name_lower) 

787 ) 

788 if through_user: 

789 dependencies.append( 

790 (through_user[0], through_user[1], through_user[2], False) 

791 ) 

792 # Finally, make the operation, deduping any dependencies 

793 self.add_operation( 

794 package_label, 

795 operations.DeleteModel( 

796 name=model_state.name, 

797 ), 

798 dependencies=list(set(dependencies)), 

799 ) 

800 

801 def create_renamed_fields(self): 

802 """Work out renamed fields.""" 

803 self.renamed_operations = [] 

804 old_field_keys = self.old_field_keys.copy() 

805 for package_label, model_name, field_name in sorted( 

806 self.new_field_keys - old_field_keys 

807 ): 

808 old_model_name = self.renamed_models.get( 

809 (package_label, model_name), model_name 

810 ) 

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

812 new_model_state = self.to_state.models[package_label, model_name] 

813 field = new_model_state.get_field(field_name) 

814 # Scan to see if this is actually a rename! 

815 field_dec = self.deep_deconstruct(field) 

816 for rem_package_label, rem_model_name, rem_field_name in sorted( 

817 old_field_keys - self.new_field_keys 

818 ): 

819 if rem_package_label == package_label and rem_model_name == model_name: 

820 old_field = old_model_state.get_field(rem_field_name) 

821 old_field_dec = self.deep_deconstruct(old_field) 

822 if ( 

823 field.remote_field 

824 and field.remote_field.model 

825 and "to" in old_field_dec[2] 

826 ): 

827 old_rel_to = old_field_dec[2]["to"] 

828 if old_rel_to in self.renamed_models_rel: 

829 old_field_dec[2]["to"] = self.renamed_models_rel[old_rel_to] 

830 old_field.set_attributes_from_name(rem_field_name) 

831 old_db_column = old_field.get_attname_column()[1] 

832 if old_field_dec == field_dec or ( 

833 # Was the field renamed and db_column equal to the 

834 # old field's column added? 

835 old_field_dec[0:2] == field_dec[0:2] 

836 and dict(old_field_dec[2], db_column=old_db_column) 

837 == field_dec[2] 

838 ): 

839 if self.questioner.ask_rename( 

840 model_name, rem_field_name, field_name, field 

841 ): 

842 self.renamed_operations.append( 

843 ( 

844 rem_package_label, 

845 rem_model_name, 

846 old_field.db_column, 

847 rem_field_name, 

848 package_label, 

849 model_name, 

850 field, 

851 field_name, 

852 ) 

853 ) 

854 old_field_keys.remove( 

855 (rem_package_label, rem_model_name, rem_field_name) 

856 ) 

857 old_field_keys.add((package_label, model_name, field_name)) 

858 self.renamed_fields[ 

859 package_label, model_name, field_name 

860 ] = rem_field_name 

861 break 

862 

863 def generate_renamed_fields(self): 

864 """Generate RenameField operations.""" 

865 for ( 

866 rem_package_label, 

867 rem_model_name, 

868 rem_db_column, 

869 rem_field_name, 

870 package_label, 

871 model_name, 

872 field, 

873 field_name, 

874 ) in self.renamed_operations: 

875 # A db_column mismatch requires a prior noop AlterField for the 

876 # subsequent RenameField to be a noop on attempts at preserving the 

877 # old name. 

878 if rem_db_column != field.db_column: 

879 altered_field = field.clone() 

880 altered_field.name = rem_field_name 

881 self.add_operation( 

882 package_label, 

883 operations.AlterField( 

884 model_name=model_name, 

885 name=rem_field_name, 

886 field=altered_field, 

887 ), 

888 ) 

889 self.add_operation( 

890 package_label, 

891 operations.RenameField( 

892 model_name=model_name, 

893 old_name=rem_field_name, 

894 new_name=field_name, 

895 ), 

896 ) 

897 self.old_field_keys.remove( 

898 (rem_package_label, rem_model_name, rem_field_name) 

899 ) 

900 self.old_field_keys.add((package_label, model_name, field_name)) 

901 

902 def generate_added_fields(self): 

903 """Make AddField operations.""" 

904 for package_label, model_name, field_name in sorted( 

905 self.new_field_keys - self.old_field_keys 

906 ): 

907 self._generate_added_field(package_label, model_name, field_name) 

908 

909 def _generate_added_field(self, package_label, model_name, field_name): 

910 field = self.to_state.models[package_label, model_name].get_field(field_name) 

911 # Adding a field always depends at least on its removal. 

912 dependencies = [(package_label, model_name, field_name, False)] 

913 # Fields that are foreignkeys/m2ms depend on stuff. 

914 if field.remote_field and field.remote_field.model: 

915 dependencies.extend( 

916 self._get_dependencies_for_foreign_key( 

917 package_label, 

918 model_name, 

919 field, 

920 self.to_state, 

921 ) 

922 ) 

923 # You can't just add NOT NULL fields with no default or fields 

924 # which don't allow empty strings as default. 

925 time_fields = (models.DateField, models.DateTimeField, models.TimeField) 

926 preserve_default = ( 

927 field.null 

928 or field.has_default() 

929 or field.many_to_many 

930 or (field.blank and field.empty_strings_allowed) 

931 or (isinstance(field, time_fields) and field.auto_now) 

932 ) 

933 if not preserve_default: 

934 field = field.clone() 

935 if isinstance(field, time_fields) and field.auto_now_add: 

936 field.default = self.questioner.ask_auto_now_add_addition( 

937 field_name, model_name 

938 ) 

939 else: 

940 field.default = self.questioner.ask_not_null_addition( 

941 field_name, model_name 

942 ) 

943 if ( 

944 field.unique 

945 and field.default is not models.NOT_PROVIDED 

946 and callable(field.default) 

947 ): 

948 self.questioner.ask_unique_callable_default_addition(field_name, model_name) 

949 self.add_operation( 

950 package_label, 

951 operations.AddField( 

952 model_name=model_name, 

953 name=field_name, 

954 field=field, 

955 preserve_default=preserve_default, 

956 ), 

957 dependencies=dependencies, 

958 ) 

959 

960 def generate_removed_fields(self): 

961 """Make RemoveField operations.""" 

962 for package_label, model_name, field_name in sorted( 

963 self.old_field_keys - self.new_field_keys 

964 ): 

965 self._generate_removed_field(package_label, model_name, field_name) 

966 

967 def _generate_removed_field(self, package_label, model_name, field_name): 

968 self.add_operation( 

969 package_label, 

970 operations.RemoveField( 

971 model_name=model_name, 

972 name=field_name, 

973 ), 

974 # We might need to depend on the removal of an 

975 # order_with_respect_to or index operation; 

976 # this is safely ignored if there isn't one 

977 dependencies=[ 

978 (package_label, model_name, field_name, "order_wrt_unset"), 

979 ], 

980 ) 

981 

982 def generate_altered_fields(self): 

983 """ 

984 Make AlterField operations, or possibly RemovedField/AddField if alter 

985 isn't possible. 

986 """ 

987 for package_label, model_name, field_name in sorted( 

988 self.old_field_keys & self.new_field_keys 

989 ): 

990 # Did the field change? 

991 old_model_name = self.renamed_models.get( 

992 (package_label, model_name), model_name 

993 ) 

994 old_field_name = self.renamed_fields.get( 

995 (package_label, model_name, field_name), field_name 

996 ) 

997 old_field = self.from_state.models[package_label, old_model_name].get_field( 

998 old_field_name 

999 ) 

1000 new_field = self.to_state.models[package_label, model_name].get_field( 

1001 field_name 

1002 ) 

1003 dependencies = [] 

1004 # Implement any model renames on relations; these are handled by RenameModel 

1005 # so we need to exclude them from the comparison 

1006 if hasattr(new_field, "remote_field") and getattr( 

1007 new_field.remote_field, "model", None 

1008 ): 

1009 rename_key = resolve_relation( 

1010 new_field.remote_field.model, package_label, model_name 

1011 ) 

1012 if rename_key in self.renamed_models: 

1013 new_field.remote_field.model = old_field.remote_field.model 

1014 # Handle ForeignKey which can only have a single to_field. 

1015 remote_field_name = getattr(new_field.remote_field, "field_name", None) 

1016 if remote_field_name: 

1017 to_field_rename_key = rename_key + (remote_field_name,) 

1018 if to_field_rename_key in self.renamed_fields: 

1019 # Repoint both model and field name because to_field 

1020 # inclusion in ForeignKey.deconstruct() is based on 

1021 # both. 

1022 new_field.remote_field.model = old_field.remote_field.model 

1023 new_field.remote_field.field_name = ( 

1024 old_field.remote_field.field_name 

1025 ) 

1026 # Handle ForeignObjects which can have multiple from_fields/to_fields. 

1027 from_fields = getattr(new_field, "from_fields", None) 

1028 if from_fields: 

1029 from_rename_key = (package_label, model_name) 

1030 new_field.from_fields = tuple( 

1031 [ 

1032 self.renamed_fields.get( 

1033 from_rename_key + (from_field,), from_field 

1034 ) 

1035 for from_field in from_fields 

1036 ] 

1037 ) 

1038 new_field.to_fields = tuple( 

1039 [ 

1040 self.renamed_fields.get(rename_key + (to_field,), to_field) 

1041 for to_field in new_field.to_fields 

1042 ] 

1043 ) 

1044 dependencies.extend( 

1045 self._get_dependencies_for_foreign_key( 

1046 package_label, 

1047 model_name, 

1048 new_field, 

1049 self.to_state, 

1050 ) 

1051 ) 

1052 if hasattr(new_field, "remote_field") and getattr( 

1053 new_field.remote_field, "through", None 

1054 ): 

1055 rename_key = resolve_relation( 

1056 new_field.remote_field.through, package_label, model_name 

1057 ) 

1058 if rename_key in self.renamed_models: 

1059 new_field.remote_field.through = old_field.remote_field.through 

1060 old_field_dec = self.deep_deconstruct(old_field) 

1061 new_field_dec = self.deep_deconstruct(new_field) 

1062 # If the field was confirmed to be renamed it means that only 

1063 # db_column was allowed to change which generate_renamed_fields() 

1064 # already accounts for by adding an AlterField operation. 

1065 if old_field_dec != new_field_dec and old_field_name == field_name: 

1066 both_m2m = old_field.many_to_many and new_field.many_to_many 

1067 neither_m2m = not old_field.many_to_many and not new_field.many_to_many 

1068 if both_m2m or neither_m2m: 

1069 # Either both fields are m2m or neither is 

1070 preserve_default = True 

1071 if ( 

1072 old_field.null 

1073 and not new_field.null 

1074 and not new_field.has_default() 

1075 and not new_field.many_to_many 

1076 ): 

1077 field = new_field.clone() 

1078 new_default = self.questioner.ask_not_null_alteration( 

1079 field_name, model_name 

1080 ) 

1081 if new_default is not models.NOT_PROVIDED: 

1082 field.default = new_default 

1083 preserve_default = False 

1084 else: 

1085 field = new_field 

1086 self.add_operation( 

1087 package_label, 

1088 operations.AlterField( 

1089 model_name=model_name, 

1090 name=field_name, 

1091 field=field, 

1092 preserve_default=preserve_default, 

1093 ), 

1094 dependencies=dependencies, 

1095 ) 

1096 else: 

1097 # We cannot alter between m2m and concrete fields 

1098 self._generate_removed_field(package_label, model_name, field_name) 

1099 self._generate_added_field(package_label, model_name, field_name) 

1100 

1101 def create_altered_indexes(self): 

1102 option_name = operations.AddIndex.option_name 

1103 

1104 for package_label, model_name in sorted(self.kept_model_keys): 

1105 old_model_name = self.renamed_models.get( 

1106 (package_label, model_name), model_name 

1107 ) 

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

1109 new_model_state = self.to_state.models[package_label, model_name] 

1110 

1111 old_indexes = old_model_state.options[option_name] 

1112 new_indexes = new_model_state.options[option_name] 

1113 added_indexes = [idx for idx in new_indexes if idx not in old_indexes] 

1114 removed_indexes = [idx for idx in old_indexes if idx not in new_indexes] 

1115 renamed_indexes = [] 

1116 # Find renamed indexes. 

1117 remove_from_added = [] 

1118 remove_from_removed = [] 

1119 for new_index in added_indexes: 

1120 new_index_dec = new_index.deconstruct() 

1121 new_index_name = new_index_dec[2].pop("name") 

1122 for old_index in removed_indexes: 

1123 old_index_dec = old_index.deconstruct() 

1124 old_index_name = old_index_dec[2].pop("name") 

1125 # Indexes are the same except for the names. 

1126 if ( 

1127 new_index_dec == old_index_dec 

1128 and new_index_name != old_index_name 

1129 ): 

1130 renamed_indexes.append((old_index_name, new_index_name, None)) 

1131 remove_from_added.append(new_index) 

1132 remove_from_removed.append(old_index) 

1133 

1134 # Remove renamed indexes from the lists of added and removed 

1135 # indexes. 

1136 added_indexes = [ 

1137 idx for idx in added_indexes if idx not in remove_from_added 

1138 ] 

1139 removed_indexes = [ 

1140 idx for idx in removed_indexes if idx not in remove_from_removed 

1141 ] 

1142 

1143 self.altered_indexes.update( 

1144 { 

1145 (package_label, model_name): { 

1146 "added_indexes": added_indexes, 

1147 "removed_indexes": removed_indexes, 

1148 "renamed_indexes": renamed_indexes, 

1149 } 

1150 } 

1151 ) 

1152 

1153 def generate_added_indexes(self): 

1154 for (package_label, model_name), alt_indexes in self.altered_indexes.items(): 

1155 dependencies = self._get_dependencies_for_model(package_label, model_name) 

1156 for index in alt_indexes["added_indexes"]: 

1157 self.add_operation( 

1158 package_label, 

1159 operations.AddIndex( 

1160 model_name=model_name, 

1161 index=index, 

1162 ), 

1163 dependencies=dependencies, 

1164 ) 

1165 

1166 def generate_removed_indexes(self): 

1167 for (package_label, model_name), alt_indexes in self.altered_indexes.items(): 

1168 for index in alt_indexes["removed_indexes"]: 

1169 self.add_operation( 

1170 package_label, 

1171 operations.RemoveIndex( 

1172 model_name=model_name, 

1173 name=index.name, 

1174 ), 

1175 ) 

1176 

1177 def generate_renamed_indexes(self): 

1178 for (package_label, model_name), alt_indexes in self.altered_indexes.items(): 

1179 for old_index_name, new_index_name, old_fields in alt_indexes[ 

1180 "renamed_indexes" 

1181 ]: 

1182 self.add_operation( 

1183 package_label, 

1184 operations.RenameIndex( 

1185 model_name=model_name, 

1186 new_name=new_index_name, 

1187 old_name=old_index_name, 

1188 old_fields=old_fields, 

1189 ), 

1190 ) 

1191 

1192 def create_altered_constraints(self): 

1193 option_name = operations.AddConstraint.option_name 

1194 for package_label, model_name in sorted(self.kept_model_keys): 

1195 old_model_name = self.renamed_models.get( 

1196 (package_label, model_name), model_name 

1197 ) 

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

1199 new_model_state = self.to_state.models[package_label, model_name] 

1200 

1201 old_constraints = old_model_state.options[option_name] 

1202 new_constraints = new_model_state.options[option_name] 

1203 add_constraints = [c for c in new_constraints if c not in old_constraints] 

1204 rem_constraints = [c for c in old_constraints if c not in new_constraints] 

1205 

1206 self.altered_constraints.update( 

1207 { 

1208 (package_label, model_name): { 

1209 "added_constraints": add_constraints, 

1210 "removed_constraints": rem_constraints, 

1211 } 

1212 } 

1213 ) 

1214 

1215 def generate_added_constraints(self): 

1216 for ( 

1217 package_label, 

1218 model_name, 

1219 ), alt_constraints in self.altered_constraints.items(): 

1220 dependencies = self._get_dependencies_for_model(package_label, model_name) 

1221 for constraint in alt_constraints["added_constraints"]: 

1222 self.add_operation( 

1223 package_label, 

1224 operations.AddConstraint( 

1225 model_name=model_name, 

1226 constraint=constraint, 

1227 ), 

1228 dependencies=dependencies, 

1229 ) 

1230 

1231 def generate_removed_constraints(self): 

1232 for ( 

1233 package_label, 

1234 model_name, 

1235 ), alt_constraints in self.altered_constraints.items(): 

1236 for constraint in alt_constraints["removed_constraints"]: 

1237 self.add_operation( 

1238 package_label, 

1239 operations.RemoveConstraint( 

1240 model_name=model_name, 

1241 name=constraint.name, 

1242 ), 

1243 ) 

1244 

1245 @staticmethod 

1246 def _get_dependencies_for_foreign_key( 

1247 package_label, model_name, field, project_state 

1248 ): 

1249 remote_field_model = None 

1250 if hasattr(field.remote_field, "model"): 

1251 remote_field_model = field.remote_field.model 

1252 else: 

1253 relations = project_state.relations[package_label, model_name] 

1254 for (remote_package_label, remote_model_name), fields in relations.items(): 

1255 if any( 

1256 field == related_field.remote_field 

1257 for related_field in fields.values() 

1258 ): 

1259 remote_field_model = f"{remote_package_label}.{remote_model_name}" 

1260 break 

1261 # Account for FKs to swappable models 

1262 swappable_setting = getattr(field, "swappable_setting", None) 

1263 if swappable_setting is not None: 

1264 dep_package_label = "__setting__" 

1265 dep_object_name = swappable_setting 

1266 else: 

1267 dep_package_label, dep_object_name = resolve_relation( 

1268 remote_field_model, 

1269 package_label, 

1270 model_name, 

1271 ) 

1272 dependencies = [(dep_package_label, dep_object_name, None, True)] 

1273 if getattr(field.remote_field, "through", None): 

1274 through_package_label, through_object_name = resolve_relation( 

1275 field.remote_field.through, 

1276 package_label, 

1277 model_name, 

1278 ) 

1279 dependencies.append( 

1280 (through_package_label, through_object_name, None, True) 

1281 ) 

1282 return dependencies 

1283 

1284 def _get_dependencies_for_model(self, package_label, model_name): 

1285 """Return foreign key dependencies of the given model.""" 

1286 dependencies = [] 

1287 model_state = self.to_state.models[package_label, model_name] 

1288 for field in model_state.fields.values(): 

1289 if field.is_relation: 

1290 dependencies.extend( 

1291 self._get_dependencies_for_foreign_key( 

1292 package_label, 

1293 model_name, 

1294 field, 

1295 self.to_state, 

1296 ) 

1297 ) 

1298 return dependencies 

1299 

1300 def _get_altered_foo_together_operations(self, option_name): 

1301 for package_label, model_name in sorted(self.kept_model_keys): 

1302 old_model_name = self.renamed_models.get( 

1303 (package_label, model_name), model_name 

1304 ) 

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

1306 new_model_state = self.to_state.models[package_label, model_name] 

1307 

1308 # We run the old version through the field renames to account for those 

1309 old_value = old_model_state.options.get(option_name) 

1310 old_value = ( 

1311 { 

1312 tuple( 

1313 self.renamed_fields.get((package_label, model_name, n), n) 

1314 for n in unique 

1315 ) 

1316 for unique in old_value 

1317 } 

1318 if old_value 

1319 else set() 

1320 ) 

1321 

1322 new_value = new_model_state.options.get(option_name) 

1323 new_value = set(new_value) if new_value else set() 

1324 

1325 if old_value != new_value: 

1326 dependencies = [] 

1327 for foo_togethers in new_value: 

1328 for field_name in foo_togethers: 

1329 field = new_model_state.get_field(field_name) 

1330 if field.remote_field and field.remote_field.model: 

1331 dependencies.extend( 

1332 self._get_dependencies_for_foreign_key( 

1333 package_label, 

1334 model_name, 

1335 field, 

1336 self.to_state, 

1337 ) 

1338 ) 

1339 yield ( 

1340 old_value, 

1341 new_value, 

1342 package_label, 

1343 model_name, 

1344 dependencies, 

1345 ) 

1346 

1347 def _generate_removed_altered_foo_together(self, operation): 

1348 for ( 

1349 old_value, 

1350 new_value, 

1351 package_label, 

1352 model_name, 

1353 dependencies, 

1354 ) in self._get_altered_foo_together_operations(operation.option_name): 

1355 removal_value = new_value.intersection(old_value) 

1356 if removal_value or old_value: 

1357 self.add_operation( 

1358 package_label, 

1359 operation( 

1360 name=model_name, **{operation.option_name: removal_value} 

1361 ), 

1362 dependencies=dependencies, 

1363 ) 

1364 

1365 def generate_altered_db_table(self): 

1366 models_to_check = self.kept_model_keys.union(self.kept_unmanaged_keys) 

1367 for package_label, model_name in sorted(models_to_check): 

1368 old_model_name = self.renamed_models.get( 

1369 (package_label, model_name), model_name 

1370 ) 

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

1372 new_model_state = self.to_state.models[package_label, model_name] 

1373 old_db_table_name = old_model_state.options.get("db_table") 

1374 new_db_table_name = new_model_state.options.get("db_table") 

1375 if old_db_table_name != new_db_table_name: 

1376 self.add_operation( 

1377 package_label, 

1378 operations.AlterModelTable( 

1379 name=model_name, 

1380 table=new_db_table_name, 

1381 ), 

1382 ) 

1383 

1384 def generate_altered_db_table_comment(self): 

1385 models_to_check = self.kept_model_keys.union(self.kept_unmanaged_keys) 

1386 for package_label, model_name in sorted(models_to_check): 

1387 old_model_name = self.renamed_models.get( 

1388 (package_label, model_name), model_name 

1389 ) 

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

1391 new_model_state = self.to_state.models[package_label, model_name] 

1392 

1393 old_db_table_comment = old_model_state.options.get("db_table_comment") 

1394 new_db_table_comment = new_model_state.options.get("db_table_comment") 

1395 if old_db_table_comment != new_db_table_comment: 

1396 self.add_operation( 

1397 package_label, 

1398 operations.AlterModelTableComment( 

1399 name=model_name, 

1400 table_comment=new_db_table_comment, 

1401 ), 

1402 ) 

1403 

1404 def generate_altered_options(self): 

1405 """ 

1406 Work out if any non-schema-affecting options have changed and make an 

1407 operation to represent them in state changes (in case Python code in 

1408 migrations needs them). 

1409 """ 

1410 models_to_check = self.kept_model_keys.union( 

1411 self.kept_unmanaged_keys, 

1412 # unmanaged converted to managed 

1413 self.old_unmanaged_keys & self.new_model_keys, 

1414 # managed converted to unmanaged 

1415 self.old_model_keys & self.new_unmanaged_keys, 

1416 ) 

1417 

1418 for package_label, model_name in sorted(models_to_check): 

1419 old_model_name = self.renamed_models.get( 

1420 (package_label, model_name), model_name 

1421 ) 

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

1423 new_model_state = self.to_state.models[package_label, model_name] 

1424 old_options = { 

1425 key: value 

1426 for key, value in old_model_state.options.items() 

1427 if key in AlterModelOptions.ALTER_OPTION_KEYS 

1428 } 

1429 new_options = { 

1430 key: value 

1431 for key, value in new_model_state.options.items() 

1432 if key in AlterModelOptions.ALTER_OPTION_KEYS 

1433 } 

1434 if old_options != new_options: 

1435 self.add_operation( 

1436 package_label, 

1437 operations.AlterModelOptions( 

1438 name=model_name, 

1439 options=new_options, 

1440 ), 

1441 ) 

1442 

1443 def generate_altered_order_with_respect_to(self): 

1444 for package_label, model_name in sorted(self.kept_model_keys): 

1445 old_model_name = self.renamed_models.get( 

1446 (package_label, model_name), model_name 

1447 ) 

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

1449 new_model_state = self.to_state.models[package_label, model_name] 

1450 if old_model_state.options.get( 

1451 "order_with_respect_to" 

1452 ) != new_model_state.options.get("order_with_respect_to"): 

1453 # Make sure it comes second if we're adding 

1454 # (removal dependency is part of RemoveField) 

1455 dependencies = [] 

1456 if new_model_state.options.get("order_with_respect_to"): 

1457 dependencies.append( 

1458 ( 

1459 package_label, 

1460 model_name, 

1461 new_model_state.options["order_with_respect_to"], 

1462 True, 

1463 ) 

1464 ) 

1465 # Actually generate the operation 

1466 self.add_operation( 

1467 package_label, 

1468 operations.AlterOrderWithRespectTo( 

1469 name=model_name, 

1470 order_with_respect_to=new_model_state.options.get( 

1471 "order_with_respect_to" 

1472 ), 

1473 ), 

1474 dependencies=dependencies, 

1475 ) 

1476 

1477 def generate_altered_managers(self): 

1478 for package_label, model_name in sorted(self.kept_model_keys): 

1479 old_model_name = self.renamed_models.get( 

1480 (package_label, model_name), model_name 

1481 ) 

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

1483 new_model_state = self.to_state.models[package_label, model_name] 

1484 if old_model_state.managers != new_model_state.managers: 

1485 self.add_operation( 

1486 package_label, 

1487 operations.AlterModelManagers( 

1488 name=model_name, 

1489 managers=new_model_state.managers, 

1490 ), 

1491 ) 

1492 

1493 def arrange_for_graph(self, changes, graph, migration_name=None): 

1494 """ 

1495 Take a result from changes() and a MigrationGraph, and fix the names 

1496 and dependencies of the changes so they extend the graph from the leaf 

1497 nodes for each app. 

1498 """ 

1499 leaves = graph.leaf_nodes() 

1500 name_map = {} 

1501 for package_label, migrations in list(changes.items()): 

1502 if not migrations: 

1503 continue 

1504 # Find the app label's current leaf node 

1505 app_leaf = None 

1506 for leaf in leaves: 

1507 if leaf[0] == package_label: 

1508 app_leaf = leaf 

1509 break 

1510 # Do they want an initial migration for this app? 

1511 if app_leaf is None and not self.questioner.ask_initial(package_label): 

1512 # They don't. 

1513 for migration in migrations: 

1514 name_map[(package_label, migration.name)] = ( 

1515 package_label, 

1516 "__first__", 

1517 ) 

1518 del changes[package_label] 

1519 continue 

1520 # Work out the next number in the sequence 

1521 if app_leaf is None: 

1522 next_number = 1 

1523 else: 

1524 next_number = (self.parse_number(app_leaf[1]) or 0) + 1 

1525 # Name each migration 

1526 for i, migration in enumerate(migrations): 

1527 if i == 0 and app_leaf: 

1528 migration.dependencies.append(app_leaf) 

1529 new_name_parts = ["%04i" % next_number] 

1530 if migration_name: 

1531 new_name_parts.append(migration_name) 

1532 elif i == 0 and not app_leaf: 

1533 new_name_parts.append("initial") 

1534 else: 

1535 new_name_parts.append(migration.suggest_name()[:100]) 

1536 new_name = "_".join(new_name_parts) 

1537 name_map[(package_label, migration.name)] = (package_label, new_name) 

1538 next_number += 1 

1539 migration.name = new_name 

1540 # Now fix dependencies 

1541 for migrations in changes.values(): 

1542 for migration in migrations: 

1543 migration.dependencies = [ 

1544 name_map.get(d, d) for d in migration.dependencies 

1545 ] 

1546 return changes 

1547 

1548 def _trim_to_packages(self, changes, package_labels): 

1549 """ 

1550 Take changes from arrange_for_graph() and set of app labels, and return 

1551 a modified set of changes which trims out as many migrations that are 

1552 not in package_labels as possible. Note that some other migrations may 

1553 still be present as they may be required dependencies. 

1554 """ 

1555 # Gather other app dependencies in a first pass 

1556 app_dependencies = {} 

1557 for package_label, migrations in changes.items(): 

1558 for migration in migrations: 

1559 for dep_package_label, name in migration.dependencies: 

1560 app_dependencies.setdefault(package_label, set()).add( 

1561 dep_package_label 

1562 ) 

1563 required_packages = set(package_labels) 

1564 # Keep resolving till there's no change 

1565 old_required_packages = None 

1566 while old_required_packages != required_packages: 

1567 old_required_packages = set(required_packages) 

1568 required_packages.update( 

1569 *[ 

1570 app_dependencies.get(package_label, ()) 

1571 for package_label in required_packages 

1572 ] 

1573 ) 

1574 # Remove all migrations that aren't needed 

1575 for package_label in list(changes): 

1576 if package_label not in required_packages: 

1577 del changes[package_label] 

1578 return changes 

1579 

1580 @classmethod 

1581 def parse_number(cls, name): 

1582 """ 

1583 Given a migration name, try to extract a number from the beginning of 

1584 it. For a squashed migration such as '0001_squashed_0004…', return the 

1585 second number. If no number is found, return None. 

1586 """ 

1587 if squashed_match := re.search(r".*_squashed_(\d+)", name): 

1588 return int(squashed_match[1]) 

1589 match = re.match(r"^\d+", name) 

1590 if match: 

1591 return int(match[0]) 

1592 return None