🐈

spansql へのレクイエム、あるいは Go における Spanner GoogleSQL 方言パーサの現状

2024/11/11に公開

spansql へのレクイエム、または Go における Spanner GoogleSQL 方言パーサの現状

この記事は apstndb Advent Calendar の1日目の記事です。

この記事では Spanner GoogleSQL 方言を処理する Go ライブラリの選択肢の現状について共有しつつ、 Spanner コミュニティが進むべきだと考える方向性を示します。

対象読者は下記のような方を想定しています。

  • Spanner のスキーマ管理ツールやクエリ実行ツールを Go で開発している方
  • Spanner のツールをこれから開発しようと考えている方
  • Spanner を利用しており、OSS ツールを利用している方
  • Spanner のエコシステムに関心のある方

導入

Spanner はみなさんご存知だと思いますが、 Google Cloud の分散 SQL リレーショナルデータベース管理システム(RDBMS)、いわゆる NewSQL として分類されることもあるプロダクトです。
国内外で多くの企業が導入し、みんなの銀行のようなミッションクリティカルなサービスを含む大規模なサービスで利用されています。

SQL データベースである Spanner は クエリ言語やスキーマ定義言語(DDL)、データ操作言語(DML) に SQL を使います。
SQL は ISO によって標準化されていますが、 標準 SQL はインデックスの概念を含まないなど、実用上完全な言語ではないため、世の中の SQL RDBMS は皆それぞれの方言を持っています。
Spanner も例外ではなく、 GoogleSQL という SQL 方言を持っています。

GoogleSQL 方言は Spanner 以外の Google のソフトウェア、例えば Google Cloud のフルマネージドデータウェアハウスサービスである BigQuery とも大部分を共有しており、
SQL 方言の中では利用しているユーザは少なくはないと思われます。
しかし、 MySQL や PostgreSQL のような超メジャーな OSS と比べるとどうしてもこの方言を処理するツールや再利用可能なライブラリが少ないという問題があります。

Go でいくつかの OSS に使われているライブラリとして、 Google Cloud の Go クライアントライブラリである GitHub レポジトリ googleapis/google-cloud-go 内で管理されている spansql があります。

Package spansql contains types and a parser for the Cloud Spanner SQL dialect.

Google の Go クライアントライブラリチームが開発しているのだからこれは有力な選択肢だと思うでしょう。しかし、 spansql には大きな問題があるんです。

spansql の半生

spansql が生まれたのは2019年まで遡ります。

当時の Spanner は、OSS ではないため開発に実インスタンスを使うしかなく、今と比べて開発が難しいものでした。これは、コスト面や開発環境の面で大きな課題でした。

そんな中、 Go クライアントライブラリのテストにも使えるインメモリエミュレータとして、 spannertest が Spanner クライアントライブラリのサブディレクトリ内に誕生します。

https://github.com/googleapis/google-cloud-go/issues/1181

この記事で説明している spansql も spannertest で使うための GoogleSQL のサブセットの実装が外部の開発者も使える Go のパッケージとして export されたものでした。

https://github.com/googleapis/google-cloud-go/commit/fb3c527ecb61102c3e756ad0638abb5ddc40cf6d

ところで spannertest は SELECT 文や DDL などのうちの SQL の原始的なサブセットを処理して結果を返すもので、エミュレータというよりも Spanner API gRPC サーバーのモックと言った方が近いものでした。
結果として、spansql はサブクエリや GROUP BY すら持たない SQL しか処理できない状態で生まれたのでした。

その後、 spannertest は少しずつ機能拡張されていったものの、十分に高機能な SQL 処理系である Spanner との機能差は埋まるどころか機能拡張に従いどんどん広がっていきました。
後に更新すら諦めて全く追従できていないであろう非対応リストでもこのような膨大な量になっています。

Here's a list of features that are missing or incomplete. It is roughly ordered
by ascending esotericism:

- expression functions
- NUMERIC
- JSON
- more aggregation functions
- SELECT HAVING
- more literal types
- DEFAULT
- expressions that return null for generated columns
- generated columns referencing other generated columns
- checking dependencies on a generated column before deleting a column
- expression type casting, coercion
- multiple joins
- subselects
- case insensitivity of table and column names and query aliases
- transaction simulation
- FOREIGN KEY and CHECK constraints
- set operations (UNION, INTERSECT, EXCEPT)
- STRUCT types
- partition support
- conditional expressions
- table sampling (implementation)

その後前述した Cloud Spanner Emulator のリリースもあって、 spannertest の重要性は失われていき、殆ど放置されるようになりました。
spannertest と共に成長していくかと思われた spansql ですが、 サブクエリすら parse できず SQL クエリを実装する基盤が成熟しないまま別の目的の方が使われることが増えていくこととなります。

それはサードパーティのツールが DDL 文を処理する目的です。
DDL 文は SQL クエリと比べると難しい構文が少ないため、スキーマを解釈するマイグレーションツールなどを開発する上で spansql はある程度実用に足るものでした。

ツールのユーザが非対応の DDL 構文を使おうとするとエラーが発生しますが、修正可能なのが OSS の強みです。
しばらくツール開発者が Issue を立てれば Google のクライアントライブラリチームが解決したり、必要としている外部の開発者が spansql に PR を投げることが増え、エコシステムの重要なパーツとして続くものかと思われました。

spansql に依存するパッケージの一覧

https://pkg.go.dev/cloud.google.com/go/spanner/spansql?tab=importedby

例えば下記のようなものがあります。

yo (v2)

https://github.com/cloudspannerecosystem/yo/tree/master/v2

wrench

https://github.com/cloudspannerecosystem/wrench

golang-migrate/migrate

https://github.com/golang-migrate/migrate

spansql の今

その後、2022年4月に Google クライアントライブラリチームのメンテナーが PR に対してこのようなコメントをしました。

https://github.com/googleapis/google-cloud-go/issues/5837#issuecomment-1087242796

spansql/spannertest packages lag behind compared to the actualy cloud spanner feature wise and functionality wise, We are thinking of freezing the development of spansql/spannertest packages and recommend to use officially supported https://cloud.google.com/spanner/docs/emulator for in-memory usage.

  • spansql/spannertest パッケージは実際の Cloud Spanner の機能に大きく遅れをとっている。
  • (クライアントライブラリチームは) spansql と spannertest の開発を凍結しようと考えている。
  • インメモリのユースケースは Cloud Spanner Emulator で代替することを推奨する。

実際には DDL parser としてのユースケースは Cloud Spanner Emulator では代替できないのですが、開発を凍結したいという明白な意思表示です。
このあたりから、クライアントライブラリチームの spansql へのコミットは目に見えて減少し、issue を立てても p2, p3 がつけられたまま対応されない 状況が常態化しました。下記の2022年9月の更新を最後に、クライアントライブラリチーム自身によるコミットは行われていないようです。

その後から2024年11月現在に至るまで、 spansql に依存しているツールの利用者たちであろう Google の外部の開発者からのコントリビューションで少しずつ機能は拡張していきましたが、 PR のマージすら滞るようになっていきました。試しにざっと見てみてもこのように長期間放置されているものが目立ちます。

Issue やプルリクエストが放置される OSS はやがて問題の報告すら敬遠されるようになっていきます。この X のポストはその心理の実例でしょう。

https://twitter.com/nakatakeshi/status/1580146802279747586

OSS は、必要としている開発者自身がコントリビューションすることで自己救済できるという大きな長所があります。しかし、spansql はもはや自己救済すら難しい状況になっています。スキーマ管理ツールのようにビジネスアプリケーションのデプロイの上で重要なコンポーネントを、自分たちで改善できないライブラリに依存させることは、大きなリスクとなります。

今の spansql の関係者が置かれている状況は私に言わせれば、三方良しならぬ三方悪しになっているように思います。

  • Google のクライアントライブラリチームはもうメンテナンスしたくもないパッケージを凍結できず、プルリクエストをレビューしてマージしてリリースもしないといけない。
  • spansql に依存する関連ツールの開発者は困っているユーザのために、spansql にプルリクエストを投げて長期間放置されて板挟みになるという非常に悪い体験をすることになる。
  • 関連ツールの利用者は issue を上げても長い間新しい構文に対応されないので、すぐに使いたかった Spanner の新しい機能の利用を断念することになる。

wrench の利用者である @satoIsSugar さんによるこの発表は利用者側の問題が表に出たものだと言えるでしょう。表に出てきているものの裏にもっとたくさんの問題があることは予想されます。

https://speakerdeck.com/sgash708/spansql-de-enum-woshi-itakatutahua

Spanner は生成 AI 連携やプロパティグラフ機能、 Protocol Buffers 対応など頻繁に画期的な機能が追加される高機能なデータベースですが、
新機能を使いづらいこの状況はコミュニティを停滞させることになります。
この状況は Spanner コミュニティにとって確かに存在する解決すべき問題であるように認識します。

競合の評価

さて、 spansql に問題があるのであれば他にどのような選択肢があるのでしょうか。これから見ていきましょう。

ZetaSQL

まず ZetaSQL について説明します。

ZetaSQL について知らない人も多いかもしれませんが、ZetaSQL は、Spanner だけでなく BigQuery や Google 社内のシステムでも使われている、まさに GoogleSQL のフロントエンドそのものが OSS として公開されているのです。
GoogleSQL の構文解析をする parser だけでなく他にも下記のような多くのコンポーネントを含んでいます。

実装としてはクエリ、 DML、式などこれ以上なく正しい実装になっています。
私自身2019年に ZetaSQL が公開された時には驚いてすぐに記事で紹介したものです。

GoogleSQL の実装そのものなのであればこれが答えだと誰もが思うところですが、 Spanner GoogleSQL の DDL だけは ZetaSQL では実装されていないという非常に大きな問題があります。

BigQuery GoogleSQL Spanner GoogleSQL dialect
Query ZetaSQL ZetaSQL
DML ZetaSQL ZetaSQL
DDL ZetaSQL Non-ZetaSQL (GoogleSQL !?)

まるで意味がわからんぞ!と思うかもしれませんが、これが真実です。信じたくない気持ちはわかりますが、真実を直視すべきです。

Spanner には ZetaSQL の DDL 実装とは全く互換性がない DDL 文が多く含まれていますし、 CREATE TABLE 文など ZetaSQL DDL にも存在する文をとっても全て ZetaSQL では処理できない要素があります。
FEATURE_SPANNER_LEGACY_DDL フィーチャーフラグ周りを見ると過去に統合しようとした痕跡はありますが、途中で断念したとみて間違いないようです。

また、 ZetaSQL は ASLv2 を採用したオープンソースソフトウェアではありますが、 Google 社内の実装を export しており開発はオープンではありません。

例えば、Google 社外の開発者が実装にコントリビューションすることは一切できません。

https://github.com/google/zetasql/blob/194cd32b5d766d60e3ca442651d792c7fe54ea74/CONTRIBUTING.md

We are not currently accepting external code contributions to this project. Please feel free to file bugs and feature requests using GitHub's issue tracker.

また、ZetaSQL は OSS として公開することを意図的に遅らせている部分があると思われるます。具体的には Spanner Graph 関係の構文は ZetaSQL のレイヤで実装されているように思われるが、現在 OSS の ZetaSQL には含まれません。

更に、リリースノートは網羅的には書かれないのでバージョン間の差分を理解するにはソースを読む必要があります。 1コミットに squash されているので巨大なコミット(2024.08.2 リリースの例)を読むのは容易ではありません。

それでも ZetaSQL は正しい実装であることは間違いないので、 Spanner の DDL は優先度を下げても良いから Spanner と BigQuery 両対応のツールを作りたいなどの需要がある場合には有力な選択肢かもしれません。

ZetaSQL を使いたい人向けの情報としては、 goccy さんが開発している ZetaSQL の Go バインディングである goccy/go-zetasqlgoccy/bigquery-emulator などで使われており、 Spanner のクエリや DML のパースにも使えることが期待できます。
Go のプログラムから C++ で実装された ZetaSQL とのバインディングをするため CGO に依存しますが、将来的に WebAssembly にビルドした ZetaSQL を使うことで Pure Go として実行可能にし、 ZetaSQL の更新に追從することも容易になるそうです。

https://speakerdeck.com/goccy/bigqueryemiyuretanozuo-rifang

https://goccy54.hatenablog.com/entry/2024/03/11/022640

余談: Cloud Spanner Emulator はどうしているのか?

Spanner が ZetaSQL ではないのであれば現在最も完成度が高い OSS の Spanner 互換の処理系である Cloud Spanner Emulator ではどうなっているかというと、クエリや DML は ZetaSQL を使い、 DDL は ddl_parser.jjt などに記述された構文から parser を生成しているようです。拡張子 jjt とは JavaCC/JJTree で使われているものであり、 Java 以外にも Cloud Spanner Emulator に使われている C++ に対応しています。
実は Google のエンジニアが開発して Cloud Spanner Ecosystem 下で管理されている OSS である spanner-schema-diff-tool でも同じ jjt ファイルをフォークして使っているようです。これ以上のものは Google も持っていないということになります。

https://github.com/cloudspannerecosystem/spanner-schema-diff-tool/tree/v1.23.0/src/main/jjtree-sourcest

ユーザの OSS でもこの jjt ファイルを使っても良いのですが、問題としては下記のようなものがあります。

  • Cloud Spanner Emulator がその時点で対応する機能しか実装されない。
  • JavaCC は対応言語が限られる。
    • ANTLR があればまだ良かったが…

memefish

他の選択肢として、 memefish という Go パッケージがあります。

memefish は元々 Mercari インターンシップに参加した MakeNowJust さんによって開発された GoogleSQL を扱うパッケージです。現在は GitHub の Cloud Spanner Ecosystem organization 下で管理されています。

開発当初の記事

https://engineering.mercari.com/blog/entry/2019-10-02-090000/

当初 memefish には型チェッカーなどに使うために意味解析をする analyzer を含まれていましたが、後に analyzer は削除され、 lexer と parser の開発が続けられました。

https://github.com/cloudspannerecosystem/memefish/issues/54

memefish を parser として使用しているプログラムは次のようなものが知られています。

インメモリ Spanner エミュレータ handy-spanner

https://github.com/gcpug/handy-spanner

yo (v1)

https://github.com/cloudspannerecosystem/yo

memefish の長所

  • SQL を処理するために十分な基盤
    • 字句解析器(lexer)と構文解析器(parser) から構成された構造
    • トークンや AST はソース上の位置(pos, end) を保持可能
  • 真の SQL 対応
    • DDL のみでなんとか使える spansql とは異なる
    • handy-spanner のような GoogleSQL を広くサポートするツールには不可欠

memefish の短所

  • Spanner の新機能に能動的に追従するモチベーションがあるコントリビューターが不在だった
  • 利用している OSS が多くない
    • (いくつかの OSS に DDL parser として使われた結果依存するユーザが多い spansql と比較して)
    • 結果 GoogleSQL との非互換性の issue はあまり上がっていない
  • spansql と memefish は DDL 対応が一長一短だった
  • リリースタグがまだ切られたことがないいわゆる v0.0.0 の Go パッケージ
    • 変わり続けるプロダクトに追従するために破壊的変更は避けられないが、説明するリリースノートがない
    • 最近 v0.1.0 リリースについて話しはじめた

選択肢まとめ

ここまでの選択肢を私の独断と偏見でまとめると次のようになります。

GoogleSQL への追従 Spanner DDL 対応 コントリビューションへの対応 リリース 未来
spansql × ⚪︎ ⚪︎ ×
ZetaSQL × × ⚪︎
memefish ⚪︎ ⚪︎(努力次第で ◎) ⚪︎ ×(改善予定あり) ?

ちょっと恣意的でしょうか。
とりあえず、私は OSS に使う Spanner GoogleSQL の parser ライブラリとしては、 memefish 以外にはコミュニティの努力で解決できない問題があるという評価をしました。

私ごととして

選択肢まとめで私の主観が強く出たところで私自身の話をします。

私は以前メルカリグループに所属しながら Spanner 周りのトラブルシューティングや啓蒙活動をしていました。Cloud Spanner の実行計画の活用に関する取り組み にまとめたように、当時ほとんど理解されていなかった実行計画の可視化手法と共に最適化手法を確立するため、クエリ周りのいくつかの OSS についてもオーナーシップを持っていました。
DDL 周りのツールを開発をしていないため、 spansql については、 Spanner の Go クライアントライブラリの差分を確認する時にノイズになるものだという程度にしか認識していませんでした。

https://github.com/gcpug/nouhau/issues/127

なお、 all: auto-regenerate gapics などの自動生成クライアントの更新による差分、 spansql, spannertest`, その他テスト周りの更新のみの差分については記載しない。

ある時、社内で使っているいくつかのツールが spansql 由来でエラーになるため、 Spanner の機能が使えないというような話が何度も繰り返されていることに気がつきます。
エラーが発生するツールの1つはマイグレーションツールの wrench で、 spansql に依存することとなった経緯はコメントを含む一連の DDL 文をセミコロンで分割するために spansql を採用したようでした。

https://github.com/cloudspannerecosystem/wrench/pull/54

私がメンテナの一人でもある spanner-cli には、文字列リテラルやコメントに含まれるセミコロンを正しく扱うための statement separator というコンポーネントがありました。
(現在は apstndb/gsqlsep ライブラリとしてスピンアウト)

https://github.com/cloudspannerecosystem/spanner-cli/issues/33

これは GoogleSQL lexical structure のサブセットの実装として解釈できます。

spanner-cli はこれでうまく行くことを知っていたため、 wrench で起きていることを GoogleSQL としての構文解析が必要ないところに spansql を使うというアンチパターンとして認識して、解決方法を探りました。

https://github.com/cloudspannerecosystem/wrench/issues/83

その後公私ともに色々あってバーンアウトして無職も1年が見えてきた頃、ふと気になって現状がどうなっているかを確認したところ問題が解決していないことに正直驚きました。
前述した @satoIsSugar さんが発表に先駆けて8月にしたこの X のポストがきっかけだったと記憶しています。

https://x.com/satoIsSugar/status/1833132864797335955

変わったことといえば退職する前に聞いていた通り memefish が cloudspannerecosystem org に移管 したことくらいでしょうか。

現状にはあの時に解決可能だった問題を解決できなかった私に一定の責任があるので、今度こそ問題を不可逆的に解決するために地獄から蘇った spansql スレイヤーとして活動をはじめたのでした。

spansql を使う必要がないようにするためにはどうするか。私は spansql に対して一長一短ではない代替を用意することが最も重要だと考えました。
memefish がその代替の条件を満たせるようにするため、 spansql のテストを全て memefish で走らせ parse できないものを全て修正することで一長一短ではなく、機能的にスーパーセットの状態を達成しました。

https://github.com/cloudspannerecosystem/memefish/issues/115

また、今後もユーザから issue が上がってから対応するのでは遅いため、最新の機能も含め既知の DDL 文は全て実装してプルリクエストを用意しました。レビュー待ちでまだマージされていないものや、個々の機能では対応できていないものはありますが、一つのマイルストーンでしょう。

https://github.com/cloudspannerecosystem/memefish/issues/178

更に今後も非互換性に気付けるように spanner-cli の個人的なフォークである spanner-mycli に memefish を組み込む ことで memefish のドッグフーディングができるようにしています。

memefish を spansql の完全な代替にするためのタスクは終わりました。これからの話をしましょう。

主張

私の主張は次のようなものです。

  • spansql に未来はありません。
  • クライアントライブラリチームが spansql の開発を終わらせられるようにしましょう。
    • 新しい機能を使うために spansql にコントリビューションするのはもはや良い選択ではありません。
  • spansql を新規で採用するのはやめて、最初から他の選択肢の採用を検討しましょう。
    • ライブラリ起因で失敗すると分かっているソフトウェアを新しく生む必要はありません。
  • spansql に依存しているのであれば、他の選択肢へ移行しましょう。
  • 助けが必要であればコミュニティに相談しましょう。
    • GCPUG Slack#cloud-spanner チャンネルは開かれています。

他の選択肢とは何か

  • memefish
    • DDL だけでなく、 DML やクエリの構文解析がしたい時も十分に推奨できる選択肢です。
    • 構文解析が必要ない用途であれば字句解析のみの利用も可能です。
      • 例えば、 apstndb/spanner-mycli では apstndb/gsqlutils 内に memefish.Lexer を使って実装したステートメント分割とステートメント種別判別のロジックで要件を満たせています。
      • 本当に構文解析が必要なのは CREATE TABLECREATE INDEX を元に Go のプログラムを生成する yo のように一つ一つの文の中の要素を解釈する必要があるものでしょう。
    • もしも memefish に非互換性を見つけたら issue を上げよう。
      • memefish なら自分で修正しなくてもコントリビューターが直してくれるはず!
  • DDL 対応が不要なくて BigQuery と両対応したいなどの要件なら ZetaSQL(goccy/go-zetasql) も有力な選択肢です。

最後に

X のフォロワーや読者の方は何故私がこんなに spansql を滅ぼそうとしているのかや memefish の開発をしているのかは疑問だったと思うので、いくつかのマイルストーンを達成したところで頭の中を書き出してみました。

足枷になっている spansql が役割を終えることで、 Spanner コミュニティが未来に向けて推進できるようになることを願います。

ユウナのガード ワッカの言葉

今だ!異界に送っちまえ!

追記

(2024-11-12) wrench と yo/v2 の memefish 移行

memefish に不慣れなオーナーがいつ修正できるかは分からないので、 wrench と yo/v2 には直接 memefish 移行のプルリクエストを投げました。
私自身がユーザーではないので検証は十分ではありませんが、急ぎの方はブランチから直接使うことができます。

https://github.com/cloudspannerecosystem/wrench/pull/121

https://github.com/cloudspannerecosystem/yo/pull/145

Discussion