SQL クライアントの将来についての期待
Snowflake や BigQuery をチームで利用する機会があり、自動テストをチーム内で導入したいという目的から Turu を作成しました。
しかし、 Turu が望ましいライブラリかと聞かれると、そうではないと個人的には考えているため、開発に至った動機と理想の姿を記録しておきたいと思います。
将来的な期待
- データベースベンダーはフォーマッタ・リンタ・ローカル開発環境を提供して欲しい
- 文字列で書いたクエリを Python コード上でフォーマット・リントする方式が成長して欲しい
- SQL <-> Python の間のやり取りで必要なのは ORM ではなく型チェック・補完が効くプロトコル
クラウドベースの SQL データベースの自動テストのしにくさ
今回 Turu を作成した最大の動機は Snowflake や BigQuery での自動テスト(ローカル環境・ CI/CD) のしにくさです。
レコーディングとテスト用のモック機能を提供することが Turu の開発目標の一つとなっています。
しかし、理想について述べると、このような開発ツールは本来データベースのベンダー側が用意するものであると思います。
AWS では Locak Stack などが使われることがありますが、新しいプログラミング言語がフォーマッタやリンターを公式が用意するようになったように、データベースのベンダーもフォーマッタ・リンタに加えてローカルテスト用のツールを提供する形が望ましいです。
開発者は易きに流れる傾向があるので、どれかのデータベースが開発環境の提供を行いシェアを獲得すると、他のデータベースベンダーも追従していくと推測しています。
ORM の苦境
最大の懸念は、 SQLAlchemy のような ORM は新しく登場しているデータベースに追従できるほどの開発リソースを確保できていないことです。 SQL を扱うデータベースの選択肢は増え続けていますが、それぞれの独自の方言を Python で綺麗にラップしていくことは非常に高コストになってきています。
また、ORM を採用する理由としてインピーダンスミスマッチが上げられていますが、 ORM が登場した時代に比べ、SQLを書けることは一般的な技術要求になってきていると思います。
実際、開発者の感想を聞いていると、望んでいる SQL を生成するためにはどのように ORM を書けば良いかで悩んでいる開発者の方が多い感覚があります。
私はいくつかのチームでの開発を経て、下記の認識を持つようになりました。
最近の開発者はインピーダンスミスマッチを起こしておらず、頭の中では SQL を書いている
将来的な SQL の書き方について
Template Literal Types
私が将来のデータベース開発で最も期待しているのは、 TypeScript が提供する Template Literal Types のような技術を用いた、SQLを文字列として記述し、エディタ上でバリデーションまで行う類の技術です。
Rustの sqlx ようにコンパイル時にデータベースに問い合わせてバリデーションを行う技術もありますが、実際にデータベースに問い合わせるのではなく、フォーマッタやリンターによって成り立つのがより望ましいと思います。
理想のコードは次のようなものでしょうか。
import pydantic
class User(pydantic.BaseModel):
id: int
name: str
cursor: Cursor[Row] = connection.execute_map(
Row,
SQL["select * from users where id = {id}"],
{"id": 1}
)
for row in cursor:
print(row.id) # Out: 1
print(row.name) # Out: 'taro'
最近の Python は TypeScript の影響も強く受けており、 Literal のような型が提供されています。
将来への期待として、 "Template Literal Types" は文字列の内容自体が独自の言語としてフォーマット機能・リント機能を持つようにならないかと考えています。
これは夢物語の話でしょうか? そうは思いません。
たとえば今のエディタでも markdown 中のプログラム言語のシンタックスハイライトや、 jinja のようなテンプレートエンジン内で、 html/python などのハイライトができており、その延長線として十分登場しえる技術であると思います。
Language Server Protcol のように、それぞれの文字列の中で直接 SQL データベース言語のリンターやフォーマッタを組み込めるようなプロトコルが登場すれば、データベースごとに各プログラミング言語で ORM やクエリビルダーを実装するコストがなくなるでしょう(プロトコルに対応した API を SQL 側が提供すれば良いだけ)。
多くの SQL 方言が発生し、プログラミング言語のライブラリの対応状況が追いついていない現状では、このような発展は十分考えられると思います。
SQL クライアントとして求められるのは基本的には下記です。
- プレースホルダーの適用(引数のクエリへの安全な展開)
- プログラム言語の構造体・オブジェクトへのクエリ結果の安全な変換
つまり、下記のコードに集約されます。
cursor: Cursor[Row] = connection.execute_map(
Row,
SQL["select * from users where id = {id}"],
{"id": 1}
)
これは SQL だけではなく、たとえば Jinja テンプレートエンジンのようなものにも応用できそうなアイデアです。
template.render(JINJA["select {{id}}", Lang["sql"]], id=1)
あぁ、こんな未来が訪れれば良いのに...
NOTE: 代替としてのクエリビルダー
Template Literal Types は素晴らしいアイデアに思えます。
しかし、現状の TypeScript の SQL ライブラリには特に計算量の問題があり、複雑なクエリは解析できません。
また、チームとして採用していた Python では、 Literal 型にそこまでの表現力はありません。
そこで、 ORM が直感的ではないことに対する問題から提案されることの多いクエリビルダーは良い代替案でしょう。
私も Python でクエリビルダーの検討をしたことがあります。
クエリビルダーはデータベースごとの方言に対応するには、愚直な実装をしなければならない欠点がありますが、下記の利点もありました。
- 一時テーブル用のクエリを Python オブジェクトとして保持し再利用できるので、型解析の結果のキャッシュができる
- Python の言語の補完機能の恩恵をフルに受けることができる
しかし、クエリビルダーも根本的には ORM と同じ問題に遭遇することになり、データベースごとの方言をライブラリ側が自前で実装する必要があります。
プロトコルを提供することで SQL 側が提供するフォーマッタなどを利用できる仕組みの方が、遥かに利便性の高いアイデアのように見えます。
最後に
Python での型安全なクエリビルダーの開発を断念した後、私は PEP 249 – Python Database API Specification v2.0 に戻って Turu を実装しました。
このデータベースの PEP は古い仕様ですが(1999年作成!)、将来的なことを考えたとき、データベースクライアントとして適度なインターフェースを既に持っているのかもしれません。
いずれにせよ、 async にも対応していないため、そろそろ 3.0 の仕様を検討して欲しいと思います。
Discussion