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 alembicTo 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 thatalembiclooks 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 SQLAlchemyREADMEscript.py.mako- aMakotemplate 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 yetCreate 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.dbIf 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_dataCreate 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 ... doneAnd 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 headWhich 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 tableWe 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 tableSQLite 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,
SQLiteshould be installed by default - On Windows,
SQLitecan 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>.exitUpgrade Commands
To upgrade to the most recent revision:
alembic upgrade headTo upgrade the current state by a certain number of revisions:
alembic upgrade +1To upgrade to a particular revision:
alembic upgrade rl6192cd49d6Downgrade Commands
To downgrade to the earliest revision:
alembic downgrade baseTo downgrade the current state by a certain number of revisions:
alembic downgrade -1To downgrade to a particular revision:
alembic downgrade rl6192cd49d6Update 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 ... doneAnd 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 headWhich 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 columnWe 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