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

1 from plain import models

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

3 from plain.models.migrations.state import ModelState

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

5 from plain.utils.functional import cached_property

6

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

8

9

10 def _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

20 class 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

40 class 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

264 class 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

299 class 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

418 class 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

428 class 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

483 class 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

523 class 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

590 class 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

641 class 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

670 class IndexOperation(Operation):

671 option_name = "indexes"

672

673 @cached_property

674 def model_name_lower(self):

675 return self.model_name.lower()

676

677

678 class 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

732 class 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

775 class 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

917 class 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

955 class 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()}"