Skip to content

A guide to creating and using the Yoyo migrations tool with Postgres

License

Notifications You must be signed in to change notification settings

medwig/yoyo-migration-tutorial

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

A guide walking through creating a Postgres database and examples of using Yoyo migrations to create, update, and rollback tables.

Install dependencies:

$ pip install -r requirements.txt

Create Postgres Database

Install Postgres:

$ sudo apt-get update

$ sudo apt-get install postgresql

Create test database and set password:

$ sudo -u postgres psql -c "ALTER USER postgres PASSWORD 'postgres';"

$ sudo -u postgres psql -c "CREATE DATABASE testdb;"

Start postgres server and connect to it:

$ sudo service postgresql start

Setup connection config:

$ echo "[local]\n\ 
host=127.0.0.1\n\ 
user=postgres\n\ 
dbname=testdb\n\ 
password=postgres\n\ 
port=5432" >> ~/.pg_service.conf

Connect to Postgres local server:

$ psql service=local

Confirm database is empty:

$ testdb=# \d

Did not find any relations.

Migration 1: Create Table

Make the create-table migration script with yoyo:

$ yoyo new ./migrations -m "CREATE TABLE users (id INT, name VARCHAR(20), PRIMARY KEY (id)"

# migrations/20190604_01_Vra0v-create-users-table.py

"""
Create users table
"""

from yoyo import step

__depends__ = {}

steps = [
    step("CREATE TABLE users (id INT, name VARCHAR(20), PRIMARY KEY (id))")
]

Created file ./migrations/20190604_01_BnWl8-create-table-users-id-int-name-varchar-20-primary-key-id.py
Save migration configuration to yoyo.ini?
This is saved in plain text and contains your database password.

Answering 'y' means you do not have to specify the migration source or database connection for future runs [yn]: y

Apply migration:

cli

$ yoyo apply --database postgresql://postgres:postgres@localhost/testdb ./migrations

python

# scripts/apply.py                       
from yoyo import read_migrations, get_backend

backend = get_backend('postgres://postgres:postgres@localhost/testdb')
migrations = read_migrations('./migrations')
backend.apply_migrations(backend.to_apply(migrations))

The to_apply method insures we don't try to re-run steps that have already ran.

$ python scripts/apply.py

Confirm table creation:

$ psql service=local

$ testdb=# \d

              List of relations
 Schema |      Name       | Type  |  Owner   
--------+-----------------+-------+----------
 public | _yoyo_log       | table | postgres
 public | _yoyo_migration | table | postgres
 public | _yoyo_version   | table | postgres
 public | users           | table | postgres
 public | yoyo_lock       | table | postgres
(5 rows)

testdb=# select * from users;
 id | name 
----+------
(0 rows)
 

Migration 2: Add Column to Table

Create migration file for new column:

$ yoyo new ./migrations -m "Add age column to users table"

# migrations/20190604_02_ORgoL-add-age-column-to-users-table.py

"""
Add age column to users table
"""

from yoyo import step

__depends__ = {'20190604_01_Vra0v-create-users-table'}

steps = [
    step("ALTER TABLE users ADD COLUMN age INT")
]

Apply migration:

$ yoyo apply --database postgresql://postgres:postgres@localhost/testdb ./migrations

Confirm migration worked:

$ testdb=# \d+ users
                                          Table "public.users"
 Column |         Type          | Collation | Nullable | Default | Storage  | Stats target | Description 
--------+-----------------------+-----------+----------+---------+----------+--------------+-------------
 id     | integer               |           | not null |         | plain    |              | 
 name   | character varying(20) |           |          |         | extended |              | 
 age    | integer               |           |          |         | plain    |              | 

Migration 3: Rollback

To roll back migrations, the rollback step must be defined explicitly.

Edit the two migration scripts to add a rollback command to each step.

# migrations/20190604_01_Vra0v-create-users-table.py

"""
Create users table
"""

from yoyo import step

__depends__ = {}

steps = [
    step("CREATE TABLE users (id INT, name VARCHAR(20), PRIMARY KEY (id))",
         "DROP TABLE users"),  # rollback command
]
# migrations/20190604_02_ORgoL-add-age-column-to-users-table.py 

"""
Add age column to users table
"""

from yoyo import step

__depends__ = {'20190604_01_Vra0v-create-users-table'}

steps = [
    step("ALTER TABLE users ADD COLUMN age INT",
         "ALTER TABLE users DROP COLUMN age"),  # rollback command
]       

Run rollback of the add column step:

cli

$ yoyo rollback --database postgresql://postgres:postgres@localhost/testdb ./migrations
[20190604_02_ORgoL-add-age-column-to-users-table]
Shall I rollback this migration? [Ynvdaqjk?]: Y

[20190604_01_Vra0v-create-users-table]
Shall I rollback this migration? [Ynvdaqjk?]: n

Selected 1 migration:
  [20190604_02_ORgoL-add-age-column-to-users-table]
Rollback this migration to postgresql://postgres:postgres@localhost/testdb [Yn]: Y

python

# scripts/rollback.py                                         

from yoyo import read_migrations, get_backend

backend = get_backend('postgres://postgres:postgres@localhost/testdb')
migrations = read_migrations('./migrations')
sorted_migrations = sorted(migrations, key=lambda x: x.id, reverse=True)
backend.rollback_migrations(sorted_migrations)

$ python scripts/rollback.py

If the sort step is not used it will throw an exception when the rollback steps are executed in the wrong order - from oldest to most recent, which will delete the users table and then attempting to drop the age column. Seems like an odd bug in yoyo?

psycopg2.errors.UndefinedTable: relation "users" does not exist

The migrations are first reverse sorted by id (equivalent to date:step_num) to get around this issue, then the rollback runs fine.

Confirm that the column has been removed:

$ testdb=# \d+ users;

                                          Table "public.users"
 Column |         Type          | Collation | Nullable | Default | Storage  | Stats target | Description 
--------+-----------------------+-----------+----------+---------+----------+--------------+-------------
 id     | integer               |           | not null |         | plain    |              | 
 name   | character varying(20) |           |          |         | extended |              | 

You can also check the _yoyo_migration table to confirm which migrations have been applied to the database:

testdb=# select * from _yoyo_migration;

                          migration_hash                          |             migration_id             |       applied_at_utc       
------------------------------------------------------------------+--------------------------------------+----------------------------
 fe8fc11fadfb920b12a7f3eb691a376c7b4453321a48f409987e5b5db5eaca44 | 20190604_01_Vra0v-create-users-table | 2019-06-06 16:33:01.881926
(1 row)

Rollback a specific migation:

cli

yoyo rollback --database postgresql://postgres:postgres@localhost/testdb ./migrations -r 20190604_01_Vra0v-create-users-table

If the specified migration has dependent downstream migrations yoyo is smart enough to rollback those as well:

Selected 2 migrations:
  [20190604_02_ORgoL-add-age-column-to-users-table]
  [20190604_01_Vra0v-create-users-table]
Rollback these 2 migrations to postgresql://postgres:postgres@localhost/testdb [Yn]: Y

python

cat scripts/rollback_to_target.py
from yoyo import read_migrations, get_backend, descendants

backend = get_backend('postgres://postgres:postgres@localhost/testdb')
migrations = read_migrations('./migrations')

revision = '20190604_01_Vra0v-create-users-table'

targets = [m for m in migrations if revision in m.id]
if len(targets) == 0:
    raise InvalidArgument("'{}' doesn't match any revisions."
                          .format(args.revision))
if len(targets) > 1:
    raise InvalidArgument("'{}' matches multiple revisions. "
                          "Please specify one of {}.".format(
                              args.revision,
                              ', '.join(m.id for m in targets)))
target = targets[0]

deps = descendants(target, migrations)
target_plus_deps = deps | {target}
migrations = migrations.filter(lambda m: m in target_plus_deps)
sorted_migrations = sorted(migrations, key=lambda x: x.id, reverse=True)

backend.rollback_migrations(sorted_migrations)

This implementation in python is suspiciously verbose, perhaps I've missed something, but this is what I ended having to write to avoid errors. The dependencies for a revision have to manually found and then all revisions sorted and ran.

Done!

To avoid the confirmation prompts just add the --batch flag to any command and it will auto-approve.

That's about it!

About

A guide to creating and using the Yoyo migrations tool with Postgres

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages