all posts

ENGINEERING NOTES

Queen v1.0.0: миграции как код

Королева была первым моим пет-проектом, к которому я не потерял интерес спустя месяц, который вырос из желания улучшить себе DX. Я никогда не любил миграции в виде SQL, хоть и понимал их удобство как для DBA, так и для CI. Собственно, когда появляется идея такого рода и желание это делать, руки сами идут и делают. Уже почти год стукнул с первых наработок, и с тех пор проект оброс многими фичами и многие потерял (как, например, поддержку NoSQL и YDB). Несколько раз пересматривал уже готовый функционал и API, некоторым командам давал её пощупать и собирал фидбек, за что им огромный поклон. Удалось познакомиться и поработать с прекрасными и сильными людьми, у которых я много чему научился, которые привнесли в королеву свой взгляд со стороны, благодаря чему библиотека щас находится именно тут.

Теперь к самой Queen!

Собственно, главная идея — миграции как код или как функции. Миграции описываются прямо в Go-коде, можно выполнять как SQL, так и Go-функции или вовсе совмещать оба подхода. Библиотеку можно встроить в приложение, тесты или отдельный бинарь для деплоя. Важно понимать, что уже применено, не изменился ли код старой миграции и что именно произойдёт при следующем запуске.

Queen хранит историю и контрольные суммы миграций, проверяет состояние базы и помогает посмотреть план до выполнения. PostgreSQL — основной production-сценарий Queen. Также есть драйверы для MySQL, SQLite, ClickHouse, CockroachDB и SQL Server — с особенностями и ограничениями для каждой базы.

Что есть в v1:

  • SQL-миграции и миграции на Go-функциях, в том числе смешанные.
  • Проверка контрольных сумм, обнаружение изменённых миграций и опасных расхождений в истории.
  • DryRun, plan и explain, чтобы заранее изучить предстоящие изменения.
  • Транзакционное выполнение миграции вместе с записью её истории в PostgreSQL.
  • Режим для отдельных нетранзакционных команд, например CREATE INDEX CONCURRENTLY, с маркером прерванной миграции и ручным восстановлением.
  • CLI, который встраивается в собственный Go-бинарник. В нём есть проверка registry, планы для деплоя, doctor, проверка rollback, поиск пропусков в истории и TUI.
  • Goose import и перенос уже применённой истории Goose в Queen.
  • Tap-события для наблюдения за выполнением миграций и SQL.

А вот и пример:

package main

import (
    "context"
    "database/sql"
    "log"
    "os"

    "github.com/dmedovich/queen"
    "github.com/dmedovich/queen/drivers/postgres"
    _ "github.com/jackc/pgx/v5/stdlib"
)

func main() {
    if err := run(context.Background()); err != nil {
        log.Fatal(err)
    }
}

func run(ctx context.Context) error {
    db, err := sql.Open("pgx", os.Getenv("DATABASE_URL"))
    if err != nil {
        return err
    }

    q := queen.New(postgres.New(db))
    defer q.Close()

    q.MustAdd(queen.M{
        Version: "001",
        Name:    "create_users",
        UpSQL:   `CREATE TABLE users (id BIGSERIAL PRIMARY KEY, email TEXT NOT NULL UNIQUE)`,
        DownSQL: `DROP TABLE users`,
    })

    return q.Up(ctx)
}

В DATABASE_URL передаём строку подключения к PostgreSQL. q.Close() закрывает и соединение с базой.

Перед выполнением можно проверить историю и получить план. Этот кусок вставляем в run перед return q.Up(ctx):

if err := q.Validate(ctx); err != nil {
    return err
}

plans, err := q.DryRun(ctx, queen.DirectionUp, 0)
if err != nil {
    return err
}

for _, plan := range plans {
    log.Printf("%s %s: %s\n%s", plan.Version, plan.Name, plan.Type, plan.SQL)
}

Для Go-миграции можно задать явную checksum, которую нужно менять вместе с логикой функции. Регистрируем её после 001, до вызова q.Up(ctx):

q.MustAdd(queen.M{
    Version:        "002",
    Name:           "normalize_emails",
    ManualChecksum: "normalize-emails-v1",
    UpFunc: func(ctx context.Context, tx *sql.Tx) error {
        _, err := tx.ExecContext(ctx,
            `UPDATE users SET email = LOWER(TRIM(email))`)
        return err
    },
})

Тут нужно заранее проверить, что после нормализации не появятся одинаковые email: иначе UNIQUE не даст выполнить миграцию, и транзакция откатится. Обратного преобразования тут нет, поэтому DownFunc не задаём.

А если нужен CREATE INDEX CONCURRENTLY, для PostgreSQL есть NonTransactional:

q.MustAdd(queen.M{
    Version:          "003",
    Name:             "index_users_email_lower",
    UpSQL:            `CREATE INDEX CONCURRENTLY users_email_lower_idx ON users (LOWER(email))`,
    DownSQL:          `DROP INDEX CONCURRENTLY users_email_lower_idx`,
    NonTransactional: true,
})

Эту миграцию тоже регистрируем до q.Up(ctx). В таком режиме в каждом направлении допускается одна SQL-команда. Если выполнение прервётся, Queen оставит маркер, а перед повторным запуском нужно проверить состояние индекса и восстановить историю через recover.

CLI для отдельного миграционного бинарника

Queen позволяет собрать миграции в отдельную программу и запускать её перед обновлением приложения:

package main

import (
    "github.com/dmedovich/queen/cli"
    "myapp/migrations"
)

func main() {
    cli.Run(migrations.Register)
}

myapp/migrations — пакет миграций в своём проекте, путь нужно заменить на свой. В нём должна быть функция Register(q *queen.Queen), которая регистрирует миграции через q.MustAdd(...).

Сохраняем этот файл в cmd/migrate/main.go и собираем бинарь:

go build -o migrate ./cmd/migrate

Типичный сценарий в CI/CD:

./migrate status --driver postgres --dsn "$DATABASE_URL"
./migrate validate --driver postgres --dsn "$DATABASE_URL"
./migrate plan --driver postgres --dsn "$DATABASE_URL"
./migrate up --driver postgres --dsn "$DATABASE_URL" --yes

Главные киллер-фичи Queen перед тем же гусем или golang-migrate — это checksum drift, планы, диагностика, embedded CLI.

Более подробней можете глянуть на сайте и исходниках:

P.S Хигу и Даша отдельная благодарность в становление королевы! Без вас ее бы не было

all posts