Plain is headed towards 1.0! Subscribe for development updates →

  1from __future__ import annotations
  2
  3from abc import ABC, abstractmethod
  4from typing import TYPE_CHECKING, Any
  5
  6if TYPE_CHECKING:
  7    from plain.models.backends.base.base import BaseDatabaseWrapper
  8    from plain.models.backends.base.schema import BaseDatabaseSchemaEditor
  9    from plain.models.migrations.state import ProjectState
 10
 11
 12class Operation(ABC):
 13    """
 14    Base class for migration operations.
 15
 16    It's responsible for both mutating the in-memory model state
 17    (see db/migrations/state.py) to represent what it performs, as well
 18    as actually performing it against a live database.
 19
 20    Note that some operations won't modify memory state at all (e.g. data
 21    copying operations), and some will need their modifications to be
 22    optionally specified by the user (e.g. custom Python code snippets)
 23
 24    Due to the way this class deals with deconstruction, it should be
 25    considered immutable.
 26    """
 27
 28    # Set by __new__ to capture constructor arguments for deconstruction
 29    _constructor_args: tuple[tuple[Any, ...], dict[str, Any]]
 30    # Set by autodetector to track operation dependencies
 31    # Each dependency is a 4-tuple: (package_label, model_name, field_name, create/delete/alter)
 32    _auto_deps: list[tuple[str, str, str | None, bool | str]]
 33
 34    # Can this migration be represented as SQL? (things like RunPython cannot)
 35    reduces_to_sql = True
 36
 37    # Should this operation be forced as atomic even on backends with no
 38    # DDL transaction support (i.e., does it have no DDL, like RunPython)
 39    atomic = False
 40
 41    # Should this operation be considered safe to elide and optimize across?
 42    elidable = False
 43
 44    serialization_expand_args: list[str] = []
 45
 46    def __new__(cls, *args: Any, **kwargs: Any) -> Operation:
 47        # We capture the arguments to make returning them trivial
 48        self = object.__new__(cls)
 49        self._constructor_args = (args, kwargs)
 50        return self
 51
 52    def deconstruct(self) -> tuple[str, tuple[Any, ...], dict[str, Any]]:
 53        """
 54        Return a 3-tuple of class import path (or just name if it lives
 55        under plain.models.migrations), positional arguments, and keyword
 56        arguments.
 57        """
 58        return (
 59            self.__class__.__name__,
 60            self._constructor_args[0],
 61            self._constructor_args[1],
 62        )
 63
 64    @abstractmethod
 65    def state_forwards(self, package_label: str, state: ProjectState) -> None:
 66        """
 67        Take the state from the previous migration, and mutate it
 68        so that it matches what this migration would perform.
 69        """
 70        ...
 71
 72    @abstractmethod
 73    def database_forwards(
 74        self,
 75        package_label: str,
 76        schema_editor: BaseDatabaseSchemaEditor,
 77        from_state: ProjectState,
 78        to_state: ProjectState,
 79    ) -> None:
 80        """
 81        Perform the mutation on the database schema in the normal
 82        (forwards) direction.
 83        """
 84        ...
 85
 86    def describe(self) -> str:
 87        """
 88        Output a brief summary of what the action does.
 89        """
 90        return f"{self.__class__.__name__}: {self._constructor_args}"
 91
 92    @property
 93    def migration_name_fragment(self) -> str | None:
 94        """
 95        A filename part suitable for automatically naming a migration
 96        containing this operation, or None if not applicable.
 97        """
 98        return None
 99
100    def references_model(self, name: str, package_label: str) -> bool:
101        """
102        Return True if there is a chance this operation references the given
103        model name (as a string), with an app label for accuracy.
104
105        Used for optimization. If in doubt, return True;
106        returning a false positive will merely make the optimizer a little
107        less efficient, while returning a false negative may result in an
108        unusable optimized migration.
109        """
110        return True
111
112    def references_field(self, model_name: str, name: str, package_label: str) -> bool:
113        """
114        Return True if there is a chance this operation references the given
115        field name, with an app label for accuracy.
116
117        Used for optimization. If in doubt, return True.
118        """
119        return self.references_model(model_name, package_label)
120
121    def allow_migrate_model(self, connection: BaseDatabaseWrapper, model: Any) -> bool:
122        """Return whether or not a model may be migrated."""
123        if not model.model_options.can_migrate(connection):
124            return False
125
126        return True
127
128    def reduce(
129        self, operation: Operation, package_label: str
130    ) -> list[Operation] | bool:
131        """
132        Return either a list of operations the actual operation should be
133        replaced with or a boolean that indicates whether or not the specified
134        operation can be optimized across.
135        """
136        if self.elidable:
137            return [operation]
138        elif operation.elidable:
139            return [self]
140        return False
141
142    def __repr__(self) -> str:
143        return "<{} {}{}>".format(
144            self.__class__.__name__,
145            ", ".join(map(repr, self._constructor_args[0])),
146            ",".join(" {}={!r}".format(*x) for x in self._constructor_args[1].items()),
147        )