Skip to content

Commit 41330d0

Browse files
authored
feat: fine-grained migration step control (closes #86) (#92)
* feat: fine-grained migration step control — upgrade_to, upgrade_one, downgrade_one, downgrade_to, current_revision (#86) * style: ruff format plugin.py
1 parent 4b8950d commit 41330d0

2 files changed

Lines changed: 160 additions & 0 deletions

File tree

pytest_mrt/plugin.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,9 +106,31 @@ def __init__(self, config: MRTConfig):
106106
def upgrade(self, revision: str = "head") -> None:
107107
self._runner.upgrade(revision)
108108

109+
def upgrade_to(self, revision: str) -> None:
110+
"""Upgrade to a specific revision. Equivalent to upgrade(revision)."""
111+
self._runner.upgrade(revision)
112+
113+
def upgrade_one(self) -> None:
114+
"""Upgrade exactly one step from the current revision."""
115+
self._runner.upgrade("+1")
116+
109117
def downgrade(self, revision: str = "-1") -> None:
110118
self._runner.downgrade(revision)
111119

120+
def downgrade_one(self) -> None:
121+
"""Downgrade exactly one step from the current revision."""
122+
self._runner.downgrade("-1")
123+
124+
def downgrade_to(self, revision: str) -> None:
125+
"""Downgrade to a specific revision."""
126+
self._runner.downgrade(revision)
127+
128+
def current_revision(self) -> str | None:
129+
"""Return the current Alembic revision, or None if at base."""
130+
if self._django_mode:
131+
raise RuntimeError("current_revision() is not available in Django mode.")
132+
return self._runner.current_revision()
133+
112134
# ── manual seeding ────────────────────────────────────────────────
113135

114136
def seed(self, table: str, rows: list[dict], pk_col: str = "id") -> None:

tests/test_plugin.py

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1111,3 +1111,141 @@ def pytest_configure(config):
11111111
result = pytester.runpytest("-v")
11121112
# Normal user test should still run even if injection fails
11131113
assert result.ret in (0, 1, 2) # not a hard crash
1114+
1115+
1116+
# ── fine-grained step control (#86) ──────────────────────────────────────────
1117+
1118+
1119+
def _two_step_migrations(versions_dir: str) -> None:
1120+
_add_migration(
1121+
versions_dir,
1122+
"001_users.py",
1123+
textwrap.dedent("""
1124+
revision = '001'
1125+
down_revision = None
1126+
branch_labels = None
1127+
depends_on = None
1128+
import sqlalchemy as sa
1129+
from alembic import op
1130+
def upgrade():
1131+
op.create_table('users',
1132+
sa.Column('id', sa.Integer, primary_key=True),
1133+
sa.Column('name', sa.String(64), nullable=False),
1134+
)
1135+
def downgrade():
1136+
op.drop_table('users')
1137+
"""),
1138+
)
1139+
_add_migration(
1140+
versions_dir,
1141+
"002_posts.py",
1142+
textwrap.dedent("""
1143+
revision = '002'
1144+
down_revision = '001'
1145+
branch_labels = None
1146+
depends_on = None
1147+
import sqlalchemy as sa
1148+
from alembic import op
1149+
def upgrade():
1150+
op.create_table('posts',
1151+
sa.Column('id', sa.Integer, primary_key=True),
1152+
sa.Column('title', sa.String(128), nullable=False),
1153+
)
1154+
def downgrade():
1155+
op.drop_table('posts')
1156+
"""),
1157+
)
1158+
1159+
1160+
def _table_names(db_url: str) -> list[str]:
1161+
engine = create_engine(db_url)
1162+
with engine.connect() as conn:
1163+
rows = conn.execute(text("SELECT name FROM sqlite_master WHERE type='table'")).fetchall()
1164+
engine.dispose()
1165+
return [r[0] for r in rows]
1166+
1167+
1168+
def test_upgrade_to(alembic_env):
1169+
_two_step_migrations(alembic_env["versions"])
1170+
cfg = MRTConfig(alembic_ini=alembic_env["ini"], db_url=alembic_env["db_url"])
1171+
fixture = MRTFixture(cfg)
1172+
1173+
fixture.upgrade_to("001")
1174+
tables = _table_names(alembic_env["db_url"])
1175+
assert "users" in tables
1176+
assert "posts" not in tables
1177+
fixture.reset()
1178+
1179+
1180+
def test_upgrade_one(alembic_env):
1181+
_two_step_migrations(alembic_env["versions"])
1182+
cfg = MRTConfig(alembic_ini=alembic_env["ini"], db_url=alembic_env["db_url"])
1183+
fixture = MRTFixture(cfg)
1184+
1185+
fixture.upgrade_to("001")
1186+
fixture.upgrade_one()
1187+
tables = _table_names(alembic_env["db_url"])
1188+
assert "users" in tables
1189+
assert "posts" in tables
1190+
fixture.reset()
1191+
1192+
1193+
def test_downgrade_one(alembic_env):
1194+
_two_step_migrations(alembic_env["versions"])
1195+
cfg = MRTConfig(alembic_ini=alembic_env["ini"], db_url=alembic_env["db_url"])
1196+
fixture = MRTFixture(cfg)
1197+
1198+
fixture.upgrade("head")
1199+
fixture.downgrade_one()
1200+
tables = _table_names(alembic_env["db_url"])
1201+
assert "posts" not in tables
1202+
assert "users" in tables
1203+
fixture.reset()
1204+
1205+
1206+
def test_downgrade_to(alembic_env):
1207+
_two_step_migrations(alembic_env["versions"])
1208+
cfg = MRTConfig(alembic_ini=alembic_env["ini"], db_url=alembic_env["db_url"])
1209+
fixture = MRTFixture(cfg)
1210+
1211+
fixture.upgrade("head")
1212+
fixture.downgrade_to("001")
1213+
tables = _table_names(alembic_env["db_url"])
1214+
assert "posts" not in tables
1215+
assert "users" in tables
1216+
fixture.reset()
1217+
1218+
1219+
def test_current_revision(alembic_env):
1220+
_two_step_migrations(alembic_env["versions"])
1221+
cfg = MRTConfig(alembic_ini=alembic_env["ini"], db_url=alembic_env["db_url"])
1222+
fixture = MRTFixture(cfg)
1223+
1224+
assert fixture.current_revision() is None
1225+
fixture.upgrade_to("001")
1226+
assert fixture.current_revision() == "001"
1227+
fixture.upgrade_one()
1228+
assert fixture.current_revision() == "002"
1229+
fixture.reset()
1230+
1231+
1232+
def test_step_control_data_migration_pattern(alembic_env):
1233+
"""Issue #86 use case: insert legacy data at mid-migration, verify transformation."""
1234+
_two_step_migrations(alembic_env["versions"])
1235+
cfg = MRTConfig(alembic_ini=alembic_env["ini"], db_url=alembic_env["db_url"])
1236+
fixture = MRTFixture(cfg)
1237+
1238+
# Upgrade to first migration, seed data, then upgrade further
1239+
fixture.upgrade_to("001")
1240+
fixture.seed("users", [{"id": 1, "name": "legacy_user"}], pk_col="id")
1241+
fixture.upgrade_one()
1242+
1243+
# Data seeded before second migration should still be there
1244+
engine = create_engine(alembic_env["db_url"])
1245+
with engine.connect() as conn:
1246+
result = conn.execute(text("SELECT name FROM users WHERE id=1")).fetchone()
1247+
engine.dispose()
1248+
assert result is not None
1249+
assert result[0] == "legacy_user"
1250+
1251+
fixture.reset()

0 commit comments

Comments
 (0)