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

464 statements  

« prev     ^ index     » next       coverage.py v7.6.9, created at 2024-12-23 11:16 -0600

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 f"Delete model {self.name}" 

293 

294 @property 

295 def migration_name_fragment(self): 

296 return f"delete_{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 f"alter_{self.name_lower}_table" 

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 ( 

582 f"Set order_with_respect_to on {self.name} to {self.order_with_respect_to}" 

583 ) 

584 

585 @property 

586 def migration_name_fragment(self): 

587 return f"alter_{self.name_lower}_order_with_respect_to" 

588 

589 

590class AlterModelOptions(ModelOptionOperation): 

591 """ 

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

593 (like ordering). Python code in migrations 

594 may still need them. 

595 """ 

596 

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

598 ALTER_OPTION_KEYS = [ 

599 "base_manager_name", 

600 "default_manager_name", 

601 "default_related_name", 

602 "get_latest_by", 

603 "managed", 

604 "ordering", 

605 "select_on_save", 

606 ] 

607 

608 def __init__(self, name, options): 

609 self.options = options 

610 super().__init__(name) 

611 

612 def deconstruct(self): 

613 kwargs = { 

614 "name": self.name, 

615 "options": self.options, 

616 } 

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

618 

619 def state_forwards(self, package_label, state): 

620 state.alter_model_options( 

621 package_label, 

622 self.name_lower, 

623 self.options, 

624 self.ALTER_OPTION_KEYS, 

625 ) 

626 

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

628 pass 

629 

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

631 pass 

632 

633 def describe(self): 

634 return f"Change Meta options on {self.name}" 

635 

636 @property 

637 def migration_name_fragment(self): 

638 return f"alter_{self.name_lower}_options" 

639 

640 

641class AlterModelManagers(ModelOptionOperation): 

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

643 

644 serialization_expand_args = ["managers"] 

645 

646 def __init__(self, name, managers): 

647 self.managers = managers 

648 super().__init__(name) 

649 

650 def deconstruct(self): 

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

652 

653 def state_forwards(self, package_label, state): 

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

655 

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

657 pass 

658 

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

660 pass 

661 

662 def describe(self): 

663 return f"Change managers on {self.name}" 

664 

665 @property 

666 def migration_name_fragment(self): 

667 return f"alter_{self.name_lower}_managers" 

668 

669 

670class IndexOperation(Operation): 

671 option_name = "indexes" 

672 

673 @cached_property 

674 def model_name_lower(self): 

675 return self.model_name.lower() 

676 

677 

678class AddIndex(IndexOperation): 

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

680 

681 def __init__(self, model_name, index): 

682 self.model_name = model_name 

683 if not index.name: 

684 raise ValueError( 

685 "Indexes passed to AddIndex operations require a name " 

686 f"argument. {index!r} doesn't have one." 

687 ) 

688 self.index = index 

689 

690 def state_forwards(self, package_label, state): 

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

692 

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

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

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

696 schema_editor.add_index(model, self.index) 

697 

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

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

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

701 schema_editor.remove_index(model, self.index) 

702 

703 def deconstruct(self): 

704 kwargs = { 

705 "model_name": self.model_name, 

706 "index": self.index, 

707 } 

708 return ( 

709 self.__class__.__qualname__, 

710 [], 

711 kwargs, 

712 ) 

713 

714 def describe(self): 

715 if self.index.expressions: 

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

717 self.index.name, 

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

719 self.model_name, 

720 ) 

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

722 self.index.name, 

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

724 self.model_name, 

725 ) 

726 

727 @property 

728 def migration_name_fragment(self): 

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

730 

731 

732class RemoveIndex(IndexOperation): 

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

734 

735 def __init__(self, model_name, name): 

736 self.model_name = model_name 

737 self.name = name 

738 

739 def state_forwards(self, package_label, state): 

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

741 

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

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

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

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

746 index = from_model_state.get_index_by_name(self.name) 

747 schema_editor.remove_index(model, index) 

748 

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

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

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

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

753 index = to_model_state.get_index_by_name(self.name) 

754 schema_editor.add_index(model, index) 

755 

756 def deconstruct(self): 

757 kwargs = { 

758 "model_name": self.model_name, 

759 "name": self.name, 

760 } 

761 return ( 

762 self.__class__.__qualname__, 

763 [], 

764 kwargs, 

765 ) 

766 

767 def describe(self): 

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

769 

770 @property 

771 def migration_name_fragment(self): 

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

773 

774 

775class RenameIndex(IndexOperation): 

776 """Rename an index.""" 

777 

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

779 if not old_name and not old_fields: 

780 raise ValueError( 

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

782 "set." 

783 ) 

784 if old_name and old_fields: 

785 raise ValueError( 

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

787 ) 

788 self.model_name = model_name 

789 self.new_name = new_name 

790 self.old_name = old_name 

791 self.old_fields = old_fields 

792 

793 @cached_property 

794 def old_name_lower(self): 

795 return self.old_name.lower() 

796 

797 @cached_property 

798 def new_name_lower(self): 

799 return self.new_name.lower() 

800 

801 def deconstruct(self): 

802 kwargs = { 

803 "model_name": self.model_name, 

804 "new_name": self.new_name, 

805 } 

806 if self.old_name: 

807 kwargs["old_name"] = self.old_name 

808 if self.old_fields: 

809 kwargs["old_fields"] = self.old_fields 

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

811 

812 def state_forwards(self, package_label, state): 

813 if self.old_fields: 

814 state.add_index( 

815 package_label, 

816 self.model_name_lower, 

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

818 ) 

819 else: 

820 state.rename_index( 

821 package_label, self.model_name_lower, self.old_name, self.new_name 

822 ) 

823 

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

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

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

827 return 

828 

829 if self.old_fields: 

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

831 columns = [ 

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

833 ] 

834 matching_index_name = schema_editor._constraint_names( 

835 from_model, column_names=columns, index=True 

836 ) 

837 if len(matching_index_name) != 1: 

838 raise ValueError( 

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

840 len(matching_index_name), 

841 from_model._meta.db_table, 

842 ", ".join(columns), 

843 ) 

844 ) 

845 old_index = models.Index( 

846 fields=self.old_fields, 

847 name=matching_index_name[0], 

848 ) 

849 else: 

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

851 old_index = from_model_state.get_index_by_name(self.old_name) 

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

853 if old_index.name == self.new_name: 

854 return 

855 

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

857 new_index = to_model_state.get_index_by_name(self.new_name) 

858 schema_editor.rename_index(model, old_index, new_index) 

859 

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

861 if self.old_fields: 

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

863 return 

864 

865 self.new_name_lower, self.old_name_lower = ( 

866 self.old_name_lower, 

867 self.new_name_lower, 

868 ) 

869 self.new_name, self.old_name = self.old_name, self.new_name 

870 

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

872 

873 self.new_name_lower, self.old_name_lower = ( 

874 self.old_name_lower, 

875 self.new_name_lower, 

876 ) 

877 self.new_name, self.old_name = self.old_name, self.new_name 

878 

879 def describe(self): 

880 if self.old_name: 

881 return ( 

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

883 ) 

884 return ( 

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

886 f"{self.new_name}" 

887 ) 

888 

889 @property 

890 def migration_name_fragment(self): 

891 if self.old_name: 

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

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

894 self.model_name_lower, 

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

896 self.new_name_lower, 

897 ) 

898 

899 def reduce(self, operation, package_label): 

900 if ( 

901 isinstance(operation, RenameIndex) 

902 and self.model_name_lower == operation.model_name_lower 

903 and operation.old_name 

904 and self.new_name_lower == operation.old_name_lower 

905 ): 

906 return [ 

907 RenameIndex( 

908 self.model_name, 

909 new_name=operation.new_name, 

910 old_name=self.old_name, 

911 old_fields=self.old_fields, 

912 ) 

913 ] 

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

915 

916 

917class AddConstraint(IndexOperation): 

918 option_name = "constraints" 

919 

920 def __init__(self, model_name, constraint): 

921 self.model_name = model_name 

922 self.constraint = constraint 

923 

924 def state_forwards(self, package_label, state): 

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

926 

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

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

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

930 schema_editor.add_constraint(model, self.constraint) 

931 

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

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

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

935 schema_editor.remove_constraint(model, self.constraint) 

936 

937 def deconstruct(self): 

938 return ( 

939 self.__class__.__name__, 

940 [], 

941 { 

942 "model_name": self.model_name, 

943 "constraint": self.constraint, 

944 }, 

945 ) 

946 

947 def describe(self): 

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

949 

950 @property 

951 def migration_name_fragment(self): 

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

953 

954 

955class RemoveConstraint(IndexOperation): 

956 option_name = "constraints" 

957 

958 def __init__(self, model_name, name): 

959 self.model_name = model_name 

960 self.name = name 

961 

962 def state_forwards(self, package_label, state): 

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

964 

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

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

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

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

969 constraint = from_model_state.get_constraint_by_name(self.name) 

970 schema_editor.remove_constraint(model, constraint) 

971 

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

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

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

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

976 constraint = to_model_state.get_constraint_by_name(self.name) 

977 schema_editor.add_constraint(model, constraint) 

978 

979 def deconstruct(self): 

980 return ( 

981 self.__class__.__name__, 

982 [], 

983 { 

984 "model_name": self.model_name, 

985 "name": self.name, 

986 }, 

987 ) 

988 

989 def describe(self): 

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

991 

992 @property 

993 def migration_name_fragment(self): 

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