Yes, it is possible using alembic and not very hacky.
I use alembic autogenerate API. compare_metadata
is convenient to generate diff for review, and produce_migrations
is convenient to generate migration operations, which are than invoked by Opertaions.invoke
.
One thing to notice is that upgrade_ops
may contain OpContainer
, which itself is not invoke
-able, but has ops
attribute containing nested operations.
Below is my code to achieve this:
@bp.route("/migrate", methods=["GET", "POST"])
def migrate():
from alembic.runtime.migration import MigrationContext
# use `db.session.connection()` instead of `db.engine.connect()`
# to avoid database lock hang
context = MigrationContext.configure(db.session.connection())
if request.method == "GET":
import pprint
from alembic.autogenerate import compare_metadata
diff = compare_metadata(context, db.metadata)
diff_str = pprint.pformat(diff, indent=2, width=20)
logger.info("Migrate steps: %s", diff_str)
return respond_success(migration=diff_str)
from alembic.autogenerate import produce_migrations
from alembic.operations import Operations
from alembic.operations.ops import OpContainer
migration = produce_migrations(context, db.metadata)
operation = Operations(context)
for outer_op in migration.upgrade_ops.ops:
logger.info("Invoking %s", outer_op)
if isinstance(outer_op, OpContainer):
for inner_op in outer_op.ops:
logger.info("Invoking %s", inner_op)
operation.invoke(inner_op)
else:
operation.invoke(outer_op)
return respond_success()
For anyone reading this, if you want to ensure the reviewed operations is the same as operations to invoke, try POSTing the hash of diff_str
and comparing them.