Database Migrations with Alembic and SQLAlchemy
07-11-2021
Overview
Alembic is a database migration tool built by the authors of SQLAlchemy. Alembic provides an intuitive interface to create migration scripts used to upgrade or downgrade the state of your database schemas.
Migration Environment
To start using Alembic, you need to initialise a migration environment. This migration environment is created once and maintained alongside the source code.
To install Alembic:
pip install alembic
To create a migration environment:
alembic init <environment_name> # 'environment_name' is usually 'alembic'
This will create the following files and folders:
.
├── alembic.ini
├── /alembic
│ ├── env.py
│ ├── README
│ └── script.py.mako
└── /versions
└── ...
alembic.ini
- a file thatalembic
looks for when a script is invoked (keep in the same directory where migration scripts will be invoked)alembic/
- a directory that contains all necessary informationenv.py
- a script which is run whenever a migration is invoked and contains the configuration neccessary to create a migration engine with SQLAlchemyREADME
script.py.mako
- aMako
template used to generate migration scripts
versions/
- a directory which holds the individual reversion scripts (ie theupgrade()
anddowngrade()
functions)
To view the revision history:
alembic history # which returns nothing because we haven't made a revision yet
Create Table Model
We will use SQLAlchemy to create a basic Table model (in this case a customer
table).
from sqlalchemy import Column, Integer, MetaData, String, Table
base_meta_data = MetaData()
table = Table(
"customer",
base_meta_data,
Column("id", Integer, autoincrement=True, primary_key=True),
Column("first_name", String(), nullable=False),
Column("last_name", String(), nullable=False),
Column("address", String(), nullable=False),
Column("city", String(), nullable=False),
)
Configure Database Connection URL
Within the alembic.ini
file, a sqlalchemy.url
attribute is defined. This attribute must be overriden with an appropriate database connection string. In our case, we will use the sqlite3
library to setup an in-memory database on top of our local file system.
sqlalchemy.url = sqlite:///application.db
If you don't want to hard-code a sqlalchemy.url
attribute into the alembic.ini
configuration file, you can override this configuration value within env.py
. For example:
from config import DATABASE_URL
url = DATABASE_URL
config.set_main_option("sqlalchemy.url", url)
Furthermore, there may be connection string problems due to characters not escaping correctly, try this:
url = str(url).replace("%", "%%")
Configure Autogeneration
Alembic provides the ability to automatically compare snapshots of Table objects across each revision and determine relevant migrations scripts based on these differences. For autogenerated scripts to work, we must modify the env.py
module so that it gets access to the table metadata (this metadata object contains Table objects that define the database). We pass this metadata object to the context via the target_metadata
variable within env.py
.
from models import base_meta_data
target_metadata = base_meta_data
Create First Migration Script
To autogenerate a migration script:
alembic revision --autogenerate -m "Add customer table"
Which returns the following output:
INFO [alembic.runtime.migration] Context impl SQLiteImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
INFO [alembic.autogenerate.compare] Detected added table 'customer'
Generating /project/alembic/versions/b4352a83e78a_add_customer_table.py ... done
And creates the following migration script:
"""Add customer table
Revision ID: b4352a83e78a
Revises:
Create Date: 2021-11-06 16:46:02.203960
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'b4352a83e78a'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('customer',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('first_name', sa.String(), nullable=False),
sa.Column('last_name', sa.String(), nullable=False),
sa.Column('address', sa.String(), nullable=False),
sa.Column('city', sa.String(), nullable=False),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('customer')
# ### end Alembic commands ###
As you can see, Alembic has autogenerated the required revision scripts and assigned this script with the revision identifier b4352a83e78a
. On upgrade, a customer
table is created with three columns (id
, first_name
, last_name
). On downgrade, the table is dropped.
Creating a revision script doesn't run a migration. You must first make sure to check that the scripts conform to your expectations. Following this, migrations must be run with the upgrade
and downgrade
alembic commands, as explained below.
Run First Migration
To run an upgrade revision:
alembic upgrade head
Which returns the following:
INFO [alembic.runtime.migration] Context impl SQLiteImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
INFO [alembic.runtime.migration] Running upgrade -> b4352a83e78a, Add customer table
We can then inspect that these changes have been reflected in our database. We use the sqlite3
command-line interface tool to inspect the database:
sqlite3 application.db
SQLite version 3.36.0 2021-06-18 18:58:49
Enter ".help" for usage hints.
sqlite> .tables
alembic_version customer
sqlite> .schema customer
CREATE TABLE customer (
id INTEGER NOT NULL,
first_name VARCHAR NOT NULL,
last_name VARCHAR NOT NULL,
address VARCHAR NOT NULL,
city VARCHAR NOT NULL,
PRIMARY KEY (id)
);
As you can see, our SQLite
database has been updated with the customer
table.
To view Alembic history:
alembic history
<base> -> b4352a83e78a (head), Add customer table
SQLite is a lightweight, fast, self-contained disk-based database that runs on top your file system. It is useful for prototyping and testing.
Installation:
- On macOS,
SQLite
should be installed by default - On Windows,
SQLite
can be downloaded from the SQLite website. To install: extract and add the resulting executables to your path environement variable.
Basic usage:
# run sqlite
sqlite3
# display all databases in current connection
sqlite> .database
# open database file
sqlite> .open path/to/database.db
# add additional database to current connection
sqlite> ATTACH DATABASE "path/to/database.db" AS db_name;
# view table schema
sqlite> .schema <table_name>
# exit sqlite3
sqlite>.exit
Upgrade Commands
To upgrade to the most recent revision:
alembic upgrade head
To upgrade the current state by a certain number of revisions:
alembic upgrade +1
To upgrade to a particular revision:
alembic upgrade rl6192cd49d6
Downgrade Commands
To downgrade to the earliest revision:
alembic downgrade base
To downgrade the current state by a certain number of revisions:
alembic downgrade -1
To downgrade to a particular revision:
alembic downgrade rl6192cd49d6
Update Table Model
Now that we have run our first migration and Alembic is pointing to the latest revision, we will update our table to include an age attribute.
table = Table(
"customer",
base_meta_data,
Column("id", Integer, autoincrement=True, primary_key=True),
Column("first_name", String(), nullable=False),
Column("last_name", String(), nullable=False),
Column("address", String(), nullable=False),
Column("city", String(), nullable=False),
Column("age", Integer, nullable=False),
)
Create Second Migration Script
Given this update to our code, we would like these changes to be reflected in our database.
To autogenerate a migration script:
alembic revision --autogenerate -m "Add age column"
Which returns the following output:
INFO [alembic.runtime.migration] Context impl SQLiteImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
INFO [alembic.autogenerate.compare] Detected added column 'customer.age'
Generating /project/alembic/versions/c2e7eea3f80e_add_age_column.py ... done
And creates the following migration script:
"""Add age column
Revision ID: c2e7eea3f80e
Revises: b4352a83e78a
Create Date: 2021-11-06 17:28:01.437872
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'c2e7eea3f80e'
down_revision = 'b4352a83e78a'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('customer', sa.Column('age', sa.Integer(), nullable=False))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('customer', 'age')
# ### end Alembic commands ###
As you can see, Alembic has autogenerated the required revision scripts and assigned this script with the revision identifier c2e7eea3f80e
. On upgrade, the age
column is added. On downgrade, the age
column is dropped.
Run Second Migration
To run an upgrade revision:
alembic upgrade head
Which returns the following output:
INFO [alembic.runtime.migration] Context impl SQLiteImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
INFO [alembic.runtime.migration] Running upgrade b4352a83e78a -> c2e7eea3f80e, Add age column
We can then inspect that these changes have been reflected in our database. Again, we use the sqlite3
command-line interface tool to inspect the database:
> sqlite3 application.db
SQLite version 3.36.0 2021-06-18 18:58:49
Enter ".help" for usage hints.
sqlite> .tables
alembic_version customer
sqlite> .schema customer
CREATE TABLE customer (
id INTEGER NOT NULL,
first_name VARCHAR NOT NULL,
last_name VARCHAR NOT NULL,
address VARCHAR NOT NULL,
city VARCHAR NOT NULL,
age INTEGER NOT NULL,
PRIMARY KEY (id)
);
As you can see, our SQLite
database has been updated with the age
column in the customer
table.
To view Alembic history:
alembic history
b4352a83e78a -> c2e7eea3f80e (head), Add age column
<base> -> b4352a83e78a, Add customer table