Skip to content

Commit

Permalink
Migration tests (#636)
Browse files Browse the repository at this point in the history
Adds some basic tests to ensure our migrations run successfully and
yield the same resulting db schema as our models.

How we do it:
- Create an initial migration from the empty database to the database
before our current first migration
- Leverage pytest-alembic, a plugin that runs these basic tests for you
  • Loading branch information
jonahkagan authored Jul 8, 2020
1 parent c6a4c7f commit 1f24228
Show file tree
Hide file tree
Showing 12 changed files with 597 additions and 14 deletions.
1 change: 1 addition & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ pylint = "*"
black = "==19.10b0"
snapshottest = "*"
alembic = "*"
pytest-alembic = "*"

[packages]
authlib = "*"
Expand Down
10 changes: 9 additions & 1 deletion Pipfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion alembic.ini
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

[alembic]
# path to migration scripts
script_location = migrations
script_location = server/migrations

# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s
Expand Down
1 change: 0 additions & 1 deletion migrations/README

This file was deleted.

9 changes: 9 additions & 0 deletions mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,13 @@ ignore_missing_imports = True
ignore_missing_imports = True

[mypy-snapshottest]
ignore_missing_imports = True

[mypy-alembic]
ignore_missing_imports = True

[mypy-alembic.config]
ignore_missing_imports = True

[mypy-pytest_alembic.tests]
ignore_missing_imports = True
29 changes: 29 additions & 0 deletions server/migrations/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Migrations

We use [Alembic](https://alembic.sqlalchemy.org/en/latest/) to create and run incremental database migrations when we change the Arlo data model.

The Alembic docs have great info on the ins and outs of creating and running migrations, but here's a sample workflow:

## Create a migration script

First, make sure the database is in the existing, unmigrated state (e.g. by checking out the last commit and running `make resetdb`, or restoring from a database backup).

Then, use Alembic to autogenerate a migration script:

pipenv run alembic revision --autogenerate -m "Some description of the migration"

The resulting migration script will be in `migrations/versions`.

## Edit the migration script

The autogeneration capabilities of Alembic will only get you so far, so you _always_ need to go manually check and edit the script. For example, Alembic won't autogenerate updates to primary key constraints. Read [the docs](https://alembic.sqlalchemy.org/en/latest/autogenerate.html#what-does-autogenerate-detect-and-what-does-it-not-detect) for specific info about the autogeneration capabilities.

Note that we don't support reverse migrations (`alembic downgrade`) because we don't think it's worth the effort to implement them. So you should comment out the autogenerated downgrade code and replace it with `pass`.

## Test the migration script

Populate your local database with some data (e.g. restored from a backup), then run:

pipenv run alembic upgrade head

...and see if it works!
Empty file added server/migrations/__init__.py
Empty file.
22 changes: 14 additions & 8 deletions migrations/env.py → server/migrations/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)
fileConfig(config.config_file_name, disable_existing_loggers=False)

# add your model's MetaData object here
# for 'autogenerate' support
Expand All @@ -39,7 +39,7 @@
# ... etc.


def run_migrations_offline():
def run_migrations_offline(): # pragma: no cover
"""Run migrations in 'offline' mode.
This configures the context with just a URL
Expand All @@ -55,6 +55,9 @@ def run_migrations_offline():
context.configure(
url=url,
target_metadata=target_metadata,
compare_type=True,
compare_server_default=True,
include_schemas=True,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
Expand All @@ -70,11 +73,14 @@ def run_migrations_online():
and associate a connection with the context.
"""
connectable = engine_from_config(
config.get_section(config.config_ini_section),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
connectable = config.attributes.get("connection", None)

if connectable is None:
connectable = engine_from_config(
config.get_section(config.config_ini_section),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)

with connectable.connect() as connection:
context.configure(connection=connection, target_metadata=target_metadata)
Expand All @@ -84,6 +90,6 @@ def run_migrations_online():


if context.is_offline_mode():
run_migrations_offline()
run_migrations_offline() # pragma: no cover
else:
run_migrations_online()
File renamed without changes.
Loading

0 comments on commit 1f24228

Please sign in to comment.