Plain is headed towards 1.0! Subscribe for development updates →

  1from .base import Operation
  2
  3
  4class SeparateDatabaseAndState(Operation):
  5    """
  6    Take two lists of operations - ones that will be used for the database,
  7    and ones that will be used for the state change. This allows operations
  8    that don't support state change to have it applied, or have operations
  9    that affect the state or not the database, or so on.
 10    """
 11
 12    serialization_expand_args = ["database_operations", "state_operations"]
 13
 14    def __init__(self, database_operations=None, state_operations=None):
 15        self.database_operations = database_operations or []
 16        self.state_operations = state_operations or []
 17
 18    def deconstruct(self):
 19        kwargs = {}
 20        if self.database_operations:
 21            kwargs["database_operations"] = self.database_operations
 22        if self.state_operations:
 23            kwargs["state_operations"] = self.state_operations
 24        return (self.__class__.__qualname__, [], kwargs)
 25
 26    def state_forwards(self, package_label, state):
 27        for state_operation in self.state_operations:
 28            state_operation.state_forwards(package_label, state)
 29
 30    def database_forwards(self, package_label, schema_editor, from_state, to_state):
 31        # We calculate state separately in here since our state functions aren't useful
 32        for database_operation in self.database_operations:
 33            to_state = from_state.clone()
 34            database_operation.state_forwards(package_label, to_state)
 35            database_operation.database_forwards(
 36                package_label, schema_editor, from_state, to_state
 37            )
 38            from_state = to_state
 39
 40    def describe(self):
 41        return "Custom state/database change combination"
 42
 43
 44class RunSQL(Operation):
 45    """
 46    Run some raw SQL.
 47
 48    Also accept a list of operations that represent the state change effected
 49    by this SQL change, in case it's custom column/table creation/deletion.
 50    """
 51
 52    def __init__(self, sql, *, state_operations=None, elidable=False):
 53        self.sql = sql
 54        self.state_operations = state_operations or []
 55        self.elidable = elidable
 56
 57    def deconstruct(self):
 58        kwargs = {
 59            "sql": self.sql,
 60        }
 61        if self.state_operations:
 62            kwargs["state_operations"] = self.state_operations
 63        return (self.__class__.__qualname__, [], kwargs)
 64
 65    def state_forwards(self, package_label, state):
 66        for state_operation in self.state_operations:
 67            state_operation.state_forwards(package_label, state)
 68
 69    def database_forwards(self, package_label, schema_editor, from_state, to_state):
 70        self._run_sql(schema_editor, self.sql)
 71
 72    def describe(self):
 73        return "Raw SQL operation"
 74
 75    def _run_sql(self, schema_editor, sqls):
 76        if isinstance(sqls, list | tuple):
 77            for sql in sqls:
 78                params = None
 79                if isinstance(sql, list | tuple):
 80                    elements = len(sql)
 81                    if elements == 2:
 82                        sql, params = sql
 83                    else:
 84                        raise ValueError("Expected a 2-tuple but got %d" % elements)  # noqa: UP031
 85                schema_editor.execute(sql, params=params)
 86        else:
 87            statements = schema_editor.connection.ops.prepare_sql_script(sqls)
 88            for statement in statements:
 89                schema_editor.execute(statement, params=None)
 90
 91
 92class RunPython(Operation):
 93    """
 94    Run Python code in a context suitable for doing versioned ORM operations.
 95    """
 96
 97    reduces_to_sql = False
 98
 99    def __init__(self, code, *, atomic=None, elidable=False):
100        self.atomic = atomic
101        # Forwards code
102        if not callable(code):
103            raise ValueError("RunPython must be supplied with a callable")
104        self.code = code
105        self.elidable = elidable
106
107    def deconstruct(self):
108        kwargs = {
109            "code": self.code,
110        }
111        if self.atomic is not None:
112            kwargs["atomic"] = self.atomic
113        return (self.__class__.__qualname__, [], kwargs)
114
115    def state_forwards(self, package_label, state):
116        # RunPython objects have no state effect. To add some, combine this
117        # with SeparateDatabaseAndState.
118        pass
119
120    def database_forwards(self, package_label, schema_editor, from_state, to_state):
121        # RunPython has access to all models. Ensure that all models are
122        # reloaded in case any are delayed.
123        from_state.clear_delayed_models_cache()
124        # We now execute the Python code in a context that contains a 'models'
125        # object, representing the versioned models as an app registry.
126        # We could try to override the global cache, but then people will still
127        # use direct imports, so we go with a documentation approach instead.
128        self.code(from_state.models_registry, schema_editor)
129
130    def describe(self):
131        return "Raw Python operation"