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 }