clickhouse-sys 0.0.1

Static clickhouse-cpp bridge for the clickhouse-cxx workspace
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
# clickhouse-cxx

A thread-affine ClickHouse SDK with completion-driven socket I/O. Native TCP and
TCPS use [`clickhouse-cpp`](https://github.com/ClickHouse/clickhouse-cpp)
through [`cxx`](https://cxx.rs/); HTTP and HTTPS use
[`nexora-rs`](https://github.com/trunkszi/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](https://github.com/ClickHouse/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::Proactor` and 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`, and `TlsSession` handles
  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**: `ThreadLocalClient` keeps one client per calling
  OS thread. `WorkerClient` leases 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**: `Batch` and `Block` expose explicit capacity retention
  through `BufferPolicy`.

## 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 |

```toml
# Native TCP/TCPS
clickhouse = { path = "crates/clickhouse" }
```

```toml
# HTTP/2 only; clickhouse-sys is not compiled
clickhouse = { path = "crates/clickhouse", default-features = false, features = ["http"] }
```

```toml
# Both transports
clickhouse = { path = "crates/clickhouse", features = ["tcp", "http"] }
```

```toml
# Cold control-plane telemetry
clickhouse = { path = "crates/clickhouse", features = ["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

```rust
use clickhouse::{Batch, Client, Endpoint, Row};
use clickhouse::row::RowClient;

#[derive(Row, Default)]
#[row(table = "events", rename_all = "snake_case")]
struct Event {
    event_id: u64,
    kind: String,
    score: f64,
}

fn main() -> clickhouse::Result<()> {
    let endpoint = Endpoint::parse("tcp://127.0.0.1:9000")?;
    let mut client = Client::connect(endpoint, "default", "", "default")?;

    let mut batch = EventBatch::with_capacity(2);
    batch.push(&Event { event_id: 1, kind: String::from("trade"), score: 0.5 });
    client.insert_batch(&batch)?;

    let rows = client.query_rows::<Event>("SELECT event_id, kind, score FROM events")?;
    println!("{} rows", rows.len());
    Ok(())
}
```

## 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 a `Block`;
- `EventRef<'a>`, a borrowed row made of `&str`, `&[u8]`, and borrowed slices;
- the `Row` and `OwnedRow` implementations used by `RowClient`.

```rust
let block = batch.to_block()?;
let view = EventBlockView::try_from_block(&block)?;

for row in view.iter() {
    println!("{} {}", row.event_id, row.kind);
}

// Fixed-width columns borrow block storage directly.
let ids: &[u64] = view.event_id;
assert_eq!(ids.as_ptr(), block.column_slice_by_name::<u64>("event_id")?.as_ptr());

// Refill a batch without rebuilding its column buffers.
batch.clear();
batch.push_ref(EventRef { event_id: 2, kind: "quote", score: 1.5 });
```

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.

```rust
let rows: Vec<Event> = client.query_rows("SELECT event_id, kind, score FROM events")?;
let serde_rows: Vec<Metric> = SerdeBridge::block_to_rows(&block)?;
```

### Streaming callbacks

Callbacks keep the backing block alive and avoid an intermediate row vector.

```rust
client.query_views::<Event, _>(sql, |view| {
    for row in view.iter() {
        process(row)?;
    }
    Ok(())
})?;

let mut kinds = Vec::new();
client.query_row_refs::<Event, _>(sql, |row| {
    kinds.push(row.kind.to_owned());
    Ok(())
})?;

let count = client.query_for_each::<Event, _>(sql, |row| {
    consume(row);
    Ok(())
})?;

let decoded = SerdeBridge::from_rowbinary_with::<Metric, _>(&bytes, |row| {
    consume(row);
    Ok(())
})?;

let decoded = SerdeBridge::block_to_rows_with::<Metric, _>(&block, |row| {
    consume(row);
    Ok(())
})?;
```

### 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.

```rust
let mut ids = vec![0u64; 1024];
let copied = native.read_into_slice(0, &mut ids)?;

let mut payload = Vec::new();
let len = native.read_bytes_into(1, 0, &mut payload)?;

let mut events: Vec<Event> = std::iter::repeat_with(Event::default).take(1024).collect();
let written = client.query_rows_into_slice(sql, &mut events)?;

let mut batch = EventBatch::with_capacity(1024);
let written = client.query_batch_into::<Event>(sql, &mut batch)?;

let mut block = Block::new();
let written = client.query_block_into::<Event>(sql, &mut block)?;

let mut metrics = std::iter::repeat_with(Metric::default).take(1024).collect::<Vec<_>>();
let written = SerdeBridge::from_rowbinary_into_slice(&bytes, &mut metrics)?;
```

`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.

```rust
use std::thread;
use clickhouse::{Client, ClientConfig};

let config = ClientConfig::parse(
    "tcp://10.0.0.1:9000,tcp://10.0.0.2:9000,tcp://10.0.0.3:9000",
)?.with_workers(8);

let workers = Client::connect_workers(config)?;
thread::scope(|scope| {
    for _ in 0..8 {
        scope.spawn(|| workers.with(|client| client.ping()));
    }
});
```

`ThreadLocalClient` is the one-client-per-thread form:

```rust
let local = Client::thread_local(
    ClientConfig::parse("tcp://10.0.0.1:9000")?.with_workers(4),
)?;

thread::scope(|scope| {
    for _ in 0..4 {
        scope.spawn(|| local.with(|client| client.ping()));
    }
});
```

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.

```rust
let mut policy = BufferPolicy::default();
policy.max_retained_rows = 32_768;
policy.max_retained_bytes = 4 * 1024 * 1024;

let before = batch.allocated_bytes();
batch.clear_with_policy(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`.

```rust
use std::collections::BTreeMap;

#[derive(Default, serde::Serialize, serde::Deserialize)]
struct Metric {
    ts: u32,
    tags: BTreeMap<String, u64>,
}

let (names, bytes) = SerdeBridge::to_rowbinary(&metrics)?;
let rows: Vec<Metric> = SerdeBridge::from_rowbinary(&bytes)?;

let block = SerdeBridge::rows_to_block(&metrics)?;
let rows: Vec<Metric> = SerdeBridge::block_to_rows(&block)?;
```

`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

```rust
use std::time::Duration;
use clickhouse::{
    Client, ClientConfig, EndpointSelection, FailoverPolicy, TlsSettings,
};

let tls = TlsSettings::new()
    .with_ca_pem(std::fs::read("ca.pem")?)
    .with_sni("clickhouse.internal")
    .with_connect_timeout(Duration::from_secs(3))
    .with_read_timeout(Duration::from_secs(5));

let policy = FailoverPolicy::new()
    .with_selection(EndpointSelection::LatencyAware)
    .with_send_retries(2)
    .with_retry_timeout(Duration::from_millis(250))
    .with_circuit(3, Duration::from_secs(30));

let config = ClientConfig::parse("tcps://node-a:9440,tcps://node-b:9440")?
    .with_tls(tls)
    .with_policy(policy)
    .on_failover(|event| {
        eprintln!("{} -> {}", event.old_endpoint, event.new_endpoint);
    });

let mut client = Client::new(config)?;
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:

```rust
let mut request = client.submit_http(&Query::new("SELECT 1 FORMAT TabSeparated"), &[])?;
while !request.is_complete() {
    client.poll_http()?;
    // Run other application work between bounded completion turns.
}
let body = request.try_take().expect("completed result")?;
assert_eq!(body, b"1\n");
```

`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)` calls `FnMut(&[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. |

```bash
cargo run -p clickhouse --example 01_quickstart_native_tcp
cargo run -p clickhouse --example 02_derive_row_and_zero_copy_views
cargo run -p clickhouse --example 03_memory_policy_and_batch_reuse
cargo run -p clickhouse --example 04_full_column_type_matrix
cargo run -p clickhouse --example 05_dual_transport_and_row_client
cargo run -p clickhouse --example 06_serde_compatibility
cargo run -p clickhouse --example 07_thread_local_and_workers
cargo run -p clickhouse --example 08_tls_and_failover_policy
cargo run -p clickhouse --features http --example 09_http2_rowbinary_and_arrow
```

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

```bash
cargo test --workspace
cargo clippy --workspace --all-targets -- -D warnings
cargo fmt --all -- --check
```

`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:

```sh
cargo fmt --all -- --check
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspace
RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps
```

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.