Презентацию можно посмотреть тут - slides.pdf
Типичный пайплайн в компаниях для разработчиков миграций состоит из:
Проблема заключается во втором этапе. SQL-инструкции очень тяжело читается, а ревью для разработчиком - не их первостепенная работа. Поэтому со временем пайплайн деградирует - ревьюверы начинают невдумчиво читать код, из-за чего миграции могут принести вред базе
Тогда появляется потребность эти миграции тестировать
Самый простой тест - сделать миграцию up, потом откатную миграцию down и надеяться на ошибку от интерпретатора СУБД. Но, если в таком примере миграции
-- up
ALTER TYPE mood ADD VALUE 'happy' AFTER 'ok';
-- down
сделать миграцию up, миграцию down (она пуста, так как PostgreSQL не поддерживает удаление значения из перечисления), а потом снова up - то мы получим ошибку, так как такое значение уже есть
Поэтому можно сделать два вывода:
Накат up-down не проверяет идемпотентность миграций
Миграция down важнее up, так как она отвечает за приведение к инвариантному, рабочему состоянию базы данных
Второй способ - Staircase-тест:
Если миграций четыре, то база данных пройдет через миграции так:
Такой подход проверяет идемпотентность, но не приведение к инварианту. Пример:
-- up
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_enum
WHERE enumlabel = 'happy'
AND enumtypid = 'mood'::regtype
) THEN
ALTER TYPE mood ADD VALUE 'happy' AFTER 'ok';
END IF;
END;
$$;
-- down
Такая миграция при откате не удалит 'happy'
из перечисления
Поэтому стоит проверять сами состояния базы данных. Для этого при первом накате будем делать снапшоты схем базы данных снапшоты схем. Снапшоты схем можно делать при помощи специальных служебных таблиц, которые есть в каждой базе данных
Если снапшоты после up-down равны, то инвариант соблюдается и миграция работает. Для проведения Staircase-теста можно воспользоваться утилитой seqwall
Пока что все тесты миграции мы проводили со схемой базы данных, но не с данными. Взглянем на этот пример:
-- up
ALTER TABLE users
ALTER COLUMN updated_at TYPE timestamp
USING updated_at::timestamp;
-- down
ALTER TABLE users
ALTER COLUMN updated_at TYPE text
USING updated_at::text;
Здесь текст, содержащий дату и время, кастится к встроенному типу timestamp
. Однако, если в базе дата и время записывается как 2025-03-14 12:00:00+03
, то при касте информация о часовом поясе +03
теряется, поэтому корректнее использовать каст к timestamptz
Чтобы выявлять подобные ошибки, надо наполнить тестовую базу данных какими-то данными. Можно:
Генерировать данные, например, с помощью faker, greenmask, hypothesis, regex
Это зачастую не репрезентативно, сгенерированные ранее данные устаревают. Очень сложно работать с внешними фидами - когда типы и данные предоставляются third-party сервисами. Также это практически невозможно на больших схемах
Сэпмлировать данные, то есть брать их из базы данных в продакшене
Для этого нужна инфраструктура. Зачастую некоторые данные нельзя передать (например, данные паспорта). Хорошо сэмплировать, то есть подобрать репрезентативную выборку, довольно сложно
Поэтому генерация используется, если:
А сэмплы, когда:
Теперь мы можем разделить миграции на:
Теперь перед каждой up-миграцией можем сделать снапшот данных (например, с помощью pg_dump
), затем после up-down сравнивать снапшот. Таким образом,
Теперь заметим другое: каст делается для всех значений атрибута, поэтому нужна глобальная блокировка таблицы (Access Exclusive Lock). В продакшене такая миграция вызовет задержку всех запросов. Однако это можно отследить: все блокировки записываются в таблицу pg_locks
, а из таблицы pg_stat_activity
можно получить дополнительную информацию
Также блокировки можно отлавливать при помощи утилиты squawk
При написании миграций рекомендуется использовать линтеры (например, SQLFluff или Sqruff) и форматтеры
Резюме: