# clickhouse-async.h

Ioless variant of client. Never touches a socket: caller submits bytes and
drives event loop (epoll, io_uring, `WaitLatchOrSocket`) however it likes.
Same packet loop as [clickhouse-client.h](clickhouse-client.md)
— Hello / Query / Data / EOS / Exception / Progress / Pong, with compression —
built on the ioless [buffered reader](clickhouse.md#buffered-reader).

## Submission

The client never reads or writes a socket. The caller moves bytes both ways:

* **inbound** — socket bytes the caller received go in via `chc_async_submit`.
* **outbound** — bytes the client wants to send accumulate in an internal
  buffer; the caller drains them with `chc_async_pending_out` /
  `chc_async_consume_out` and writes them to the socket.

```c
int  chc_async_submit      (chc_async_client *c, const void *buf, size_t len,
                            chc_err *err);
void chc_async_pending_out (chc_async_client *c, const uint8_t **buf, size_t *len);
void chc_async_consume_out (chc_async_client *c, size_t n);
```

`pending_out` hands back a pointer/length into the pending bytes (zero length
when nothing is queued); after the socket accepts `n` of them, call
`consume_out(n)`. A partial write is fine — report only what the socket took.
When fully drained the buffer resets to length 0, keeping its allocation.

No write backpressure: sends never block, never return `CHC_WOULD_BLOCK`, and
grow the out buffer unbounded (only OOM fails). Outbound flow control is the
caller's job — watch `pending_out`'s length, stop issuing sends when it grows
too large, resume after `consume_out` drains it.

## Lifecycle & opts

```c
typedef struct chc_async_client chc_async_client;

int  chc_async_client_init(chc_async_client **out, const chc_client_opts *opts,
                           const chc_alloc *al, chc_err *err);
void chc_async_client_free(chc_async_client *c);
const chc_server_info *chc_async_server_info(const chc_async_client *c);
```

`opts` is the same [`chc_client_opts`](clickhouse-client.md#opts--handshake) as
the blocking client (`NULL` for defaults). Unlike `chc_client_init`, init does
**no** I/O — it cannot block and only OOM fails; the handshake runs later via
`chc_async_handshake`. The Hello string fields (`client_name`, `database`,
`user`, `password`) are copied internally, so `opts` and its strings need not
outlive the call.

`opts->codec` carries compression: pass a filled `codec` plus
`compression = CHC_COMP_LZ4`/`CHC_COMP_ZSTD` to (de)compress Data packets. See
[clickhouse-compression.md](clickhouse-compression.md).

`chc_async_server_info` returns the negotiated server info (revision is
`min(client, server)`); meaningful only after the handshake completes.

## Drive

```c
int  chc_async_handshake    (chc_async_client *c, chc_err *err);
int  chc_async_send_query   (chc_async_client *c,
                             const char *sql,      size_t sql_len,
                             const char *query_id, size_t query_id_len,
                             chc_err *err);
int  chc_async_send_data    (chc_async_client *c, const chc_block_builder *bb,
                             chc_err *err);
int  chc_async_send_data_end(chc_async_client *c, chc_err *err);
int  chc_async_recv_packet  (chc_async_client *c, chc_packet *out, chc_err *err);
void chc_async_packet_clear (chc_async_client *c, chc_packet *p);
```

Every call returns `CHC_OK`, `CHC_WOULD_BLOCK`, or a hard `CHC_ERR_*`.
`CHC_WOULD_BLOCK` from `handshake` / `recv_packet` means the input buffer
drained mid-parse: submit more inbound bytes and call again. Parser state is
preserved across the retry — a Data block streamed over many reads resumes at
the in-progress column rather than re-parsing. Compressed Data resumes at frame
granularity: at most one frame is re-decompressed and one column re-parsed.

* `chc_async_handshake` runs the Hello / Pong exchange as a resumable state
  machine. Call it after init, re-driving on `CHC_WOULD_BLOCK`, until `CHC_OK`.
* `send_query` / `send_data` only append to the out buffer; they never block.
  `send_data(bb)` appends a Data block; `send_data_end` appends the empty
  terminating block that ends an INSERT stream. Semantics mirror
  [the blocking client](clickhouse-client.md#send).
* `recv_packet` decodes one packet. Server exceptions arrive as
  `CHC_PKT_EXCEPTION` with `CHC_OK` (not `CHC_ERR_SERVER`); only transport-level
  failures return non-OK. `chc_async_packet_clear` frees `block` / `exception`
  via the client's allocator, exactly like `chc_packet_clear`. Packet kinds and
  `chc_packet` layout are [in the client doc](clickhouse-client.md#receive).

## Driving loop

Sketch of a single-threaded event loop (see `test/test_async_uring.c` for a
working liburing driver):

```c
chc_async_client *c;
chc_async_client_init(&c, &opts, &al, &err);

/* handshake */
for (;;) {
    int rc = chc_async_handshake(c, &err);
    if (rc == CHC_OK) break;
    if (rc != CHC_WOULD_BLOCK) goto fail;
    pump(c);              /* drain pending_out -> socket; recv -> submit */
}

chc_async_send_query(c, sql, sql_len, "", 0, &err);

for (;;) {
    chc_packet pkt = {};
    int rc = chc_async_recv_packet(c, &pkt, &err);
    if (rc == CHC_WOULD_BLOCK) { pump(c); continue; }
    if (rc != CHC_OK) goto fail;

    if (pkt.kind == CHC_PKT_END_OF_STREAM) { chc_async_packet_clear(c, &pkt); break; }
    if (pkt.kind == CHC_PKT_DATA) { /* consume pkt.block */ }
    chc_async_packet_clear(c, &pkt);
}
```

`pump` writes `chc_async_pending_out` bytes to the socket (then
`chc_async_consume_out`) and feeds received bytes into `chc_async_submit` — the
shape of that loop is whatever the caller's reactor dictates.

## Threading

Single-threaded, like `chc_client`. There are no `chc_io` callbacks at all; the
caller's event loop is the only thing that touches the socket.
