昨日はYoshikuniJujoさんの「型レベルで値の個数の範囲を指定できるリストを使ってFinger Treeを実装する - GHCの型チェックプラグインを活用する」でした。
今年2月くらいから
Database.HDBC
で ClickHouse にアクセスするライブラリを少しずつ書いていて、基本的な SELECT / INSERT ができるようになったので書いてみた感想などをまとめる。
リポジトリはzaneli/hdbc-clickhouse。
発端
そもそもなぜ Haskell で ClickHouse にアクセスするライブラリを書こうかと思ったかというと、前職同僚から、Haskell には ClickHouse クライアントが無いため ODBC 接続せざるを得なかった、
というような話を聞いたのがきっかけだった。
(ちょうどその頃別の仕事でも ClickHouse を使っていて、Go と Rust から接続していた。)
とはいえ実用的なものを目指すというよりは、個人的な興味を満たすのが主な目的だった。
こんな事を言っていたので、一応有言実行になったのかな?clickhouse-go の実装を読み込んで Database.HDBC のお作法を学んで時間がたっぷりあったら hdbc-clickhouse を書いてみたい人生だった。
— ザネリ (@so_zaneli) December 2, 2019
ClickHouse API
ClickHouse にはHTTPインターフェースとネイティブ(TCP)インターフェースがあるようだ。ただのHTTPアクセスするライブラリでは面白味がないので、ここはネイティブインターフェースを使用するライブラリを目指す事にする。
しかし、「Unfortunately, native ClickHouse protocol does not have formal specification yet」との事で、
頑張って既存のライブラリの実装を読み解きながら手探りでやっていく必要がある。
頼みの綱の ClickHouse ソースコードは C++ が不慣れなため(個人的には)あまり参考にできなかったが、
既存のサードパーティライブラリ clickhouse-go が大いに役に立った。
Haskell の ClickHouse クライアント書こうとしてめちゃくちゃ苦戦してる…。やっと hello (サーバーの情報取ってくるやーつ)と ping ができた…。Socket で ByteString をゴニョゴニョしていて同じ Haskell でもプロジェクトオイラー解いてた時と全く別の難解さがあるな…。 pic.twitter.com/ogvDd5Tspv
— ザネリ (@so_zaneli) February 13, 2020
Haskell の ClickHouse クライアント、SELECT クエリ投げて結果取れる所まで進めたいがムズい。クエリ投げたら ClickHouse から Exception が返るところまでできた。(何一つできていないけど、不完全なリクエストsendしたらrecvを待ち続けるので、何か返ってきただけでも進捗だ…) pic.twitter.com/zqI6wqNoLQ
— ザネリ (@so_zaneli) February 14, 2020
既存ライブラリのソースコードを読み解いたり実際にデータを送信したりして分かった事としては、最初の1バイトで送信するデータの種類が決まる。うおおおワイはやると言ったらやるんや!(Haskell で ByteString を foldr showHex したやつと Go で MultiWriter に渡した bytes.Buffer を hex.Dump() したやつを見比べている。)だんだん読めてきたし、Haskell でもクエリ通るところまでいけたっぽい。さてレスポンスをデコードするのが次の地獄…。 pic.twitter.com/K1NS6e6BJw
— ザネリ (@so_zaneli) February 15, 2020
例えばクエリなら
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版クライアントの実装…というかコメントを参考にようやく動作させる事ができた。
しかし、分かっている範囲では配列型を含むレコードをClickHouse の書き込み、ネストしたArray型 Array(Array(String)) みたいなやつが最難関と思っていたが、 Python 版に懇切丁寧なコメントがついていて涙を流して拝んでいる。そして Haskell に移植したら動いたぞ…! https://t.co/ojXySxEzkD
— ザネリ (@so_zaneli) July 30, 2020
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」にしておけば複数のユースケースを合成したときに上の層でエラーハンドリングが楽にできて最高です!」です。