sqlcの「無謀な試み」— MIN(timestamp)の型推論がなぜ難しいのか | Resilire Tech Blog
はじめに
サプライチェーンリスク管理クラウドサービスResilireでエンジニアをしている奥村 @arashigaoka3です。
最近このxを拝見し、これをきっかけとしてHacker Newsのスレッドを見て、おもしろい内容だなと感じました。
Resilireではsqlcを採用しており、現状と今後の把握は非常に重要です。この記事では、sqlcで採用されているbuilt-in analyzerとdatabase-backed analyzerについて説明します。
built-in analyzer の狙いとメリット
私がsqlcに感じているメリットとして、SQLからコード生成する際、通信をしない点が素晴らしいなと思っています。これによって安定して高速な開発体験が可能になっていると感じています。
これはsqlcに内蔵されたbuilt-in analyzerが実現しています。すなわち、sqlcではDDLファイルからスキーマを取得して解析することで、オフラインでのコード生成が可能になっています。
built-in 方式の難しさ
とはいえ、PostgreSQLの型システムは非常に複雑であり、これを再実装し続けるのは容易なことではありません。sqlcを実際に使用されている方は、複雑なクエリの場合に挙動がおかしくなるケースに遭遇した方も少なくないと思います。
sqlc公式としても、v1.23.0のリリースノートで下記のように述べられています。
We can do better, but attempting to recreate the entirety of a database’s analysis engine inside of sqlc is at best a difficult moving target and at worst a fool’s errand.
すなわち、PostgreSQLの型システムを再実装し続けるのは "fool’s errand" すなわち「無謀な試み」だと考えているようです。
built-in analyzer の具体的な問題点
具体的に問題が起きるのは、例えば下記のようなクエリの場合です。
CREATE TABLE activities (
account_id BIGINT NOT NULL,
event_time TIMESTAMP WITH TIME ZONE NOT NULL
);
SELECT
MIN(event_time) AS MinDate
FROM activities
WHERE account_id = $1;
この場合に、本来event_time
はtime.Time
であるべきですが、built-in analyzerではinterface{}
になってしまいます。
これがなぜ起こるのか、それはPostgreSQLのMIN関数の仕様が関係しています。
PostgreSQLのMIN関数の仕様
MIN関数は固定の返却型を持たず、引数として任意の型を受け取り、引数と同じ型を返す仕様となっています。
これは、pg_catalog(PostgreSQLのシステムスキーマ)上は以下のように多重定義されているような形になっています。
min関数の定義を取得するクエリ
SELECT
p.oid::regprocedure AS signature,
p.prorettype::regtype AS returns
FROM pg_proc p
JOIN pg_namespace n ON n.oid = p.pronamespace
WHERE n.nspname = 'pg_catalog'
AND p.proname = 'min'
ORDER BY signature ;
結果
signature | returns |
---|---|
min(anyarray) | anyarray |
min(bigint) | bigint |
min(integer) | integer |
min(smallint) | smallint |
min(oid) | oid |
min(real) | real |
min(double precision) | double precision |
min(date) | date |
min(time without time zone) | time without time zone |
min(time with time zone) | time with time zone |
min(money) | money |
min(timestamp without time zone) | timestamp without time zone |
min(timestamp with time zone) | timestamp with time zone |
min(interval) | interval |
min(text) | text |
min(numeric) | numeric |
min(character) | character |
min(tid) | tid |
min(anyenum) | anyenum |
min(inet) | inet |
min(pg_lsn) | pg_lsn |
理想的には、MIN関数をsqlcで解決する際には、引数の型も含めて参照し、どの関数定義が該当するかを探索すべきです。しかし実装を読むと、実際には命名と引数の性質(デフォルト引数か、可変長引数か)しか探索していないようです。
実装箇所: ResolveFuncCall
このtimestampの問題が指摘されたissueで集約関数のためのパッチも検討されていましたが、nullabilityを一貫したルールで決めるのが難しいという問題がありました。
実はPostgreSQLの仕様として、集約関数はCOUNT関数を除き、選択された行がない場合にnullを返却するとされています。
It should be noted that except for count, these functions return a null value when no rows are selected.
こうしたPostgreSQLの仕様は、pg_catalogからは判別が難しく、単に集約関数のための実装を追加するだけでは正確な型推論はできません。
database-backed analyzer について
そんな中、冒頭のHacker Newsのスレッドでは、sqlcのメンテナの方から以下のようなコメントがありました。
Maintainer of sqlc here. Thanks for the kind words! I'm considering switching to the sqlx model of talking to a running database simply because trying to re-implement PostgreSQL internals has been a huge challenge. It works for most queries, but for the long tail of features, it's a losing battle.
すなわち、sqlcはdatabase-backed analyzerの路線を強化していくことを検討しているようです。
database-backed analyzer の狙いとメリット
database-backed analyzerは実DBに接続して解析します。PostgreSQLではpgxを介してParse/Describeを行い、ParameterDescription / RowDescriptionからバインド変数と結果列の型OIDを取得します。これにより、DBが最終的に解決した型をそのまま採用できます。
(ただし、この型OIDにnullabilityの情報は含まれていないので、結局そこも含めて型推論するには個別実装が必要そうな気がするなとか思いましたが、この辺りどなたか詳しい方がいらっしゃいましたらコメントいただけると嬉しいです)
desync について
強力な型推論が可能になった一方で、database-backed analyzerを採用する場合には、実DBとの型のズレ(desync)が発生する点が指摘されています。これが発生すると実行時エラーにつながるおそれがあり、sqlcのメリットが失われてしまいます。desyncを防ぐためには、CIパイプラインでマイグレーション後のスキーマとクエリの型を比較することで検出するなど、運用を強化することが望ましいと思っています。
sqlcの今後について
sqlcは現在、database-backed analyzerの路線を強化しているようです。一方、built-in analyzerのメンテナンスを継続するかについては、特に言及は見つかりませんでした。我々としては、しばらくbuilt-in analyzerを利用していきたいと思っていますが、将来的には切り替えざるを得ないシーンが出てくるかもしれないと思っています。引き続き動向を注視していきます。
終わりに
🎉 Goカンファレンスでお会いしましょう!
Resilireは9/27-28開催のGoカンファレンスのGoldスポンサーです。
プロダクトや開発体制の話、キャリアのご相談など、ぜひブースで気軽に声をかけてください。
また、もしこの記事を読んでご興味持たれた方がいらっしゃいましたら、ぜひ一度カジュアルにお話しさせてください!
詳しい採用情報はこちらからご確認いただけます:
Discussion