ClickHouse¶
Connects to a ClickHouse server over the native (TCP) protocol. Both a source (a bounded
SELECT) and a sink (INSERT).
Overview¶
The ClickHouse connector integrates a ClickHouse server through the clickhouse-cpp native client. The sink batches incoming std::string records and issues INSERT INTO db.table FORMAT ... statements, where each record is one row encoded as either TSV or JSONEachRow. The source executes a single SELECT statement and drains the result blocks; it emits either typed ClickHouseRow records (column names plus stringified cell values) or, on the string channel, each row flattened to a delimiter-joined string or to a JSON object keyed by column name. Cell values are kept as text so the source stays schema-agnostic, with downstream operators parsing to concrete types.
Dependency and version¶
| Component | Provenance | Version |
|---|---|---|
clickhouse-cpp |
Debian: built from source (scripts/install-system-deps.sh); macOS: system package via brew |
v2.5.1 on Debian; brew version not pinned by clink |
| cityhash, lz4, zstd | Sibling archives linked alongside clickhouse-cpp (system) |
not pinned by clink |
| OpenSSL | System; linked when found, for the client TLS path | not pinned by clink |
The sink and source path do not use Arrow; records cross the connector boundary as std::string (or typed ClickHouseRow), not Arrow batches.
Enabling it¶
Controlled by the CLINK_WITH_CLICKHOUSE CMake option (AUTO / ON / OFF, default AUTO). Under AUTO the clink::clickhouse target is defined only if clickhouse-cpp is found (via find_package(clickhouse-cpp CONFIG) or by locating the headers and clickhouse-cpp-lib); with ON a missing client is a fatal configure error; OFF skips the target entirely. When the target is built, it compiles with CLINK_HAS_CLICKHOUSE defined; without the client the sink and source throw on construction.
On Debian the client is built from source by scripts/install-system-deps.sh (cloned at tag v2.5.1); on macOS it comes from brew.
cmake -S . -B build -DCLINK_WITH_CLICKHOUSE=ON
Factories¶
| Factory name | Direction | Record type |
|---|---|---|
clickhouse_sink |
sink | std::string |
clickhouse_row_source |
source | ClickHouseRow |
clickhouse_text_source |
source | std::string (columns joined by delim) |
clickhouse_source |
source | std::string (JSON object keyed by column name) |
The connector also registers the ClickHouseRow typed channel (kChannelClickHouseRow) with its codec so rows can travel end to end across the cluster without flattening.
Configuration¶
Sink (clickhouse_sink)¶
| Option | Required | Default | Description |
|---|---|---|---|
table |
yes | (none) | Target table name. Construction fails if empty. |
host |
no | localhost |
ClickHouse server host. |
port |
no | 9000 |
Native protocol port. |
database |
no | default |
Target database. |
user |
no | default |
Auth: username. |
password |
no | `` (empty) | Auth: password. |
format |
no | tsv |
Row encoding: tsv or jsoneachrow (also accepts JSONEachRow). Any other value falls back to TSV. |
batch_rows |
no | 1000 |
Buffered rows before a flush is forced. |
batch_interval_ms |
no | 1000 |
Time-based flush interval in milliseconds. |
Sources (clickhouse_row_source, clickhouse_text_source, clickhouse_source)¶
All three sources share the same option parser.
| Option | Required | Default | Description |
|---|---|---|---|
query |
yes | (none) | Single SELECT statement, no trailing semicolon. Construction fails if empty. |
host |
no | localhost |
ClickHouse server host. |
port |
no | 9000 |
Native protocol port. |
database |
no | default |
Database to connect to. |
user |
no | default |
Auth: username. |
password |
no | `` (empty) | Auth: password. |
batch_size |
no | 1024 |
Rows emitted per produce() call. A single server block may straddle multiple calls. |
delim |
no | \| |
clickhouse_text_source only: delimiter joining a row's column values into one string. Ignored by the other source factories. |
Sources expose host, port, database, user, password, query and batch_size; the source Options struct has no table field (the table comes from the query).
SQL usage¶
Mapped in src/sql/physical_plan.cpp as connector='clickhouse' for both a sink and a source.
The SQL source binds to the clickhouse_source factory (each row arrives as a JSON object keyed by column name and is bridged to a Row table via json_string_to_row). The SQL sink binds to clickhouse_sink with format=jsoneachrow forced by the binding, since each Row is serialised to a JSON object before insertion.
-- Source: read a bounded SELECT into a Row table
CREATE TABLE events_in (
id BIGINT,
name VARCHAR
) WITH (
connector = 'clickhouse',
host = 'clickhouse.internal',
port = '9000',
database = 'analytics',
user = 'reader',
password = 'secret',
query = 'SELECT id, name FROM analytics.events'
);
-- Sink: INSERT rows as JSONEachRow
CREATE TABLE events_out (
id BIGINT,
name VARCHAR
) WITH (
connector = 'clickhouse',
host = 'clickhouse.internal',
port = '9000',
database = 'analytics',
table = 'events_copy',
user = 'writer',
password = 'secret',
format = 'json'
);
mode='upsert' and exactly-once delivery are rejected at planning time for the SQL sink.
Example¶
Programmatic use through the fluent builder for the sink (clink/api/clickhouse_builders.hpp), which produces a SinkDescriptor for the clickhouse_sink factory:
#include "clink/api/clickhouse_builders.hpp"
auto sink = clink::api::ClickHouseSink::builder()
.host("clickhouse.internal")
.port(9000)
.database("analytics")
.table("events")
.user("writer")
.password("secret")
.format("jsoneachrow")
.batch_rows(5000)
.batch_interval_ms(2000)
.build();
// `sink` is a SinkDescriptor (op_type = "clickhouse_sink", channel = "string").
Using the typed connector class directly:
#include "clink/connectors/clickhouse_sink.hpp"
clink::ClickHouseSink::Options opts;
opts.host = "clickhouse.internal";
opts.table = "events";
opts.format = clink::ClickHouseSink::Format::JSONEachRow;
clink::ClickHouseSink sink(std::move(opts));
sink.open();
// sink.on_data(batch); sink.flush(); sink.close();
The source side is reached through the registered factories (clickhouse_row_source, clickhouse_text_source, clickhouse_source), each requiring a query.
Delivery semantics¶
Sink: at-least-once. Records are buffered and flushed by row count (batch_rows) or time (batch_interval_ms), and close() flushes the remaining buffer. There is no two-phase commit and no row deduplication, so an INSERT replayed after a failure re-inserts its rows. The SQL planner reflects this: it rejects exactly_once and mode='upsert'.
Source: a SELECT materialises a finite (bounded) result set. The source persists a cursor (the row index into the materialised snapshot) and can resume mid result-set after a restart; open() clamps a restored cursor to the re-materialised row count. Exactly-once at the source boundary holds only for a deterministically ordered query (an explicit ORDER BY) over data unchanged between runs, because row index N is "the same row" only under those conditions. The SQL source binding treats it as a bounded query with no cursor checkpoint.
Limitations¶
- Sink input is a single
std::stringper record, interpreted as one row (TSV or JSONEachRow). It is not a multi-column typed insert at the C++ sink layer; multi-column Rows are serialised to a JSON object string upstream (the SQL path) before the sink sees them. - Sink batches are concatenated in memory and inserted with
client.Execute(); there is no streaming insert, no 2PC, and no upsert/dedup. - Source is a one-shot bounded
SELECT, not a streaming tail or CDC feed. Result-row order is arbitrary without an explicitORDER BY. - The
clickhouse_source(string-channel JSON) requires column names from the server; if they are absent it fails loudly rather than emit positional keys. - The connector path is not Arrow-native; records cross the boundary as
std::stringor typedClickHouseRowtext values, with type coercion left to downstream operators. - Sink unsupported in SQL with
exactly_onceormode='upsert'(rejected at planning time).
Testing¶
The in-tree tests are in-process smoke tests, not live-server integration tests. They do not stand up a ClickHouse server and there is no CLINK_*_TEST_ENDPOINT gate. Instead they gate on whether the build linked clickhouse-cpp:
- If
clickhouse-cppis not linked,ClickHouseSink::is_real_implementation()/ClickHouseSource::is_real_implementation()return false and the real-impl testsGTEST_SKIP(). - When linked, the tests exercise the constructor and the lifecycle (
open()against an unreachable port should throw cleanly;flush()/close()beforeopen()must be safe), plus factory registration and the fluent builder.
Run them with:
cmake -S . -B build -DCLINK_WITH_CLICKHOUSE=ON -DCLINK_BUILD_TESTS=ON
cmake --build build -j --target clink_clickhouse_tests
ctest --test-dir build -L clickhouse
To exercise against a real server, point the sink/source options (host, port, database, table / query, user, password) at a running ClickHouse instance and run a job manually; there is no automated live test wired into the suite.