clickhouse-c は、ClickHouse のネイティブプロトコル向けのヘッダーオンリーCクライアントです。
ソースコードと各ヘッダーのリファレンスは、GitHubリポジトリにあります。
上位レベルのクライアントとは異なり、このライブラリは意図的に多くのことを行いません。コアヘッダーは、ユーザーが提供する I/O コールバックを介して Native フォーマットの ブロック をデコードおよび
エンコードします。socket、TLS コンテキスト、アロケータ、再試行、接続プーリングはユーザー側で管理します。そのため、埋め込みに適した
十分な小ささを実現しています。clickhouse.h をインクルードするだけで、リンク時の依存関係は libc 以外に発生しません。
このライブラリは現在も活発に開発されています。v1 では ClickHouse の主要な types をデコードします。
制限事項や不足している機能は、issue tracker から報告してください。
ただし、このライブラリでは設計上、あえて実装していない機能があることをご理解ください。
ライブラリが行わないこと
- HTTP プロトコル。HTTP インターフェイス については、libcurl を直接ラップしてください。
- DNS 名前解決、エンドポイントのフェイルオーバー、接続プーリング、再試行、バックオフ。
- TLS コンテキストのライフサイクル。OpenSSL バックエンドは、すでに接続済みの
SSLを使用します。 - スレッド化。各
chc_clientは設計上シングルスレッドです。 - ライブラリ内部での非同期 I/O。ブロッキングクライアントは
chc_io.readを同期的に呼び出します。ライブラリ自体では I/O を行わない イベントループクライアントが必要な場合は、I/O を行わないクライアント を使用してください。
ライブラリの構成
clickhouse-c はフラットなヘッダ群として提供されます。各ヘッダには、センチネルマクロで保護された宣言と実装の両方が含まれています。
ビルドに必要なヘッダを選択してください。
| Header | Purpose | Link flags |
|---|---|---|
clickhouse.h | コア: 型、エラー、アロケータ、I/O vtable、型名パーサー、ブロックリーダー、ライター | — |
clickhouse-client.h | TCP パケットループ: Hello、Query、Data、EndOfStream、Exception、Progress、Pong | — |
clickhouse-async.h | I/O を行わないクライアント: 呼び出し元からのバイト入力で駆動する同じパケットループ、ソケット不要 | — |
clickhouse-compression.h | 圧縮フレームのレイアウト、CityHash128、codec のディスパッチ、LZ4/ZSTD アダプター | -llz4 -lzstd |
clickhouse-posix-io.h | ブロッキング read(2)/write(2) 上で動作する I/O バックエンド | — |
clickhouse-openssl.h | SSL_read/SSL_write 上で動作する I/O バックエンド | -lssl -lcrypto |
必須のサーバー設定
プロジェクトへの追加
CHC_IMPLEMENTATION を定義して実装を取り込む翻訳単位は、必ず1つだけにしてください。
それ以外のすべての単位では、宣言のみを行うために同じヘッダーファイルをインクルードします。
chc_alloc_stdlib を使用するには、clickhouse.h をインクルードする前に CHC_PROVIDE_STDLIB_ALLOC を定義します。
clickhouse-compression.h で lz4/zstd への依存関係をなくすには、CHC_NO_LZ4 または CHC_NO_ZSTD を定義します。
TCP 経由で接続する
chc_io でラップして chc_client_init に渡します。すると、Hello ハンドシェイクが同期的に実行されます。ライブラリは DNS 解決、フェイルオーバー、再接続、プーリングを行わないため、これらは呼び出し側で処理する必要があります。
chc_client はシングルスレッドで動作し、1 つの connection を扱います。ライブラリは chc_io
callbacks を同期的に呼び出します。これらの callbacks が内部で何を行うか (epoll、io_uring、
WaitLatchOrSocket) は、ユーザー側に委ねられます。
クエリの実行
CHC_PKT_END_OF_STREAM に達するまでパケットを読み出します。必要なサーバー設定を
指定するには chc_client_send_query_ex を使用してください。chc_client_send_query は
空の設定リストを送信するため、サーバーのデフォルト設定がそのまま適用されます。
CHC_PKT_EXCEPTION パケットとして到着し、chc_client_recv_packet から
非OKが返るわけではありません。非OKが返るのはトランスポート層の障害時だけです。結果の最初の CHC_PKT_DATA
パケットは、スキーマを示す 0 行のヘッダーブロックで、その後にデータブロックが続きます。
chc_packet_clear はパケットのブロックまたは例外を解放します。代わりに所有権を取得する場合は、まずパケット上のそれらのフィールドを null にしてください。
カラムデータの読み取り
chc_column_layout が返す物理レイアウトがあり、
それに応じて処理を分岐します。宣言された型は chc_block_column_type から取得します。複合レイアウトはネストされるため、
Nullable(Array(String)) を読み取る場合は、まず Nullable をアンラップし、配列の offsets をたどってから、
文字列データをスライスします。
| Layout | Accessors |
|---|---|
CHC_COL_FIXED | chc_column_fixed_data(c, &elem_size) — n_rows * elem_size 分のリトルエンディアンのバイト列 |
CHC_COL_STRING | chc_column_string_data(c), chc_column_string_offsets(c) — offsets[i] はホストのバイトオーダーにおける行 i の排他的な終端位置で、行 0 は 0 から始まります |
CHC_COL_NULLABLE | chc_column_null_map(c) (1 行あたり 1 バイト、1 = NULL) 、chc_column_nullable_inner(c) |
CHC_COL_ARRAY | chc_column_array_offsets(c) (累積終端) 、chc_column_array_values(c);Map は Array(Tuple(K, V)) としてデコードされます |
CHC_COL_TUPLE | chc_column_tuple_arity(c), chc_column_tuple_child(c, i) — 各子要素の行数は同じです |
CHC_COL_LOW_CARDINALITY | chc_column_lc_key_size(c) (1/2/4/8) 、chc_column_lc_keys(c), chc_column_lc_dict(c);Dictionary の slot 0 はデフォルト値です |
CHC_COL_FIXED データは、wire 上ではリトルエンディアンです。ビッグエンディアンのホストでは、複数バイトの
整数を自分でバイトスワップする必要があります。offsets と LowCardinality キーは、デコード時にすでにホストオーダーに変換されています。
UUIDs は 2 つのリトルエンディアン UInt64 の半分からなり、IPv4 は 4 バイトのリトルエンディアン整数、IPv6 は
ネットワークバイトオーダーです。DateTime64 の tick は UTC で、型に含まれるタイムゾーンはメタデータにすぎません。
信頼できないピアから取り込む場合は、各カラムを走査する前に chc_column_validate を呼び出してください。
chc_block_read は、配列の offsets や
LowCardinality キーのようなフィールド間の不変条件を検証しないため、
そうしないと偽造された block によって内部カラムの境界を越えて読み出されるおそれがあります。
データの挿入
chc_block_builder でブロックを作成し、それを chc_client_send_data に渡します。ビルダーは
コピーするのではなくポインタを保持するため、カラムスラブは送信処理が完了するまで有効である必要があります。INSERT はクエリを送信し、
サーバーのヘッダーブロックを待機してから、1 つ以上のデータブロックを送信し、最後に空のブロックを送って
ストリームを終了します。
chc_block_builder_append_fixed は、n_rows * elem_size バイトのリトルエンディアンデータを受け取ります。
chc_block_builder_append_string は、packed slab 上の累積的な排他的終端オフセットをホストバイトオーダーで受け取ります。
ビルダーを下位レベルの
chc_block_write ではなく chc_client_send_data 経由にすることで、クライアントはネゴシエートされたリビジョンに基づいてブロックオプションを設定し、
圧縮を適用できます。
圧縮
chc_client_opts に、圧縮モードと設定済みの codec を渡します。クライアントは受信した
データパケットを展開し、送信するパケットを圧縮します。圧縮ヘッダーには LZ4 と ZSTD のアダプターが用意されていますが、
各初期化処理で埋まるスロットはそれぞれ自身のものだけです。どちらにも対応するには、両方を呼び出してください。
chc_codec を自分で実装してください。
vtable は clickhouse-compression.h で宣言されています。
TLS
clickhouse-openssl.h は、SSL_read/SSL_write を利用する chc_io バックエンドを提供します。OpenSSL の制御は利用者側で行います。
このライブラリは SSL_CTX の作成、証明書の検証、SNI の設定、SSL_connect / SSL_shutdown の呼び出しを一切行いません。chc_io.read が呼ばれる時点で、ハンドシェイクは完了している必要があります。
check_cancel コールバックと、
chc_openssl_io_set_deadline / chc_posix_io_set_deadline による読み取りデッドラインをサポートします。
I/O を行わないクライアント (非同期)
clickhouse-async.h は、イベントループ向け TCP クライアントの I/O を行わない版です。ソケットには一切触れません。
受信したバイト列を渡し、送信したいバイト列を取り出して、epoll、io_uring、または WaitLatchOrSocket は自分で駆動します。オプション、パケット型、ブロック builder は
ブロッキング クライアントと同じです。
chc_async_client_init は I/O を行わず、ブロックすることもありません。ハンドシェイクはその後、
再開可能な状態機械として実行され、送受信もすべて同様です。パース処理が渡されたバイト列を超えて進もうとすると、
呼び出しはブロックする代わりに CHC_WOULD_BLOCK を返します。追加の受信バイト列を渡して再度呼び出すと、
パーサーは ブロック の途中から再開します。
pump はバイトを双方向にやり取りします。Outbound では、chc_async_pending_out がキュー内のバイト列へのポインタと長さを
返します。ソケットがその一部を受け付けたら、そのバイト数を指定して chc_async_consume_out を呼び出してください。部分書き込みでも問題ありません。
Inbound では、ソケットから読み取ったデータを chc_async_submit に渡します。送信でブロックしたりバックプレッシャーがかかったりすることはないため、
pending-out の長さを監視し、大きくなりすぎたら送信の発行を止めてください。
動作する liburing ドライバーは
test/test_async_uring.c
にあります。
メモリとアロケータ
chc_alloc の vtable を受け取るため、メモリ割り当てはホスト側で使われている方式に従います。
clickhouse.h をインクルードする前に CHC_PROVIDE_STDLIB_ALLOC を定義し、標準の malloc ベースのアロケータを使用するには chc_alloc_stdlib() を呼び出します。
エラーとサーバー例外
CHC_OK (0) または 0 以外の CHC_ERR_* コードを返します。コードは戻り値であり、
呼び出し元のスタック上に確保された chc_err に、人間が読めるメッセージが格納されます。ライブラリが
エラーをヒープに確保することはありません。
chc_err の失敗ではありません。これらはパケットストリーム上で
CHC_PKT_EXCEPTION として到着し、サーバーの code、display_text、stack_trace を伴います。
chc_err のチェックは、トランスポート、プロトコル、デコードの失敗に限定してください。
サポートされているデータ型
Int8–Int256,UInt8–UInt256Float32,Float64,BFloat16BoolDecimal32,Decimal64,Decimal128,Decimal256Date,Date32,DateTime,DateTime64,Time,Time64String,FixedString(N)UUID,IPv4,IPv6Enum8,Enum16Nullable(T),Array(T),Tuple(...),Map(K, V),Nested(...)LowCardinality(T)IntervalQBit(...)Point,Ring,Polygon,MultiPolygonSimpleAggregateFunction(f, T)。これは内部のTとしてデコードされますJSONとObject('json')。文字列シリアライゼーションではStringカラムとしてデコードされます (以下を参照)
JSON と Object('json') は、クエリで output_format_native_write_json_as_string=1 を設定した場合にのみデコードされます。
各行は CHC_COL_STRING カラム内の 1 つの JSON ドキュメントとして渡されるため、文字列アクセサで読み取れます。
builder は chc_block_builder_append_json_string で同じ形式を書き込みます。その他の JSON
シリアル化バージョンでは、設定名を示した CHC_ERR_TYPE が返されます。
Variant, Dynamic, AggregateFunction はまだデコードされず、CHC_ERR_TYPE を返します。
フォールバックとして、サーバー側でそれらを String にキャストしてください。