Королева была первым моим пет-проектом, к которому я не потерял интерес спустя месяц, который вырос из желания улучшить себе 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 Хигу и Даша отдельная благодарность в становление королевы! Без вас ее бы не было