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