Мигратор схемы кажется тонкой обёрткой над «применить SQL-файлы по порядку». На деле это программа с собственными требованиями к базе. Ей нужна своя таблица-журнал, нужно знать, в какой схеме она работает, нужно уметь прочитать версию сервера и системные каталоги. Именно эти требования ломаются первыми, когда вы уходите от ванильного PostgreSQL к базе, которая «говорит на его протоколе». Ваши миграции при этом могут быть безупречны — до них просто не доходит дело.
Поводом стал живой случай из работы над Picodata: goose не смог создать даже собственную таблицу версий. Чтобы понять, частный ли это сбой, я собрал стенд architecture/schema-migrators. Пять миграторов — goose v3.28.0, Flyway OSS 13.9.0, Liquibase 5.0.4, Atlas community 1.3.3 и Alembic 1.20.0 — против трёх СУБД с PostgreSQL-протоколом: PostgreSQL 18.4 как контроль, CockroachDB v26.2.5 и Picodata 26.1.3. Миграции нарочно тривиальны — создать таблицу, добавить колонку, откатить, — поэтому любой отказ здесь — отказ инструмента, а не SQL.
В статье
- На чём стоит любой мигратор
- Что ломается за пределами мейнстрима
- Если миграция упала на середине
- Императивный против декларативного
- Кто владеет вашей схемой
- Что в бесплатных редакциях стоит денег
- Как выбирать
На чём стоит любой мигратор
У любого мигратора, независимо от языка и подхода, есть четыре опоры, и каждая — требование к базе, а не к вашим миграциям.
Журнал применённых версий в самой базе. Мигратор помнит, что уже применено, в собственной таблице: goose_db_version, flyway_schema_history, databasechangelog, atlas_schema_revisions, alembic_version. Создаёт её он сам, своим DDL, — и этот DDL написан под PostgreSQL. Если база не понимает какую-то конструкцию в нём, инструмент не может начать работу вообще.
Защита от одновременного применения. Когда приложение запускается в нескольких репликах и каждая при старте применяет миграции, они не должны делать это одновременно. Это требование к процессу, а не встроенное свойство любого мигратора: защиту даёт инструмент, его настройка или внешний механизм — отдельный шаг деплоя, который запускает миграции один раз. Liquibase держит для этого таблицу databasechangeloglock, другие используют advisory locks, а, например, у goose в режиме Provider блокировка по умолчанию выключена и включается отдельной опцией. Стенд эту опору не проверяет, но на базе без advisory locks — а у CockroachDB их нет — её реализация тоже под вопросом.
Транзакционный DDL — или его отсутствие. В PostgreSQL миграцию можно обернуть в транзакцию: упала середина — откатилось всё. Многие инструменты на это рассчитывают и применяют миграцию вместе с записью в журнал одной транзакцией. Если база ведёт себя иначе, расходятся состояние схемы и журнал — и ниже будет видно, как это выглядит на CockroachDB.
Интроспекция системных каталогов. Чтобы узнать текущую схему подключения, версию сервера, список таблиц, мигратор читает pg_catalog, information_schema, системные переменные. Совместимость «по протоколу» здесь не помогает: каталоги — это не протокол, а внутреннее устройство PostgreSQL, и база с собственным ядром воспроизводит их частично или не воспроизводит совсем. Инструменты, чья ключевая функция построена на интроспекции — сравнение схемы с моделью, генерация миграции по разнице, — ломаются первыми.
Что ломается за пределами мейнстрима
Для каждой пары «инструмент × СУБД» стенд проходит пять шагов: применить первую миграцию, проверить, появился ли журнал, применить вторую, прочитать схему штатной командой инструмента и откатить вторую миграцию. Чтение схемы есть только у двоих, и проверяет оно разное: alembic check сравнивает схему базы с моделью, а для atlas schema inspect стенд проверяет лишь, что инструмент прочитал каталог и нашёл в нём таблицу и колонку, — это инспекция, а не сравнение с эталоном. Исход шага определяется не по коду возврата, а по паре «что сказал инструмент + что на самом деле в базе», проверенной отдельным SQL-запросом. Перед замерами каждый инструмент получил заведомо невалидную миграцию на PostgreSQL, и все пять честно упали — значит, их «ok» в матрице что-то значит.
| Инструмент | PostgreSQL 18.4 | CockroachDB v26.2.5 | Picodata 26.1.3 |
|---|---|---|---|
| goose | проверенные шаги прошли | проверенные шаги прошли* | не может начать: журнал не создаётся |
| Flyway | шаги прошли; откат — только в платной редакции | шаги прошли*; откат — только в платной редакции | не может начать |
| Liquibase | проверенные шаги прошли, включая откат | проверенные шаги прошли*, включая откат | не может начать |
| Atlas | шаги прошли, включая инспекцию; откат — не в открытой сборке | журнал создан, миграции не применяются | не может начать |
| Alembic | проверенные шаги прошли, включая сравнение с моделью | не может начать: не разбирает версию сервера | не может начать |
«Прошли» здесь значит ровно одно: успешные операции — создать таблицу, добавить колонку, откатить её обратным DDL — дали в базе то состояние, о котором сообщил инструмент. Звёздочка у CockroachDB — оговорка, ради которой ниже есть отдельный раздел: если миграция упадёт на середине, там она ведёт себя иначе, чем на PostgreSQL.
На PostgreSQL, как и положено контролю, отказов по существу нет: только откаты, которые бесплатные поставки не дают. Дальше — интереснее.
Picodata: ни один не дошёл до первой миграции
Picodata принимает соединения по PostgreSQL-протоколу, но SQL у неё разбирает и планирует собственный распределённый движок sbroad. Пять инструментов упали пятью разными способами — и ни разу на пользовательской миграции:
| Инструмент | На чём упал | Ответ Picodata |
|---|---|---|
| goose | создание собственной таблицы goose_db_version с GENERATED BY DEFAULT AS IDENTITY |
sbroad: rule parsing error |
| Flyway | SHOW search_path — определение текущей схемы |
Unable to determine the original schema for the connection |
| Liquibase | запрос JDBC-метаданных к pg_catalog с регулярным выражением ~ '^pg_' |
sbroad: rule parsing error |
| Atlas | чтение системных переменных через current_setting |
SQL function current_setting not found |
| Alembic | select pg_catalog.version() — определение версии сервера |
sbroad: rule parsing error |
Вот как это выглядит у goose — отказ на первой же строке служебного DDL:
$ docker run --rm --network smig -v "$PWD/tools/goose/migrations:/mig" smig-goose -dir /mig \
postgres 'postgres://admin:Picodata1@pico:5432/picodata?sslmode=disable' up-to 1
2026/10/05 08:10:12 goose run: ERROR: sbroad: table with name "goose_db_version" not found (SQLSTATE XX000); ERROR: sbroad: rule parsing error: --> 2:14
|
2 | id integer PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
| ^---
|
= expected ArrayKeywordSuffix, ArrayBound, or NotFlag (SQLSTATE XX000)
Пять разных точек отказа — это и есть главный вывод про «совместимость по протоколу». Каждый инструмент опирается на свой кусок PostgreSQL за пределами протокола: один — на синтаксис identity-колонок, другой — на SHOW, третий — на конкретный запрос JDBC-драйвера к каталогам, четвёртый — на системную функцию. Проверить совместимость инструмента с базой можно только прогоном: из списка поддерживаемых базой конструкций этого не вывести. Почему Picodata устроена так и что из PostgreSQL у неё работает, разобрано в статье про границы её совместимости с PostgreSQL; миграции Picodata, которые едут вместе с плагином, — в статье про плагины на Rust.
CockroachDB: три инструмента прошли, два споткнулись о служебную часть
goose, Flyway и Liquibase на CockroachDB отработали так же, как на PostgreSQL, включая откат у тех, кому он доступен. Atlas и Alembic — нет, и оба раза причина не в миграциях.
Alembic не дошёл даже до подключения к схеме: штатный диалект postgresql в SQLAlchemy разбирает строку версии сервера, ожидая формат PostgreSQL, и строку CockroachDB разобрать не может:
$ docker run --rm --network smig -e SMIG_URL='postgresql+psycopg://root@crdb:26257/t_alembic?sslmode=disable' \
-v "$PWD/tools/alembic:/w" smig-alembic upgrade 0001 2>&1 | grep -E '^AssertionError'
AssertionError: Could not determine version from string 'CockroachDB CCL v26.2.5 (aarch64-unknown-linux-gnu, built 2026/07/28 18:55:27, go1.25.5)'
Обходной путь документирован — отдельный диалект sqlalchemy-cockroachdb. С ним Alembic применил и откатил обе миграции. Но его сравнение схемы с моделью — то самое, на котором держится autogenerate, — увидело изменения, которых нет:
$ docker run --rm --network smig -e SMIG_URL='cockroachdb+psycopg://root@crdb:26257/t_alembic_w?sslmode=disable' \
-v "$PWD/tools/alembic:/w" smig-alembic-crdb check
FAILED: New upgrade operations detected: [[('modify_type', None, 'probe', 'name', {'existing_nullable': True, 'existing_server_default': False, 'existing_comment': None}, VARCHAR(), Text())], [('modify_type', None, 'probe', 'note', {'existing_nullable': True, 'existing_server_default': False, 'existing_comment': None}, VARCHAR(), Text())]]
Колонки в модели — Text, в базе они созданы как TEXT, но CockroachDB хранит их как STRING, отражение схемы возвращает VARCHAR, и autogenerate предлагает «поменять тип» у колонок, которые совпадают с моделью. На реальной схеме это значит шумные автосгенерированные миграции, которые нужно вычищать руками, — ровно та точка отказа «интроспекция ломается первой», с которой начиналась статья.
Atlas упал с сообщением, которое ничего не объясняет:
$ docker run --rm --network smig -v "$PWD/tools/atlas/migrations:/mig" arigaio/atlas:1.3.3-community \
migrate apply --url 'postgres://root@crdb:26257/t_atlas?sslmode=disable&search_path=public' --dir file:///mig 1 2>&1 | head -1
Error: pq: unexpected transaction status idle
При этом журнал atlas_schema_revisions создан, а таблица из миграции — нет: учёт есть, применения нет. Ключ к причине — семантика DDL у CockroachDB. В версии 26.2 по умолчанию включена настройка autocommit_before_ddl: открытая транзакция молча коммитится перед DDL. Atlas применяет миграцию внутри транзакции, и сообщение драйвера — «неожиданное состояние транзакции: её нет» — согласуется с тем, что транзакция закрылась без ведома инструмента. Подтверждает это и проба ниже: стоит выключить настройку, как поведение меняется. Та же настройка делает в CockroachDB неоткатываемыми и DDL в обычных транзакциях — это разбиралось в обзоре distributed SQL.
Если выключить autocommit_before_ddl параметром подключения, первая миграция проходит, а вторая упирается в следующую особенность — новые таблицы в CockroachDB 26.2 создаются заблокированными для изменения схемы:
$ docker run --rm --network smig postgres:18.4 psql 'postgres://root@crdb:26257/t_lock?sslmode=disable' \
-tAc "CREATE TABLE probe (id INT PRIMARY KEY, name TEXT); SHOW CREATE TABLE probe"
CREATE TABLE
probe|CREATE TABLE public.probe (
id INT8 NOT NULL,
name STRING NULL,
CONSTRAINT probe_pkey PRIMARY KEY (id ASC)
) WITH (schema_locked = true);
Вне транзакции CockroachDB снимает эту блокировку сам, внутри — нет, и ALTER TABLE probe ADD COLUMN в транзакции Atlas отклоняется: this schema change is disallowed because table "probe" is locked and this operation cannot automatically unlock the table. После этого Atlas не может записать даже собственную ревизию — транзакция уже прервана. Режимы --tx-mode file и --tx-mode none на стенде не помогли: первый упирается в ту же блокировку, второй падает с исходной ошибкой ещё до миграций. Это согласуется с тарифной сеткой Atlas: CockroachDB официально поддерживается только в платной редакции Pro, а бесплатная — MySQL, MariaDB, PostgreSQL и SQLite.
Почему goose, Flyway и Liquibase прошли там, где Atlas нет? Механизм у них разный, и стенд различает его не для всех. Flyway знает об ограничении базы: на CockroachDB он сам помечает каждую миграцию [non-transactional] и выполняет её вне транзакции — так он документированно поступает с базами, где DDL нельзя держать в транзакции. Отсюда и отсутствие блокировки: ALTER идёт вне транзакции, и CockroachDB снимает schema_locked сама. goose и Liquibase на PostgreSQL выполняют миграцию в транзакции. Что происходит с этой транзакцией на CockroachDB — закрывает ли её autocommit_before_ddl перед DDL или что-то иное, — стенд напрямую не видит. Зато он видит следствие, и оно одинаково для всех трёх: многооператорная миграция на CockroachDB не атомарна. Это проверено отдельной пробой в следующем разделе. Миграция настоящей схемы с PostgreSQL на CockroachDB со всеми её особенностями — тема статьи про эксплуатацию и миграцию с PostgreSQLготовится, с 2 ноября.
Если миграция упала на середине
Главный риск мигратора — не отказ, а частичное применение: половина миграции в базе, а журнал об этом не знает. Стенд проверяет его отдельной пробой. Вторая миграция — успешный ALTER TABLE probe ADD COLUMN note, а за ним заведомо ошибочный INSERT в несуществующую таблицу. После отказа сверяются схема и журнал, затем та же миграция запускается повторно.
Колонка note после отказа |
Журнал | Повторный запуск | |
|---|---|---|---|
| goose, PostgreSQL | нет | только первая миграция | та же ошибка |
| Flyway, PostgreSQL | нет | только первая | та же ошибка |
| Liquibase, PostgreSQL | нет | только первая | та же ошибка |
| Atlas, PostgreSQL | нет | только первая | та же ошибка |
| Alembic, PostgreSQL | нет и таблицы probe |
журнала нет вовсе | та же ошибка |
| goose, CockroachDB | есть | только первая | column "note" … already exists |
| Flyway, CockroachDB | есть | первая — успех, вторая — success = false |
отказ: «Validate failed» |
| Liquibase, CockroachDB | есть | только первая | column "note" … already exists |
На PostgreSQL всё сходится: миграция откатилась целиком, журнал правдив, повтор упирается в ту же ошибку, а после её исправления пройдёт. Одно отличие — у Alembic: по умолчанию он выполняет весь upgrade head одной транзакцией, поэтому откатилась и первая миграция, вместе с таблицей журнала. Это тоже честное состояние — просто граница отката шире, чем одна миграция.
На CockroachDB схема и журнал разошлись. Колонка note в базе есть, а у goose и Liquibase в журнале вторая миграция не значится. Повторный запуск поэтому падает уже не на исходной ошибке, а на применённой половине:
$ docker run --rm --network smig -e LIQUIBASE_ANALYTICS_ENABLED=false -v "$PWD/tools/liquibase:/liquibase/changelog" smig-liquibase \
--url='jdbc:postgresql://crdb:26257/t_liquibase?sslmode=disable' --username=root --password='' \
--search-path=/liquibase/changelog --changelog-file=partial.sql update 2>&1 | grep 'Primary Reason'
ERROR: Exception Primary Reason: ERROR: column "note" of relation "probe" already exists
Flyway ведёт себя аккуратнее. Он знает, что работал без транзакции, записывает неудачную миграцию в журнал с success = false и прямо говорит, что откатывать придётся вам:
$ docker run --rm --network smig -v "$PWD/tools/flyway/partial:/flyway/sql" flyway/flyway:13.9.0 \
-url='jdbc:postgresql://crdb:26257/t_flyway?sslmode=disable' -user=root -password='' migrate 2>&1 \
| grep -E 'non-transactional|failed!'
Migrating schema "public" to version "1 - create probe" [non-transactional]
Migrating schema "public" to version "2 - add note then fail" [non-transactional]
ERROR: Migration of schema "public" to version "2 - add note then fail" [non-transactional] failed! Please restore backups and roll back database and code!
Следующий запуск Flyway откажется работать — Validate failed … Detected failed migration to version 2 — пока вы не приведёте базу в порядок и не уберёте запись о неудаче командой flyway repair. Это неудобно, но честно: состояние «половина применена» не маскируется.
Как восстанавливаться, стенд тоже проверил — на CockroachDB, штатными командами двух инструментов, у которых они есть. Путей два, и выбирать между ними нужно до того, как трогать журнал: довести изменение до полного ожидаемого результата или вернуть исходное состояние. Отметить частично выполненную миграцию «выполненной», не сделав ни того, ни другого, — значит записать в журнал неправду.
Liquibase — довести до конца. Недостающая часть changeset выполнена руками (здесь — создана таблица и вставлена строка), схема проверена, и только потом changeset отмечен выполненным штатной командой:
$ docker run --rm --network smig -e LIQUIBASE_ANALYTICS_ENABLED=false -v "$PWD/tools/liquibase:/liquibase/changelog" smig-liquibase \
--url='jdbc:postgresql://crdb:26257/t_liquibase?sslmode=disable' --username=root --password='' \
--search-path=/liquibase/changelog --changelog-file=partial.sql mark-next-changeset-ran 2>&1 | grep -iE 'Marking|successfully|ERROR'
Liquibase command 'mark-next-changeset-ran' was executed successfully.
$ docker run --rm --network smig postgres:18.4 psql 'postgres://root@crdb:26257/t_liquibase?sslmode=disable' \
-tAc "SELECT string_agg(id || ':' || exectype, ' ' ORDER BY orderexecuted) FROM databasechangelog"
1:EXECUTED 2:EXECUTED
После этого update сообщает, что применять нечего (Run: 0, Previously run: 2).
Flyway — вернуть исходное. Применённая половина откачена руками (ALTER TABLE probe DROP COLUMN note), схема проверена, flyway repair убрал из журнала запись о неудаче, и исправленная миграция применилась заново:
$ docker run --rm --network smig -v "$PWD/tools/flyway/partial-fixed:/flyway/sql" flyway/flyway:13.9.0 \
-url='jdbc:postgresql://crdb:26257/t_flyway?sslmode=disable' -user=root -password='' repair 2>&1 | grep -iE 'repair|removed|ERROR'
Successfully repaired schema history table "public"."flyway_schema_history" (execution time 00:00.022s).
$ docker run --rm --network smig -v "$PWD/tools/flyway/partial-fixed:/flyway/sql" flyway/flyway:13.9.0 \
-url='jdbc:postgresql://crdb:26257/t_flyway?sslmode=disable' -user=root -password='' migrate 2>&1 | grep -E 'Migrating|Successfully applied|ERROR'
Migrating schema "public" to version "2 - add note then fail" [non-transactional]
Successfully applied 1 migration to schema "public", now at version v2 (execution time 00:00.080s)
Журнал после этого — 1:true 2:true, колонка на месте. У goose штатной команды для такой отметки нет: согласовывать его журнал приходится запросом к goose_db_version — тем же порядком, сначала схема, потом журнал.
Отсюда практические правила для базы без транзакционного DDL:
- одна миграция — один DDL-оператор уменьшает объём восстановления: «половина» миграции становится невозможной. Но разрыв между схемой и журналом остаётся — DDL может зафиксироваться, а запись в журнал не появиться, если мигратор упадёт или потеряет соединение между ними. Поэтому после любого сбоя результат DDL и запись в журнале проверяются отдельно;
IF NOT EXISTS— с осторожностью.ADD COLUMN IF NOT EXISTSиCREATE INDEX IF NOT EXISTSделают повтор возможным, но проверяют только имя: существующая колонка может иметь другой тип, индекс с нужным именем — другие колонки или без уникальности, и команда молча пропустит создание, а мигратор запишет неверное состояние как успех. Документация PostgreSQL прямо предупреждает, чтоCREATE INDEX IF NOT EXISTSне гарантирует соответствия существующего индекса тому, который был бы создан. Повтор допустим, только если определение уже существующего объекта проверено и при несовпадении миграция падает;- после отказа — сначала сверка, потом решение: довести до конца или вернуть, затем проверить схему и только после этого согласовать журнал — штатными командами (
mark-next-changeset-ranу Liquibase,repairу Flyway) или, где их нет, запросом к таблице журнала; - данные — отдельно от DDL.
INSERTилиUPDATEв той же миграции, что иALTER, на такой базе превращает частичный сбой в частичные данные.
Императивный против декларативного
Здесь полезно развести два вопроса, которые обычно смешивают: как миграцию получают и как её применяют.
Как получают. Миграцию можно написать руками — так работают goose, Flyway и Liquibase. А можно сгенерировать из разницы между желаемой схемой и текущей. Так делает autogenerate у Alembic: сравнивает модель SQLAlchemy со схемой базы и создаёт черновик обычной ревизии с upgrade() и downgrade(). Похоже устроен atlas migrate diff, который пишет разницу в версионированный файл, — с важным отличием в том, с чем сравнивают. Alembic сравнивает модель с текущей целевой базой. Atlas обычно получает исходную схему, воспроизводя каталог уже написанных миграций во вспомогательной dev-базе, то есть сравнивает желаемое состояние с тем, что должно получиться из истории, а не с тем, что лежит в проде. Поэтому дрейф целевой базы Alembic «увидит» в черновике, а atlas migrate diff — нет. Генерация в обоих случаях опирается на интроспекцию: чтобы вычислить разницу, инструмент обязан прочитать схему — целевую или воспроизведённую. Ошибка интроспекции превращается в неверный черновик — как фантомная смена типов у Alembic на CockroachDB.
Как применяют. Почти всё на стенде применяется императивно: версионированный файл или ревизия выполняются как написаны, а журнал запоминает, что уже выполнено. Сгенерированная ревизия Alembic после генерации — обычная императивная миграция, и откатывается она её же downgrade(), а не новым вычислением разницы. По-настоящему декларативное применение — когда инструмент при каждом запуске сам сравнивает базу с желаемым состоянием и приводит её к нему, — на стенде представлено только возможностью Atlas: atlas schema apply работает именно так, но в матрицу этот режим не входит.
Чем платит каждое сочетание:
- генерация экономит рутину, но надёжна ровно настолько, насколько интроспекция видит вашу схему. Сгенерированный черновик нужно читать, и вне PostgreSQL — особенно внимательно;
- императивное применение требует от базы немногого: журнал, DDL и служебные запросы самого инструмента. На стенде так применяли все пять, и различия в результатах объясняются не подходом, а диалектами, служебными запросами и обработкой транзакций у конкретных инструментов. Слабое место императивного применения — дрейф: если схему поменяли руками, журнал этого не заметит;
- декларативное применение видит дрейф и сам строит план, но каждый запуск зависит от интроспекции, а ревьюить приходится вычисленный план, а не написанный файл. Защита здесь — в процессе проверки плана:
atlas schema applyпо умолчанию показывает SQL и ждёт подтверждения, есть--dry-run. Риск растёт там, где план применяется без просмотра, — например, с--auto-approveв пайплайне. Стенд декларативный режим не сравнивал.
Вывод для выбора: чем дальше база от ванильного PostgreSQL, тем осторожнее стоит относиться ко всему, что опирается на интроспекцию. Генерацию миграций можно оставить, если каждый черновик читает человек. Декларативное применение уместно, пока интроспекция надёжна и план перед применением кто-то смотрит; без просмотра её ошибка превращается в изменение схемы.
Кто владеет вашей схемой
История миграций проекта — это два артефакта: файлы миграций в репозитории и журнал в базе. Оба привязаны к инструменту.
Формат файлов. У goose и Flyway это обычный SQL с минимальной разметкой — комментарием -- +goose Up или соглашением об именах V1__…sql. У Liquibase на стенде — formatted SQL, но в реальных проектах часто XML или YAML changelog со своим словарём операций. У Alembic миграции — Python-код против API Alembic. У Atlas — SQL плюс файл контрольных сумм atlas.sum, без которого он отказывается применять каталог.
Журнал. Таблицы журнала у всех разные и несовместимы. Сменить инструмент значит не «переложить файлы», а заново объявить новому инструменту текущее состояние как исходное (baseline) и дальше вести историю у него.
Ключевой вопрос здесь не «есть ли экспорт», а «смогу ли я применить накопленное другим инструментом». Для SQL-файлов goose и Flyway ответ — почти да: это SQL, его можно выполнить чем угодно. Для XML-changelog Liquibase и Python-миграций Alembic — нет: это программа на чужом языке операций, и её придётся переписать или конвертировать.
Что в бесплатных редакциях стоит денег
Лицензии и тарифы меняются, поэтому факты ниже проверены на 5 октября 2026 года — по самим инструментам на стенде, по файлам LICENSE в репозиториях на тегах использованных версий и по страницам вендоров.
Flyway. Исходники открытой части — Apache 2.0, но undo в бесплатной редакции нет:
$ docker run --rm --network smig -v "$PWD/tools/flyway/migrations:/flyway/sql" flyway/flyway:13.9.0 \
-url='jdbc:postgresql://pg:5432/t_flyway' -user=postgres -password='smig' undo 2>&1 | grep ERROR
ERROR: Flyway Redgate Edition Required: undo is not supported by OSS Edition
Платные редакции Teams и Enterprise продаются подпиской на пользователя — на один, два или три года; Teams ограничен 100 схемами, дальше — Enterprise (FAQ Redgate по лицензированию, лицензия открытой части).
Liquibase. С версии 5.0 сменил лицензию: на теге v5.0.4 в репозитории лежит Functional Source License 1.1 с переходом в Apache 2.0 через два года (FSL-1.1-ALv2); на v4.33.0 был Apache 2.0. Сам вендор признаёт, что FSL — не open source; issue #7374 о том, что проект продолжает называть себя открытым, на сегодня закрыт. Два практических наблюдения со стенда. Откат (rollback-count) в бесплатной поставке работает — расхожее утверждение «откат только в платном Liquibase» к 5.0.4 неверно. А официальный Docker-образ 5.0.4 поставляется без JDBC-драйверов:
$ docker run --rm --network smig -e LIQUIBASE_ANALYTICS_ENABLED=false liquibase/liquibase:5.0.4 \
--url='jdbc:postgresql://pg:5432/postgres' --username=postgres --password='smig' \
status --changelog-file=none.sql 2>&1 | grep -E 'Liquibase Version|Cannot find database driver'
Liquibase Version: 5.0.4
ERROR: Exception Primary Reason: Cannot find database driver: org.postgresql.Driver
Unexpected error running Liquibase: Cannot find database driver: org.postgresql.Driver
Драйвер ставится штатным менеджером пакетов lpm — одной строкой в Dockerfile стенда, — но пайплайн, обновивший образ с 4.x на 5.x без этого шага, сломается.
Atlas. Ядро в репозитории — Apache 2.0, и у него две поставки: открытая community-сборка и официальная. Откат в открытой сборке отсутствует:
$ docker run --rm arigaio/atlas:1.3.3-community migrate down --help 2>&1 | head -1
'atlas migrate down' is not supported by the community version.
По странице тарифов: бесплатный Starter поддерживает MySQL, MariaDB, PostgreSQL и SQLite, а остальные базы, включая CockroachDB, — только в Pro за $9 за разработчика в месяц плюс плата за использование в CI. Там же линтинг миграций — функция Pro и выше.
Из этого складываются три вопроса, которые стоит задать до того, как инструмент врастёт в пайплайн.
- За что именно платят. Платить за поддержку и сертификацию — одно. Платить за функцию, без которой ваш процесс не работает, — откат, поддержку вашей СУБД, проверку миграций в CI, — другое: вы зависите от продления подписки, а не только от бюджета.
- Что останется при отказе от платной редакции. Написанные миграции — ваши, но в формате инструмента, а журнал — в его таблице. Если процесс построен на платной функции, отказ от подписки означает перестройку процесса, а не просто смену лицензии.
- Почему мигратор дороже сменить, чем библиотеку. Библиотеку выбирает и меняет разработчик в своём сервисе. Мигратор встроен в CI и в запуск всех сервисов, его выбирает платформенная команда, и смена затрагивает все проекты сразу. Поэтому смена лицензии или вынос функции в платную редакцию уже после того, как инструмент набрал аудиторию, бьёт сильнее всего именно по миграторам.
Как выбирать
Рейтинга здесь нет: на PostgreSQL все пять справились со своей работой. Выбор определяют не возможности, а условия.
- Насколько ваша база — PostgreSQL. Если это ванильный PostgreSQL, годится любой. Если база лишь говорит на его протоколе, требования инструмента к базе важнее его возможностей: проверьте прогоном, заведёт ли он журнал, применит ли DDL и прочтёт ли каталоги — и отдельно, как он ведёт себя при сбое на середине миграции. На стенде результат решал не подход, а служебные запросы и обработка транзакций у конкретного инструмента, так что выводы по одной базе на другую не переносятся.
- Нужен ли откат как функция инструмента. У Flyway и Atlas в бесплатных поставках его нет; у goose, Liquibase и Alembic — есть. Многие команды и так откатывают «новой миграцией вперёд» — тогда этот пункт не важен.
- Опирается ли процесс на интроспекцию. Autogenerate,
schema apply, детект дрейфа удобны, но надёжны ровно настолько, насколько интроспекция видит вашу схему. Вне PostgreSQL проверяйте сгенерированные планы особенно внимательно. - Насколько вендорский формат. Чем дальше формат от обычного SQL, тем дороже обойдётся смена инструмента.
- История лицензии, а не текущий прайс. Чем глубже инструмент врастёт в пайплайн, тем внимательнее нужно смотреть, как вендор менял условия раньше, а не только что сегодня бесплатно.
Как менять саму схему под нагрузкой, не останавливая сервис, — блокирующие DDL, expand-contract, порядок деплоя кода и миграций, — разобрано в парной статье про миграции без простояготовится, с 25 октября. Тот же вопрос для CQL — в сравнении scyllamigrate и goose.
Комментарии