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 )