SQLで羃等にDBスキーマ管理ができるツール「sqldef」を作った

sqldefのリポジトリ

github.com

これは何か

Ridgepoleというツールをご存じでしょうか。 これはRubyDSLcreate_tableadd_index等を書いてスキーマ定義をしておくとそれと実際のスキーマの差異を埋めるために必要なDDLを自動で生成・適用できる便利なツールです。一方、

f:id:k0kubun:20180825111557p:plain:w400

で言われているように、Ridgepoleを動作させるためにはRubyActiveRecordといった依存をインストールする必要があり、Railsアプリケーション以外で使う場合には少々面倒なことになります。*1 *2

そこで、Pure Goで書くことでワンバイナリにし、また別言語圏の人でも使いやすいよう、RubyDSLのかわりに、誰でも知ってるSQLCREATE TABLEALTER TABLEを書いて同じことができるようにしたのがsqldefです。

使用例

現時点ではMySQLPostgreSQLに対応しているのですが、このツールはmysqlコマンドやpsqlコマンドとインターフェースを揃えるため、 それぞれのDBに対しmysqldef、psqldefという別のコマンドを提供しています。

README用にgifアニメを用意しておいたので、こちらで雰囲気を感じてください。

f:id:k0kubun:20180825111815g:plain

どうやって動いているのか

mysqldef

  1. show tables; と show create table xxx; を発行して現在のスキーマを取得
  2. 入力のDDLとインクリメンタルに比較を行ない、必要なDDLを生成
  3. 生成したDDLを実行


簡単ですね。go-sql-driver/mysqlというPure GoのMySQLのDBドライバを使っており、mysql(1)やlibmysqlclientに依存していません。

psqldef

  1. pg_dump コマンドを実行して現在のスキーマを取得
  2. 入力のDDLとインクリメンタルに比較を行ない、必要なDDLを生成
  3. 生成したDDLを実行


これもlib/pqというPostgreSQLのDBドライバがPure Goのため、こちらもpsql(1)やlibpqに依存していません。 pg_dump への依存は、クエリだけでスキーマを取れるようにし、そのうちなくせたらいいなと思ってます。

実装済の機能

  • MySQL
    • カラムの追加、変更、削除
    • テーブルの追加、削除
    • インデックスの追加、削除
    • 外部キーの追加、削除
  • PostgreSQL
    • カラムの追加、削除
    • テーブルの追加、削除
    • インデックスの追加、削除
    • 外部キーの追加、削除

あまり無駄にシミュレートをがんばりたくないのと、どうせ対応しても僕は使わないので、CREATE TABLE, CREATE INDEX, ALTER TABLE ADD INDEX の羅列以外の入力に対応していません。DROPは常に書いてあるものを消すことで生成する想定です。テーブルやカラムにリネームが必要な場合は手動でリネームを発行して --export し直す想定です。

お試しください

まだ本番じゃ全然使えないクオリティなんですが、ISUCONとかでは割と便利に使えるかもしれません。 sqldefがそのまま使えるスキーマ定義が置いてあることが多いようですし。

そういうわけで、よろしくお願いします

github.com

追記: schemalexとの比較

schemalexの作者の方にschemalexと比較して欲しいというコメントをいただいているので軽く補足します。

SQL同士を比較してSQLを生成する既存のマイグレーションツールはあるんですが、その中でもGo製でMySQL向けにスキーマ生成ができるschemalexが既にある中で何故一から作ったかというと、正直なところ完全に調査不足で羃等にスキーマ管理するツールをRidgepole以外に知らなかったことによります。

その上で、2018年8月現時点でsqldefに実装されている機能とschemalexを比較すると、それぞれ主に以下の利点があると思います。

  • schemalex
    • URL形式やgit形式同士で柔軟な比較ができ、広いユースケースをカバーしている
    • 任意の2者の比較に特化した、テストや検証向きのCLI
  • sqldef
    • PostgreSQLに対応している
    • mysql-clientをインストールしなくてもスキーマを適用できる機能をリリース済 *3
    • mysqlpsqlコマンドとほぼ同じインターフェースで使えるCLI

どれもお互い今後の開発次第で解決できる問題ですが、おそらく思想的に変わらなそうなのはそれぞれ最後のCLIに関する部分でしょう。なので、私が羃等なスキーマ管理ツールのユースケースとして想定している以下の状況における、それぞれの現時点での使い方を比較したいと思います。

  • DBサーバーのスキーマのexport
  • スキーマ変更を伴うPull Request時、Pull Requestマージ後に実行されるDDLの表示、適用
  • 最新のスキーマファイルのDBサーバーへの適用

DBサーバーのスキーマのexport

schemalex

$ echo "" | schemalex -o schema.sql - "mysql://root:@tcp(localhost:3306)/test"

BEGIN;COMMIT; が含まれているのを消す必要があるため、私が把握していないだけでより正しい方法があるかもしれません。

sqldef

$ mysqldef -uroot test --export > schema.sql

Pull Requestマージ後に実行されるDDLの表示、適用

schemalex

もともとgit-schemalexとして開発されていた機能が活用できるため、こちらはschemalexが便利なユースケースだと思います。

# PRチェックアウト時、masterのスキーマ適用のためのDDL表示
$ schemalex "mysql://root:@tcp(localhost:3306)/test" "local-git://.?file=schema.sql&commitish=master"

BEGIN;

SET FOREIGN_KEY_CHECKS = 0;

CREATE TABLE `users` (
`id` BIGINT (20) DEFAULT NULL,
`name` VARCHAR (40) DEFAULT NULL
);

SET FOREIGN_KEY_CHECKS = 1;

COMMIT;

# PRチェックアウト時、masterのスキーマの適用
$ schemalex "mysql://root:@tcp(localhost:3306)/test" "local-git://.?file=schema.sql&commitish=master" | mysql -uroot test

# マージ後に実行されるDDLの表示
$ schemalex "mysql://root:@tcp(localhost:3306)/test" schema.sql

BEGIN;

SET FOREIGN_KEY_CHECKS = 0;

ALTER TABLE `users` ADD COLUMN `created_at` DATETIME NOT NULL;

SET FOREIGN_KEY_CHECKS = 1;

COMMIT;

# マージ後のスキーマの適用
$ schemalex "mysql://root:@tcp(localhost:3306)/test" schema.sql | mysql -uroot test

sqldef

sqldefは標準でgit連携を持っていないため、mysqlコマンドが不要なかわりにgitコマンドが必要になります。*4

# PRチェックアウト時、masterのスキーマ適用のためのDDL表示
$ git show master:schema.sql | mysqldef -uroot test --dry-run
-- dry run --
CREATE TABLE users (  id bigint,  name varchar(40) DEFAULT NULL);

# PRチェックアウト時、masterのスキーマの適用
$ git show master:schema.sql | mysqldef -uroot test
-- Apply --
CREATE TABLE users (  id bigint,  name varchar(40) DEFAULT NULL);

# マージ後に実行されるDDLの表示
$ mysqldef -uroot test --dry-run < schema.sql
-- dry run --
ALTER TABLE users ADD COLUMN created_at datetime NOT NULL;

# マージ後のスキーマの適用
$ mysqldef -uroot test < schema.sql
-- Apply --
ALTER TABLE users ADD COLUMN created_at datetime NOT NULL;

なおBEGIN;COMMIT;は表示していませんが、applyはトランザクション下で行なわれます。

最新のスキーマファイルのDBサーバーへの適用

schemalex

# masterチェックアウト時、スキーマ適用のためのDDL表示
$ schemalex "mysql://root:@tcp(localhost:3306)/test" schema.sql

BEGIN;

SET FOREIGN_KEY_CHECKS = 0;

ALTER TABLE `users` ADD COLUMN `created_at` DATETIME NOT NULL;

SET FOREIGN_KEY_CHECKS = 1;

COMMIT;

# スキーマの適用
schemalex "mysql://root:@tcp(localhost:3306)/test" schema.sql | mysql -uroot test

sqldef

# masterチェックアウト時、スキーマ適用のためのDDL表示
$ mysqldef -uroot test --dry-run < schema.sql
-- dry run --
ALTER TABLE users ADD COLUMN created_at datetime NOT NULL;

$ mysqldef -uroot test < schema.sql
-- Apply --
ALTER TABLE users ADD COLUMN created_at datetime NOT NULL;

まとめ

どちらのCLIユースケースにマッチするかは要件によると思いますが、2018年8月現時点では、MySQLの用途においては実績のあるschemalexを採用するのが現実的だと思います。私自身は自分の自由が効くsqldefをMySQLでも使いメンテを続けるつもりのため、時間が経てばこの問題は解決するでしょう。

一方PostgreSQLでワンバイナリのスキーマ管理ツールが必要な場合は、PostgreSQL未対応のschemalexに対応を入れるよりはsqldefを使ってしまう方が楽かと思われます。

2021-10-30 edit: おかげ様で様々な会社様から本番利用事例をいただいております。まだまだ全てのケースに対応しているとは言えずissueベースでユースケースのカバーを続けている状態ですが、公開から3年以上経った今、schemalexやskeemaと比べても遜色ない利用実績があると言えるでしょう。

言及例:

*1:例えばRuby以外の言語でアプリを書いてCircleCIでテストする場合、CirlceCI公式のDockerイメージは普通に一つの言語しか入ってないので、アプリ用の言語とRidgepole用のRubyが両方入ったDockerイメージを自分で用意しないといけないですよね

*2:Ridgepoleの作者のwinebarrelさんにコメントをいただいてますが、現在はomnibus-rubyによってRubyを同梱したrpmdebのパッケージとしても配布されているため、バイナリをダウンロードするかわりにパッケージをダウンロードしてインストールする、ということもできそうです。一方手元でmacOSを使っていたりするとrpmdebは使えないですし、これはItamaeとmitamaeに関しても言える話ですが、一切依存のないバイナリ一つで動作する方が何かと管理が楽であろうという考えのもとこれらのツールを作っています。

*3:schemalexでもこのPR https://github.com/schemalex/schemalex/pull/52 で同様の機能が実装されると理解していますが、未マージのようです

*4:GitHubリポジトリを置いていたらCIからcloneしてくる際に必要になるので、普通は入ってるとは思います

*5:00:25:10~: "多分sqldefの方が使われていると思うんですけど" と言及いただきました