database_rewinderにpull requestを投げた結果

Railsエンジニアの越川です。

弊社では、テストを書く際にdatabase_rewinderを使うことが多いのですが、先日困ったことがありましたのでエンジニアブログで共有します。
 

database_nameを明示したモデルで困った

今回、複数のデータベースのテーブルを結合する際に明示的にデータベース名を指定したいケースがありました。
例えば、railsでいうとhas_many through: 'hogehoge'というシーンで、データベース名を明示したいケースがありました。

対象としたいモデルには以下の様な記述をします。

class Post < ActiveRecord::Base
  self.table_name = 'my_database.posts'
end

このようなモデルのデータがdatabase_rewinderで削除されないという事象がありました。 
 

database_rewinderの仕組み

ところで、database_rewinderの仕組みはどうなっているかというと、

  • テストが実行中に、実行されるINSERT文を分析して、対象となるtableを記憶する
  • テストが終わった際に、対象となったtablesDELETE文を発行する

こうすることで、データの削除対象を最小化し高速化を実現しています。
 

原因はなにか探る

さて、お話を戻すと、先のPostモデルを操作した際、railspostsテーブルに対して発行するSQLを調査すると以下のようになっていました。

INSERT INTO `my_database`.`posts` (`created_at`, `updated_at`) 
VALUES ('2015-01-26 05:57:42', '2015-01-26 05:57:42')

そもそも、database_rewinderがこのSQLをどう解釈し、削除テーブルを記憶しているのか調べました。

すると、それっぽい正規表現を発見。

lib/database_rewinder.rb#L50

match = sql.match(/\AINSERT INTO [`"]?([^\s`"]+)[`"]?/i)
table = match[1] if match
if table
  cleaner.inserted_tables << table unless cleaner.inserted_tables.include? table
  cleaner.pool ||= connection.pool
end

なるほど、この正規表現だと、

INSERT INTO `my_database`.`posts` (`created_at`, `updated_at`) 
VALUES ('2015-01-26 05:57:42', '2015-01-26 05:57:42')

というSQLは、my_databaseにマッチしてしまいますね。原因がわかりました。
 

pull requestを出そう

というわけで、さっそくpull requestを出しました。
database_rewinderの作者は日本の松田明さんですが、OSSであれば英語でpull requestです。

そんなこともあり、勇気を出して英語で出しました。
社内でこの英語で良いかな?みたいな話を相談したら「you! その想いが大事、そのまま出しちゃいなよ」と言った意見を頂いたために割とそのまま出しました。

Support table_name with database name by ppworks · Pull Request #23 · amatsuda/database_rewinder

具体的にはこんな差分です

-  match = sql.match(/\AINSERT INTO [`"]?([^\s`"]+)[`"]?/i)
-  table = match[1] if match
+  match = sql.match(/\AINSERT INTO [`"]?([^\s`"]+)[`"]?(?:\.[`"]?([^\s`"]+)[`"]?)?/i)
+  return unless match
+
+  table = match[2] || match[1]

そして、すぐに取り込んで頂きました:)
 

さらに pull requestを出そう

最初のpull requestの中でこのようなやりとりがありました。

amatsuda commented 4 days ago
Wait, I merged this because it’s not a bad change, but asked this question because I’m still not sure if this regexp is perfect.
One actual example that I’m aware now is MS SQLServer. SQLServer accepts more than one dots in an object notation https://msdn.microsoft.com/en-us/library/ms177563.aspx
Please give me some more time to think.

なるほど、server.scheme.database.table みたいな記述もありうるのですね。

というわけで別のpull requestを出しました。

Support more than one dots in an object notation

-  match = sql.match(/\AINSERT INTO [`"]?([^\s`"]+)[`"]?(?:\.[`"]?([^\s`"]+)[`"]?)?/i)
+  match = sql.match(/\AINSERT INTO (?:\.*[`"]?([^.\s`"]+)[`"]?)*/i)
   return unless match

-  table = match[2] || match[1]
+  table = match[1]

その後、このpull reqが取り込まれ、v0.4.2がリリースされ、弊社のプロジェクト内でもrubygemsから最新バージョンのdatabase_rewinderを取り込み、今回の問題を解決出来ました。
 

最後に

マネーフォワードでは、仕事の中でも自然とOSSに貢献することを推奨しています。
仕事中でもOSSに貢献したいエンジニアを募集しています!みなさまのご応募お待ちしております!

マネーフォワード採用サイト
https://recruit.moneyforward.com/

Wantedly
https://www.wantedly.com/companies/moneyforward

Pocket