Coverage for /Users/davegaeddert/Development/dropseed/plain/plain-models/plain/models/migrations/operations/models.py: 34%

464 statements  

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

1from plain import models 

2from plain.models.migrations.operations.base import Operation 

3from plain.models.migrations.state import ModelState 

4from plain.models.migrations.utils import field_references, resolve_relation 

5from plain.utils.functional import cached_property 

6 

7from .fields import AddField, AlterField, FieldOperation, RemoveField, RenameField 

8 

9 

10def _check_for_duplicates(arg_name, objs): 

11 used_vals = set() 

12 for val in objs: 

13 if val in used_vals: 

14 raise ValueError( 

15 f"Found duplicate value {val} in CreateModel {arg_name} argument." 

16 ) 

17 used_vals.add(val) 

18 

19 

20class ModelOperation(Operation): 

21 def __init__(self, name): 

22 self.name = name 

23 

24 @cached_property 

25 def name_lower(self): 

26 return self.name.lower() 

27 

28 def references_model(self, name, package_label): 

29 return name.lower() == self.name_lower 

30 

31 def reduce(self, operation, package_label): 

32 return super().reduce(operation, package_label) or self.can_reduce_through( 

33 operation, package_label 

34 ) 

35 

36 def can_reduce_through(self, operation, package_label): 

37 return not operation.references_model(self.name, package_label) 

38 

39 

40class CreateModel(ModelOperation): 

41 """Create a model's table.""" 

42 

43 serialization_expand_args = ["fields", "options", "managers"] 

44 

45 def __init__(self, name, fields, options=None, bases=None, managers=None): 

46 self.fields = fields 

47 self.options = options or {} 

48 self.bases = bases or (models.Model,) 

49 self.managers = managers or [] 

50 super().__init__(name) 

51 # Sanity-check that there are no duplicated field names, bases, or 

52 # manager names 

53 _check_for_duplicates("fields", (name for name, _ in self.fields)) 

54 _check_for_duplicates( 

55 "bases", 

56 ( 

57 base._meta.label_lower 

58 if hasattr(base, "_meta") 

59 else base.lower() 

60 if isinstance(base, str) 

61 else base 

62 for base in self.bases 

63 ), 

64 ) 

65 _check_for_duplicates("managers", (name for name, _ in self.managers)) 

66 

67 def deconstruct(self): 

68 kwargs = { 

69 "name": self.name, 

70 "fields": self.fields, 

71 } 

72 if self.options: 

73 kwargs["options"] = self.options 

74 if self.bases and self.bases != (models.Model,): 

75 kwargs["bases"] = self.bases 

76 if self.managers and self.managers != [("objects", models.Manager())]: 

77 kwargs["managers"] = self.managers 

78 return (self.__class__.__qualname__, [], kwargs) 

79 

80 def state_forwards(self, package_label, state): 

81 state.add_model( 

82 ModelState( 

83 package_label, 

84 self.name, 

85 list(self.fields), 

86 dict(self.options), 

87 tuple(self.bases), 

88 list(self.managers), 

89 ) 

90 ) 

91 

92 def database_forwards(self, package_label, schema_editor, from_state, to_state): 

93 model = to_state.packages.get_model(package_label, self.name) 

94 if self.allow_migrate_model(schema_editor.connection.alias, model): 

95 schema_editor.create_model(model) 

96 

97 def database_backwards(self, package_label, schema_editor, from_state, to_state): 

98 model = from_state.packages.get_model(package_label, self.name) 

99 if self.allow_migrate_model(schema_editor.connection.alias, model): 

100 schema_editor.delete_model(model) 

101 

102 def describe(self): 

103 return f"Create model {self.name}" 

104 

105 @property 

106 def migration_name_fragment(self): 

107 return self.name_lower 

108 

109 def references_model(self, name, package_label): 

110 name_lower = name.lower() 

111 if name_lower == self.name_lower: 

112 return True 

113 

114 # Check we didn't inherit from the model 

115 reference_model_tuple = (package_label, name_lower) 

116 for base in self.bases: 

117 if ( 

118 base is not models.Model 

119 and isinstance(base, models.base.ModelBase | str) 

120 and resolve_relation(base, package_label) == reference_model_tuple 

121 ): 

122 return True 

123 

124 # Check we have no FKs/M2Ms with it 

125 for _name, field in self.fields: 

126 if field_references( 

127 (package_label, self.name_lower), field, reference_model_tuple 

128 ): 

129 return True 

130 return False 

131 

132 def reduce(self, operation, package_label): 

133 if ( 

134 isinstance(operation, DeleteModel) 

135 and self.name_lower == operation.name_lower 

136 ): 

137 return [] 

138 elif ( 

139 isinstance(operation, RenameModel) 

140 and self.name_lower == operation.old_name_lower 

141 ): 

142 return [ 

143 CreateModel( 

144 operation.new_name, 

145 fields=self.fields, 

146 options=self.options, 

147 bases=self.bases, 

148 managers=self.managers, 

149 ), 

150 ] 

151 elif ( 

152 isinstance(operation, AlterModelOptions) 

153 and self.name_lower == operation.name_lower 

154 ): 

155 options = {**self.options, **operation.options} 

156 for key in operation.ALTER_OPTION_KEYS: 

157 if key not in operation.options: 

158 options.pop(key, None) 

159 return [ 

160 CreateModel( 

161 self.name, 

162 fields=self.fields, 

163 options=options, 

164 bases=self.bases, 

165 managers=self.managers, 

166 ), 

167 ] 

168 elif ( 

169 isinstance(operation, AlterModelManagers) 

170 and self.name_lower == operation.name_lower 

171 ): 

172 return [ 

173 CreateModel( 

174 self.name, 

175 fields=self.fields, 

176 options=self.options, 

177 bases=self.bases, 

178 managers=operation.managers, 

179 ), 

180 ] 

181 elif ( 

182 isinstance(operation, AlterOrderWithRespectTo) 

183 and self.name_lower == operation.name_lower 

184 ): 

185 return [ 

186 CreateModel( 

187 self.name, 

188 fields=self.fields, 

189 options={ 

190 **self.options, 

191 "order_with_respect_to": operation.order_with_respect_to, 

192 }, 

193 bases=self.bases, 

194 managers=self.managers, 

195 ), 

196 ] 

197 elif ( 

198 isinstance(operation, FieldOperation) 

199 and self.name_lower == operation.model_name_lower 

200 ): 

201 if isinstance(operation, AddField): 

202 return [ 

203 CreateModel( 

204 self.name, 

205 fields=self.fields + [(operation.name, operation.field)], 

206 options=self.options, 

207 bases=self.bases, 

208 managers=self.managers, 

209 ), 

210 ] 

211 elif isinstance(operation, AlterField): 

212 return [ 

213 CreateModel( 

214 self.name, 

215 fields=[ 

216 (n, operation.field if n == operation.name else v) 

217 for n, v in self.fields 

218 ], 

219 options=self.options, 

220 bases=self.bases, 

221 managers=self.managers, 

222 ), 

223 ] 

224 elif isinstance(operation, RemoveField): 

225 options = self.options.copy() 

226 

227 order_with_respect_to = options.get("order_with_respect_to") 

228 if order_with_respect_to == operation.name_lower: 

229 del options["order_with_respect_to"] 

230 return [ 

231 CreateModel( 

232 self.name, 

233 fields=[ 

234 (n, v) 

235 for n, v in self.fields 

236 if n.lower() != operation.name_lower 

237 ], 

238 options=options, 

239 bases=self.bases, 

240 managers=self.managers, 

241 ), 

242 ] 

243 elif isinstance(operation, RenameField): 

244 options = self.options.copy() 

245 

246 order_with_respect_to = options.get("order_with_respect_to") 

247 if order_with_respect_to == operation.old_name: 

248 options["order_with_respect_to"] = operation.new_name 

249 return [ 

250 CreateModel( 

251 self.name, 

252 fields=[ 

253 (operation.new_name if n == operation.old_name else n, v) 

254 for n, v in self.fields 

255 ], 

256 options=options, 

257 bases=self.bases, 

258 managers=self.managers, 

259 ), 

260 ] 

261 return super().reduce(operation, package_label) 

262 

263 

264class DeleteModel(ModelOperation): 

265 """Drop a model's table.""" 

266 

267 def deconstruct(self): 

268 kwargs = { 

269 "name": self.name, 

270 } 

271 return (self.__class__.__qualname__, [], kwargs) 

272 

273 def state_forwards(self, package_label, state): 

274 state.remove_model(package_label, self.name_lower) 

275 

276 def database_forwards(self, package_label, schema_editor, from_state, to_state): 

277 model = from_state.packages.get_model(package_label, self.name) 

278 if self.allow_migrate_model(schema_editor.connection.alias, model): 

279 schema_editor.delete_model(model) 

280 

281 def database_backwards(self, package_label, schema_editor, from_state, to_state): 

282 model = to_state.packages.get_model(package_label, self.name) 

283 if self.allow_migrate_model(schema_editor.connection.alias, model): 

284 schema_editor.create_model(model) 

285 

286 def references_model(self, name, package_label): 

287 # The deleted model could be referencing the specified model through 

288 # related fields. 

289 return True 

290 

291 def describe(self): 

292 return "Delete model %s" % self.name 

293 

294 @property 

295 def migration_name_fragment(self): 

296 return "delete_%s" % self.name_lower 

297 

298 

299class RenameModel(ModelOperation): 

300 """Rename a model.""" 

301 

302 def __init__(self, old_name, new_name): 

303 self.old_name = old_name 

304 self.new_name = new_name 

305 super().__init__(old_name) 

306 

307 @cached_property 

308 def old_name_lower(self): 

309 return self.old_name.lower() 

310 

311 @cached_property 

312 def new_name_lower(self): 

313 return self.new_name.lower() 

314 

315 def deconstruct(self): 

316 kwargs = { 

317 "old_name": self.old_name, 

318 "new_name": self.new_name, 

319 } 

320 return (self.__class__.__qualname__, [], kwargs) 

321 

322 def state_forwards(self, package_label, state): 

323 state.rename_model(package_label, self.old_name, self.new_name) 

324 

325 def database_forwards(self, package_label, schema_editor, from_state, to_state): 

326 new_model = to_state.packages.get_model(package_label, self.new_name) 

327 if self.allow_migrate_model(schema_editor.connection.alias, new_model): 

328 old_model = from_state.packages.get_model(package_label, self.old_name) 

329 # Move the main table 

330 schema_editor.alter_db_table( 

331 new_model, 

332 old_model._meta.db_table, 

333 new_model._meta.db_table, 

334 ) 

335 # Alter the fields pointing to us 

336 for related_object in old_model._meta.related_objects: 

337 if related_object.related_model == old_model: 

338 model = new_model 

339 related_key = (package_label, self.new_name_lower) 

340 else: 

341 model = related_object.related_model 

342 related_key = ( 

343 related_object.related_model._meta.package_label, 

344 related_object.related_model._meta.model_name, 

345 ) 

346 to_field = to_state.packages.get_model(*related_key)._meta.get_field( 

347 related_object.field.name 

348 ) 

349 schema_editor.alter_field( 

350 model, 

351 related_object.field, 

352 to_field, 

353 ) 

354 # Rename M2M fields whose name is based on this model's name. 

355 fields = zip( 

356 old_model._meta.local_many_to_many, new_model._meta.local_many_to_many 

357 ) 

358 for old_field, new_field in fields: 

359 # Skip self-referential fields as these are renamed above. 

360 if ( 

361 new_field.model == new_field.related_model 

362 or not new_field.remote_field.through._meta.auto_created 

363 ): 

364 continue 

365 # Rename columns and the M2M table. 

366 schema_editor._alter_many_to_many( 

367 new_model, 

368 old_field, 

369 new_field, 

370 strict=False, 

371 ) 

372 

373 def database_backwards(self, package_label, schema_editor, from_state, to_state): 

374 self.new_name_lower, self.old_name_lower = ( 

375 self.old_name_lower, 

376 self.new_name_lower, 

377 ) 

378 self.new_name, self.old_name = self.old_name, self.new_name 

379 

380 self.database_forwards(package_label, schema_editor, from_state, to_state) 

381 

382 self.new_name_lower, self.old_name_lower = ( 

383 self.old_name_lower, 

384 self.new_name_lower, 

385 ) 

386 self.new_name, self.old_name = self.old_name, self.new_name 

387 

388 def references_model(self, name, package_label): 

389 return ( 

390 name.lower() == self.old_name_lower or name.lower() == self.new_name_lower 

391 ) 

392 

393 def describe(self): 

394 return f"Rename model {self.old_name} to {self.new_name}" 

395 

396 @property 

397 def migration_name_fragment(self): 

398 return f"rename_{self.old_name_lower}_{self.new_name_lower}" 

399 

400 def reduce(self, operation, package_label): 

401 if ( 

402 isinstance(operation, RenameModel) 

403 and self.new_name_lower == operation.old_name_lower 

404 ): 

405 return [ 

406 RenameModel( 

407 self.old_name, 

408 operation.new_name, 

409 ), 

410 ] 

411 # Skip `ModelOperation.reduce` as we want to run `references_model` 

412 # against self.new_name. 

413 return super(ModelOperation, self).reduce( 

414 operation, package_label 

415 ) or not operation.references_model(self.new_name, package_label) 

416 

417 

418class ModelOptionOperation(ModelOperation): 

419 def reduce(self, operation, package_label): 

420 if ( 

421 isinstance(operation, self.__class__ | DeleteModel) 

422 and self.name_lower == operation.name_lower 

423 ): 

424 return [operation] 

425 return super().reduce(operation, package_label) 

426 

427 

428class AlterModelTable(ModelOptionOperation): 

429 """Rename a model's table.""" 

430 

431 def __init__(self, name, table): 

432 self.table = table 

433 super().__init__(name) 

434 

435 def deconstruct(self): 

436 kwargs = { 

437 "name": self.name, 

438 "table": self.table, 

439 } 

440 return (self.__class__.__qualname__, [], kwargs) 

441 

442 def state_forwards(self, package_label, state): 

443 state.alter_model_options( 

444 package_label, self.name_lower, {"db_table": self.table} 

445 ) 

446 

447 def database_forwards(self, package_label, schema_editor, from_state, to_state): 

448 new_model = to_state.packages.get_model(package_label, self.name) 

449 if self.allow_migrate_model(schema_editor.connection.alias, new_model): 

450 old_model = from_state.packages.get_model(package_label, self.name) 

451 schema_editor.alter_db_table( 

452 new_model, 

453 old_model._meta.db_table, 

454 new_model._meta.db_table, 

455 ) 

456 # Rename M2M fields whose name is based on this model's db_table 

457 for old_field, new_field in zip( 

458 old_model._meta.local_many_to_many, new_model._meta.local_many_to_many 

459 ): 

460 if new_field.remote_field.through._meta.auto_created: 

461 schema_editor.alter_db_table( 

462 new_field.remote_field.through, 

463 old_field.remote_field.through._meta.db_table, 

464 new_field.remote_field.through._meta.db_table, 

465 ) 

466 

467 def database_backwards(self, package_label, schema_editor, from_state, to_state): 

468 return self.database_forwards( 

469 package_label, schema_editor, from_state, to_state 

470 ) 

471 

472 def describe(self): 

473 return "Rename table for {} to {}".format( 

474 self.name, 

475 self.table if self.table is not None else "(default)", 

476 ) 

477 

478 @property 

479 def migration_name_fragment(self): 

480 return "alter_%s_table" % self.name_lower 

481 

482 

483class AlterModelTableComment(ModelOptionOperation): 

484 def __init__(self, name, table_comment): 

485 self.table_comment = table_comment 

486 super().__init__(name) 

487 

488 def deconstruct(self): 

489 kwargs = { 

490 "name": self.name, 

491 "table_comment": self.table_comment, 

492 } 

493 return (self.__class__.__qualname__, [], kwargs) 

494 

495 def state_forwards(self, package_label, state): 

496 state.alter_model_options( 

497 package_label, self.name_lower, {"db_table_comment": self.table_comment} 

498 ) 

499 

500 def database_forwards(self, package_label, schema_editor, from_state, to_state): 

501 new_model = to_state.packages.get_model(package_label, self.name) 

502 if self.allow_migrate_model(schema_editor.connection.alias, new_model): 

503 old_model = from_state.packages.get_model(package_label, self.name) 

504 schema_editor.alter_db_table_comment( 

505 new_model, 

506 old_model._meta.db_table_comment, 

507 new_model._meta.db_table_comment, 

508 ) 

509 

510 def database_backwards(self, package_label, schema_editor, from_state, to_state): 

511 return self.database_forwards( 

512 package_label, schema_editor, from_state, to_state 

513 ) 

514 

515 def describe(self): 

516 return f"Alter {self.name} table comment" 

517 

518 @property 

519 def migration_name_fragment(self): 

520 return f"alter_{self.name_lower}_table_comment" 

521 

522 

523class AlterOrderWithRespectTo(ModelOptionOperation): 

524 """Represent a change with the order_with_respect_to option.""" 

525 

526 option_name = "order_with_respect_to" 

527 

528 def __init__(self, name, order_with_respect_to): 

529 self.order_with_respect_to = order_with_respect_to 

530 super().__init__(name) 

531 

532 def deconstruct(self): 

533 kwargs = { 

534 "name": self.name, 

535 "order_with_respect_to": self.order_with_respect_to, 

536 } 

537 return (self.__class__.__qualname__, [], kwargs) 

538 

539 def state_forwards(self, package_label, state): 

540 state.alter_model_options( 

541 package_label, 

542 self.name_lower, 

543 {self.option_name: self.order_with_respect_to}, 

544 ) 

545 

546 def database_forwards(self, package_label, schema_editor, from_state, to_state): 

547 to_model = to_state.packages.get_model(package_label, self.name) 

548 if self.allow_migrate_model(schema_editor.connection.alias, to_model): 

549 from_model = from_state.packages.get_model(package_label, self.name) 

550 # Remove a field if we need to 

551 if ( 

552 from_model._meta.order_with_respect_to 

553 and not to_model._meta.order_with_respect_to 

554 ): 

555 schema_editor.remove_field( 

556 from_model, from_model._meta.get_field("_order") 

557 ) 

558 # Add a field if we need to (altering the column is untouched as 

559 # it's likely a rename) 

560 elif ( 

561 to_model._meta.order_with_respect_to 

562 and not from_model._meta.order_with_respect_to 

563 ): 

564 field = to_model._meta.get_field("_order") 

565 if not field.has_default(): 

566 field.default = 0 

567 schema_editor.add_field( 

568 from_model, 

569 field, 

570 ) 

571 

572 def database_backwards(self, package_label, schema_editor, from_state, to_state): 

573 self.database_forwards(package_label, schema_editor, from_state, to_state) 

574 

575 def references_field(self, model_name, name, package_label): 

576 return self.references_model(model_name, package_label) and ( 

577 self.order_with_respect_to is None or name == self.order_with_respect_to 

578 ) 

579 

580 def describe(self): 

581 return "Set order_with_respect_to on {} to {}".format( 

582 self.name, 

583 self.order_with_respect_to, 

584 ) 

585 

586 @property 

587 def migration_name_fragment(self): 

588 return "alter_%s_order_with_respect_to" % self.name_lower 

589 

590 

591class AlterModelOptions(ModelOptionOperation): 

592 """ 

593 Set new model options that don't directly affect the database schema 

594 (like ordering). Python code in migrations 

595 may still need them. 

596 """ 

597 

598 # Model options we want to compare and preserve in an AlterModelOptions op 

599 ALTER_OPTION_KEYS = [ 

600 "base_manager_name", 

601 "default_manager_name", 

602 "default_related_name", 

603 "get_latest_by", 

604 "managed", 

605 "ordering", 

606 "select_on_save", 

607 ] 

608 

609 def __init__(self, name, options): 

610 self.options = options 

611 super().__init__(name) 

612 

613 def deconstruct(self): 

614 kwargs = { 

615 "name": self.name, 

616 "options": self.options, 

617 } 

618 return (self.__class__.__qualname__, [], kwargs) 

619 

620 def state_forwards(self, package_label, state): 

621 state.alter_model_options( 

622 package_label, 

623 self.name_lower, 

624 self.options, 

625 self.ALTER_OPTION_KEYS, 

626 ) 

627 

628 def database_forwards(self, package_label, schema_editor, from_state, to_state): 

629 pass 

630 

631 def database_backwards(self, package_label, schema_editor, from_state, to_state): 

632 pass 

633 

634 def describe(self): 

635 return "Change Meta options on %s" % self.name 

636 

637 @property 

638 def migration_name_fragment(self): 

639 return "alter_%s_options" % self.name_lower 

640 

641 

642class AlterModelManagers(ModelOptionOperation): 

643 """Alter the model's managers.""" 

644 

645 serialization_expand_args = ["managers"] 

646 

647 def __init__(self, name, managers): 

648 self.managers = managers 

649 super().__init__(name) 

650 

651 def deconstruct(self): 

652 return (self.__class__.__qualname__, [self.name, self.managers], {}) 

653 

654 def state_forwards(self, package_label, state): 

655 state.alter_model_managers(package_label, self.name_lower, self.managers) 

656 

657 def database_forwards(self, package_label, schema_editor, from_state, to_state): 

658 pass 

659 

660 def database_backwards(self, package_label, schema_editor, from_state, to_state): 

661 pass 

662 

663 def describe(self): 

664 return "Change managers on %s" % self.name 

665 

666 @property 

667 def migration_name_fragment(self): 

668 return "alter_%s_managers" % self.name_lower 

669 

670 

671class IndexOperation(Operation): 

672 option_name = "indexes" 

673 

674 @cached_property 

675 def model_name_lower(self): 

676 return self.model_name.lower() 

677 

678 

679class AddIndex(IndexOperation): 

680 """Add an index on a model.""" 

681 

682 def __init__(self, model_name, index): 

683 self.model_name = model_name 

684 if not index.name: 

685 raise ValueError( 

686 "Indexes passed to AddIndex operations require a name " 

687 "argument. %r doesn't have one." % index 

688 ) 

689 self.index = index 

690 

691 def state_forwards(self, package_label, state): 

692 state.add_index(package_label, self.model_name_lower, self.index) 

693 

694 def database_forwards(self, package_label, schema_editor, from_state, to_state): 

695 model = to_state.packages.get_model(package_label, self.model_name) 

696 if self.allow_migrate_model(schema_editor.connection.alias, model): 

697 schema_editor.add_index(model, self.index) 

698 

699 def database_backwards(self, package_label, schema_editor, from_state, to_state): 

700 model = from_state.packages.get_model(package_label, self.model_name) 

701 if self.allow_migrate_model(schema_editor.connection.alias, model): 

702 schema_editor.remove_index(model, self.index) 

703 

704 def deconstruct(self): 

705 kwargs = { 

706 "model_name": self.model_name, 

707 "index": self.index, 

708 } 

709 return ( 

710 self.__class__.__qualname__, 

711 [], 

712 kwargs, 

713 ) 

714 

715 def describe(self): 

716 if self.index.expressions: 

717 return "Create index {} on {} on model {}".format( 

718 self.index.name, 

719 ", ".join([str(expression) for expression in self.index.expressions]), 

720 self.model_name, 

721 ) 

722 return "Create index {} on field(s) {} of model {}".format( 

723 self.index.name, 

724 ", ".join(self.index.fields), 

725 self.model_name, 

726 ) 

727 

728 @property 

729 def migration_name_fragment(self): 

730 return f"{self.model_name_lower}_{self.index.name.lower()}" 

731 

732 

733class RemoveIndex(IndexOperation): 

734 """Remove an index from a model.""" 

735 

736 def __init__(self, model_name, name): 

737 self.model_name = model_name 

738 self.name = name 

739 

740 def state_forwards(self, package_label, state): 

741 state.remove_index(package_label, self.model_name_lower, self.name) 

742 

743 def database_forwards(self, package_label, schema_editor, from_state, to_state): 

744 model = from_state.packages.get_model(package_label, self.model_name) 

745 if self.allow_migrate_model(schema_editor.connection.alias, model): 

746 from_model_state = from_state.models[package_label, self.model_name_lower] 

747 index = from_model_state.get_index_by_name(self.name) 

748 schema_editor.remove_index(model, index) 

749 

750 def database_backwards(self, package_label, schema_editor, from_state, to_state): 

751 model = to_state.packages.get_model(package_label, self.model_name) 

752 if self.allow_migrate_model(schema_editor.connection.alias, model): 

753 to_model_state = to_state.models[package_label, self.model_name_lower] 

754 index = to_model_state.get_index_by_name(self.name) 

755 schema_editor.add_index(model, index) 

756 

757 def deconstruct(self): 

758 kwargs = { 

759 "model_name": self.model_name, 

760 "name": self.name, 

761 } 

762 return ( 

763 self.__class__.__qualname__, 

764 [], 

765 kwargs, 

766 ) 

767 

768 def describe(self): 

769 return f"Remove index {self.name} from {self.model_name}" 

770 

771 @property 

772 def migration_name_fragment(self): 

773 return f"remove_{self.model_name_lower}_{self.name.lower()}" 

774 

775 

776class RenameIndex(IndexOperation): 

777 """Rename an index.""" 

778 

779 def __init__(self, model_name, new_name, old_name=None, old_fields=None): 

780 if not old_name and not old_fields: 

781 raise ValueError( 

782 "RenameIndex requires one of old_name and old_fields arguments to be " 

783 "set." 

784 ) 

785 if old_name and old_fields: 

786 raise ValueError( 

787 "RenameIndex.old_name and old_fields are mutually exclusive." 

788 ) 

789 self.model_name = model_name 

790 self.new_name = new_name 

791 self.old_name = old_name 

792 self.old_fields = old_fields 

793 

794 @cached_property 

795 def old_name_lower(self): 

796 return self.old_name.lower() 

797 

798 @cached_property 

799 def new_name_lower(self): 

800 return self.new_name.lower() 

801 

802 def deconstruct(self): 

803 kwargs = { 

804 "model_name": self.model_name, 

805 "new_name": self.new_name, 

806 } 

807 if self.old_name: 

808 kwargs["old_name"] = self.old_name 

809 if self.old_fields: 

810 kwargs["old_fields"] = self.old_fields 

811 return (self.__class__.__qualname__, [], kwargs) 

812 

813 def state_forwards(self, package_label, state): 

814 if self.old_fields: 

815 state.add_index( 

816 package_label, 

817 self.model_name_lower, 

818 models.Index(fields=self.old_fields, name=self.new_name), 

819 ) 

820 else: 

821 state.rename_index( 

822 package_label, self.model_name_lower, self.old_name, self.new_name 

823 ) 

824 

825 def database_forwards(self, package_label, schema_editor, from_state, to_state): 

826 model = to_state.packages.get_model(package_label, self.model_name) 

827 if not self.allow_migrate_model(schema_editor.connection.alias, model): 

828 return 

829 

830 if self.old_fields: 

831 from_model = from_state.packages.get_model(package_label, self.model_name) 

832 columns = [ 

833 from_model._meta.get_field(field).column for field in self.old_fields 

834 ] 

835 matching_index_name = schema_editor._constraint_names( 

836 from_model, column_names=columns, index=True 

837 ) 

838 if len(matching_index_name) != 1: 

839 raise ValueError( 

840 "Found wrong number ({}) of indexes for {}({}).".format( 

841 len(matching_index_name), 

842 from_model._meta.db_table, 

843 ", ".join(columns), 

844 ) 

845 ) 

846 old_index = models.Index( 

847 fields=self.old_fields, 

848 name=matching_index_name[0], 

849 ) 

850 else: 

851 from_model_state = from_state.models[package_label, self.model_name_lower] 

852 old_index = from_model_state.get_index_by_name(self.old_name) 

853 # Don't alter when the index name is not changed. 

854 if old_index.name == self.new_name: 

855 return 

856 

857 to_model_state = to_state.models[package_label, self.model_name_lower] 

858 new_index = to_model_state.get_index_by_name(self.new_name) 

859 schema_editor.rename_index(model, old_index, new_index) 

860 

861 def database_backwards(self, package_label, schema_editor, from_state, to_state): 

862 if self.old_fields: 

863 # Backward operation with unnamed index is a no-op. 

864 return 

865 

866 self.new_name_lower, self.old_name_lower = ( 

867 self.old_name_lower, 

868 self.new_name_lower, 

869 ) 

870 self.new_name, self.old_name = self.old_name, self.new_name 

871 

872 self.database_forwards(package_label, schema_editor, from_state, to_state) 

873 

874 self.new_name_lower, self.old_name_lower = ( 

875 self.old_name_lower, 

876 self.new_name_lower, 

877 ) 

878 self.new_name, self.old_name = self.old_name, self.new_name 

879 

880 def describe(self): 

881 if self.old_name: 

882 return ( 

883 f"Rename index {self.old_name} on {self.model_name} to {self.new_name}" 

884 ) 

885 return ( 

886 f"Rename unnamed index for {self.old_fields} on {self.model_name} to " 

887 f"{self.new_name}" 

888 ) 

889 

890 @property 

891 def migration_name_fragment(self): 

892 if self.old_name: 

893 return f"rename_{self.old_name_lower}_{self.new_name_lower}" 

894 return "rename_{}_{}_{}".format( 

895 self.model_name_lower, 

896 "_".join(self.old_fields), 

897 self.new_name_lower, 

898 ) 

899 

900 def reduce(self, operation, package_label): 

901 if ( 

902 isinstance(operation, RenameIndex) 

903 and self.model_name_lower == operation.model_name_lower 

904 and operation.old_name 

905 and self.new_name_lower == operation.old_name_lower 

906 ): 

907 return [ 

908 RenameIndex( 

909 self.model_name, 

910 new_name=operation.new_name, 

911 old_name=self.old_name, 

912 old_fields=self.old_fields, 

913 ) 

914 ] 

915 return super().reduce(operation, package_label) 

916 

917 

918class AddConstraint(IndexOperation): 

919 option_name = "constraints" 

920 

921 def __init__(self, model_name, constraint): 

922 self.model_name = model_name 

923 self.constraint = constraint 

924 

925 def state_forwards(self, package_label, state): 

926 state.add_constraint(package_label, self.model_name_lower, self.constraint) 

927 

928 def database_forwards(self, package_label, schema_editor, from_state, to_state): 

929 model = to_state.packages.get_model(package_label, self.model_name) 

930 if self.allow_migrate_model(schema_editor.connection.alias, model): 

931 schema_editor.add_constraint(model, self.constraint) 

932 

933 def database_backwards(self, package_label, schema_editor, from_state, to_state): 

934 model = to_state.packages.get_model(package_label, self.model_name) 

935 if self.allow_migrate_model(schema_editor.connection.alias, model): 

936 schema_editor.remove_constraint(model, self.constraint) 

937 

938 def deconstruct(self): 

939 return ( 

940 self.__class__.__name__, 

941 [], 

942 { 

943 "model_name": self.model_name, 

944 "constraint": self.constraint, 

945 }, 

946 ) 

947 

948 def describe(self): 

949 return f"Create constraint {self.constraint.name} on model {self.model_name}" 

950 

951 @property 

952 def migration_name_fragment(self): 

953 return f"{self.model_name_lower}_{self.constraint.name.lower()}" 

954 

955 

956class RemoveConstraint(IndexOperation): 

957 option_name = "constraints" 

958 

959 def __init__(self, model_name, name): 

960 self.model_name = model_name 

961 self.name = name 

962 

963 def state_forwards(self, package_label, state): 

964 state.remove_constraint(package_label, self.model_name_lower, self.name) 

965 

966 def database_forwards(self, package_label, schema_editor, from_state, to_state): 

967 model = to_state.packages.get_model(package_label, self.model_name) 

968 if self.allow_migrate_model(schema_editor.connection.alias, model): 

969 from_model_state = from_state.models[package_label, self.model_name_lower] 

970 constraint = from_model_state.get_constraint_by_name(self.name) 

971 schema_editor.remove_constraint(model, constraint) 

972 

973 def database_backwards(self, package_label, schema_editor, from_state, to_state): 

974 model = to_state.packages.get_model(package_label, self.model_name) 

975 if self.allow_migrate_model(schema_editor.connection.alias, model): 

976 to_model_state = to_state.models[package_label, self.model_name_lower] 

977 constraint = to_model_state.get_constraint_by_name(self.name) 

978 schema_editor.add_constraint(model, constraint) 

979 

980 def deconstruct(self): 

981 return ( 

982 self.__class__.__name__, 

983 [], 

984 { 

985 "model_name": self.model_name, 

986 "name": self.name, 

987 }, 

988 ) 

989 

990 def describe(self): 

991 return f"Remove constraint {self.name} from model {self.model_name}" 

992 

993 @property 

994 def migration_name_fragment(self): 

995 return f"remove_{self.model_name_lower}_{self.name.lower()}"