Files
tver/internal/store/migrate.go
T

74 lines
2.0 KiB
Go

package store
import (
"context"
"embed"
"fmt"
"io/fs"
"sort"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
//go:embed migrations/*.sql
var migrations embed.FS
const migrationLockID int64 = 0x747665725f646231
// Migrate は同じ接続で排他ロックを保持し、未適用の SQL を順に実行する。
func Migrate(ctx context.Context, pool *pgxpool.Pool) error {
conn, err := pool.Acquire(ctx)
if err != nil {
return fmt.Errorf("migration connection: %w", err)
}
defer conn.Release()
if _, err := conn.Exec(ctx, "SELECT pg_advisory_lock($1)", migrationLockID); err != nil {
return fmt.Errorf("migration lock: %w", err)
}
defer func() {
// 接続を返す前にロックを解放する。解放に失敗した接続は閉じる。
if _, err := conn.Exec(context.Background(), "SELECT pg_advisory_unlock($1)", migrationLockID); err != nil {
conn.Conn().Close(context.Background())
}
}()
if _, err := conn.Exec(ctx, `CREATE TABLE IF NOT EXISTS schema_migrations (
version TEXT PRIMARY KEY,
applied_at TIMESTAMPTZ NOT NULL DEFAULT now()
)`); err != nil {
return fmt.Errorf("migration metadata: %w", err)
}
files, err := fs.Glob(migrations, "migrations/*.sql")
if err != nil {
return err
}
sort.Strings(files)
for _, name := range files {
tx, err := conn.Begin(ctx)
if err != nil {
return err
}
var applied bool
err = tx.QueryRow(ctx, "SELECT EXISTS (SELECT 1 FROM schema_migrations WHERE version=$1)", name).Scan(&applied)
if err == nil && !applied {
var sql []byte
sql, err = migrations.ReadFile(name)
if err == nil {
_, err = tx.Exec(ctx, string(sql), pgx.QueryExecModeSimpleProtocol)
}
if err == nil {
_, err = tx.Exec(ctx, "INSERT INTO schema_migrations (version) VALUES ($1)", name)
}
}
if err != nil {
_ = tx.Rollback(ctx)
return fmt.Errorf("migration %s: %w", name, err)
}
if err := tx.Commit(ctx); err != nil {
return fmt.Errorf("migration %s commit: %w", name, err)
}
}
return nil
}