Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
ecd96bd
feat(native): add Bool type and sparse serialization support
catinspace-au Mar 10, 2026
a942a2a
feat(native): comprehensive type coverage — extended scalars, DateTim…
catinspace-au Mar 10, 2026
5dfe86c
feat(native): connection pooling for native TCP transport
catinspace-au Mar 10, 2026
40d8df6
feat(native): add native transport infrastructure — INSERT, encoder, …
catinspace-au Mar 10, 2026
971e98c
refactor(native): deadpool pool, cursor drain, connection health chec…
catinspace-au Mar 10, 2026
1688e11
feat(native): LowCardinality INSERT + fix LC(Nullable(T)) reader
catinspace-au Mar 10, 2026
a99623d
feat: AsyncInserter<T> — concurrent MPSC inserter for HTTP + native TCP
catinspace-au Mar 12, 2026
06e56a5
test: add Filebeat/Winlogbeat JSON payload tests for AsyncInserter
catinspace-au Mar 12, 2026
0563b0d
docs: add extended documentation for native transport, batching, type…
catinspace-au Mar 12, 2026
d2c26d4
docs: convert ASCII diagrams to Mermaid for GitHub rendering
catinspace-au Mar 12, 2026
b53115b
docs: rename branch prefix from feature/ to hyperi/
catinspace-au Mar 12, 2026
85e3afb
feat(dynamic): add ParsedType and DynamicError modules
catinspace-au Mar 17, 2026
c6dabdc
feat(dynamic): add DynamicSchema, cache, and system.columns fetch
catinspace-au Mar 17, 2026
3c1fa90
feat(dynamic): runtime RowBinary encoder for serde_json::Value
catinspace-au Mar 17, 2026
e5460f3
feat(dynamic): DynamicInsert API with schema recovery
catinspace-au Mar 17, 2026
6103ad9
feat(dynamic): DynamicBatcher — async auto-flushing dynamic inserter
catinspace-au Mar 17, 2026
ba0beb5
test(dynamic): integration tests for DynamicInsert and DynamicBatcher
catinspace-au Mar 17, 2026
d04283d
sec: bump lz4_flex 0.11.3 -> 0.11.6 (GHSA-vvp9-7p8x-rfvv)
catinspace-au Mar 18, 2026
b171413
fix: replace abandoned fxhash and linked-hash-map dev-deps
catinspace-au Mar 19, 2026
2888cf9
fix: remove polonius-the-crab, inline unsafe reborrow (RUSTSEC-2024-0…
catinspace-au Mar 19, 2026
d8e63d6
test: add cursor reborrow tests for polonius removal
catinspace-au Mar 19, 2026
9166480
Merge branch 'hyperi/remediation-polonius' into hyperi/optimise-1
catinspace-au Mar 19, 2026
5a571ff
chore: remove STATE.md — content merged into CLAUDE.md (local)
catinspace-au Mar 24, 2026
f4daec0
fix: restore .gitignore to upstream state (local excludes in .git/inf…
catinspace-au Mar 24, 2026
523424c
feat(unified): add UnsupportedTransport error variant
catinspace-au Mar 24, 2026
10ece36
feat(unified): add Transport enum and UnifiedClient with builders
catinspace-au Mar 24, 2026
0aa2386
feat(unified): add UnifiedQuery with execute/fetch dispatch
catinspace-au Mar 24, 2026
5a86c58
feat(unified): add UnifiedCursor<T> with owned-only dispatch
catinspace-au Mar 24, 2026
53cf26d
feat(unified): add UnifiedInsert<T> with write/end dispatch
catinspace-au Mar 24, 2026
99e5194
feat(unified): add ping() dispatch
catinspace-au Mar 24, 2026
91925fa
feat(unified): expose ServerVersion from native handshake
catinspace-au Mar 24, 2026
31350a5
feat(unified): expose PoolStats from deadpool
catinspace-au Mar 24, 2026
038d1f7
feat(native): add per-query settings and query ID to NativeQuery
catinspace-au Mar 24, 2026
0dbf2aa
feat(unified): wire query ID and settings into UnifiedQuery
catinspace-au Mar 24, 2026
c78fee4
feat(native): make Progress and ProfileInfo structs public
catinspace-au Mar 24, 2026
bad02c4
feat(native): add observability callbacks for Progress and ProfileInfo
catinspace-au Mar 24, 2026
0a4e089
feat(unified): wire observability callbacks into UnifiedQuery
catinspace-au Mar 24, 2026
a88a2b7
feat(unified): make DynamicInsert transport-agnostic (HTTP insert, bo…
catinspace-au Mar 24, 2026
0c5355d
feat(native): add named parameter support via param_ settings
catinspace-au Mar 24, 2026
c71bff2
feat: add optional tracing feature for query instrumentation
catinspace-au Mar 24, 2026
74d7325
feat(native): multi-host round-robin failover
catinspace-au Mar 24, 2026
2aef05c
feat(unified): add query cancellation via KILL QUERY
catinspace-au Mar 24, 2026
8f5df19
fix: gate transport_name() behind tracing feature to suppress dead_co…
catinspace-au Mar 24, 2026
ed651f5
fix: resolve all dead_code warnings in native transport module
catinspace-au Mar 24, 2026
c42ee99
fix(tests): make test infra configurable for remote ClickHouse deploy…
catinspace-au Mar 24, 2026
473222f
feat(native): add TLS support via rustls (MaybeTlsStream)
catinspace-au Mar 25, 2026
324db06
refactor: extract shared Quantities and Ticks for both transports
catinspace-au Mar 25, 2026
1cf7aa7
feat(http): add ping() and server_version() for transport parity
catinspace-au Mar 25, 2026
ebbfea0
feat(native): add insert timeouts (send_timeout, end_timeout)
catinspace-au Mar 25, 2026
45e2a83
feat(http): add with_query_id() and with_settings() to Query
catinspace-au Mar 25, 2026
c33561b
test(native): add tests for new features — server version, pool stats,
catinspace-au Mar 25, 2026
ee886d6
feat(native): add role support (SET ROLE) for transport parity
catinspace-au Mar 25, 2026
aee758d
fix(native): send param_ settings with Custom flag (0x02) on wire
catinspace-au Mar 25, 2026
d91ff61
fix: replace all non-ASCII characters in code comments with ASCII
catinspace-au Mar 25, 2026
03f14bb
fix(native): consume nested exceptions, fix parameter wire position
catinspace-au Mar 25, 2026
d0b97e8
fix(native): strip param_ prefix for native wire parameters
catinspace-au Mar 25, 2026
3a2566b
test: add upstream backwards compatibility test suite
catinspace-au Mar 25, 2026
05bcc83
fix: replace panic with warning on DNS resolution failure in with_addr
catinspace-au Mar 25, 2026
55da383
fix: remove tracing dependency from with_addr (tracing is optional)
catinspace-au Mar 25, 2026
4e7796e
fix: re-export PoolStats from crate root
catinspace-au Mar 25, 2026
95042e8
perf: port DFE hot path optimisations to encode pipeline
catinspace-au Mar 26, 2026
809d83f
docs: explain hot path optimisations in encode.rs
catinspace-au Mar 26, 2026
215f1ee
fix: DT review -- SQL injection, escaping, Defence in depth
catinspace-au Mar 27, 2026
a9ee478
fix: DT review -- stale test DB race on cluster DDL propagation
catinspace-au Mar 27, 2026
14aaedc
fix: DT review -- fetch_schema injection + array/map offset underflow
catinspace-au Mar 27, 2026
ebf5f5b
fix: DT review -- upstream security fixes and bug corrections
catinspace-au Mar 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 24 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -92,12 +92,25 @@ default = ["lz4"]

test-util = ["hyper/server"]
inserter = ["dep:quanta"]
async-inserter = ["inserter", "tokio/time", "tokio/sync"]
batcher = ["async-inserter"]
uuid = ["dep:uuid"]
time = ["dep:time"]
lz4 = ["dep:lz4_flex", "dep:cityhash-rs"]
chrono = ["dep:chrono"]
futures03 = []

## Native TCP protocol transport
native-transport = ["dep:cityhash-rs", "dep:lz4_flex", "dep:zstd", "dep:socket2", "dep:deadpool", "tokio/net", "tokio/io-util"]

## TLS for the native TCP transport (rustls). Enables `NativeClient::with_tls()`.
## Reuses the same rustls already pulled in by rustls-tls-*.
## Loads both webpki (public) and native (OS) root certs for maximum compat.
native-tls-rustls = ["native-transport", "dep:tokio-rustls", "dep:rustls", "dep:webpki-roots", "dep:rustls-native-certs"]

## Optional tracing instrumentation for query execution
tracing = ["dep:tracing"]

## TLS
native-tls = ["dep:hyper-tls"]
# ext: native-tls-alpn
Expand Down Expand Up @@ -141,18 +154,25 @@ hyper-rustls = { version = "0.27.3", default-features = false, features = [
url = "2.1.1"
futures-util = { version = "0.3.5", default-features = false, features = ["sink", "io"] }
futures-channel = { version = "0.3.30", features = ["sink"] }
lz4_flex = { version = "0.11.3", default-features = false, features = [
lz4_flex = { version = "0.11.6", default-features = false, features = [
"std",
], optional = true }
cityhash-rs = { version = "=1.0.1", optional = true } # exact version for safety, this package has been stable for years
zstd = { version = "0.13", optional = true }
socket2 = { version = "0.6", features = ["all"], optional = true }
uuid = { version = "1", optional = true }
time = { version = "0.3", optional = true }
chrono = { version = "0.4", optional = true, features = ["serde"] }
bstr = { version = "1.11.0", default-features = false }
quanta = { version = "0.12", optional = true }
polonius-the-crab = "0.5.0"

bnum = "0.13.0"
deadpool = { version = "0.12", features = ["managed", "rt_tokio_1"], optional = true }
tracing = { version = "0.1", optional = true }
tokio-rustls = { version = "0.26", default-features = false, optional = true }
webpki-roots = { version = "1", optional = true }
rustls-native-certs = { version = "0.8", optional = true }
serde_json = "1"
rustc-hash = "2"

[dev-dependencies]
clickhouse-macros = { version = "0.3.0", path = "macros" }
Expand All @@ -161,8 +181,7 @@ serde = { version = "1.0.106", features = ["derive"] }
tokio = { version = "1.0.1", features = ["full", "test-util", "io-util"] }
hyper = { version = "1.1", features = ["server"] }
indexmap = { version = "2.10.0", features = ["serde"] }
linked-hash-map = { version = "0.5.6", features = ["serde_impl"] }
fxhash = { version = "0.2.1" }
rustc-hash = "2"
serde_bytes = "0.11.4"
serde_json = "1"
serde_repr = "0.1.7"
Expand Down
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -602,6 +602,42 @@ The functionality can be enabled with the `test-util` feature. Use it **only** i

See [the example](https://github.com/ClickHouse/clickhouse-rs/tree/main/examples/mock.rs).

## Native TCP Transport (HyperI Fork)

This fork adds a native TCP protocol client (`feature = "native-transport"`)
that connects on port 9000 — the same binary protocol used by `clickhouse-client`
and the Go client.

```rust
use clickhouse::native::NativeClient;

let client = NativeClient::default()
.with_addr("localhost:9000")
.with_database("default")
.with_lz4();
```

The same `#[derive(Row)]` structs work with both HTTP and native transports.

### Additional feature flags (HyperI)

| Feature | Description |
|---|---|
| `native-transport` | Native TCP client with connection pooling, SELECT + INSERT |
| `async-inserter` | `AsyncInserter<T>` — concurrent MPSC-based inserter (HTTP + native) |
| `batcher` | `TableBatcher<T>` — Go-style `append`/`flush`/`send` wrapper |

### Extended documentation

| Guide | Description |
|---|---|
| [Native Transport](docs/native-transport.md) | Connect, query, and insert over TCP |
| [Connection Pooling](docs/connection-pooling.md) | Deadpool pool, health checks, recycling |
| [Batching](docs/batching.md) | AsyncInserter, AsyncNativeInserter, TableBatcher |
| [Types](docs/types.md) | Full type coverage matrix (HTTP + native) |
| [Wire Format](docs/wire-format.md) | LowCardinality, Dynamic, Variant encoding internals |
| [Migration](docs/migration.md) | HTTP vs native trade-offs, switching guide |

## Support Policies

### Minimum Supported Rust Version (MSRV)
Expand Down
166 changes: 166 additions & 0 deletions docs/batching.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
# Batching and Concurrent Inserters

This crate provides a hierarchy of inserter types for different concurrency and
transport needs. All share the same three-threshold flush policy: **row count**,
**byte size**, and **time period**.

## Inserter hierarchy

```mermaid
graph TD
subgraph HTTP Transport
TB["TableBatcher&lt;T&gt;<br/><i>feature = batcher</i><br/>Go-style append/flush/send"]
AI["AsyncInserter&lt;T&gt;<br/><i>feature = async-inserter</i><br/>MPSC channel + background task"]
I["Inserter&lt;T&gt;<br/><i>feature = inserter</i><br/>Single-owner &amp;mut self"]
TB -- delegates to --> AI
AI -- wraps --> I
end

subgraph Native TCP Transport
ANI["AsyncNativeInserter&lt;T&gt;<br/><i>feature = native-transport</i><br/>MPSC channel + background task"]
NI["NativeInserter&lt;T&gt;<br/><i>feature = native-transport</i><br/>Single-owner &amp;mut self"]
ANI -- wraps --> NI
end
```

## Choosing an inserter

| Type | Transport | Concurrency | Use case |
|---|---|---|---|
| `Insert<T>` / `NativeInsert<T>` | HTTP / Native | Single owner | One-shot batch, manual control |
| `Inserter<T>` / `NativeInserter<T>` | HTTP / Native | Single owner (`&mut self`) | Long-running pipeline, single task |
| `AsyncInserter<T>` | HTTP | Multi-task (`&self` + handles) | Fan-in from many producers |
| `AsyncNativeInserter<T>` | Native | Multi-task (`&self` + handles) | Fan-in from many producers |
| `TableBatcher<T>` | HTTP | Multi-task (Go-style API) | Drop-in replacement for Go `Batch` |

## AsyncInserter / AsyncNativeInserter

Both share identical architecture — an MPSC channel feeding a background tokio
task that owns the underlying `Inserter` or `NativeInserter`:

```mermaid
graph TD
A["Task A<br/>tx.send()"] --> CH{{"bounded mpsc channel<br/>(default: 8192 slots)"}}
B["Task B<br/>tx.send()"] --> CH
C["Task C<br/>tx.send()"] --> CH
CH --> BG["Background Task<br/><br/>select! {<br/>&nbsp;&nbsp;cmd = rx.recv() &nbsp; ← biased<br/>&nbsp;&nbsp;_ = interval.tick() ← periodic flush<br/>}<br/><br/>serialize → buffer<br/>check limits → flush"]
BG --> CK[("ClickHouse server")]
```

### Key properties

- **`&self` on `write()` and `flush()`** — safe to call from multiple tasks
without external synchronization.
- **Backpressure** — the bounded channel blocks producers when the background
task can't keep up. Tune with `with_channel_capacity()`.
- **Error propagation** — each `write()` returns a `Result<()>` via a oneshot
channel. Serialization or network errors are surfaced to the caller.
- **`RowOwned` requirement** — rows cross a channel boundary, so `T` must own
its data (no borrowed `&str` fields). This is automatic for `#[derive(Row)]`
structs with owned fields.

### Configuration

```rust
use clickhouse::async_inserter::{AsyncInserter, AsyncInserterConfig};

let config = AsyncInserterConfig::default()
.with_max_rows(100_000) // flush at 100K rows
.with_max_bytes(10_485_760) // flush at 10 MiB
.with_max_period(Duration::from_secs(5)) // flush every 5s
.with_channel_capacity(4096); // backpressure at 4K pending

let inserter = AsyncInserter::<MyRow>::new(&client, "my_table", config);
```

### Handles

Handles are cheap clones of the channel sender. Use them to fan out writes
across tasks:

```rust
let inserter = AsyncInserter::<MyRow>::new(&client, "my_table", config);

for i in 0..10 {
let h = inserter.handle();
tokio::spawn(async move {
h.write(MyRow { id: i, data: format!("task-{i}") }).await.unwrap();
});
}

let stats = inserter.end().await?; // graceful shutdown
```

### Lifecycle

1. **`new()`** — spawns background task immediately.
2. **`write(row)`** — sends row over channel; blocks if full.
3. **`flush()`** — forces immediate flush of buffered rows.
4. **`end()`** — sends shutdown command, waits for final flush, joins task.

Dropping without `end()` causes the background task to flush and exit when all
senders are dropped (including handles).

## TableBatcher

`TableBatcher<T>` is a thin wrapper over `AsyncInserter<T>` with Go client
naming conventions:

| TableBatcher | AsyncInserter | Go `Batch` |
|---|---|---|
| `append(row)` | `write(row)` | `Append(args...)` |
| `flush()` | `flush()` | `Flush()` |
| `send()` | `end()` | `Send()` |

```rust
use clickhouse::batcher::{TableBatcher, BatchConfig};

let config = BatchConfig {
max_rows: 100_000,
max_bytes: 10 * 1024 * 1024,
max_period: Some(Duration::from_secs(5)),
};

let batcher = TableBatcher::<MyRow>::new(&client, "my_table", config);
batcher.append(MyRow { id: 1, data: "foo".into() }).await?;
batcher.append(MyRow { id: 2, data: "bar".into() }).await?;

let stats = batcher.send().await?; // final flush + shutdown
```

## Default thresholds

All inserter configs share these defaults, aligned with ClickHouse server
settings and the Go client:

| Setting | Default | Rationale |
|---|---|---|
| `max_rows` | 100,000 | Upper end of recommended per-INSERT batch size |
| `max_bytes` | 10 MiB | Matches `async_insert_max_data_size` server default |
| `max_period` | 5 seconds | Balances latency vs throughput |
| `channel_capacity` | 8,192 | Backpressure threshold for concurrent inserters |

### Why client-side batching?

ClickHouse's server-side `async_insert` is convenient for distributed agents but
has drawbacks for high-throughput pipelines:

- **OOM risk** — server buffers data in memory until thresholds are met
- **Delayed visibility** — data is not queryable until the server flushes
- **Error opacity** — insert errors may be lost or delayed
- **No client control** — can't tune batch size per table or per source

Client-side batching (as implemented here) gives you immediate query visibility,
per-table tuning, and explicit error handling at the cost of managing batch
state in the client.

### MergeTree part fragmentation

Each INSERT creates one part per partition in MergeTree. Too many small INSERTs
cause part fragmentation:

- `parts_to_delay_insert` (default: 150) — slows down inserts
- `parts_to_throw_insert` (default: 300) — hard "Too many parts" error

The defaults here (100K rows, 10 MiB) are designed to produce reasonably-sized
parts. For tables with multiple partitions, adjust downward.
95 changes: 95 additions & 0 deletions docs/connection-pooling.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Connection Pooling

The native transport uses [deadpool](https://docs.rs/deadpool) for connection
pooling. Each `NativeClient` owns a pool; clones share it (the pool is
`Arc`-backed internally).

## Configuration

```rust
use clickhouse::native::NativeClient;

let client = NativeClient::default()
.with_pool_size(20); // max 20 connections (default: 10)
```

The pool is bounded — when all connections are in use, `acquire()` waits until
one is returned. There is no idle timeout; connections persist until the client
is dropped or they fail a health check.

## Health checks (recycle)

When a connection is returned to the pool, deadpool calls `recycle()` which
runs `check_alive()`:

1. **Poisoned flag** — if `discard()` was called, the connection is dropped
unconditionally.
2. **Buffered data** — if the `BufReader` has leftover bytes from an incomplete
read, the connection is dropped (stale protocol state).
3. **Non-blocking poll** — a non-blocking `poll_read` detects EOF or unexpected
server data. If either is found, the connection is dropped.

Connections that pass all three checks are returned to the idle queue for reuse.

## The discard pattern

When an I/O error or incomplete protocol exchange leaves a connection in an
unrecoverable state, call `discard()` on the `PooledConnection`. This sets a
`poisoned` flag that causes `recycle()` to drop the connection rather than
returning it to the pool.

This is used internally by:
- `NativeRowCursor::Drop` — if a cursor is dropped mid-stream (e.g. after
`fetch_one` without draining), the connection is discarded.
- Error paths in `NativeInsert` — if an INSERT fails mid-stream, the
connection is discarded rather than risk protocol desync.

## Cursor drain

`NativeRowCursor` provides a `drain()` method that reads and discards all
remaining server packets until `EndOfStream`. This is called automatically by
`fetch_one` and `fetch_optional` to clean up the connection before returning it
to the pool:

```mermaid
graph LR
F["fetch_one()"] --> N["next().await?<br/>get first row"]
N --> D["drain().await?<br/>consume remaining packets"]
D --> R["return row<br/>connection returned to pool"]
```

If `drain()` is not called (e.g. cursor dropped early), the `Drop` impl
discards the connection as a safety net.

## Pool rebuild on config change

Builder methods that affect connection parameters trigger `rebuild_pool()`,
which creates a new pool instance. Existing connections from the old pool are
not immediately closed — they drain naturally as they're returned and not
recycled into the new pool.

Affected methods:
- `with_addr()`
- `with_database()`
- `with_user()` / `with_password()`
- `with_lz4()`
- `with_pool_size()`
- `with_setting()`

## Architecture

```mermaid
graph TD
NC["NativeClient"] --> POOL["pool: NativePool<br/>(deadpool::managed::Pool)"]
NC --> SC["schema_cache: Arc&lt;NativeSchemaCache&gt;<br/>HashMap&lt;table, (columns, expires_at)&gt;"]
NC --> SET["settings: Arc&lt;Vec&lt;(key, value)&gt;&gt;"]

POOL --> MGR["NativeConnectionManager"]
MGR -->|"create()"| OPEN["NativeConnection::open()"]
MGR -->|"recycle()"| CHECK["check_alive()"]

POOL --> IDLE["Idle queue<br/>(bounded semaphore)"]
IDLE --> C1["conn 1"]
IDLE --> C2["conn 2"]
IDLE --> C3["..."]
```
Loading