Версионирование схемы данных и миграции в YDB с использованием "goose"
Введение
Goose – open-source инструмент, который помогает версионировать схему данных в БД и управлять миграциями. Goose поддерживает множество различных баз данных, включая YDB. Goose использует файлы миграций и хранит состояние миграций непосредственно в базе данных в специальной таблице.
Установка goose
Варианты установки goose описаны в документации.
Аргументы запуска goose
Утилита goose вызывается командой:
$ goose <DB> <CONNECTION_STRING> <COMMAND> <COMMAND_ARGUMENTS>
где:
<DB>- движок базы данных, в случае YDB следует писатьgoose ydb<CONNECTION_STRING>- строка подключения к базе данных.<COMMAND>- команда, которую требуется выполнить. Полный перечень команд доступен во встроенной справке (goose help).<COMMAND_ARGUMENTS>- аргументы команды.
Строка подлкючения к YDB
Для подключения к YDB следует использовать строку подключения вида
<protocol>://<host>:<port>/<database_path>?go_query_mode=scripting&go_fake_tx=scripting&go_query_bind=declare,numeric
где:
<protocol>- протокол подключения (grpcдля незащищенного соединения илиgrpcsдля защищенного (TLS) соединения). При этом, для защищенного подключения (сTLS) следует явно подключить сертификаты YDB, например так:export YDB_SSL_ROOT_CERTIFICATES_FILE=/path/to/ydb/certs/CA.pem.<host>- адрес подключения к YDB.<port>- порт подключения к YDB.<database_path>- путь к базе данных в кластере YDB.go_query_mode=scripting- специальный режимscriptingвыполнения запросов по умолчанию в драйвере YDB. В этом режиме все запросы от goose направляются в YDB сервисscripting, который позволяет обрабатывать какDDL, так иDMLинструкцииSQL.go_fake_tx=scripting- поддержка эмуляции транзакций в режиме выполнения запросов через сервис YDBscripting. Дело в том, что в YDB выполнениеDDLинструкцийSQLв транзакции невозможно (или несет значительные накладные расходы). В частности сервисscriptingне позволяет делать интерактивные транзакции (с явнымиBegin+Commit/Rollback). Соответственно, режим эмуляции транзакций на деле не делает ничего (nop) на вызовахBegin+Commit/Rollbackизgoose. Этот трюк в редких случаях может привести к тому, что отдельный шаг миграции может оказаться в промежуточном состоянии. Команда YDB работает на новым сервисомquery, который должен помочь убрать этот риск.go_query_bind=declare,numeric- поддержка биндингов авто-выведения типов YQL из параметров запросов (declare) и поддержка биндингов нумерованных параметров (numeric). Дело в том, чтоYQL- язык со строгой типизацией, требующий явным образом указывать типы параметров запросов в теле самогоSQL-запроса с помощью специальной инструкцииDECLARE. ТакжеYQLподдерживает только именованные параметры запроса (напрмиер,$my_arg), в то время как ядроgooseгенерируетSQL-запросы с нумерованными параметрами ($1,$2, и т.д.). Биндингиdeclareиnumericмодифицируют исходные запросы изgooseна уровне драйвера YDB, что позволило в конечном счете встроиться вgoose.
В случае подключения к ломальному докер-контейнеру YDB строка подключения должна иметь вид:
grpc://localhost:2136/local?go_query_mode=scripting&go_fake_tx=scripting&go_query_bind=declare,numeric
Давайте сохраним эту строку в переменную окружения для дальнейшего использования:
export YDB_CONNECTION_STRING="grpc://localhost:2136/local?go_query_mode=scripting&go_fake_tx=scripting&go_query_bind=declare,numeric"
Далее примеры вызова команд goose будут содержать именно эту строку подключения.
Директория с файлами миграций
Создадим директорию migrations и далее все команды goose следует выполнять в этой директории:
$ mkdir migrations && cd migrations
Управление миграциями с помощью goose
Создание файлов миграций и применение их к базе
Файл миграции можно создать командой goose create:
$ goose ydb $YDB_CONNECTION_STRING create 00001_create_first_table sql
2024/01/12 11:52:29 Created new file: 20240112115229_00001_create_first_table.sql
В результате выполнения команды был создан файл <timestamp>_00001_create_table_users.sql:
$ ls
20231215052248_00001_create_table_users.sql
Файл <timestamp>_00001_create_table_users.sql был создан со следующим содержимым:
-- +goose Up
-- +goose StatementBegin
SELECT 'up SQL query';
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
SELECT 'down SQL query';
-- +goose StatementEnd
Такая структура файла миграции помогает держать в контексте внимания ровно одну миграцию - шаги, чтобы обновить состояние базы, и шаги, чтобы откатить назад измения.
Файл миграции состоит из двух секций. Первая секция +goose Up содержит SQL-команды обновления схемы. Вторая секция +goose Down отвечает за откат изменений, выполненных в секции +goose Up. Goose заботливо вставил плейсхолдеры:
SELECT 'up SQL query';
и
SELECT 'down SQL query';
Мы можем заменить эти выражения на необходимые нам SQL-команды создания таблицы users и удаления ее в случае отката миграции:
-- +goose Up
-- +goose StatementBegin
CREATE TABLE users (
id Uint64,
username Text,
created_at Timestamp,
PRIMARY KEY (id)
);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP TABLE users;
-- +goose StatementEnd
Проверим статус миграций:
$ goose ydb $YDB_CONNECTION_STRING status
2024/01/12 11:53:50 Applied At Migration
2024/01/12 11:53:50 =======================================
2024/01/12 11:53:50 Pending -- 20240112115229_00001_create_first_table.sql
Статус Pending означает, что миграция еще не применена.
Применим миграцию с помощью команды goose up:
$ goose ydb $YDB_CONNECTION_STRING up
2024/01/12 11:55:18 OK 20240112115229_00001_create_first_table.sql (93.58ms)
2024/01/12 11:55:18 goose: successfully migrated database to version: 20240112115229
Проверим статус миграций goose status:
$ goose ydb $YDB_CONNECTION_STRING status
2024/01/12 11:56:00 Applied At Migration
2024/01/12 11:56:00 =======================================
2024/01/12 11:56:00 Fri Jan 12 11:55:18 2024 -- 20240112115229_00001_create_first_table.sql
Статус Pending заменился на временную отметку Fri Jan 12 11:55:18 2024 - это означает, что миграция успешно применена. Мы также можем убедиться в этом и другими способами:

$ ydb -e grpc://localhost:2136 -d /local scheme describe users
<table> users
Columns:
┌────────────┬────────────┬────────┬─────┐
│ Name │ Type │ Family │ Key │
├────────────┼────────────┼────────┼─────┤
│ id │ Uint64? │ │ K0 │
│ username │ Utf8? │ │ │
│ created_at │ Timestamp? │ │ │
└────────────┴────────────┴────────┴─────┘
Storage settings:
Store large values in "external blobs": false
Column families:
┌─────────┬──────┬─────────────┬────────────────┐
│ Name │ Data │ Compression │ Keep in memory │
├─────────┼──────┼─────────────┼────────────────┤
│ default │ │ None │ │
└─────────┴──────┴─────────────┴────────────────┘
Auto partitioning settings:
Partitioning by size: true
Partitioning by load: false
Preferred partition size (Mb): 2048
Min partitions count: 1
Давайте создадим второй файл миграции с добавлением колонки password_hash в таблицу users:
$ goose ydb $YDB_CONNECTION_STRING create 00002_add_column_password_hash_into_table_users sql
2024/01/12 12:00:57 Created new file: 20240112120057_00002_add_column_password_hash_into_table_users.sql
Отредактируем файл <timestamp>_00002_add_column_password_hash_into_table_users.sql до следующего содержимого:
-- +goose Up
-- +goose StatementBegin
ALTER TABLE users ADD COLUMN password_hash Text;
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
ALTER TABLE users DROP COLUMN password_hash;
-- +goose StatementEnd
Проверим статус миграций:
$ goose ydb $YDB_CONNECTION_STRING status
2024/01/12 12:02:40 Applied At Migration
2024/01/12 12:02:40 =======================================
2024/01/12 12:02:40 Fri Jan 12 11:55:18 2024 -- 20240112115229_00001_create_first_table.sql
2024/01/12 12:02:40 Pending -- 20240112120057_00002_add_column_password_hash_into_table_users.sql
Мы видим, что первая миграция применена, а вторая только запланирована (Pending).
Применим вторую миграцию с помощью команды goose up-by-one (в отличие от goose uo команда goose up-by-one применяет ровно одну "следующую" миграцию):
$ goose ydb $YDB_CONNECTION_STRING up-by-one
2024/01/12 12:04:56 OK 20240112120057_00002_add_column_password_hash_into_table_users.sql (59.93ms)
Проверим статус миграций:
$ goose ydb $YDB_CONNECTION_STRING status
2024/01/12 12:05:17 Applied At Migration
2024/01/12 12:05:17 =======================================
2024/01/12 12:05:17 Fri Jan 12 11:55:18 2024 -- 20240112115229_00001_create_first_table.sql
2024/01/12 12:05:17 Fri Jan 12 12:04:56 2024 -- 20240112120057_00002_add_column_password_hash_into_table_users.sql
Обе миграции успешно применены. Убедимся в этом альтернативными способами:

$ ydb -e grpc://localhost:2136 -d /local scheme describe users
<table> users
Columns:
┌───────────────┬────────────┬────────┬─────┐
│ Name │ Type │ Family │ Key │
├───────────────┼────────────┼────────┼─────┤
│ id │ Uint64? │ │ K0 │
│ username │ Utf8? │ │ │
│ created_at │ Timestamp? │ │ │
│ password_hash │ Utf8? │ │ │
└───────────────┴────────────┴────────┴─────┘
Storage settings:
Store large values in "external blobs": false
Column families:
┌─────────┬──────┬─────────────┬────────────────┐
│ Name │ Data │ Compression │ Keep in memory │
├─────────┼──────┼─────────────┼────────────────┤
│ default │ │ None │ │
└─────────┴──────┴─────────────┴────────────────┘
Auto partitioning settings:
Partitioning by size: true
Partitioning by load: false
Preferred partition size (Mb): 2048
Min partitions count: 1
Все последующие миграции можно создавать аналогичным образом.
Откат миграции
Откатим последнюю миграцию с помощью команды goose down:
$ goose ydb $YDB_CONNECTION_STRING down
2024/01/12 13:07:18 OK 20240112120057_00002_add_column_password_hash_into_table_users.sql (43ms)
Проверим статус миграций goose status:
$ goose ydb $YDB_CONNECTION_STRING status
2024/01/12 13:07:36 Applied At Migration
2024/01/12 13:07:36 =======================================
2024/01/12 13:07:36 Fri Jan 12 11:55:18 2024 -- 20240112115229_00001_create_first_table.sql
2024/01/12 13:07:36 Pending -- 20240112120057_00002_add_column_password_hash_into_table_users.sql
Статус Fri Jan 12 12:04:56 2024 заменился на статус Pending - это означает, что последняя миграция успешно отменена. Мы также можем убедиться в этом и другими способами:

$ ydb -e grpc://localhost:2136 -d /local scheme describe users
<table> users
Columns:
┌────────────┬────────────┬────────┬─────┐
│ Name │ Type │ Family │ Key │
├────────────┼────────────┼────────┼─────┤
│ id │ Uint64? │ │ K0 │
│ username │ Utf8? │ │ │
│ created_at │ Timestamp? │ │ │
└────────────┴────────────┴────────┴─────┘
Storage settings:
Store large values in "external blobs": false
Column families:
┌─────────┬──────┬─────────────┬────────────────┐
│ Name │ Data │ Compression │ Keep in memory │
├─────────┼──────┼─────────────┼────────────────┤
│ default │ │ None │ │
└─────────┴──────┴─────────────┴────────────────┘
Auto partitioning settings:
Partitioning by size: true
Partitioning by load: false
Preferred partition size (Mb): 2048
Min partitions count: 1
"Горячий" список команд "goose"
Утилита goose позволяет управлять миграциями через командную строку:
goose status- посмотреть статус применения миграций. Например,goose ydb $YDB_CONNECTION_STRING status.goose up- применить все известные миграции. Например,goose ydb $YDB_CONNECTION_STRING up.goose up-by-one- применить ровно одну "следующую" миграцию. Например,goose ydb $YDB_CONNECTION_STRING up-by-one.goose redo- пере-применить последнюю миграцию. Например,goose ydb $YDB_CONNECTION_STRING redo.goose down- откатить последнюю миграцию. Например,goose ydb $YDB_CONNECTION_STRING down.goose reset- откатить все миграции. Например,goose ydb $YDB_CONNECTION_STRING reset.
Важно
Будьте осторожны: команда goose reset может удалить все ваши миграции, включая данные в таблицах. Это происходит за счет инструкций в блоке +goose Down. Регулярно делайте бекапы и проверяйте их на возможность восстановления, чтобы минимизировать этот риск.