Evolutionary Architectures

takezawa's blog

データベーススキーマの差分を解消するツールを作った話

この記事は Kyash Advent Calendar 2019 2日目の記事です。

今回はKyashで利用しているPostgreSQLにおいて、スキーマの差分を解消するツールを作った話についてです。

ツールのリポジトリはこちらになります。 github.com

このツールは、2つのPostgreSQLインスタンスからpg_dumpすることにより得られたDDL間の差分を検出し、差分を解消するためのDDLを生成することができます。

社内システムで活用できることを最初のゴールとして開発しました。

こんなことができます。

例えば、入力用のDDLsource.sqldesired.sqlとして用意し、 source.sqlの状態からdesired.sqlの状態に変更するためのDDLを生成してみます。

pg_dumpを使うことを想定して最低限の実装をしているので、入力用のDDLがこんな感じになっています。

-- source.sql: 変更前のスキーマを表すDDL 
CREATE TABLE public.sessions (
  id bigint,
  name character(4)
);
-- desired.sql: 変更後のスキーマを表すDDL
CREATE TABLE public.sessions (
    id bigint NOT NULL,
    key character varying(255)
);

ALTER TABLE ONLY public.sessions
    ADD CONSTRAINT sessions_pkey PRIMARY KEY (id);

CREATE TABLE public.users (
    id bigint
);

CREATE SEQUENCE public.users_id_seq
    START WITH 1
    INCREMENT BY 1
    NO MINVALUE
    NO MAXVALUE
    CACHE 1;

ALTER SEQUENCE public.users_id_seq OWNED BY public.users.id;

ALTER TABLE ONLY public.users ALTER COLUMN id SET DEFAULT nextval('public.users_id_seq'::regclass);

これらを入力とし、変更するためのDDL(patch.sql)を生成します。

go run cmd/pgconverger/main.go -source "source.sql" -desired "desired.sql" > patch.sql

以下のようなDDL(patch.sql)が生成されます。

-- Table: "public"."sessions"
ALTER TABLE "public"."sessions" ALTER COLUMN "id" SET NOT NULL;
ALTER TABLE "public"."sessions" DROP COLUMN "name";
ALTER TABLE "public"."sessions" ADD COLUMN "key" character varying(255);
ALTER TABLE ONLY "public"."sessions" ADD CONSTRAINT "sessions_pkey" PRIMARY KEY ("id");

-- Table: "public"."users"
CREATE TABLE "public"."users" (
    "id" bigint
);
CREATE SEQUENCE "public"."users_id_seq"
    START WITH 1
    INCREMENT BY 1
    NO MINVALUE
    NO MAXVALUE
    CACHE 1;
ALTER SEQUENCE "public"."users_id_seq" OWNED BY "users"."id";
ALTER TABLE ONLY "public"."users" ALTER COLUMN "id" SET DEFAULT "nextval"('public.users_id_seq'::"regclass");

経緯

Kyashの開発においては今のところスキーマ変更の適用が自動化されておらず(これから対応する予定のため)、本番環境、検証環境、開発環境、などの間で微妙に差分があり、差分が日々大きくなる、というようなことが起こっています。

こういった差分が起こってしまう主な要因としては、緊急で本番環境にインデックスを追加した場合や、開発や検証の目的で検証環境を変更したあとに、ちゃんと本番に適用してなかったり、その開発項目が中断や延期されたりした場合などに多かったと思います。
そのため、データベースに関する調査改善を行うときは、そもそもスキーマの差分がどうなっているか、などを確認するところからやる必要がありましたし、積もり積もってその差分はかなり大きくなっていました。
自動化しておけばいいだけの話なのですが、すぐに困るというほどのことでもなく、差分解消してからでないと何かしら問題が残ってしまうのでなかなか改善がされなかったわけです。

個人的にDDLの自動生成らへんは興味があるトピックだったので、自分の裁量で進められるよう他の開発者にもあまり影響のないような実装方針で、業務外の時間を使ってサイドプロジェクトとして開発をすることにしました。

完遂するために心がけたこと

「Kyashのスキーマ差分解消において役立てること」を最低限のゴールとして強く意識していました。

寄り道したり欲張ったりすると機能は途方もなく増えてしまいますし、どこまで考慮すればいいのかわからなくて設計もおぼつかなくなってしまいます。例えばCREATE TABLE文にはとてつもない表現力があり、サポート対象を絞らざるを得ません(参考: CREATE TABLE - PostgreSQL 11.5文書)。

個人開発をしていると様々なことを考えまくって結局終わらない、というのがよくあったのですが、今回のプロジェクトが目的を果たせたことで明確なゴールを持つ重要性が実感できました。

既存のツールを使っていない理由

自分で作ってみたかったという理由も大きいのですが、他の理由としては以下のような機能における差分を考慮したDDLを作成できるツールがなくて、Kyashのスキーマ差分解消というゴールを果たしづらかったためです。

実装の参考にしたもの

まず「Go言語でつくるインタプリタ」が大部分で参考になりました。この本を参考にしたおかげでPrattパーサという構文解析を学べましたし、すんなりとテストコードを書きながら実装ができました。

Go言語でつくるインタプリタ

Go言語でつくるインタプリタ

  • 作者:Thorsten Ball
  • 発売日: 2018/06/16
  • メディア: 単行本(ソフトカバー)

他には以下の資料やソースコードも実装方法の参考にしています。 特にLexical Scanning in Go - Rob PikeにおいてTokenizerとLexerを別々のgoroutineとして動かすアイデアや、Lexerがstate functionを返すことでstate machineを表現するアイデアは面白く、そのまま活用させて頂きました。

あとはひたすらPostgreSQLのドキュメントを参照しました。特に以下のページを使いまくりました。

改善できそうなところ

とにかくゴールを果たすために、今回はpg_dumpで出力したDDLをソースとして入力とすることを想定していたのですが、PostgreSQLのバージョンを変えるとpg_dumpの出力も変わることがありうるため変化に弱そうです。pg_dumpの出力は互換性も考慮されているためにシンプルとは言い難いDDLとなっています。

information_schemaを入力ソースとして扱う方法も考えていたのですが、こちらのほうが良かったかもしれません。データベースに接続しなければいけませんが構文解析の必要もなくなりそうです。

あとがき

定例の1on1でCTOの椎野に相談をして、サイドプロジェクトについてコーチングをしてもらっていました。コーチングの中でゴール設定も行え、無事に作り上げることができました。感謝してもしきれません。