Plain is headed towards 1.0! Subscribe for development updates →

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