12.2 Operations: the base class and its two halves
An operation is one change that a migration makes, an instance of a subclass of django. It can make the change in two places: its state half, Operation, in a project state, and its database half, Operation and Operation, in a database. The base class also keeps the arguments an operation was made with, for Operation, and gives every operation its flags, its description and Operation.
Django needs to know what the models were at any point of the history without going near a database: the project state that makemigrations compares the models with is made by running state halves alone. Changing a schema is another matter, and an operation’s own arguments are not enough for it: an AlterField holds the field as it is to be, and the schema editor’s alter_field wants the field as it was too. The database half is handed two project states, the one before the operation and the one after, and finds both fields in them; the state half makes the second out of the first.
AddField on yard’s Dock: what each is given, and what it changes.The operations themselves are in The model operations: CreateModel, DeleteModel, RenameModel and the option operations, The field operations, and those on indexes and constraints and RunPython, RunSQL and SeparateDatabaseAndState.
state_forwards, database_forwards and database_backwards
Operation declares the methods of the two halves only to raise NotImplementedError. A subclass provides all three; every other member has a default in the base.
The state half is given a project state, which it changes in place, and the label of the application whose migration holds the operation, which the operation itself does not know. In the operations that django exports, set out in the table below, it calls methods of ProjectState, passes the call to operations it holds or, in RunPython, does nothing (Project state: ProjectState and its model states).
The database half is two methods with the same parameters: the label, a schema editor and two project states. Forwards, the from-state is the state before the operation and the to-state the state after. Backwards they change places: either way the from-state describes the database as the method finds it and the to-state as it is to leave it. No state half runs backwards: Migration first runs the state halves forwards and keeps the states between (A migration: its operations, its dependencies and how it is applied).
The shape of a database half
Apart from RunSQL, RunPython and SeparateDatabaseAndState, and from the operations whose database halves do nothing, each of those operations has a database half of one shape. Forwards, it takes the historical model from the to-state’s registry, ProjectState, or in DeleteModel, RemoveField and RemoveIndex from the from-state’s, which still has what they remove. It asks Operation whether the schema editor’s connection is to have that model, and returns without a word if not. Then it looks up whatever else the change needs, for an AddField the from-state’s model, and calls the schema editor where there is something to change (Historical models: StateApps and rendering, The schema editor: BaseDatabaseSchemaEditor).
allow_migrate_model asks the model’s options before any router: Options is false for a proxy, a swapped-out or an unmanaged model, and for one whose Options is another vendor than the connection’s or, with no vendor named, whose Options include a feature the connection lacks. Only for a model that passes is the router asked, by ConnectionRouter, which hands the routers the historical model among their hints (Routing between databases: ConnectionRouter).
A recording is of a running Django, here on SQLite: a line is a call, set in under the call that made it, or Python as it was run; -> is followed by what came back, parentheses name the class that declares a method where the object’s class is another, square brackets hold an operation’s description, and only the lines a passage is about are written, the rest left out. This one is the database half of an AddField of a field metres to yard’s Dock, called by hand with two project states: the recording’s from_state, which lacks the field, and its to_state, a clone of that on which the state half had been run.
AddField.database_forwards(from_state=from_state, to_state=to_state) [Add field metres to dock]
AddField.allow_migrate_model(connection_alias='default', model=the historical model yard.Dock) (Operation.allow_migrate_model)
Options.can_migrate('default') -> True
ConnectionRouter.allow_migrate_model(db='default', model=the historical model yard.Dock)
-> True
-> True
DatabaseSchemaEditor.add_field(model=the historical model yard.Dock, field=the field yard.Dock.metres)
A refusal by Operation is silent. The state half has run, the migration goes on to its next operation, and the executor writes the migration’s row as for any other: a database that a router keeps a model out of has the row and no table for the model.
The database halves that do nothing are those of AlterModelOptions, AlterModelManagers and AlterConstraint: their change lives in the state alone. RunSQL and RunPython concern no model and never call allow_migrate_model: each asks ConnectionRouter itself, with the application’s label and the hints it was made with. SeparateDatabaseAndState asks nothing and hands on to the operations it holds.
An operation as a value
The writer turns an operation into the text of a call that makes it again, and asks the operation for the arguments it was made with. Operation runs before any subclass’s __init__ and keeps the positional and the keyword arguments of the call on the instance, as the pair _constructor_args. Operation, which OperationWriter calls, returns the class’s name and the two members of that pair (Writing a migration file: MigrationWriter). None of those operations is left with the base’s deconstruct: each builds its arguments from the instance’s attributes. The base’s serves an operation that defines none, such as CreateExtension of django (The PostgreSQL application: PostgresConfig, operations and aggregates).
Operation makes another operation of the same class from the kept arguments with some of them changed: a change is given by keyword, and a positional argument is matched to it by its name in the class’s signature, as the last line below shows. copy.replace calls it, as does Django’s own copy of that function for Python 3.12. That is how CreateModel, asked by the optimizer about a later RenameModel of its model, answers with the CreateModel made again under the new name (CreateModel; The optimizer: MigrationOptimizer and reduce). In the recording an AddField is made with positional arguments and asked what it kept and what its own deconstruct returns, and a copy is asked what it kept:
positional = migrations.AddField('dock', 'metres', models.PositiveSmallIntegerField(default=120), False)
positional._constructor_args -> (('dock', 'metres', an unbound PositiveSmallIntegerField, False), {})
positional.deconstruct() -> ('AddField', [], {'model_name': 'dock', 'name': 'metres', 'field': an unbound PositiveSmallIntegerField, 'preserve_default': False})
...
copy.replace(positional, name='length')._constructor_args -> (('dock', 'length', an unbound PositiveSmallIntegerField, False), {})
The class’s docstring says an operation is to be considered immutable. The kept arguments do not follow an attribute assigned later: assign an AddField another name and its deconstruct, which reads the attribute, reports the new one, while a copy made by __replace__ has the old.
The flags
Class attributes of Operation say how an operation may be run (Operation, Migration; A migration: its operations, its dependencies and how it is applied).
Operation, true in the base: whether the operation can be run backwards, read by.reversible Migration..unapply RunSQLandRunPythonmake it a property, true where they were given something to run backwards.Operation, true in the base: whether the operation can be written as SQL, read by.reduces_to_sql Migration.applyandMigrationwhere statements are collected and not sent, as for sqlmigrate..unapply RunPythonalone sets it false.Operation.atomic, false in the base: whether the operation is to be run in an atomic block even on a backend that cannot roll back DDL, which its comment says is for an operation that has no DDL, likeRunPython.Migration.applyandMigrationread it with the migration’s own.unapply atomic, andRunPythontakes it as an argument.Operation, false in the base: whether.elidable Operation.reducemay leave the operation out of a list being optimized.RunSQLandRunPythontake it as an argument.
category, describe and the name fragment
Operation returns a phrase for the operation; none of those operations is left with the base’s, which falls back on the class’s name and the kept arguments. Operation is a member of OperationCategory, an enum.StrEnum whose values are signs: ADDITION is "+", REMOVAL "-", ALTERATION "~", PYTHON "p", SQL "s" and MIXED "?". Operation puts the sign and a space before the description, with "?" for a category of None, the base’s.
Unless its verbosity is 0, makemigrations prints the formatted description of each operation of a migration it writes or, under --dry-run, would write, and of the branches it is asked to merge; sqlmigrate and migrate --plan use describe and so show no sign. Operation is a property: the operation’s part of a name for the migration that holds it, read by Migration (A migration: its operations, its dependencies and how it is applied), and None in the base and in RunSQL, RunPython and SeparateDatabaseAndState.
What the optimizer asks
The optimizer, which shortens a list of operations, asks an operation through Operation.reduce about one that comes later in the list: the answer is a list of operations to stand in place of both, or true or false for whether the two may change places (Operation.reduce; The optimizer: MigrationOptimizer and reduce). The reduce methods in turn ask Operation and Operation whether an operation has anything to do with a model or a field. The docstring’s rule is: when in doubt, true, since a wrong true only costs the optimizer some efficiency and a wrong false may leave a migration that cannot be used. The base’s answer to both is true.
The operations of django.db.migrations.operations
The exported operations are the classes of this table, and django exports them again, which is where a migration file finds them.
AlterModelTable to AlterModelManagers inherit their category from ModelOptionOperation. The state half’s methods are ProjectState’s and the database half’s the schema editor’s (django/db/migrations/operations/ ).| class | category | state half | database half, forwards |
|---|---|---|---|
CreateModel |
category ADDITION |
state half add_model |
database half, forwards create_model; alter_ where the model state has "index_together" |
DeleteModel |
category REMOVAL |
state half remove_model |
database half, forwards delete_model |
RenameModel |
category ALTERATION |
state half rename_model |
database half, forwards alter_db_table; alter_field for fields pointing at the model; _alter_ for many-to-many tables of its own |
AlterModelTable |
category ALTERATION |
state half alter_ |
database half, forwards alter_db_table, for the model’s table and each many-to-many table made for it |
Alter |
category ALTERATION |
state half alter_ |
database half, forwards alter_ |
Alter |
category ALTERATION |
state half alter_ |
database half, forwards alter_ |
Alter |
category ALTERATION |
state half alter_ |
database half, forwards alter_ |
Alter |
category ALTERATION |
state half alter_ |
database half, forwards add_field or remove_field of the column "_order", where the option is gained or lost |
Alter |
category ALTERATION |
state half alter_ |
database half, forwards nothing |
Alter |
category ALTERATION |
state half alter_ |
database half, forwards nothing |
AddField |
category ADDITION |
state half add_field |
database half, forwards add_field |
RemoveField |
category REMOVAL |
state half remove_field |
database half, forwards remove_field |
AlterField |
category ALTERATION |
state half alter_field |
database half, forwards alter_field |
RenameField |
category ALTERATION |
state half rename_field |
database half, forwards alter_field |
AddIndex |
category ADDITION |
state half add_index |
database half, forwards add_index |
RemoveIndex |
category REMOVAL |
state half remove_index |
database half, forwards remove_index |
RenameIndex |
category ALTERATION |
state half rename_index; given old_fields, the fields of an index that has no name in the state, add_index and remove_ |
database half, forwards rename_index, unless the name is unchanged; given old_fields, _constraint_ first, to find the index’s name |
AddConstraint |
category ADDITION |
state half add_constraint |
database half, forwards add_constraint |
RemoveConstraint |
category REMOVAL |
state half remove_ |
database half, forwards remove_ |
AlterConstraint |
category ALTERATION |
state half alter_constraint |
database half, forwards nothing |
RunSQL |
category SQL |
state half the state halves of its state_operations |
database half, forwards execute, for each statement |
RunPython |
category PYTHON |
state half nothing | database half, forwards its function, given the from-state’s registry and the schema editor |
Separate |
category MIXED |
state half the state halves of its state_operations |
database half, forwards the database halves of its database_ |