Please check the build logs for more information.
See Builds for ideas on how to fix a failed build, or Metadata for how to configure docs.rs builds.
If you believe this is docs.rs' fault, open an issue.
clickhouse-cxx
A thread-affine ClickHouse SDK with completion-driven socket I/O. Native TCP and
TCPS use clickhouse-cpp
through cxx; HTTP and HTTPS use
nexora-rs with HTTP/1.1, HTTP/2,
and rustls.
Typed rows use #[derive(Row)]; serde-only structs use SerdeBridge.
Why clickhouse-cxx?
ClickHouse rewards large columnar inserts, but the worker filling those columns may also need a small query result before its next decision. A market-data collector or strategy engine often already owns its ingest buffers, flush cadence, and query budget. In a shared-nothing design, keeping that work on its original worker matters as much as batching the rows efficiently.
Async-first Rust clients such as
clickhouse-rs build on Tokio and
Hyper. Using that model from a synchronous pipeline means introducing a runtime
and a block_on bridge, or propagating async through the processing code.
Executor queues and wakeups add scheduling boundaries that can contribute to
jitter and tail latency around otherwise short ClickHouse operations.
clickhouse-cxx keeps connection progress on the calling thread. Native TCP and
TCPS use clickhouse-cpp, with socket operations driven by a persistent
single-threaded liburing::Proactor and hostname lookup through cares-rs.
HTTP/1.1 and HTTP/2 use nexora-rs on the same caller-owned execution model.
Neither transport requires an async runtime. Native queries and convenience
HTTP calls return synchronously; HTTP submission and polling also let the worker
interleave other work while a response is pending.
The data path stays column-oriented. Reusable batches hold insert columns, and
BlockView<'a> borrows fixed-width query columns from the backing block without
materializing an owned Rust object for every row. ThreadLocalClient reuses one
client per OS thread; WorkerClient leases worker slots and balances connections
on the calling thread. Blocks, borrowed views, and callbacks stay with their
owner instead of being handed to another task or thread.
That ownership also helps when an INSERT stalls or a query times out. Breakpoints
in batch construction, block decoding, and deadline handling lead through explicit
client calls and completion dispatch, without an executor's task-polling stack.
LLM-assisted query and ingestion code has the same concrete lifecycle to follow:
borrow a client, submit or execute the query, consume its result, and release the
borrow. There is no runtime handle to invent or borrow to carry across an await.
The goal is predictable client-side work for high-throughput ingestion and latency-sensitive queries: explicit buffer reuse, local ownership, and no executor handoffs. This does not bound ClickHouse execution time, network delay, or OS scheduling; it gives each worker direct control over its part of the request.
Design
- Completion-driven: Native sockets use a persistent thread-local
liburing::Proactorand c-ares DNS. HTTP exposes nonblocking submission and single-turn polling through Nexora. No external executor runs the client. - Compatibility calls: Native C++ protocol operations and the convenience HTTP calls retain synchronous return semantics. Native socket adapters drive SQEs to completion on that thread; they do not use blocking socket reads or writes. Use the HTTP submission interface to interleave application work.
- Thread-affine:
Client, transport,Proactor, andTlsSessionhandles are!Send + !Sync. Sockets, blocks, views, and callbacks stay on the thread that created them, and the I/O path uses no blocking locks. thread_local!scaling:ThreadLocalClientkeeps one client per calling OS thread.WorkerClientleases lock-free slots on the calling thread and balances requests across its own connections. No request or result crosses a thread boundary.- Dual transport: Native TCP/TCPS and HTTP/HTTPS expose the same
Block,BlockView,Batch, and row APIs. - Zero-copy reads:
BlockView<'a>projects fixed-width columns as&'a [T]over the native buffers. - Reusable buffers:
BatchandBlockexpose explicit capacity retention throughBufferPolicy.
Install and feature matrix
crates/clickhouse defines default = ["tcp"]. Native TCP and TCPS are
available out of the box; the HTTP transport is opt-in.
| Build | Cargo features | Transports | ClickHouse C++ toolchain |
|---|---|---|---|
| Native TCP/TCPS (default) | default (tcp) |
tcp://, tcps:// |
clickhouse-sys + clickhouse-cpp |
| HTTP/2 only | default-features = false, features = ["http"] |
http://, https:// |
none |
| Both | features = ["tcp", "http"] |
all schemes | required |
| Tracing | add "tracing" to either build |
unchanged | unchanged |
# Native TCP/TCPS
= { = "crates/clickhouse" }
# HTTP/2 only; clickhouse-sys is not compiled
= { = "crates/clickhouse", = false, = ["http"] }
# Both transports
= { = "crates/clickhouse", = ["tcp", "http"] }
# Cold control-plane telemetry
= { = "crates/clickhouse", = ["tracing"] }
http pulls nexora from the main branch of
https://github.com/trunkszi/nexora-rs.git, plus
lz4_flex and zstd. HTTP requires Linux io_uring and a C build toolchain
for the upstream ring and DNS bindings, without compiling ClickHouse C++.
tcp pulls clickhouse-sys and cxx, plus Git-main liburing and cares for
Native I/O and DNS;
clickhouse-sys builds clickhouse-cpp 3.x statically with CMake. The
tracing feature emits only connection lifecycle, failover, and OnServerLog
events; data-plane reads and writes emit no logs.
crates/clickhouse-sys/build.rs resolves the C++ source through
CLICKHOUSE_CPP_DIR, then ../../vendor/clickhouse-cpp, then
target/vendor-cache/clickhouse-cpp. It contains no hardcoded absolute path.
Quick start
use ;
use RowClient;
Derived rows and zero-copy views
#[derive(Row)] generates four companions:
EventBatch, the reusable struct-of-arrays write buffer;EventBlockView<'a>, the checked view over aBlock;EventRef<'a>, a borrowed row made of&str,&[u8], and borrowed slices;- the
RowandOwnedRowimplementations used byRowClient.
let block = batch.to_block?;
let view = try_from_block?;
for row in view.iter
// Fixed-width columns borrow block storage directly.
let ids: & = view.event_id;
assert_eq!;
// Refill a batch without rebuilding its column buffers.
batch.clear;
batch.push_ref;
Row attributes cover table metadata, column naming, binary strings, and renamed
runtime crates: #[row(table = "...")], #[row(rename_all = "...")],
#[row(column = "...")], #[row(bytes)], and #[row(crate_path = "...")].
The derive also honors #[serde(rename_all)], #[serde(rename)], and
#[serde(with = "serde_bytes")].
Three read modes
Owned rows
Use owned materialization when the row must outlive its backing block.
let rows: = client.query_rows?;
let serde_rows: = block_to_rows?;
Streaming callbacks
Callbacks keep the backing block alive and avoid an intermediate row vector.
client.?;
let mut kinds = Vecnew;
client.?;
let count = client.?;
let decoded = ?;
let decoded = ?;
Caller-supplied memory
The _into and _into_slice methods write into buffers the caller already
owns, so a steady-state reader can reuse one allocation.
let mut ids = vec!;
let copied = native.read_into_slice?;
let mut payload = Vecnew;
let len = native.read_bytes_into?;
let mut events: = repeat_with.take.collect;
let written = client.query_rows_into_slice?;
let mut batch = with_capacity;
let written = client.?;
let mut block = new;
let written = client.?;
let mut metrics = repeat_with.take.;
let written = from_rowbinary_into_slice?;
Column::read_into_slice and Column::read_bytes_into provide the same
destination-buffer contract for the pure-Rust row layer.
Multi-threaded scaling on the calling thread
Client is !Send + !Sync; share a factory or a worker pool instead of a
client. WorkerClient leases N slots per calling thread, so every connection
and every callback still runs on that thread.
use thread;
use ;
let config = parse?.with_workers;
let workers = connect_workers?;
scope;
ThreadLocalClient is the one-client-per-thread form:
let local = thread_local?;
scope;
Each worker slot stripes its endpoint list with
endpoints[(worker_id + step) % endpoints.len()], so concurrent workers start
on different nodes while each slot keeps its own connections.
BufferPolicy memory governance
BufferPolicy::default() retains at most 65,536 rows and 8 MiB per buffer.
unbounded() keeps every warmed allocation; minimal() releases capacity on
every clear.
let mut policy = default;
policy.max_retained_rows = 32_768;
policy.max_retained_bytes = 4 * 1024 * 1024;
let before = batch.allocated_bytes;
batch.clear_with_policy;
batch.shrink_to_fit;
let after = batch.allocated_bytes;
The same policy applies to Block::clear_with_policy, Block::shrink_to_fit,
and every generated <Name>Batch.
SerdeBridge
SerdeBridge accepts structs that implement only serde::Serialize and
serde::Deserialize.
use BTreeMap;
let = to_rowbinary?;
let rows: = from_rowbinary?;
let block = rows_to_block?;
let rows: = block_to_rows?;
Option<T> maps to Nullable, Vec<T> to Array, BTreeMap/HashMap to
Map(K, V), and serde_bytes payloads to binary String. Logical fixed
columns (Date, Date32, DateTime, DateTime64, IPv4,
Decimal32/64/128) decode into their underlying integer width; UUID and
IPv6 decode into a serde_bytes byte field.
Native TCPS and failover
use Duration;
use ;
let tls = new
.with_ca_pem
.with_sni
.with_connect_timeout
.with_read_timeout;
let policy = new
.with_selection
.with_send_retries
.with_retry_timeout
.with_circuit;
let config = parse?
.with_tls
.with_policy
.on_failover;
let mut client = new?;
client.ping?;
The four selection strategies are RoundRobin, Random, LatencyAware, and
Sticky. A circuit opens after the configured consecutive failure threshold
and admits a half-open probe after the cooldown. Failover never replays a write
whose server-side outcome is uncertain.
Column family map
| ClickHouse family | Rust surface | Notes |
|---|---|---|
| Numeric | u8..i128, f32, f64, WireColumn::fixed |
fixed-width &[T] |
| Bool | ChBool, bool |
one byte |
| String | StringColumn, String, &str, Cow<str> |
UTF-8 or binary |
| FixedString(N) | FixedStringColumn::new(N) |
fixed byte width |
| Date/DateTime/Time | Date, Date32, DateTime, DateTime64, Time, Time64 |
integer newtypes |
| Decimal | Decimal32, Decimal64, Decimal128, Decimal256Column |
width follows precision |
| UUID | Uuid([u8; 16]) |
16 bytes |
| IPv4/IPv6 | Ipv4(u32), Ipv6([u8; 16]) |
fixed width |
| Enum8/Enum16 | Enum8(i8), Enum16(i16) |
fixed width |
| Nullable(T) | NullableColumn, Option<T> |
null map plus values |
| Array(T) | ArrayColumn, Vec<T> |
flat offsets |
| LowCardinality(T) | LowCardinalityColumn |
dictionary encoded |
| Tuple(...) | TupleColumn |
composite columns |
| Map(K, V) | MapColumn, BTreeMap, HashMap |
key and value columns |
| Geo | GeoColumn, GeoKind |
Point, Ring, Polygon, MultiPolygon |
| JSON | StringColumn::new_json() |
JSON column |
| Nothing | NothingColumn |
empty payload |
Native column ownership
Column projections borrow their owning column or block without copying. Native
block attachment shares storage initially; mutating an independent owner first
detaches the shared column tree. Extracted column and nested-child handles retain
their original storage across changes to another owner. This copy-on-write step
may allocate. Projection caches remain immutable until an exclusive mutation.
The raw sys mutation methods require Pin<&mut ColumnHandle>; shared handles
provide read-only access.
Native completion-driven I/O
Each calling thread retains one ring and a fixed 1 MiB DMA allocation. Native
TCP and TCPS open nonblocking close-on-exec descriptors and submit connect,
receive, and send SQEs. TLS records use StreamOwned<ClientConnection, ProactorStream> over the same socket adapter. IP literals bypass DNS; hostnames
submit IPv4 and IPv6 lookups through cares::ProactorResolver on that ring.
DNS and address attempts share one connection deadline.
Read and write budgets use independent timeout SQEs. When a deadline expires, the adapter cancels the target and consumes both the target and cancellation completions before reusing its DMA bytes. A failed drain quarantines the socket and retains referenced allocations. The synchronous C++ callback ABI still waits for each operation by advancing the ring; it does not provide a resumable Native query state machine to application callers.
HTTP, RowBinary, and Arrow
Enable http to compile the synchronous Nexora/rustls transport, LZ4/Zstd body
compression, and HTTP client settings. Shared Block/Batch/View, serde conversion,
Arrow IPC projection, and format codecs remain available in TCP-only and
HTTP-only builds. A disabled transport returns an explicit error before IO.
Client::connect accepts one http:// or https:// endpoint. Managed
ClientConfig endpoint lists, Native TLS settings, and failover policies remain
Native TCP/TCPS configuration; HTTP or mixed lists are rejected. HTTP setters
return Result and reject Native clients when both transports are enabled.
Each HTTP transport owns a nexora::Proactor and nexora::HttpClient on the
calling thread. IP literals are pinned with resolve_host; hostnames use
Nexora's non-blocking c-ares resolver within the request deadline.
Requests use the POST method and structured headers. The caller advances the
completion state machine with submit and drive; no background runtime runs.
Linux with working io_uring support is required for HTTP requests.
HttpVersionChoice |
Cleartext | TLS |
|---|---|---|
Http1 |
HTTP/1.1 | HTTP/1.1 |
Http2PriorKnowledge |
HTTP/2 prior knowledge | HTTP/2 prior knowledge preference |
Http2Tls |
Protocol error | Offers only h2; rejects incompatible TLS peers before POST |
Negotiate |
HTTP/1.1 | HTTP/2 ALPN preference, HTTP/1.1 fallback |
New HTTP and HTTPS clients default to Negotiate; select Http2Tls explicitly
to require HTTP/2 without fallback, including when a peer omits ALPN. http_preferences() describes requested options; negotiated_http_version()
reports the structured response version. Request deadlines include DNS and connection
establishment; connection attempts are also capped at ten seconds. After a
protocol change, submit_http returns a configuration error until the explicit,
synchronous reset_http() boundary applies it. Convenience HTTP calls apply
pending configuration synchronously. Destruction drains outstanding socket and
timer completions on the owning thread. Redirects are not
followed, so credentials are never replayed to a redirect destination.
For application-controlled progress, submit raw SQL without waiting for a peer:
let mut request = client.submit_http?;
while !request.is_complete
let body = request.try_take.expect?;
assert_eq!;
submit_http owns the SQL and body before returning. poll_http advances one
bounded DNS/network turn; it never waits for response bytes. try_take consumes
the result once and may perform bounded decompression. cancel_http(&request)
terminates an owned request exactly once. Foreign-client cancellation and a
protocol change with pending requests return configuration errors. Dropping the
client terminates outstanding handles; dropping a handle alone does not cancel
its network request. reset_http() explicitly cancels all pending handles and
synchronously drains the old driver; keep it outside nonblocking polling loops.
A single request failure never implicitly resets the driver or aborts its sibling
requests. These entry points require an HTTP endpoint; Native query methods retain
their synchronous C++ ABI.
Typed RowClient operations encode shared Block columns as FORMAT Native.
HTTP serde inserts use Client::serde_insert_rows with FORMAT RowBinary;
serde_query_rows(&Query) requests RowBinaryWithNamesAndTypes and maps returned
columns to serde fields by name. The block-based trait methods insert_serde
and query_serde preserve their existing semantics. HTTP Native decoding supports
fixed widths, String, Nullable, Array, and Map; unsupported layouts return errors
rather than silently altering their representation.
set_compression selects HTTP LZ4 or Zstd body compression. Query::compression
overrides it for one request. Query settings and deduplication tokens are
forwarded to the server; the destination table engine controls deduplication
semantics. Statements and inserts are not automatically replayed on failure.
For raw data, fetch_bytes(&Query) collects a response and
insert_formatted(&Query, &[u8]) sends the format specified by SQL. The raw
callback read APIs accept borrowed receivers and caller-owned destinations:
query_raw_with(&Query, callback)callsFnMut(&[u8]) -> Result<()>with decoded chunks after the encoded response is collected. A chunk may only be borrowed during that invocation. Receiver errors stop delivery and propagate unchanged.query_raw_into(&Query, &mut Vec<u8>)clears length and reuses existing capacity.query_raw_into_slice(&Query, &mut [u8])writes from offset zero and preserves unused tail bytes. Overflow returns an error; an error may leave a partial prefix, which must not be consumed as a complete response.
Raw callback delivery supports identity, LZ4, and Zstd responses. Encoded and
decoded bodies each have a 256 MiB limit. The encoded response is buffered by
Nexora; decompression feeds a reusable 16 KiB stack buffer to the receiver.
Callback delivery therefore does not provide network backpressure or delivery
before the response completes. select_http_stream(&Query) instead fully buffers decoded shared
blocks. http_inserter(table) stages shared blocks and sends them only at end;
dropping it unfinished performs no insertion. These HTTP APIs do not replace
Native select_stream and inserter, which retain their interactive session,
cleanup, and caller-thread contracts.
ArrowBlockView::decode_stream validates and owns decoded Arrow record batches;
IPC parsing can allocate. Primitive slice and string projections borrow the
decoded arrays without copying. Nullable primitive columns require validity-aware
column access. Malformed IPC and trailing bytes return errors.
Examples
The tutorial under crates/clickhouse/examples/ runs offline by default. Live
sections read CLICKHOUSE_URL (tcp://127.0.0.1:9000), CLICKHOUSE_USER,
CLICKHOUSE_PASSWORD, and CLICKHOUSE_DATABASE.
| Example | Topic |
|---|---|
01_quickstart_native_tcp |
Block and WireBlock construction, schema inspection, zero-copy slices, optional Native TCP round-trip. |
02_derive_row_and_zero_copy_views |
#[derive(Row)], reusable Batch, BlockView, Ref, and callback/caller-buffer reads. |
03_memory_policy_and_batch_reuse |
BufferPolicy, allocated_bytes, clear_with_policy, and shrink_to_fit. |
04_full_column_type_matrix |
All 17 column families, including wrappers, Geo, JSON, and Nothing. |
05_dual_transport_and_row_client |
One RowClient surface over an in-memory transport and Native TCP. |
06_serde_compatibility |
RowBinary and Block conversion, maps, logical types, and serde attributes. |
07_thread_local_and_workers |
Thread-local factories and caller-thread worker slot leasing without sharing client handles. |
08_tls_and_failover_policy |
Native TCPS trust, four endpoint policies, observers, and circuit diagnostics. |
09_http2_rowbinary_and_arrow |
HTTP protocol selection, serde RowBinary, Arrow IPC projections, and incremental caller-owned response sinks. |
The TLS example reads CLICKHOUSE_TCPS_URLS for its live section, with
CLICKHOUSE_CA_PEM and CLICKHOUSE_TLS_SNI for custom trust and identity.
Testing
cargo test -p clickhouse -p clickhouse-macros starts
clickhouse/clickhouse-server:26.8.2.7 on ephemeral Native and HTTP ports,
runs the typed client operations, and removes its container on drop.
Benchmarks
The three benchmark targets use a fixed-iteration harness with no statistics
framework dependency. Run cargo bench -p clickhouse --all-features; set
CLICKHOUSE_BENCH_ITERATIONS to a positive count (default 100). -- --test
runs one measured iteration per workload as a smoke check. Sonic serializes each
measurement as JSON with mean nanoseconds per iteration and elements per second,
without confidence intervals.
Build and validation
Run the standard workspace checks from the repository root:
RUSTDOCFLAGS="-D warnings"
The GitHub workflow uses ubuntu-24.04 and the nightly Rust toolchain. Transport
changes also require cargo test --workspace --all-features and HTTP-only Clippy
with cargo clippy -p clickhouse --no-default-features --features http --all-targets -- -D warnings.
Live tests provision owned ClickHouse Docker fixtures; the Docker daemon must be
available. Default workspace tests cover Native TCP; the all-feature run also
covers the HTTP transport.