Nativeclick
Fork of Protryon/klickhouse (
masterbranch).
Nativeclick is a pure Rust client for ClickHouse over the native TCP protocol, for async (tokio) applications, with derive-based rows and minimal boilerplate.
Versioning and compatibility
- Version scheme: the crate version follows the ClickHouse version it targets,
0.MAJOR.MINOR.0.26.9targets ClickHouse 26.9. - Supported servers: each release supports the targeted version and the 10 releases before it. For
0.26.9that is ClickHouse 25.11 to 26.9, and CI runs the whole test suite against each of them. - Protocol: the client speaks native protocol revision 54493, and each connection negotiates
min(client, server). Every revision-dependent field and column format follows the negotiated revision, so older servers keep working. - Rust: MSRV is 1.89, tested in CI.
See CHANGELOG.md for the changes since klickhouse and how to migrate.
nativeclick vs klickhouse
Compared with klickhouse 0.15 (the fork point, protocol revision 54448):
| klickhouse 0.15 | nativeclick 0.26.9 | |
|---|---|---|
| Compatibility | ||
| Protocol revision | 54448 (ClickHouse 22.x era) | 54493, negotiated per connection |
| ClickHouse 26.9 (ZSTD answers by default) | ❌ empty results, then the connection dies | ✅ |
| Versions tested in CI | 25.8 | 25.11 → 26.9 (11 versions) |
| Compressed answers read | LZ4 only | LZ4, ZSTD, none |
| Sparse / replicated columns, size-stream strings | ❌ (revision too old to receive them) | ✅ |
| Reliability | ||
| Unsupported column type | ❌ "OK, 0 rows", then the connection silently dies | ✅ clear error naming the type |
Concurrent use of a cloned Client |
❌ can corrupt the stream | ✅ |
| INSERT row that fails to serialize | ❌ silently skipped | ✅ error, nothing sent for that block |
| Server-side INSERT errors (constraints…) | ❌ lost | ✅ returned |
DateTime64 before 1970 |
❌ panics | ✅ |
Decimal(P, 0), DateTime64(0) inserts |
❌ rejected | ✅ |
Server log packets (send_logs_level) |
❌ panic the connection | ✅ |
| Malformed server data | panics (unimplemented!, asserts) |
errors for offsets, maps, kinds, packets |
| Features | ||
| Per-query settings | ❌ | ✅ QueryOptions::setting |
Server-side {name:Type} parameters |
❌ | ✅ param, param_string |
| Query id | random, hidden | ✅ settable, used in progress events |
| Query timeout | ❌ | ✅ cancels on the server |
| Cancel a running query | ❌ keeps running | ✅ on stream drop or timeout |
| Connect timeout, quota key | ❌ | ✅ ClientOptions |
| Ping, server info (version, timezone, profile settings) | ❌ | ✅ ping(), server_info() |
| TLS in the bb8 pool | ❌ | ✅ ConnectionManager::with_tls |
| Types | ||
Bool |
as UInt8 |
✅ own type (u8 still reads it) |
Date32, Time, Time64, Interval* |
❌ | ✅ |
Nothing (SELECT NULL, []) |
❌ | ✅ |
| Named tuples | ❌ | ✅ |
Enum names with , ' ( |
❌ misparsed | ✅, read/write by value or by name |
LineString, MultiLineString, MultiPoint, Geometry |
❌ | ✅ |
Variant, Dynamic, JSON |
❌ | ✅ |
SimpleAggregateFunction |
❌ | ✅ |
AggregateFunction |
❌ | ❌ by design, explicit error pointing to finalizeAggregation |
| Code health | ||
| Unmaintained / dead dependencies | paste, compiler-tools, rustc_version + dead build.rs |
removed |
| Error kinds | catch-all ProtocolError(String) |
ConnectionClosed, Timeout, Compression, Unsupported, … |
| Tests | 13 integration tests, run on one version | 116 unit + 61 integration, on 11 versions |
Decoding speed of this crate's decoder before and after the 0.26.9 optimizations. Measured with examples/throughput.rs (2M rows × 5 columns, ClickHouse 26.9 on localhost, ZSTD answers):
| before | after | |
|---|---|---|
| SELECT, blocks | 4.0 M rows/s | 6.1 M rows/s |
| SELECT, derived rows | 2.8 M rows/s | 3.6 M rows/s |
| INSERT, derived rows | 0.94 M rows/s | 1.5 M rows/s |
Getting started
[]
= "0.26.9"
= { = "1", = ["macros", "rt-multi-thread"] }
use ;
async
More in the examples.
Queries
| Method | Use |
|---|---|
query::<T> |
stream of rows T: Row |
query_collect, query_one, query_opt |
all rows, the first one, or the first one if any |
query_raw |
stream of raw Blocks |
execute |
run a statement and wait for it; execute_now does not wait |
insert_native, insert_native_block |
insert rows; the query must end with FORMAT Native |
insert_native_raw |
insert raw Blocks |
Rows are any struct with #[derive(Row)], RawRow (dynamic access by index or name), or UnitValue<T> for a single column. Field names match column names, and can be adjusted with #[nativeclick(rename = "...")], rename_all, default, flatten, nested.
Per-query options
Client::with_options returns a handle on the same connection whose queries use the given options. The original handle is unchanged.
# async
- Parameters are values sent separately from the SQL, so they cannot inject SQL.
paramtakes the text exactly asclickhouse-client --param_x=...does: numbers, dates,[1,2],['a','b'].param_stringtakes any text for aStringparameter and escapes it.
- Timeouts fail the query with
NativeclickError::Timeoutand cancel it on the server, so the connection stays usable. - Dropping a result stream before its end also cancels the query on the server.
Connection
# async
A Client is cheap to clone and can be used from many tasks; queries on one connection run one after the other. For parallelism, use several connections, for example with the bb8 pool:
// features = ["bb8"] (and "tls" for with_tls)
let manager = new.await?;
let pool = builder.max_size.build.await?;
let client = pool.get.await?;
Connection-level failures are reported to every waiting call, and later calls on the same Client return the error that closed it. Client::is_closed() tells when to reconnect, and the bb8 pool does it for you.
Errors
NativeclickError is #[non_exhaustive] and Clone. The main variants:
| Variant | Meaning |
|---|---|
ServerException { code, name, message, .. } |
ClickHouse rejected the query |
Timeout |
QueryOptions::timeout or ClientOptions::connect_timeout expired |
ConnectionClosed |
the connection is gone |
DeserializeError, SerializeError, UnexpectedType*, TypeParseError |
a value or type does not match |
Compression, ProtocolError |
corrupt frame or unexpected server data |
Unsupported |
the server is too old for a requested feature |
Types
| ClickHouse | Rust |
|---|---|
Int8…Int256, UInt8…UInt256, Float32/64, BFloat16 |
integers, floats, i256/u256, bf16 |
Bool |
bool (u8 also reads it) |
Decimal(P, S), Decimal32/64/128/256 |
FixedPoint32/64/128/256<S>, rust_decimal::Decimal |
String, FixedString(N) |
String, Bytes; Vec<u8> writes |
UUID, IPv4, IPv6 |
Uuid, Ipv4, Ipv6 |
Date, Date32 |
Date, Date32, chrono::NaiveDate |
DateTime, DateTime64(p) |
DateTime, DateTime64<P> (from_ticks/ticks for dates before 1970), chrono::DateTime |
Time, Time64(p) |
chrono::TimeDelta |
Interval* |
i64 |
Enum8, Enum16 |
i8/i16 (value) or String (name) |
Nullable(T), Nothing |
Option<T> |
Array(T) |
Vec<T>, [T; N] |
Map(K, V) |
HashMap, BTreeMap, IndexMap |
Tuple(...), named tuples |
Rust tuples |
LowCardinality(T), SimpleAggregateFunction(f, T) |
as T |
Point, Ring, LineString, MultiPoint, Polygon, MultiLineString, MultiPolygon |
Point, Ring, Polygon, MultiPolygon (and geo-types with the feature) |
Variant(...), Dynamic, Geometry |
Value / DynamicValue (value plus actual type), or T / Option<T> when rows have that type |
JSON |
String (JSON text), Json<T> (serde) |
AggregateFunction(...) |
not readable: states have no generic format. Select finalizeAggregation(col) instead. |
Notes:
- JSON and Dynamic formats: the client asks the server for
JSONcolumns as text and for the flattenedDynamiclayout, with two settings sent before the caller's own (which can override them). - Writing
Variant/Dynamic/Geometry:- a
DynamicValuechooses the type explicitly; - a plain value goes to the first variant that accepts it (
Variant) or to its natural type (Dynamic).
- a
- Recent column formats: sparse and replicated columns, and the size-stream string layout (revision 54492), are decoded transparently.
Feature flags
derive: the#[derive(Row)]macro. Default.compression: compression of client/server traffic: writeslz4, readslz4andzstd. Default.serde:serdesupport on values, and theJson<T>wrapper. Default.bfloat16: theBFloat16type. Default.tls: TLS via tokio-rustls, forClient::connect_tlsand the bb8 pool.bb8: a bb8ConnectionManager.refinery: migrations via refinery.geo-types: conversions with the geo-types crate.rust_decimal: conversions with rust_decimal.
Running the tests
The integration tests need a ClickHouse server:
# export NATIVECLICK_TEST_USER=default NATIVECLICK_TEST_PASSWORD= NATIVECLICK_TEST_DATABASE=default
The tests share one server and its tables, so run them on one thread.
To measure throughput:
NATIVECLICK_TEST_ADDR=127.0.0.1:19000
Credit
Nativeclick started as a fork of klickhouse by Protryon. nativeclick_derive was made by copy/paste/simplify of serde_derive to get maximal functionality and performance at the lowest time cost. In a prototype, serde was used directly, but this was abandoned because of the lock-in of serde's data model.