If you’ve done any work with SQLite databases you surely know that this database is very limited in terms of making changes to the database schema. When working with a migration framework such as Flask-Migrate, it is common to end up with migration scripts that fail to upgrade or downgrade just because they need to remove or modify a column in a table, something that SQLite does not support.
In this article I’m going to discuss this limitation of the SQLite database, and show you a workaround that is specific to Flask-Migrate and Alembic.
SQLite’s ALTER TABLE Implementation
Changes that you make to the fields in your model or to the constraints associated with them will end up as an ALTER TABLE
statement sent to the database. If you are using MySQL, Postgres or most other database servers besides SQLite, this isn’t a problem. With SQLite, however, the ALTER TABLE
command only supports adding or renaming columns. Any other change to columns or constraints is going to be rejected with an error.
You can test this yourself very easily. Take any Flask-SQLAlchemy application (you can use one of mine) and after making sure your database is up to date, remove or comment out a column in one of the models. Then generate a migration:
(venv) $ flask db migrate -m "remove a column"
If you open the generated migration script everything will look correct. Below you can see the migration that was generated after I removed a column named about_me
from the User
model:
"""remove a column
Revision ID: ec813e760b53
Revises: 834b1a697901
Create Date: 2020-07-19 18:18:44.066766
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'ec813e760b53'
down_revision = '834b1a697901'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('user', 'about_me')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('user', sa.Column('about_me', sa.VARCHAR(length=140), nullable=True))
# ### end Alembic commands ###
The problem occurs when you try to upgrade the database with this migration:
(venv) $ flask db upgrade
[2020-07-19 18:21:14,268] INFO in __init__: Microblog startup
INFO [alembic.runtime.migration] Context impl SQLiteImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
INFO [alembic.runtime.migration] Running upgrade 834b1a697901 -> ec813e760b53, remove a column
Traceback (most recent call last):
File "/Users/mgrinberg/Documents/dev/python/microblog/venv/lib/python3.8/site-packages/sqlalchemy/engine/base.py", line 1283, in _execute_context
self.dialect.do_execute(
File "/Users/mgrinberg/Documents/dev/python/microblog/venv/lib/python3.8/site-packages/sqlalchemy/engine/default.py", line 590, in do_execute
cursor.execute(statement, parameters)
sqlite3.OperationalError: near "DROP": syntax error
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
[ ... removed a long list of uninteresting stack frames ... ]
sqlalchemy.exc.OperationalError: (sqlite3.OperationalError) near "DROP": syntax error
[SQL: ALTER TABLE user DROP COLUMN about_me]
(Background on this error at: http://sqlalche.me/e/e3q8)
If you tried this on your database, delete the broken migration script before continuing. You will learn how to generate migrations that work better in the next section.
Using Batch Mode
Unfortunately there is no simple way to perform these operations that SQLite has not implemented. The only way to do this is to generate a brand new table with the new schema and copy all the data. Alembic includes support for migrating a table in this way with a feature called «batch mode».
You can enable batch mode in Flask-Migrate right when you initialize the extension. If you use the direct method of initialization:
migrate = Migrate(app, db, render_as_batch=True)
If you use the two-step initialization:
migrate = Migrate()
def create_app():
# ...
migrate.init_app(app, db, render_as_batch=True)
# ...
Now that you have batch mode enabled, try to generate the migration again:
(venv) $ flask db migrate -m "remove a column"
Nothing appears to be different, but if you look at the new migration script, you will see differences:
"""remove a column
Revision ID: adfac8a1b2ee
Revises: 834b1a697901
Create Date: 2020-07-19 18:32:27.782197
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'adfac8a1b2ee'
down_revision = '834b1a697901'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('user', schema=None) as batch_op:
batch_op.drop_column('about_me')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('user', schema=None) as batch_op:
batch_op.add_column(sa.Column('about_me', sa.VARCHAR(length=140), nullable=True))
# ### end Alembic commands ###
Complications
There are situations in which batch mode alone does not solve upgrade errors.
A nasty type of issue occurs when the ALTER TABLE
error occurs in the middle of a migration, after some operations were already applied. This could leave your database in an inconsistent state, where some changes from the migration script have been applied, but because of the error the version information is still pointing to the previous migration.
To unblock a database after a partial migration was applied I follow these steps:
- Determine which of the operations were applied.
- Delete everything from the
upgrade()
function. - Edit the
downgrade()
function so that it only contains the reverse of the operations that were applied to your database. - Run
flask db upgrade
. This is going to succeed because now you are running an empty upgrade. The database version will be upgraded. - Run
flask db downgrade
. This will undo those partial changes that were applied earlier, and reset the database version back to the last good state. - Delete the migration script and try again with batch mode enabled.
Another common issue occurs when your table has unnamed constraints, which the batch mode process can’t delete or modify because there is no way to refer to them by name. The Alembic documentation has some information on how to deal with unnamed constraints when using batch mode.
Conclusion
I hope this was a useful tip that can help you improve your migration workflow. Have you had issues with SQLite migrations that are not covered in this article? Let me know below in the comments.
Содержание
- Error on flask db upgrade #341
- Comments
- Fixing ALTER TABLE errors with Flask-Migrate and SQLite
- SQLite’s ALTER TABLE Implementation
- Using Batch Mode
- Complications
- Conclusion
Error on flask db upgrade #341
Hi! I’m trying to implement flask_migrate on my project. I use app factory pattern and followed the instructions from the blog. After setting «FLASK_APP=run.py», flask db init works fine and creates every file as expected. I create a test model, run flask db migrate , and the script is generated just fine. But when I run flask db upgrade, I get an error:
(venv) [guillermo@arch softalq]$ flask db upgrade
/home/guillermo/PycharmProjects/softalq/venv/lib/python3.7/site-packages/flask_sqlalchemy/init.py:835: FSADeprecationWarning: SQLALCHEMY_TRACK_MODIFICATIONS adds significant overhead and will be disabled by default in the future. Set it to True or False to suppress this warning.
‘SQLALCHEMY_TRACK_MODIFICATIONS adds significant overhead and ‘
INFO [alembic.runtime.migration] Context impl MySQLImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
INFO [alembic.runtime.migration] Running upgrade -> cc85dbd830d7, empty message
Traceback (most recent call last):
File «/home/guillermo/PycharmProjects/softalq/venv/lib/python3.7/site-packages/sqlalchemy/engine/base.py», line 1284, in _execute_context
cursor, statement, parameters, context
File «/home/guillermo/PycharmProjects/softalq/venv/lib/python3.7/site-packages/sqlalchemy/engine/default.py», line 590, in do_execute
cursor.execute(statement, parameters)
File «/home/guillermo/PycharmProjects/softalq/venv/lib/python3.7/site-packages/pymysql/cursors.py», line 170, in execute
result = self._query(query)
File «/home/guillermo/PycharmProjects/softalq/venv/lib/python3.7/site-packages/pymysql/cursors.py», line 328, in _query
conn.query(q)
File «/home/guillermo/PycharmProjects/softalq/venv/lib/python3.7/site-packages/pymysql/connections.py», line 517, in query
self._affected_rows = self._read_query_result(unbuffered=unbuffered)
File «/home/guillermo/PycharmProjects/softalq/venv/lib/python3.7/site-packages/pymysql/connections.py», line 732, in _read_query_result
result.read()
File «/home/guillermo/PycharmProjects/softalq/venv/lib/python3.7/site-packages/pymysql/connections.py», line 1075, in read
first_packet = self.connection._read_packet()
File «/home/guillermo/PycharmProjects/softalq/venv/lib/python3.7/site-packages/pymysql/connections.py», line 657, in _read_packet
packet_header = self._read_bytes(4)
File «/home/guillermo/PycharmProjects/softalq/venv/lib/python3.7/site-packages/pymysql/connections.py», line 707, in _read_bytes
CR.CR_SERVER_LOST, «Lost connection to MySQL server during query»)
pymysql.err.OperationalError: (2013, ‘Lost connection to MySQL server during query’)
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File «/home/guillermo/PycharmProjects/softalq/venv/bin/flask», line 10, in
sys.exit(main())
File «/home/guillermo/PycharmProjects/softalq/venv/lib/python3.7/site-packages/flask/cli.py», line 967, in main
cli.main(args=sys.argv[1:], prog_name=»python -m flask» if as_module else None)
File «/home/guillermo/PycharmProjects/softalq/venv/lib/python3.7/site-packages/flask/cli.py», line 586, in main
return super(FlaskGroup, self).main(*args, **kwargs)
File «/home/guillermo/PycharmProjects/softalq/venv/lib/python3.7/site-packages/click/core.py», line 782, in main
rv = self.invoke(ctx)
File «/home/guillermo/PycharmProjects/softalq/venv/lib/python3.7/site-packages/click/core.py», line 1259, in invoke
return _process_result(sub_ctx.command.invoke(sub_ctx))
File «/home/guillermo/PycharmProjects/softalq/venv/lib/python3.7/site-packages/click/core.py», line 1259, in invoke
return _process_result(sub_ctx.command.invoke(sub_ctx))
File «/home/guillermo/PycharmProjects/softalq/venv/lib/python3.7/site-packages/click/core.py», line 1066, in invoke
return ctx.invoke(self.callback, **ctx.params)
File «/home/guillermo/PycharmProjects/softalq/venv/lib/python3.7/site-packages/click/core.py», line 610, in invoke
return callback(*args, **kwargs)
File «/home/guillermo/PycharmProjects/softalq/venv/lib/python3.7/site-packages/click/decorators.py», line 21, in new_func
return f(get_current_context(), *args, **kwargs)
File «/home/guillermo/PycharmProjects/softalq/venv/lib/python3.7/site-packages/flask/cli.py», line 426, in decorator
return __ctx.invoke(f, *args, **kwargs)
File «/home/guillermo/PycharmProjects/softalq/venv/lib/python3.7/site-packages/click/core.py», line 610, in invoke
return callback(*args, **kwargs)
File «/home/guillermo/PycharmProjects/softalq/venv/lib/python3.7/site-packages/flask_migrate/cli.py», line 134, in upgrade
_upgrade(directory, revision, sql, tag, x_arg)
File «/home/guillermo/PycharmProjects/softalq/venv/lib/python3.7/site-packages/flask_migrate/init.py», line 96, in wrapped
f(*args, **kwargs)
File «/home/guillermo/PycharmProjects/softalq/venv/lib/python3.7/site-packages/flask_migrate/init.py», line 271, in upgrade
command.upgrade(config, revision, sql=sql, tag=tag)
File «/home/guillermo/PycharmProjects/softalq/venv/lib/python3.7/site-packages/alembic/command.py», line 298, in upgrade
script.run_env()
File «/home/guillermo/PycharmProjects/softalq/venv/lib/python3.7/site-packages/alembic/script/base.py», line 489, in run_env
util.load_python_file(self.dir, «env.py»)
File «/home/guillermo/PycharmProjects/softalq/venv/lib/python3.7/site-packages/alembic/util/pyfiles.py», line 98, in load_python_file
module = load_module_py(module_id, path)
File «/home/guillermo/PycharmProjects/softalq/venv/lib/python3.7/site-packages/alembic/util/compat.py», line 184, in load_module_py
spec.loader.exec_module(module)
File «», line 728, in exec_module
File «», line 219, in call_with_frames_removed
File «migrations/env.py», line 96, in
run_migrations_online()
File «migrations/env.py», line 90, in run_migrations_online
context.run_migrations()
File «», line 8, in run_migrations
File «/home/guillermo/PycharmProjects/softalq/venv/lib/python3.7/site-packages/alembic/runtime/environment.py», line 846, in run_migrations
self.get_context().run_migrations(**kw)
File «/home/guillermo/PycharmProjects/softalq/venv/lib/python3.7/site-packages/alembic/runtime/migration.py», line 520, in run_migrations
step.migration_fn(**kw)
File «/home/guillermo/PycharmProjects/softalq/migrations/versions/cc85dbd830d7.py», line 21, in upgrade
op.drop_column(‘datos’, ‘test’)
File «», line 8, in drop_column
File «», line 3, in drop_column
File «/home/guillermo/PycharmProjects/softalq/venv/lib/python3.7/site-packages/alembic/operations/ops.py», line 2049, in drop_column
return operations.invoke(op)
File «/home/guillermo/PycharmProjects/softalq/venv/lib/python3.7/site-packages/alembic/operations/base.py», line 374, in invoke
return fn(self, operation)
File «/home/guillermo/PycharmProjects/softalq/venv/lib/python3.7/site-packages/alembic/operations/toimpl.py», line 81, in drop_column
operation.table_name, column, schema=operation.schema, **operation.kw
File «/home/guillermo/PycharmProjects/softalq/venv/lib/python3.7/site-packages/alembic/ddl/impl.py», line 240, in drop_column
self._exec(base.DropColumn(table_name, column, schema=schema))
File «/home/guillermo/PycharmProjects/softalq/venv/lib/python3.7/site-packages/alembic/ddl/impl.py», line 140, in _exec
return conn.execute(construct, *multiparams, **params)
File «/home/guillermo/PycharmProjects/softalq/venv/lib/python3.7/site-packages/sqlalchemy/engine/base.py», line 1020, in execute
return meth(self, multiparams, params)
File «/home/guillermo/PycharmProjects/softalq/venv/lib/python3.7/site-packages/sqlalchemy/sql/ddl.py», line 72, in _execute_on_connection
return connection._execute_ddl(self, multiparams, params)
File «/home/guillermo/PycharmProjects/softalq/venv/lib/python3.7/site-packages/sqlalchemy/engine/base.py», line 1082, in _execute_ddl
compiled,
File «/home/guillermo/PycharmProjects/softalq/venv/lib/python3.7/site-packages/sqlalchemy/engine/base.py», line 1324, in execute_context
e, statement, parameters, cursor, context
File «/home/guillermo/PycharmProjects/softalq/venv/lib/python3.7/site-packages/sqlalchemy/engine/base.py», line 1518, in handle_dbapi_exception
sqlalchemy_exception, with_traceback=exc_info[2], from=e
File «/home/guillermo/PycharmProjects/softalq/venv/lib/python3.7/site-packages/sqlalchemy/util/compat.py», line 178, in raise
raise exception
File «/home/guillermo/PycharmProjects/softalq/venv/lib/python3.7/site-packages/sqlalchemy/engine/base.py», line 1284, in _execute_context
cursor, statement, parameters, context
File «/home/guillermo/PycharmProjects/softalq/venv/lib/python3.7/site-packages/sqlalchemy/engine/default.py», line 590, in do_execute
cursor.execute(statement, parameters)
File «/home/guillermo/PycharmProjects/softalq/venv/lib/python3.7/site-packages/pymysql/cursors.py», line 170, in execute
result = self._query(query)
File «/home/guillermo/PycharmProjects/softalq/venv/lib/python3.7/site-packages/pymysql/cursors.py», line 328, in _query
conn.query(q)
File «/home/guillermo/PycharmProjects/softalq/venv/lib/python3.7/site-packages/pymysql/connections.py», line 517, in query
self._affected_rows = self._read_query_result(unbuffered=unbuffered)
File «/home/guillermo/PycharmProjects/softalq/venv/lib/python3.7/site-packages/pymysql/connections.py», line 732, in _read_query_result
result.read()
File «/home/guillermo/PycharmProjects/softalq/venv/lib/python3.7/site-packages/pymysql/connections.py», line 1075, in read
first_packet = self.connection._read_packet()
File «/home/guillermo/PycharmProjects/softalq/venv/lib/python3.7/site-packages/pymysql/connections.py», line 657, in _read_packet
packet_header = self._read_bytes(4)
File «/home/guillermo/PycharmProjects/softalq/venv/lib/python3.7/site-packages/pymysql/connections.py», line 707, in _read_bytes
CR.CR_SERVER_LOST, «Lost connection to MySQL server during query»)
sqlalchemy.exc.OperationalError: (pymysql.err.OperationalError) (2013, ‘Lost connection to MySQL server during query’)
[SQL: ALTER TABLE datos DROP COLUMN test]
(Background on this error at: http://sqlalche.me/e/e3q8)
The changes are reflected to the db, but if I make another change in models.py and try to do another flask db migrate , it reports the db is not up to date. I’m using Mariadb on Arch Linux, latest SQLAlchemy and Flask_migrate with Python 3.7. I hope I’ve given enough info.
Cheers
The text was updated successfully, but these errors were encountered:
Источник
Fixing ALTER TABLE errors with Flask-Migrate and SQLite
If you’ve done any work with SQLite databases you surely know that this database is very limited in terms of making changes to the database schema. When working with a migration framework such as Flask-Migrate, it is common to end up with migration scripts that fail to upgrade or downgrade just because they need to remove or modify a column in a table, something that SQLite does not support.
In this article I’m going to discuss this limitation of the SQLite database, and show you a workaround that is specific to Flask-Migrate and Alembic.
SQLite’s ALTER TABLE Implementation
Changes that you make to the fields in your model or to the constraints associated with them will end up as an ALTER TABLE statement sent to the database. If you are using MySQL, Postgres or most other database servers besides SQLite, this isn’t a problem. With SQLite, however, the ALTER TABLE command only supports adding or renaming columns. Any other change to columns or constraints is going to be rejected with an error.
You can test this yourself very easily. Take any Flask-SQLAlchemy application (you can use one of mine) and after making sure your database is up to date, remove or comment out a column in one of the models. Then generate a migration:
If you open the generated migration script everything will look correct. Below you can see the migration that was generated after I removed a column named about_me from the User model:
The problem occurs when you try to upgrade the database with this migration:
If you tried this on your database, delete the broken migration script before continuing. You will learn how to generate migrations that work better in the next section.
Using Batch Mode
Unfortunately there is no simple way to perform these operations that SQLite has not implemented. The only way to do this is to generate a brand new table with the new schema and copy all the data. Alembic includes support for migrating a table in this way with a feature called «batch mode».
You can enable batch mode in Flask-Migrate right when you initialize the extension. If you use the direct method of initialization:
If you use the two-step initialization:
Now that you have batch mode enabled, try to generate the migration again:
Nothing appears to be different, but if you look at the new migration script, you will see differences:
Complications
There are situations in which batch mode alone does not solve upgrade errors.
A nasty type of issue occurs when the ALTER TABLE error occurs in the middle of a migration, after some operations were already applied. This could leave your database in an inconsistent state, where some changes from the migration script have been applied, but because of the error the version information is still pointing to the previous migration.
To unblock a database after a partial migration was applied I follow these steps:
- Determine which of the operations were applied.
- Delete everything from the upgrade() function.
- Edit the downgrade() function so that it only contains the reverse of the operations that were applied to your database.
- Run flask db upgrade . This is going to succeed because now you are running an empty upgrade. The database version will be upgraded.
- Run flask db downgrade . This will undo those partial changes that were applied earlier, and reset the database version back to the last good state.
- Delete the migration script and try again with batch mode enabled.
Another common issue occurs when your table has unnamed constraints, which the batch mode process can’t delete or modify because there is no way to refer to them by name. The Alembic documentation has some information on how to deal with unnamed constraints when using batch mode.
Conclusion
I hope this was a useful tip that can help you improve your migration workflow. Have you had issues with SQLite migrations that are not covered in this article? Let me know below in the comments.
Hello, and thank you for visiting my blog! If you enjoyed this article, please consider supporting my work on this blog on Patreon!
Источник
On a client project recently we wanted to understand what happens when a flask db upgrade
operation against a Postgres database fails.
More specifically we wanted to find out what happens when a batch of scripts is being applied, and 1 script in the middle of the batch fails, e.g: in this scenario:
Given a migration set consisting of [X, Y, Z]
When the migration X succeeds
And the migration Y fails
Then ?
Flask could do one of two things:
- Leave script X applied to the database, roll back script Y
- Roll back both scripts X and Y
However in the documentation it wasn’t wholly clear what it would do.
We theorised the following would happen:
Scenario: Single migration in set succeeds
Given a migration set consisting of [X]
When the migration X succeeds
Then the migration X is applied to the database
Scenario: Single migration in set fails
Given a migration set consisting of [X]
When the migration X fails
Then the migration X is rolled back
Scenario: Multiple migrations in set, 1 fails in the set
Given a migration set consisting of [X, Y, Z]
When the migration X succeeds
And the migration Y fails
Then the migration Y is rolled back
And the migration X is rolled back
And the migration Z is never applied
How would we test this though?
Validating the Theory
All the below code can be found in this repository along with instructions on how to set this up locally: https://github.com/lukemerrett/flask-migration-rollback-research
Environment
Firstly we’ll provision an environment with a Postgres database running using Docker compose, hosted on port 6456.
# docker-compose.yml
version: "3.9"
services:
db:
image: postgres
restart: always
environment:
- POSTGRES_DB=postgres
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
ports:
- "6456:5432"
Then spin it up using docker compose up -d
Application
Next we have an application that is used to generate 4 dummy database migration scripts that looks like this:
# app.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "postgresql://postgres:postgres@localhost:6456/postgres"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
db = SQLAlchemy(app)
migrate = Migrate(app, db)
class User(db.Model):
name = db.Column(db.String(128), primary_key=True)
date = db.Column(db.DateTime())
department = db.Column(db.String(128))
Modifying the User
model and running flask db migrate
will generate a new script applying those changes. The above code is the «final state» of the migration.
Migration Scripts
Through modifying the User
and running flask db migrate
a few times we end up with the below 4 migration scripts:
Script 1: dd7c69423ad9_.py
Creates the user
table with an id
integer primary key and a name
string field.
"""empty message
Revision ID: dd7c69423ad9
Revises:
Create Date: 2021-08-20 09:41:57.938972
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'dd7c69423ad9'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('user',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=128), nullable=True),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('user')
# ### end Alembic commands ###
Script 2: 5a77262c502b_.py
Adds the date
column of type date time to the user
table:
"""empty message
Revision ID: 5a77262c502b
Revises: dd7c69423ad9
Create Date: 2021-08-20 11:57:30.389344
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '5a77262c502b'
down_revision = 'dd7c69423ad9'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('user', sa.Column('date', sa.DateTime(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('user', 'date')
# ### end Alembic commands ###
Script 3: 653d68312991_.py — Broken Script
This is a modified script that will fail, it tries to drop a column on a table that doesn’t exist. We’ve had to manually add that line in to break it.
When flask db upgrade
reaches this script, it will error.
"""empty message
Revision ID: 653d68312991
Revises: 5a77262c502b
Create Date: 2021-08-20 12:04:14.107041
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '653d68312991'
down_revision = '5a77262c502b'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('user', 'name',
existing_type=sa.VARCHAR(length=128),
nullable=False)
op.drop_column('jim', 'id')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('user', sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False))
op.alter_column('user', 'name',
existing_type=sa.VARCHAR(length=128),
nullable=True)
# ### end Alembic commands ###
Script 4: 5929dc9c624a_.py
The final script, one we expect to never run due to the broken script before it, adds a department
field to the user
table:
"""empty message
Revision ID: 5929dc9c624a
Revises: 653d68312991
Create Date: 2021-08-20 12:26:57.567531
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '5929dc9c624a'
down_revision = '653d68312991'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('user', sa.Column('department', sa.String(length=128), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('user', 'department')
# ### end Alembic commands ###
Running the Upgrade Tests
Now we need to test our 3 scenarios with the 4 scripts created above.
Scenario: Single migration in set succeeds
Given a migration set consisting of [X]
When the migration X succeeds
Then the migration X is applied to the database
To test this we run flask db upgrade dd7c69423ad9
(the first script in our batch) which applies a valid script to the database. This succeeds as expected and the database is brought up to date with the new user
table.
Scenario: Single migration in set fails
Given a migration set consisting of [X]
When the migration X fails
Then the migration X is rolled back
To test this we first run flask db upgrade 5a77262c502b
, which is the second script that we also know works correctly. The database now has the first 2 scripts applied and is ready for the broken script to be run,
Running flask db upgrade 653d68312991
(the broken script) correctly fails and any changes are rolled back. Because we’ve run scripts dd7c69423ad9
and 5a77262c502b
in separate upgrade operations they are still applied to the database, in this scenario only script 653d68312991
is rolled back.
Scenario: Multiple migrations in set, 1 fails in the set
Given a migration set consisting of [X, Y, Z]
When the migration X succeeds
And the migration Y fails
Then the migration Y is rolled back
And the migration X is rolled back
And the migration Z is never applied
Firstly we need to get our database into the correct state so there is a batch of upgrades that starts with 1 valid script, then a broken one, then a valid one again.
To do this we run:
# Ensure we've at least applied the stable first revision
flask db upgrade dd7c69423ad9
# Ensure if other revisions have been applied since, we return to the stable revision
flask db downgrade dd7c69423ad9
Leaving us with scripts 5a77262c502b
(working), 653d68312991
(broken) and 5929dc9c624a
(working) in a batch to be applied on the next upgrade
.
Now running flask db upgrade
will do the following:
- Apply
5a77262c502b
, succeeded - Apply
653d68312991
, failed - Roll back
653d68312991
- Roll back
5a77262c502b
- Never attempt to run
5929dc9c624a
Findings
If any of the scripts in a batch fail during a flask db upgrade
, then none of the scripts in that batch are applied.
This means the database isn’t left in a state where only partial migrations have been applied.
This proves all our theorised scenarios are correct.
This works exactly the same in multidb
mode; if 1 script across any of the databases fails on upgrade
, every script in the batch across all databases is rolled back (see the behaviour-on-multi-db branch in the aforementioned repo).