Перевірка міграцій БД «up → down без слідів» у CI: пакети Migration Checker для Symfony та Laravel
Вітаю! Мене звати Олександр Рослов. Я програміст із
Про що і для кого стаття
У цій статті я коротко розповідаю:
- яку проблему міграцій я закриваю і чому вона часто «не болить», доки не стане пізно;
- як працює підхід «up → down → schema diff» як автоматична перевірка якості;
- що саме роблять мої пакети Migration Checker для Symfony та Laravel і як їх запускати локально та в CI;
- які типові фейли проходять код-рев’ю, і як їх ловить автоматична перевірка.
Чому вирішив написати? Бо бачу, що тема «міграції як артефакт, який треба тестувати» досі недооцінена: часто ми рев’юємо міграції очима, але не маємо механічної гарантії, що відкат поверне схему в нульовий стан. А саме це відрізняє «міграції працюють» від «міграції безпечні».
Стаття буде корисна:
- PHP-розробникам на Symfony/Laravel, які регулярно додають міграції;
- тим, хто займається код-рев’ю і хоче менше «людського фактора»;
- DevOps/Tech Lead’ам, які відповідають за стабільні деплої та rollback;
- командам з raw SQL у міграціях (тригери, індекси, constraints), де ризики вищі.
У більшості команд міграції сприймаються як «код, що еволюціонує схему». Але з практики проблема часто не в up, а в тому, що down не повертає базу в початковий стан: лишаються таблиці, індекси, тригери, «тимчасові» колонки, зміни default-значень, коментарі або інші артефакти. Це може довго не проявлятися — аж поки не знадобиться rollback, відкат релізу або прогін міграцій у чистому середовищі.
Саме через це я зробив набір пакетів Migration Checker, який формалізує просте правило: після up → down стан схеми має бути ідентичним, і це має перевірятися автоматично (локально та в CI).

Чому я взагалі до цього дійшов
Класичний біль міграцій виглядає так:
up()додає колонку/індекс/constraint/тригер, аdown()забуває його прибрати.down()«відкотив» таблицю, але лишились супутні об’єкти або зміни метаданих.- У CI ми проганяємо міграції «тільки вгору», але не перевіряємо, що rollback реально повертає схему назад.
І тут вступає в гру код-рев’ю. Бо реальність така: навіть сильні рев’ювери не тримають у голові всі нюанси БД, і в PR легко пропустити дрібні, але критичні деталі.
Приклади з код-рев’ю: що найчастіше прослизає
Нижче — типові ситуації, які я бачу на рев’ю міграцій (і через які, власне, й народився Migration Checker).
1) «Колонку видалили — значить все ок»... але індекс лишився
На рев’ю зазвичай дивляться на головну дію: «додали колонку — прибрали колонку». Але індекс міг створюватися:
- окремою командою;
- у сирому SQL;
- або автоматично через ORM-хелпер.
В результаті down() колонку прибрав, а індекс — ні. І поки ніхто не робить rollback — все тихо.
Як це виглядає в рев’ю:
«Так,
dropColumn()є. LGTM».
Як це виглядає при реальному відкаті:
«Relation already exists / index already exists / cannot drop column because...»
2) «Додали FK» — але відкат не симетричний
У up() легко додати foreign key, але в down():
- забути
dropForeign; - переплутати ім’я constraint’а;
- або відкотити таблицю, залишивши constraint (залежить від СУБД і сценарію).
На рев’ю часто не ловиться, бо назви constraint’ів в різних командах можуть генеритись по-різному, а людина не завжди запускає міграцію локально «вгору-вниз».
3) «Тригер/функція» через raw SQL — і down() пустий
Все, що виходить за межі стандартного DSL міграцій, на рев’ю стає ризикованим:
CREATE TRIGGER,CREATE FUNCTION,- специфічні типи (наприклад, enum/типи в Postgres).
Часто down() або взагалі пропускають, або пишуть «потім доробимо».
На рев’ю це виглядає як:
«Так, воно треба для бізнес-логіки, давайте мерджити, rollback не плануємо».
А потім rollback «раптом» планується.
4) «Rename» — один з найпідступніших сценаріїв
Перейменування таблиці/колонки вгору може бути коректним, але відкат:
- повертає ім’я, але забуває про індекси;
- або індекси повертаються з іншими назвами;
- або ORM генерує різні імена залежно від версії.
Це складно виявити очима в PR, бо треба «симулювати» кінцеву схему.
5) «Змінили default / nullability / charset / collation» — а назад не повернули
Міграція може міняти:
DEFAULT;NOT NULL;charset/collation;- коментарі;
- порядок колонок (інколи важливо);
- типи (наприклад,
int→bigint).
На рев’ю такі правки виглядають дрібними, але down() часто роблять «спрощеним»: повернули тип — і забули default/collation.
6) «Дропнули таблицю в down()» як універсальний відкат
Це взагалі окремий жанр:
up()додає кілька змін;down()простоdropTable()«щоб гарантовано відкотити».
Таке часто проходить рев’ю, бо «працює». Але воно несиметричне і може:
- знести таблицю, яка існувала до міграції (якщо помилились контекстом);
- або знищити дані в dev/stage, де відкат роблять частіше.
Як працює Migration Checker
Алгоритм простий:
- snapshot-схеми;
up;down;- snapshot-схеми;
- diff;
upі далі по списку.
Якщо після rollback лишився хоч один об’єкт — інструмент показує різницю і падає.

Як це допомагає саме в процесі код-рев’ю
Я сприймаю Migration Checker як «другу пару очей», яка:
- не втомлюється;
- не пропускає дрібниці;
- не покладається на «я впевнений, що down ок»;
- економить час код-рев’ю.
І практично це змінює рев’ю так:
- Рев’ювер більше фокусується на сенсі міграції (чи правильно ми міняємо домен), а не на «чи всі дрібні об’єкти видалені».
- Якщо в PR є raw SQL — checker стає обов’язковою страховкою.
- Зникає клас «LGTM, але down потім» — бо CI просто не пропустить.
Встановлення та локальний запуск
Symfony
composer require --dev roslov/migration-checker-bundle php bin/console migration-checker:check --env=test -vv
Laravel
composer require --dev roslov/laravel-migration-checker php artisan migration-checker:check --env=testing -vv
Підключення до CI
Нижче наведу приклад перевірки міграцій на Jenkins’і:
// Підготовлюємо тестове середовище
sh """
docker stop ${service}-test-db || true
docker rm --force ${service}-test-db || true
docker network rm ${service}-network || true
docker network create ${service}-network
"""
// Запускаємо тестову базу
sh """
docker run --name ${service}-test-db --network=${service}-network -d --rm \
-e MYSQL_ROOT_PASSWORD=rootpass \
-e MYSQL_DATABASE=${service}_test \
-e MYSQL_USER=${service}testuser \
-e MYSQL_PASSWORD=${service}testpass \
mysql:8.4.5 \
--character-set-server=utf8mb4 \
--collation-server=utf8mb4_0900_ai_ci \
--log_bin_trust_function_creators=1 \
--explicit_defaults_for_timestamp=0
"""
// Очікуємо, доки база буде готова приймати запити
sh """
while ! docker exec ${service}-test-db \
mysql --user=${service}testuser --password=${service}testpass \
-e 'SELECT 1' \
>/dev/null 2>&1; do
echo 'Waiting for database connection...'
sleep 1
done
"""
if (service == 'service1') {
// Запускаємо перевірку міграцій для сервіса `service1`.
// В цьому випадку це Symfony-мікросервіс, в якому ми прописали підключення до тестової бази
// в файлі `.env.test`
sh """
docker run --network=${service}-network --rm \
${service}-migration-check \
php bin/console migration-checker:check --env=test -vv
"""
}
if (service == 'service2') {
// Запускаємо перевірку міграцій для сервіса `service2`.
// В цьому випадку це Laravel-мікросервіс, в якому ми НЕ прописали підключення до тестової бази
// в файлі `.env.testing`, тому використовуємо змінну середовища для підключення
sh """
docker run --network=${service}-network --rm \
-e DB_URL="mysql://${service}testuser:${service}testpass@${service}-test-db:3306/${service}_test
?serverVersion=8.4.5&charset=utf8mb4" \
${service}-migration-check \
php artisan migration-checker:check --env=testing -vv
"""
}
// Зупиняємо тестове середовище
sh """
docker stop ${service}-test-db
docker network rm ${service}-network
"""
Де подивитися й спробувати
- roslov/migration-checker-bundle — додає консольну команду для перевірки міграцій в Symfony.
- roslov/laravel-migration-checker — додає консольну команду для перевірки міграцій в Laravel.
- roslov/migration-checker — в цьому пакеті знаходиться основна бізнес-логіка.
Пакети наближаються до стабільних релізів та вже «прикручені» до CI кількох комерційних проєктів. Буду вдячний за коментарі та знайдені баги.
Сподобалась стаття? Підписуйтесь на автора, щоб отримувати сповіщення про нові публікації на пошту.
4 коментарі
Додати коментар Підписатись на коментаріВідписатись від коментарів