How To Add Flask-Migrate To An Existing Project

Posted by
on under

A question that I frequently get is how to start tracking database migrations with my Flask-Migrate extension when the project has an established database, making it impossible to delete everything and start over from scratch. In this article and companion video I'm going to show you how to set up Flask-Migrate with zero risk for your current database.

Installing and Configuring Flask-Migrate

This step is the same for new and existing projects. Assuming you have a project that uses Flask-SQLAlchemy to manage a database, you begin by installing the Flask-Migrate extension:

(venv) $ pip install flask-migrate

The extension then needs to be added to the project. This involves creating and initializing a Migrate instance. If you use the direct method of creating extensions:

from flask_migrate import Migrate

app = Flask(__name__)
db = SQLAlchemy(app)
migrate = Migrate(app, db)

If you use an application factory function you very likely use the delayed method to create your extensions. In that case you would initialize Flask-Migrate as follows:

from flask_migrate import Migrate

db = SQLAlchemy()
migrate = Migrate()

def create_app():
    app = Flask(__name__)
    db.init_app(app)
    migrate.init_app(app, db)
    return app

Once the extension is added to the project you will have a flask db command available:

(venv) $ flask db
Usage: flask db [OPTIONS] COMMAND [ARGS]...

  Perform database migrations.

Options:
  --help  Show this message and exit.

Commands:
  branches   Show current branch points
  current    Display the current revision for each...
  downgrade  Revert to a previous version
  edit       Edit a revision file
  heads      Show current available heads in the script...
  history    List changeset scripts in chronological...
  init       Creates a new migration repository.
  merge      Merge two revisions together, creating a new...
  migrate    Autogenerate a new revision file (Alias for...
  revision   Create a new revision file.
  show       Show the revision denoted by the given...
  stamp      'stamp' the revision table with the given...
  upgrade    Upgrade to a later version

Creating the Migration Repository

The next step in the process is to create a database migration repository. This is a directory where all the migration scripts are going to be stored. The migration repository should be considered part of your source code, and should be added to your source control.

To create the repository you can use the init command:

(venv) $ flask db init
  Creating directory /Users/mgrinberg/microblog/migrations ... done
  Creating directory /Users/mgrinberg/microblog/migrations/versions ... done
  Generating /Users/mgrinberg/microblog/migrations/script.py.mako ... done
  Generating /Users/mgrinberg/microblog/migrations/env.py ... done
  Generating /Users/mgrinberg/microblog/migrations/README ... done
  Generating /Users/mgrinberg/microblog/migrations/alembic.ini ... done
  Please edit configuration/connection/logging settings in 'migrations/alembic.ini' before proceeding.

This command adds a migrations directory in the root of your project. Add the directory and all of its contents to source control.

Creating the Initial Migration

If you look at the documentation, the next step in the process is to create an initial migration for your project, using the migrate command:

(venv) $ flask db migrate

This step does not actually work if you have an existing project that has a populated database:

(venv) $ flask db migrate
INFO  [alembic.runtime.migration] Context impl SQLiteImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.env] No changes in schema detected.

The "No changes in schema detected" message indicates that there is no need to migrate the database. But how can that be if we are just getting started with migrations in this project?

To understand the problem you have to think about how Alembic (the migration engine used by Flask-Migrate) generates database migrations. The contents of a migration are obtained by running a comparison between the current model definitions and the current schema of your database.

In a new project the models are going to be compared against a brand new (empty) database, so the migration script will include all the model definitions. For an existing project the database is up to date and in sync with the models, so there are no differences, and thus, Alembic thinks that there is nothing to put in the database migration script.

The trick to generate this initial migration for a project that uses an up to date database is to temporarily switch to an empty database, just for the flask db migrate command. In my projects I use a DATABASE_URL environment variable to configure the location of the database. This is a good practice that I recommend you follow as well, because with this variable it is easy to redirect to another database.

In general you can replace your real database with an in-memory SQLite database, just for the migrate command:

(venv) $ DATABASE_URL=sqlite:/// flask db migrate
INFO  [alembic.runtime.migration] Context impl SQLiteImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.autogenerate.compare] Detected added table 'user'
INFO  [alembic.autogenerate.compare] Detected added index 'ix_user_email' on '['email']'
INFO  [alembic.autogenerate.compare] Detected added index 'ix_user_token' on '['token']'
INFO  [alembic.autogenerate.compare] Detected added index 'ix_user_username' on '['username']'
INFO  [alembic.autogenerate.compare] Detected added table 'followers'
INFO  [alembic.autogenerate.compare] Detected added table 'message'
INFO  [alembic.autogenerate.compare] Detected added index 'ix_message_timestamp' on '['timestamp']'
INFO  [alembic.autogenerate.compare] Detected added table 'notification'
INFO  [alembic.autogenerate.compare] Detected added index 'ix_notification_name' on '['name']'
INFO  [alembic.autogenerate.compare] Detected added index 'ix_notification_timestamp' on '['timestamp']'
INFO  [alembic.autogenerate.compare] Detected added table 'post'
INFO  [alembic.autogenerate.compare] Detected added index 'ix_post_timestamp' on '['timestamp']'
INFO  [alembic.autogenerate.compare] Detected added table 'task'
INFO  [alembic.autogenerate.compare] Detected added index 'ix_task_name' on '['name']'
  Generating migrations/versions/97180cecd04b_.py ... done

Using an in-memory SQLite database is very convenient, but will only work if you are in one of the two following situations:

  • You are also using SQLite for your real database
  • You are using another database engine (MySQL, Postgres, etc.), but your models are written generically, without using any constructs that are specific to your chosen engine.

If you cannot use an in-memory SQLite database, then go to your database server and create a new database there, and then set DATABASE_URL to it for the migrate command. Once you have generated the first migration you can delete the empty database from your server.

The next step according to the documentation is to run the upgrade command, which executes the migration script and applies the changes in it to your database. Obviously this is also going to fail, because the database does not need updating. Instead you have to tell Flask-Migrate and Alembic that the database is up to date. You can do this with the stamp command"

(venv) $ flask db stamp head

This command will add a alembic_version table to your database, and will store the initial migration generated above as the current migration. The head alias always points to the most recent migration, which in our case is the first and only one currently in the repository.

As I mentioned above, the contents of the migrations directory needs to be under source control. Remember to add the new migration script, which you will find in the migrations/versions directory.

Updating Additional Databases

You will likely have multiple databases for your application, at least one for development and one for production. You may be part of a team, so each team member will have their own development database in their local systems. Once you created the initial migration and added it to source control, you can just "stamp" all the additional databases. For each additional database make sure the code is refreshed from source control to include the migration repository and then just run the stamp command from above to "fix" the database:

(venv) $ flask db stamp head

Migration Workflow

From this point on you have a project that is fully enabled to use database migrations. The normal migration process goes as follows:

  • You will make some changes to your models in your Python source code.
  • You will then run flask db migrate to generate a new database migration for these changes.
  • You will finally apply the changes to the database by running flask db upgrade.

This cycle repeats every time new changes to the database schema are needed.

Conclusion

I hope this short tutorial clarifies how to add database migrations to your existing project. If you want to avoid the extra trouble required to get that first migration generated, in your next project consider adding Flask-Migrate from the start. It may seem unnecessary at first, but you'll eventually want to have it.

Become a Patron!

Hello, and thank you for visiting my blog! If you enjoyed this article, please consider supporting my work on this blog on Patreon!

71 comments
  • #26 GW said

    I am on Windows, when I try to run 'flask db migrate' it says: Error: Failed to find Flask application or factory in module "run". Use "FLASK_APP=run:name to specify one.

    I have set FLASK_APP with 'set FLASK_APP=run.py'.
    File structure is like this:
    ├───run.py
    ├───app_name
    │ ├───init.py

    Any idea how to fix this?

  • #27 Miguel Grinberg said

    @GW: what do you have in run.py? The error suggests you don't have an application instance, or if you do, then you did not name it app.

  • #28 Elisabeth said

    Dear Miguel,
    Thank you for all this. I've been using Flask, Flask-Sqlalchemy, and Flask-Migrate on an app and a Postgres database hosted on Heroku and it all works great. For scaling reasons, I'm considering adding databases. First to split the models into 2 databases, as there are two types of data in the current database (user data and a large amount of separate data that users sometimes read, but never write to), and secondly to add follower database(s) on Heroku. I'm hoping that will make the app faster as the database is the main bottleneck.
    I understand flask-migrate can be used with multiple databases and the binds feature of Flask-SQLAlchemy and by setting up the repository with
    flask db init --multidb
    My question is can I do that after the fact ? If so, what is the best way to do that after the fact, as I already have a large number of migrations to that original database. Of course I can't start the databases from scratch, as I can't lose user data. Any help would be appreciated, thanks

  • #29 Miguel Grinberg said

    @Elisabeth: Alembic does not provide an option to switch configurations mid-project, so the only option is to create a new migration repository with the multidb option, and then manually import your migration scripts from the old place to the new place, making the necessary adjustments. I recommend that you create a test project with the database configuration you intend to work with and then create a couple test migrations so that you see what is the structure of the migration script when using multiple databases. Once you see how the migration script is supposed to look you can take the old migrations and adapt them.

  • #30 Harun Gachanja said

    Am not using ORMs. Am using using raw sql queries to create and destroy tables. Kindly advice on how I can go ahead integrating flask migrate into my project

  • #31 Jerry Kauffman said

    Thank you for putting this together. I had developed a MySQL database prior to signing up for your Mega Tutorial and was stuck. But this did it. Following your examples in chapters 1-4 I was able to create classes in models.py for each of my existing tables (10 with data, 15 more without data - I deleted them so I could ignore them until needed). I am using PyCharm on Win 10. With your help, I am learning a lot. Again Thanks!

  • #32 Miguel Grinberg said

    @Harun: Flask-Migrate works with Flask-SQLAlchemy, so you cannot use it without it. Consider using Alembic directly if you have other needs.

  • #33 Harun Gachanja said

    Thank you for putting this together. I'm getting this error while trying to execute the "flask db upgrade" command. Kindly advise on how to fix this.

    psycopg2.errors.DependentObjectsStillExist: cannot drop table courses because other objects depend on it
    DETAIL: constraint apply_course_course_fkey on table apply_course depends on table courses
    constraint checklist_course_fkey on table checklist depends on table courses
    HINT: Use DROP ... CASCADE to drop the dependent objects too.

  • #34 Miguel Grinberg said

    @Harun: the error is clear, you are trying to drop a table that has foreign keys pointing to it. Before you can drop the table you have to remove all those foreign keys in other tables.

  • #35 Harun Gachanja said

    Sorry to bother you, Miguel. How do I remove these keys before dropping the table?
    This is how am creating my models and their relationships.

    class Department(db.Model):
    """ Department Model for storing department related details """
    tablename = "departments"

    department_id = db.Column(
        db.Integer, autoincrement=True)
    department_name = db.Column(
        db.String(120), primary_key=True, unique=True, nullable=False)
    created_on = db.Column(db.DateTime, nullable=False,
                           default=datetime.utcnow)
    courses = db.relationship(
        'Course', backref='department_obj', cascade="all, delete",
        passive_deletes=True)
    

    class Course(db.Model):
    """ Course Model for storing course related details """
    tablename = "courses"

    course_id = db.Column(
        db.Integer, autoincrement=True)
    course_name = db.Column(
        db.String(120), primary_key=True, unique=True, nullable=False)
    department = db.Column(db.String(120), db.ForeignKey(
        'departments.department_name', ondelete='CASCADE'), nullable=False)
    created_on = db.Column(db.DateTime, nullable=False,
                           default=datetime.utcnow)
    courses = db.relationship(
        'ApplyCourse', backref='course_obj', cascade="all, delete",
        passive_deletes=True)
    
  • #36 Miguel Grinberg said

    @Harun: you need to look through all your models and remove any foreign keys that point to courses. Once you remove all those foreign keys you should be able to delete the courses table.

  • #37 Harun Gachanja said

    Thank you for your response. Just curious, the Course Model also has some relations with other tables. If I remove the FOREIGN KEYS how do I maintain the table relations? Am working with about 20 tables. And some tables have more than just one relationship.

  • #38 Miguel Grinberg said

    @Harun: You are complicating this too much. It's really simple. You want to delete your courses table. Before you can do that, you have to remove all foreign keys that point to the courses table in other tables. Think about it, it doesn't make sense to have a foreign key that points to a table that was deleted, right? Other relationships are fine, they don't need to be touched.

  • #39 Jonas Bergroth said

    Hi Miguel,
    I have a Flask app using Blueprints and ran the init from the main folder where I have the run.py file in, using a FLASK_APP link to the folder i have my init.py file in. The problem is that i get "No changes in schema detected." even thought I use "SQLALCHEMY_DATABASE_URL=sqlite:/// flask db migrate". Do you know what the problem can be, should I maybe run migrate from the __init__py folder?

    Many thanks,
    Jonas

  • #40 Miguel Grinberg said

    @Jonas: Have you imported your models? SQLAlchemy register models only when they are imported, if you do not import them it is as if they don't exist in terms of Flask-Migrate and Alembic.

  • #41 Aj said

    Do we follow the same steps for adding Flask-Migrate to an existing Heroku PostgreSQL database in production?

  • #42 Miguel Grinberg said

    @Aj: Add your migration repository on a development database, why mess with the production database before you are sure you got everything right?

  • #43 Tim said

    Hi Miguel. Thank you for all the hard work you do to educate people like me :)

    I am a bit confused re co-existence of dev, pre-prod and prod databases. So, let's say I initialize Migration in my dev environment using existing Models and empty DB trick. I then stamp it, and finally push my changes. Pre-prod environment will download the updated source code, but tests will fail.

    Does it mean flask db upgrade will have to be included into CI/CD pipeline?

    Is this all (flask db upgrade) that's needed to make sure DB in pre-prod is aligned with latest changes in the models?

    Does it uses Alembic table to track what migrations have been applied and as such it's absence in pre-prod environment will signal Migration that something HAS to be done once flask db upgrade is executed

    Finally, I've noticed that Migrate is not compatible with SQLAlchemy's db.create_all() - I had this line in my Flask factory function and it kicks off way before migration can take control of App context. As such, everytime I was executing flask db migrate one empty DB in memory it was telling me that everything is up to date, because SQLAlchemy was creating all tables before migration (so, DB wasn't empty). Does it mean that db.create_all() cannot be used with Migration ? If so, does it mean that DB creation MUST not be controlled by the app, but instead via Migration shell

    Many thanks!

  • #44 Miguel Grinberg said

    @Tim: Here are some answers:

    "Pre-prod environment will download the updated source code, but tests will fail."

    Why will tests fail? Your tests should run on their own database, which is created and discarded for each run. You do not want to reuse an existing database for tests, since its initial state can affect the result of the tests, as you are indicating.

    "Does it mean flask db upgrade will have to be included into CI/CD pipeline?"

    Your test database should be brand new. At the start of each test you should wipe your database clean, then run a flask db upgrade, or a db.create_all(). The latter will be faster, but less like your real environments.

    "Is this all (flask db upgrade) that's needed to make sure DB in pre-prod is aligned with latest changes in the models?"

    When you introduce migrations into an existing project you need to stamp all your databases, dev, prod and any others than you have. After that initial set up new migrations should work fine with the normal workflow.

    "Does it uses Alembic table to track what migrations have been applied and as such it's absence in pre-prod environment will signal Migration that something HAS to be done once flask db upgrade is executed"

    I don't understand this. The "something" that you mention is that stamp command, which you have to apply to all your databases, not just dev.

    "Does it mean that db.create_all() cannot be used with Migration ?"

    The db.create_all() function competes with Alembic and Flask-Migrate. You have to choose one of the two to use. For tests you can go with the former, since it is a throwaway database. For your dev/prod/etc environments you should only do upgrades through the flask db upgrade command.

  • #45 Tim said

    @Miguel thank you! It does make sense. May I just confirm one thing. When you say "you have to stamp all DBs". Does it mean - stamp all environments, then commit/merge to make sure stamp changes are all merged into master? And only after this point I can tell that Migrate tracks the state of DB models in my whole project.

  • #46 Miguel Grinberg said

    @Tim: stamping is about the databases, not your code or your environment. It simply sets that Alembic revision that reflects the state of the database. So there's nothing to commit or merge, just run flask db stamp head on all your databases so that they are marked as updated and can execute future migrations going forward.

  • #47 Tim said

    Perfect! Thank you.
    So, stamping is about creating Alembic table in the DB with a snapshot of current version.
    Many many many thanks! I hope you'll carry on as your Flask tutorials are one of the best.

  • #48 Suri said

    Hello Miguel,

    I have used SQL Alchemy+alembic, i developed an application, it running fine with good amount of Data, Now i have to add the new tables to my DB without disturbing production DB.
    So when i executed flask db migrate on dev setup i get bellow Error, Please help in resolving this issue.

    ERROR [flask_migrate] Error: Target database is not up to date.

  • #49 Miguel Grinberg said

    @Suri: the error means that your database has not been upgraded to the latest migration, or that you have extra migrations that are not supposed to be in your repository. Upgrade your database, or delete the unused migrations to remove this error.

  • #50 Graeme said

    Hi Miguel, I've been following your guide to set up migrations in my pre-existing database, however when i attempt to migrate for the first time, the version that is auto-generated tries to drop all of my existing tables, as if it cant see the models that i have defined. Any idea on how to fix this? Im using a PostgreSQL database, and im using a macbook.

    def upgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.drop_table('volume')
    op.drop_table('sig')
    op.drop_table('venue')
    op.drop_table('paper')
    op.drop_table('extraction')
    # ### end Alembic commands ###

    def downgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.create_table('extraction',
    sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False),
    sa.Column('paper_id', sa.VARCHAR(), autoincrement=False, nullable=True),
    sa.Column('full_paper_text', sa.TEXT(), autoincrement=False, nullable=True),
    sa.Column('introduction_text', sa.TEXT(), autoincrement=False, nullable=True),
    sa.Column('method_text', sa.TEXT(), autoincrement=False, nullable=True),
    sa.Column('results_text', sa.TEXT(), autoincrement=False, nullable=True),
    sa.Column('references_text', sa.TEXT(), autoincrement=False, nullable=True),
    sa.ForeignKeyConstraint(['paper_id'], ['paper.id'], name='extraction_paper_id_fkey'),
    sa.PrimaryKeyConstraint('id', name='extraction_pkey')
    )
    op.create_table('paper',
    sa.Column('id', sa.VARCHAR(), autoincrement=False, nullable=False),
    sa.Column('title', sa.VARCHAR(), autoincrement=False, nullable=True),
    sa.Column('volume', sa.VARCHAR(), autoincrement=False, nullable=True),
    sa.Column('abstract', sa.VARCHAR(), autoincrement=False, nullable=True),
    sa.Column('author', postgresql.ARRAY(sa.VARCHAR()), autoincrement=False, nullable=True),
    sa.Column('url', sa.VARCHAR(), autoincrement=False, nullable=True),
    sa.Column('pdf', sa.VARCHAR(), autoincrement=False, nullable=True),
    sa.ForeignKeyConstraint(['volume'], ['volume.id'], name='paper_volume_fkey'),
    sa.PrimaryKeyConstraint('id', name='paper_pkey')
    )
    op.create_table('venue',
    sa.Column('id', sa.VARCHAR(), autoincrement=False, nullable=False),
    sa.Column('name', sa.VARCHAR(), autoincrement=False, nullable=True),
    sa.Column('years', postgresql.ARRAY(sa.INTEGER()), autoincrement=False, nullable=True),
    sa.Column('volumes', postgresql.ARRAY(sa.VARCHAR()), autoincrement=False, nullable=True),
    sa.PrimaryKeyConstraint('id', name='venue_pkey')
    )
    op.create_table('sig',
    sa.Column('id', sa.VARCHAR(), autoincrement=False, nullable=False),
    sa.Column('title', sa.VARCHAR(), autoincrement=False, nullable=True),
    sa.Column('url', sa.VARCHAR(), autoincrement=False, nullable=True),
    sa.PrimaryKeyConstraint('id', name='sig_pkey')
    )
    op.create_table('volume',
    sa.Column('id', sa.VARCHAR(), autoincrement=False, nullable=False),
    sa.Column('title', sa.VARCHAR(), autoincrement=False, nullable=True),
    sa.Column('venues', postgresql.ARRAY(sa.VARCHAR()), autoincrement=False, nullable=True),
    sa.Column('sigs', postgresql.ARRAY(sa.VARCHAR()), autoincrement=False, nullable=True),
    sa.Column('year', sa.INTEGER(), autoincrement=False, nullable=True),
    sa.Column('url', sa.VARCHAR(), autoincrement=False, nullable=True),
    sa.Column('pdf', sa.VARCHAR(), autoincrement=False, nullable=True),
    sa.PrimaryKeyConstraint('id', name='volume_pkey')
    )
    # ### end Alembic commands ###

Leave a Comment