Разработчик обошёл платный откат миграций в Flyway на Python

Инструмент для миграций баз данных Flyway в бесплатной Community edition умеет применять миграции (migrate), проверять их (validate) и чинить историю (repair), но не умеет откатывать их назад, управляемый откат доступен только в платном тарифе Teams. Автор блога aconcan.io описал способ получить такой откат без покупки лицензии.

Идея в том, что для Flyway откат, это просто ещё одна миграция вперёд. У инструмента есть два жёстких правила: номера версий миграций должны только расти, а таблица истории схемы flyway_schema_history считается главным источником истины. Вместо того чтобы просить Flyway отменить, скажем, миграцию V2, автор пишет новую миграцию с более высоким номером версии, тело которой, это обратная (реверсная) SQL-логика миграции V2, и просит Flyway применить её как обычную миграцию вперёд. Формально это выглядит как ещё один шаг вперёд, а по факту база данных возвращается в прежнее состояние.

Для этого на диске держат две параллельные папки: migrations/ с обычными миграциями и rollbacks/ с «down»-скриптами, каждый из которых называется по тому же номеру версии, что и исходная миграция (например, V1__create_customers.down.sql для V1__create_customers.sql). При каждом запуске migrate скрипт сравнивает список применённых версий до и после запуска и сохраняет разницу («батч») в JSON-файл на диске, так система знает, что именно откатывать при следующем вызове rollback.

При откате «down»-скрипты для версий из батча копируются во временную папку под новыми, более высокими номерами версий, чем максимальный уже применённый, так удовлетворяется требование Flyway о постоянно растущих номерах. Порядок при этом разворачивается: если были применены V1, а затем V2, при откате первым отменяется V2, а уже потом V1 (более новая миграция получает меньший из новых номеров). Вместе с этими файлами в ту же временную папку кладут скрипт afterMigrate.sql, специальный callback, который выполняется в самом конце Flyway-запуска и удаляет из таблицы flyway_schema_history строки и по исходным миграциям, и по их «undo»-версиям. В результате история схемы выглядит так, будто отменённых миграций никогда не было, а сами исходные файлы миграций на диске вновь помечаются Flyway как «Pending», готовые к повторному применению.

Чтобы откат не мог остановиться на середине, весь процесс запускается с флагом -group=true, который оборачивает все ожидающие миграции в одну транзакцию базы данных: либо применяются и «down»-скрипты, и очищающий callback целиком, либо (при сбое) Postgres откатывает всё целиком и база остаётся нетронутой. Автор отмечает, что эта гарантия работает только если сам DDL транзакционен.

Автор перечисляет и ограничения способа: он никак не защищает от потери данных, если исходная миграция делает что-то необратимое, например, удаляет столбец с данными; «down»-скрипты, как и при использовании платного Flyway Teams, приходится писать вручную, автогенерации обратной миграции нет; состояние отслеживаемого батча хранится в файле на файловой системе контейнера, поэтому пересборка контейнера между migrate и rollback приведёт к потере записи о том, что именно откатывать. Отдельно автор признаёт, что удаление строк из служебной таблицы flyway_schema_history, операция, которая может вызвать оторопь у пуристов Flyway, но настаивает, что она безопасна, поскольку затрагивает строго те версии, что участвовали в откате, и выполняется в той же транзакции, что и сама реверсная миграция.

Ключевые факты

  • Flyway Community edition не поддерживает управляемый откат миграций, эта функция доступна только в платном тарифе Teams
  • Технику: «down»-скрипт для отмены миграции применяется не как откат, а как обычная новая миграция вперёд с более высоким номером версии
  • После применения callback-скрипт afterMigrate.sql удаляет из таблицы flyway_schema_history строки и исходной, и «undo»-миграции, чтобы история выглядела так, будто их не было
  • Весь откат оборачивается в одну транзакцию флагом -group=true, что исключает состояние "наполовину откачено", при условии, что сам DDL транзакционен
  • Ограничения: способ не защищает от потери данных при необратимых операциях, «down»-скрипты пишутся вручную, а состояние батча хранится в файле на файловой системе контейнера и теряется при его пересоздании

Почему это важно

Flyway, популярный инструмент для управления миграциями баз данных, и отсутствие отката в бесплатной версии, известная и часто обсуждаемая боль разработчиков, которых это подталкивает либо покупать Teams-лицензию, либо писать откатывающие SQL-скрипты вручную. Материал интересен не как новость об изменении продукта, а как техническая демонстрация: автор показывает, что жёсткие правила инструмента (номера версий только растут, история схемы, источник истины) можно использовать не как ограничение, а как строительный материал для нужной функциональности.

Кому это важно

В первую очередь бэкенд- и DevOps-инженерам, которые используют Flyway Community edition и упираются в отсутствие управляемого отката, но не готовы платить за Teams. Также полезно тем, кто проектирует собственные инструменты миграций или CI/CD-пайплайны для баз данных и ищет паттерны безопасного (транзакционного) отката.

Как это применить

Нужно завести две параллельные папки, с обычными миграциями и с «down»-скриптами, названными по тому же номеру версии, что и исходная миграция. При каждом запуске migrate сохранять список только что применённых версий («батч») в отдельный файл состояния. При откате, собрать во временной папке «down»-скрипты для версий из батча в обратном порядке, присвоив им номера версий выше текущего максимума, добавить туда же afterMigrate.sql, удаляющий из истории строки по обеим сторонам «раунд-трипа», и запустить migrate с флагом -group=true, чтобы весь процесс шёл в одной транзакции.

Можно ли доверять

Это техническая заметка одного автора в личном блоге (aconcan.io), а не официальный анонс от разработчиков Flyway или Redgate, способ не подтверждён и не одобрен производителем инструмента. Описание внутренне последовательно и подкреплено реальным кодом на Python и SQL, но применимость и надёжность решения проверены только на уровне рассуждений автора, без указания версий Flyway/Postgres или опыта эксплуатации в проде.

Риски и подводные камни

Способ не защищает от потери данных, если исходная миграция необратима (например, удаляет столбец с данными), тогда откатывать физически нечего. Скрипты отмены миграций пишутся вручную и должны быть заранее подготовлены для каждой миграции. Состояние («какой батч откатывать») хранится в файле на файловой системе контейнера, если контейнер пересоздать между migrate и rollback, эта информация теряется. Наконец, техника вручную удаляет строки из служебной таблицы flyway_schema_history в обход штатных механизмов Flyway, и хотя автор считает это безопасным при соблюдении транзакционности, это прямое вмешательство во внутреннюю бухгалтерию инструмента, о последствиях которого стоит помнить.

«У Flyway есть два принципа, на которых построено всё решение: номера версий должны только расти, а таблица истории схемы, это библия.»

— автор блога aconcan.io