Python Friday #87: Handling Pre-Existing Tables With Alembic and SQLAlchemy

SQLAlchemy and Alembic are a great combination to keep your database in sync with your application as we saw in the last post. But what do you do when you already created some tables with SQLAlchemy that now contain data we cannot lose? Let us look how we can fix this without much extra work.

This post is part of my journey to learn Python. You can find the other parts of this series here. You find the code for this post in my PythonFriday repository on GitHub.

 

How SQLAlchemy and Alembic work together

If we use SQLAlchemy and Alembic from the start, all our tables are created with migrations. In this case a new developer can take the code, create an empty database and run the migrations to get the same set of tables as everyone else. If this matches your situation, you do not have a problem we try to solve here.

 

The problem

If you created tables in the past from the SQLAlchemy metadata, you have no migrations to create those tables. You can start to use Alembic, but it will only work with a pre-existing database containing those tables. New developers cannot recreate the database and need a copy of an existing one. Sooner than later this will be a big problem. Therefore, let us fix this now before you start making big changes with Alembic.

 

A solution

There are different solutions to this problem, but I like one that shows the clear intention of the project to start with a new concept and makes this transition explicit. We create a migration with all existing tables and a helper, that only creates the tables when they do not exist yet. In this case the new developer can start with running the migrations and get the exactly same structure as the team members who have databases since the start of the project.

The only pre-requisite is that everyone who wants to keep their database is on the newest state. You either upgrade to the same level now or create an empty database when the migrations are in place. That applies to the production database as well. If in doubt, start with the production database as your base level.

 

Create the helper

Inside the alembic folder we create a alembic_helpers.py file with this content:

This helper uses the Reflection.Inspector in the SQLAlchemy Engine to check if a table exists or not. We invert the answer to get simpler code inside our migrations since we only want to create the table when it does not exist.

 

Remove existing migrations

You need to get rid of all the existing migrations in alembic/versions. Otherwise Alembic sees that the database is not on the latest version and it will not work. Removing the migrations is not a problem, then Alembic will create all the tables you currently have as one big migration script.

Delete the alembic_version table if it exists or at least delete the row it may contain. Otherwise the checks of Alembic will also prevent you from creating a new migration script for your tables.

 

Get an empty database

We need an empty database so that Alembic can create a database migration for our “old” tables. This can be as simple as changing the connection string to a non-existing SQLite file in alembic.ini:

For other database systems you may need to create an empty database first and then change the connection string.

 

Auto-generate the migration

We now can tell Alembic to create a migration for the current state of our application.

INFO [alembic.runtime.migration] Context impl SQLiteImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
INFO [alembic.autogenerate.compare] Detected added table ‘Employee’
INFO [alembic.autogenerate.compare] Detected added table ‘Publisher’
INFO [alembic.autogenerate.compare] Detected added table ‘Book’
INFO [alembic.autogenerate.compare] Detected added table ‘BookAuthor’
INFO [alembic.autogenerate.compare] Detected added table ‘BookDetail’
Generating D:\Python\ORM\alembic\versions\347126fc065d_initial_tables.py … done

This creates us the file 347126fc065d_initial_tables.py with CREATE Table statements for all our tables:

 

Use the helper

We need to include the location of our helper in the Python path in all migration files that need them. When this is done, we can use it to only create missing tables:

 

Back to our database

Revert the changes to alembic.ini to use the connection string that points to your database that contains the tables and your data.

 

Run the migration

We can now run the migration and Alembic will register the migration but not create the tables:

INFO [alembic.runtime.migration] Context impl SQLiteImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
INFO [alembic.runtime.migration] Running upgrade -> 347126fc065d, Initial tables

INFO [alembic.runtime.migration] Context impl SQLiteImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
347126fc065d (head)

If you run the migration in an empty database, it will create the tables. In either way, you now have a database with a registered migration revision and your tables. From here, everyone in the project can apply future changes to the database with Alembic.

 

Next

With this solution in place, you can change your application in whatever way you seam fit. SQLAlchemy and Alembic will apply your changes from your Python code to the database. Next week we look how we can create an index and use constraints to enforce data integrity.

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.