> ## Documentation Index
> Fetch the complete documentation index at: https://omniflow-vpn.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Изменения базы данных

> Схема-первый процесс на Atlas и sqlc, как миграция рецензируется и применяется и какие таблицы нельзя переписывать никогда

PostgreSQL — единственный устойчивый источник истины в Omniflow. Valkey держит кеш, ограничения
частоты запросов, блокировки и временное состояние, и ничто значимое не переживает только там.
Из-за этого изменение схемы — одна из немногих правок в этом репозитории, которая после релиза
действительно необратима, и поэтому описанный ниже процесс предписывающий, а не рекомендательный.

Три артефакта двигаются вместе:

| Артефакт | Что это |
| - | - |
| `database/schema.sql` | **Текущая форма**: дамп только схемы базы данных, которую строят миграции, сгенерированный `tools/schema-snapshot.sh`. Читайте его, чтобы увидеть таблицу, не проигрывая тридцать файлов; никогда не редактируйте его руками. CI падает, если он устарел. |
| `database/migrations/` | **Выпущенная история**: по одному `.sql`-файлу с меткой времени на каждое изменение плюс `atlas.sum`. Именно это каждая установка проигрывает по порядку и всегда. |
| `database/queries/` | **Слой запросов**, который sqlc компилирует в `internal/database/dbgen`. |

Порядок всегда один и тот же, и он зафиксирован в `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**

```bash theme={null}
export APP_DATABASE_URL=postgres://omniflow:omniflow@localhost:5432/omniflow?sslmode=disable
make atlas-apply   # atlas migrate apply --env local
```

Миграция, которая только компилируется, не проверена. Конфликты имён, конфликты ограничений и
ошибки в порядке колонок проявляются при применении и больше нигде, поэтому `AGENTS.md` требует
этого шага до коммита файла, а не после того, как о нём сообщит CI.

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

```bash theme={null}
make schema-snapshot   # sh tools/schema-snapshot.sh
```

Скрипт применяет все миграции к одноразовому PostgreSQL 18 и снимает схему собственным
`pg_dump` этого сервера, поэтому результат не зависит от ваших локальных клиентских утилит.
Черновик из diff на предыдущем шаге перезаписывается настоящей формой.

**5. Перегенерируйте sqlc**

```bash theme={null}
go tool sqlc generate
```

Или `make generate`, который заодно обновляет привязки Go и TypeScript, выведенные из OpenAPI.

**6. Закоммитьте весь набор целиком**

Миграция, `atlas.sum`, снимок схемы, перегенерированный `internal/database/dbgen`, запросы и
тесты относятся к одному коммиту. Их разделение порождает ревизию репозитория, которую нельзя
ни собрать, ни мигрировать.

> [!NOTE]
>
> `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, когда
локального тулчейна не хватает:

```bash theme={null}
docker run --rm -v "$PWD/database/migrations:/migrations" arigaio/atlas:1.3.0 \
  migrate hash --dir file:///migrations
```

Каждая миграция в репозитории начинается с текстового заголовка, объясняющего, что она добавляет,
чего намеренно не добавляет и какие из её таблиц доступны только на добавление. Следуйте этому
соглашению: заголовок — единственное место, где будущий читатель узнает, зачем существует колонка, и
его читают гораздо чаще, чем сообщение коммита.

## Правило `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 доступны только на
добавление, если только задокументированный жизненный цикл явно не разрешает изменение. На практике:

* **У книги проводок кошелька нигде в кодовой базе нет пути обновления или удаления.** Исправление —
  это компенсирующая проводка, потому что «это было начислено, а потом отозвано, вот этим
  оператором, по вот этой причине» — факт, который стоит сохранить, а отредактированная строка его
  уничтожает. См. [Финансовые инварианты](https://omniflow-vpn.mintlify.site/ru/commerce/financial-invariants.md).
* **Журнал аудита** растёт всё время жизни установки. Ретенция удаляет расходуемые строки и никогда
  не удаляет событие аудита.
* **История прав на услуги, записи согласий, события провайдеров и история попыток выполнения
  заказа** подчиняются тому же правилу. Там, где колонке жизненного цикла позволено меняться —
  статус корзины, отметка времени отзыва сессии, — заголовок миграции говорит об этом на уровне
  таблицы.

Добавляя таблицу, решите, к какому из этих случаев она относится, и запишите решение в
комментарий-заголовок. Таблица, изменяемость которой никто не оговорил, становится таблицей, которую
кто-нибудь отредактирует.

> [!NOTE]
>
> В `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, сохраняет их. См. [Задачи и очереди](https://omniflow-vpn.mintlify.site/ru/operations/jobs-and-queues.md#схема-очереди-поставляется-вместе-с-миграциями).

Чтобы обновить 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 попытается
проиграть историю с самого начала против базы, в которой она уже есть, — это стоит знать, когда вы
следуете инструкции [Резервное копирование и восстановление](https://omniflow-vpn.mintlify.site/ru/operations/backup-restore.md).

## Что доказывает CI

Задача `migrations` выполняет `atlas migrate validate`, а затем, по одному разу на каждую
поддерживаемую базовую точку, применяет базовую точку, обновляется до head и применяет head второй
раз, чтобы доказать, что это no-op. Базовые точки — пустая база данных плюс каждый выпущенный
префикс истории, поэтому задача отвечает на вопрос, на который ревью ответить не может: сможет ли
установка, работающая на любой выпущенной версии, обновиться до этого изменения?

Набор на Testcontainers в `internal/integrationtest` применяет те же закоммиченные файлы по порядку
против настоящего PostgreSQL перед тем, как проверять репозитории, что делает его вторым заслоном
для миграций. Подробности — на странице [Стратегия тестирования](https://omniflow-vpn.mintlify.site/ru/contributing/testing.md).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.