Spanner で 羃等スキーマ管理をするツール hammer の話

Cloud Spanner でも schemalex 見たいなスキーマ管理がしたかったので作った :tools: 

https://github.com/daichirata/hammer

スキーマ管理ツールはだいたい二種類あって、ActiveRecord見たいな変更差分のDDLをどんどん書いていって順番よく適用していく奴と、schemalexやRidgepoleみたいなテーブル定義のファイルから差分を出して都度適用していく奴があってこのツールは後者の方。

Spannerのスキーマ管理ツールを作る上で多分考える必要がありそうな以下の２点にも対応している。

- Interleave
- Not Null

Interleaveに関しては単純にテーブル定義でツリーを作り、自身が再作成でしか適用できない差分だった場合(主キーの変更やPARENTの変更)に子を全て削除してから自分を削除する。

Not Nullに関しては一度Nullableなカラムを追加した後にPartitionedUpdateでデフォルト値を設定してNot Nullを再設定する力技で解決している。

どちらもリリース済みの本番環境で気軽に行えることではなかったりするので注意が必要だけど、開発ではかなり役立つと思う。その他にもSTRING BYTE変換や長さ制限の変更など、ALTER出来る差分に関してはその様に適用してくれる。(そもそもSpannerはAlterで出来ることが大分少ないけど...)

## 使い方

```
$ hammer -h
hammer is a command-line tool to schema management for Google Cloud Spanner.

Usage:
  hammer [command]

Examples:

* Export spanner schema
  hammer export spanner://projects/projectId/instances/instanceId/databases/databaseName > schema.sql

* Apply local schema file
  hammer apply spanner://projects/projectId/instances/instanceId/databases/databaseName /path/to/file

* Create database and apply local schema (faster than running database creation and schema apply separately)
  hammer create spanner://projects/projectId/instances/instanceId/databases/databaseName /path/to/file

* Copy database
  hammer create spanner://projects/projectId/instances/instanceId/databases/databaseName1 spanner://projects/projectId/instances/instanceId/databases/databaseName2

* Compare local files
  hammer diff /path/to/file /another/path/to/file

* Compare local file against spanner schema
  hammer diff /path/to/file spanner://projects/projectId/instances/instanceId/databases/databaseName

* Compare spanner schema against spanner schema
  hammer diff spanner://projects/projectId/instances/instanceId/databases/databaseName1 spanner://projects/projectId/instances/instanceId/databases/databaseName2

Available Commands:
  apply       Apply schema
  create      Create database and apply schema
  diff        Diff schema
  export      Export schema
  help        Help about any command

Flags:
  -h, --help   help for hammer

Use "hammer [command] --help" for more information about a command.
```

これが

```sql
CREATE TABLE users (
  user_id STRING(36) NOT NULL,
) PRIMARY KEY(user_id);
```

こうして

```sql
CREATE TABLE users (
  user_id STRING(36) NOT NULL,
  age INT64,
  name STRING(MAX) NOT NULL,
) PRIMARY KEY(user_id);
CREATE INDEX idx_users_name ON users (name);
```

こうなる。

```sql
$ hammer diff old.sql new.sql

ALTER TABLE users ADD COLUMN age INT64
ALTER TABLE users ADD COLUMN name STRING(MAX)
UPDATE users SET name = '' WHERE name IS NULL
ALTER TABLE users ALTER COLUMN name STRING(MAX) NOT NULL
CREATE INDEX idx_users_name ON users(name)
```

### Data Source

データの取得先はSourceという概念で抽象化されていて、SpannerとSpannerのDiffを取ったりSpannerのDBの定義を元にして別のDBを作成とかも出来たりする。

Spannerの接続先はこんな感じのフォーマットで、credentialsは省略するとApplication Defaultが使用される。

```
spanner://projects/{projectId}/instances/{instanceId}/databases/{databaseName}?credentials=/path/to/file.json
```

現在はローカルファイルとSpannerからのスキーマ取得に対応していて、必要に応じていろいろなData Sourceを追加できる様にしている。

### create

最初はdiffとapplyだけのツールにしようと思っていたんだけど、SpannerはDBの作成と一緒にスキーマ定義を渡すとDB作成と同時にスキーマを適用してくれるAPIになっており、スキーマ定義なしでDB作成を行った場合とほぼ同程度の時間でスキーマを適用することが出来るのでコマンドとして用意した。

なのでCIの実行毎に１からDBを作成する際などにスキーマ適用時間(MySQLとは比べ物にならないほど長い)分まるっと短縮できるのでもし機会があれば活用して欲しい。


## スキーマ管理で困っていたら是非

1ヶ月くらい実際のチーム開発とCIでガンガン使ってスキーマの更新をかけてみたけど問題ないので、もしスキーマ管理で困っていることがあれば是非試してみてください。