<?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: a</title>
    <description>The latest articles on DEV Community by a (@aconcan).</description>
    <link>https://dev.to/aconcan</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4094699%2F40bf5797-df27-49c6-a6b4-b460631ec48e.png</url>
      <title>DEV Community: a</title>
      <link>https://dev.to/aconcan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aconcan"/>
    <language>en</language>
    <item>
      <title>Flyway for Freeloaders — Rollback without Teams</title>
      <dc:creator>a</dc:creator>
      <pubDate>Tue, 25 Aug 2026 19:57:48 +0000</pubDate>
      <link>https://dev.to/aconcan/flyway-for-freeloaders-rollback-without-teams-aif</link>
      <guid>https://dev.to/aconcan/flyway-for-freeloaders-rollback-without-teams-aif</guid>
      <description>&lt;p&gt;&lt;strong&gt;find me @ &lt;a href="https://aconcan.io/blog/" rel="noopener noreferrer"&gt;aconcan.io&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A single command to roll back applied migrations with Flyway Community Edition, no license, no &lt;code&gt;undo&lt;/code&gt; command, no crying in the club.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you've ever priced Flyway Teams just for the &lt;code&gt;undo&lt;/code&gt; command, you know what I'm talking about. Managed rollback functionality is the most delectable morsel that lives behind the Flyway paywall, and it's the one feature everyone wants at 2am when a migration hits the fan in prod. The Community edition gives you &lt;code&gt;migrate&lt;/code&gt;, &lt;code&gt;info&lt;/code&gt;, &lt;code&gt;validate&lt;/code&gt;, &lt;code&gt;repair&lt;/code&gt;, and stubs its cigarette out in your eye when you ask it to go backwards.&lt;/p&gt;

&lt;p&gt;A solution you ask? It turns out you don't need the paid &lt;code&gt;undo&lt;/code&gt;, you just need to be a little bit devious about what "rollback" actually &lt;em&gt;is&lt;/em&gt;. The following rollback implementation is achieved entirely via Flyway commands, without running any SQL directly against the database via a separate driver (#minimalism). Here's how it works, buckle up girls.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Core Concept: Rollback is just Migration in a Wig
&lt;/h2&gt;

&lt;p&gt;Here's the mental unlock. Flyway Community will happily run any versioned migration you hand it. It doesn't care whether that migration creates a table or drops one, SQL is SQL. &lt;/p&gt;

&lt;p&gt;There are however, two things Flyway is very passionate about, and this solution is built around them: &lt;strong&gt;version numbers must always go up&lt;/strong&gt; and &lt;strong&gt;the schema history table is bible&lt;/strong&gt;.&lt;br&gt;
So instead of asking Flyway to reverse &lt;code&gt;V2&lt;/code&gt;, we write a &lt;em&gt;new&lt;/em&gt;, higher-versioned migration whose body happens to be the reverse of &lt;code&gt;V2&lt;/code&gt;, and we ask Flyway to migrate &lt;em&gt;forward&lt;/em&gt; into it. Basically an "undo" expressed as &lt;code&gt;migrate&lt;/code&gt;, and the database ends up back where it started.&lt;/p&gt;

&lt;p&gt;Then we clean up the paper trail of the migrations, and their respective "undo" migrations, in the &lt;code&gt;flyway_schema_history&lt;/code&gt; table so it lines back up with the database state, and the files on disk.&lt;/p&gt;
&lt;h2&gt;
  
  
  What you need on disk
&lt;/h2&gt;

&lt;p&gt;Two directories per scope. Your intended migrations, and a parallel set of "down" scripts that reverse the logic applied by these migrations. Something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
migrations/
  V1__create_customers.sql
rollbacks/
  V1__create_customers.down.sql 

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The naming convention is load-bearing. "down" scripts should be matched to their initial migration by the &lt;code&gt;V&amp;lt;version&amp;gt;__&lt;/code&gt; prefix. &lt;/p&gt;

&lt;p&gt;As part of the rollback the down script versions will be incremented, so Flyway sees them as new migrations to be applied (we'll get to this shortly). I'd recommend validating that there is equivalent "down" script for every migration ready to go up front, so nothing is found to be missing mid-rollback:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_down_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rollbacks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;matches&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rollbacks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;V&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;__*.down.sql&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RollbackError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no down script for V&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; in &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;rollbacks&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What are we actually rolling back?
&lt;/h2&gt;

&lt;p&gt;An important question: when someone runs &lt;code&gt;rollback&lt;/code&gt;, what exactly are they rolling back? The last migration? The last &lt;em&gt;batch&lt;/em&gt;? What if the last &lt;code&gt;migrate&lt;/code&gt; applied three files at once?&lt;/p&gt;

&lt;p&gt;This is solved by recording the batch of successfully applied migrations whenever &lt;code&gt;flyway migrate&lt;/code&gt; is run. This is done by getting a diff the applied versions before and after the &lt;code&gt;migrate&lt;/code&gt;, and storing the applied versions in a JSON file. Basically, whatever versions are new is "the batch".&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;migrate&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;PgConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Get currently applied versions using info -outputType=json
&lt;/span&gt;    &lt;span class="n"&gt;before&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;flyway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;applied_versions&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="c1"&gt;# Run migrate
&lt;/span&gt;    &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;flyway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;migrate&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="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;migrations&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;
    &lt;span class="c1"&gt;# Get diff of the migrations applied before and after `migrate`
&lt;/span&gt;    &lt;span class="n"&gt;after&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;flyway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;applied_versions&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;batch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;after&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;before&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="c1"&gt;# Store to a JSON statefile
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write_batch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Write it to a path in your container that will survive the separate &lt;code&gt;migrate&lt;/code&gt; and &lt;code&gt;rollback&lt;/code&gt; invocations if you're doing this via a process driven CLI approach.&lt;/p&gt;

&lt;p&gt;There's a quiet piece of defensiveness in &lt;code&gt;if batch:&lt;/code&gt;. If you run &lt;code&gt;migrate&lt;/code&gt; twice by accident, the second run applies nothing new, &lt;code&gt;batch&lt;/code&gt; is empty, and the recorded batch isn't overwritten with &lt;code&gt;[]&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Staging the rollback scripts
&lt;/h2&gt;

&lt;p&gt;Now that we've persisted a record of what was applied on the last &lt;code&gt;migrate&lt;/code&gt; invocation, we know exactly what we're rolling back. We can start to stage the "down" migrations with modified version prepends to satisfy the Flyway condition of "versions must always go up". Given a batch like &lt;code&gt;[1, 2]&lt;/code&gt;, we build a temp directory full of synthetic migrations, purely for Flyways consumption:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_stage&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;PgConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;staging&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Get the highest version applied
&lt;/span&gt;    &lt;span class="n"&gt;highest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;flyway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;applied_versions&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="c1"&gt;# Reverse the order and increment sequentially from the highest of the currently applied versions
&lt;/span&gt;    &lt;span class="n"&gt;undo_versions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;reversed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;new_version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;highest&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;undo_versions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;new_version&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;sql&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_down_file&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;version&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;staging&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;V&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;new_version&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;__undo_V&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.sql&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;write_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we have all the "down" files that will be used in the rollback in a format that Flyway will readily apply. Two things worth noting:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. We reverse the batch.&lt;/strong&gt; If &lt;code&gt;V1&lt;/code&gt; was applied and then &lt;code&gt;V2&lt;/code&gt;, &lt;code&gt;V2&lt;/code&gt; must be undone before &lt;code&gt;V1&lt;/code&gt;. So the last-applied migration is undone first, and gets the &lt;em&gt;lowest&lt;/em&gt; new version number. Batch &lt;code&gt;[1, 2]&lt;/code&gt; with a current high-water mark of &lt;code&gt;2&lt;/code&gt; becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;V3__undo_V2.sql
V4__undo_V1.sql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Version numbers keep climbing.&lt;/strong&gt; We never reuse &lt;code&gt;1&lt;/code&gt; or &lt;code&gt;2&lt;/code&gt;. Flyway's cardinal rule is that versions only go up, so undo scripts are numbered &lt;em&gt;above&lt;/em&gt; the current maximum. This is what lets us sneak the reversal past Community edition as an ordinary forward migrate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bringing the &lt;code&gt;flyway_schema_history&lt;/code&gt; table into alignment
&lt;/h2&gt;

&lt;p&gt;If we stopped here, after staging only the "down" scripts and running &lt;code&gt;migrate&lt;/code&gt;, &lt;code&gt;flyway info&lt;/code&gt; would show records for &lt;code&gt;V3__undo_V2&lt;/code&gt;, &lt;code&gt;V4__undo_V1&lt;/code&gt;, despite the fact that these migrations don't exist as files in our migrations directory (and despite the fact that they cancel out the logic of previous migrations in the database, returning it to initial state). Because these migrations don't exist as files, Flyway will now flag the history as out of sync and blow up on future &lt;code&gt;validate&lt;/code&gt; and &lt;code&gt;migrate&lt;/code&gt; commands.&lt;/p&gt;

&lt;p&gt;So the last file we stage in our temporary directory along with the "down" scripts is an &lt;code&gt;afterMigrate.sql&lt;/code&gt; callback, which deletes the round-trip rows from the history table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;    &lt;span class="c1"&gt;# Delete rows for both the original migrations and their respective "down" migrations
&lt;/span&gt;    &lt;span class="n"&gt;versions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;undo_versions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;staging&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;afterMigrate.sql&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;write_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DELETE FROM &lt;/span&gt;&lt;span class="si"&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;schema&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.flyway_schema_history &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;WHERE version IN (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;versions&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;);&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We delete &lt;em&gt;both&lt;/em&gt; sides of the round trip, the originals (&lt;code&gt;1&lt;/code&gt;, &lt;code&gt;2&lt;/code&gt;) &lt;strong&gt;and&lt;/strong&gt; the undo entries (&lt;code&gt;3&lt;/code&gt;, &lt;code&gt;4&lt;/code&gt;). When the dust settles, the history table has no memory that any of this ever happened. The &lt;code&gt;customers&lt;/code&gt; table is gone, &lt;code&gt;V1__create_customers.sql&lt;/code&gt; is still sitting on disk, and &lt;code&gt;flyway info&lt;/code&gt; cheerfully reports it as &lt;code&gt;Pending&lt;/code&gt; again, ready to re-apply, as if you'd never migrated.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;afterMigrate&lt;/code&gt; runs inside Flyway's own execution, after the versioned migrations succeed. It fires only if the migration succeeded.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making it atomic
&lt;/h2&gt;

&lt;p&gt;The whole reason you want rollback functionality is that things go wrong. So the rollback itself had &lt;em&gt;better not&lt;/em&gt; leave you half-reversed. This is where we lean on a Flyway flag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;migrate&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;PgConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;locations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Run as a single db transaction. If any pending migration fails, the whole batch rolls back.
&lt;/span&gt;    &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_base_args&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;locations&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-group=true&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;migrate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;returncode&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;-group=true&lt;/code&gt; wraps all the pending migrations in a single transaction. Either every "down" script and the purge callback commit together, or Postgres rolls the whole thing back and your database is untouched. There's no state where &lt;code&gt;V2&lt;/code&gt; got undone but &lt;code&gt;V1&lt;/code&gt; didn't. (This does rely on your DDL being transactional)&lt;/p&gt;

&lt;p&gt;And the assembled &lt;code&gt;rollback&lt;/code&gt; puts it all together:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;rollback&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;PgConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Read the batch of just-applied migration versions from the state file
&lt;/span&gt;    &lt;span class="n"&gt;batch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_batch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RollbackError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no recorded migration batch to undo for &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Verify every down script exists
&lt;/span&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;_down_file&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;version&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Stage temporary flyway-versioned down scripts and callback, then call migrate
&lt;/span&gt;    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;tempfile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TemporaryDirectory&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;tmp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;_stage&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;batch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tmp&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;flyway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;migrate&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="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;migrations&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tmp&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

    &lt;span class="c1"&gt;# Clear the state file of migrations just applied (the initial batch has been rolled back)
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clear_batch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ordering here is deliberate:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;No batch? Hard fail&lt;/strong&gt;
 Rolling back with nothing recorded is a mistake. If the rollback isn't occurring in the current session, it's likely rolling back previous migrations that have been in place for some time; in which case they should be fixed forward, not rolled back.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check every "down" script exists &lt;em&gt;before&lt;/em&gt; touching the database.&lt;/strong&gt;
 If you're missing the &lt;code&gt;.down.sql&lt;/code&gt; for even one version in the batch, it's time to fail. Much like an arrow to the knee, half a rollback is worse than none.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Migrate over both locations&lt;/strong&gt;
 Notice Flyway needs the real migrations directory &lt;strong&gt;&lt;em&gt;and&lt;/em&gt; the temp staging dir&lt;/strong&gt;. Flyway needs the originals in its locations to reconcile history, while the temp dir supplies the new down versions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Only clear state on success&lt;/strong&gt;
 If the migrate returns non-zero, the batch file stays as is so you can fix the problem and retry.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The temp directory evaporates with the &lt;code&gt;with&lt;/code&gt; block. Upon completion, the temporary directory and all of its contents are automatically deleted. The Flyway specific versions of the "down" files exist for exactly one Flyway invocation, and then they're gone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;p&gt;Honestly, I don't think there are many hairy parts in the solution, but it's worth mentioning some of the limitations to know where the edges lie. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Nothing in this solution accounts for data loss:&lt;/strong&gt;
 This leaves you in the &lt;em&gt;same position you'd be in if you had actually paid for Flyway Teams&lt;/em&gt;, but I have to mention it anyway because transparency is my passion. If your "up" script does something irreversible like dropping a column that has data in it, I hope you didn't really need it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You write the "down" scripts by hand:&lt;/strong&gt;
 There's no auto-generated inverse, but guess what, &lt;em&gt;you also need to do this if you pay for Flyway teams&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State lives on the container filesystem:&lt;/strong&gt;
 Rollback context is tied to that filesystem's lifetime. Blow away the container between migrate and rollback and you've lost the batch. (This doesn't apply to you stateless enjoyers)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;We delete rows from &lt;code&gt;flyway_schema_history&lt;/code&gt;:&lt;/strong&gt;
 Flyway purists will feel a chill up their very erect spines. I'm comfortable with it because the deletion is scoped to exactly the versions in the round trip and runs inside the same transaction as the reversal. That said, you are editing Flyway's bookkeeping out from under it. Know that you're doing it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why this is actually nice
&lt;/h2&gt;

&lt;p&gt;Even though this solution is 2 evil Flyway operations on top of eachother in a trenchcoat, the result is a rollback that's honest. &lt;/p&gt;

&lt;p&gt;It runs your reversal SQL through the same engine, with the same transactional guarantees as your forward migrations. Modification of the &lt;code&gt;flyway_schema_history&lt;/code&gt; table is done via a Flyway supported feature. You're pretty protected from weird half-jobs presuming you do use database transactions. When it's done, your schema history is clean, and the migrations you rolled back are simply pending again, ready to re-apply (or I assume remediate).&lt;/p&gt;

&lt;p&gt;No license. No &lt;code&gt;undo&lt;/code&gt; command. Just a temp directory, a version counter that only goes up, and one saucy &lt;code&gt;DELETE&lt;/code&gt;. A story for the ages.&lt;/p&gt;

</description>
      <category>flyway</category>
      <category>rollback</category>
      <category>migrations</category>
      <category>undo</category>
    </item>
  </channel>
</rss>
