<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: croc100</title>
    <description>The latest articles on DEV Community by croc100 (@croc100).</description>
    <link>https://dev.to/croc100</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3925557%2F27f0d728-86b5-4e0d-91cc-3693af7efdc8.png</url>
      <title>DEV Community: croc100</title>
      <link>https://dev.to/croc100</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/croc100"/>
    <language>en</language>
    <item>
      <title>We found a data-loss bug that alembic downgrade ran clean</title>
      <dc:creator>croc100</dc:creator>
      <pubDate>Fri, 05 Jun 2026 20:18:55 +0000</pubDate>
      <link>https://dev.to/croc100/we-found-a-data-loss-bug-that-alembic-downgrade-ran-clean-n4h</link>
      <guid>https://dev.to/croc100/we-found-a-data-loss-bug-that-alembic-downgrade-ran-clean-n4h</guid>
      <description>&lt;p&gt;&lt;code&gt;alembic downgrade -1&lt;/code&gt; ran clean. No errors. Monitoring went green.&lt;/p&gt;

&lt;p&gt;The users' phone numbers were gone. The column came back. The data didn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why standard migration tests miss this
&lt;/h2&gt;

&lt;p&gt;Most CI pipelines verify:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;alembic upgrade head&lt;/code&gt; exits 0&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;alembic downgrade -1&lt;/code&gt; exits 0&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Neither checks that data &lt;em&gt;survived&lt;/em&gt; the round-trip. The SQL can be perfectly valid, the migration fully reversible in schema terms, and rows still disappear silently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two ways to catch it
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Static analysis — no database needed
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;pytest-mrt
mrt check migrations/versions/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;30 patterns checked: &lt;code&gt;op.drop_column()&lt;/code&gt; in upgrade, no-op &lt;code&gt;downgrade()&lt;/code&gt;, PostgreSQL ENUM adds that can't be rolled back, &lt;code&gt;op.execute()&lt;/code&gt; without reverse, &lt;code&gt;NOT NULL&lt;/code&gt; without &lt;code&gt;server_default&lt;/code&gt;, and more. Runs in ~22ms for 10 migrations.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Dynamic verification — seed rows, roll back, check they survived
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# conftest.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pytest_mrt&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;MRTConfig&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;pytest_configure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_mrt_config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MRTConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;alembic_ini&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;alembic.ini&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;db_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TEST_DATABASE_URL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# tests/test_migrations.py
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_migrations_are_safe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mrt&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;mrt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assert_all_reversible&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;mrt&lt;/code&gt; fixture seeds rows before each migration, runs downgrade, asserts nothing was lost. Catches the case above — column exists after downgrade, rows don't.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to use each
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Static (&lt;code&gt;mrt check&lt;/code&gt;)&lt;/th&gt;
&lt;th&gt;Dynamic (&lt;code&gt;mrt&lt;/code&gt; fixture)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Speed&lt;/td&gt;
&lt;td&gt;~22ms / 10 migrations&lt;/td&gt;
&lt;td&gt;~330ms / migration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Needs DB&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Catches logic bugs&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best for&lt;/td&gt;
&lt;td&gt;every commit&lt;/td&gt;
&lt;td&gt;PR merge / nightly&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Databases
&lt;/h2&gt;

&lt;p&gt;PostgreSQL, MySQL/MariaDB, SQLite, Oracle, SQL Server.&lt;/p&gt;




&lt;p&gt;GitHub: &lt;a href="https://github.com/croc100/pytest-mrt" rel="noopener noreferrer"&gt;https://github.com/croc100/pytest-mrt&lt;/a&gt;&lt;br&gt;
Docs: &lt;a href="https://croc100.github.io/pytest-mrt" rel="noopener noreferrer"&gt;https://croc100.github.io/pytest-mrt&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The incident at the top is a reconstruction of a pattern I've seen across production systems and open-source repos.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>django</category>
      <category>testing</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
