💭

私が spanner-cli をフォークした理由: spanner-mycli の紹介

2024/12/01に公開

この記事は apstndb Advent Calendar の2日目の記事かつ Spanner Advent Calendar の1日目の記事です。

Spanner ユーザの皆さんは spanner-cli はお使いでしょうか。この記事では私が自分用に開発している spanner-cli のフォークである apstndb/spanner-mycli について紹介をします。

  • TL;DR
    • 変化し続ける私の私による私のためのツールがほしくなったからフォークした。

spanner-cli とは何か

spanner-cli はターミナル上で Spanner の SQL を実行できる対話型ツールです。現在 Google Cloud 所属の yfuruyama さんにより作られ、 Cloud Spanner Ecosystem GitHub organization 以下で開発が続けられています。

README.md から引用すると、下記のような対話型操作で Spanner のクエリ/DML/DDL を実行できるだけでなく、データベース内のテーブルを列挙する SHOW TABLES; のような便利なコマンドを持っています。

$ spanner-cli -p myproject -i myinstance -d mydb
Connected.
spanner> CREATE TABLE users (
      ->   id INT64 NOT NULL,
      ->   name STRING(16) NOT NULL,
      ->   active BOOL NOT NULL
      -> ) PRIMARY KEY (id);
Query OK, 0 rows affected (30.60 sec)

spanner> SHOW TABLES;
+----------------+
| Tables_in_mydb |
+----------------+
| users          |
+----------------+
1 rows in set (18.66 msecs)

spanner> INSERT INTO users (id, name, active) VALUES (1, "foo", true), (2, "bar", false);
Query OK, 2 rows affected (5.08 sec)

spanner> SELECT * FROM users ORDER BY id ASC;
+----+------+--------+
| id | name | active |
+----+------+--------+
| 1  | foo  | true   |
| 2  | bar  | false  |
+----+------+--------+
2 rows in set (3.09 msecs)

Spanner には Google Cloud 公式の Web UI である Cloud Console に含まれる Spanner Studio や公式の CLI ツールである gcloud に含まれる gcloud spanner databases execute-sql コマンドなどのクエリ実行手段が提供されています。しかし、他の RDBMS でも一般的な対話型ツールは公式に提供されていません。そこで、コミュニティで改善可能な OSS かつ他の RDBMS でも一般的な対話型ツールという特徴を持った spanner-cli は非常に重要なツールであると言えます。

spanner-cli と私

Spanner は新しいデータベースであり、サービスに存在する機能に対してどのような UI があるべきかというのは公式も十分に提示できていません。
本当に必要なものはコミュニティで用意する必要があるため、私も実行計画に関するツールをいくつか考えた後に、 spanner-cli クエリ最適化に関する機能をはじめ、いくつかのコントリビューションをしました。実行計画に関する試みの一部は下記の記事にまとめられています。

https://engineering.mercari.com/blog/entry/20201210-cloud-spanner-query-plan/

事実としてはオーナーの次に貢献したコントリビューターであったようです。

https://github.com/cloudspannerecosystem/spanner-cli/graphs/contributors

私しか必要ではなさそうなマイナーなユースケースについては自作のコマンドラインツールの execspansql などに追加していましたが、 spanner-cli は Spanner ユーザが欲しいであろう機能を私が実装する場所として第一の選択肢であり続けました。

A rolling stone gathers no moss

今年私は前半休んでいたため後半になってコントリビューションを再開する中で、今まであまり手をつけていなかったプロンプト周りなどを提案しました。
その中で、今の spanner-cli は「新しい設定を導入しないこと」、「挙動を変えないこと」がかなり重要視されるようになってきていることに気がつきました。

spanner-cli は2024年12月現在 v0.10.8 であり README.md にはこのような免責事項が書いてありますが、実情 spanner-cli に求められているものは既にこれを超えているのだという予想がつきます。

Do not use this tool for production databases as the tool is still alpha quality.

思えば、 Spanner は既に金融業界の中でも最も要件が厳しい銀行法が適用される みんなの銀行という事例 をはじめとしたエンタープライズユーザを多く抱えています。
Spanner には枯れた対話型ツールが必要であるということは間違いありません。

その反面、 Spanner は変化が激しいソフトウェアです。BigQuery 上にあるリリースノートのデータセットを使って数えると、今年の実績として週に1回以上のリリースがあるようです。

$ bq query --project_id apstndb-sandbox --nouse_legacy_sql \
   'SELECT COUNT(DISTINCT published_at) AS release_days
    FROM `bigquery-public-data.google_cloud_release_notes.release_notes`
    WHERE product_name LIKE "%Spanner%" AND published_at >= DATE "2024-01-01"'
+--------------+
| release_days |
+--------------+
|           51 |
+--------------+

量だけではなく質の面でも、今年はかなり大きなアップデートが多かったように思います。

これらをはじめとして、まだどう対話型ツールで対応するのが理想であるか分からないような機能もあります。
目新しい機能はユーザからのニーズも聞こえてくることが少なく、ある程度の仕様変更を覚悟しなくては机上の空論では有用性を証明することも難しいでしょう。

よく引用される英語のことわざに "a rolling stone gathers no moss"、「転がる石には苔が生えない」というものがあります。
このことわざにはポジティブとネガティブ2つの意味があるとされています。

https://ja.wiktionary.org/wiki/a_rolling_stone_gathers_no_moss

(主に英国文化圏の解釈)世の中に合わせ行動を軽々しく変える人は結局成功しない。
(主に米国文化圏の解釈)世の中に合わせて、柔軟に行動が変わることにより、失敗を避けることができる。

変化をし続けるものと、変化を少なくとどめるもののどちらが良いのではなく、どちらも受け手の誰かにとって良いものである。
そう考えると変化し続ける「転がる石のソフトウェア」と安定を提供する「苔がむすソフトウェア」の両方存在するのが健全ではないでしょうか。

ということで、私は spanner-cli をフォークして自分なりの機能を実装したバージョン、 spanner-mycli をメンテナンスしてみることにしました。

spanner-mycli の Goal/non-Goal

Goal

私という想定ユーザが満足できるツールにする

私は Spanner のドキュメントを一通り読んだ上で更新を一通り追い続けており、一番面倒なユーザの一人であるという自負があります。
よって、他のユーザへのヒアリングを待たなくても大体のことはとりあえず暫定解が出せるので、とりあえず私が満足できるように開発すれば機能を実装することはできるでしょう。

例えば、 Protocol Buffers 関係の機能については Spanner Studio では未対応、 gcloud コマンドや spanner-cli など他のツールでは使うことがかなり難しいものとなっています。
この難しさを解消するため、Spanner インスタンス側とローカルそれぞれに登録済みのメッセージの型名を列挙できるコマンドを追加したり、spanner-mycli の実行途中に新しい proto descriptor ファイルを読み込むなど、積極的に自分がほしいと思った機能を実装しています。

https://github.com/apstndb/spanner-mycli/blob/fcbc3a896f0adbff60cf0873d37ffad52709ff18/README.md#protocol-buffers-support

$ spanner-mycli --proto-descriptor-file=testdata/protos/order_descriptors.pb 
Connected.
spanner> SHOW LOCAL PROTO;
+---------------------------------+-------------------+--------------------+
| full_name                       | package           | file               |
+---------------------------------+-------------------+--------------------+
| examples.shipping.Order         | examples.shipping | order_protos.proto |
| examples.shipping.Order.Address | examples.shipping | order_protos.proto |
| examples.shipping.Order.Item    | examples.shipping | order_protos.proto |
| examples.shipping.OrderHistory  | examples.shipping | order_protos.proto |
+---------------------------------+-------------------+--------------------+
4 rows in set (0.00 sec)

spanner> SET CLI_PROTO_DESCRIPTOR_FILE += "testdata/protos/query_plan_descriptors.pb";
Empty set (0.00 sec)

spanner> SHOW LOCAL PROTO;
+----------------------------------------------------------------+-------------------+-----------------------------------------------+
| full_name                                                      | package           | file                                          |
+----------------------------------------------------------------+-------------------+-----------------------------------------------+
| examples.shipping.Order                                        | examples.shipping | order_protos.proto                            |
| examples.shipping.Order.Address                                | examples.shipping | order_protos.proto                            |
| examples.shipping.Order.Item                                   | examples.shipping | order_protos.proto                            |
| examples.shipping.OrderHistory                                 | examples.shipping | order_protos.proto                            |
| google.protobuf.Struct                                         | google.protobuf   | google/protobuf/struct.proto                  |
| google.protobuf.Struct.FieldsEntry                             | google.protobuf   | google/protobuf/struct.proto                  |
| google.protobuf.Value                                          | google.protobuf   | google/protobuf/struct.proto                  |
| google.protobuf.ListValue                                      | google.protobuf   | google/protobuf/struct.proto                  |
| google.protobuf.NullValue                                      | google.protobuf   | google/protobuf/struct.proto                  |
| google.spanner.v1.PlanNode                                     | google.spanner.v1 | googleapis/google/spanner/v1/query_plan.proto |
| google.spanner.v1.PlanNode.Kind                                | google.spanner.v1 | googleapis/google/spanner/v1/query_plan.proto |
| google.spanner.v1.PlanNode.ChildLink                           | google.spanner.v1 | googleapis/google/spanner/v1/query_plan.proto |
| google.spanner.v1.PlanNode.ShortRepresentation                 | google.spanner.v1 | googleapis/google/spanner/v1/query_plan.proto |
| google.spanner.v1.PlanNode.ShortRepresentation.SubqueriesEntry | google.spanner.v1 | googleapis/google/spanner/v1/query_plan.proto |
| google.spanner.v1.QueryPlan                                    | google.spanner.v1 | googleapis/google/spanner/v1/query_plan.proto |
+----------------------------------------------------------------+-------------------+-----------------------------------------------+
15 rows in set (0.00 sec)

spanner> CREATE PROTO BUNDLE (`examples.shipping.Order`);
Query OK, 0 rows affected (6.34 sec)

spanner> SHOW REMOTE PROTO;
+-------------------------+-------------------+
| full_name               | package           |
+-------------------------+-------------------+
| examples.shipping.Order | examples.shipping |
+-------------------------+-------------------+
1 rows in set (0.94 sec)

spanner> ALTER PROTO BUNDLE INSERT (`examples.shipping.Order.Item`);
Query OK, 0 rows affected (9.25 sec)

spanner> SHOW REMOTE PROTO;
+------------------------------+-------------------+
| full_name                    | package           |
+------------------------------+-------------------+
| examples.shipping.Order      | examples.shipping |
| examples.shipping.Order.Item | examples.shipping |
+------------------------------+-------------------+
2 rows in set (0.82 sec)

データベース以下の機能であればスコープ内とする

Spanner のリソースモデルはプロジェクトの下にインスタンスがあり、インスタンスの下にデータベースがあるという形になっています。
spanner-mycli はデータベース以下をスコープとして扱うものと定義し、これ以下の機能は対応しない理由を考えないことにすることで最終的には何かしらの機能がある状態にできるはずです。

  • REST Resource: v1.projects.instances.databases
  • REST Resource: v1.projects.instances.databases.backupSchedules
  • REST Resource: v1.projects.instances.databases.databaseRoles
  • REST Resource: v1.projects.instances.databases.operations
  • REST Resource: v1.projects.instances.databases.sessions

対話型ツール内で一通りの機能に対応することで Spanner の検証や機能のショーケースにも使えることが期待できます。

Spanner JDBC driver とできるだけ合わせる

Google によって開発された OSS として公開されている Spanner JDBC driver には多くの機能がコマンドとして実装されています。
このモデルは既に独自コマンドを持つ spanner-cli とも親和性が高く、 Partitiond Query のように spanner-cli に未実装な機能もあるため実装する上での指針となります。
同じ機能をわざわざ違う形で実装する必要はないので、原則として Spanner JDBC driver と同じコマンドに対応していく方針を決めました。

汎用的な概念で拡張性を確保する

今まで spanner-cli では新しい機能を実装する場合は専用のコマンドラインフラグを追加するか、専用のコマンドを追加する必要がありました。
変化を許容すると言っても Spanner の機能が増えるにつれて専用のフラグやコマンドを増やしていては認知負荷は上がり続けてしまいます。
そこで、 psql コマンドの \set に着想を得た汎用的なシステム変数の概念を導入することにし、構文は前述のように Spanner JDBC driver のプロパティのものを採用することとしました。

これにより spanner-cli ではほぼ全ての設定は起動時にしか設定できませんでしたが、起動途中でも更新が可能が可能になり、プロンプトへの表示も可能になりました。

$ spanner-mycli --set CLI_PROMPT="spanner> "   
# or 
$ spanner-mycli --prompt="spanner> "   
Connected.
spanner> SET CLI_PROMPT = "%p:%i:%d(%{RPC_PRIORITY})> ";
Empty set (0.00 sec)

emulator-project:emulator-instance:emulator-database(MEDIUM)> SET RPC_PRIORITY = "HIGH";
Empty set (0.00 sec)

emulator-project:emulator-instance:emulator-database(HIGH)> 

対話型ツールとしての理想を求める

個人的に UI 周りは苦手意識があったため spanner-cli のコントリビュータとしてはあまり変更を加えてはいませんでしたが、spanner-mycli のオーナーとして対話型ツールとして不満がある挙動については積極的に変えていくこととしました。
例えば、 spanner-cli は複数行編集に対応していない chzyer/readline を使っていたため、複数行の SQL の編集は前の行に戻ることや履歴を扱うことができず困難でした。

https://github.com/chzyer/readline/issues/212

spanner-mycli ではまず Go で使える複数行対応の readline ライブラリの代替(reeflective/readline, hymkor/go-multiline-ny )を試した上で issue を上げてみたところ、 hymkor/go-multiline-ny はすぐに対応していただけて致命的なブロッカーがなくなったため採用することとなりました。

できるだけ再利用可能な実装にする

spanner-mycli を拡張していく上で、可能な限り spanner-mycli の main パッケージではなく再利用可能な形で実装をし、ドッグフーディングをするようにしています。

趣味プロジェクトとして開発する

趣味を兼ねて自由に開発できる個人プロジェクトということで、プロダクト開発や他にオーナーが居るような OSS であったらレビューで弾かれるような実験を積極的にすることにしています。
例えば Go 1.23 iterator を濫用してみたり、 encoding/json の次世代のプロトタイプとして開発されている go-json-experimental/json を取り入れてみるなどです。

non-Goal

spanner-mycli が転がる石であり続けるためにはやることだけでなくやらないことも決める必要があります。
現時点では次のようなものを定めています。

品質を優先しない

工学的にどうすれば品質を高められるかや、問題が再発しないようにすることは一定検討しますが、バグが発生することや設計が破綻することを恐れて機能拡張を止めることはしないようにします。
むしろ破綻が判明するところまで行くことではじめて現在の設計の問題点やより良い設計について考えるきっかけになると考えています。

互換性は求めない

リリース間で互換性を確保しようとすることは行いません。Spanner のある機能に対してより良い UI が思いついたら破壊的変更をしてもそちらに移行します。
この方針の表明として、安定版としての 1.0 は出さない ZeroVer 前提のバージョンポリシーを決めました。

This software will not have a stable release. In other words, v1.0.0 will never be released. It will be operated as a kind of ZeroVer.

v0.X.Y will be operated as follows:

The initial release version is v0.1.0, forked from spanner-cli v0.10.6.
The patch version Y will be incremented for changes that include only bug fixes.
The minor version X will always be incremented when there are new features or changes related to compatibility.
As a general rule, unreleased updates to the main branch will be released within one week.

今後の spanner-cli へのスタンス

今後は spanner-mycli を先行開発していくことに集中します。
その中で気付いたバグや設計上の問題については spanner-cli の issue として報告しますし、必要に応じて issue での議論やコードレビュー等に参加しますが、
個人的に必要だと考えている新機能を spanner-cli に入れるための議論はあまり頑張らないこととします。

まとめと個人的な感想

この記事では spanner-mycli としてフォークを開発することになった理由と方針について説明しました。

spanner-mycli は今日 v0.1.0 をリリースし、今後も継続して開発していく予定です。

https://github.com/apstndb/spanner-mycli/releases/tag/v0.1.0

この記事で触れた以外にも spanner-mycli 固有の機能については下記のようなものが README.md やリリースノートに書かれています。一部は今後このアドベントカレンダー期間で裏側の実装も含めて紹介していきたいと思います。

  • --log-grpc による gRPC 通信のログ
  • Cloud Spanner Emulator 連携
  • root-partitionable 判定機能
  • Mutation サポート
  • クエリパラメータサポート
  • DDL の進捗状況表示
  • ターミナル幅に合わせた列の自動調整と折り返し機能
  • 実行計画 linter
  • Sampled query plan の表示

気が付けばもう既に spanner-mycli では spanner-cli にコントリビューションした総量と同じだけのコードを書いているようです。しかし、やりたいことはまだまだ尽きません。

ずっと PoC のようなコードばかり開発していたためコマンドラインツールは作っても UI 周りは触れることをずっと避けていましたが、開発というものがなかなか面白いことを思い出すことができました。

皆さんも自分のためのツールを開発してみてはいかがでしょうか。

Discussion