# Миграции

> Планирование, применение и доказательство изменений схемы.

Source: https://ormgo.vercel.app/ru/docs/migrations/
Symbols: https://ormgo.vercel.app/api/orm.txt — the generated list of every exported name.

---
## Модель

В режиме managed декларации — это желаемое состояние. `makemigrations` сравнивает их с состоянием, которое описывают существующие артефакты миграций, — **не** с живой базой — и пишет разницу артефактом.

Разница существенна: планирование по живой базе дало бы миграцию, зависящую от той базы, на которой её планировали.

```bash
orm makemigrations                   # спланировать и записать
orm makemigrations --dry-run --sql   # показать SQL, ничего не писать
orm makemigrations --check           # упасть, если что-то не спланировано
orm migrate                          # применить
orm migrate --plan                   # показать, что будет применено
orm showmigrations                   # что применено, что ожидает
```

## Артефакты переносимы

Миграция — это JSON с описанием операций, а не SQL-скрипт. Два следствия:

- Она одинаково воспроизводится на любом поддерживаемом мажоре PostgreSQL.
- В ней нет ничего серверно-локального: ни OID, ни имени базы, ни версии сервера, ни разобранного сервером определения, ни абсолютных путей. Артефакт с любым из этого сходится на машине, где его написали, и больше нигде.

## Транзакции

Каждая миграция применяется в одной транзакции вместе с записью в свою историю. Упавшая миграция откатывается и **не записывается** — то есть неудачный запуск оставляет базу ровно такой, какой она была, а повторный запуск падает так же, а не применяется наполовину.

```text
Applying 0002_add_orders ... FAILED

orm migrate: migration 0002_add_orders failed at operation 1
(alter column public.orders.total: type text -> numeric); the transaction was
rolled back and the migration is not recorded: ERROR: column "total" cannot be
cast automatically to type numeric (SQLSTATE 42804)
```

Обратите внимание, что здесь произошло: планировщик спланировал, а отказал **PostgreSQL**. Библиотека не придумывает выражение `USING`, чтобы такая миграция прошла, — это было бы решением инструмента за вас о том, что делать со строками, которые не конвертируются.

## Разрушающие изменения проходят через шлюз

Удаление колонки или таблицы не планируется молча. Шлюз существует потому, что цена ошибочного `DROP` неограниченна, а цена лишнего подтверждения — одна команда.

## Чего миграции не делают

Две замороженные границы:

- **Миграции не создают схемы.** `CREATE SCHEMA` — ваш.
- **Миграции не создают домены и расширения.** По той же причине.

Это предпосылки, а не изменение схемы, и притворяться иначе значило бы делать `orm migrate` командой для суперпользователя.

## Данные и аварийный выход в сырой SQL

Порождённая миграция описывает операции над схемой, и в `create table` или
`add column` строкам места нет. Но это не вся правда, а документация до сих пор
позволяла думать, что вся.

`orm makemigrations --empty` создаст её за вас, чтобы вы правили файл, а не
выдумывали его с нуля:

```console
$ orm makemigrations --empty --name seed_tags
wrote migrations/0002_seed_tags.json

Fill in Up with the SQL to run, and Down with the SQL that undoes it.
```

Она пишется независимо от того, менялись ли модели: данные — ровно тот случай,
которого не видит сравнение схем. Оставленная заготовка не «ничего не делает», а
бросает исключение, поэтому созданная и забытая миграция упадёт, а не будет
записана как применённая.

Артефакт — это JSON, а операция называется `raw_sql`:

```json
{
  "op": "raw_sql",
  "args": {
    "Up": "INSERT INTO user_tags (text) VALUES ('music'), ('sports') ON CONFLICT (text) DO NOTHING",
    "Down": "DELETE FROM user_tags WHERE text IN ('music', 'sports')",
    "Atomic": true,
    "Description": "seed the starting tags"
  }
}
```

`Down` необязателен, и именно его отсутствие делает операцию необратимой: это
сказано прямо, а не подделано пустышкой, которая якобы что-то откатила.
`Atomic` говорит, можно ли выполнять это внутри транзакции.

Это же делает возможным изменение колонки в три шага, недостижимое ни для одного
инструмента, который умеет только схему:

1. добавить колонку как nullable;
2. `raw_sql`, чтобы её заполнить;
3. поставить `NOT NULL`.

Движок не разбирает SQL, поэтому `raw_sql` ничего не меняет в состоянии миграций
и объявляет себя разрушительной операцией — осторожное предположение о том, что
прочитать нельзя. Если ваш SQL всё-таки меняет схему, добавьте рядом
`state_only`, чтобы состояние осталось правдой; иначе следующий план попытается
внести это изменение снова.

Начальные данные — случай попроще и тот же механизм. Где им место, в миграции или
в отдельном шаге, — это настоящий выбор: миграция выполняется по разу на базу и
просматривается вместе с тем изменением схемы, к которому относится, а файл
начальных данных, выполняемый при каждом развёртывании, требует
`ON CONFLICT DO NOTHING` и уникального ограничения, по которому это сработает.
Справочные данные, без которых схема бессмысленна, относятся к миграции.
Удобные фикстуры разработчика — нет.

## Материализованные представления

Материализованное представление хранит строки, вычисленные по телу запроса, поэтому смена этого тела — не изменение колонки: после неё строки просто неверны. Планировщик отказывается менять определение молча и просит написать явную миграцию.

Индексы на матпредставлении планируются отдельно от самого отношения — именно это делает пригодность к конкурентному обновлению фактом, который генератор может записать. См. [Представления](/ru/docs/views/).

## Проверка в CI

```bash
orm makemigrations --check   # за каждой декларацией стоит миграция
orm check --generated        # закоммиченный сгенерированный код актуален
```

Первая падает, когда кто-то поменял структуру и забыл спланировать. Вторая — когда спланировал и забыл перегенерировать.

## Разобранные примеры

### Безопасное добавление колонки

Добавить nullable-колонку — мгновенно. Добавить `NOT NULL` без умолчания —
переписать таблицу и заблокировать запись на это время, поэтому это три миграции,
а не одна:

```go
// 1. Добавить nullable.
Currency *string

// 2. Заполнить, вне миграции, пачками.
// 3. Затем сделать NOT NULL.
Currency string `orm:"default:'EUR'"`
```

`orm makemigrations --dry-run --sql` показывает, что из этого PostgreSQL сделает
дёшево, — до того как вы узнаете это на проде.

### Переименование без простоя

Планировщик видит удалённую колонку и добавленную, а не переименование, а
удаление колонки удаляет её данные. Добавить, писать в обе, заполнить, удалить —
четыре выката:

```bash
orm makemigrations --dry-run --sql   # прочитайте, прежде чем поверить
```

### Проверка, что выкат завершён

```bash
orm showmigrations         # что применено, что ожидает
orm migrate --plan         # что именно сделает следующий запуск
orm check --generated      # закоммиченный код соответствует схеме
```

### Шлюз в CI

```yaml
- run: orm makemigrations --check   # декларация, которую никто не спланировал
- run: orm check --generated        # план, под который никто не перегенерировал
```

Первая падает, когда структуру изменили и забыли. Вторая — когда спланировали и
забыли перегенерировать. Вдвоём они не дают трём представлениям разойтись.
