くりにっき

フルスタックキュアエンジニアです

Railsでmysql-clientとmysql-serverのバージョンが食い違ってハマった

GitLab CIでRailsアプリをお手軽CI開発する - Tech Inside Drecom で書ききれなかったおまけです。

tl;dr

あらすじ

GitLab CIでRailsアプリをお手軽CI開発する - Tech Inside Drecom を書いてる時に .gitlab-ci.yml

services:
  - mysql:5.5

って書いていたのになぜか activerecordmysql 5.7以降でしか使えないDDLを発行してmigrationが失敗するという事象が発生。

エラーの状況

migrationファイル

こんな感じのmigrationを実行したかった

class TableRenaemStatusToProfile < ActiveRecord::Migration
  def change
    rename_table :user_statuses, :user_profiles
  end
end

ローカル (MySQL 5.5)

問題なし

Migrating to TableRenaemStatusToProfile (20140126044757)
   (25.6ms)  RENAME TABLE `user_statuses` TO `user_profiles`
   (13.4ms)  CREATE UNIQUE INDEX `index_user_profiles_on_user_id`  ON `user_profiles` (`user_id`)
   (8.7ms)  DROP INDEX `index_user_statuses_on_user_id` ON `user_profiles`

リネーム対象のテーブルにあったindexも新テーブル名に即したindex名に変更された。(activerecord賢い)

GitLab CI

シンタックスエラー(;´Д`)

Mysql2::Error: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'INDEX `index_user_statuses_on_user_id` TO `index_user_profiles_on_user_id`' at line 1: ALTER TABLE `user_profiles` RENAME INDEX `index_user_statuses_on_user_id` TO `index_user_profiles_on_user_id`

user_statusesuser_id というカラムにunique indexが貼られていたため rename_table 内でindex名変更のDDLが発行されていたのですが、MySQL 5.7以降でしか使えない RENAME INDEX が発行されてエラーになってる模様。いやお前5.5だろ。。。

activerecordどの〜!

原因

原因はactiverecordmysql serverのバージョンではなく、Dockerイメージで使ってるlibmysqlclientのバージョンが5.7で、activerecordはそこからMySQLのバージョンを取得していたためでした。*1

ためしにビルドスクリプトデバッグログを出したところ、activerecord上は確かに5.7でした

+ bundle exec rails r 'puts ActiveRecord::Base.connection.send(:full_version)'
5.7.12

今回使ってたDockerのイメージの中を調べたらlibmysqlclientとかのバージョンとも完全一致

$ docker run -i -t drecom/ubuntu-ruby bash
root@8c92dcb22de8:/# dpkg -l | grep mysql
ii  libmysqlclient-dev                   5.7.12-0ubuntu1                     amd64        MySQL database development files
ii  libmysqlclient20:amd64               5.7.12-0ubuntu1                     amd64        MySQL database client library
ii  libqt4-sql-mysql:amd64               4:4.8.7+dfsg-5ubuntu2               amd64        Qt 4 MySQL database driver
ii  mysql-client                         5.7.12-0ubuntu1                     all          MySQL database client (metapackage depending on the latest version)
ii  mysql-client-5.7                     5.7.12-0ubuntu1                     amd64        MySQL database client binaries
ii  mysql-client-core-5.7                5.7.12-0ubuntu1                     amd64        MySQL database core client binaries
ii  mysql-common                         5.7.12-0ubuntu1                     all          MySQL database common files, e.g. /etc/mysql/my.cnf

MySQLのバージョンによるmigrationの挙動の違い

activerecordのソースを読んだところ、MySQL 5.7以降だと RENAME INDEX を使って、MySQL 5.6以下だと CREATE INDEX してから DROP INDEX しているようでした

対処法

冒頭に書いた通りRails 5系では治ってます。Rails 4系でこの問題に直面したら config/initializers/activerecord_mysql_version_patch.rb のようなモンキーパッチを入れれば治ります

# activerecordでクライアントではなくサーバのMySQLのバージョンを使用するためのモンキーパッチ
# via. https://github.com/rails/rails/commit/977ffe880624bbd05f5ee1cc6e4fa51a999884ab

current_version = ActiveRecord.version
if Gem::Version.new("5.0.0.beta1") <= current_version
  raise "This monkeypatch's lifetime is over. Check ActiveRecord version."
end

module ActiveRecord
  module ConnectionAdapters
    class Mysql2Adapter < AbstractMysqlAdapter
      def full_version
        @full_version ||= @connection.server_info[:version]
      end
    end
  end
end

Rails 5系では治っているので、Rails 5系に上げた後にモンキーパッチが残り続けないようにわざとエラーを投げるようにしています。(いわゆる「モンキーパッチの賞味期限」というやつ)

モンキーパッチの賞味期限は下記のスライドが詳しいです

https://speakerdeck.com/eagletmt/activerecord-3-dot-2-4-dot-1?slide=26

まとめ

mysql clientとmysql serverのバージョンが食い違うことは早々ないと思いますが上記のようなやり方で回避は可能です

これはGitLab CI以外にも、Dockerコンテナ採用しているWerckerでも同じ現象が起こりそうな予感。

kamipoさんには足を向けて眠れません m(_ _)m https://github.com/rails/rails/commit/977ffe880624bbd05f5ee1cc6e4fa51a999884ab

*1:activerecord 5系だとmysql serverからバージョンを取得している