File: operations.rst

package info (click to toggle)
alembic 1.18.4-1
  • links: PTS, VCS
  • area: main
  • in suites: experimental
  • size: 3,348 kB
  • sloc: python: 42,842; makefile: 103; sh: 5
file content (241 lines) | stat: -rw-r--r-- 9,415 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
.. _alembic.operations.toplevel:

=====================
Operation Directives
=====================

.. note:: this section discusses the **internal API of Alembic** as regards
   the internal system of defining migration operation directives.
   This section is only useful for developers who wish to extend the
   capabilities of Alembic.  For end-user guidance on Alembic migration
   operations, please see :ref:`ops`.

Within migration scripts, actual database migration operations are handled
via an instance of :class:`.Operations`.   The :class:`.Operations` class
lists out available migration operations that are linked to a
:class:`.MigrationContext`, which communicates instructions originated
by the :class:`.Operations` object into SQL that is sent to a database or SQL
output stream.

Most methods on the :class:`.Operations` class are generated dynamically
using a "plugin" system, described in the next section
:ref:`operation_plugins`.   Additionally, when Alembic migration scripts
actually run, the methods on the current :class:`.Operations` object are
proxied out to the ``alembic.op`` module, so that they are available
using module-style access.

For an overview of how to use an :class:`.Operations` object directly
in programs, as well as for reference to the standard operation methods
as well as "batch" methods, see :ref:`ops`.

.. _operation_plugins:

Operation Plugins
=====================

The Operations object is extensible using a plugin system.   This system
allows one to add new ``op.<some_operation>`` methods at runtime.  The
steps to use this system are to first create a subclass of
:class:`.MigrateOperation`, register it using the :meth:`.Operations.register_operation`
class decorator, then build a default "implementation" function which is
established using the :meth:`.Operations.implementation_for` decorator.

Below we illustrate a very simple operation ``CreateSequenceOp`` which
will implement a new method ``op.create_sequence()`` for use in
migration scripts::

    from alembic.operations import Operations, MigrateOperation

    @Operations.register_operation("create_sequence")
    class CreateSequenceOp(MigrateOperation):
        """Create a SEQUENCE."""

        def __init__(self, sequence_name, schema=None):
            self.sequence_name = sequence_name
            self.schema = schema

        @classmethod
        def create_sequence(cls, operations, sequence_name, **kw):
            """Issue a "CREATE SEQUENCE" instruction."""

            op = CreateSequenceOp(sequence_name, **kw)
            return operations.invoke(op)

        def reverse(self):
            # only needed to support autogenerate
            return DropSequenceOp(self.sequence_name, schema=self.schema)

    @Operations.register_operation("drop_sequence")
    class DropSequenceOp(MigrateOperation):
        """Drop a SEQUENCE."""

        def __init__(self, sequence_name, schema=None):
            self.sequence_name = sequence_name
            self.schema = schema

        @classmethod
        def drop_sequence(cls, operations, sequence_name, **kw):
            """Issue a "DROP SEQUENCE" instruction."""

            op = DropSequenceOp(sequence_name, **kw)
            return operations.invoke(op)

        def reverse(self):
            # only needed to support autogenerate
            return CreateSequenceOp(self.sequence_name, schema=self.schema)

Above, the ``CreateSequenceOp`` and ``DropSequenceOp`` classes represent
new operations that will
be available as ``op.create_sequence()`` and ``op.drop_sequence()``.
The reason the operations
are represented as stateful classes is so that an operation and a specific
set of arguments can be represented generically; the state can then correspond
to different kinds of operations, such as invoking the instruction against
a database, or autogenerating Python code for the operation into a
script.

In order to establish the migrate-script behavior of the new operations,
we use the :meth:`.Operations.implementation_for` decorator::

    @Operations.implementation_for(CreateSequenceOp)
    def create_sequence(operations, operation):
        if operation.schema is not None:
            name = "%s.%s" % (operation.schema, operation.sequence_name)
        else:
            name = operation.sequence_name
        operations.execute("CREATE SEQUENCE %s" % name)


    @Operations.implementation_for(DropSequenceOp)
    def drop_sequence(operations, operation):
        if operation.schema is not None:
            name = "%s.%s" % (operation.schema, operation.sequence_name)
        else:
            name = operation.sequence_name
        operations.execute("DROP SEQUENCE %s" % name)

Above, we use the simplest possible technique of invoking our DDL, which
is just to call :meth:`.Operations.execute` with literal SQL.  If this is
all a custom operation needs, then this is fine.  However, options for
more comprehensive support include building out a custom SQL construct,
as documented at :ref:`sqlalchemy.ext.compiler_toplevel`.

With the above two steps, a migration script can now use new methods
``op.create_sequence()`` and ``op.drop_sequence()`` that will proxy to
our object as a classmethod::

    def upgrade():
        op.create_sequence("my_sequence")

    def downgrade():
        op.drop_sequence("my_sequence")

The registration of new operations only needs to occur in time for the
``env.py`` script to invoke :meth:`.MigrationContext.run_migrations`;
within the module level of the ``env.py`` script is sufficient.

.. seealso::

    :ref:`autogen_custom_ops` - how to add autogenerate support to
    custom operations.

.. _operation_objects:
.. _alembic.operations.ops.toplevel:

Built-in Operation Objects
==============================

The migration operations present on :class:`.Operations` are themselves
delivered via operation objects that represent an operation and its
arguments.   All operations descend from the :class:`.MigrateOperation`
class, and are registered with the :class:`.Operations` class using
the :meth:`.Operations.register_operation` class decorator.  The
:class:`.MigrateOperation` objects also serve as the basis for how the
autogenerate system renders new migration scripts.

.. seealso::

    :ref:`operation_plugins`

    :ref:`customizing_revision`

The built-in operation objects are listed below.

.. automodule:: alembic.operations.ops
    :members:

.. _operations_extending_builtin:

Extending Existing Operations
==============================

.. versionadded:: 1.17.2

The :paramref:`.Operations.implementation_for.replace` parameter allows
replacement of existing operation implementations, including built-in
operations such as :class:`.CreateTableOp`. This enables customization of
migration execution for purposes such as logging operations, running
integrity checks, conditionally canceling operations, or adapting
operations with dialect-specific options.

The example below illustrates replacing the implementation of
:class:`.CreateTableOp` to log each table creation to a separate metadata
table::

    from alembic import op
    from alembic.operations import Operations
    from alembic.operations.ops import CreateTableOp
    from alembic.operations.toimpl import create_table as _create_table
    from sqlalchemy import MetaData, Table, Column, String

    # Define a metadata table to track table operations
    log_table = Table(
        "table_metadata_log",
        MetaData(),
        Column("operation", String),
        Column("table_name", String),
    )

    @Operations.implementation_for(CreateTableOp, replace=True)
    def create_table_with_logging(operations, operation):
        # First, run the original CREATE TABLE implementation
        _create_table(operations, operation)

        # Then, log the operation to the metadata table
        operations.execute(
            log_table.insert().values(
                operation="create",
                table_name=operation.table_name
            )
        )

The above code can be placed in the ``env.py`` file to ensure it is loaded
before migrations run. Once registered, all ``op.create_table()`` calls
within migration scripts will use the augmented implementation.

The original implementation is imported from :mod:`alembic.operations.toimpl`
and invoked within the replacement implementation. The ``replace`` parameter
also enables conditional execution or complete replacement of operation
behavior. The example below demonstrates skipping a :class:`.CreateTableOp`
based on custom logic::

    from alembic.operations import Operations
    from alembic.operations.ops import CreateTableOp
    from alembic.operations.toimpl import create_table as _create_table

    @Operations.implementation_for(CreateTableOp, replace=True)
    def create_table_conditional(operations, operation):
        # Check if the table should be created based on custom logic
        if should_create_table(operation.table_name):
            _create_table(operations, operation)
        else:
            # Skip creation and optionally log
            operations.execute(
                "-- Skipped creation of table %s" % operation.table_name
            )

    def should_create_table(table_name):
        # Custom logic to determine if table should be created
        # For example, check a configuration or metadata table
        return table_name not in get_ignored_tables()