ch_connect 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -23,9 +23,13 @@
23
23
  extern "C" {
24
24
  #endif
25
25
 
26
- /* Default protocol revision the client speaks. Matches clickhouse-cpp's
27
- * DBMS_MIN_PROTOCOL_VERSION_WITH_PARAMETERS pin. */
28
- #define CHC_CLIENT_DEFAULT_REVISION 54459u
26
+ /* Protocol revisions & feature gates are documented in the Native Protocol
27
+ * spec: clickhouse.com/docs/reference/interfaces/specs/NativeProtocol
28
+ *
29
+ * Oldest supported server is ClickHouse 23.3. Client advertises
30
+ * SPARSE_SERIALIZATION (54465). */
31
+ #define CHC_SERVER_MIN_REVISION 54462u
32
+ #define CHC_CLIENT_REVISION 54465u
29
33
 
30
34
  typedef struct chc_client_opts {
31
35
  /* Identity. Defaults applied when fields are zero/NULL. */
@@ -33,7 +37,6 @@ typedef struct chc_client_opts {
33
37
  uint64_t client_version_major; /* default 0 */
34
38
  uint64_t client_version_minor; /* default 0 */
35
39
  uint64_t client_version_patch; /* default 0 */
36
- uint64_t client_revision; /* default CHC_CLIENT_DEFAULT_REVISION */
37
40
 
38
41
  /* Hello body. */
39
42
  const char *database; /* default "default" */
@@ -50,20 +53,27 @@ typedef struct chc_client_opts {
50
53
 
51
54
  typedef struct chc_server_info {
52
55
  char name[64];
53
- char timezone[64];
56
+ char timezone[64]; /* updated by CHC_PKT_TIMEZONE_UPDATE */
54
57
  char display_name[128];
55
58
  uint64_t version_major;
56
59
  uint64_t version_minor;
57
60
  uint64_t version_patch;
58
- uint64_t revision; /* min(client_revision, server_revision) */
61
+ uint64_t revision; /* min(CHC_CLIENT_REVISION, server revision) */
59
62
  } chc_server_info;
60
63
 
61
64
  typedef struct chc_client chc_client;
65
+ typedef struct chc_exception chc_exception;
62
66
 
63
67
  /* Performs Hello / HelloAck handshake immediately. On failure caller may
64
- * call chc_client_close to free any partially-allocated state. */
68
+ * call chc_client_close to free any partially-allocated state.
69
+ *
70
+ * Handshake rejection returns CHC_ERR_SERVER and leaves err->msg empty. When
71
+ * exc is not NULL, caller owns *exc and frees it with chc_exception_free.
72
+ * Other results leave *exc unchanged. Query exceptions still arrive as
73
+ * packets, but handshake rejection produces no client */
65
74
  int chc_client_init(chc_client **out, const chc_client_opts *opts,
66
- const chc_alloc *al, chc_io *io, chc_err *err);
75
+ const chc_alloc *al, chc_io *io, chc_exception **exc,
76
+ chc_err *err);
67
77
 
68
78
  void chc_client_close(chc_client *c);
69
79
 
@@ -74,8 +84,9 @@ int chc_client_send_query(chc_client *c,
74
84
  const char *query_id, size_t query_id_len,
75
85
  chc_err *err);
76
86
 
77
- /* Per-query setting. name / value are NUL-terminated. Matches
78
- * clickhouse-cpp's QuerySettingsField.flags low two bits. */
87
+ /* Per-query setting. name / value are NUL-terminated. Flags layout per the
88
+ * Native Protocol spec (Query packet, settings entry); only the low two
89
+ * bits are assigned. */
79
90
  typedef struct chc_query_setting {
80
91
  const char *name;
81
92
  const char *value;
@@ -88,8 +99,7 @@ typedef struct chc_query_setting {
88
99
  * always CUSTOM. The server parses value via Field::restoreFromDump, so
89
100
  * callers must format value as a typed Field literal: e.g. `'hello'` for a
90
101
  * String, `42` for an integer, `[1,2,3]` for an array. NULL is `'\\N'`.
91
- * (Unlike clickhouse-cpp's higher-level Client::SetParam, which auto-quotes
92
- * raw strings, this library passes the value through verbatim so callers
102
+ * The value goes on the wire verbatim -- no auto-quoting -- so callers
93
103
  * keep full control of typed and non-string values.) */
94
104
  typedef struct chc_query_param {
95
105
  const char *name;
@@ -122,11 +132,11 @@ typedef enum chc_packet_kind {
122
132
  CHC_PKT_LOG = 10,
123
133
  CHC_PKT_TABLE_COLUMNS = 11,
124
134
  CHC_PKT_PROFILE_EVENTS = 14,
135
+ CHC_PKT_TIMEZONE_UPDATE = 17, /* no payload, see chc_server_info.timezone */
125
136
  } chc_packet_kind;
126
137
 
127
138
  /* CHC_PKT_EXCEPTION payload. Caller frees with chc_exception_free
128
139
  * if produced. */
129
- typedef struct chc_exception chc_exception;
130
140
  struct chc_exception {
131
141
  int32_t code;
132
142
  char *name; /* allocated in chc_alloc */
@@ -143,7 +153,7 @@ typedef struct chc_packet {
143
153
  chc_packet_kind kind;
144
154
 
145
155
  /* Payload selected by kind; exactly one member is live, none for
146
- * PONG / END_OF_STREAM / TABLE_COLUMNS. */
156
+ * PONG / END_OF_STREAM / TABLE_COLUMNS / TIMEZONE_UPDATE. */
147
157
  union {
148
158
  /* CHC_PKT_DATA / TOTALS / EXTREMES / LOG / PROFILE_EVENTS:
149
159
  * caller-owned chc_block, freed with chc_block_destroy. */
@@ -155,7 +165,9 @@ typedef struct chc_packet {
155
165
  /* CHC_PKT_PROGRESS. */
156
166
  struct {
157
167
  uint64_t rows, bytes, total_rows;
158
- uint64_t written_rows, written_bytes; /* >= rev 54420 */
168
+ uint64_t total_bytes; /* 0 below rev 54463 */
169
+ uint64_t written_rows, written_bytes;
170
+ uint64_t elapsed_ns;
159
171
  } progress;
160
172
 
161
173
  /* CHC_PKT_PROFILE_INFO. */
@@ -191,25 +203,18 @@ int chc_client_send_ping(chc_client *c, chc_err *err);
191
203
  #include <stdlib.h>
192
204
  #include <string.h>
193
205
 
194
- /* ----- protocol revision constants (mirror clickhouse-cpp client.cpp) ----- */
195
- #define CHC__REV_TEMPORARY_TABLES 50264u
196
- #define CHC__REV_TOTAL_ROWS_IN_PROGRESS 51554u
197
- #define CHC__REV_BLOCK_INFO 51903u
198
- #define CHC__REV_CLIENT_INFO 54032u
199
- #define CHC__REV_SERVER_TIMEZONE 54058u
200
- #define CHC__REV_QUOTA_KEY_IN_CLIENT 54060u
201
- #define CHC__REV_SERVER_DISPLAY_NAME 54372u
202
- #define CHC__REV_VERSION_PATCH 54401u
203
- #define CHC__REV_CLIENT_WRITE_INFO 54420u
204
- #define CHC__REV_SETTINGS_AS_STRINGS 54429u
205
- #define CHC__REV_INTERSERVER_SECRET 54441u
206
- #define CHC__REV_OPENTELEMETRY 54442u
207
- #define CHC__REV_DISTRIBUTED_DEPTH 54448u
208
- #define CHC__REV_INITIAL_QUERY_START 54449u
209
- #define CHC__REV_PARALLEL_REPLICAS 54453u
210
- #define CHC__REV_CUSTOM_SERIALIZATION 54454u
211
- #define CHC__REV_ADDENDUM 54458u
212
- #define CHC__REV_PARAMETERS 54459u
206
+ /* Only gate above CHC_SERVER_MIN_REVISION, Progress omits total_bytes below
207
+ * it. TimezoneUpdate (54464) & sparse (54465) decode at any revision */
208
+ #define CHC__REV_TOTAL_BYTES_IN_PROGRESS 54463u
209
+
210
+ /* Mirror ClickHouse DBMS_MAX_HELLO_STRING_SIZE & DBMS_MAX_PASSWORD_COMPLEXITY_RULES */
211
+ #define CHC__MAX_HELLO_STRING 4096u
212
+ #define CHC__MAX_PASSWORD_RULES 256u
213
+
214
+ static const chc_block_opts chc__tcp_block_opts = {
215
+ .has_block_info = true,
216
+ .has_custom_serialization = true,
217
+ };
213
218
 
214
219
  /* Client → server packet kinds. */
215
220
  #define CHC__CLIENT_HELLO 0u
@@ -227,7 +232,6 @@ struct chc_client {
227
232
  uint64_t client_version_major;
228
233
  uint64_t client_version_minor;
229
234
  uint64_t client_version_patch;
230
- uint64_t client_revision;
231
235
  chc_compression compression;
232
236
  const chc_codec *codec;
233
237
 
@@ -303,19 +307,38 @@ chc__client_send_hello(chc_client *c, const chc_client_opts *opts, chc_err *err)
303
307
  if ((rc = chc__write_string (c->io, name, name_len, err))) return rc;
304
308
  if ((rc = chc__write_varuint(c->io, opts->client_version_major, err))) return rc;
305
309
  if ((rc = chc__write_varuint(c->io, opts->client_version_minor, err))) return rc;
306
- if ((rc = chc__write_varuint(c->io, c->client_revision, err))) return rc;
310
+ if ((rc = chc__write_varuint(c->io, CHC_CLIENT_REVISION, err))) return rc;
307
311
  if ((rc = chc__write_string (c->io, db, strlen(db), err))) return rc;
308
312
  if ((rc = chc__write_string (c->io, us, strlen(us), err))) return rc;
309
313
  if ((rc = chc__write_string (c->io, pw, strlen(pw), err))) return rc;
310
314
  return CHC_OK;
311
315
  }
312
316
 
313
- static void
314
- chc__copy_short(char *dst, size_t cap, const char *src, size_t len)
317
+ /* Read Hello-bounded server string into dst, truncating to cap - 1 bytes */
318
+ static int
319
+ chc__read_server_string(chc_in *in, char *dst, size_t cap, chc_err *err)
315
320
  {
316
- size_t n = len < cap - 1 ? len : cap - 1;
317
- if (n) memcpy(dst, src, n);
321
+ uint64_t len;
322
+ int rc = chc__read_varuint(in, &len, err);
323
+ if (rc != CHC_OK) return rc;
324
+ if (len > CHC__MAX_HELLO_STRING)
325
+ return chc__err_set(err, CHC_ERR_PROTOCOL, "server string too long: %llu",
326
+ (unsigned long long) len);
327
+ size_t n = len < cap ? (size_t) len : cap - 1;
328
+ if ((rc = chc__read_bytes(in, dst, n, err))) return rc;
318
329
  dst[n] = '\0';
330
+ return chc__skip_bytes(in, (size_t) len - n, err);
331
+ }
332
+
333
+ static void
334
+ chc__client_setup(chc_client *c, const chc_client_opts *opts)
335
+ {
336
+ c->client_version_major = opts->client_version_major;
337
+ c->client_version_minor = opts->client_version_minor;
338
+ c->client_version_patch = opts->client_version_patch;
339
+ c->server.revision = CHC_CLIENT_REVISION;
340
+ c->compression = opts->codec ? opts->compression : CHC_COMP_NONE;
341
+ c->codec = opts->codec;
319
342
  }
320
343
 
321
344
  /* Reads chained exception. Caller frees via chc_exception_free. */
@@ -339,7 +362,7 @@ chc__read_exception(chc_client *c, chc_exception **out, chc_err *err)
339
362
  }
340
363
 
341
364
  static int
342
- chc__client_recv_hello(chc_client *c, chc_err *err)
365
+ chc__client_recv_hello(chc_client *c, chc_exception **exc, chc_err *err)
343
366
  {
344
367
  uint64_t kind;
345
368
  int rc = chc__read_varuint(&c->in, &kind, err);
@@ -348,12 +371,9 @@ chc__client_recv_hello(chc_client *c, chc_err *err)
348
371
  chc_exception *e = NULL;
349
372
  rc = chc__read_exception(c, &e, err);
350
373
  if (rc != CHC_OK) return rc;
351
- chc__err_set(err, CHC_ERR_SERVER, "%s",
352
- e->display_text ? e->display_text : (e->name ? e->name : ""));
353
- err->server_code = e->code;
354
- chc__copy_short(err->server_name, sizeof err->server_name,
355
- e->name, e->name_len);
356
- chc_exception_free(e, c->al);
374
+ if (exc) *exc = e;
375
+ else chc_exception_free(e, c->al);
376
+ chc_err_reset(err);
357
377
  return CHC_ERR_SERVER;
358
378
  }
359
379
  if (kind != CHC_PKT_HELLO)
@@ -361,33 +381,40 @@ chc__client_recv_hello(chc_client *c, chc_err *err)
361
381
  "expected Hello, got %llu",
362
382
  (unsigned long long) kind);
363
383
 
364
- char *s; size_t slen;
365
- if ((rc = chc__read_string(&c->in, &s, &slen, err))) return rc;
366
- chc__copy_short(c->server.name, sizeof c->server.name, s, slen);
367
- c->al->free(c->al->ud, s, slen + 1);
368
-
369
- if ((rc = chc__read_varuint(&c->in, &c->server.version_major, err))) return rc;
370
- if ((rc = chc__read_varuint(&c->in, &c->server.version_minor, err))) return rc;
371
- if ((rc = chc__read_varuint(&c->in, &c->server.revision, err))) return rc;
372
-
373
- if (c->server.revision >= CHC__REV_SERVER_TIMEZONE) {
374
- if ((rc = chc__read_string(&c->in, &s, &slen, err))) return rc;
375
- chc__copy_short(c->server.timezone, sizeof c->server.timezone, s, slen);
376
- c->al->free(c->al->ud, s, slen + 1);
377
- }
378
- if (c->server.revision >= CHC__REV_SERVER_DISPLAY_NAME) {
379
- if ((rc = chc__read_string(&c->in, &s, &slen, err))) return rc;
380
- chc__copy_short(c->server.display_name, sizeof c->server.display_name, s, slen);
381
- c->al->free(c->al->ud, s, slen + 1);
382
- }
383
- if (c->server.revision >= CHC__REV_VERSION_PATCH) {
384
- if ((rc = chc__read_varuint(&c->in, &c->server.version_patch, err))) return rc;
385
- }
384
+ /* Commit only complete Hello, async handshake retries on WOULD_BLOCK */
385
+ chc_server_info s = {};
386
+ uint64_t n_rules, nonce;
387
+ char rule[1];
388
+ if ((rc = chc__read_server_string(&c->in, s.name, sizeof s.name, err)) ||
389
+ (rc = chc__read_varuint(&c->in, &s.version_major, err)) ||
390
+ (rc = chc__read_varuint(&c->in, &s.version_minor, err)) ||
391
+ (rc = chc__read_varuint(&c->in, &s.revision, err)))
392
+ return rc;
393
+ if (s.revision < CHC_SERVER_MIN_REVISION)
394
+ return chc__err_set(err, CHC_ERR_PROTOCOL,
395
+ "server revision %llu older than %u",
396
+ (unsigned long long) s.revision, CHC_SERVER_MIN_REVISION);
397
+ if (s.revision > CHC_CLIENT_REVISION) s.revision = CHC_CLIENT_REVISION;
398
+
399
+ if ((rc = chc__read_server_string(&c->in, s.timezone, sizeof s.timezone, err)) ||
400
+ (rc = chc__read_server_string(&c->in, s.display_name, sizeof s.display_name, err)) ||
401
+ (rc = chc__read_varuint(&c->in, &s.version_patch, err)) ||
402
+ (rc = chc__read_varuint(&c->in, &n_rules, err)))
403
+ return rc;
404
+ if (n_rules > CHC__MAX_PASSWORD_RULES)
405
+ return chc__err_set(err, CHC_ERR_PROTOCOL, "too many password rules: %llu",
406
+ (unsigned long long) n_rules);
407
+ /* Pattern & message pairs, discarded */
408
+ for (uint64_t i = 0; i < 2 * n_rules; i++)
409
+ if ((rc = chc__read_server_string(&c->in, rule, sizeof rule, err))) return rc;
410
+ /* Nonce only serves interserver secret */
411
+ if ((rc = chc__read_u64_le(&c->in, &nonce, err))) return rc;
412
+ c->server = s;
386
413
  return CHC_OK;
387
414
  }
388
415
 
389
416
  static int
390
- chc__recv_pong(chc_client *c, chc_err *err)
417
+ chc__recv_pong(chc_client *c, chc_exception **exc, chc_err *err)
391
418
  {
392
419
  bool ioless = c->in.io == NULL;
393
420
  if (ioless) chc__in_checkpoint(&c->in);
@@ -398,12 +425,9 @@ chc__recv_pong(chc_client *c, chc_err *err)
398
425
  chc_exception *e = NULL;
399
426
  rc = chc__read_exception(c, &e, err); /* frees its partial on non-OK */
400
427
  if (rc != CHC_OK) goto maybe_rewind;
401
- chc__err_set(err, CHC_ERR_SERVER, "%s",
402
- e->display_text ? e->display_text : (e->name ? e->name : ""));
403
- err->server_code = e->code;
404
- chc__copy_short(err->server_name, sizeof err->server_name,
405
- e->name, e->name_len);
406
- chc_exception_free(e, c->al);
428
+ if (exc) *exc = e;
429
+ else chc_exception_free(e, c->al);
430
+ chc_err_reset(err);
407
431
  return CHC_ERR_SERVER;
408
432
  }
409
433
  if (kind != CHC_PKT_PONG)
@@ -417,7 +441,8 @@ maybe_rewind:
417
441
 
418
442
  int
419
443
  chc_client_init(chc_client **out, const chc_client_opts *opts,
420
- const chc_alloc *al, chc_io *io, chc_err *err)
444
+ const chc_alloc *al, chc_io *io, chc_exception **exc,
445
+ chc_err *err)
421
446
  {
422
447
  chc_client_opts def_opts = {};
423
448
  if (!opts) opts = &def_opts;
@@ -426,44 +451,30 @@ chc_client_init(chc_client **out, const chc_client_opts *opts,
426
451
  if (!c) return CHC_ERR_OOM;
427
452
  c->al = al;
428
453
  c->io = io;
429
- c->client_version_major = opts->client_version_major;
430
- c->client_version_minor = opts->client_version_minor;
431
- c->client_version_patch = opts->client_version_patch;
432
- c->client_revision = opts->client_revision ? opts->client_revision
433
- : CHC_CLIENT_DEFAULT_REVISION;
434
- c->compression = opts->codec ? opts->compression : CHC_COMP_NONE;
435
- c->codec = opts->codec;
454
+
455
+ chc__client_setup(c, opts);
436
456
 
437
457
  int rc = chc_in_init(&c->in, io, al, opts->read_buffer_bytes, err);
438
458
  if (rc != CHC_OK) { al->free(al->ud, c, sizeof *c); return rc; }
439
459
 
440
460
  rc = chc__client_send_hello(c, opts, err);
441
461
  if (rc != CHC_OK) goto fail;
442
- rc = chc__client_recv_hello(c, err);
462
+ rc = chc__client_recv_hello(c, exc, err);
443
463
  if (rc != CHC_OK) goto fail;
444
464
 
445
- /* Server's effective revision is min(ours, server). After the
446
- * handshake we use this to gate optional fields on subsequent
447
- * packets. */
448
- if (c->server.revision > c->client_revision)
449
- c->server.revision = c->client_revision;
450
-
451
465
  /* Addendum: send empty quota_key. */
452
- if (c->server.revision >= CHC__REV_ADDENDUM) {
453
- rc = chc__write_string(c->io, "", 0, err);
454
- if (rc != CHC_OK) goto fail;
455
- }
466
+ rc = chc__write_string(c->io, "", 0, err);
467
+ if (rc != CHC_OK) goto fail;
456
468
 
457
469
  /* Probe Ping. Server-side late-stage rejections (eg invalid
458
470
  * default_database in 24.x) only surface after the Addendum is read,
459
471
  * not in the Hello reply. Without a probe, the rejection races the
460
472
  * caller's first query: caller's writes may hit ECONNRESET before the
461
473
  * exception packet is read. The Ping forces a round-trip here so the
462
- * exception is delivered at init time instead. Matches clickhouse-cpp's
463
- * SetPingBeforeQuery posture for the connection-establishment case. */
474
+ * exception is delivered at init time instead. */
464
475
  rc = chc_client_send_ping(c, err);
465
476
  if (rc != CHC_OK) goto fail;
466
- rc = chc__recv_pong(c, err);
477
+ rc = chc__recv_pong(c, exc, err);
467
478
  if (rc != CHC_OK) goto fail;
468
479
 
469
480
  *out = c;
@@ -507,20 +518,13 @@ chc_client_send_cancel(chc_client *c, chc_err *err)
507
518
  * both the uncompressed direct path and the compressed buffer-then-emit
508
519
  * path. */
509
520
  static int
510
- chc__client_write_block_body(chc_client *c, chc_io *sink,
511
- const chc_block_builder *bb, chc_err *err)
521
+ chc__client_write_block_body(chc_io *sink, const chc_block_builder *bb,
522
+ chc_err *err)
512
523
  {
513
524
  int rc;
514
- chc_block_opts opts = {
515
- .has_block_info = c->server.revision >= CHC__REV_BLOCK_INFO,
516
- .has_custom_serialization = c->server.revision >= CHC__REV_CUSTOM_SERIALIZATION,
517
- };
518
- if (bb) return chc_block_write(sink, bb, &opts, err);
525
+ if (bb) return chc_block_write(sink, bb, &chc__tcp_block_opts, err);
519
526
 
520
- if (opts.has_block_info) {
521
- rc = chc__write_block_info(sink, err);
522
- if (rc != CHC_OK) return rc;
523
- }
527
+ if ((rc = chc__write_block_info(sink, err))) return rc;
524
528
  if ((rc = chc__write_varuint(sink, 0, err))) return rc; /* n_cols */
525
529
  if ((rc = chc__write_varuint(sink, 0, err))) return rc; /* n_rows */
526
530
  return CHC_OK;
@@ -535,12 +539,10 @@ chc__client_write_data(chc_client *c, const chc_block_builder *bb, chc_err *err)
535
539
  int rc;
536
540
  if ((rc = chc__write_varuint(c->io, CHC__CLIENT_DATA, err))) return rc;
537
541
  /* Temporary table name (always empty from us). */
538
- if (c->server.revision >= CHC__REV_TEMPORARY_TABLES) {
539
- if ((rc = chc__write_string(c->io, "", 0, err))) return rc;
540
- }
542
+ if ((rc = chc__write_string(c->io, "", 0, err))) return rc;
541
543
 
542
544
  if (c->compression == CHC_COMP_NONE) {
543
- return chc__client_write_block_body(c, c->io, bb, err);
545
+ return chc__client_write_block_body(c->io, bb, err);
544
546
  }
545
547
 
546
548
  if (!c->codec)
@@ -550,7 +552,7 @@ chc__client_write_data(chc_client *c, const chc_block_builder *bb, chc_err *err)
550
552
  chc__mem_sink ms;
551
553
  chc_io sink_io;
552
554
  chc__mem_sink_init(&ms, &sink_io, c->al);
553
- rc = chc__client_write_block_body(c, &sink_io, bb, err);
555
+ rc = chc__client_write_block_body(&sink_io, bb, err);
554
556
  if (rc != CHC_OK) { chc__mem_sink_free(&ms); return rc; }
555
557
  rc = chc__comp_emit_chunks(c->io, c->codec, c->compression,
556
558
  ms.buf, ms.len, c->al, err);
@@ -575,71 +577,48 @@ chc_client_send_query_ex(chc_client *c, const char *sql, size_t sql_len,
575
577
  if ((rc = chc__write_varuint(c->io, CHC__CLIENT_QUERY, err))) return rc;
576
578
  if ((rc = chc__write_string (c->io, opts->query_id, opts->query_id_len, err))) return rc;
577
579
 
578
- /* ClientInfo. clickhouse-cpp sends a fully-populated struct; we send
579
- * the minimum the server tolerates (initial fields blank, iface=TCP). */
580
- if (c->server.revision >= CHC__REV_CLIENT_INFO) {
581
- uint8_t query_kind = 1; /* INITIAL_QUERY */
582
- if ((rc = chc__write_bytes (c->io, &query_kind, 1, err))) return rc;
583
- if ((rc = chc__write_string(c->io, "", 0, err))) return rc; /* initial_user */
584
- if ((rc = chc__write_string(c->io, "", 0, err))) return rc; /* initial_query_id */
585
- if ((rc = chc__write_string(c->io, "[::ffff:127.0.0.1]:0", 20, err))) return rc;
586
- if (c->server.revision >= CHC__REV_INITIAL_QUERY_START) {
587
- uint8_t z8[8] = {};
588
- if ((rc = chc__write_bytes(c->io, z8, 8, err))) return rc; /* int64 */
589
- }
590
- uint8_t iface_type = 1; /* TCP */
591
- if ((rc = chc__write_bytes (c->io, &iface_type, 1, err))) return rc;
592
- if ((rc = chc__write_string(c->io, "", 0, err))) return rc; /* os_user */
593
- if ((rc = chc__write_string(c->io, "", 0, err))) return rc; /* client_hostname */
594
- if ((rc = chc__write_string(c->io, "clickhouse-c client", 19, err))) return rc;
595
- if ((rc = chc__write_varuint(c->io, c->client_version_major, err))) return rc;
596
- if ((rc = chc__write_varuint(c->io, c->client_version_minor, err))) return rc;
597
- if ((rc = chc__write_varuint(c->io, c->client_revision, err))) return rc;
598
-
599
- if (c->server.revision >= CHC__REV_QUOTA_KEY_IN_CLIENT)
600
- if ((rc = chc__write_string(c->io, "", 0, err))) return rc;
601
- if (c->server.revision >= CHC__REV_DISTRIBUTED_DEPTH)
602
- if ((rc = chc__write_varuint(c->io, 0, err))) return rc;
603
- if (c->server.revision >= CHC__REV_VERSION_PATCH)
604
- if ((rc = chc__write_varuint(c->io, c->client_version_patch, err))) return rc;
605
- if (c->server.revision >= CHC__REV_OPENTELEMETRY) {
606
- uint8_t no_otel = 0;
607
- if ((rc = chc__write_bytes(c->io, &no_otel, 1, err))) return rc;
608
- }
609
- if (c->server.revision >= CHC__REV_PARALLEL_REPLICAS) {
610
- if ((rc = chc__write_varuint(c->io, 0, err))) return rc;
611
- if ((rc = chc__write_varuint(c->io, 0, err))) return rc;
612
- if ((rc = chc__write_varuint(c->io, 0, err))) return rc;
613
- }
614
- }
580
+ /* ClientInfo. Body layout per the Native Protocol spec (Query packet).
581
+ * We send the minimum the server tolerates (initial fields blank,
582
+ * iface=TCP); only client_name identifies us. */
583
+ uint8_t query_kind = 1; /* INITIAL_QUERY */
584
+ uint8_t z8[8] = {};
585
+ uint8_t iface_type = 1; /* TCP */
586
+ uint8_t no_otel = 0;
587
+ if ((rc = chc__write_bytes (c->io, &query_kind, 1, err))) return rc;
588
+ if ((rc = chc__write_string(c->io, "", 0, err))) return rc; /* initial_user */
589
+ if ((rc = chc__write_string(c->io, "", 0, err))) return rc; /* initial_query_id */
590
+ if ((rc = chc__write_string(c->io, "[::ffff:127.0.0.1]:0", 20, err))) return rc;
591
+ if ((rc = chc__write_bytes (c->io, z8, 8, err))) return rc; /* initial_query_start_time */
592
+ if ((rc = chc__write_bytes (c->io, &iface_type, 1, err))) return rc;
593
+ if ((rc = chc__write_string(c->io, "", 0, err))) return rc; /* os_user */
594
+ if ((rc = chc__write_string(c->io, "", 0, err))) return rc; /* client_hostname */
595
+ if ((rc = chc__write_string(c->io, "clickhouse-c client", 19, err))) return rc;
596
+ if ((rc = chc__write_varuint(c->io, c->client_version_major, err))) return rc;
597
+ if ((rc = chc__write_varuint(c->io, c->client_version_minor, err))) return rc;
598
+ if ((rc = chc__write_varuint(c->io, CHC_CLIENT_REVISION, err))) return rc;
599
+ if ((rc = chc__write_string(c->io, "", 0, err))) return rc; /* quota_key */
600
+ if ((rc = chc__write_varuint(c->io, 0, err))) return rc; /* distributed_depth */
601
+ if ((rc = chc__write_varuint(c->io, c->client_version_patch, err))) return rc;
602
+ if ((rc = chc__write_bytes (c->io, &no_otel, 1, err))) return rc;
603
+ /* Parallel replicas: collaborate_with_initiator, count, number */
604
+ if ((rc = chc__write_varuint(c->io, 0, err))) return rc;
605
+ if ((rc = chc__write_varuint(c->io, 0, err))) return rc;
606
+ if ((rc = chc__write_varuint(c->io, 0, err))) return rc;
615
607
 
616
608
  /* Per-query settings: name + varuint(flags) + value, repeated, then
617
- * empty-string terminator. Pre-54429 binary serialization isn't
618
- * implemented; the empty-list path still works because the terminator
619
- * is shape-compatible. */
620
- if (c->server.revision >= CHC__REV_SETTINGS_AS_STRINGS) {
621
- for (size_t i = 0; i < opts->n_settings; i++) {
622
- const chc_query_setting *s = &opts->settings[i];
623
- size_t nlen = s->name ? strlen(s->name) : 0;
624
- size_t vlen = s->value ? strlen(s->value) : 0;
625
- uint64_t flags = (s->important ? 1u : 0u) | (s->custom ? 2u : 0u);
626
- if ((rc = chc__write_string (c->io, s->name, nlen, err))) return rc;
627
- if ((rc = chc__write_varuint(c->io, flags, err))) return rc;
628
- if ((rc = chc__write_string (c->io, s->value, vlen, err))) return rc;
629
- }
630
- if ((rc = chc__write_string(c->io, "", 0, err))) return rc;
631
- } else {
632
- if (opts->n_settings)
633
- return chc__err_set(err, CHC_ERR_PROTOCOL,
634
- "server revision %llu < %u: query settings unsupported",
635
- (unsigned long long) c->server.revision,
636
- CHC__REV_SETTINGS_AS_STRINGS);
637
- if ((rc = chc__write_string(c->io, "", 0, err))) return rc;
609
+ * empty-string terminator. */
610
+ for (size_t i = 0; i < opts->n_settings; i++) {
611
+ const chc_query_setting *s = &opts->settings[i];
612
+ size_t nlen = s->name ? strlen(s->name) : 0;
613
+ size_t vlen = s->value ? strlen(s->value) : 0;
614
+ uint64_t flags = (s->important ? 1u : 0u) | (s->custom ? 2u : 0u);
615
+ if ((rc = chc__write_string (c->io, s->name, nlen, err))) return rc;
616
+ if ((rc = chc__write_varuint(c->io, flags, err))) return rc;
617
+ if ((rc = chc__write_string (c->io, s->value, vlen, err))) return rc;
638
618
  }
619
+ if ((rc = chc__write_string(c->io, "", 0, err))) return rc;
639
620
 
640
- if (c->server.revision >= CHC__REV_INTERSERVER_SECRET) {
641
- if ((rc = chc__write_string(c->io, "", 0, err))) return rc;
642
- }
621
+ if ((rc = chc__write_string(c->io, "", 0, err))) return rc; /* interserver secret */
643
622
 
644
623
  /* Stages::Complete = 2. */
645
624
  if ((rc = chc__write_varuint(c->io, 2, err))) return rc;
@@ -650,21 +629,15 @@ chc_client_send_query_ex(chc_client *c, const char *sql, size_t sql_len,
650
629
  if ((rc = chc__write_string(c->io, sql, sql_len, err))) return rc;
651
630
 
652
631
  /* Parameters: same shape as settings; flags always CUSTOM (bit 1). */
653
- if (c->server.revision >= CHC__REV_PARAMETERS) {
654
- for (size_t i = 0; i < opts->n_params; i++) {
655
- const chc_query_param *p = &opts->params[i];
656
- size_t nlen = p->name ? strlen(p->name) : 0;
657
- size_t vlen = p->value ? strlen(p->value) : 0;
658
- if ((rc = chc__write_string (c->io, p->name, nlen, err))) return rc;
659
- if ((rc = chc__write_varuint(c->io, 2u, err))) return rc;
660
- if ((rc = chc__write_string (c->io, p->value, vlen, err))) return rc;
661
- }
662
- if ((rc = chc__write_string(c->io, "", 0, err))) return rc;
663
- } else if (opts->n_params) {
664
- return chc__err_set(err, CHC_ERR_PROTOCOL,
665
- "server revision %llu < %u: query parameters unsupported",
666
- (unsigned long long) c->server.revision, CHC__REV_PARAMETERS);
632
+ for (size_t i = 0; i < opts->n_params; i++) {
633
+ const chc_query_param *p = &opts->params[i];
634
+ size_t nlen = p->name ? strlen(p->name) : 0;
635
+ size_t vlen = p->value ? strlen(p->value) : 0;
636
+ if ((rc = chc__write_string (c->io, p->name, nlen, err))) return rc;
637
+ if ((rc = chc__write_varuint(c->io, 2u, err))) return rc;
638
+ if ((rc = chc__write_string (c->io, p->value, vlen, err))) return rc;
667
639
  }
640
+ if ((rc = chc__write_string(c->io, "", 0, err))) return rc;
668
641
 
669
642
  /* Finalize: send an empty Data block as the query-text terminator. */
670
643
  return chc__client_write_data(c, NULL, err);
@@ -698,14 +671,10 @@ chc_packet_clear(chc_client *c, chc_packet *p)
698
671
  }
699
672
  }
700
673
 
701
- /* Read the leading string a block-bearing packet carries before block body.
702
- * DATA/TOTALS/EXTREMES gate a temp-table name on REV_TEMPORARY_TABLES (gated=1);
703
- * LOG/PROFILE_EVENTS always prepend a tag (gated=0). */
674
+ /* Skip temp-table name or log tag block-bearing packets carry before block */
704
675
  static int
705
- chc__recv_skip_lead_string(chc_client *c, int gated, chc_err *err)
676
+ chc__recv_skip_lead_string(chc_client *c, chc_err *err)
706
677
  {
707
- if (gated && c->server.revision < CHC__REV_TEMPORARY_TABLES)
708
- return CHC_OK;
709
678
  char *s; size_t slen;
710
679
  int rc = chc__read_string(&c->in, &s, &slen, err);
711
680
  if (rc != CHC_OK) return rc;
@@ -834,10 +803,7 @@ static int
834
803
  chc__recv_packet_resumable(chc_client *c, chc_packet *out, chc_err *err)
835
804
  {
836
805
  bool ioless = c->in.io == NULL;
837
- chc_block_opts opts = {
838
- .has_block_info = c->server.revision >= CHC__REV_BLOCK_INFO,
839
- .has_custom_serialization = c->server.revision >= CHC__REV_CUSTOM_SERIALIZATION,
840
- };
806
+ const chc_block_opts *opts = &chc__tcp_block_opts;
841
807
  int rc;
842
808
  int is_log = 0; /* LOG/PROFILE_EVENTS: never compressed */
843
809
 
@@ -859,17 +825,17 @@ chc__recv_packet_resumable(chc_client *c, chc_packet *out, chc_err *err)
859
825
 
860
826
  case CHC_PKT_PROGRESS:
861
827
  out->kind = CHC_PKT_PROGRESS;
862
- if ((rc = chc__read_varuint(&c->in, &out->progress.rows, err)) ||
863
- (rc = chc__read_varuint(&c->in, &out->progress.bytes, err)))
828
+ if ((rc = chc__read_varuint(&c->in, &out->progress.rows, err)) ||
829
+ (rc = chc__read_varuint(&c->in, &out->progress.bytes, err)) ||
830
+ (rc = chc__read_varuint(&c->in, &out->progress.total_rows, err)))
864
831
  goto maybe_rewind;
865
- if (c->server.revision >= CHC__REV_TOTAL_ROWS_IN_PROGRESS)
866
- if ((rc = chc__read_varuint(&c->in, &out->progress.total_rows, err)))
832
+ if (c->server.revision >= CHC__REV_TOTAL_BYTES_IN_PROGRESS)
833
+ if ((rc = chc__read_varuint(&c->in, &out->progress.total_bytes, err)))
867
834
  goto maybe_rewind;
868
- if (c->server.revision >= CHC__REV_CLIENT_WRITE_INFO) {
869
- if ((rc = chc__read_varuint(&c->in, &out->progress.written_rows, err)) ||
870
- (rc = chc__read_varuint(&c->in, &out->progress.written_bytes, err)))
871
- goto maybe_rewind;
872
- }
835
+ if ((rc = chc__read_varuint(&c->in, &out->progress.written_rows, err)) ||
836
+ (rc = chc__read_varuint(&c->in, &out->progress.written_bytes, err)) ||
837
+ (rc = chc__read_varuint(&c->in, &out->progress.elapsed_ns, err)))
838
+ goto maybe_rewind;
873
839
  goto control_done;
874
840
 
875
841
  case CHC_PKT_PONG:
@@ -902,6 +868,15 @@ chc__recv_packet_resumable(chc_client *c, chc_packet *out, chc_err *err)
902
868
  goto control_done;
903
869
  }
904
870
 
871
+ /* Empty timezone means server default, as clickhouse-client treats it */
872
+ case CHC_PKT_TIMEZONE_UPDATE: {
873
+ out->kind = CHC_PKT_TIMEZONE_UPDATE;
874
+ char tz[sizeof c->server.timezone];
875
+ if ((rc = chc__read_server_string(&c->in, tz, sizeof tz, err))) goto maybe_rewind;
876
+ memcpy(c->server.timezone, tz, sizeof tz);
877
+ goto control_done;
878
+ }
879
+
905
880
  /* ---- block-bearing: commit kind + leading string, then resume ---- */
906
881
  case CHC_PKT_DATA: out->kind = c->recv_kind = CHC_PKT_DATA; break;
907
882
  case CHC_PKT_TOTALS: out->kind = c->recv_kind = CHC_PKT_TOTALS; break;
@@ -916,7 +891,7 @@ chc__recv_packet_resumable(chc_client *c, chc_packet *out, chc_err *err)
916
891
  (unsigned long long) kind);
917
892
  }
918
893
 
919
- rc = chc__recv_skip_lead_string(c, !is_log, err);
894
+ rc = chc__recv_skip_lead_string(c, err);
920
895
  if (rc != CHC_OK) goto maybe_rewind;
921
896
 
922
897
  /* Kind + leading string committed; resume owns the cursor from here.
@@ -934,7 +909,7 @@ chc__recv_packet_resumable(chc_client *c, chc_packet *out, chc_err *err)
934
909
  /* LOG/PROFILE_EVENTS blocks are never compressed; route through the
935
910
  * uncompressed resume path regardless of c->compression. */
936
911
  if (c->compression == CHC_COMP_NONE || is_log) {
937
- rc = chc__block_resume_in(&c->in, c->al, &opts,
912
+ rc = chc__block_resume_in(&c->in, c->al, opts,
938
913
  &c->recv_partial, &c->recv_next_col, err);
939
914
  if (rc == CHC_WOULD_BLOCK) return rc; /* resume owns rewind; partial retained */
940
915
  c->recv_in_block = 0;
@@ -954,7 +929,7 @@ chc__recv_packet_resumable(chc_client *c, chc_packet *out, chc_err *err)
954
929
  * decompressor + ioless decompressed buffer. io-backed:
955
930
  * baseline rebuild-per-call (chc__recv_block_compressed). */
956
931
  if (ioless) {
957
- rc = chc__recv_block_compressed_resume(c, &opts, err);
932
+ rc = chc__recv_block_compressed_resume(c, opts, err);
958
933
  if (rc == CHC_WOULD_BLOCK) return rc; /* partial block + decomp state retained */
959
934
  c->recv_in_block = 0;
960
935
  if (rc != CHC_OK) return rc; /* resume tore down its state + recv_partial */
@@ -965,7 +940,7 @@ chc__recv_packet_resumable(chc_client *c, chc_packet *out, chc_err *err)
965
940
  return CHC_OK;
966
941
  }
967
942
 
968
- rc = chc__recv_block_compressed(c, &opts, &out->block, err);
943
+ rc = chc__recv_block_compressed(c, opts, &out->block, err);
969
944
  if (rc == CHC_WOULD_BLOCK) return rc;
970
945
  c->recv_in_block = 0;
971
946
  if (rc != CHC_OK) return rc;