]> git.ipfire.org Git - thirdparty/sqlalchemy/alembic.git/commitdiff
Add cookbook recipe illustrating autogenerate->invoke directly
authorMike Bayer <mike_mp@zzzcomputing.com>
Thu, 5 Nov 2020 14:05:42 +0000 (09:05 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Thu, 5 Nov 2020 14:05:42 +0000 (09:05 -0500)
Change-Id: I46c0436971f50c0969f70ecf99c191533db65dcf

docs/build/cookbook.rst

index 08306a846a43bd79244a2211a45d44a3f0211224..4277ad1c91f067f0189e7269d82c963494e8689f 100644 (file)
@@ -1292,6 +1292,127 @@ Output::
     op.create_index('property_name_users_id', 'user_properties', ['property_name', 'users_id'], unique=True)
     # ### end Alembic commands ###
 
+Run Alembic Operation Objects Directly (as in from autogenerate)
+================================================================
+
+The :class:`.Operations` object has a method known as
+:meth:`.Operations.invoke` that will generically invoke a particular operation
+object.  We can therefore use the :func:`.autogenerate.produce_migrations`
+function to run an autogenerate comparison, get back a
+:class:`.ops.MigrationScript` structure representing the changes, and with a
+little bit of insider information we can invoke them directly.
+
+The traversal through the :class:`.ops.MigrationScript` structure is as
+follows::
+
+    use_batch = engine.name == "sqlite"
+
+    stack = [migrations.upgrade_ops]
+    while stack:
+        elem = stack.pop(0)
+
+        if use_batch and isinstance(elem, ModifyTableOps):
+            with operations.batch_alter_table(
+                elem.table_name, schema=elem.schema
+            ) as batch_ops:
+                for table_elem in elem.ops:
+                    # work around Alembic issue #753
+                    if hasattr(table_elem, "column"):
+                        table_elem.column = table_elem.column.copy()
+                    batch_ops.invoke(table_elem)
+
+        elif hasattr(elem, "ops"):
+            stack.extend(elem.ops)
+        else:
+            # work around Alembic issue #753
+            if hasattr(elem, "column"):
+                elem.column = elem.column.copy()
+            operations.invoke(elem)
+
+Above, we detect elements that have a collection of operations by looking
+for the ``.ops`` attribute.   A check for :class:`.ModifyTableOps` allows
+us to use a batch context if we are supporting that.   Finally there's a
+workaround for an Alembic issue that exists for SQLAlchemy 1.3.20 and greater
+combined with Alembic older than 1.5.
+
+A full example follows.  The overall setup here is copied from the example
+at :func:`.autogenerate.compare_metadata`::
+
+    from sqlalchemy import Column
+    from sqlalchemy import create_engine
+    from sqlalchemy import Integer
+    from sqlalchemy import MetaData
+    from sqlalchemy import String
+    from sqlalchemy import Table
+
+    from alembic.autogenerate import produce_migrations
+    from alembic.migration import MigrationContext
+    from alembic.operations import Operations
+    from alembic.operations.ops import ModifyTableOps
+
+
+    engine = create_engine("sqlite://", echo=True)
+
+    with engine.connect() as conn:
+        conn.execute(
+            """
+            create table foo (
+                id integer not null primary key,
+                old_data varchar(50),
+                x integer
+            )"""
+        )
+
+        conn.execute(
+            """
+            create table bar (
+                data varchar(50)
+            )"""
+        )
+
+    metadata = MetaData()
+    Table(
+        "foo",
+        metadata,
+        Column("id", Integer, primary_key=True),
+        Column("data", Integer),
+        Column("x", Integer, nullable=False),
+    )
+    Table("bat", metadata, Column("info", String(100)))
+
+    mc = MigrationContext.configure(engine.connect())
+
+    migrations = produce_migrations(mc, metadata)
+
+    operations = Operations(mc)
+
+    use_batch = engine.name == "sqlite"
+
+    stack = [migrations.upgrade_ops]
+    while stack:
+        elem = stack.pop(0)
+
+        if use_batch and isinstance(elem, ModifyTableOps):
+            with operations.batch_alter_table(
+                elem.table_name, schema=elem.schema
+            ) as batch_ops:
+                for table_elem in elem.ops:
+                    # work around Alembic issue #753
+                    if hasattr(table_elem, "column"):
+                        table_elem.column = table_elem.column.copy()
+                    batch_ops.invoke(table_elem)
+
+        elif hasattr(elem, "ops"):
+            stack.extend(elem.ops)
+        else:
+            # work around Alembic issue #753
+            if hasattr(elem, "column"):
+                elem.column = elem.column.copy()
+            operations.invoke(elem)
+
+
+
+
 Test current database revision is at head(s)
 ============================================