Skip to main content
PostgreSQL — единственный устойчивый источник истины в Omniflow. Valkey держит кеш, ограничения частоты запросов, блокировки и временное состояние, и ничто значимое не переживает только там. Из-за этого изменение схемы — одна из немногих правок в этом репозитории, которая после релиза действительно необратима, и поэтому описанный ниже процесс предписывающий, а не рекомендательный. Три артефакта двигаются вместе: Порядок всегда один и тот же, и он зафиксирован в 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

Обновите снимок схемы

Скрипт применяет все миграции к одноразовому PostgreSQL 18 и снимает схему собственным 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 выражает это как удаление и создание без всякого указания замысла между ними.
  1. Добавьте файл в database/migrations/, используя существующее именование <UTC timestamp>_<slug>.sql.
  2. Запустите make atlas-hash (atlas migrate hash --dir file://database/migrations).
  3. Отрецензируйте её ровно так же, как сгенерированную, и примените против настоящей базы данных.
  4. Запустите make schema-snapshot и закоммитьте вместе с ней перегенерированный database/schema.sql.
Ни один из этих шагов не требует Atlas CLI на вашей машине. Оба запускаются из того же образа, что использует compose-сервис 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 применяет каждую миграцию поверх каждой поддерживаемой базовой точки, поэтому недозаполненная колонка падает там, а не на ревью.
  • Уместна ли она вообще. Таблица, запрос или обработчик, до которых ничто не дотягивается, — это не задел, а незавершённая работа. Доводите возможность до конца в рамках одного изменения.
Никогда не редактируйте миграцию, попавшую в релиз, и никогда не генерируйте diff во время запуска в продакшене. Продакшен применяет только закоммиченные миграции.

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:
  1. Поднимите версию модуля River.
  2. Если новый релиз добавляет версии схемы, добавьте новую миграцию Atlas с up-SQL каждой новой версии из собственной линии миграций River, без шаблона схемы, и следом её строку river_migration.
  3. Выполните 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 перед тем, как проверять репозитории, что делает его вторым заслоном для миграций. Подробности — на странице Стратегия тестирования.