Порядок всегда один и тот же, и он зафиксирован в
AGENTS.md: напишите отрецензированную миграцию,
примените её против настоящего PostgreSQL, обновите снимок схемы, затем перегенерируйте sqlc.
Процесс
1
Напишите миграцию
Добавьте файл в
database/migrations/ по принятой схеме имён <UTC timestamp>_<slug>.sql и
запустите make atlas-hash. Большинство изменений здесь пишутся руками — замена ограничения
CHECK, добавление колонки с backfill, — потому что так виден замысел, которого diff не выражает.
Если у вас есть atlas login, черновик может набросать make atlas-diff: приведите
database/schema.sql к нужной форме, и Atlas запишет разницу с историей. В любом случае
коммитится файл, который вы прочитали.2
Отрецензируйте SQL
Прочитайте каждую строку, прежде чем оставить её. То, что вы ищете, перечислено ниже в разделе
что проверяют на ревью.
3
Примените её против настоящего PostgreSQL
AGENTS.md требует
этого шага до коммита файла, а не после того, как о нём сообщит CI.4
Обновите снимок схемы
pg_dump этого сервера, поэтому результат не зависит от ваших локальных клиентских утилит.
Черновик из diff на предыдущем шаге перезаписывается настоящей формой.5
Перегенерируйте sqlc
make generate, который заодно обновляет привязки Go и TypeScript, выведенные из OpenAPI.6
Закоммитьте весь набор целиком
Миграция,
atlas.sum, снимок схемы, перегенерированный internal/database/dbgen, запросы и
тесты относятся к одному коммиту. Их разделение порождает ревизию репозитория, которую нельзя
ни собрать, ни мигрировать.make atlas-diff требует atlas login: на Atlas 1.3 загрузчик желаемого состояния отвечает
extensions are available to logged-in users only, потому что схема начинается с
CREATE EXTENSION. Хеширование, валидация и применение входа не требуют, поэтому по умолчанию
миграции здесь пишутся руками.Миграции, написанные руками
Миграцию можно написать напрямую, когда так изменение выходит понятнее, чем через diff: обычный случай — замена ограниченияCHECK, а не его добавление, потому что diff выражает это как удаление
и создание без всякого указания замысла между ними.
- Добавьте файл в
database/migrations/, используя существующее именование<UTC timestamp>_<slug>.sql. - Запустите
make atlas-hash(atlas migrate hash --dir file://database/migrations). - Отрецензируйте её ровно так же, как сгенерированную, и примените против настоящей базы данных.
- Запустите
make schema-snapshotи закоммитьте вместе с ней перегенерированныйdatabase/schema.sql.
migrate, — так же, как запускаются детектор гонок и sqlc, когда
локального тулчейна не хватает:
Правило atlas.sum
database/migrations/atlas.sum генерируется только командой atlas migrate hash. Никогда не
правьте его руками, никогда не пересчитывайте запись вручную и никогда не вставляйте контрольную
сумму из лога CI.
Несовпадение контрольной суммы — это не устаревший файл. Это значит, что байты миграции изменились
после того, как её захешировали, а для уже выпущенного файла это значит, что где-то установка
применила SQL, отличный от того, который держите в руках вы. Atlas отказывается применять каталог,
чья сумма не совпадает, и в этом отказе весь смысл.
Если Atlas на вашей машине недоступен, остановитесь и скажите об этом. Записав файл самостоятельно,
вы сломаете единственный механизм, обнаруживающий переписанную миграцию.
Что проверяют на ревью
Читайте миграцию — свою или чужую — по этому списку.- Разрушающие операции.
DROP COLUMNилиDROP TABLEнельзя отменить более поздней миграцией, и данные исчезают у каждой установки, которая её применит. - Блокировки и перезаписи. Колонка, добавленная с изменчивым значением по умолчанию, смена типа
или индекс, построенный без
CONCURRENTLY, держат блокировку всё это время на таблице, которая в продакшене может быть большой. - Имена ограничений. Называйте ограничение уровня таблицы явно и уникально. PostgreSQL выводит
<table>_<column>_checkиз встроенной проверки колонки, поэтому явное ограничение, переиспользующее это имя, не применяется — и падает оно во время применения на заполненной базе, а не во время diff. - Дозаполнение данных. Миграции, добавляющей колонку
NOT NULLв заполненную таблицу, нужен либо значение по умолчанию, либо оператор дозаполнения в том же файле; матрица CI применяет каждую миграцию поверх каждой поддерживаемой базовой точки, поэтому недозаполненная колонка падает там, а не на ревью. - Уместна ли она вообще. Таблица, запрос или обработчик, до которых ничто не дотягивается, — это не задел, а незавершённая работа. Доводите возможность до конца в рамках одного изменения.
sqlc и прямые запросы
database/queries/ содержит сгенерированный sqlc доступ к данным, которым пользуются доменные
сервисы. Его входная схема — database/migrations, а не database/schema.sql, поэтому sqlc
проверяет типы каждого запроса против истории ровно так, как её проигрывает установка. Запрос,
ссылающийся на колонку, которую не добавляет ни одна миграция, оказывается ошибкой генерации, а не
времени выполнения, и это сделано намеренно.
Код клиентских поверхностей, читающий только собственные проекции — каталог, оформление заказа,
поддержка и новости в боте, — использует прямые запросы pgx в том же пакете, что и поверхность; этот
шаблон задан хранилищем бота. Добавляйте запрос в database/queries/ и перегенерируйте всякий раз,
когда результат принадлежит общему доменному сервису, а не одной поверхности.
Сгенерированный вывод в internal/database/dbgen коммитится и никогда не правится руками. CI
перегенерирует его и падает на diff.
Таблицы, доступные только на добавление
Финансовые, аудиторские, идентификационные, вебхучные записи и записи outbox доступны только на добавление, если только задокументированный жизненный цикл явно не разрешает изменение. На практике:- У книги проводок кошелька нигде в кодовой базе нет пути обновления или удаления. Исправление — это компенсирующая проводка, потому что «это было начислено, а потом отозвано, вот этим оператором, по вот этой причине» — факт, который стоит сохранить, а отредактированная строка его уничтожает. См. Финансовые инварианты.
- Журнал аудита растёт всё время жизни установки. Ретенция удаляет расходуемые строки и никогда не удаляет событие аудита.
- История прав на услуги, записи согласий, события провайдеров и история попыток выполнения заказа подчиняются тому же правилу. Там, где колонке жизненного цикла позволено меняться — статус корзины, отметка времени отзыва сессии, — заголовок миграции говорит об этом на уровне таблицы.
В
outbox_events пишут три публикатора — order.paid, wallet.credited и addon.purchased, — и
ничто в репозитории никогда не выставляет published_at. Проход ретенции удаляет только строки, у
которых published_at IS NOT NULL, поэтому он не удаляет ничего, и таблица растёт без границ.
Если вы добавляете публикатор, вы добавляете его именно в это состояние.Схема River поставляется миграцией Atlas
Таблицы устойчивых задач (river_job, river_leader, river_queue, river_notification,
river_migration) создаёт 20260905130000_river_queue.sql. Этот файл — собственная линия миграций
main River v0.43.0, версии с 1 по 7, дословно скопированная из riverpgxv5 без шаблона схемы, за
которой следуют строки river_migration, которые записал бы сам rivermigrate. Всё тело миграции
пропускается, если river_migration уже существует, поэтому установка, создавшая таблицы вручную через
CLI River, сохраняет их. См. Задачи и очереди.
Чтобы обновить River:
- Поднимите версию модуля River.
- Если новый релиз добавляет версии схемы, добавьте новую миграцию Atlas с up-SQL каждой новой
версии из собственной линии миграций River, без шаблона схемы, и следом её строку
river_migration. - Выполните
make atlas-hash.
TestRiverSchemaShipsWithTheMigrations в internal/integrationtest/harness_test.go запускает
rivermigrate против мигрированной базы данных и падает, если тот видит неприменённую версию River,
поэтому обновление без соответствующей миграции не проходит CI.
Как миграции доезжают до установки
Сервисmigrate в compose запускает arigaio/atlas:1.3.0 с
migrate apply --url postgres://omniflow:omniflow@postgres:5432/omniflow?sslmode=disable --dir file:///migrations,
монтируя database/migrations только на чтение. API, бот и воркер объявляют
depends_on: migrate: service_completed_successfully, поэтому ни один процесс не обслуживает трафик
поверх базы данных, отстающей от head.
Эта строка подключения жёстко зашита в compose.yaml и игнорирует APP_DATABASE_URL. Смена учётных
данных базы данных означает правку compose.yaml наравне с .env — иначе сервисы подключатся туда,
куда шаг миграции никогда не заглядывал.
Atlas записывает, какие ревизии были применены, в собственную схему atlas_schema_revisions.
Восстановление в свежую базу данных должно принести с собой и эту схему, иначе Atlas попытается
проиграть историю с самого начала против базы, в которой она уже есть, — это стоит знать, когда вы
следуете инструкции Резервное копирование и восстановление.
Что доказывает CI
Задачаmigrations выполняет atlas migrate validate, а затем, по одному разу на каждую
поддерживаемую базовую точку, применяет базовую точку, обновляется до head и применяет head второй
раз, чтобы доказать, что это no-op. Базовые точки — пустая база данных плюс каждый выпущенный
префикс истории, поэтому задача отвечает на вопрос, на который ревью ответить не может: сможет ли
установка, работающая на любой выпущенной версии, обновиться до этого изменения?
Набор на Testcontainers в internal/integrationtest применяет те же закоммиченные файлы по порядку
против настоящего PostgreSQL перед тем, как проверять репозитории, что делает его вторым заслоном
для миграций. Подробности — на странице Стратегия тестирования.