-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Add new test which asserts parity between upgrade and downgrade…
… detectable effects.
- Loading branch information
Showing
11 changed files
with
212 additions
and
42 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
19 changes: 19 additions & 0 deletions
19
...les/test_downgrade_leaves_no_trace_failure/migrations/versions/bbbbbbbbbbbb_create_foo.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import sqlalchemy as sa | ||
from alembic import op | ||
|
||
revision = "bbbbbbbbbbbb" | ||
down_revision = "aaaaaaaaaaaa" | ||
branch_labels = None | ||
depends_on = None | ||
|
||
|
||
def upgrade(): | ||
op.create_table( | ||
"foo", | ||
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), | ||
sa.PrimaryKeyConstraint("id"), | ||
) | ||
|
||
|
||
def downgrade(): | ||
op.rename_table("foo", "bar") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,3 @@ | ||
[tool:pytest] | ||
pytest_alembic_exclude = upgrade,single_head_revision,model_definitions_match_ddl,up_down_consistency | ||
pytest_alembic_include_experimental = downgrade_leaves_no_trace |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,3 @@ | ||
[tool:pytest] | ||
pytest_alembic_exclude = single_head_revision | ||
pytest_alembic_include_experimental = downgrade_leaves_no_trace,all_models_register_on_metadata |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,6 @@ | ||
[tool.poetry] | ||
name = "pytest-alembic" | ||
version = "0.5.1" | ||
version = "0.6.0" | ||
description = "A pytest plugin for verifying alembic migrations." | ||
authors = [ | ||
"Dan Cardin <[email protected]>", | ||
|
119 changes: 95 additions & 24 deletions
119
src/pytest_alembic/tests/experimental/downgrade_leaves_no_trace.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,41 +1,112 @@ | ||
from typing import List, Optional, Set, Tuple | ||
|
||
import alembic.migration | ||
from alembic.autogenerate import produce_migrations, render_python_code | ||
from sqlalchemy import MetaData | ||
|
||
from pytest_alembic.plugin.error import AlembicTestFailure | ||
from pytest_alembic.runner import MigrationContext | ||
|
||
try: | ||
from sqlalchemy.ext.declarative import DeclarativeMeta | ||
except ImportError: # pragma: no cover | ||
from sqlalchemy.declarative import DeclarativeMeta | ||
|
||
def test_downgrade_leaves_no_trace(alembic_runner: MigrationContext): | ||
"""Assert equal states of the MetaData before and after an upgrade/downgrade cycle. | ||
This test works by attempting to produce two autogenerated migrations. | ||
1. The first is the comparison between the original state of the database before the | ||
given migration's upgrade occurs, and the `MetaData` produced by having performed | ||
the upgrade. | ||
This should approximate the autogenerated migration that alembic | ||
would have generated to produce your upgraded database state itself. | ||
2. The 2nd is the comparison between the state of the database after having | ||
performed the upgrade -> downgrade cycle for this revision, and the same | ||
`MetaData` used in the first comparison. | ||
def test_downgrade_leaves_no_trace(alembic_runner: MigrationContext, alembic_engine): | ||
"""Assert that all tables defined on your `MetaData`, are imported in the `env.py`. | ||
This should approximate what alembic would have autogenerated if you | ||
**actual** performed the downgrade on your database. | ||
In the event these two autogenerations do not match, it implies that your | ||
upgrade -> downgrade cycle produces a database state which is different | ||
(enough for alembic to detect) from the state of the database without having | ||
performed the migration at all. | ||
**note** this isn't perfect! Alembic autogeneration will not detect many | ||
kinds of changes! If you encounter some scenario in which this does not | ||
detect a change you'd expect it to, alembic already has extensive ability | ||
to customize and extend the autogeneration capabilities. | ||
""" | ||
original_metadata = MetaData() | ||
original_metadata.reflect(alembic_engine) | ||
command_executor = alembic_runner.command_executor | ||
engine = command_executor.connection | ||
|
||
alembic_runner.migrate_up_one() | ||
# Swap the original engine for a connection to enable us to rollback the transaction | ||
# midway through. | ||
connection = engine.connect() | ||
command_executor.alembic_config.attributes["connection"] = connection | ||
|
||
upgrade_metadata = MetaData() | ||
upgrade_metadata.reflect(alembic_engine) | ||
revisions = alembic_runner.history.revisions[:-1] | ||
if len(revisions) == 1: | ||
return | ||
|
||
alembic_runner.migrate_down_one() | ||
for revision in revisions: | ||
# Leaves the database in its previous state, to avoid subtle upgrade -> downgrade issues. | ||
check_revision_cycle(alembic_runner, connection, revision) | ||
|
||
downgrade_metadata = MetaData() | ||
downgrade_metadata.reflect(alembic_engine) | ||
# So we need to proceed by one. | ||
alembic_runner.migrate_up_to(revision) | ||
|
||
# old_tables = {k: v for k, v in old_metadata.tables.items()} | ||
# new_tables = {k: v for k, v in new_metadata.tables.items() if k != 'alembic_version'} | ||
|
||
import pdb; pdb.set_trace() | ||
if new_tables: | ||
tables = ', '.join(new_tables.keys()) | ||
raise AlembicTestFailure( | ||
f"{new_tables}" | ||
def check_revision_cycle(alembic_runner, connection, original_revision): | ||
migration_context = alembic.migration.MigrationContext.configure(connection) | ||
|
||
# We first need to produce a `MetaData` which represents the state of the database | ||
# we're trying to get to. | ||
with connection.begin() as trans: | ||
alembic_runner.migrate_up_one() | ||
upgrade_revision = alembic_runner.current | ||
|
||
upgrade_metadata = MetaData() | ||
upgrade_metadata.reflect(connection) | ||
|
||
# Having procured the target `MetaData`, we need the database back in its original state. | ||
trans.rollback() | ||
|
||
with connection.begin() as trans: | ||
# Produce a canonically autogenerated upgrade relative to the original. | ||
autogenerated_upgrade = produce_migrations(migration_context, upgrade_metadata) | ||
rendered_autogenerated_upgrade = render_python_code(autogenerated_upgrade.upgrade_ops) | ||
|
||
# Now, we can perform the upgrade -> downgrade cycle! | ||
alembic_runner.migrate_up_one() | ||
alembic_runner.migrate_down_one() | ||
|
||
downgrade_metadata = MetaData() | ||
downgrade_metadata.reflect(connection) | ||
|
||
# Produce a canonically autogenerated upgrade relative to the post-downgrade state. | ||
autogenerated_post_downgrade = produce_migrations(migration_context, upgrade_metadata) | ||
rendered_autogenerated_post_downgrade = render_python_code( | ||
autogenerated_post_downgrade.upgrade_ops | ||
) | ||
|
||
raise | ||
# **This** rollback is to ensure we leave the database back in it's original state for the next revision. | ||
trans.rollback() | ||
|
||
if rendered_autogenerated_upgrade != rendered_autogenerated_post_downgrade: | ||
raise AlembicTestFailure( | ||
( | ||
f"There is a difference between the pre-'{upgrade_revision}'-upgrade `MetaData`, " | ||
f"and the post-'{upgrade_revision}'-downgrade `MetaData`. This implies that the " | ||
"upgrade performs some set of DDL changes which the downgrade does not " | ||
"precisely undo." | ||
), | ||
context=[ | ||
( | ||
f"DDL diff for {original_revision} -> {upgrade_revision}", | ||
rendered_autogenerated_upgrade, | ||
), | ||
( | ||
f"DDL diff after performing the {upgrade_revision} -> {original_revision} downgrade", | ||
rendered_autogenerated_post_downgrade, | ||
), | ||
], | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters