April 26

Миграции БД с Claude Code — Alembic, Prisma, TypeORM

Миграция — самое опасное изменение в проекте. Неправильный ALTER TABLE блокирует таблицу на часы. Пропущенный downgrade превращает откат в рулетку. Claude Code умеет генерировать миграции, но требует ревью с упором на безопасность.

Канал с гайдами и контентом по claude code, выкладываем новости (когда режут лимиты в 10 раз) и какие инструменты через claude реализуем для проектов, канал: https://t.me/claudedevolper

CLAUDE.md для миграций

## Правила миграций

### Нельзя в одном релизе
- Добавить NOT NULL без default
- Переименовать колонку без deprecation-фазы
- Удалить колонку, которую ещё читает прод-код
- CREATE INDEX без CONCURRENTLY на таблице > 1M строк

### Обязательно
- Каждая миграция — атомарна (одно изменение)
- downgrade() всегда описан
- Для Postgres: CONCURRENTLY для индексов, CHECK NOT VALID для ограничений
- Проверка на staging с копией прод-данных

### Workflow
1. Изменил модель → сгенерировал миграцию
2. Ревью диффа — особенно op.drop_column
3. Прогон на dev/staging
4. Deploy: миграция СНАЧАЛА, потом новый код (или наоборот — зависит от изменения)

Alembic (SQLAlchemy)

Генерация:

alembic revision --autogenerate -m "add phone to users"

Результат:

# migrations/versions/2026_04_17_add_phone.py
from alembic import op
import sqlalchemy as sa

revision = "e7a2b8c1f9d0"
down_revision = "a3b2c1d4e5f6"

def upgrade():
    op.add_column(
        "users",
        sa.Column("phone", sa.String(20), nullable=True),
    )
    op.create_index(
        "idx_users_phone",
        "users",
        ["phone"],
        postgresql_concurrently=True,
    )

def downgrade():
    op.drop_index("idx_users_phone", table_name="users")
    op.drop_column("users", "phone")

Запуск:

alembic upgrade head
alembic downgrade -1        # откат на одну
alembic history --verbose

Prisma (Node.js)

npx prisma migrate dev --name add_phone_to_users

Prisma сравнит schema.prisma с БД и сгенерит SQL. Пример:

-- migration.sql
ALTER TABLE "users" ADD COLUMN "phone" TEXT;
CREATE INDEX "users_phone_idx" ON "users"("phone");

Для прода — prisma migrate deploy (без генерации, только apply).

Откат Prisma не поддерживает «из коробки» — пишешь новую миграцию, которая восстанавливает состояние.

Zero-downtime добавление NOT NULL

Три деплоя вместо одного:

Шаг 1 — колонка nullable + default в приложении:

def upgrade():
    op.add_column("users", sa.Column("phone", sa.String(20), nullable=True))

Шаг 2 — backfill в фоне:

UPDATE users SET phone = '' WHERE phone IS NULL;
-- лучше батчами по 10K строк

Шаг 3 — NOT NULL:

def upgrade():
    op.alter_column("users", "phone", nullable=False, server_default="")

Безопасное переименование колонки

Нельзя просто op.alter_column("users", "email", new_column_name="email_address") — старый код сломается.

Схема:

1. Добавить email_address + триггер, копирующий из email 2. Задеплоить код, который пишет в обе, читает из email_address 3. Backfill существующих записей 4. Задеплоить код, который пишет только в email_address 5. Удалить email

Проверка миграции перед применением

# Показать SQL без выполнения
alembic upgrade head --sql > up.sql

# Prisma
npx prisma migrate diff \
  --from-schema-datamodel schema.prisma \
  --to-schema-datasource schema.prisma \
  --script > up.sql

Чтение up.sql глазами — обязательный этап для прод-миграции.

CONCURRENTLY в PostgreSQL

-- НЕ CREATE INDEX idx_users_email ON users(email);
CREATE INDEX CONCURRENTLY idx_users_email ON users(email);

Без CONCURRENTLY — эксклюзивный лок на всю таблицу. На миллионной таблице — минуты простоя.

В Alembic:

op.create_index(
    "idx_users_email",
    "users",
    ["email"],
    postgresql_concurrently=True,
)

Нужен autocommit_block:

def upgrade():
    with op.get_context().autocommit_block():
        op.create_index(
            "idx_users_email", "users", ["email"],
            postgresql_concurrently=True,
        )

Подводные камни

  • autogenerate не видит ENUM-значения. Добавление варианта в ENUM — руками.
  • Foreign key без индекса. DELETE по parent таблице превращается в seq scan по child.
  • Prisma шизу делает при ручных правках SQL. Не трогай миграционный файл после создания.
  • alembic downgrade не всегда обратим. Если в upgrade был DROP COLUMN — данные уже не вернуть.

Как попробовать

1. Добавь в CLAUDE.md секцию миграций 2. Сгенерируй тестовую миграцию 3. Прогони --sql чтобы увидеть реальный SQL 4. Примени на staging → убедись, что downgrade работает

Канал с гайдами и контентом по claude code, выкладываем новости (когда режут лимиты в 10 раз) и какие инструменты через claude реализуем для проектов, канал: https://t.me/claudedevolper