sqlc internals
sqlc は何をやっているのか、問題に遭遇したときに調査するべき箇所はどこか?
というのを sqlc 1.20 時点の情報をもとに書いていきます。
背景
最近 sqlc に PR を送るようになり sqlc についての理解が深まってきたのでまとめておこうというのと理解を共有しておくことで PR を送る人が増えると良いなという思惑があります。
sqlc とは
SQL Compiler の略でスキーマとクエリからパラメータと結果の型を推論するツールです。 その推論された型からコードを生成したり、lint のようなことが可能です。 コード生成が主な機能ですが v1.20 から sqlc vet が導入されています。
Linting queries — sqlc 1.21.0 documentation
internals
sqlc はスキーマとクエリからパラメータや結果の型を推論しますが流れとしては以下のようになっています。
- parse
- compile
- codegen
parse
スキーマとクエリをパースします。
対応しているのは PostgreSQL, MySQL, SQLite の3つで
PostgreSQL は github.com/pganalyze/pg_query_go を使っているので基本的にパースできない SQL はないです。 internal/sql/ast は pg_query_go の ast をベースに作られています。
MySQL は github.com/pingcap/tidb/parser を使っているのでほとんどの SQL はパースできます。 内部のパッケージは internal/engine/dolphin になっています。
SQLite は https://github.com/antlr/grammars-v4 の SQLite の文法をベースに自前のパーサーで処理しています。
sqlc.arg
などをパースできるようにするために独自の拡張(SQLite は schema に紐づく関数がないため)が含まれています。
元の文法のルールはエラーにはならないものの正しいノードに分類されていないケースがありそれらの対応も含まれています。
SQLite そのものが持つ柔軟性を再現することが難しいケースがあるため動かない SQL も多々あります。
それそれのパーサーから得られた AST から internal/sql/ast に変換する処理が convert 関数です。 ですが convert 関数の完成度が engine ごとに大きく異なっていて推論が動く範囲で適当な AST を返すようになっていたり そもそも実装時間の都合で未対応になっているものが ast.TODO というノードになっていたりします。
完成度は PostgreSQL, MySQL, SQLite の順になっています。
問題を修正したいときにクエリに対して正しい AST がなにかというのがわからないことが多いです。 基本的に pg_query_go の結果を正として MySQL, SQLite に移植することになります。 pg_query_go は pg_query_go でどのときにどういうノードが返るかについてのドキュメントがないので大変です。
compile
得られた AST から catalog (スキーマの型とクエリのパラメータと結果の型) を生成する処理をコンパイルとしています。
型の推論を AST から行うのですが複雑なクエリだと動かないケースがあります。
interface{}
になったりコード生成に失敗したりするケースは明示的に CAST を書くというのがワークアラウンドです。
*
の展開や sqlc.embed, sqlc.arg, sqlc.slice などの処理もしています。
特定のカラムやテーブルが見つからないというエラーに遭遇した場合はここで発生しています。 エラーは compile で発生しますが原因が不完全な AST というケースもあります。
codegen
catalog からコードを生成する処理が codegen です。 builtin で go のコードジェネレーターがあります。text/template パッケージが使われています。
sqlc.slice はここでも処理が必要です。
生成コード自体がおかしい場合はここに問題があるケースが多いです。 オプションが多くなってきているので適切にハンドリングできていなかったりします。 もちろん catalog がおかしいケースもあります。
元々は python, kotlin も builtin でしたが切り離されて sqlc-gen-python, sqlc-gen-kotlin になりました。 おそらく debug 用ですが catalog を json を出力する機能もあります。 process plugin と wasm plugin があって wasm plugin が推奨されていますができることが違うので考えて選択する必要があります。
plugin については 前の記事 を参照してください。
builtin の go のコードジェネレーターは簡単に使えますが拡張性はあまりないのでコードが好みではない場合は自分で plugin を書くのが良いと思います。 builtin なのでバグの修正リリースが本体と同じタイミングというところも良し悪しがあります。
debug tips
バグに遭遇したら https://play.sqlc.dev/ で再現するか確認してください。 再現できて自分で修正するのが難しかったりよくわからない場合は issue にしてください。 自分で修正したいと思ったら手元で実行できるようにしてください。
適当に以下のようなツールを作って playground の URL を渡すと go のテストを生成して GoLand でデバッグできるようにしています。
僕がこういう playground に依存したワークフローをしている都合上、issue に playground のリンクがないものの対応優先度がとても低いです。
問題が修正できたら internal/endtoend のテストを編集したり追加したりしてください。 sqlc はほとんどE2Eテストによって動作を確認しています。 ただ internal/endtoend の問題点として一覧性が悪いというのがあります。 修正に対して変更するべきテストがわからないケースは気合で解決する必要があります。
テストの変更・追加が終わったら scripts/regenerate を実行して期待値を更新します。
最後に PR を作れば終わりです。
終わりに
sqlc の内部についてとバグに遭遇したときの対応、どの辺にバグがあるかの勘所を紹介しました。 これをきっかけに sqlc に貢献してくれる人が増えると良いなと思います。