Flask-Migrate: Alembic database migration wrapper for Flask
Posted by
on underIn this post I introduce you to Flask-Migrate, a new database migration handler for Flask based on Alembic that I just made public.
Is a New Extension Necessary?
If you read the database chapter of my Mega-Tutorial, you know that I have chosen sqlalchemy-migrate for database migrations.
I liked sqlalchemy-migrate back then (I still do, actually), but its development appears to have halted completely. Support for SQLAlchemy 0.8.x has not been implemented yet, six months past the 0.8.0 release.
On the other side, since I wrote my migration Mega-Tutorial chapter Alembic has gained notoriety. Alembic is written by zzzeek (Mike Bayer), who is the author of SQLAlchemy. He is actively developing Alembic on bitbucket.
There is an extension called Flask-Alembic out there that has many similarities to mine, but that project also appears to have stalled, there haven't been any commits or messages from the developers in several months. The project was never made available on the Python Package Index (PyPI), so while it is possible to install directly from git, that is less ideal, and might be a deal breaker for some.
That is why I have decided to write Flask-Migrate. Out of respect for the Flask-Alembic project I decided to use a different name on PyPI, in case they ever decide to resume work on their project and publish it.
Using Flask-Migrate
Flask-Migrate provides a set of command line options that attach to Flask-Script.
To install the extension you use pip
as usual:
$ pip install flask-migrate
As part of the installation you will also get Flask, Flask-SQLAlchemy and Flask-Script.
Below is a sample application that initializes Flask-Migrate and registers it with Flask-Script. As is typically the case with Flask-Script, the script is called manage.py
:
from flask import Flask
from flask.ext.sqlalchemy import SQLAlchemy
from flask.ext.script import Manager
from flask.ext.migrate import Migrate, MigrateCommand
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///app.db'
db = SQLAlchemy(app)
migrate = Migrate(app, db)
manager = Manager(app)
manager.add_command('db', MigrateCommand)
class User(db.Model):
id = db.Column(db.Integer, primary_key = True)
name = db.Column(db.String(128))
if __name__ == '__main__':
manager.run()
When you run the application you get an additional db
option in the command line (you can call it differently if you want, of course):
$ python manage.py --help
usage: manage.py [-h] {shell,db,runserver} ...
positional arguments:
{shell,db,runserver}
shell Runs a Python shell inside Flask application context.
db Perform database migrations
runserver Runs the Flask development server i.e. app.run()
optional arguments:
-h, --help show this help message and exit
The db
command exposes most of the Alembic options:
$ python manage.py db --help
usage: Perform database migrations
positional arguments:
{upgrade,migrate,current,stamp,init,downgrade,history,revision}
upgrade Upgrade to a later version
migrate Alias for 'revision --autogenerate'
current Display the current revision for each database.
stamp 'stamp' the revision table with the given revision;
dont run any migrations
init Generates a new migration
downgrade Revert to a previous version
history List changeset scripts in chronological order.
revision Create a new revision file.
optional arguments:
-h, --help show this help message and exit
To add migration support to your database you just need to run the init
command:
$ python manage.py db init
Creating directory /home/miguel/app/migrations...done
Creating directory /home/miguel/app/migrations/versions...done
Generating /home/miguel/app/alembic.ini...done
Generating /home/miguel/app/migrations/env.py...done
Generating /home/miguel/app/migrations/env.pyc...done
Generating /home/miguel/app/migrations/README...done
Generating /home/miguel/app/migrations/script.py.mako...done
Please edit configuration/connection/logging settings in
'/home/miguel/app/migrations/alembic.ini' before proceeding.
Note that you should replace manage.py
with the name of your launch script if you used a different name.
When you use Alembic alone you have to edit a couple of configuration files, but Flask-Migrate handles all that for you. When the init
command completes you will have a migrations folder with the configuration files ready to be used.
To issue your first migration you can run the following command:
$ python manage.py db migrate
INFO [alembic.migration] Context impl SQLiteImpl.
INFO [alembic.migration] Will assume non-transactional DDL.
INFO [alembic.autogenerate] Detected added table 'user'
Generating /home/miguel/app/migrations/versions/4708a5190f2_.py...done
The migrate
command adds a new migration script. You should review it and edit it to be accurate, as Alembic cannot detect all changes that you make to your models. In particular it does not detect indexes, so those need to be added manually to the script.
If you prefer to write your migration scripts from scratch then use revision
instead of migrate
:
$ python manage.py db revision
Generating /home/miguel/app/migrations/versions/15c04479d683_.py...done
You can read Alembic's documentation to learn how to write migration scripts.
The next step is to apply the migration to the database. For this you use the upgrade
command:
$ python manage.py db upgrade
INFO [alembic.migration] Context impl SQLiteImpl.
INFO [alembic.migration] Will assume non-transactional DDL.
INFO [alembic.migration] Running upgrade None -> 4708a5190f2, empty message
And that's it! Your database is now synchronized with your models.
You should add all the files in the migrations folder to version control along with your source files. If you need to update another system to the latest database version you just need to update your source tree on that other system and then run db upgrade
, like you did above.
If you have any suggestions to improve this extension please let me know below in the comments.
Miguel
-
#101 Leon said
Hi Miguel, Thank you for your tutorial. I was trying to add a custom data type in flasky to support json. The module is in app/utils/customDataType.py. And the new migration scripts is generated in migrations/versions/...
In the script, I need to import the 'JsonType'.
from ...app.utils.customDataType import JsonType
Also, I add init.py files in root, migrations and versions folder, which didn't help solve the import exception "Attempted relative import in non-package".
How can I import module in app? Thank you.
-
#102 Shahab said
Hi Miguel,
I am following you tutorial, but get stock at this point:
$ python manage.py db init
Traceback (most recent call last):
File "./manage.py", line 139, in <module>
manager.run()
File "/home/shahab/projects/coreapis/venv/local/lib/python2.7/site-packages/flask_script/init.py", line 412, in run
result = self.handle(sys.argv[0], sys.argv[1:])
File "/home/shahab/projects/coreapis/venv/local/lib/python2.7/site-packages/flask_script/init.py", line 383, in handle
res = handle(args, config)
File "/home/shahab/projects/coreapis/venv/local/lib/python2.7/site-packages/flask_script/commands.py", line 216, in call
return self.run(args, **kwargs)
File "/home/shahab/projects/coreapis/venv/local/lib/python2.7/site-packages/flask_migrate/init.py", line 58, in init
directory = current_app.extensions['migrate'].directory
KeyError: 'migrate'Could you please tell me what I have done wrong.
Thank you
-
#103 Miguel Grinberg said
@Shahab: did you initialize the Migrate class?
-
#104 Miguel Grinberg said
@Leon: It seems you have a Python import problem, unrelated to Flask or Flask-Migrate. I recommend that you write a question on Stack Overflow and add the relevant code and the full stack trace of the error.
-
#105 Valentin said
Hi,
Thanks for this great tool.
I just have one problem/question.
I have now a bunch of revisions files. It works great to update any system to the last version. I wonder however how I can handle setting up a new system from scratch.
I can of course generate the database using sqlalchemy db.create_all() but I don't consider it a good solution because it's different from what I do for existing systems. I want to be sure they ends with the same results. It can also be a problem if some custom statements were added to the revisions scripts, they would not be executed for this new system.
What I would expect: a way to create the database in the initial version before any migrations applied and then apply all migrations scripts.
Does this make sense? Is there a simple way to do this? -
#106 Miguel Grinberg said
@Valentin: database creation always happens outside of SQLAlchemy and Alembic, and you need to do it on your own (except for sqlite, where the db file is created automatically by SQLAlchemy). The db.create_all() function only creates tables, it does not create a database. So the steps to create an updated database from scratch are two: #1 create the database using the db admin tool of your choice, and #2 run the "db upgrade" command to let Alembic create the tables via the migration scripts.
-
#107 Jason said
Hi Miguel,
Thanks so much for the tutorials, they are really helpful!
I have been able to follow it while using multiple databases:
$ python manage.py db init --multidb
$ python manage.py db migrate
However, it throws an database is locked error when I try to do the upgrade:
$ python manage.py db upgrade
The error:
sqlalchemy.exc.OperationalError: (sqlite3.OperationalError) database is lockedCould you help me solve this please? Thanks!
-
#108 Miguel Grinberg said
@Jason: Could be that your database file is open by another process at the time you are trying to migrate it. The error is unrelated to Flask-Migrate, Alembic or SQLAlchemy.
-
#109 Pranay Khilari said
Hi Miguel,
I read your document which is excellent, Also I read all the comments one thing is still not clear for me is below case.
1) I have model package which contains all the models.
2) I am not writing the model in manage.py file.
3) If I used the flask-migrate it is not detecting the models which are present in my model package.
4) My database is empty.Please help me.
-
#110 Miguel Grinberg said
@Pranay: you need to import your models. If you do not import them, then SQLAlchemy does not know about them.
-
#111 Mike A said
Dear Sir. First, than you so much for all your contributions to the community. Your mega-tutorial gave me the leg up I needed.
When using Migrate in a simple flask app, there is an issue with mysql and boolean fields. In the models file, the field is specified like so: "db.Column(db.Boolean, unique=False, default=False)". When the column is created in the DB, its created as a tinyint, which is fine, but every time migrate is used, it detects the tinyint/boolean difference and tries to update it:
"INFO [alembic.autogenerate.compare] Detected type change from TINYINT(display_width=1) to Boolean() on 'circuits.is_child'"I currently go in and curate the .py file in the versions folder to deal with it, but it becomes tedious now that I have 40 booleans in my app. I know I could change the column type in models to "db.Column(TINYINT(display_width=1), default=0)" but this outputs as a "1" rather than as "True" so im not sure its the best way to go.
Any thoughts?
-
#112 Miguel Grinberg said
@Mike: this issue explains the problem and provides some solutions: https://github.com/miguelgrinberg/Flask-Migrate/issues/143.
-
#113 Ravina said
Hi Miguel,
Can u help me out this error
Can't load plugin: sqlalchemy.dialects:mysql.pymysql
How to rectify this error -
#114 Miguel Grinberg said
@Ravina: you need to install pymysql into your virtual environment.
-
#115 Tim said
Hi Miguel and thanks for the great tutorial!
I created a model with incorrect column type (instead of String I used Integer on task id). I tried changing it afterwards using ALTER TABLE via MYSQL Workbench but because the table had a foreign key restriction to result table I was not able to do this. I tried to remove the foreign key restriction, but eventually got tired of figuring out how to do this, and removed both task and result tables with DROP TABLE.
I thought that Flask-Migrate would just re-create the missing tables. But now when I run "flask db migrate" and "flask db upgrade" I get an error. So is there a way to just start over? All I have is one line in the user table, so I would just like to be able to re-create all tables.
-
#116 Miguel Grinberg said
If you want tables to be recreated you would run the upgrade command. The migrate command generates a new migration, that is used when you make changes to the models and want those changes applied to the database.
Maybe you should show the error that you are getting if you can't get this to work when you run the upgrade command alone.
-
#117 Tim said
It seems that it wants the tables to exist:
(venv) user@mypc:/mnt/c/flask/myapp$ flask db upgrade
INFO [alembic.runtime.migration] Context impl MySQLImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
INFO [alembic.runtime.migration] Running upgrade 26159dd471d8 -> 4ee052488c48, changed task id type
Traceback (most recent call last):
File "/home/user/.virtualenvs/venv/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 1244, in _execute_context
cursor, statement, parameters, context
File "/home/user/.virtualenvs/venv/lib/python3.6/site-packages/sqlalchemy/engine/default.py", line 550, in do_execute
cursor.execute(statement, parameters)
File "/home/user/.virtualenvs/venv/lib/python3.6/site-packages/pymysql/cursors.py", line 170, in execute
result = self._query(query)
File "/home/user/.virtualenvs/venv/lib/python3.6/site-packages/pymysql/cursors.py", line 328, in _query
conn.query(q)
File "/home/user/.virtualenvs/venv/lib/python3.6/site-packages/pymysql/connections.py", line 517, in query
self._affected_rows = self._read_query_result(unbuffered=unbuffered)
File "/home/user/.virtualenvs/venv/lib/python3.6/site-packages/pymysql/connections.py", line 732, in _read_query_result
result.read()
File "/home/user/.virtualenvs/venv/lib/python3.6/site-packages/pymysql/connections.py", line 1075, in read
first_packet = self.connection._read_packet()
File "/home/user/.virtualenvs/venv/lib/python3.6/site-packages/pymysql/connections.py", line 684, in _read_packet
packet.check_error()
File "/home/user/.virtualenvs/venv/lib/python3.6/site-packages/pymysql/protocol.py", line 220, in check_error
err.raise_mysql_exception(self._data)
File "/home/user/.virtualenvs/venv/lib/python3.6/site-packages/pymysql/err.py", line 109, in raise_mysql_exception
raise errorclass(errno, errval)
pymysql.err.ProgrammingError: (1146, "Table 'myapp.result' doesn't exist")The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "/home/user/.virtualenvs/venv/bin/flask", line 10, in <module>
sys.exit(main())
File "/home/user/.virtualenvs/venv/lib/python3.6/site-packages/flask/cli.py", line 966, in main
cli.main(prog_name="python -m flask" if as_module else None)
File "/home/user/.virtualenvs/venv/lib/python3.6/site-packages/flask/cli.py", line 586, in main
return super(FlaskGroup, self).main(args, kwargs)
File "/home/user/.virtualenvs/venv/lib/python3.6/site-packages/click/core.py", line 717, in main
rv = self.invoke(ctx)
File "/home/user/.virtualenvs/venv/lib/python3.6/site-packages/click/core.py", line 1137, in invoke
return _process_result(sub_ctx.command.invoke(sub_ctx))
File "/home/user/.virtualenvs/venv/lib/python3.6/site-packages/click/core.py", line 1137, in invoke
return _process_result(sub_ctx.command.invoke(sub_ctx))
File "/home/user/.virtualenvs/venv/lib/python3.6/site-packages/click/core.py", line 956, in invoke
return ctx.invoke(self.callback, ctx.params)
File "/home/user/.virtualenvs/venv/lib/python3.6/site-packages/click/core.py", line 555, in invoke
return callback(args, kwargs)
File "/home/user/.virtualenvs/venv/lib/python3.6/site-packages/click/decorators.py", line 17, in new_func
return f(get_current_context(), *args, kwargs)
File "/home/user/.virtualenvs/venv/lib/python3.6/site-packages/flask/cli.py", line 426, in decorator
return ctx.invoke(f, args, kwargs)
File "/home/user/.virtualenvs/venv/lib/python3.6/site-packages/click/core.py", line 555, in invoke
return callback(args, kwargs)
File "/home/user/.virtualenvs/venv/lib/python3.6/site-packages/flask_migrate/cli.py", line 134, in upgrade
_upgrade(directory, revision, sql, tag, x_arg)
File "/home/user/.virtualenvs/venv/lib/python3.6/site-packages/flask_migrate/init.py", line 95, in wrapped
f(*args, kwargs)
File "/home/user/.virtualenvs/venv/lib/python3.6/site-packages/flask_migrate/__init.py", line 280, in upgrade
command.upgrade(config, revision, sql=sql, tag=tag)
File "/home/user/.virtualenvs/venv/lib/python3.6/site-packages/alembic/command.py", line 276, in upgrade
script.run_env()
File "/home/user/.virtualenvs/venv/lib/python3.6/site-packages/alembic/script/base.py", line 475, in run_env
util.load_python_file(self.dir, "env.py")
File "/home/user/.virtualenvs/venv/lib/python3.6/site-packages/alembic/util/pyfiles.py", line 90, in load_python_file
module = load_module_py(module_id, path)
File "/home/user/.virtualenvs/venv/lib/python3.6/site-packages/alembic/util/compat.py", line 177, in load_module_py
spec.loader.exec_module(module)
File "<frozen importlib._bootstrap_external>", line 678, in exec_module
File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
File "migrations/env.py", line 96, in <module>
run_migrations_online()
File "migrations/env.py", line 90, in run_migrations_online
context.run_migrations()
File "<string>", line 8, in run_migrations
File "/home/user/.virtualenvs/venv/lib/python3.6/site-packages/alembic/runtime/environment.py", line 839, in run_migrations
self.get_context().run_migrations(kw)
File "/home/user/.virtualenvs/venv/lib/python3.6/site-packages/alembic/runtime/migration.py", line 362, in run_migrations
step.migration_fn(kw)
File "/mnt/c/flask/myapp/migrations/versions/4ee052488c48_changed_task_id_type.py", line 24, in upgrade
existing_nullable=True)
File "<string>", line 8, in alter_column
File "<string>", line 3, in alter_column
File "/home/user/.virtualenvs/venv/lib/python3.6/site-packages/alembic/operations/ops.py", line 1775, in alter_column
return operations.invoke(alt)
File "/home/user/.virtualenvs/venv/lib/python3.6/site-packages/alembic/operations/base.py", line 345, in invoke
return fn(self, operation)
File "/home/user/.virtualenvs/venv/lib/python3.6/site-packages/alembic/operations/toimpl.py", line 56, in alter_column
operation.kw
File "/home/user/.virtualenvs/venv/lib/python3.6/site-packages/alembic/ddl/mysql.py", line 96, in alter_column
else existing_comment,
File "/home/user/.virtualenvs/venv/lib/python3.6/site-packages/alembic/ddl/impl.py", line 134, in _exec
return conn.execute(construct, *multiparams, params)
File "/home/user/.virtualenvs/venv/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 988, in execute
return meth(self, multiparams, params)
File "/home/user/.virtualenvs/venv/lib/python3.6/site-packages/sqlalchemy/sql/ddl.py", line 72, in _execute_on_connection
return connection._execute_ddl(self, multiparams, params)
File "/home/user/.virtualenvs/venv/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 1050, in _execute_ddl
compiled,
File "/home/user/.virtualenvs/venv/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 1248, in _execute_context
e, statement, parameters, cursor, context
File "/home/user/.virtualenvs/venv/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 1466, in _handle_dbapi_exception
util.raise_from_cause(sqlalchemy_exception, exc_info)
File "/home/user/.virtualenvs/venv/lib/python3.6/site-packages/sqlalchemy/util/compat.py", line 399, in raise_from_cause
reraise(type(exception), exception, tb=exc_tb, cause=cause)
File "/home/user/.virtualenvs/venv/lib/python3.6/site-packages/sqlalchemy/util/compat.py", line 153, in reraise
raise value.with_traceback(tb)
File "/home/user/.virtualenvs/venv/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 1244, in _execute_context
cursor, statement, parameters, context
File "/home/user/.virtualenvs/venv/lib/python3.6/site-packages/sqlalchemy/engine/default.py", line 550, in do_execute
cursor.execute(statement, parameters)
File "/home/user/.virtualenvs/venv/lib/python3.6/site-packages/pymysql/cursors.py", line 170, in execute
result = self._query(query)
File "/home/user/.virtualenvs/venv/lib/python3.6/site-packages/pymysql/cursors.py", line 328, in _query
conn.query(q)
File "/home/user/.virtualenvs/venv/lib/python3.6/site-packages/pymysql/connections.py", line 517, in query
self._affected_rows = self._read_query_result(unbuffered=unbuffered)
File "/home/user/.virtualenvs/venv/lib/python3.6/site-packages/pymysql/connections.py", line 732, in _read_query_result
result.read()
File "/home/user/.virtualenvs/venv/lib/python3.6/site-packages/pymysql/connections.py", line 1075, in read
first_packet = self.connection._read_packet()
File "/home/user/.virtualenvs/venv/lib/python3.6/site-packages/pymysql/connections.py", line 684, in _read_packet
packet.check_error()
File "/home/user/.virtualenvs/venv/lib/python3.6/site-packages/pymysql/protocol.py", line 220, in check_error
err.raise_mysql_exception(self._data)
File "/home/user/.virtualenvs/venv/lib/python3.6/site-packages/pymysql/err.py", line 109, in raise_mysql_exception
raise errorclass(errno, errval)
sqlalchemy.exc.ProgrammingError: (pymysql.err.ProgrammingError) (1146, "Table 'myapp.result' doesn't exist")
[SQL: ALTER TABLE result MODIFY payload_json TEXT(2000) NULL]
(Background on this error at: http://sqlalche.me/e/f405)I want to run several tasks at the same time and also save the task results. I am still trying to figure out the best database design for this, so most likely I will be modifying the DB as I go. Here are my models for 'task' and 'result' tables:
class Result(db.Model):
id = db.Column(db.Integer, primary_key=True)
task_id = db.Column(db.String, db.ForeignKey('task.id'))
payload_json = db.Column(db.Text(2000))class Task(db.Model):
__tablename__ = 'task' id = db.Column(db.String(50), primary_key=True) name = db.Column(db.String(128), index=True) user_id = db.Column(db.Integer, db.ForeignKey('user.id')) results = db.relationship('Result', backref='task', lazy='dynamic') complete = db.Column(db.Boolean, default=False)
I thought about re-creating the tables via MySQL Workbench but I wasn't 100% sure what SQLAlchemy actually does with backref and lazy commands (what are the corresponding SQL commands).
-
#118 Miguel Grinberg said
@Tim: the migration script seems to assume the myapp.result table exists, but in reality it does not. You may want to delete that migration script and generate a new one. Then you should ensure this table is detected as a new table, not as an existing table that needs a change.
-
#119 Vova said
Hi Miguel. I have a question. Is it okay that - after opening app.db - I see a wrong encoding? I mean like this https://clip2net.com/s/4767OF3. After I change the encoding, the migration breaks, and it's impossible to do anything even if I return to the previous encoding. I have to completely remove the app.db to make everything work again, but it's a bed solution in many ways I guess...
-
#120 Miguel Grinberg said
@Vova: were you opening the sqlite file in a text editor? That will never work, this file is not in any encoding, it is a binary file, so it will never display correctly in a text editor.
-
#121 Andrés Darío Altamirano said
Hello Miguel! I have a doubt regarding the db migrate. What do you think it is the best approach for using migrations that are from different packages such as sqlalchemy_utils?
I am trying to use sqlalchemy_utils.UUID.types.UUIDType for a field on a MySQL database, but when a migration is created, Alembic does not import that sqlalchemy_utils library so the migration crashes. Of course it would be easy to add the import inside the migration file, or, add it in the script.py.mako so it can be used broadly, but, in case you are in a project where the whole migrations folder is not versioned and docker takes care of creating that folder and apply the detected migrations, I can't find an decent way to achieve the use of a third party library as this one.Regards
Andrés
-
#122 Miguel Grinberg said
@Andrés: the only two solutions I know are the two that you mentioned: either add the import manually, or add it to the template migration file. I do not know what use case you have where migrations aren't versioned. Neither Flask-Migrate nor Alembic are intended to be use that way, since automatic migration generation is not always possible.
-
#123 Rahul said
How do we manage migrations when working on the same project with multiple team members using a central remote repo, what strategy can be used so that migration versions don't diverge when someone is making changes?
-
#124 Miguel Grinberg said
@Rahul: This is handled by Alembic, not this extension. See the branching and merging support in the Alembic docs.