これはHaskell Advent Calendar 2020の19日目の記事です。
昨日はYoshikuniJujoさんの「型レベルで値の個数の範囲を指定できるリストを使ってFinger Treeを実装する - GHCの型チェックプラグインを活用する」でした。

今年2月くらいからDatabase.HDBCClickHouse にアクセスするライブラリを少しずつ書いていて、
基本的な SELECT / INSERT ができるようになったので書いてみた感想などをまとめる。
リポジトリはzaneli/hdbc-clickhouse

発端

そもそもなぜ Haskell で ClickHouse にアクセスするライブラリを書こうかと思ったかというと、
前職同僚から、Haskell には ClickHouse クライアントが無いため ODBC 接続せざるを得なかった、
というような話を聞いたのがきっかけだった。
(ちょうどその頃別の仕事でも ClickHouse を使っていて、GoRust から接続していた。)
とはいえ実用的なものを目指すというよりは、個人的な興味を満たすのが主な目的だった。
こんな事を言っていたので、一応有言実行になったのかな?

ClickHouse API

ClickHouse にはHTTPインターフェースネイティブ(TCP)インターフェースがあるようだ。
ただのHTTPアクセスするライブラリでは面白味がないので、ここはネイティブインターフェースを使用するライブラリを目指す事にする。
しかし、「Unfortunately, native ClickHouse protocol does not have formal specification yet」との事で、
頑張って既存のライブラリの実装を読み解きながら手探りでやっていく必要がある。

頼みの綱の ClickHouse ソースコードは C++ が不慣れなため(個人的には)あまり参考にできなかったが、
既存のサードパーティライブラリ clickhouse-go が大いに役に立った。
既存ライブラリのソースコードを読み解いたり実際にデータを送信したりして分かった事としては、最初の1バイトで送信するデータの種類が決まる。
例えばクエリなら 1 を送り、その後クエリ本体を送る。
Haskell 的には Data.ByteString などを使ってデータを作り、
Network.Socket.ByteString (sendAll, recv)Network.Socket にデータを送受信するような実装にしている。
ほとんど IO の中で処理が行われ予期しないデータが来たら throwIO を返したりもしているので、
あまり Haskell らしい純粋な実装というよりはリアルワールドとの泥臭いやりとりに終始している感はある…。

HDBC

さらに HDBC も不慣れだったため、こちらも既存の別 DB 向けライブラリを参考に書く事にした。
名前からの勝手な印象で、HDBC とは Java での JDBC に位置するような、DB アクセスに用いる標準的な方法なのかと思っていたが、
どうもそこまで一般的でもないらしい。

どのように実装するかはhdbc-sqlite3の実装から雰囲気を掴む事にした。
Database.HDBC.Types.IConnectionを満たすように関数を定義していけば良いようだ。
hdbc-mysql, hdbc-postgresql などの実装も軽く見てみたが、だいたい FFI で C ライブラリを呼び出しているようで、
ピュア Haskell 実装は見つけられなかった。
(実用的なライブラリでいうと、いたずらに車輪の再発明をするより既存の別言語ライブラリを Haskell から呼べるようにするのが正しいとは思う。)

ClickHouse はデータ型が豊富にあり、
Database.HDBC.Typesとのマッピングには苦労した。
UUID型は自力で、 IPv4型,IPv6型Data.IPを使って SqlString にするなど頑張っている。
配列型の変換がなかなか大変で、特にネストした配列は半ば諦め気味だったのだが、
Python版クライアントの実装…というかコメントを参考にようやく動作させる事ができた。
しかし、分かっている範囲では配列型を含むレコードを executeMany で複数件まとめて書き込むと動作しないなど、TODOは残っている。
(恐らく配列型のデータを書き込む際の実装に不備があるのだろうが、原因の特定に至っていない…。)
時刻型も SqlUTCTime を使った書き込みにしか対応できておらす、SqlZonedTime などにも対応したいがまだ手をつけられていない。

テスト

さて、簡単な読み取りと書き込みができるようになったので単体テストを書こう。
テストには hspecを使用した。
また、自作ライブラリで書き込んだ結果を自作ライブラリで読み取るテストを書いても意味が薄いので、
System.Processを使って
Docker 上の ClickHouse クライアントからデータを読み取る事で hdbc-clickhouse の書き込み結果をアサートするようにしている。

ClickHouse には DELETE や UPDATE がないので、
before_一旦 DROP TABLE, CREATE TABLE してからテストを流すようにも一時期はしていたのだが、
その後手を抜いて事前に対象データが無い事を確認してから hdbc-clickhouse を使って書き込み、再度読み取ってアサートするようにしてみた。
これは同じテーブルを使いまわしているからで、テストケースごとにテーブルを分けて事前に CREATE TABLE するなど、色々他にも方法はありそうだ。

今後

実用性を目指すというよりは個人的な興味で突っ走ってきたので、一旦は読み書きが動くものができて一区切りした気にはなっているが、
時間とやる気があればまだまだやり残した事はあるので引き続き開発を進めていきたい。
  • 対応できていないデータ型の対応
  • テストケースのパターン拡充
  • ドキュメント整備(現状どのデータ型に対応できているのか等)

実用的という意味でいうと、ClickHouse は大量データの読み書きに適していると思うのだが
今の実装が大量データの読み書きに耐えうるパフォーマンスが出るのかどうかも確認できていない点だ。

開発してみた感想としては、Haskell では今まで Project Euler を解くのに少し書いてみた程度だったが
それとはまた違った Socket に読み書きするライブラリを調べながら書くのはとても楽しかった。
幸い(?)まだ足りていない事があるという事はまだやれる事があるという事なので、今後も気が向けば少しずつ改善していきたい。

明日はryota-kaさんの「ユースケース層が投げうるエラーの型を「量化した open union」にしておけば複数のユースケースを合成したときに上の層でエラーハンドリングが楽にできて最高です!」です。

Copyright© 2011-2021 Shunsuke Otani All Right Reserved .